文章目录

  • 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相关的驱动。
  • Linux driver架构 linux驱动框架讲解_#include

Q5:新创建leds目录,为什么不是空白?

Linux driver架构 linux驱动框架讲解_#include_02


这个跟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[] 是一个结构体数组。内部有三个元素(第三个可以先不管,没有使用到),brightnessmax_brightness相当于/sys/class/leds/底下设备里头的文件名,意味着,只要在leds目录下的每个设备都包含了这两个文件。
  • Linux driver架构 linux驱动框架讲解_枚举类型_03

  • led_brightness_showled_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文件夹,相当于一个设备。

Linux driver架构 linux驱动框架讲解_Linux driver架构_04

  • 在驱动文件当中,我只需调用这个函数就能完成设备的注册。
    过往的操作是这样:
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的范围。