第四章:Android灯光系统(2)-led_class驱动

在我们的安卓手机中,有多种灯光,在SDK/hardware/libhardware/include/hardware文件中,可以看到如下定义

#define LIGHT_ID_BACKLIGHT          "backlight"
#define LIGHT_ID_KEYBOARD           "keyboard"
#define LIGHT_ID_BUTTONS            "buttons"
#define LIGHT_ID_BATTERY            "battery"
#define LIGHT_ID_NOTIFICATIONS      "notifications"
#define LIGHT_ID_ATTENTION          "attention"
#define LIGHT_ID_BLUETOOTH          "bluetooth"
#define LIGHT_ID_WIFI               "wifi"

这些都是android系统逻辑上支持的灯光,他到底对应开发板上的那个LED,由我们的HAL和驱动决定。灯光的使用方式有以下几种:

1.brightness:灯光亮度,可调节0~255;
2.cclor:RGB颜色设定
3.blink:闪烁,onms(亮的时间),offms(灭的时间),通过定时器实现

led_class接口分析

在linux内核中,有一个led子系统,该系统为我们提供了很多接口,在编写灯光系统驱动的时候,我们并不需要从零开始编写,在/drivers/leds目录下,存在文件led_class.c,该文件为我们提供了很多led子系统的接口,其中包括了设置brightness与blink的功能,先查看源码:

static int __init leds_init(void)
{
	leds_class = class_create(THIS_MODULE, "leds");
	if (IS_ERR(leds_class))
		return PTR_ERR(leds_class);
	leds_class->pm = &leds_class_dev_pm_ops;
	leds_class->dev_groups = led_groups;
	return 0;
}

入口函数中,在sys/class目录下创建一个leds类,往上我们可以看到一个SIMPLE_DEV_PM_OPS宏, 将led-class中的suspend中的指针,以及resume的指针初始化当系统休眠或唤醒的时候,遍历class,调用其中的pm的书suspend() 或resume()

	static SIMPLE_DEV_PM_OPS(leds_class_dev_pm_ops, led_suspend, led_resume);
	==>
	static const struct dev_pm_ops leds_class_dev_pm_ops = { 
		SET_SYSTEM_SLEEP_PM_OPS(led_suspend, led_resume) 
	}
	==>
	static const struct dev_pm_ops leds_class_dev_pm_ops = {
		.suspend = led_suspend,
		.resume = led_resume,

然后还会在sys/class/leds目录具体的led下创建3个子节点brightness、max_brightness、trigge,

static DEVICE_ATTR_RW(brightness);			// 可读可写
static DEVICE_ATTR_RO(max_brightness);		// 只读
static DEVICE_ATTR(trigger, 0644, led_trigger_show, led_trigger_store);	// 可读

我们可以用其创建的子节点对LED进行控制,如

	读 cat /sys/class/leds/xxx/brightness
	写 echo 255 > /sys/class/leds/xxx/brightness

我们就能查询或者控制led的亮度了,那么为什么呢?

static ssize_t brightness_show(struct device *dev,
		struct device_attribute *attr, char *buf)
static ssize_t brightness_store(struct device *dev,
		struct device_attribute *attr, const char *buf, size_t size)
static DEVICE_ATTR_RW(brightness);

当往brightness文件写入时,会调用 brightness_store函数,当从brightness读出时,会调用brightness_show函数,我们brightness_store函数中会调用led_set_brightness(led_cdev, state)函数,然后根据led_cdev->flags 判断之后,如果为SET_BRIGHTNESS_ASYNC则最终调用led_cdev->brightness_set(led_cdev, value),如果为SET_BRIGHTNESS_SYNC则最终调用 led_cdev->brightness_set_sync(led_cdev,led_cdev->brightness);从这里可以看出来,我们编写驱动程序的时候,必须根据led_cdev->flags实现一个led_cdev->brightness_set或者brightness_set_sync函数。

编写led驱动程序

根据前面的分析,如果我们通过led子系统提供的框架编写驱动程序,那么我们至少需要实现一下几点,

1.分配一个struct led_classdev  led_cdev,
2.设置 
	led_cdev->max_brightness最大亮度值。
	led_cdev->brightness
	led_cdev->flgs(SET_BRIGHTNESS_SYNC,SET_BRIGHTNESS_ASYNC)
	led_cdev->brightness_set_sync或者led_cdev->brightness_set函数。
3.注册:led_classdev_register(struct device *parent, struct led_classdev *led_cdev)

编写led_gec3399_drv.c具体代码如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/ioport.h>
#include <linux/miscdevice.h>
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/gpio.h>
#include <linux/leds.h>
#include <linux/slab.h>

struct led_desc {
	int gpio;
	char *name;
};

struct led_classdev_gec3399 {
	struct led_classdev cdev;
	int gpio;	
};

static struct led_desc led_gpios[] = {
	{(32*0 + 8*1 + 4),"led1"},
	{(32*0 + 8*1 + 0),"led2"},
};


static struct led_classdev_gec3399 *led_devs;

static void brightness_set_gec3399(struct led_classdev *led_cdev,enum led_brightness brightness)
{
	struct led_classdev_gec3399 *dev = (struct led_classdev_gec3399*)led_cdev;
	if(brightness != LED_OFF)
		gpio_set_value(dev->gpio,0);
	else
		gpio_set_value(dev->gpio,1);
	
	led_cdev->brightness = brightness;
}

static int __init gec3399_leds_init(void)
{
	int i=0;
	int ret = 0;
	/*1.alloc led_classdev*/
	led_devs = (void*)kzalloc(sizeof(struct led_classdev_gec3399)*sizeof(led_gpios)/sizeof(led_gpios[0]),GFP_KERNEL);
	if (!led_devs){
		printk("%s %d NO memory for device\n",__FUNCTION__,__LINE__);
		return -ENOMEM;
	}
	for(i=0; i<sizeof(led_gpios)/sizeof(led_gpios[0]); i++){
		gpio_direction_output(led_gpios[i].gpio,1);      //鍒濆鍒扡ED1涓虹唲鐏姸鎬?
		
		/*2.set*/
		led_devs[i].cdev.max_brightness = LED_FULL;
		led_devs[i].cdev.brightness_set = brightness_set_gec3399;
		led_devs[i].cdev.flags = LED_CORE_SUSPENDRESUME;//设置具有休眠和唤醒功能
		led_devs[i].cdev.brightness = LED_OFF;
		led_devs[i].cdev.name = led_gpios[i].name;
		led_devs[i].gpio = led_gpios[i].gpio;
		
		/*3.led_classdev_register*/
		ret = led_classdev_register(NULL, &led_devs[i].cdev);
		if(ret){
			i--;
			while(i >= 0)
			{
				led_classdev_unregister(&led_devs[i].cdev);
				i--;
			}
			kfree(led_devs);
			return -EIO;
		}
	}
	return 0;
	
}
static void __exit gec3399_leds_exit(void)
{
	int i;
	for(i=0; i<sizeof(led_gpios)/sizeof(led_gpios[0]); i++){
		led_classdev_unregister(&led_devs[i].cdev);
	}
	kfree(led_devs);
}

module_init(gec3399_leds_init);
module_exit(gec3399_leds_exit);

MODULE_LICENSE("GPL");

上述驱动程序,使用for(i=0; i<sizeof(led_gpios)/sizeof(led_gpios[0]); i++)循环,注册了两个leds,编写完成以后,我们把改驱动编译进内核

1.拷贝led_gec3399_drv.c文件到源码目录SDK/drivers/leds目录下。

2.修改SDK/drivers/leds/Makefile,添加obj-y += led_gec3399_drv.o

3.修改menuconfig,打开一下选项
			CONFIG_LEDS_TRIGGERS
			 Device Drivers  ---> 
				 LED Support  --->
				 	LED Trigger support  ---> 
				 		LED Timer Trigger
		打开该配置是为了后续实现闪烁功能
		
4.在kernel目录下执行
		make ARCH=arm64 rk3399-sapphire-excavator-edp.img -j12
		等待编译完成之后,执行
		source build/envsetup.sh
		lunch rk3399_all-userdebug
		make bootimage -j3

编译完成之后,把新生成的out/target/product/rk3399/boot.img烧写到开发板中, 在sys/class/leds下会存在两个目录,分别为led1,与led2。然后我们操控目录下的文件,就可以实现对LED的控制了

命令操控

led控制

我们cd到 /sys/class/leds/led1目录下可以看到:
brightness max_brightness power subsystem trigger uevent
我们执行
echo 255 > /sys/class/leds/led1/brightness
即可点亮LED1
echo 0 > /sys/class/leds/led1/brightness
即可熄灭LED1
执行cat trigger可以看到:
[none] rc-feedback test_ac-online test_battery-charging-or-full test_battery-charging test_battery-full test_battery-charging-blink-full-solid test_usb-online mmc0 mmc1 timer backlight default-on rfkill0 mmc2
其中[]代表现在闪烁的模式,none代表还没有设置
执行
echo > timer /sys/class/leds/led1/trigger
即可设置led以定时器模式闪烁
此时我们再次查看/sys/class/leds/led1:
brightness delay_off delay_on max_brightness power subsystem trigger uevent
可以看到多了delay_on,delay_off文件,我们可以通过echo往里面写值,控制灯亮和灯灭的时间
同样我们可以往max_brightness写入或者读取,去获取或者设置亮度值
echo 255 > /sys/class/leds/led1/brightness

backlight

和上面相似,在sys/class目录下存在backlight文件夹,我们cd到该目录下可以看到如下文件:

actual_brightness      brightness    max_brightness 
subsystem uevent      bl_power          device     power          type 

和前面一样,我们可以通过
echo xxx > /sys/class/backlight/backlight/brightness可以改变屏幕的亮度

trigger分析

为什么我们通过echo > timer /sys/class/leds/led1/trigger就能控制led进行闪烁呢?在前面我们提到过led_class.c文件中,存在宏static DEVICE_ATTR(trigger, 0644, led_trigger_show, led_trigger_store),他会在/sys/classled1或led2目录下(我们注册的led)创建文件trigger,当我们往该文件写入时,会调用led_trigger_store,读取时会调用led_trigger_show。
在led_trigger.c文件中,我们可以看到led_trigger_store函数的定义:

ssize_t led_trigger_store(struct device *dev, struct device_attribute *attr,
		const char *buf, size_t count)
{
	struct led_classdev *led_cdev = dev_get_drvdata(dev);
	char trigger_name[TRIG_NAME_MAX];
	struct led_trigger *trig;
	size_t len;
	int ret = count;

	mutex_lock(&led_cdev->led_access);

	if (led_sysfs_is_disabled(led_cdev)) {
		ret = -EBUSY;
		goto unlock;
	}

	trigger_name[sizeof(trigger_name) - 1] = '\0';
	strncpy(trigger_name, buf, sizeof(trigger_name) - 1);
	len = strlen(trigger_name);

	if (len && trigger_name[len - 1] == '\n')
		trigger_name[len - 1] = '\0';

	if (!strcmp(trigger_name, "none")) {
		led_trigger_remove(led_cdev);
		goto unlock;
	}

	down_read(&triggers_list_lock);
	list_for_each_entry(trig, &trigger_list, next_trig) {
		if (!strcmp(trigger_name, trig->name)) {
			down_write(&led_cdev->trigger_lock);
			led_trigger_set(led_cdev, trig);
			up_write(&led_cdev->trigger_lock);

			up_read(&triggers_list_lock);
			goto unlock;
		}
	}
	up_read(&triggers_list_lock);

unlock:
	mutex_unlock(&led_cdev->led_access);
	return ret;

调用strncpy(trigger_name, buf, sizeof(trigger_name) - 1);从buf中取出trigger,在之前的实验中我们设置为timer。然后执行list_for_each_entry(trig, &trigger_list, next_trig) ,即用取出的trigger与trigger_list中的每一项进行匹配,如果能匹配成功,则执行led_trigger_set(led_cdev, trig),其中有代码如下:

		led_cdev->trigger = trig;
		if (trig->activate)
			trig->activate(led_cdev);

如果存在trig->activate,则会调用该函数,下面我们查看ledtrig-timer.c文件的入口函数:

static struct led_trigger timer_led_trigger = {
	.name     = "timer",
	.activate = timer_trig_activate,
	.deactivate = timer_trig_deactivate,
};

static int __init timer_trig_init(void)
{
	return led_trigger_register(&timer_led_trigger);
}

可知其设定了.activate = timer_trig_activate,其 timer_trig_activate函数如下:

static void timer_trig_activate(struct led_classdev *led_cdev)
{
	int rc;

	led_cdev->trigger_data = NULL;

	rc = device_create_file(led_cdev->dev, &dev_attr_delay_on);
	if (rc)
		return;
	rc = device_create_file(led_cdev->dev, &dev_attr_delay_off);
	if (rc)
		goto err_out_delayon;

	led_blink_set(led_cdev, &led_cdev->blink_delay_on,
		      &led_cdev->blink_delay_off);
	led_cdev->activated = true;

	return;

err_out_delayon:
	device_remove_file(led_cdev->dev, &dev_attr_delay_on);
}

可以知道其创建了两个文件即前面出现的delay_on与delay_off。并设置这两个文件的读写函数,并执行led_blink_set(led_cdev, &led_cdev->blink_delay_on,&led_cdev->blink_delay_off)使LED进行闪烁,led_blink_set中调用了led_set_software_blink,其led_set_software_blink代码如下:

static void led_set_software_blink(struct led_classdev *led_cdev,
				   unsigned long delay_on,
				   unsigned long delay_off)
{
	int current_brightness;

	current_brightness = led_get_brightness(led_cdev);
	if (current_brightness)
		led_cdev->blink_brightness = current_brightness;
	if (!led_cdev->blink_brightness)
		led_cdev->blink_brightness = led_cdev->max_brightness;

	led_cdev->blink_delay_on = delay_on;
	led_cdev->blink_delay_off = delay_off;

	/* never on - just set to off */
	if (!delay_on) {
		led_set_brightness_async(led_cdev, LED_OFF);
		return;
	}

	/* never off - just set to brightness */
	if (!delay_off) {
		led_set_brightness_async(led_cdev, led_cdev->blink_brightness);
		return;
	}

	mod_timer(&led_cdev->blink_timer, jiffies + 1);
}

可以知道,最终使用mod_timer(&led_cdev->blink_timer, jiffies + 1);实现了闪烁功能。

小节结语

该小节,主要分析了led子系统提供的接口,然后利用该接口实现我们的驱动程序,以及如何在命令行控制LED,最后在对led子系统闪烁功能进行了讲解

猜你喜欢

转载自blog.csdn.net/weixin_43013761/article/details/86777778