驱动第二次答辩
字符设备驱动框架
对于应用层和接口
函数接口通过函数指针来规定返回值和参数规定死;上层代码只认这个,底层通过注册回调函数来填充实现,调用时通过指针间接执行。
对硬件来说只写操作设备的接口
由于对于上层来说只有设备文件;所以要设备文件和接口关联;
函数接口通过结构体file_operations规定;里面存放了很多函数;
高类聚低耦合:把相关的紧密放一起(高内聚),不相关的别扯上关系(低耦合)
常见函数
| 函数 | 功能分类 | 核心作用 |
|---|---|---|
| cdev_init() | cdev初始化 | 初始化cdev结构体,绑定file_operations |
| alloc_chrdev_region() | 设备号注册 | 自动注册设备号 |
| cdev_add() | cdev绑定 | 将cdev和设备号绑定在cdev_map上 |
| class_create() | 设备类创建 | 创建设备类/sys/class |
| device_create() | 设备节点创建 | 通过struct class发送uevent事件通知mdev创建设备节点 |
应用程序访问底层驱动过程
当驱动被加载时,它会向内核注册自己的主设备号和一组操作函数(file_operations)。当用户程序调用open打开/dev下的对应设备文件时,内核通过文件中的主设备号找到并绑定该驱动,之后用户程序对该文件描述符的读写操作,就会自动被内核转发到驱动注册的对应函数,从而实现对硬件的控制。
Linux Platform子系统框架
总线、设备、驱动 => 增加驱动的可扩展性和可移植性
将设备的信息从驱动中分离出来,我们需要在操作系统中,添加设备和驱动两部分。
设备中包含设备的信息(资源),驱动中包含的是操作设备函数接口。
为了让驱动最终能操作我们的硬件设备,我们在驱动中必须获取设备的信息(资源)。
设备和驱动是分离的,它们都会向内核总线(如platform总线)注册。当任何一方(无论是设备先来,还是驱动先来)注册时,内核都会触发一次总线上的匹配扫描,寻找名称或ID匹配的对方。一旦匹配成功,操作系统就会自动调用驱动中实现的probe函数。
当设备和驱动匹配成功后,内核会将这个设备的详细信息(包括设备树解析出的资源、名称、ID等)封装成platform_device结构体,并通过probe函数的参数传递给驱动。
在Linux内核中,总线本质上是两个用于管理和匹配的链表,分别挂载着该总线上的设备和驱动;当设备或驱动注册时,内核通过总线遍历这两个链表进行匹配,匹配成功则调用驱动的probe函数。
基于总线写驱动流程:
1.根据设备确定总线类型
2.根据总线类型,确定设备在总线上如何描述struct platform_device;
内核解析设备树后,自动创建struct platform_device
3.根据总线类型,确定驱动在总线上如何描述struct platform_driver;
4.根据总线类型,确定如何在总线上注册设备platform_device_register;
设备树自动创建
5.根据总线类型,确定如何在总线上注册驱动platform_driver_register;
6.确定设备和驱动匹配原则(name、id_table、of_match_table)
7.设备和驱动匹配后,linux调用probe函数获取硬件资源、注册字符设备
常见函数
| 函数 | 功能分类 | 核心作用 |
|---|---|---|
| platform_get_resource | 资源获取 | 从platform_device中获取指定类型的资源信息(如中断号、寄存器地址、DMA通道等) |
| platform_set_drvdata | 数据保存 | 将私有数据指针保存到platform_device中,供后续回调函数使用 |
| platform_get_drvdata | 数据获取 | 从platform_device中获取之前保存的私有数据指针 |
| 资源类型宏 | 含义 | 示例 |
|---|---|---|
IORESOURCE_MEM | 内存地址区域 | 寄存器基址、RAM 区域 |
IORESOURCE_IO | I/O 端口地址 | x86 的 I/O 端口 |
IORESOURCE_IRQ | 中断号 | 硬件中断线 |
IORESOURCE_DMA | DMA 通道 | DMA 控制器通道号 |
IORESOURCE_BUS | 总线地址 | 总线地址范围 |
设备树
Linux 内核platform总线上设备与驱动的匹配规则
<1>设备树中的compatible属性与驱动中指定的of_match_table中的compatible进行匹配
如果没有匹配成功:
<2>如果驱动中有id_table,则拿id_table中记录的名字与设备的名字匹配
如果驱动中没有id_table,则拿驱动的名字与设备的名字(platform_device结构体中name字段)匹配
Linux 中断子系统框架
1.中断打断其他程序的执行,所以中断处理的时候需要尽可能的快,不能在中断处理过程中做耗时很长的事情。
2.中断打断的当前的程序执行,所以在中断处理的时候,需要先保存现场(CPU的状态和CPU内部寄存器的值:压栈保存)在中断处理结束的时候,则恢复现场。
中断控制器(GIC )负责对每个中断源进行编号、使能、屏蔽和优先级仲裁,并根据配置将中断请求分类为普通中断(IRQ)或快速中断(FIQ),最终分发给ARM 核心(ARM Core)。ARM 核接收到中断信号后,会立即保存当前程序的执行上下文(现场),然后跳转到异常处理向量表,执行对应的中断服务例程(ISR),处理完后再恢复现场并返回被打断的程序继续执行。
全局的irq_desc数组按中断号(0到NR_IRQS-1)索引,每个irq_desc结构体记录对应中断号、指向特定中断控制器操作函数集(irq_chip,如VIC0/VIC1)的指针,以及一个由irqaction结构体组成的链表。irqaction用于描述具体的中断处理动作,包含处理函数handler、传递给处理函数的参数、中断标志、设备名称,并通过next指针支持多个设备共享同一中断号,当发生中断时,内核会通过该中断号找到对应的irq_desc,进而调用其irq_chip中的控制函数(如enable/disable)并遍历执行action链表上的所有处理函数。
常见函数
| 函数 | 功能分类 | 核心作用 |
|---|---|---|
| platform_get_resource | 资源获取 | 从platform_device中获取指定类型的资源信息(如中断号、触发方式、名称等) |
| devm_request_irq | 中断注册 | 注册中断号、触发方式和中断回调函数,中断生命周期绑定设备,设备移除时自动释放 |
中断上半部和下半部
将中断处理函数中需要做的事情,分成两部分,在不同的函数中完成。
上半部在中断产生时立即执行,在中断上下文中运行且屏蔽中断,只负责紧急、快速的任务(如清中断标志、拷贝硬件数据);
下半部则在合适的时机(通常上半部结束后触发)执行,不屏蔽中断、可被打断,负责耗时较长或可能需要休眠的剩余工作(如数据处理、协议解析)。
下半部的实现机制
软中断
tasklet
tasklet 是基于软中断实现的。
软中断不能直接使用,Linux 内核为了方便开发者,在软中断之上封装了 tasklet 接口,使用更简单。所以,除非对性能要求特别高,否则都应使用 tasklet。
常见函数
| tasklet_init() | 注册tasklet回调函数 |
| tasklet_schedule() | 调用软中断 |
work queue
工作队列(work queue)是另一种将工作推后执行的形式,它把工作交由一个内核线程去执行,在进程上下文运行,但不能访问用户空间。其最大特点是允许重新调度甚至睡眠。
选择工作队列还是软中断/tasklet,可遵循以下规则:
推后执行的任务需要睡眠→ 只能选工作队列;
任务需要延时指定时间再触发→ 选工作队列(可利用timer延时);
任务需要在一个tick内处理→ 选软中断或tasklet(可抢占普通进程和内核线程);
任务对延迟时间无要求(无关紧要的任务) → 选工作队列。
另外,如果需要用一个可以重新调度的实体来执行下半部,应使用工作队列。它是唯一能在进程上下文运行的下半部机制,也只有它可以睡眠。这在需要获取大量内存、获取信号量、执行阻塞式I/O操作时非常有用。
常见函数
| 函数 | 功能分类 | 核心作用 |
|---|---|---|
| INIT_WORK | 工作队列初始化 | 初始化work_struct结构体,并绑定工作队列回调函数 |
| schedule_work | 工作队列调度 | 将工作项放入系统默认工作队列的链表中,并唤醒工作线程执行 |
| cancel_work_sync | 工作队列同步取消 | 取消已提交但未执行的工作项,并同步等待正在执行的工作项完成 |
| 下半部机制 | 上下文 | 复杂度 | 执行性能 | 顺序执行保障 |
|---|---|---|---|---|
| 软中断 | 中断 | 高 (需要自己确保软中断的执行顺序及锁机制) | 好 (全部自己实现,便于调优) | 没有 |
| tasklet | 中断 | 中 (提供了简单的接口来使用软中断) | 中 | 同类型不能同时执行 |
| 工作队列 | 进程 | 低 (在进程上下文中运行,与写用户程序差不多) | 差 | 没有 (和进程上下文一样被调度) |
Linux Input子系统框架
Input 子系统用于管理各类输入设备(如按键、触摸屏、鼠标等),负责对外部事件的感知与上报。它将硬件层产生的原始输入事件,经过内核驱动和核心层处理后,通过统一的接口当input_register_device()执行时,内核会自动创建(如/dev/input/eventX)提供给用户空间应用程序使用。
linux内核注册了多个handler驱动模块,并通过input_handler_list链表维护
设备匹配流程
Linux 内核中维护了一个名为 input_handler_list 的链表, 每个 input_handler 都会被注册到这个链表 上, 而链表是通过类型为 list_head 的成员 node 串联起来的。
和input_dev匹配成功后会创建input_handle,会分配新的次设备号、注册字符设备(提供evdev_fops)并创建设备节点(/dev/input/eventX)。
常见函数
| 函数 | 功能分类 | 核心作用 |
|---|---|---|
| devm_input_allocate_device | 资源分配 | 申请input_dev资源并在remove时自动释放;并绑定参数代表设备的生命周期 |
| input_register_device | 设备注册 | 注册input_dev设备,并创建一个input设备节点给应用层访问 |
| input_report_key | 事件上报 | 向核心层上报按键值,存入事件缓存 |
| input_sync | 同步事件 | 发送同步事件,表示应用层可读取一组输入事件 |
硬件
led
在源码/Documentation/devicetree/bindings:有各个设备树的解释
grep "gpio-cells" * -nR | grep "厂家名字"查找到的信息如下
在自己的设备树中引用该节点
按键
在设备树下查找;看看芯片厂家怎么写的
grep "interrupt-controller" * -nRcombiner要看芯片手册支不支持
就近原则:谁直接控制我们中断就写谁;我们选图三
在芯片的参考文档linux-3.14/Documentation/devicetree/bindings中找
grep "#interrupt-cells" * -nR | grep exynos先找板子再找厂家;从范围小的找起
| 参数 | 值 | 来源 |
|---|---|---|
| 第一个参数(中断号) | 1 | 硬件原理图:按键连接在 GPX1_1 引脚,对应中断编号 1 |
| 第二个参数(触发方式) | 8 | 芯片手册/文档:低电平触发 |
所以写出来的设备树是
现象
问题
在同一个驱动管理多个设备时,open函数如何准确获取当前打开的是哪个设备的私有数据,从而正确操作对应硬件的问题。
| 对比维度 | 方法一:container_of | 方法二:次设备号查链表/数组 |
|---|---|---|
| 依据 | 通过inode->i_cdev反推包含它的外层结构体地址 | 通过iminor(inode)获取次设备号,作为索引或键值查找 |
| 适用环境 | 每个设备独立拥有自己的cdev,注册时cdev_add的 count 为 1 | 多个设备共享同一个cdev,注册时cdev_add的 count 大于 1 |
