一,Linux字符设备驱动框架
字符设备驱动:以字节流形式访问设备文件(/dev/ 下设备对应的设备文件)
网络设备驱动:通过写入数据,读取数据的方式来操作网卡等网络设备
块设备驱动:以固定数据量(块)来进行读写操作的设备。
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL v2");//开源协议 //file_operations操作函数数个 //file_operations结构体填充 //probe函数提供设备注册 //remove函数提供设备注销 //platform_driver结构体填充 // 加载驱动进入初始化 static int led_init(void) { //驱动注册 return 0; } // 驱动卸载推出函数 static void led_exit(void) { //驱动注销 return; } module_init(led_init); // 向内核告知加载驱动时的入口 module_exit(led_exit); // 向内核告知推出驱动时运行该函数Linux将设备抽象为文件进行操作,将对驱动的访问控制,抽象为对文件的read,write,open,close。这些操作对应的函数(指针)填充于结构体file_operations,注册驱动时通过存入结构体cdev传递给内核。为了方便系统管理驱动设备,驱动运行时需要向内核注册并提交相关信息。
二,应用程序访问底层驱动的过程
系统加载驱动时根据驱动声明的入口函数启动驱动,通常情况下用于注册驱动程序,需提交注入名字,拥有者以及用于设备探测,移除,匹配的相关信息。完成所有注册相关操作并帮助驱动成功找到设备后,VFS在/dev/下创建设备文件以及对应的inode结构体。
1 ,VFS(虚拟文件系统)根据文件名找到 inode以及存放设备号(dev_t),内核拿设备号查cdev_map,查到驱动注册的 struct cdev。
2,将 cdev->ops (file_operations) 赋值给 struct file->f_op,调用file_operations中存放的open函数等待驱动初始化。
3,内核分配struct file,记录打开模式和偏移( file->f_op 已经指向 file_operations后续 read/write/ioctl 直接通过 file->f_op 调用不再经过 cdev_map 查找),将 fd 绑定到 struct file 最后返回给用户态。
后续 read()/write()/ioctl(),通过 file->f_op 直接调用驱动函数。
cdev_init() 初始化cdev结构体
alloc_chrdev_region() 注册设备号
cdev_add() 将cdev和设备号绑定在cdev_map上
class_create() 创建设备类/sys/class
device_create() 通过struct class发送uevent事件通知mdev创建设备
三,Linux platform子系统框架
3.1,概述与基本结构
为了方便代码的移植,原来的驱动被分为总线(bus),设备(device),驱动(driver)。Platform 设备驱动专门用于处理**不可自动发现(non-discoverable)** 的设备,即 SoC 内部直接集成或硬连线到主板上的外设(如 UART、SPI控制器、GPIO控制器、I2C控制器等)。通过维护驱动链表和设备链表来存放设备信息和驱动信息。通过将硬件资源(寄存器地址、中断号、时钟、GPIO)写入设备树(Board File)来减小Linux核心文件的大小。通过probe/remove 回调来进行硬件初始化与业务逻辑的注册。
Linux Device Model (统一设备模型)
├── struct bus_type → platform_bus_type (Platform 总线类型)
├── struct device → platform_device (Platform 设备实例)
├── struct device_driver → platform_driver (Platform 驱动实例)
└── struct kobject/kset → sysfs 文件系统暴露接口
static int my_probe(struct platform_device *pdev) { // 1. 获取硬件资源 struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0); int irq = platform_get_irq(pdev, 0); // 2. 初始化硬件 void __iomem *base = devm_ioremap_resource(&pdev->dev, res); // 3. 注册字符设备,向上层提供接口 // ... return 0; } static const struct of_device_id my_of_match[] = { { .compatible = "vendor,my-device" }, { } }; static struct platform_driver my_driver = { .probe = my_probe, .remove = my_remove, .driver = { .name = "my-device", .of_match_table = my_of_match, }, }; module_platform_driver(my_driver);3.2驱动编写
1,确认设备类型。(字符,寄存器,块,)
2,确认设备和描述设备所需资源。
3,确认设备总线匹配移除函数,规划驱动持有者,名字,匹配列表。
4,确认设备注册需要的信息和驱动注册需要的信息。(struct platform_device *pdev和struct platform_driver *pdriver)
5,确认设备匹配优先级:匹配表中专用设备,匹配表中通用设备,设备名与驱动名。
6,匹配好后VFS会调用注册驱动注册信息中的probe函数并提供注入信息的platform_device指针。此函数需要以该结构体指针获取的信息请求相应设备资源并对对应设备进行注册。
7,设备离线后相应资源应该释放。VFS调用的remove函数需写入清除或释放相关资源的程序。
1. of_match_table(设备树 compatible 匹配) ← 最高优先级 2. id_table(平台设备 ID 表匹配) 3. driver.name == device.name(名称匹配) ← 最低优先级3.3设备树
随着设备越来越多,描述设备资源需要的资源越来越多,让Linux内核存储这些会使内核文件变得大而杂乱,所以要求所有设备信息进入设备树存储以保证内核干净和可移植性。
.dts 文件 → dtb → 内核启动时解析 → 自动创建 platform_devicecompatible:驱动通过of_match_table中的compatible与设备树匹配
reg = <寄存器的起始地址 地址的长度>
status:disable不注册,okay注册
设备名-gpios = <&gpio控制器的标签名 管脚编号 标志>
interrupt-parent = <&中断控制器的标签名>
interrupts = < 1个或多个uint32的数字描述中断的信息 >,<...>;// #interrupt-cells
四,Linux 中断子系统框架
cpu资源是有限的,不可能要求cpu时刻关注输入设备的状态。为了让cpu有更多时间处理主任务,片上新增加一个中断控制器(GIC)通知cpu设备发来信息。GIC通注册信息管理中断相关资源,包括是否使能,屏蔽,中断号(对应的设备结构体),中断类型(普通IRQ快速FIQ),中断优先级。通过以上信息和接收到的信号返回中断号,设备结构体。
SPI:共享外设中断,由 Distributor 路由到指定 CPU,UAR,GPIO。
PPI:私有外设中断,每个 CPU 核一份Timer
SGI:软件触发,用于核间通信(IPI) ,核间通知
4.1注册中断,释放
int ret = request_irq(irq_number, handler_func, flags, "name", dev_id);
// 常用 flags:
// IRQF_SHARED - 共享中断线
// IRQF_TRIGGER_RISING - 上升沿触发
// IRQF_TRIGGER_FALLING - 下降沿触发
// 释放中断
free_irq(irq_number, dev_id);
4.2共享中断
中断控制存在共线共号问题,为了判断是谁触发的中断信号,soc通常会停供相应寄存器或类似机制来辅助判断。
4.3中断上下半
由于中断处理必须及时处理,但又不能耽搁cpu太久,需要对中断处理做切割,将信息快速保存的流程或快速硬件操作的指令等打包为中断上半部分让中断回调直接拉起执行。解析,校验,拆分,包处理,多段采样等则放在下半。
- 如果一个任务时间十分敏感,将其放在上半部
- 如果一个任务和硬件有关,将其放在上半部
- 如果一个任务要保证不被其他中断打断,将其放在上半部
- 其他所有任务,考虑放在下半部
4.4 tasklet(下半部实现1)
Linux除了由硬件触发的中断以外,还存在软中断(soft IRQ),通过软件实现中断效果的方式可以让中断不依赖外部中断控制器的情况发生。tasklet则是Linux官方封装的软中断使用接口。
| tasklet_init() | 注册tasklet回调函数 |
| tasklet_schedule() | 调用软中断 |
一个tasklet被tasklet_schedule调用过,它的state状态就被设置为TASKLET_STATE_SCHED,接下来等待被执行。tasklet被执行完后,它的状态才会被清除。如果重复调用一个tasklet,它如果没有被执行过,是不会重复执行的。
4.5workqueue(下半实现2)
工作队列(workqueue)是另外一种将工作推后执行的形式,它和我们前面讨论的所有其他形式都不相同。工作队列可以把工作推后,交由一个内核线程去执行一这个下半部分总是会在进程上下文执行,但由于是内核线程,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠。
通常,在工作队列和软中断/tasklet中作出选择非常容易。可使用以下规则:
1,如果推后执行的任务需要睡眠,那么只能选择工作队列;
2,如果推后执行的任务需要延时指定的时间再触发,那么使用工作队列,因为其可以利用timer延时;
3, 如果推后执行的任务需要在一个tick之内处理,则使用软中断或tasklet,因为其可以抢占普通进程和内核线程;
4,如果推后执行的任务对延迟的时间没有任何要求,则使用工作队列,此时通常为无关紧要的任务。
另外如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列。它是惟一能在进程上
下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在你需要获得大量的内存时、在你需要获取信号
量时,在你需要执行阻塞式的I/O操作时,它都会非常有用。
实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销
毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以我们也推荐使用工作队列。
| INIT_WORK | 初始化work_struct结构体,并绑定工作队列回调函数 |
| schedule_work | 将工作项放入系统默认工作队列的链表中,并唤醒工作线程执行 |
| cancel_work_sync | 取消已提交但未执行的工作项,并同步等待正在执行的工作项完成 |
五,Linux Input子系统框架
5.1概述
Input 子系统用于管理各类输入设备(如按键、触摸屏、鼠标等),负责对外部事件的感知与上报。它将硬件层产生的原始输入事件,经过设备驱动层和核心层处理后,通过统一的接口,当 input_register_device() 执行时,内核会自动创建(如 /dev/input/eventX)提供给用户空间应用程序使用。
linux内核注册了多个handler驱动模块,并通过input_handler_list链表维护
Linux内核中维护了一个名为input_handler_list的链表,每个input_handler都会被注册到这个链表 上,而链表是通过类型为list_head的成员node串联起来的。
input_register_device() 时: 遍历 input_handler_list 每个 handler 调用自己的 match() 函数 匹配成功 → 调用 handler->connect() 例如: evdev handler 匹配所有设备 → 创建 /dev/input/eventX kbd handler 只匹配键盘 → 创建 /dev/input/ttyX5.2数据结构(示例)
// 输入设备结构体 struct input_dev { const char *name; // 设备名称 const char *phys; // 物理路径 struct input_id id; // 设备ID (vendor, product, version) unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 支持的事件类型 unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 支持的按键 unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 支持的相对坐标 unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 支持的绝对坐标 // ... 其他字段 };5.3 Input 驱动编写流程
// 1. 分配 input_dev struct input_dev *input = input_allocate_device(); // 2. 设置设备信息 input->name = "my-keypad"; input->id.bustype = BUS_HOST; input->id.vendor = 0x0001; input->id.product = 0x0001; // 3. 设置支持的事件类型 input->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP); input->keybit[BIT_WORD(KEY_A)] = BIT_MASK(KEY_A); // 4. 注册设备 input_register_device(input); // 5. 在中断或定时器中上报事件 input_report_key(input, KEY_A, 1); // 按下 input_sync(input); // 同步 input_report_key(input, KEY_A, 0); // 释放 input_sync(input);EV_KEY 0x01 按键事件
EV_REL 0x02 相对坐标(鼠标移动)
EV_ABS 0x03 绝对坐标(触摸屏)
EV_SYN 0x00 同步事件
EV_LED 0x11 LED事件