ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

驱动第二阶段

驱动第二阶段 一.Linux 字符设备驱动框架及应用程序访问底层驱动过程1.1字符设备驱动框架1.封装自己的字符设备结构体struct mycdev { // 自己字符设备的信息私有数据缓冲区、状态、标志等 ... // 内嵌内核通用字符设备对象 struct cdev cdev; };通过container_of宏由struct cdev反向拿到struct mycdev私有结构体。2.实现 struct file_operations把上面 open/read/write/ioctl 函数赋值给这个结构体建立用户系统调用和驱动函数的桥梁。3.模块入口函数(1) 分配设备结构体空间kmalloc动态分配struct mycdev内存也可以静态全局分配(2) 初始化 cdevcdev_init()把file_operations和内嵌的cdev绑定(3) 申请设备号register_chrdev_region()向内核申请主 / 次设备号(4) 添加设备进内核cdev_add()把 cdev 对象注册到内核设备正式可用4.模块出口函数(顺序和注册相反)(1) kfree()释放 kmalloc 申请的设备内存(2) cdev_del()从内核移除字符设备(3) unregister_chrdev_region()释放申请的设备号Linux 驱动的分层思想上层file_operations 提供标准接口中层cdev 是内核的通用字符设备抽象下层mycdev 承载驱动私有的业务逻辑linux驱动本质就是向应用层的程序提供设备的操作函数接口1.2应用程序访问底层驱动过程应用 open(/dev/xxx) ↓系统调用陷入内核 VFS虚拟文件系统根据文件名找到 inodeinode存放设备号 ↓ 内核拿设备号查询 cdev_map找到驱动注册的 struct cdev ↓ 把 cdev-file_operations 赋值给本次打开的 struct file-f_op ↓ 后续 read/write/ioctl → 通过 file-f_op 直接调用驱动函数用户态open()触发内核路径解析得到inode通过inode-i_cdev或cdev_map找到cdev从cdev-ops拿到驱动提供的file_operations内核先分配struct file记录打开模式和偏移再调用fops-open()让驱动做初始化如申请硬件资源成功后把 fd 绑到struct file返回给用户。二、Linux Platform子系统框架由于内核版本差异、硬编码、依赖平台等原因字符驱动框架的可移植性差。衍生了一种总线、设备、驱动的框架目的就在于增加驱动的可扩展性和可移植性。其核心思想将设备的信息从驱动中分离出来我们需要在操作系统中添加设备和驱动两部分。设备中包含是设备的信息(资源)驱动中包含的是操作设备函数接口。为了能让驱动最终能操作我们的硬件设备我们在驱动中必须获取设备的信息(资源)。那么设备和驱动分离了驱动是如何获取具体设备信息的呢设备和驱动都会注册到总线上当注册设备的时候会去寻找同名的驱动当注册驱动的时候也会去找同名的设备。相互查找。一旦匹配成功操作系统就会自动调用驱动提供的probe函数。我们只需要在probe函数中使用操作系统提供的通用API获取硬件的信息即可。Linux 总线分为两类Platform 平台总线platform bus用于 CPU 与片上控制器通信挂载片内控制器设备。外设总线 (I2C/SPI/USB/UART)用于板上外围设备互相通信每种总线硬件时序不同内核各自独立实现子系统。platform 总线工作机制注册设备 / 驱动后依靠name / DTS 的compatible完成匹配匹配成功触发probe多个设备就多次调用 probe。驱动用全局数组保存各个设备实例实现一份驱动管理多个同类硬件。基于总线写驱动流程1.通用思想1.判断设备属于哪一条总线 2.找到该总线对应的设备结构体描述硬件信息 3.找到该总线对应的驱动结构体填写 probe/remove、id_table 4.调用总线 API注册设备、注册驱动 5.按照该总线的匹配规则做匹配 6.匹配成功自动进入 probe在 probe 中做硬件初始化、创建字符设备等业务。2.完整流程(1)根据自己的设备来确定总线的类型platform bus / i2c bus /usb bus/....(2)根据总线的类型, 确定设备在总线上如何描述(结构体)struct platform_device{设备名;设备的资源通用的设备描述(struct device : platform_data-记录设备私有的信息)id_entry:当设备和驱动的id_table中某一个成员匹配上的时候这个成员就会记录id_table中匹配上的成员地址};struct resource{int start;资源的开始int end;资源的结束资源的类型:IO资源(寄存器地址) 中断资源(中断号) DMA资源(通道)}(3)根据总线的类型, 确定驱动在总线上如何描述(结构体)struct platform_driver{probe函数 :设备和驱动匹配的时候操作系统自动调用remove函数:设备和驱动分离的时候操作系统自动调用通用的驱动描述(struct device_driver : 这里面可以记录驱动的名字)id_table : 当前驱动支持平台设备(记录支持的平台设备名字)};(4)根据总线的类型确定在总线上如何注册设备int platform_device_register(struct platform_device *pdev);(5)根据总线的类型确定在总线上如何注册驱动int platform_driver_register(struct platform_driver *pdriver);(6)根据总线的类型, 确定设备和驱动匹配原则如果驱动提供了id_table,那就拿设备的名字和id_table中记录的名字匹配如果驱动没有提供id_table,那就拿设备的名字和驱动的名字进行匹配(7)一旦设备和驱动匹配后操作系统就会调用驱动提供的probe函数。在这个函数中一般需要做两件事情:1获取匹配的硬件资源struct resource *platform_get_resource(struct platform_device *dev,unsigned int type,int num);dev 平台设备的结构体 type 资源的类型 num 同类型资源的编号2注册字符设备(可选)三、设备树设备树是描述硬件资源的文本文件.dts/.dtsi作用把硬件信息从内核驱动代码剥离出来。内核解析 dtb 之后设备树节点 → 内核生成对应设备结构体platform_device驱动依靠compatible属性做匹配匹配成功调用probe。.dtsi公共头文件芯片 SOC 通用硬件描述多个板子共用.dts单板独有硬件配置包含 dtsi写板子外设.dtb编译后的二进制设备树镜像uboot 加载运行设备树节点基础语法node_name地址 { 属性名 属性值; 子节点 { }; };核心属性1.compatible驱动匹配的钥匙格式厂商,设备名支持多个字符串优先匹配前面的platform_driver 的of_match_table里面写一模一样字符串就完成匹配触发 probe。2.reg描述寄存器物理地址 寄存器长度3.interrupts设备中断资源填写中断号、触发方式4.statusstatus okay; 设备启用status disabled; 设备禁用。怎么写一个设备树节点首先要找到和当前平台相关的所有设备树文件再根据自己的设备类型输入grep “xxxx(如gpiointerrupt)-controller” * -nR。在我们内核源码的目录下输入cd Documentation/devicetree/bindings/在这个目录下输入grep#xxxx如gpiointerrupt-cells*-nR | grep samsung厂家名进入pinctrl//samsung-pinctrl.txtgpio-controller用于标识该节点为GPIO控制器以及其对应的引脚组。#gpio-cells表示GPI0接口中所包含的引脚数量。由于使用的是通用的GPI0接口因此该数值必须设置为2。关于各个引脚的详细信息请参见下述的GPIO接口描述格式。例如gp×2 6 0其中[GPIO控制器的节点标识符] [GPIO控制器内的引脚编号] [标志位]。GPIO接口的相关参数如下引脚编号取值范围为0到7。标志位0表示高电平有效1表示低电平有效。四、Linux中断子系统框架中断控制器GIC负责对每个中断源进行编号、使能、屏蔽和优先级仲裁并根据配置将中断请求分类为普通中断IRQ或快速中断FIQ最终分发给ARM 核心ARM Core。ARM 核接收到中断信号后会立即保存当前程序的执行上下文现场然后跳转到异常处理向量表执行对应的中断服务例程ISR处理完后再恢复现场并返回被打断的程序继续执行。外部中断由外接的设备通过管脚产生管脚由gpio控制器控制gpio控制器向GIC提出中断请求。中断源说明典型场景GPIO 控制器外部引脚PIN1/PIN2/PIN3...产生的中断按键、传感器、外部模块触发Timer 定时器片上定时器溢出 / 匹配中断内核 tick、延时、PWMCombiner中断合并器汇聚多个次级中断ADC 转换完成、其他低速外设中断上半部和中断下半部中断上半部和下半部是将中断处理函数中需要做的事情分成两部分在不同的函数中完成。中断处理函数中完成的事情 是上半部(屏蔽外面的中断)而另外一个函数中完成的事情是下半部(不屏蔽外面的中断)。如果一个任务时间十分敏感将其放在上半部如果一个任务和硬件有关将其放在上半部如果一个任务要保证不被其他中断打断将其放在上半部其他所有任务考虑放在下半部下半部的实现机制1.软中断__do_softirq()内部逻辑1循环条件循环处理次数 MAX_SOFTIRQ_RESTART内核限制最大循环次数防止软中断饿死用户进程2依次调用所有被触发的软中断对应的处理函数。3处理完一轮后再次检查是否又产生新的软中断如果还有新触发的软中断并且还没到达次数上限 → 继续循环处理。如果次数耗尽仍然还有未处理完软中断调用wakeup_softirqd()唤醒 ksoftirqd 内核线程把剩下软中断交给内核线程后台执行。4软中断全部处理完毕流程结束。2.tasklettasklet 属于中断下半部机制用来把中断处理里耗时的工作挪到中断退出之后执行。tasklet 基于软中断 (softirq)实现属于原子上下文不能睡眠。常用api:struct tasklet_struct t; tasklet_init(t, my_tasklet_func, 123); //动态初始化代码运行时tasklet_schedule(my_tasklet); //调度触发最常用在中断处理函数里调用3.workqueueworkqueue 是中断下半部方案之一运 行在内核线程上下文允许睡眠、阻塞、互斥锁。是Linux 内核中唯一能在进程上下文中执行中断下半部的机制。为什么需要workqueue?Softirq 和 Tasklet 虽然快但有致命限制——不能睡眠。以下操作在中断上下文里做会直接触发内核 panic死机kmalloc(GFP_KERNEL) 内存分配失败时等待mutex_lock() 互斥锁竞争时阻塞copy_from_user() / copy_to_user()访问文件系统、网络发送某些场景Workqueue 把这些耗时、可能阻塞的操作搬到进程上下文里执行完美解决这个矛盾。常用api://提交工作到系统全局队列立刻排队执行 schedule_work(my_work);//延迟工作 delayed_work schedule_delayed_work(my_dwork, 2*HZ); //延迟2s执行//取消已提交但未执行的工作项并同步等待正在执行的工作项完成 cancel_delayed_sync(my_dwork);workqueue /tasklet/softirq 对比表项目softirqtaskletworkqueue上下文软中断 (原子)软中断 (原子)内核进程上下文是否允许睡眠禁止禁止允许 msleep、锁并发同类型 softirq 多核并行同一个 tasklet 串行不同 tasklet 可并行完全由线程调度可以并发触发接口raise_softirqtasklet_scheduleschedule_work适用场景内核底层驱动极少直接用短、快、不能休眠的下半部需要阻塞、延时、耗时任务选择指南中断来了 ↓ 是否需要极低的延迟 ──YES──→ Softirq内核核心子系统专用 ↓ NO 是否需要多 CPU 并行 ──YES──→ Tasklet ↓ NO 是否需要睡眠/阻塞/访问用户态 ──YES──→ Workqueue ↓ NO 简单驱动不想管线程 ──YES──→ Tasklet五、Linux Input子系统框架input子系统属于输入类子系统作用是感知外部事件输入统一管理按键、触摸屏、鼠标、陀螺仪、游戏手柄等输入设备把底层硬件差异屏蔽向上提供统一 /dev/input/eventX 字符设备接口。linux内核注册了多个handler驱动模块并通过input_handler_list链表维护1.底层驱动调用input_register_device(input_dev)注册设备2.input 核心遍历系统里所有已经注册的input_handler3.执行 handler 的.match匹配函数如果匹配成功内核分配一个input_handle结构体handle-dev input_dev; handle-handler input_handler;将handle-d_node加入input_dev-h_list蓝色链表将handle-h_node加入input_handler-h_list绿色链表4.调用 handler 的.connect函数evdev 的 connect 就会创建/dev/input/eventX字符设备这种设计让 Linux 的输入子系统极其灵活——驱动开发者只关心硬件怎么读事件处理开发者只关心数据怎么用两者通过核心层的合同机制自由组合。按键输入子系统1.原理图2.设备树节点3.实验现象输入子系统逻辑框架1首先向内核申请一个struct input_dev对象关键函数input_allocate_device()2填充 input_dev 属性3把设备挂到内核 Input 核心层的链表中。内核自动遍历所有 handlerevdev 匹配成功后调用evdev_connect()。→ 结果/dev/input/eventX 节点诞生4上报一个按键事件code KEY_1 或 KEY_2哪个键value 1按下或 0松开同步帧。把前面 report 的事件打包成一个完整帧 flush 到 evdev 的缓冲区唤醒阻塞在read()上的用户进程。→ 结果用户空间 read() 读到 struct input_event5注销从 Input 核心层注销设备evdev 会自动销毁对应的/dev/input/eventX节点。关键函数input_unregister_device释放input_allocate_device()申请的struct input_dev内存。关键函数input_free_device驱动代码里没有字符设备注册函数为啥在上层应用依旧可以调用orenread...呢evdev_connect流程图┌─────────────────┐ input_register_device() ┌─────────────────┐ │ input_dev │ ────────────────────────────────→ │ evdev_connect │ │ (GPIO按键) │ 匹配成功 │ │ └─────────────────┘ └────────┬────────┘ │ ┌───────────────────────────────────────────────────────────┘ │ ▼ ① 分配 minor 号决定是 event0/event1... │ ▼ ② 创建 evdev 结构体含等待队列、锁、client链表 │ ▼ ③ 建立 input_handle连接 input_dev ↔ evdev_handler │ ▼ ④ 初始化字符设备 cdev绑定 evdev_fops │ ▼ ⑤ device_add() → sysfs uevent → /dev/input/eventX 诞生 │ ▼ ⑥ 用户空间 open(/dev/input/event0) → 可以 read() 按键事件
返回列表