一、驱动程序基础概念
驱动程序定义
- 驱动程序是用于操作硬件的软件程序。
- 驱动程序使设备能够被操作系统控制和使用。
驱动程序与操作系统的关系
- 早期程序直接操作硬件,无操作系统参与。
- 当前任务是将裸机程序整合进操作系统中作为驱动模块。
- 驱动程序属于操作系统内核的一部分,提供设备管理功能。
计算机系统结构演变
- 早期硬件复杂、体积大,软件简单。
- 集成电路发展使硬件小型化、集成度提高。
- 现代系统中软件规模和复杂性远超硬件。
二、设备驱动架构与分类
软件层次结构
- 操作系统软件分为内核、根文件系统和应用层三部分。
- 内核位于最底层,直接管理硬件资源。
- Bootloader为一次性程序,启动内核后即退出运行。
设备与驱动对应关系
- 每个硬件设备需有对应的驱动程序才能工作。
- 驱动程序与硬件设备为一一对应关系。
- 数据流向因设备类型而异:LED为控制输出,按键为状态读取,UART为双向通信。
驱动程序接口统一机制
- Linux系统遵循“一切皆文件”原则。
- 应用层通过标准文件操作接口(open、read、write、close)访问硬件。
- 实际操作由内核将文件操作转换为具体驱动函数调用。
设备节点与路径
- 所有设备在/dev目录下以设备节点形式存在。
- 设备节点表现为特殊文件,用于用户空间与内核交互。
- 设备节点名称如ttyUSB0代表具体硬件设备。

三、设备号与设备管理
设备号结构
- 设备号为32位无符号整数,分为主设备号(高12位)和次设备号(低20位)。
- 主设备号标识设备类别,次设备号标识同类设备中的具体实例。
- 设备号用于内核内部唯一标识和管理驱动程序。
设备号管理机制
- 字符设备与块设备的设备号独立管理,允许相同编号存在于不同类型中。(cat /proc/devices 可以查看)
- 网络设备不使用设备号,而是通过设备名进行管理。
- 设备号与设备节点名称通过系统维护的映射表关联。
驱动(程序)分类
- 字符设备驱动:按字节流顺序访问,典型如LED、按键、串口等。(百分之九十多)
- 块设备驱动:按固定大小的数据块随机访问,典型如磁盘、存储设备。
- 网络设备驱动:处理网络协议栈,支持复杂数据封装与解析,如网卡、Wi-Fi模块。

四、系统调用与函数注册机制
系统调用流程
- 用户层调用open、read等函数时,经C库封装后触发系统调用。
- 系统调用通过SWI指令陷入内核态,切换至SVC模式执行。
- 不同系统调用由指令后缀数字区分,参数通过寄存器或内存传递。
文件描述符机制
- open成功后返回文件描述符(如3、4),供后续read/write/close使用。
- 内核维护进程级文件描述符表,关联设备ID与具体驱动操作函数。
- read/write等操作基于文件描述符查找对应驱动函数执行。
驱动注册要素
- 必须实现基本操作函数(open、read、write、release等)。
- 需向内核注册驱动,使其知晓该驱动存在。
- 需建立设备号与设备节点名称的映射关系。
五、内核开发环境与工具
内核编程限制
- 驱动代码运行于内核空间,无法使用标准库头文件(如stdio.h)。
- 开发依赖内核源码中的头文件,无man手册可用。
- 函数查阅需通过源码定位声明与定义。
- 使用ctags -R生成符号索引文件tags,记录函数、结构体等符号位置。
- Vim中通过Ctrl+]跳转到符号定义,Ctrl+o返回原位置。
- 支持跨文件函数追踪,提升内核代码阅读效率。
- 每个项目目录需独立生成tags文件。
六、模块初始化与编译机制
模块加载与卸载
- module_init宏标记驱动初始化函数,内核启动时自动执行。
- module_exit宏标记清理函数,关机或卸载模块时调用。
- 这两个宏构成驱动模块的入口与出口。
宏展开与链接机制
- module_init宏最终展开为__initcall函数指针声明。
- 该指针被放置在特定链接段(.initcall6.init)中。
- 内核启动时遍历该段所有函数指针并依次执行,完成模块初始化。
数据类型命名规范
- u32表示32位无符号整数,s16表示16位有符号短整型。
- 类型命名体现符号性与位宽,便于开发者识别。
- 此类命名广泛用于内核代码及嵌入式开发中。
七、驱动开发实践准备
结构体与函数定义
- 使用struct file_operations定义操作函数集合。
- 该结构体包含open、read、write、release等函数指针成员。
- 驱动需实现这些函数并将地址赋值给对应指针。
八、驱动程序基础架构
设备号管理
- 使用主设备号和次设备号组合生成设备号,主设备号通常取255或以下以保持兼容性。
- 通过MAKEDEV宏将主次设备号合并为完整的设备号。
文件操作接口定义
- 定义file_operations结构体,包含open、read、write、release等函数指针。
- 函数参数理解:文件指针、缓冲区、数据长度、偏移量等。
内核打印与浮点运算限制
- 使用printk进行内核空间打印,用法类似printf。
- 避免在内核中使用浮点数运算,因老式处理器可能不支持硬件浮点且影响效率。
九、字符设备注册流程
cdev结构体初始化
- 定义struct cdev结构体作为字符设备的核心数据结构。
- 包含设备号、操作函数集、引用计数和内核对象等成员。
设备注册接口调用
- 使用cdev_init初始化cdev结构体,关联file_operations。
- 通过cdev_add将设备添加到系统,register_chrdev_region注册设备号。
资源清理与注销
- 在模块卸载时调用unregister_chrdev_region释放设备号。
- 使用cdev_del删除cdev结构体,确保资源正确释放。
三、应用程序交互与设备节点
设备节点手动创建
- 使用mknod命令在/dev目录下创建设备节点。
- 指定设备名称、类型(c表示字符设备)、主设备号和次设备号。
应用程序开发
- 编写简单应用程序通过open、read、write、close系统调用与驱动交互。
- 应用程序编译后在开发板上运行验证驱动功能。
根文件系统挂载问题
- 驱动在内核启动时加载,但/dev目录在根文件系统挂载后才存在。
- 因此需要手动创建设备节点,无法自动出现在/dev目录中。
十、错误处理与代码优化
返回值检查
- 对内核API调用返回值进行检查,及时处理错误情况。
- 错误码如-ENOMEM表示内存不足,-ENODEV表示无此设备等。
goto错误处理模式
- 采用goto语句集中处理错误退出,避免代码冗余。
- 按相反顺序撤销已分配的资源,确保正确清理。
代码简洁性考虑
- goto方式使错误处理代码更紧凑,便于维护和扩展。
- 尽管会牺牲部分精确错误信息,但提高了整体代码质量。
驱动程序编写完成后
1. 编译生成新的zImage
2. 拷贝zImage到tftp服务目录下
3. 重新启动(开发板)内核
4. 在开发板执行 mknod /dev/demo c 255 0 创建一个设备节点
mknod (手动)创建设备节点
/dev/demo 设备节点名
c 字符设备
255 主设备号
0 次设备号
5. 编写并编译应用程序 ----因为开发板的根目录挂载在ubuntu的nfs下,所以 在ubuntu的nfs下编译的程序在开发板的根目录下可以直接运行
6. 在开发板端运行应用程序调用驱动接口
十一、硬件控制与地址映射
物理地址访问
- 用户空间地址为虚拟地址,内核需获取实际物理地址才能操作硬件寄存器。
- 使用ioremap将物理地址映射到内核可访问的虚拟地址空间。
寄存器操作
- 定义硬件寄存器的物理地址,通过指针操作实现读写。
- 包括引脚功能配置、方向设置和数据寄存器操作。
IO重映射管理
- ioremap用于建立物理地址到虚拟地址的映射。
- iounmap在模块卸载时解除映射,释放资源。
十二、用户空间数据安全访问
空间隔离原则
- 内核空间与用户空间应保持隔离,避免直接访问用户数据。
- 直接访问可能导致安全漏洞或系统崩溃。
安全数据拷贝
- 使用copy_from_user从用户空间安全拷贝数据到内核空间。
- 使用copy_to_user将数据从内核空间拷贝到用户空间。
边界检查与长度控制
- 拷贝长度取用户请求长度和缓冲区大小的较小值,防止越界。
- 必须检查copy_from_user返回值,处理拷贝失败情况。
十三、驱动模板与最佳实践
模块初始化与退出
- 使用__init标记初始化函数,加载后可释放其占用的内存。
- 使用__exit标记退出函数,确保正确清理资源。
static关键字作用
- 使用static限制函数作用域,避免不同驱动间的命名冲突。
- 允许多个驱动使用相同的open、read等通用函数名。
驱动开发模板
- 总结出包含设备号、操作函数集、cdev结构体的标准驱动框架。
- 大部分字符设备驱动都遵循这一基本结构。
字符设备驱动模板:
dev_t dev; //设备号 由主设备号及次设备号组成
struct file_operations fops; //操作方法, open read write close
struct cdev cdev; //字符设备结构 设备号及操作方法的组合
static int_init demo_init(void) // __init 初始化函数的标识,执行完成后释放非必要的空间
{
MKDEV();
cdev_init();
cdev_add();
register_chrdev_region();
class_create();
device_create();
ioremap();
}
static void_exit demo_exit(void)
{
iounmap();
device_destroy();
class_destroy();
unregister_chrdev_region();
cdev_del();
}
module_init(demo_init); //驱动程序入口 被module_init修饰的函数在系统启动时自动执行
module_exit(demo_exit); //驱动程序出口
copy_to_user(); //内核到用户空间
copy_from_user(); //用户到内核空间