ARTICLE DETAIL

资讯详情

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

驱动二阶段——设备驱动

驱动二阶段——设备驱动 一、linux 字符设备驱动框架1.1linux基本驱动框架分类字符设备是流式的如键盘、串口。块设备是存储式的如硬盘、U盘。网络设备是通讯式的如网卡通过Socket访问。在/dev目录下使用ls -l查看设备的具体类型信息linux驱动模块的基本框架linux驱动的本质就是向应用层的程序提供设备的操作函数接口#include linux/init.h #include linux/module.h MODULE_LICENSE(GPL v2); // 驱动入口函数 static int hello_init(void) { return 0; } // 驱动出口函数 static void hello_exit(void) { return; } module_init(hello_init); // 告诉Linux这个驱动入口函数是那个装载入口函数 module_exit(hello_exit); // 告诉Linux这个驱动出口函数是那个装载出口函数1.2Linux 字符设备驱动框架通过container_of宏由struct cdev反向拿到struct mycdev私有结构体。1.3应用程序访问底层驱动过程struct inode是VFS层用来描述文件元数据权限、大小、设备号的。struct file是VFS层用来描述打开的文件实例当前偏移量、访问模式的。struct cdev是内核驱动层用来描述字符设备的。structfile_operations是存放对应字符函数的.c程序内部函数的名字的。在Linux中设备被抽象为文件存放在/dev目录下。每个驱动都注册了一个struct file_operations结构体其中包含了open、read、write等函数指针。驱动被加载时会先把自己的cdev地址和dev封装成一个节点挂载到全局的cdev_map哈希表字符设备映射表中。应用 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() 让驱动做初始化如申请硬件资源打开成功后系统会将cdev结构体相关信息cdev-ops填充到file结构体file-f_op中后续操作可直接通过file结构体调用底层接口无需重复查找哈希表提高操作效率。二、Linux Platform子系统框架字符驱动框架它的可移植性差,原因:驱动中包含了特定平台的硬件信息如果是其他平台硬件信息会有差异所以驱动无法直接使用。为了增加驱动的可扩展性和可移植性衍生了一种总线、设备、驱动的框架。将设备的信息从驱动中分离出来我们需要在操作系统中添加设备和驱动两部分。设备中包含是设备的信息(资源)驱动中包含的是操作设备函数接口。为了能让驱动最终能操作我们的硬件设备我们在驱动中必须获取设备的信息(资源)。设备和驱动都会注册到总线上当注册设备的时候会去寻找同名的驱动当注册驱动的时候也会去找同名的设备相互查找。一旦匹配成功操作系统就会自动调用驱动提供的probe函数。我们只需要在probe函数中使用操作系统提供的通用API获取硬件的信息即可。理解总线在实际的硬件上总线种类可以分成两大类1.平台总线platform bus(CPU核与硬件控制器之间的通信),挂载都是控制器设备2.边缘设备之间通信的总线,挂载是符合总线时序的外围设备i2c , spi ,usb , uart ....不同的边缘设备之间通信的总线总线时序是不一样的对于这些总线Linux 内核是单独实现的总线在操作系统中本质就是两个链表:挂载设备的链表和挂载驱动的链表。2.1基于总线写驱动的流程(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注册字符设备(可选)三、设备树设备的信息是针对于特定平台的如果我们在Linux内核中包含太多设备信息则Linux内核移植性就会变差。引入设备树之后设备的信息的描述不再是以代码的形式存在于 Linux内核源代码中,这种做法实际上是将设备的信息从Linux 内核中独立出来单独描述(用设备树语法规则来描述设备的信息)。compatible:驱动通过of_match_table中的compatible与设备树匹配reg 寄存器的起始地址 地址的长度statusdisable不注册okay注册设备名-gpios gpio控制器的标签名 管脚编号 标志interrupt-parent 中断控制器的标签名interrupts 1个或多个uint32的数字描述中断的信息 ,...;// #interrupt-cellsinterrupt-names 名称1,名称2;在开发板上面查看设备树节点: cd /proc/devicetree内核实际分配设备树节点/sys/bus/platform/devices四、Linux 上半部和下半部机制及特点中断控制器GIC负责对每个中断源进行编号、使能、屏蔽和优先级仲裁并根据配置将中断请求分类为普通中断IRQ或快速中断FIQ最终分发给ARM 核心ARM Core。ARM 核接收到中断信号后会立即保存当前程序的执行上下文现场然后跳转到异常处理向量表执行对应的中断服务例程ISR处理完后再恢复现场并返回被打断的程序继续执行。外部中断由外接的设备通过管脚产生管脚由gpio控制器控制gpio控制器向GIC提出中断请求。中断源说明典型场景GPIO 控制器外部引脚PIN1/PIN2/PIN3...产生的中断按键、传感器、外部模块触发Timer 定时器片上定时器溢出 / 匹配中断内核 tick、延时、PWMCombiner中断合并器汇聚多个次级中断ADC 转换完成、其他低速外设1--中断上半部和下半部将中断处理函数中需要做的事情分成两部分在不同的函数中完成。中断处理函数中完成的事情 是上半部(屏蔽外面的中断)。而另外一个函数中完成的事情是下半部(不屏蔽外面的中断)。打游戏(进程)----------(中断)吃饭:重要(上半部)吃虾:耗时时间长(下半部:进程上下文或中断上下文)----------女友电话(中断)上半部:中断处理函数不可以被打断下半部:可以被打断可以在中断上下文或进程上下文执行问:上半部什么时候执行?答:中断产生,就执行中断处理函数(上半部)问:下半部什么时候执行?答:在合适的时间点执行(一般是在上半部结束的时候开始触发下半部)问:什么事情放在上半部什么事情放在下半部?答:紧急的事情耗费时间不多我们放在上半部耗时的时间长或需要休眠我们放在下半部网卡(轮询或中断) 中断----- 网卡收到数据 (以太网头|ip头|tcp/udp头|用户数据)2--下半部的实现机制1-软中断2-tasklettasklet 翻 译 成 中 文 叫 做 ” 小 任 务 它 也 是基 于 软 中 断 实 现的 。Linux内核为了方便我们用软中断在软中断之上封装了一层接囗 让我们用起来更简单 也就是tasklet 。 所以 除非你性能要求别高 否则都应该用tasklet 。一个tasklet被tasklet_schedule调用过它的state状态就被设置为TASKLET_STATE_SCHED,接下来等待被执行。tasklet被执行完后它的状态才会被清除。如果重复调用一个tasklet,它如果没有被执行过是不会重复执行的。对应多次上半部打印信息和一次下半部tasklet打印信息3-workqueue注意:1软中断相关的函数接口Linux 内核并没有导出,所以在驱动中不可以直接使用2tasklet是基于软中断实现3workqueue它是进程上下文的下半部机制下半部机制上下文复杂度执行性能顺序执行保障软中断中断高好没有tasklet中断中中同类型不能同时执行workqueue进程低差没有3--中断上下文和进程上下文当内核代表某个进程执行时就说内核处于进程上下文。当 CPU 响应硬件中断如网卡收到数据包、磁盘 IO 完成、定时器到期时内核会跳转到对应的中断处理程序ISR, Interrupt Service Routine执行。此时内核处于中断上下文。中断上下文不能休眠。进程上下文睡眠当一个进程比如你的微信或浏览器调用sleep()时内核要做三件事把这个进程的内存状态寄存器、栈指针打包好存进它的task_struct进程描述符里。把这个进程A从“运行队列”里拿出来丢进“等待队列”。从运行队列里挑另一个进程B把它的task_struct里的数据恢复到 CPU 上。关键点task_struct就是一张“床位”。调度器Scheduler只认task_struct谁有这张床谁就能被存进去、被叫醒。中断处理函数只是回调函数没有床位task_struct。↑ 这个过程的前提是有一个进程A可以被挂起和恢复中断上下文睡眠ISR 调用 sleep() → ??? 谁来恢复 ISR 的执行↑ 没有进程关联 ISR调度器不知道该唤醒谁五、INPUT子系统框架1-input介绍input子系统的设计旨在实现对于各种输入设备的统一抽象和处理。其主要组成部分包括1、输入核心层Input core 2、输入事件层Input Events 3、设备驱动程序层Input DeviceDriverLinux内核中注册了多个事件处理handler驱动模块分别是evdev通用输入事件接口我们可以通过设备文件/dev/eventX或/dev/input/eventX读取事件mousedev一个早期的鼠标事件接口joydev专为游戏手柄、摇杆设计处理多轴、按键事件生成/dev/input/jsX设备节点input_leds管理输入设备的LED状态通过sysfs如 /sys/class/leds或ioctl控制LED开关。Linux内核中维护了一个名为input_handler_list的链表每个input_handler都会被注册到这个链表上而链表是通过类型为list_head的成员node串联起来的。1.底层驱动调用input_register_device(input_dev)注册设备2.input 核心遍历系统 input_handler_list里所有已经注册的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字符设备2-按键数据上报子系统1.引脚原理图2.设备树路径与节点设置1-设备树路径2-设备树节点3实验现象开发板查看eventX的指令cat /proc/bus/input/devices | grep -A 5 fs4412-key-input结果图3.输入子系统程序框架1.创建一个结构体用于保存input指针和GPIO引脚编号2.向内核申请一个struct input_dev对象驱动卸载时自动释放3.设置按键并注册input设备4.填入GPIO编号input设备指针5.上报数据给应用程序驱动代码里没有字符设备注册函数为啥在上层应用依旧可以调用orenread...呢?因为这个驱动不是传统意义上的“字符设备驱动”而是输入子系统驱动。open和read操作是由内核中通用的输入核心层evdev提供的而不是由你这个驱动提供的。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() 按键事件
返回列表