news 2026/8/25 13:47:28

驱动二阶段——设备驱动

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
驱动二阶段——设备驱动

一、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结构体,其中包含了openreadwrite等函数指针。驱动被加载时,会先把自己的cdev地址和dev封装成一个节点,挂载到全局的cdev_map哈希表(字符设备映射表)中。

应用 open("/dev/xxx") ↓(系统调用陷入内核) VFS(虚拟文件系统)根据文件名找到 inode,inode存放设备号 ↓ 内核拿设备号查询 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 = <寄存器的起始地址 地址的长度>
status:disable不注册,okay注册
设备名-gpios = <&gpio控制器的标签名 管脚编号 标志>
interrupt-parent = <&中断控制器的标签名>
interrupts = < 1个或多个uint32的数字描述中断的信息 >,<...>;// #interrupt-cells

interrupt-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、延时、PWM
Combiner中断合并器,汇聚多个次级中断ADC 转换完成、其他低速外设

1--中断上半部和下半部

将中断处理函数中需要做的事情,分成两部分,在不同的函数中完成。
中断处理函数中完成的事情 ,是上半部(屏蔽外面的中断)。
而另外一个函数中完成的事情,是下半部(不屏蔽外面的中断)。

打游戏(进程)<----------(中断)
吃饭:重要(上半部)
吃虾:耗时时间长(下半部:进程上下文或中断上下文)<----------女友电话(中断)

上半部:
中断处理函数,不可以被打断


下半部:
可以被打断,可以在中断上下文或进程上下文执行


问:上半部什么时候执行?
答:中断产生,就执行中断处理函数(上半部)

问:下半部什么时候执行?
答:在合适的时间点执行(一般是在上半部结束的时候,开始触发下半部)

问:什么事情放在上半部,什么事情放在下半部?
答:紧急的事情,耗费时间不多,我们放在上半部
耗时的时间长或需要休眠,我们放在下半部

网卡(轮询或中断) 中断-----> 网卡收到数据 (以太网头|ip头|tcp/udp头|用户数据)

2--下半部的实现机制

1-软中断

2-tasklet

tasklet 翻 译 成 中 文 叫 做 ” 小 任 务 " , 它 也 是基 于 软 中 断 实 现的 。

Linux内核为了方便我们用软中断,在软中断之上封装了一层接囗, 让我们用起来更简单, 也就是tasklet 。 所以, 除非你性能要求别高 ,否则都应该用tasklet 。

一个tasklet被tasklet_schedule调用过,它的state状态就被设置为TASKLET_STATE_SCHED,接下来等待被执行。tasklet被执行完后,它的状态才会被清除。如果重复调用一个tasklet,它如果没有被执行过,是不会重复执行的。(对应多次上半部打印信息和一次下半部tasklet打印信息)

3-workqueue

注意:
<1>软中断相关的函数接口,Linux 内核并没有导出,所以在驱动中不可以直接使用
<2>tasklet是基于软中断实现
<3>workqueue它是进程上下文的下半部机制

下半部机制上下文复杂度执行性能顺序执行保障
软中断中断没有
tasklet中断同类型不能同时执行
workqueue进程没有

3--中断上下文和进程上下文

当内核代表某个进程执行时,就说内核处于进程上下文。

当 CPU 响应硬件中断(如网卡收到数据包、磁盘 IO 完成、定时器到期)时,内核会跳转到对应的中断处理程序(ISR, Interrupt Service Routine)执行。此时内核处于中断上下文。

中断上下文不能休眠。

进程上下文睡眠:

当一个进程(比如你的微信或浏览器)调用sleep()时,内核要做三件事:

  1. 把这个进程的内存状态(寄存器、栈指针)打包好,存进它的task_struct(进程描述符)里。

  2. 把这个进程A从“运行队列”里拿出来,丢进“等待队列”。

  3. 从运行队列里挑另一个进程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 DeviceDriver)

Linux内核中注册了多个事件处理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_handler
3.执行 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.上报数据给应用程序

驱动代码里没有字符设备注册函数为啥在上层应用依旧可以调用oren,read...呢?

因为这个驱动不是传统意义上的“字符设备驱动”,而是输入子系统驱动openread操作是由内核中通用的输入核心层(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() 按键事件
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/25 13:44:59

PHP报名系统源码一出,人工报名直接变废纸

“基于 PHP - Mysql 框架的网络报名系统开发”, 这个资源所描述的是一个借助 PHP MySQL 框架搭建而成的网络报名系统, 它主要是用来处理等级考试&#xff08;就像计算机等级考试那样&#xff09;的在线报名流程的, 该系统的目的在于解决传统人工报名方式所带来的工作量巨大、容…

作者头像 李华
网站建设 2026/8/25 13:43:22

刘二大人深度学习实践笔记--梯度下降算法

目录 梯度下降算法&#xff08;GD&#xff09; 代码 随机梯度下降算法 &#xff08;SGD&#xff09; 代码 批量梯度下降算法&#xff08;MBGD&#xff09; 在寻找loss最低的权重时&#xff0c;之前使用穷举法&#xff0c;但是如果模型中有多个需要确定的参数&#xff0c;会…

作者头像 李华
网站建设 2026/8/25 13:38:35

Agent系列

开篇&#xff1a;行业不景气&#xff0c;一个普通程序员决定把会的东西全写出来&#xff08;附 42 篇系列目录&#xff09;CSDN 承接版 2026-08-23 同步发抖音/小红书图文&#xff08;卡片版&#xff09;&#xff1b;本篇是 CSDN 专栏的第一篇 四个免费专栏的开篇说明先交代…

作者头像 李华
网站建设 2026/8/25 13:33:36

跨设备剪贴板共享工程化笔记:可读代码与可复现页面并行推进

手机剪贴板与设备粘贴演示&#xff1a;从输入到历史记录的完整交互 写在前面 很多人第一次看到“跨设备剪贴板”这样的页面标题时&#xff0c;会自然联想到系统级剪贴板、设备发现、账号同步和真正的跨设备传输。不过&#xff0c;判断一个页面能做什么&#xff0c;不能只看标题…

作者头像 李华
网站建设 2026/8/25 13:24:57

模板小程序、SaaS商城和定制开发有什么区别?费用和交付边界对比

模板小程序、SaaS商城和定制开发有什么区别&#xff1f;费用和交付边界对比模板小程序、SaaS商城和定制开发的区别&#xff0c;主要在交付方式和维护责任。模板偏页面和快速搭建&#xff0c;SaaS商城偏成熟后台和年费制使用&#xff0c;定制开发偏复杂功能、源码、接口和私有化…

作者头像 李华
网站建设 2026/8/25 13:24:47

模板建站哪个平台更适合企业?页面效果、后台权限和后期修改对比

模板建站哪个平台更适合企业&#xff1f;页面效果、后台权限和后期修改对比模板建站平台适不适合企业&#xff0c;不能只看模板截图。企业官网至少要经得起三件事&#xff1a;页面效果能不能体现品牌&#xff0c;后台权限能不能分工&#xff0c;后期产品、案例、新闻和表单能不…

作者头像 李华