通过platform实现LED设备驱动一些基础知识
首先要知道一个字符设备驱动的流程为:分配file_operations结构体,设置file_operations结构体,注册file_operations结构体。具体来说就是:1.确定一个主设备号,2.定义自己的file_operations结构体,3.实现file_operations结构体中的open/wirte/read函数,4.把file_operations结构体告诉内核,即注册file_operations结构体,5.实现入口函数,在这个入口函数中注册file_operations结构体。
首先要区分:设备号(devid),主设备号(major),次设备号(minor)
主设备号:用于标识不同的驱动程序
次设备号:用于区分同一主设备号下的不同设备实例。
设备号:内核设备的唯一标识符。是主设备号和次设备号的组合,通过MKDEV(major,minor)宏生成。
三者之间的关系:
- 设备号
devid通过devid=MKDEV(marjor,minor)获得 - 主设备号
major通过marjor=MAJOR(devid)获得 - 次设备号
minor通过minor=MINOR(devid)获得
举例1:动态分配设备号
- 通过alloc_chrdev_region获得设备号,使用MAJOR和MINOR得到主设备号major和次设备号MINOR
- class_create会在/sys/class/下创建一个名为“GPIO_NAME”的类
- 根据设备号devid,通过device_create()创建GPIOLED_NAME设备 。会在/sys/class/GPIO_NAME的类下面再创建一个名为GPIO_NAME的设备节点。同样该节点在/dev/GPIO_NAME中,应用程序可以open打开这个设备
1 |
|
举例2:分配设备号
- 通过register_chrdev注册lfile_operation结构体,得到主设备号
- 再使用MKDEV(major,i)得到设备号devid,根据devid创建100ask_led0等设备
1 | major = register_chrdev(0, "100ask_led", &led_drv); /* /dev/100ask_led */ |
再来看下如何分配设备号:
设备号的分配可以分为动态分配和静态分配。
静态分配函数原型:
1 | static inline int regiser_chrdev(unsigned int major,const char *name,const struct file_operations* fops) |
使用示例:
1 | major = register_chrdev(0, "100ask_led", &led_drv); |
参数:
- major:主设备号,一般为0,让系统自动分配一个未使用的主设备号
- name:名字,可以通过cat /proc/devices查看
- fops:需要注册的file_operations结构体
返回值:成功则返回主设备号
动态分配函数原型:
1 | int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) |
使用示例:
1 | alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME); |
参数:
- dev:用于保存申请到的设备号,dev_t为unsigned int 类型数据
- baseminor:次设备号的起始地址,一般为0表示次设备号从0开始
- count:需要申请的设备号数量,一般为1
- name:设备名字,同register_chrdev一样,可以在/proc/devices查看
返回值:
- 成功返回
如果给定了设备的主设备号和次设备号,使用register_chrdev_region
函数原型:
1 | int register_chrdev_region(dev_t from, unsigned count, const char *name) |
动态分配设备号下注册字符设备驱动
静态分配不仅分配了设备号,还直接完成了字符设备file_operations的注册,但是会占据主设备号下面的所有次设备号。
动态分配仅仅分配了字符设备的设备号(devid),需要配合cdev_init和cdev_add将设备和文件操作绑定。在分析cdev_init和cdev_add之前,先来看看cdev结构体有哪些成员
cdev结构体:
1 | struct cdev { |
其中dev_t dev就是设备号,file_operations *ops就是最重要的那个设备文件操作结构体指针,一般包括以下内容,这里仅作为示例:
1 | static const struct file_operations led_fops = { |
cdev_init
定义好cdev变量之后,cdev_add用于向Linux系统添加字符设备(cdev结构体),但在使用cdev_add之前需要先对cdev结构体变量进行初始化,cdev_init函数原型如下:
1 | void cdev_init(struct cdev* cdev,const struct file_operations *fops) |
参考示例:
1 | cdev_init(&gpioled.cdev, &led_fops); |
参数:
- cdev:cdev就是要进行初始化的cdev结构体变量
- fops:就是我们一直提到的file_operations结构体,通过cdev_init将cdev和file_operations绑定
cdev_add
对cdev初始化之后就可以通过cdev_add向linux系统中添加这个字符设备。cdev_add原型为:
1 | int cdev_add(struct cdev *p,dev_t dev,unsigned count) |
参考示例:
1 | cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT); |
参数:
- p:指向cdev指针
- dev:显示传入的设备号,应该会将devid(通过alloc_chrdev_region)保存到cdev->dev(设备号)中
- count:要添加的设备的数量
以上便实现了一个字符设备驱动的大部分内容了:包括确定主设备号(动态分配和静态分配),定义自己的file_operations结构体,3.实现file_operations结构体中的open/wirte/read函数(没写出细节),4.把file_operations结构体告诉内核,即注册file_operations结构体(静态注册register_chrdev,cdev_add),5.实现入口函数(实际上上述所有案例代码都位于init函数或者probe函数中)
设备树
- 在根节点下创建gpioled
1 | gpioled { |
- 在imx6ul-evk下创建pinctrl_leds节点
1 | pinctrl_leds: ledgrp { |
看几个重要的结构体
cdev再看一遍,实际上只用到了owner,其他暂时未用到
1 | struct cdev { |
再看class这个庞大的结构体,反正就是通过gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);创建了一个类,啥也没设置,类下边有很多的成员,同样也有owner name啥的。在class结构体前,可以看到这样的一句话:
1 | * A class is a higher-level view of a device that abstracts out low-level |
翻译过来大概就是:类是设备的更高的视角,类抽象出了低层次的实现细节。比如,驱动程序会区分SCSI磁盘和ATA磁盘,但是在类看来,他们都是简单的磁盘。类允许用户空间根据他们的功能进行使用,而不是设备的连接方式和工作方式。
1 | struct class { |
device结构体也是一个庞大的结构体,gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
platform_device结构体
1 | struct platform_device { |
platform_driver -> device_driver->
///代码源码在nfs_rootfs/myled_driver目录下