linux驱动_leds-gpio

问题

项目里面有几个通信通道,每个通道有个状态指示灯(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里面选上才会编译,如下:
linux驱动_leds-gpio_第1张图片
后面先介绍一下怎么用的,然后再分析框架。

使用 led-gpio 实现点灯效果

主要有以下3步:

  1. leds-gpio 驱动及设备挂载
  2. 设置trigger类型和参数
  3. 控制LED

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灯灭,亮,闪烁的方法,下面分析是如何实现的。

led-gpio 分析

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即可满足工作需要。

trigger 分析

目前就看了"timer"和"heartbeat"两种trigger,其它的暂不分析。

  • “timer” 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_ondev_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

“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

你可能感兴趣的:(工业物联网关,linux,linux,驱动开发)