文章目录
- Q1:到底什么是驱动框架?
- Q2:内核驱动框架中的LED位置在哪?
- Q3:leds目录的特点是什么?
- Q4:怎么在内核中使能LED驱动框架?
- Q5:新创建leds目录,为什么不是空白?
- Q6:class-led.c的部分原理
- (1)subsys_initcall(leds_init);
- (2)static int __init leds_init入口函数
- (3)static void __exit leds_exit(void)退出函数
- (4)led_classdev_register函数
- Q7: 怎么去点灯,以及它的原理是什么?
- 问题一:brightness写入之后,怎么就能自动调用对应的函数?
- 问题二:关于led_cdev->brightness_set(led_cdev, value), value是枚举类型,为何能识别到写入的1?
Q1:到底什么是驱动框架?
(1)驱动框架由内核维护者设计,他们将每种类的驱动设计成一套成熟的、标准的、典型的驱动实现。将各个厂家相同的硬件驱动部分抽出来自己实现,留出一个接口出来,而不同的部分则留给驱动工程师来完成,这就是驱动框架。
(2)驱动加框有这么个特点,遵循先申请资源再使用资源的原则,比如要使用某个GPIO,就要先调用特殊的接口申请,再使用,使用完后再释放资源。譬如,LED框架中类的申请leds_class = class_create(THIS_MODULE, "leds");
(3)提供的接口函数,内部使用一些特定的数据结构,如链表、队列等来管理资源,这些是驱动框架的直接表现。
Q2:内核驱动框架中的LED位置在哪?
- 在内核源码目录下,进入
driver/leds
即可找到。
Q3:leds目录的特点是什么?
(1)在leds目录下,我们重点要关注led-core.h 和 led-class.c这个两个文件,他俩属于LED驱动框架的第一部分,或者说是最基本的文件、最基本的单元。它是由内核维护者提供,内部的实现是这个LED设备通用的方法。
(2)leds-xxx.c 这些文件是LED驱动框架的第二部分,是由不同厂商的驱动工程师编写添加,厂商驱动工程师结合自己公司的硬件的不同情况来对LED进行操作,使用第一部分提供的接口来和驱动框架进行交互,最终实现驱动的功能。
Q4:怎么在内核中使能LED驱动框架?
- 在源码目录下,make menuconfig —》Device Drivers --》 LED Support --》LED class Support (选上),最后 make源码,得到zImage, 把它下载到开发板。
(一般情况下,menuconfig的选项跟内核源码目录很接近,应该很容易找到自己想要的选项。) - 查看是否驱动成功。
- leds类中,专门存放led相关的驱动。
Q5:新创建leds目录,为什么不是空白?
这个跟mmc的驱动有关联,mmc可能需要使用led作为指示,在mmc的驱动中调用了 led_classdev_register
Q6:class-led.c的部分原理
- class-led.c也是一个模块文件,看着一类的文件,一般是从最底下看起。
(1)subsys_initcall(leds_init);
- 它的作用相当于是 module_init,也是一个宏来。
往下追溯subsys_initcall
#define subsys_initcall(fn) __define_initcall("4",fn,4)
↓
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" level ".init"))) = fn
往下追溯module_init
#define module_init(x) __initcall(x);
↓
#define __initcall(fn) __define_initcall("1", fn)
↓
#define __define_initcall(level,fn) \
static initcall_t __initcall_##fn __used \
__attribute__((__section__(".initcall" level ".init"))) = fn
- 结论:subsys_initcall最终将入口函数放在
.initcall4.init
段中,而module_init则将入口函数放在.initcall1.init
中。 - 这些安排有什么作用?
内核启动时候,是按照先后顺序去做很多初始化操作。
内核的解决方案就是给内核启动时要调用的所有函数归类,然后每个类按照一定的次序去调用执行。这些分类名就叫 .initcalln.init,n的值从1到8。内核开发者在编写内核代码时只要将函数设置合适的级别,这些函数就会被链接的时候放入特定的段,内核启动时再按照段顺序去依次执行各个段即可。
(2)static int __init leds_init入口函数
static int __init leds_init(void)
{
leds_class = class_create(THIS_MODULE, "leds");
...
leds_class->dev_attrs = led_class_attrs;
return 0;
}
- 我们重点关注这两行。
class_create
作用是在 /sys/class中创建一个 leds文件夹。leds_class->dev_attrs = led_class_attrs;
这步操作类似于 foperation跟设备驱动绑定在一起 的作用。
跟踪 led_class_attrs
👇👇👇
👇👇👇
结构体的定义
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};
👇👇👇
👇👇👇
static struct device_attribute led_class_attrs[] = {
__ATTR(brightness, 0644, led_brightness_show, led_brightness_store),
__ATTR(max_brightness, 0444, led_max_brightness_show, NULL),
#ifdef CONFIG_LEDS_TRIGGERS
__ATTR(trigger, 0644, led_trigger_show, led_trigger_store),
#endif
__ATTR_NULL,
};
👇👇👇
👇👇👇
#define __ATTR(_name,_mode,_show,_store) { \
.attr = {.name = __stringify(_name), .mode = _mode }, \
.show = _show, \
.store = _store, \
}
- 首先,从
struct device_attribute
结构体的定义可知,它是用来函数指针的方式来指向我们定义的方法,我需要需要实现 show和store功能。 - 在结构体里面,我们还看到
struct attribute attr
,这个有什么作用呢?作用是让程序可以通过/sys/class/leds/目录下的属性文件操作驱动进而操作硬件。 - attribute其实是另一条驱动实现的路线。跟file_operations所走的路线不同。
- 然后,
led_class_attrs[]
是一个结构体数组。内部有三个元素(第三个可以先不管,没有使用到),brightness
和max_brightness
相当于/sys/class/leds/
底下设备里头的文件名,意味着,只要在leds目录下的每个设备都包含了这两个文件。 led_brightness_show
和led_brightness_store
相当于读写方法。往文件里读写内容就会调用这两个函数。
(3)static void __exit leds_exit(void)退出函数
- 调用
class_destroy(leds_class)
相当于删除类,即删除/sys/class/leds/下对应的设备。
(4)led_classdev_register函数
它的内部主要调用led_cdev->dev = device_create(leds_class, parent, 0, led_cdev, "%s", led_cdev->name);
作用是在/sys/class/leds/下创建一个设备,如创建了一个led_driver文件夹,相当于一个设备。
- 在驱动文件当中,我只需调用这个函数就能完成设备的注册。
过往的操作是这样:
retval = alloc_chrdev_region(&dev, 2, MAJORCNT, NAME); //内核分配一个设备号
cdev_init(&mycdev, &fops); //绑定fops
cdev_add(&mycdev, dev, MAJORCNT);
led_class = class_create(THIS_MODULE, "module_test");
device_create(led_class, NULL, dev, NULL, NAME);
Q7: 怎么去点灯,以及它的原理是什么?
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/leds.h>
#include <asm/io.h>
#include <plat/map-base.h>
#include <plat/map-s5p.h>
#define GPL2CON ((volatile unsigned int *)(S5P_VA_GPIO2 + 0x100))
#define GPL2DAT ((volatile unsigned int *)(S5P_VA_GPIO2 + 0x104))
struct led_classdev led_dev;
static void exynos_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
printk(KERN_INFO "exynos_led_set successful!\n");
if (value == LED_OFF)
{
writel(0x11111111, GPL2CON);
writel((0<<0), GPL2DAT);
}
else
{
writel(0x11111111, GPL2CON);
writel((1<<0), GPL2DAT);
}
}
static int __init exynos_led_driver_init(void)
{
int ret = -1;
led_dev.name = "led_driver";
led_dev.brightness = 0;
led_dev.brightness_set = exynos_led_set;
ret = led_classdev_register(NULL, &led_dev);
if (ret < 0) {
printk(KERN_ERR "led_classdev_register ERROR!\n");
return ret;
}
return 0;
}
static void __exit exynos_led_driver_exit(void)
{
led_classdev_unregister(&led_dev);
}
module_init(exynos_led_driver_init);
module_exit(exynos_led_driver_exit);
MODULE_AUTHOR("zhou <549963118@qq.com>");
MODULE_DESCRIPTION("LED driver");
MODULE_LICENSE("GPL");
- 首先,自己编写一个驱动模块,我对端口采用的静态映射。在驱动模块中,我们想要实现点灯,就得实现
exynos_led_set
函数,然后传给led_dev.brightness_set
这一函数指针。 - 在
/sys/class/leds/led_driver/
目录下,echo 1 > brightness
, 开发板上的led灯被点亮。写入0,led灯灭。 - 怎么理解 往brightness写入内容,就能操作led??
大致的调用顺序如下:
echo 1 > brightness
👇
调用 led_brightness_store() (在led_class_attrs结构体中被使用)
👇
led_set_brightness(led_cdev, state);
👇
led_cdev->brightness_set(led_cdev, value); //最终回调我们编写的exynos_led_set函数,这个在我们驱动模块的入口函数中被指定。
问题一:brightness写入之后,怎么就能自动调用对应的函数?
我的猜测是,这是由应用层的udev来管理,不知是不是这个原因。我对udev的理解甚少。
问题二:关于led_cdev->brightness_set(led_cdev, value), value是枚举类型,为何能识别到写入的1?
void (*brightness_set)(struct led_classdev *led_cdev,enum led_brightness brightness);
//函数指针原型
它所对应的枚举如下:
enum led_brightness {
LED_OFF = 0,
LED_HALF = 127,
LED_FULL = 255,
};
这个枚举类型只定义了三种亮度形式,对于LED来说,能应付绝大多数的场景了。
要理解为何能写入1也能把值传进去,而不需传入它指定的枚举类型,这得要先明白枚举的本质。
枚举的本质的一个集合,内部的成员只是一个数字,并非int类型的集合。正如上面的枚举类型,它的范围是0~255,相当于只需用一个字节来标志,也就占用一个字节的空间,只要在范围赋值就是没问题的。而内部所定义的元素,可以理解为有特殊意义的符号,如LED_HAL = 127
,将127这个数字叫做 LED_HAL。
当我们 echo 1 > brightness
之后,之所以没问题,是因为没有超出枚举0~255的范围。