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>;
...
...
}
首先需要对几个属性进行简单描述:
- regulator-boot-on:在boot开机阶段,就开始上电,即对定义的gpio进行拉高或者拉低操作。
- regulator-always-on:除了跟regulator-boot-on有一样的功能外,还代表了禁止对此电源掉电,也就是无法对该gpio进行后续的控制。
- regulator-name:若其他模块用到这个regulator,则使用该名字
- 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
背光控制的实际操作函数,背光亮度和背光电源都在本文件中实现。
- 先看驱动适配函数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驱动注册,也是最核心的一个函数,接下来重点介绍一下。
- 再来看一下背光控制的实际操作函数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.