项目里面有几个通信通道,每个通道有个状态指示灯(LED)。预期断开是灭,已连接是亮,数据传输时闪烁。一开始使用通用sysfs文件系统控制GPIO的方式控制,例如用以下脚本控制GPIO:
echo 6 > /sys/class/gpio/export # pin脚6 GPIO使能
echo 6 > /sys/class/gpio/unexport # pin脚6 GPIO去使能
echo out > /sys/class/gpio/gpio6/direction # 输出使能
echo in > /sys/class/gpio/gpio6/direction # 输入使能
echo 1 > /sys/class/gpio/gpio6/value # 输出高电平
echo 0 > /sys/class/gpio/gpio6/value # 输出低电平
cat /sys/class/gpio/gpio6/value # 读pin脚电平
在应用软件实现点灯逻辑,控制亮和灭都没问题,但是闪烁功能的实时性太差,只能考虑别的方式实现。有两种选择,一是把相关GPIO的I/O内存映射到用户空间,二是使用linux内核的leds-gpio模块,在内核态下使用定时器控制。参考网上资料和学习了一下源码,使用leds-gpio来实现也只要一些简单的改动即可,如此就尝试这种方式实现,并把过程记录在此。参考的资料如下:
https://blog.csdn.net/hanp_linux/article/details/79037610
https://blog.csdn.net/fengweibo112/article/details/102744366
https://www.cnblogs.com/soc-linux-driver/archive/2012/06/30/2561031.html
https://www.cnblogs.com/soc-linux-driver/archive/2012/07/10/2584337.html
Linux 为了广泛通用性及适应性,各种框架都做得非常灵活而又复杂,小小的LED也不例外。支持了不同的LED硬件设备,例如gpio接口,i2c接口,LED芯片等。为了支持各种点灯效果,使用了Trigger框架,除了系统默认的一些trigger外,用户可以创建自定义trigger。因此,为了点个灯,软件开发人员需要了解Linux中gpio, led, trigger三个模块。这里只记录led和trigger的学习和使用,led框架核心文件:
/kernel/include/linux/leds.h // 重要,led相关结构体,宏定义,trigger等
目录 /kernel/driver/leds/ 下
led-class.c // 定义led class及相关接口
led-core.c // export 了闪烁,设置亮灭等接口
led-gpio.c // "leds-gpio" 驱动
leds.h // 提供几个接口,如:led_init_core
trigger 框架核心文件:
目录 /kernel/driver/leds/ 下
led-triggers.c // export了许多接口,包括:led_trigger_register
目录 /kernel/driver/leds/trigger 下
ledtrig-backlight.c
ledtrig-camera.c
ledtrig-cpu.c
ledtrig-default-on.c
ledtrig-disk.c
ledtrig-gpio.c
ledtrig-heartbeat.c // 心跳灯效果
ledtrig-mtd.c
ledtrig-oneshot.c
ledtrig-panic.c
ledtrig-timer.c // 定时器
ledtrig-transient.c
可以参考上面的trigger例子写自己的trigger,或者改造,需要在make menuconfig里面选上才会编译,如下:
后面先介绍一下怎么用的,然后再分析框架。
主要有以下3步:
leds-gpio驱动内核自带,注意编进内核即可,驱动源文件路径如下:
/kernel/drivers/leds/leds-gpio.c
具体代码后面分析。设备挂载可以选择使用设备树,这里选择编译为.ko,在文件系统里面"insmod gpio_led_channels.ko"。源代码在后面分析。挂载后得到以下目录及文件:
# ls -l /sys/class/leds
total 0
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan0 -> ../../devices/platform/leds-gpio/leds/chan0
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan1 -> ../../devices/platform/leds-gpio/leds/chan1
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan2 -> ../../devices/platform/leds-gpio/leds/chan2
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan3 -> ../../devices/platform/leds-gpio/leds/chan3
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan4 -> ../../devices/platform/leds-gpio/leds/chan4
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan5 -> ../../devices/platform/leds-gpio/leds/chan5
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan6 -> ../../devices/platform/leds-gpio/leds/chan6
lrwxrwxrwx 1 root root 0 Jan 3 07:57 chan7 -> ../../devices/platform/leds-gpio/leds/chan7
cd /sys/class/leds/chan0
ls -l
total 0
-rw-r--r-- 1 root root 4096 Jan 3 07:57 brightness # 亮度
-rw-r--r-- 1 root root 4096 Jan 3 07:57 delay_off # OFF 的时延
-rw-r--r-- 1 root root 4096 Jan 3 07:57 delay_on # ON 的时延
lrwxrwxrwx 1 root root 0 Jan 3 07:57 device -> ../../../leds-gpio
-r--r--r-- 1 root root 4096 Jan 3 07:57 max_brightness # 最大亮度
lrwxrwxrwx 1 root root 0 Jan 3 07:57 subsystem -> ../../../../../class/leds
-rw-r--r-- 1 root root 4096 Jan 3 07:57 trigger # 触发器类型
-rw-r--r-- 1 root root 4096 Jan 3 07:56 uevent
分析一下几个文件的作用:
max_brightness – 最大亮度,只读,本人环境是255, 如下:
cat max_brightness
255
brightness – 亮度,可写,范围:0-max_brightness之间, 对于GPIO来说=0就是灭,=max_brightness=255就是亮。可用来控制亮灭和退出闪烁状态,操作如下:
echo 0 > brightness # 灭
cat brightness
0
echo 255 > brightness # 亮
cat brightness
255
trigger – 当前触发器类型,cat 这个文件,用"[]“选中的就是当前选中的类型,如果是”[none]"代表没有选择任何触发器,如下:
cat trigger
[none] timer heartbeat mmc0 mmc1 # 没有选中任何触发器
echo timer > trigger # 选"timer"触发器
cat trigger
none [timer] heartbeat mmc0 mmc1 # 当前触发器为timer
echo heartbeat > trigger
cat trigger
none timer [heartbeat] mmc0 mmc1 # 当前触发器为heartbeat
echo 0 > brightness # 灭LED灯,并清除触发器
cat trigger
[none] timer heartbeat mmc0 mmc1 # 没有选中任何触发器
delay_off 和 delay_on 这两个文件 timer 触发器会用到,分别代表灭和亮和时间,默认是1HZ,也就是500ms亮500ms灭,用户可根据需求改,这里改为100ms亮灭:
cat delay_on
500
cat delay_off
500
echo 100 > delay_on
echo 100 > delay_off
cat delay_on
100
cat delay_off
100
以上就是使用led-gpio来控制LED灯灭,亮,闪烁的方法,下面分析是如何实现的。
Linux内核自带驱动,源文件:/kernel/drivers/leds/leds-gpio.c
用户只需要实现设备的注册或挂载,设备注册的源文件是我自己写的,直接贴上来:
#include
#include
#include
#include
#include
#include
#include "gpio.h"
// {14, 6, 7, 8, 9, 10, 11, 12}
static struct gpio_led gpio_leds[] = {
{
.name = "chan0", // 名字, 会产生同名目录:/sys/class/leds/chan0/
.default_trigger = "timer", // 默认 trigger
.gpio = PAD_GPIO14, // gpio pin 脚号
.active_low = 1, // =1表示低电平LED点亮
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF // 默认状态, OFF|ON|KEEP
},
{
.name = "chan1",
.default_trigger = "timer",
.gpio = PAD_GPIO6,
.active_low = 1,
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF
},
{
.name = "chan2",
.default_trigger = "timer",
.gpio = PAD_GPIO7,
.active_low = 1,
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF
},
{
.name = "chan3",
.default_trigger = "timer",
.gpio = PAD_GPIO8,
.active_low = 1,
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF
},
{
.name = "chan4",
.default_trigger = "timer",
.gpio = PAD_GPIO9,
.active_low = 1,
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF
},
{
.name = "chan5",
.default_trigger = "timer",
.gpio = PAD_GPIO10,
.active_low = 1,
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF
},
{
.name = "chan6",
.default_trigger = "timer",
.gpio = PAD_GPIO11,
.active_low = 1,
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF
},
{
.name = "chan7",
.default_trigger = "timer",
.gpio = PAD_GPIO12,
.active_low = 1,
.retain_state_suspended = 1,
.default_state = LEDS_GPIO_DEFSTATE_OFF
},
};
static struct gpio_led_platform_data gpio_led_info = {
.leds = gpio_leds,
.num_leds = ARRAY_SIZE(gpio_leds),
};
static void led_channels_release(struct device *dev)
{
;
}
static struct platform_device led_channels = {
.name = "leds-gpio", // 要跟驱动名匹配上
.id = -1,
.dev.platform_data = &gpio_led_info,
.dev.release = led_channels_release,
.id = -1,
};
static int __init gpio_led_channels_init(void)
{
return platform_device_register(&led_channels);
}
static void __exit gpio_led_channels_exit(void)
{
platform_device_unregister(&led_channels);
}
module_init(gpio_led_channels_init);
module_exit(gpio_led_channels_exit);
MODULE_LICENSE("GPL");
设备的代码比较简单,我这里编译为".ko",通过 insmod 注册一个平台设备即可,名字要注意与驱动一致,代码如下:
// 这是设备的代码
static struct platform_device led_channels = {
.name = "leds-gpio", // 要跟驱动名匹配上
.id = -1,
.dev.platform_data = &gpio_led_info,
.dev.release = led_channels_release,
.id = -1,
};
// 这是驱动的代码
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe,
.shutdown = gpio_led_shutdown,
.driver = {
.name = "leds-gpio", // 驱动名
.of_match_table = of_gpio_leds_match,
},
};
设备通过"dev.platform_data"最终把gpio描述数组"struct gpio_led gpio_leds[]"传给驱动,即驱动里 gpio_led_probe 函数的参数。驱动的probe函数负责把设备参数拿到,存放到新申请的空间,然后注册led-class,简略代码流程如下:
static int create_gpio_led(const struct gpio_led *template,
struct gpio_led_data *led_dat, struct device *parent,
gpio_blink_set_t blink_set)
{
led_dat->gpiod = template->gpiod;
if (!led_dat->gpiod) {
ret = devm_gpio_request_one(parent, template->gpio, flags,
template->name); //
led_dat->gpiod = gpio_to_desc(template->gpio);
}
led_dat->cdev.name = template->name;
led_dat->cdev.default_trigger = template->default_trigger;
led_dat->blinking = 0;
led_dat->cdev.brightness = state ? LED_FULL : LED_OFF;
ret = gpiod_direction_output(led_dat->gpiod, state); // 以上都是配置led-gpio
return devm_led_classdev_register(parent, &led_dat->cdev); // 注册led-class设备
}
static int gpio_led_probe(struct platform_device *pdev)
{
struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
struct gpio_leds_priv *priv;
// 两个分支差不多的功能
if (pdata && pdata->num_leds) {
priv = devm_kzalloc(...); // 申请空间
ret = create_gpio_led(...); // 根据设备的参数配置驱动
} else {
priv = gpio_leds_create(pdev); // 里面也会调用上面分支的内容
}
platform_set_drvdata(pdev, priv); // 保存私有数据
return 0;
}
“devm_led_classdev_register” 函数在/kernel/drivers/leds/leds-class.c, 然后再调用"led_classdev_register"函数,这个函数的作用就是注册一个新的"led_classdev"对象,后面基本是"leds-class.c"和"leds-core.c" 这两个文件里面的代码实现了/sys/class/leds/下的功能。例如led-class.c中brightness方法有一个show方法和store方法,这两个方法对应用户在/sys/class/leds/chanx/brightness目录下直接去读写这个文件时实际执行的代码。当我们"cat brightness"(或者代码里面read这个文件)时,实际就会执行led_brightness_show函数。当我们"echo 1 > brightness"时,实际就会执行led_brightness_store函数。代码如下:
static ssize_t brightness_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
/* no lock needed for this */
led_update_brightness(led_cdev);
return sprintf(buf, "%u\n", led_cdev->brightness);
}
static ssize_t brightness_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned long state;
ssize_t ret;
mutex_lock(&led_cdev->led_access);
if (led_sysfs_is_disabled(led_cdev)) {
ret = -EBUSY;
goto unlock;
}
ret = kstrtoul(buf, 10, &state);
if (ret)
goto unlock;
if (state == LED_OFF)
led_trigger_remove(led_cdev);
led_set_brightness(led_cdev, state);
ret = size;
unlock:
mutex_unlock(&led_cdev->led_access);
return ret;
}
static DEVICE_ATTR_RW(brightness);
“led-class.c” 和 "led-core.c"内容和提供的接口比较多,详情看源码,目前知道怎么注册一个led-gpio即可满足工作需要。
目前就看了"timer"和"heartbeat"两种trigger,其它的暂不分析。
先看看"/kernel/drivers/leds/trigger/ledtrig-timer.c"。当模块被加载时,会注册trigger,实际上是把这个trigger加入trigger链表去。这个trigger还定义了两个回调函数:“timer_trig_activate”, “timer_trig_deactivate”,当这个trigger使能时会在/sys/devices/xxx/目录下创建"delay_on"和"delay_off"文件,去使能时移除这两个文件。从前面可以知,这两个文件就是提供给用户设置亮和灭的时延的。同时,也提供了读写这两个文件的底层处理函数:
led_delay_on_show
led_delay_on_store
led_delay_off_show
led_delay_off_store
最后还要注意 DEVICE_ATTR,不了解这个宏的话,永远找不到 dev_attr_delay_on 和 dev_attr_delay_off 的定义在哪里。下面是带有注释的整份源码:
/*
* LED Kernel Timer Trigger
*
* Copyright 2005-2006 Openedhand Ltd.
*
* Author: Richard Purdie
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
*/
#include
#include
#include
#include
#include
#include
static ssize_t led_delay_on_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
return sprintf(buf, "%lu\n", led_cdev->blink_delay_on);
}
static ssize_t led_delay_on_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned long state;
ssize_t ret = -EINVAL;
ret = kstrtoul(buf, 10, &state);
if (ret)
return ret;
led_blink_set(led_cdev, &state, &led_cdev->blink_delay_off);
led_cdev->blink_delay_on = state;
return size;
}
static ssize_t led_delay_off_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
return sprintf(buf, "%lu\n", led_cdev->blink_delay_off);
}
static ssize_t led_delay_off_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned long state;
ssize_t ret = -EINVAL;
ret = kstrtoul(buf, 10, &state);
if (ret)
return ret;
led_blink_set(led_cdev, &led_cdev->blink_delay_on, &state);
led_cdev->blink_delay_off = state;
return size;
}
/*
#include
#define __ATTR(_name,_mode,_show,_store) { \
.attr = {.name = __stringify(_name), .mode = _mode }, \
.show = _show, \
.store = _store, \
}
#include
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
从上面宏定义可知,下面这两行相当于:
static struct device_attribute dev_attr_delay_on = {...}
static struct device_attribute dev_attr_delay_off = {...}
*/
static DEVICE_ATTR(delay_on, 0644, led_delay_on_show, led_delay_on_store);
static DEVICE_ATTR(delay_off, 0644, led_delay_off_show, led_delay_off_store);
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); // 在/sys/devices/xxx/目录下创建"delay_on"文件
if (rc)
return;
rc = device_create_file(led_cdev->dev, &dev_attr_delay_off); // 在/sys/devices/xxx/目录下创建"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);
}
static void timer_trig_deactivate(struct led_classdev *led_cdev)
{
if (led_cdev->activated) {
device_remove_file(led_cdev->dev, &dev_attr_delay_on); // 移除/sys/devices/xxx/目录下的"delay_on"文件
device_remove_file(led_cdev->dev, &dev_attr_delay_off); // 移除/sys/devices/xxx/目录下的"delay_off"文件
led_cdev->activated = false;
}
/* Stop blinking */
led_set_brightness(led_cdev, LED_OFF);
}
static struct led_trigger timer_led_trigger = {
.name = "timer", // trigger 名字
.activate = timer_trig_activate, // 使能这个trigger时的回调函数
.deactivate = timer_trig_deactivate, // 去使能回调函数
};
static int __init timer_trig_init(void)
{
// 注册trigger, 调用了 "leds-triggers.c" -> "int led_trigger_register(struct led_trigger *trig)"
// 相当于把这个trigger加入了trigger链表
return led_trigger_register(&timer_led_trigger);
}
static void __exit timer_trig_exit(void)
{
led_trigger_unregister(&timer_led_trigger);
}
module_init(timer_trig_init);
module_exit(timer_trig_exit);
MODULE_AUTHOR("Richard Purdie ");
MODULE_DESCRIPTION("Timer LED trigger");
MODULE_LICENSE("GPL");
既然这个trigger已经加入trigger链表,那么LED闪烁功能在哪里实现的呢?在"/kernel/driver/leds/led-core.c"文件的"static void led_timer_function(unsigned long data)"函数里。这个函数是上一节 leds-gpio 驱动注册时被初始化的,调用过程:gpio_led_probe -> create_gpio_led -> devm_led_classdev_register -> led_classdev_register -> led_init_core -> setup_timer(led_timer_function),下面来看看这个函数(缩略版):
static void led_timer_function(unsigned long data)
{
...
// 根据状态亮或灭,获取新状态的时延
brightness = led_get_brightness(led_cdev);
if (!brightness) {
brightness = led_cdev->blink_brightness;
delay = led_cdev->blink_delay_on;
} else {
brightness = LED_OFF;
delay = led_cdev->blink_delay_off;
}
led_set_brightness_nosleep(led_cdev, brightness);
// 启动一个定时器,延时后再执行本函数,所以能一直循环闪烁
mod_timer(&led_cdev->blink_timer, jiffies + msecs_to_jiffies(delay));
}
由上面代码可知通过定时器循环调用本身函数来实现闪烁功能的,即"timer" trigger(/kernel/driver/leds/trigger/ledtrig-timer.c)点灯操作在"/kernel/driver/leds/led-core.c"文件里完成。
“heartbeat” trigger的实现并不一样,它在使能时就初始化一个定时器,绑定了本文件的回调函数,还调用了这个函数,这个函数里的定时器会循环调用回自己,所以说点灯的操作在本身trigger文件(/kernel/driver/leds/trigger/ledtrig-heartbeat.c)里面完成。
心跳点灯分为4个状态(phase),分别是"亮灭亮灭",但是最后一个灭时间会比较长,做出心跳的效果,源码如下:
static void led_heartbeat_function(unsigned long data)
{
struct led_classdev *led_cdev = (struct led_classdev *) data;
struct heartbeat_trig_data *heartbeat_data = led_cdev->trigger_data;
unsigned long brightness = LED_OFF;
unsigned long delay = 0;
if (unlikely(panic_heartbeats)) {
led_set_brightness_nosleep(led_cdev, LED_OFF);
return;
}
/* acts like an actual heart beat -- ie thump-thump-pause... */
switch (heartbeat_data->phase) {
case 0:
/*
* The hyperbolic function below modifies the
* heartbeat period length in dependency of the
* current (1min) load. It goes through the points
* f(0)=1260, f(1)=860, f(5)=510, f(inf)->300.
*/
heartbeat_data->period = 300 +
(6720 << FSHIFT) / (5 * avenrun[0] + (7 << FSHIFT));
heartbeat_data->period =
msecs_to_jiffies(heartbeat_data->period);
delay = msecs_to_jiffies(70);
heartbeat_data->phase++;
if (!heartbeat_data->invert)
brightness = led_cdev->max_brightness;
break;
case 1:
delay = heartbeat_data->period / 4 - msecs_to_jiffies(70);
heartbeat_data->phase++;
if (heartbeat_data->invert)
brightness = led_cdev->max_brightness;
break;
case 2:
delay = msecs_to_jiffies(70);
heartbeat_data->phase++;
if (!heartbeat_data->invert)
brightness = led_cdev->max_brightness;
break;
default:
delay = heartbeat_data->period - heartbeat_data->period / 4 -
msecs_to_jiffies(70);
heartbeat_data->phase = 0;
if (heartbeat_data->invert)
brightness = led_cdev->max_brightness;
break;
}
led_set_brightness_nosleep(led_cdev, brightness);
mod_timer(&heartbeat_data->timer, jiffies + delay); // 初始化下一个时间点
}
end