驱动第二周
一.Linux 字符设备驱动框架及应用程序访问底层驱动过程1.1Linux 字符设备驱动框架1.2应用程序访问底层驱动过程1.应用层打开设备文件时操作系统首先通过设备文件找到对应的inode结构体再根据inode结构体中记录的设备号到字符设备映射表中查找。2.若字符设备映射表中未找到对应设备号直接报错返回文件找不到若找到则可通过设备号和cdev结构体的关联找到对应的cdev结构体进而获取到其中的file_operation操作接口可操作底层设备。3.打开成功后系统会创建file结构体将cdev结构体相关信息填充到file结构体中后续操作可直接通过file结构体调用底层接口无需重复查找提高操作效率。补充机制说明inode这个结构体第一次打开这个设备时对应的i_cdev是没有值的发现对应的指针为空它就会通过设备号去查找去找对应的底下的cdev,找到之后会把对应的cdev的地址放到i_cdev中第二次open打开这个设备文件的时候对应的i_cdev已经记录了就可以通过i_cdev找到file这个结构体为啥也保存了file_operations的地址。因为我们对应打开文件之后一般都是读写那对于的read和write第一个参数都是文件描述符你通过文件描述就是找到的是file结构体从而找到file_operations不然的话你需要重新找一边大大提高了对应的效率。这些结构体都在VFS层虚拟文件系统VFS是一个抽象层其向上提供了统一的文件访问接口而向下则兼容了各种不同的文件系统。二.Linux Platform子系统框架字符驱动框架它的可移植性差,原因:驱动中包含了特定平台的硬件信息如果是其他平台硬件信息会有差异所以驱动无法直接使用。那么衍生了一种总线、设备、驱动的框架。目的就在于增加驱动的可扩展性和可移植性核心思想将设备的信息从驱动中分离出来我们需要在操作系统中添加设备和驱动两部分。设备中包含是设备的信息(资源)驱动中包含的是操作设备函数接口。为了能让驱动最终能操作我们的硬件设备我们在驱动中必须获取设备的信息(资源)。设备和驱动分离了驱动是如何获取具体设备信息的呢设备和驱动都会注册到总线上当注册设备的时候会去寻找同名的驱动当注册驱动的时候也会去找同名的设备。相互查找。一旦匹配成功操作系统就会自动调用驱动提供的probe函数。我们只需要在probe函数中使用操作系统提供的通用API获取硬件的信息即可。当我们的设备和驱动匹配成功之后就会出现设备中有驱动驱动中有设备也就是”你中有我我中有你”。但是驱动像一个“渣男”他可以“左拥右抱”包含多个设备而我们的设备“深情专一”只包含一个驱动。我们的总线种类可以分成两大类1.平台总线 (CPU核与硬件控制器之间的通信),挂载都是控制器设备platform bus2.边缘设备之间通信的总线,挂载是符合总线时序的外围设备i2c , spi ,usb , uart ...不同的边缘设备之间通信的总线总线时序是不一样的对于这些总线Linux 内核是单独实现的总线在操作系统中本质就是两个链表:挂载设备的链表和挂载驱动的链表。platform 总线维护设备和驱动两条链表注册时自动用名字/compatible 做 match每匹配成功一个设备就调用一次 probe多设备场景下同一个 probe 被调用多次驱动用全局指针数组pled把每次拿到的不同设备实例收集起来实现一份驱动代码管理多个同类设备。通过上文我们便可以知道驱动和设备分离了我们就需要写出两个.c文件一个就是我们的xxx_device.c设备文件另一个就是xxxx_driver.c驱动文件。(1)根据自己的设备来确定总线的类型platform bus / i2C bus /usb bus/....(2)根据总线的类型, 确定设备在总线上如何描述(结构体)struct platform_device{设备名;设备的资源通用的设备描述(struct device : platform_data-记录设备私有的信息)id_entry:当设备和驱动的id_table中某一个成员匹配上的时候这个成员就会记录id_table中匹配上的成员地址};struct resource{资源的开始资源的结束资源的类型: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获取匹配的硬件资源2注册字符设备(可选)三.设备树设备树本质是一个单独描述设备信息的文件有自己的语法规则原文件后缀为DTS编译后生成DTP文件设备信息不再以内核代码形式存在于Linux内核源码中提升了内核的通用性理论上只需更换设备树文件内核就可以适配不同平台实际仍会存在部分平台差异。设备树采用树形结构最外层是根节点节点下可包含属性信息和子节点每个节点代表一个设备属性用于描述设备的具体信息属性分为两类标准属性有固定属性名称如compatible、reg所有平台通用需按规则编写和自定义属性为单个设备单独定义可自定义名称和值。那我们应该如何操作写一个自己的设备树节点呢1第一步要找到和当前平台相关的所有设备树文件.dts、.dtsi文件其中.dtsi可类比为.h头文件.dts可类比为.c文件编译后生成.dtb二进制设备树文件单独拷贝出来后仅在拷贝的文件范围内查找信息避免内核中大量无关设备树文件的干扰。2再根据自己设备类型,再文件夹内输入grep “xxxx(如gpiointerrupt)-controller” * -nR。例3我们根据查到的信息输入vi exynos4x12-pinctrl.dtsi找到我们自己需要的节点名例4在我们内核源码的目录下输入cd Documentation/devicetree/bindings/在这个目录下输入grep#xxxx如gpiointerrupt-cells*-nR | grep samsung厂家名例5输入vi pinctrl/samsung-pinctrl.txt 29。这样我们就可以通过查询资料来编写我们自己的设备树节点。6编写自己对设备树节点在内核源码的目录下cd arch/arm/boot/dts然后vi exynos4412-fs4412.dts例四.Linux 中断子系统框架中断控制器GIC )负责对每个中断源进行编号、使能、屏蔽和优先级仲裁并根据配置将中断请求分类为普通中断IRQ或快速中断FIQ最终分发给ARM 核心ARM Core。ARM 核接收到中断信号后会立即保存当前程序的执行上下文现场然后跳转到异常处理向量表执行对应的中断服务例程ISR处理完后再恢复现场并返回被打断的程序继续执行。4.1中断上半部和下半部中断上半部和下半部是将中断处理函数中需要做的事情分成两部分在不同的函数中完成。中断处理函数中完成的事情 是上半部(屏蔽外面的中断)。而另外一个函数中完成的事情是下半部(不屏蔽外面的中断)。上半部由中断产生,就执行中断处理函数(上半部)下半部在合适的时间点执行(一般是在上半部结束的时候开始触发下半部)紧急的事情耗费时间不多我们放在上半部耗时的时间长或需要休眠我们放在下半部下半部的实现机制1软中断2taklet基于软中断实现3workqueue下半部机制上下文复杂度执行性能顺序执行保障软中断中断高(需要自己确保软中断的执行顺序及锁机制)好(全部自己实现便于调优)没有tasklet中断中(提供了简单的接口来使用软中断)中同类型不能同时执行workqueue进程低(在进程上下文中运行与写用户程序差不多)差没有(和进程上下文一样被调度)4.2进程上下文和中断上下文当内核代表某个进程执行时就说内核处于进程上下文。当 CPU 响应硬件中断如网卡收到数据包、磁盘 IO 完成、定时器到期时内核会跳转到对应的中断处理程序ISR, Interrupt Service Routine执行。此时内核处于中断上下文。中断上下文不能休眠。进程上下文睡眠进程A 调用 sleep() → 放入等待队列 → 调度器选择进程B → 进程A 被重新调度后恢复执行↑ 这个过程的前提是有一个进程A可以被挂起和恢复中断上下文睡眠ISR 调用 sleep() → ??? 谁来恢复 ISR 的执行↑ 没有进程关联 ISR调度器不知道该唤醒谁五.Linux Input子系统框架input子系统属于输入类子系统作用是感知外部事件输入向上层应用提供标准化的输入接口统一各类输入设备的访问方式降低上层应用开发和驱动开发的难度。1.核心层input core是Linux系统内核中已经预先实现好的核心部分负责input设备和input事件处理程序的匹配工作。2.事件处理层input handler对各类输入外设进行分类不同种类的外设对应不同的事件处理程序常见的默认事件处理程序包括通用事件接口evdev鼠标事件接口游戏手柄摇杆接口joydevLED状态管理接口。3.设备驱动层是开发input子系统驱动时需要编写的部分对应具体的外设硬件需要结合对应外设的基础驱动子系统如按键需结合GPIO子系统、触摸屏需结合I2C子系统、USB键盘/鼠标需结合USB子系统、部分键盘需结合SPI子系统共同实现。核心链表与结构体1.内核中维护两条链表input_handler_list链接所有已注册的事件处理程序每个节点为input_handler结构体包含事件处理名称、回调函数等成员、input_dev_list链接所有已注册的输入外设每个节点为input_dev结构体描述外设的能力属性。2.当新注册一个input_dev时核心层会遍历input_handler_list查找能匹配的事件处理程序当新注册一个input_handler时核心层会遍历input_dev_list查找能匹配的外设设备。3.二者匹配成功后会调用input_handler的connect回调函数生成input_handle结构体该结构体同时关联匹配的input_dev和input_handler将两者通过链表连接起来完成配对。自己编写的按键输入子系统1原理图2设备树节点3编写驱动代码并在板子上测试我们的input设备注册之后与handler进行匹配怎么知道我们的设备与那个handler匹配了呢在代码中我们设置了位图。这样就设置他的功能我们handler也记录了自己的功能这样我们dev与handler就会匹配。我们自己写的驱动代码里没有字符设备注册函数为啥在上层应用依旧可以调用orenread...呢那是因为在我们connect函数中设置了。你的驱动: input_register_device()│▼Input Core: input_attach_handler()│ 匹配到 evdev▼evdev_handler-connect() evdev_connect()│├── 1. 分配次设备号 (input_get_new_minor)├── 2. 分配 evdev 结构体 (kzalloc)├── 3. 建立 handle 绑定 (input_register_handle)├── 4. cdev_init cdev_add ← 注册字符设备└── 5. device_add ← 创建 /dev/input/eventX 节点