【Linux驱动篇】LCD背光驱动分析

一、概述

  Linux驱动中,可以通过pwm_bl + backlight的方式,实现LCD的背光调节、以及背光电源控制,对应的源码路径为:kernel/drivers/video/backlight/pwm_bl.c和kernel/drivers/video/backlight/backlight.c
  需要注意的是,背光由PWM模块调节,背光电源采用regulator模块控制的方式,所以接下来会对两者分别进行简单的阐述。

事实上,背光电源也可以不用regulator模块控制,根据pwm_bl.c的probe函数中,pb->enable_gpio =devm_gpiod_get_optional(&pdev->dev,“enable”,GPIOD_ASIS);可知,在dts中定义enble-gpios=<&gpio2 RK_PA4 GPIO_ACTIVE_HIGH>也可以达到背光电源控制的目的

二、设备树添加

// 配置LCD背光电源,加入到regulator电源管理
/ {
	lcd_bl_en: lcd_bl-en {
                compatible = "regulator-fixed";
                regulator-name = "lcd_bl_en"; //此名字会被backlight里的power-supply用到
                gpio = <&gpio2 RK_PA4 GPIO_ACTIVE_HIGH>;
                regulator-boot-on;
                enable-active-high;
        };

};

backlight: backlight {
		compatible = "pwm-backlight";
		status = "okay";
    	pwms = <&pwm8 0 25000 0>;
    	power-supply = <&lcd_bl_en>;  //定义背光电源使用lcd_bl_en(regulator-name)),在pwm_bl.c中会用到
		brightness-levels = <
			255 254 253 252 251 250 249 248
			...
                   ...
			7 6 5 4 3 2 1 0>;
		default-brightness-level = <50>;
	};

&dsi {
	status = "okay";

	 dsi_panel: panel@0 {
		compatible = "simple-panel-dsi";
		status = "okay";
		reg = <0>;
		backlight = <&backlight>;
		reset-gpio = <&gpio0 RK_PA5 GPIO_ACTIVE_LOW>;
		prepare-delay-ms = <2>;
		reset-delay-ms = <10>;
		init-delay-ms = <120>;
		enable-delay-ms = <120>;
		disable-delay-ms = <50>;
		unprepare-delay-ms = <20>;
          ... 
          ...
  }

首先需要对几个属性进行简单描述:

  1. regulator-boot-on:在boot开机阶段,就开始上电,即对定义的gpio进行拉高或者拉低操作。
  2. regulator-always-on:除了跟regulator-boot-on有一样的功能外,还代表了禁止对此电源掉电,也就是无法对该gpio进行后续的控制。
  3. regulator-name:若其他模块用到这个regulator,则使用该名字
  4. power-supply:根据第3点所说,这里定义为lcd_bl_en,详情见pwm_bl.c中pwm_backlight_probe->pb->power_supply = devm_regulator_get(&pdev->dev, “power”);会查找是否有定义power-supply。(若未定义power-supply。系统也会自动分配一个dummy regulator对象,可跟踪源码kernel/drivers/regulator/core.c:_regulator_get函数)

驱动在加载时,会解析lcd_bl_en,加入到regulator电源管理,详情见kernel/drivers/regulator/fixed.c,调用顺序为:
regulator_fixed_voltage_init
–> reg_fixed_voltage_probe
----> devm_regulator_register
------> regulator_register
  在regulator_register函数中,会调用到set_machine_constraints函数,部分代码如下:

static int set_machine_constraints(struct regulator_dev *rdev,
	const struct regulation_constraints *constraints)
{
	int ret = 0;
	const struct regulator_ops *ops = rdev->desc->ops;
 
    ...
    
	/* If the constraints say the regulator should be on at this point
	 * and we have control then make sure it is enabled.
	 */
	if (rdev->constraints->always_on || rdev->constraints->boot_on) {
		ret = _regulator_do_enable(rdev);
		if (ret < 0 && ret != -EINVAL) {
			rdev_err(rdev, "failed to enable\n");
			return ret;
		}
	}

	...

	print_constraints(rdev);
	return 0;
}

  可以看到,当dts配置了regulator-always-on或者regulator-boot-on属性时,在regulator注册时就会自动执行_regulator_do_enable将GPIO进行拉高或拉低

三、源码分析

pwm_bl.c

背光控制的实际操作函数,背光亮度和背光电源都在本文件中实现。

  1. 先看驱动适配函数pwm_backlight_probe
static int pwm_backlight_parse_dt(struct device *dev,
				  struct platform_pwm_backlight_data *data)
{
	struct device_node *node = dev->of_node;
	struct property *prop;
	int length;
	u32 value;
	int ret;

	if (!node)
		return -ENODEV;

	memset(data, 0, sizeof(*data));

	/* determine the number of brightness levels */
	prop = of_find_property(node, "brightness-levels", &length);
	if (!prop)
		return -EINVAL;

	data->max_brightness = length / sizeof(u32);

	/* read brightness levels from DT property */
	if (data->max_brightness > 0) {
		size_t size = sizeof(*data->levels) * data->max_brightness;

		data->levels = devm_kzalloc(dev, size, GFP_KERNEL);
		if (!data->levels)
			return -ENOMEM;

		ret = of_property_read_u32_array(node, "brightness-levels",
						 data->levels,
						 data->max_brightness);
		if (ret < 0)
			return ret;

		ret = of_property_read_u32(node, "default-brightness-level",
					   &value);
		if (ret < 0)
			return ret;

		data->dft_brightness = value;
		data->max_brightness--;
	}

	data->enable_gpio = -EINVAL;
	return 0;
}

static int pwm_backlight_probe(struct platform_device *pdev)
{
	struct platform_pwm_backlight_data *data = dev_get_platdata(&pdev->dev);
	struct platform_pwm_backlight_data defdata;
	struct backlight_properties props;
	struct backlight_device *bl;
	struct device_node *node = pdev->dev.of_node;
	struct pwm_bl_data *pb;
	struct pwm_args pargs;
	int ret;

	if (!data) {
		ret = pwm_backlight_parse_dt(&pdev->dev, &defdata);
		if (ret < 0) {
			dev_err(&pdev->dev, "failed to find platform data\n");
			return ret;
		}

		data = &defdata;
	}

	if (data->init) {
		ret = data->init(&pdev->dev);
		if (ret < 0)
			return ret;
	}

	pb = devm_kzalloc(&pdev->dev, sizeof(*pb), GFP_KERNEL);
	if (!pb) {
		ret = -ENOMEM;
		goto err_alloc;
	}

	if (data->levels) {
		unsigned int i;

		for (i = 0; i <= data->max_brightness; i++)
			if (data->levels[i] > pb->scale)
				pb->scale = data->levels[i];

		pb->levels = data->levels;
	} else
		pb->scale = data->max_brightness;

	pb->notify = data->notify;
	pb->notify_after = data->notify_after;
	pb->check_fb = data->check_fb;
	pb->exit = data->exit;
	pb->dev = &pdev->dev;
	pb->enabled = false;

	pb->enable_gpio = devm_gpiod_get_optional(&pdev->dev, "enable",
						  GPIOD_ASIS);
	if (IS_ERR(pb->enable_gpio)) {
		ret = PTR_ERR(pb->enable_gpio);
		goto err_alloc;
	}

	/*
	 * Compatibility fallback for drivers still using the integer GPIO
	 * platform data. Must go away soon.
	 */
	if (!pb->enable_gpio && gpio_is_valid(data->enable_gpio)) {
		ret = devm_gpio_request_one(&pdev->dev, data->enable_gpio,
					    GPIOF_OUT_INIT_HIGH, "enable");
		if (ret < 0) {
			dev_err(&pdev->dev, "failed to request GPIO#%d: %d\n",
				data->enable_gpio, ret);
			goto err_alloc;
		}

		pb->enable_gpio = gpio_to_desc(data->enable_gpio);
	}

	/*
	 * If the GPIO is not known to be already configured as output, that
	 * is, if gpiod_get_direction returns either GPIOF_DIR_IN or -EINVAL,
	 * change the direction to output and set the GPIO as active.
	 * Do not force the GPIO to active when it was already output as it
	 * could cause backlight flickering or we would enable the backlight too
	 * early. Leave the decision of the initial backlight state for later.
	 */
	if (pb->enable_gpio &&
	    gpiod_get_direction(pb->enable_gpio) != GPIOF_DIR_OUT)
		gpiod_direction_output(pb->enable_gpio, 1);

	pb->power_supply = devm_regulator_get(&pdev->dev, "power");
	if (IS_ERR(pb->power_supply)) {
		ret = PTR_ERR(pb->power_supply);
		goto err_alloc;
	}

	pb->pwm = devm_pwm_get(&pdev->dev, NULL);
	if (IS_ERR(pb->pwm) && PTR_ERR(pb->pwm) != -EPROBE_DEFER && !node) {
		dev_err(&pdev->dev, "unable to request PWM, trying legacy API\n");
		pb->legacy = true;
		pb->pwm = pwm_request(data->pwm_id, "pwm-backlight");
	}

	if (IS_ERR(pb->pwm)) {
		ret = PTR_ERR(pb->pwm);
		if (ret != -EPROBE_DEFER)
			dev_err(&pdev->dev, "unable to request PWM\n");
		goto err_alloc;
	}

	dev_dbg(&pdev->dev, "got pwm for backlight\n");

	/*
	 * FIXME: pwm_apply_args() should be removed when switching to
	 * the atomic PWM API.
	 */
	pwm_apply_args(pb->pwm);

	/*
	 * The DT case will set the pwm_period_ns field to 0 and store the
	 * period, parsed from the DT, in the PWM device. For the non-DT case,
	 * set the period from platform data if it has not already been set
	 * via the PWM lookup table.
	 */
	pwm_get_args(pb->pwm, &pargs);
	pb->period = pargs.period;
	if (!pb->period && (data->pwm_period_ns > 0))
		pb->period = data->pwm_period_ns;

	pb->lth_brightness = data->lth_brightness * (pb->period / pb->scale);

	memset(&props, 0, sizeof(struct backlight_properties));
	props.type = BACKLIGHT_RAW;
	props.max_brightness = data->max_brightness;
	bl = backlight_device_register(dev_name(&pdev->dev), &pdev->dev, pb,
				       &pwm_backlight_ops, &props);
	if (IS_ERR(bl)) {
		dev_err(&pdev->dev, "failed to register backlight\n");
		ret = PTR_ERR(bl);
		if (pb->legacy)
			pwm_free(pb->pwm);
		goto err_alloc;
	}

	if (data->dft_brightness > data->max_brightness) {
		dev_warn(&pdev->dev,
			 "invalid default brightness level: %u, using %u\n",
			 data->dft_brightness, data->max_brightness);
		data->dft_brightness = data->max_brightness;
	}

	bl->props.brightness = data->dft_brightness;
	bl->props.power = pwm_backlight_initial_power_state(pb);
	backlight_update_status(bl);

	platform_set_drvdata(pdev, bl);
	return 0;

err_alloc:
	if (data->exit)
		data->exit(&pdev->dev);
	return ret;
}

  probe里有几个重要的函数,这里按照调用顺序,依次进行阐述:

  • pwm_backlight_parse_dt:拿到dts里定义的brightness-levels和default-brightness-level,前者用来应用层控制不同的背光亮度,后者在初始化时会使能一个默认的背光亮度,即probe函数最后调用的backlight_update_status
  • devm_gpiod_get_optional:从dts里获取到命名为"enble-gpio"或者"enable-gpios"的引脚,此函数是对devm_gpiod_get_index_optional(dev, con_id, 0, flags);的一个封装,引脚号index参数固定为0,只取第一个GPIO。调用顺序为:
    devm_gpiod_get_optional
    –> devm_gpiod_get_index_optional
    ----> devm_gpiod_get_index
    ------> gpiod_get_index
    --------> of_find_gpio
    其核心函数为of_find_gpio,源码路径为kernel/drivers/gpio/gpiolib-of.c
/* gpio suffixes used for ACPI and device tree lookup */
static const char * const gpio_suffixes[] = { "gpios", "gpio" };

struct gpio_desc *of_find_gpio(struct device *dev, const char *con_id,
			       unsigned int idx,
			       enum gpio_lookup_flags *flags)
{
	char prop_name[32]; /* 32 is max size of property name */
	enum of_gpio_flags of_flags;
	struct gpio_desc *desc;
	unsigned int i;

	for (i = 0; i < ARRAY_SIZE(gpio_suffixes); i++) {
		if (con_id)
			snprintf(prop_name, sizeof(prop_name), "%s-%s", con_id,
				 gpio_suffixes[i]);
		else
			snprintf(prop_name, sizeof(prop_name), "%s",
				 gpio_suffixes[i]);

		desc = of_get_named_gpiod_flags(dev->of_node, prop_name, idx,
						&of_flags);
		if (!IS_ERR(desc) || (PTR_ERR(desc) != -ENOENT))
			break;
	}

	if (IS_ERR(desc))
		return desc;

	if (of_flags & OF_GPIO_ACTIVE_LOW)
		*flags |= GPIO_ACTIVE_LOW;

	if (of_flags & OF_GPIO_SINGLE_ENDED) {
		if (of_flags & OF_GPIO_OPEN_DRAIN)
			*flags |= GPIO_OPEN_DRAIN;
		else
			*flags |= GPIO_OPEN_SOURCE;
	}

	if (of_flags & OF_GPIO_SLEEP_MAY_LOSE_VALUE)
		*flags |= GPIO_SLEEP_MAY_LOSE_VALUE;

	return desc;
    }

我这里用到的是regulator控制背光电源,所以dts里没有定义enble-gpio

  • devm_regulator_get:获取regulator,参数为"power",即dts里定义的"power-supply=<&lcd_bl_en>"。调用顺序为:
    devm_regulator_get
    –> _devm_regulator_get
    ----> _regulator_get
    ------> regulator_dev_lookup
    --------> of_get_regulator
    其核心获取函数为of_get_regulator,源码路径为kernel/drivers/regulator/core.c
static struct device_node *of_get_regulator(struct device *dev, const char *supply)
{
	struct device_node *regnode = NULL;
	char prop_name[32]; /* 32 is max size of property name */

	dev_dbg(dev, "Looking up %s-supply from device tree\n", supply);

	snprintf(prop_name, 32, "%s-supply", supply);
	regnode = of_parse_phandle(dev->of_node, prop_name, 0);

	if (!regnode) {
		dev_dbg(dev, "Looking up %s property in node %pOF failed\n",
				prop_name, dev->of_node);
		return NULL;
	}
	return regnode;
}
  • backlight_device_register:向backlight驱动注册,也是最核心的一个函数,接下来重点介绍一下。
  1. 再来看一下背光控制的实际操作函数pwm_backlight_update_status,此函数为pwm_backlight_ops的update_status指针,在调用backlight_device_register时,将pwm_backlight_ops作为参数传入,赋值给backlight,其后backlight_update_status函数实际就是直接调用到了pwm_backlight_update_status函数控制背光
static void pwm_backlight_power_on(struct pwm_bl_data *pb, int brightness)
{
	int err;

	if (pb->enabled)
		return;

	err = regulator_enable(pb->power_supply);
	if (err < 0)
		dev_err(pb->dev, "failed to enable power supply\n");

	if (pb->enable_gpio)
		gpiod_set_value_cansleep(pb->enable_gpio, 1);

	pwm_enable(pb->pwm);
	pb->enabled = true;
}

static void pwm_backlight_power_off(struct pwm_bl_data *pb)
{
	if (!pb->enabled)
		return;

	pwm_config(pb->pwm, 0, pb->period);
	pwm_disable(pb->pwm);

	if (pb->enable_gpio)
		gpiod_set_value_cansleep(pb->enable_gpio, 0);

	regulator_disable(pb->power_supply);
	pb->enabled = false;
}

static int compute_duty_cycle(struct pwm_bl_data *pb, int brightness)
{
	unsigned int lth = pb->lth_brightness;
	u64 duty_cycle;

	if (pb->levels)
		duty_cycle = pb->levels[brightness];
	else
		duty_cycle = brightness;

	duty_cycle *= pb->period - lth;
	do_div(duty_cycle, pb->scale);

	return duty_cycle + lth;
}

static int pwm_backlight_update_status(struct backlight_device *bl)
{
	struct pwm_bl_data *pb = bl_get_data(bl);
	int brightness = bl->props.brightness;
	int duty_cycle;

	if (bl->props.power != FB_BLANK_UNBLANK ||
	    bl->props.fb_blank != FB_BLANK_UNBLANK ||
	    bl->props.state & BL_CORE_FBBLANK)
		brightness = 0;

	if (pb->notify)
		brightness = pb->notify(pb->dev, brightness);

	if (brightness > 0) {
		duty_cycle = compute_duty_cycle(pb, brightness);
		pwm_config(pb->pwm, duty_cycle, pb->period);
		pwm_backlight_power_on(pb, brightness);
	} else
		pwm_backlight_power_off(pb);

	if (pb->notify_after)
		pb->notify_after(pb->dev, brightness);

	return 0;
}

static const struct backlight_ops pwm_backlight_ops = {
	.update_status	= pwm_backlight_update_status,
	.check_fb	= pwm_backlight_check_fb,
};

  pwm_backlight_update_status中通过pwm_config函数调节占空比,从而调节LCD背光亮度,而pwm_backlight_power_on和pwm_backlight_power_off则是对LCD背光电源进行控制了,这里可以看到当设置的brightness大于0或者小于等于0时,会自动控制背光电源的开启和关闭,所以并不需要应用层人为的控制了。
  在pwm_backlight_power_on函数中,调用了regulator_enable,此函数就是对电源进行控制,参数为pb->power_supply,即前面通过devm_regulator_get获取到的dts中配置的power-supply。regulator_enable函数调用流程为:
regulator_enable
–> _regulator_enable
----> _regulator_do_enable
  其核心函数为_regulator_do_enable,调用regulator_ena_gpio_ctrl函数对定义的电源引脚进行使能控制。同理在pwm_backlight_power_off函数中调用的_regulator_disable则最终调用到_regulator_do_disable对禁用控制。
  其后若定义了pb->enable_gpio引脚,也会对此引脚进行使能或禁用,所以这个引脚也可以用来作为背光电源控制。

backlight.c

LCD背光驱动,导出给应用层控制背光的brightness、bl_power、actual_brightness等节点,并注册device设备

  • 初始化,其中需要重点注意一下backlight_class->dev_groups = bl_device_groups;
static int __init backlight_class_init(void)
{
	backlight_class = class_create(THIS_MODULE, "backlight");
	if (IS_ERR(backlight_class)) {
		pr_warn("Unable to create backlight class; errno = %ld\n",
			PTR_ERR(backlight_class));
		return PTR_ERR(backlight_class);
	}

	backlight_class->dev_groups = bl_device_groups;
	backlight_class->pm = &backlight_class_dev_pm_ops;
	INIT_LIST_HEAD(&backlight_dev_list);
	mutex_init(&backlight_dev_list_mutex);
	BLOCKING_INIT_NOTIFIER_HEAD(&backlight_notifier);

	return 0;
}

/*
 * if this is compiled into the kernel, we need to ensure that the
 * class is registered before users of the class try to register lcd's
 */
postcore_initcall(backlight_class_init);
  • 创建文件节点,以brightness节点为例:
static ssize_t brightness_show(struct device *dev,
		struct device_attribute *attr, char *buf)
{
	struct backlight_device *bd = to_backlight_device(dev);

	return sprintf(buf, "%d\n", bd->props.brightness);
}

int backlight_device_set_brightness(struct backlight_device *bd,
				    unsigned long brightness)
{
	int rc = -ENXIO;

	mutex_lock(&bd->ops_lock);
	if (bd->ops) {
		if (brightness > bd->props.max_brightness)
			rc = -EINVAL;
		else {
			pr_debug("set brightness to %lu\n", brightness);
			bd->props.brightness = brightness;
			rc = backlight_update_status(bd);
		}
	}
	mutex_unlock(&bd->ops_lock);

	backlight_generate_event(bd, BACKLIGHT_UPDATE_SYSFS);

	return rc;
}
EXPORT_SYMBOL(backlight_device_set_brightness);

static ssize_t brightness_store(struct device *dev,
		struct device_attribute *attr, const char *buf, size_t count)
{
	int rc;
	struct backlight_device *bd = to_backlight_device(dev);
	unsigned long brightness;

	rc = kstrtoul(buf, 0, &brightness);
	if (rc)
		return rc;

	rc = backlight_device_set_brightness(bd, brightness);

	return rc ? rc : count;
}
static DEVICE_ATTR_RW(brightness);

static struct attribute *bl_device_attrs[] = {
	&dev_attr_bl_power.attr,
	&dev_attr_brightness.attr,
	&dev_attr_actual_brightness.attr,
	&dev_attr_max_brightness.attr,
	&dev_attr_type.attr,
	NULL,
};
ATTRIBUTE_GROUPS(bl_device);

  static DEVICE_ATTR_RW(brightness)将brightness节点设置为可读写文件节点,展开如下:

#define __ATTR(_name, _mode, _show, _store) {				\
	.attr = {.name = __stringify(_name),				\
		 .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },		\
	.show	= _show,						\
	.store	= _store,						\
}

 #define __ATTR_RW(_name) __ATTR(_name, (S_IWUSR | S_IRUGO),		\
			 _name##_show, _name##_store)

#define DEVICE_ATTR_RW(_name) \
	struct device_attribute dev_attr_##_name = __ATTR_RW(_name)

  所以对应于brightness_show和brightness_store函数,在驱动注册时,会生成brightness节点,应用层可以用echo和cat命令对其读写。读操作调用brightness_show,写操作调用brightness_store,进而调用backlight_device_set_brightness进行设置背光亮度。
  接下来看ATTRIBUTE_GROUPS(bl_device)定义,首先来看ATTRIBUTE_GROUPS定义。

#define __ATTRIBUTE_GROUPS(_name)				\
static const struct attribute_group *_name##_groups[] = {	\
	&_name##_group,						\
	NULL,							\
}

#define ATTRIBUTE_GROUPS(_name)					\
static const struct attribute_group _name##_group = {		\
	.attrs = _name##_attrs,					\
};								\
__ATTRIBUTE_GROUPS(_name)

  由此可以看出相当于定义了一个bl_device_groups变量,而在初始化时,有这么一行代码:backlight_class->dev_groups = bl_device_groups; 在其后创建文件节点时,会将bl_device_groups变量里的attr,也就是bl_device_attrs全部生成相应的节点。

static const struct attribute_group *bl_device_groups[]={
    {.attrs = bl_device_attrs},
    NULL,
}
  • 此时介绍一下最重要的backlight_device_register函数,定义如下:
struct backlight_device *backlight_device_register(const char *name,
	struct device *parent, void *devdata, const struct backlight_ops *ops,
	const struct backlight_properties *props)
{
	struct backlight_device *new_bd;
	int rc;

	pr_debug("backlight_device_register: name=%s\n", name);

	new_bd = kzalloc(sizeof(struct backlight_device), GFP_KERNEL);
	if (!new_bd)
		return ERR_PTR(-ENOMEM);

	mutex_init(&new_bd->update_lock);
	mutex_init(&new_bd->ops_lock);

	new_bd->dev.class = backlight_class;
	new_bd->dev.parent = parent;
	new_bd->dev.release = bl_device_release;
	dev_set_name(&new_bd->dev, "%s", name);
	dev_set_drvdata(&new_bd->dev, devdata);

	/* Set default properties */
	if (props) {
		memcpy(&new_bd->props, props,
		       sizeof(struct backlight_properties));
		if (props->type <= 0 || props->type >= BACKLIGHT_TYPE_MAX) {
			WARN(1, "%s: invalid backlight type", name);
			new_bd->props.type = BACKLIGHT_RAW;
		}
	} else {
		new_bd->props.type = BACKLIGHT_RAW;
	}

	rc = device_register(&new_bd->dev);
	if (rc) {
		put_device(&new_bd->dev);
		return ERR_PTR(rc);
	}

	rc = backlight_register_fb(new_bd);
	if (rc) {
		device_unregister(&new_bd->dev);
		return ERR_PTR(rc);
	}

	new_bd->ops = ops;

#ifdef CONFIG_PMAC_BACKLIGHT
	mutex_lock(&pmac_backlight_mutex);
	if (!pmac_backlight)
		pmac_backlight = new_bd;
	mutex_unlock(&pmac_backlight_mutex);
#endif

	mutex_lock(&backlight_dev_list_mutex);
	list_add(&new_bd->entry, &backlight_dev_list);
	mutex_unlock(&backlight_dev_list_mutex);

	blocking_notifier_call_chain(&backlight_notifier,
				     BACKLIGHT_REGISTERED, new_bd);

	return new_bd;
}
EXPORT_SYMBOL(backlight_device_register);

  其中调用了device_register函数,在这个函数中添加设备的操作,其中包括了导出设备节点下的文件,即以上描述的brightness、bl_power等节点,其调用流程为:
device_register
–> device_add
----> device_add_attrs
------> device_add_groups
--------> sysfs_create_groups
  在sysfs_create_groups函数中最终将传入的bl_device_groups里的attr导出为文件,此时/sys/class/backlight/backlight/目录下能看到brightness、bl_power等文件节点(实际上是/sys/devices/platform/backlight/backlight/backlight/的软链接)。

No pains, no gains.

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值