知识体系构建

Linux驱动开发
├── 驱动基础
│ ├── 内核模块(加载/卸载)
│ ├── 字符设备(file_operations)
│ └── 设备号管理(主/次设备号)
├── 硬件交互
│ ├── 设备树(语法、绑定)
│ ├── I2C子系统(设备树、驱动)
│ ├── SPI子系统(数据传输)
│ └── GPIO控制(中断、方向)
├── 内核机制
│ ├── 中断处理(顶/底半部)
│ ├── 同步机制(锁、信号量)
│ └── 内存管理(DMA、kmalloc)
├── 高级框架
│ ├── IIO(传感器驱动)
│ ├── Input子系统(事件上报)
│ └── PWM(脉宽调制)
├── 调试与优化
│ ├── 工具链(dmesg, i2c-tools)
│ ├── 性能优化(DMA、中断合并)
│ └── 用户态接口(sysfs、ioctl)
└── 开发流程
├── 硬件分析 → 设备树 → 驱动开发 → 测试验证
└── 扩展知识(电源管理、安全)

开发环境搭建

方案一:clion

比较复杂,Linux驱动前期还是用vscode

方案二:vscode

ssh远程连接本地的虚拟机

知识准备

为什么Linux学起来很难——Linux下全tm是文件,封装封装还是封装

这俩句话是贯穿整个Linux系统的真言,在系统开发中我们会发现我们至始至终都是在写文件操控文件,怎么理解呢编译内核模块是将他编程.ko文件,我们实现驱动的挂载也是通过打开文件读取信息再放在内核中

封装,我认为是Linux学起来困难的最主要原因,由于Linux是一个开源系统,全世界的程序员为它增砖添瓦,不用说我们自己写的代码有时会对其逻辑挠头,更何况这个全世界顶尖程序员智慧产物,各种概念会让人摸不清头脑,但归根结底代码的方向永远是——偷懒,就是提高各种效率

linux下程序编译

不同于我们在windows下用ide写c语言,由于ide强大的功能我们编译代码是一站式编译,但在Linux下我们需要清楚了解这其中的过程

Linux中使用的是glibc,C语言的标准库,他提供了<stdio.h>这类头文件

预处理——编译——汇编——链接

注意链接是将依赖链接到一起,将机器码文件(.o)链接成一个可执行的文件,这个文件elf文件格式(通用执行文件)

链接有动态库和静态库之分,静态库可以独立运行而不需要其他依赖,但更占用空间,Linux中默认动态库

文件系统

image-20250102161121916

存储设备文件系统

我们首先想到的通常是Windows 下的FAT32、NTFS、exFAT 以及Linux 下常用的ext2、ext3 和ext4 的类型格式。

这些文件系统都是为了解决如何高效管理存储器空间的问题而诞生的。

伪文件系统

Linux 内核还提供了procfs、sysfs 和devfs 等伪文件系统。
伪文件系统存在于内存中,通常不占用硬盘空间,它以文件的形式,向用户提供了访问系统内核数据的接口。用户和应用程序可以通过访问这些数据接口,得到系统的信息,而且内核允许用户修改内核的某些参数。

虚拟文件系统

Linux 内核包含了文件管理子系统组件,它主要实现了虚拟文件系统(Virtual File System,VFS),虚拟文件系统屏蔽了各种硬件上的差异以及具体实现的细节,为所有的硬件设备提供统一的接口,从而达到设备无关性的目的,同时文件管理系统还为应用层提供统一的APi接口。为了使不同的文件系统共存,Linux 内核在用户层与具体文件系统之前增加了虚拟文件系统中间层,它对复杂的系统进行抽象化,对用户提供了统一的文件操作接口。无论是ext2/3/4、FAT32、NTFS 存储的文件,还是/proc、/sys 提供的信息还是硬件设备,无论内容是在本地还是网络上,都使用一样的open、read、write 来访问,使得“一切皆文件”的理念被实现,这也正是软件中间层的魅力。

是什么?内核模块

内核体系结构有:微内核(Micro Kernel) 、宏内核(Monolithic Kernel) 混合内核(HybridKernel) 等

1.windows和鸿蒙是微内核,他们的驱动是不被包含到内核中的,你去动驱动是不影响核心功能(ipc、进程管理啥的)

2.linux是宏内核,驱动是要被编到内核中,但在开发中每次都重新编译一边内核不太现实,所以引入内核模块的概念,提供一种动态的能力

3.我们在开发内核模块时,是将其先编译成.o文件,再链接到内核中,也就是说他可以被单独编译但不能独立运行

内核模块经过编译最终形成.ko 为后缀的ELF 文件

  • 1.ELF格式

可以使用readelf 工具查看elf 文件的头部详细信息。

  • 2.内核模块安装和卸载的过程
  • 3.内核导出符号的过程

驱动开发思路

  1. 确定设备分类
    • 字符设备:如传感器、GPIO等需要字节流操作的设备 37
    • 块设备:如硬盘、U盘等需要块数据读写的设备 1
    • 网络设备:如网卡、WiFi模块等涉及数据包传输的设备 8
  2. 硬件总线类型判断
    • 标准总线设备(如I2C、SPI、PCIe):优先使用内核提供的总线驱动框架 69
    • 非总线设备(如GPIO):选择字符设备框架,结合内核GPIO子系统API 3

Linux驱动框架分类体系(2025版)


一、基础设备类型框架

框架类型 内核路径 核心结构体 典型应用场景
字符设备驱动 drivers/char/ struct file_operations 传感器、GPIO控制
块设备驱动 drivers/block/ struct block_device_ops SSD、机械硬盘
网络设备驱动 drivers/net/ struct net_device_ops 以太网卡、WiFi模块

二、总线型驱动框架

总线类型 注册接口 匹配机制 硬件示例
I2C总线 i2c_register_driver() of_device_id+设备树 EEPROM、温度传感器
SPI总线 spi_register_driver() SPI设备ID匹配 Flash存储器、TFT屏幕
USB总线 usb_register_driver() 接口类/厂商ID HID设备、USB摄像头
PCI/PCIe总线 pci_register_driver() PCI厂商/设备ID 显卡、高速网卡
ACPI总线 acpi_bus_register_driver() ACPI设备路径匹配 电源管理设备

三、子系统级驱动框架

子系统名称 核心组件 关键API 应用案例
输入子系统 drivers/input/ input_register_device() 键盘、触摸屏
帧缓冲子系统 drivers/video/ fb_ops结构体 LCD控制器驱动
声音子系统 sound/soc/ snd_soc_component_driver 音频编解码器
DMA引擎框架 drivers/dma/ dma_device结构 高速数据传输
IIO子系统 drivers/iio/ iio_chan_spec定义 加速度计、光传感器

四、设备树(DT)关联框架

框架类型 设备树节点特征 关键函数 开发优势
Platform框架 compatible属性 of_match_table匹配 硬件抽象与驱动分离
GPIO子系统 gpio-controller定义 gpiod_get()获取引脚 跨平台引脚管理
Pinctrl子系统 pinctrl-0属性定义 pinctrl_lookup_state() 引脚复用配置自动化
Clock框架 clocks属性链 clk_get()获取时钟 统一时钟树管理
Regulator框架 regulator节点 regulator_get() 电源管理统一接口

五、高级功能框架

框架名称 技术特性 核心机制 典型应用
DMA-BUF框架 内存共享机制 dma_buf_export() GPU与VPU数据交换
V4L2框架 视频采集标准 video_device注册 摄像头驱动开发
DRM/KMS框架 图形渲染管理 drm_driver结构体 现代显卡驱动
NTB框架 跨节点通信 ntb_register_device() 服务器多机互联
RPMSG框架 多核间通信 rpmsg_send() SoC异构核通信

六、特殊设备框架

框架类型 设备特征 实现要点 代表驱动
MISC驱动 主设备号10 miscdevice结构体 看门狗、随机数生成器
UIO框架 用户空间I/O uio_info注册 FPGA加速器控制
HWMON框架 硬件监控 hwmon_device_register() 温度传感器监控
LED框架 灯光控制 led_classdev_register() LED指示灯控制
WATCHDOG框架 系统看门狗 watchdog_device结构 硬件级系统复位

七、虚拟化驱动框架

框架名称 虚拟化类型 核心接口 应用场景
VFIO框架 设备直通 vfio_pci_core_register() 云计算GPU直通
Virtio框架 半虚拟化 virtio_device_ops 虚拟网卡、块设备
Xen PV驱动 Xen虚拟化 xenbus_driver注册 云服务器虚拟设备
KVM设备模拟 硬件辅助虚拟化 kvm_ioctl() 嵌入式虚拟化平台

not helloworld ,hellomodulexiandao

1.大致流程可以总结如下:
• 实现入口函数xxx_init() 和卸载函数xxx_exit()
• 申请设备号register_chrdev_region()
• 初始化字符设备,cdev_init 函数、cdev_add 函数
• 硬件初始化,如时钟寄存器配置使能,GPIO 设置为输入输出模式等。
• 构建file_operation 结构体内容,实现硬件各个相关的操作
• 在终端上使用mknod 根据设备号来进行创建设备文件(节点) 或者自动创建(驱动使用
class_create 创建设备类、在类的下面device_create 创建设备节点)

2.但我们会发现一个问题简单的驱动会将所有的硬件信息放在代码中,如果修改驱动或二次开发会遇到问题代码重复修改麻烦,所以提出设备驱动模型分层的概念——驱动代码分成设备与驱动,设备负责提供硬件资源而驱动代码负责去使用这些设备提供的硬件资源,并由总线将它们联系起来。

image-20250113163712096

• 设备(device) :挂载在某个总线的物理设备;
• 驱动(driver) :与特定设备相关的软件,负责初始化该设备以提供一些操作该设备的操作方式;
• 总线(bus) :负责管理挂载对应总线的设备以及驱动;
• 类(class) :对于具有相同功能的设备,归结到一种类别,进行分类管理;

3./sys——sysfs 虚拟文件系统

/sys/bus 目录下的每个子目录都是注册好了的总线类型。这里是设备按照总线类型分层放置的目
录结构,每个子目录(总线类型) 下包含两个子目录——devices 和drivers 文件夹;其中devices 下是
该总线类型下的所有设备,而这些设备都是符号链接,它们分别指向真正的设备(/sys/devices/下);
如下图bus 下的usb 总线中的device 则是Devices 目录下/pci()/dev 0:10/usb2 的符号链接。而drivers
下是所有注册在这个总线上的驱动,每个driver 子目录下是一些可以观察和修改的driver 参数。
/sys/devices 目录下是全局设备结构体系,包含所有被发现的注册在各种总线上的各种物理设备。
一般来说,所有的物理设备都按其在总线上的拓扑结构来显示。/sys/devices 是内核对系统中所有
设备的分层次表达模型,也是/sys 文件系统管理设备的最重要的目录结构。
/sys/class 目录下则是包含所有注册在kernel 里面的设备类型,这是按照设备功能分类的设备模型,
我们知道每种设备都具有自己特定的功能,比如:鼠标的功能是作为人机交互的输入,按照设备
功能分类无论它挂载在哪条总线上都是归类到/sys/class/input 下。

image-20250113164809397

在设备模型框架下,设备驱动的开发是一件很简单的事情:先分配一个struct device 类型的变量,填充必要的信息后,把它注册到对应总线中;然后创建一个struct device_driver 类型,填充必要的
信息后注册。在合适的时机(驱动和设备匹配时),就调用驱动的probe、release 等回调函数。另外,在实际编程中较少直接使用device 和device_drivere,而是在它们上面加一层封装,比如platform device 。

Linux驱动如何学习和理解,借用大神总结的话,在此记录下,我初次看时感觉很受用,理解的很到位:

上层是文件系统和应用,中层是Linux内核,下层是底层硬件,Linux驱动是介于文件系统和底层硬件之间的,是嵌入到内核中的程序,应用是嵌入到文件系统中的程序,比如Android APP。

对上:Linux设备驱动给上层提供调用的接口;

对中:Linux设备驱动要注册到内核中,标准说法是 挂载在总线上;

对下:直接操作硬件,如GPIO、IIC、SPI、PWM等;

以上三个,Linux内核都提供了大量的接口函数、结构体,其实Linux驱动,就是掌握了这些东西怎么用,适应到自己要写的驱动程序中。

Linux 内核中处处体现面向对象的设计思想,为了统一形形色色的设备,Linux 系统将设备分别抽
象为struct cdev, struct block_device,struct net_devce 三个对象,具体的设备都可以包含着三种对象
从而继承和三种对象属性和操作,并通过各自的对象添加到相应的驱动模型中,从而进行统一的
管理和操作

Linux 内核中将字符设备抽象成一个具体的数据结构(struct cdev), 我们可以理解为字符设备对象,
cdev 记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口
(file_operations),在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一
个文件(设备节点) 绑定对象的cdev,当我们对这个文件进行读写操作时,就可以通过虚拟文件
系统,在内核中找到这个对象及其操作接口,从而控制设备。

***********

学习课程安排

1 最简单的内核模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include<linux/module.h>

/*
static:Linux太大了,防止命名冲突
int、void:这是一种标准,在初始化过程中我们更希望通过退出的状态码来了解,在退出中我们更希望释放资源而不是处理错误
__init、__exit:是宏,标记函数告诉内核只需要只需要调用一次
*/
static int __init hello_init(void){
printk(KERN_INFO "HELLO\n"); //了解一下printk函数,https://blog.csdn.net/wwwlyj123321/article/details/88422640
return 0;
}
static void __exit hello_exit(void){
printk(KERN_INFO "BYE\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fff");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

/*
安装头文件
fxh@fxh-virtual-machine:~/drive/nose/study$ uname -a
Linux fxh-virtual-machine 5.15.0-130-generic #140~20.04.1-Ubuntu SMP Wed Dec 18 21:35:34 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

fxh@fxh-virtual-machine:~/drive/nose/study$ sudo apt-get install linux-headers-5.15.0-130
[sudo] fxh 的密码:
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
注意,根据正则表达式 'linux-headers-5.15.0-130' 选中了 'linux-headers-5.15.0-130-generic'
linux-headers-5.15.0-130-generic 已经是最新版 (5.15.0-130.140~20.04.1)。
linux-headers-5.15.0-130-generic 已设置为手动安装。
升级了 0 个软件包,新安装了 0 个软件包,要卸载 0 个软件包,有 186 个软件包未被升级。
*/

/*
insmod 加载模块
lsmod 查看模块,名字为makefile中的
dmesg 查看内核打印
remod 移除模块
*/

/*
由于Linux下皆是文件,所以通过文件系统用户可以直接查看
每加载一个模块就会在sys/module/下生成对应的文件夹
*/

2 内核模块传参与符号共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
* @Author: Fanxiaominghang 2309200691@qq.com
* @Date: 2025-02-06 16:11:36
* @LastEditors: Fanxiaominghang 2309200691@qq.com
* @LastEditTime: 2025-02-06 22:37:29
* @FilePath: /nose/study/hello.c
* @Description:
*/
#include<linux/module.h>
#include<linux/moduleparam.h>

#define CNT 1
static int cnt=CNT;
static char *fff="hi,fmh\n";
static int arr[]={1,2,3,4,5,6};
static int nums=sizeof(arr)/sizeof(int);
/*变量传参数
直接变量=要改的值
sudo insmod hello.ko cnt=3 fff=fuck arr=444,4,5
在sys/module/hello下会有一个parameters文件夹
*/
module_param(cnt,int,S_IRUGO);
module_param(fff,charp,S_IRUGO);
module_param_array(arr,int,&nums,S_IRUGO);

/*符号导出
分着插入
有导出时候,会生成一个Module.symvers文件,可以查看导出符号
也可以在/proc/kallsyms下查看内核符号,但需要过滤
在sys/module/hello下会有一个holders文件夹
*/
EXPORT_SYMBOL(fff);

static int __init hello_init(void){
//printk(KERN_INFO "\n");
printk(KERN_INFO "HELLO\n");
printk(KERN_INFO "%s",fff);
int i;
for ( i = 0; i < cnt; i++)
{
printk(KERN_INFO "%d:%s",i,fff);
}
for ( i = 0; i < 6; i++)
{
printk(KERN_INFO "%d",arr[i]);
}
printk(KERN_INFO "nums:%d\n",nums);
return 0;
}
static void __exit hello_exit(void){
printk(KERN_INFO "BYE\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fff");
MODULE_DESCRIPTION("A simple Linux module");
MODULE_VERSION("0.1");

/*
安装头文件
fxh@fxh-virtual-machine:~/drive/nose/study$ uname -a
Linux fxh-virtual-machine 5.15.0-130-generic #140~20.04.1-Ubuntu SMP Wed Dec 18 21:35:34 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

fxh@fxh-virtual-machine:~/drive/nose/study$ sudo apt-get install linux-headers-5.15.0-130
[sudo] fxh 的密码:
正在读取软件包列表... 完成
正在分析软件包的依赖关系树
正在读取状态信息... 完成
注意,根据正则表达式 'linux-headers-5.15.0-130' 选中了 'linux-headers-5.15.0-130-generic'
linux-headers-5.15.0-130-generic 已经是最新版 (5.15.0-130.140~20.04.1)。
linux-headers-5.15.0-130-generic 已设置为手动安装。
升级了 0 个软件包,新安装了 0 个软件包,要卸载 0 个软件包,有 186 个软件包未被升级。
*/

/*
insmod 加载模块
lsmod 查看模块,名字为makefile中的
dmesg 查看内核打印
rmmod 移除模块
*/

/*
由于Linux下皆是文件,所以通过文件系统用户可以直接查看
每加载一个模块就会在sys/module/下生成对应的文件夹
*/

3 设备号

前两节只是简单框架,但在实际开发中我们用户空间是通过设备文件实现操作

在需要文件时设备号就是必须的

我们可以在/proc/devises查看设备

主设备号对应驱动程序

4驱动基本框架

5设备树

基本语法

从此以后 ARM社区就引入了 PowerPC等架构已经采用的设备树 (Flattened Device Tree),将这些描述板级硬件信息的内容都从 Linux内中分离开来,用一个专属的文件格式来描
述,这个专属的文件就叫做设备树,文件扩展名为 .dts。一个 SOC可以作出很多不同的板子,
这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他
的 .dts文件直接引用这个通用文件即可,这个通用文件就是 .dtsi文件,类似于 C语言中的头文
件。一般 .dts描述板级信息 (也就是开发板上有哪些 IIC设备、 SPI设备等 ),,.dtsi描述 SOC级信
息 (也就是 SOC有几个 CPU、主频是多少、各个外设控制器信息等 )。
这个就是设备树的由来,简而言之就是, Linux内核中 ARM架构下有太多的冗余的垃圾
板级信息文件,导致 linus震怒,然后 ARM社区引入了设备树。

因此在 .dts设
备树文件中,可以通过“ “#include”来引用 .h、 .dtsi和 .dts文件。只是,我们在编写设备树头文
件的时候最好选择 .dtsi后缀。
一般 .dtsi文件用于描述 SOC的内部外设信息,比如 CPU架构、主频、外设寄存器地址范
围,比如 UART、 IIC等等。 比如 rk3568.dtsi就是描述 RK3568芯片本身的外设信息, 内容如
下: