Linux字符设备驱动开发
一、设备驱动基本概念
1.1 什么是设备驱动?
设备驱动程序是操作系统内核中用于控制硬件设备的软件模块,它提供了应用程序访问硬件设备的接口。
类比理解:
-
硬件设备:打印机、U盘、鼠标等
-
驱动程序:设备的"翻译官",让操作系统知道如何与设备对话
-
应用程序:用户使用的软件(如Word文档打印)
1.2 软件层次结构
应用程序层 (APP/Shell)
↓
系统调用接口
↓
操作系统内核 (Kernel)
↓
设备驱动层 (Device Driver)
↓
硬件设备层 (LED/KEY/UART等)
1.3 驱动程序的三大任务
-
实现硬件操作方法:open、read、write、close等
-
申请设备号:确保设备在系统中的唯一标识
-
向系统注册驱动模块:让内核知道这个驱动的存在
二、应用程序与驱动交互流程
2.1 打开设备流程
应用程序:fd = open("/dev/led", O_RDWR)
↓
内核层:sys_open()
↓
查找设备:led→01, key→02, uart→03
↓
找到LED驱动:调用01号设备的open()
↓
驱动层:执行LED的open函数
↓ 返回
内核层:创建PCB记录fd=3
↓ 返回
应用程序:获得文件描述符fd=3
2.2 读写设备流程
应用程序:write(fd, data, size)
↓
内核层:根据fd=3找到对应驱动
↓
驱动层:执行LED驱动的write函数
↓
硬件层:控制LED亮灭
三、设备驱动分类
3.1 字符设备驱动(90%的设备)
特点 :数据按字节流顺序访问
例子:键盘、鼠标、串口、LED
3.2 块设备驱动
特点 :数据可以随机访问
例子:硬盘、U盘、SD卡
3.3 网络设备驱动
特点 :集成复杂的网络协议栈,没有设备号
例子:网卡、WiFi模块
四、设备号详解
4.1 设备号结构
设备号是一个32位无符号整数:
-
高12位:主设备号(代表设备类型/功能)
-
低20位:次设备号(同类设备中的不同个体)
例子:
主设备号255 + 次设备号0 → 设备号 = (255 << 20) | 0
4.2 地址空间
-
应用程序:操作虚拟地址(保护模式)
-
驱动程序:操作物理地址(直接控制硬件)
-
操作系统作用:隔离硬件,提供安全访问
4.3 重要规范
内核编程注意:尽量避免使用浮点数打印,以提高效率
五、第一个字符设备驱动实战
5.1 创建驱动源文件
/* drivers/char/demo_driver.c */
#include <linux/init.h> // 模块初始化相关
#include <linux/module.h> // 模块相关
#include <linux/printk.h> // 内核打印函数
#include <linux/fs.h> // 文件系统相关
#include <linux/cdev.h> // 字符设备结构体
#include <linux/kdev_t.h> // 设备号相关
// 定义设备信息
#define DEV_MAJOR 255 // 主设备号
#define DEV_MINOR 0 // 次设备号
#define DEV_NAME "demo1" // 设备名称
// 设备操作函数
static int demo_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "demo1: open called\n");
return 0;
}
static ssize_t demo_read(struct file *file, char __user *buf,
size_t size, loff_t *offset)
{
printk(KERN_INFO "demo1: read called, size=%zu\n", size);
return 0;
}
static ssize_t demo_write(struct file *file, const char __user *buf,
size_t size, loff_t *offset)
{
printk(KERN_INFO "demo1: write called, size=%zu\n", size);
return size; // 返回实际写入的字节数
}
static int demo_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "demo1: release called\n");
return 0;
}
// 定义文件操作结构体
static struct file_operations demo_fops = {
.owner = THIS_MODULE, // 防止模块在使用时被卸载
.open = demo_open, // 打开设备
.read = demo_read, // 读设备
.write = demo_write, // 写设备
.release = demo_release, // 关闭设备
};
// 全局变量
static dev_t dev_num; // 设备号
static struct cdev demo_cdev; // 字符设备结构体
// 模块初始化函数
static int __init demo_init(void)
{
int ret = 0;
// 1. 生成设备号
dev_num = MKDEV(DEV_MAJOR, DEV_MINOR);
// 2. 注册设备号区域
ret = register_chrdev_region(dev_num, 1, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "demo1: register chrdev region failed\n");
goto err_register;
}
// 3. 初始化字符设备
cdev_init(&demo_cdev, &demo_fops);
demo_cdev.owner = THIS_MODULE;
// 4. 添加字符设备到系统
ret = cdev_add(&demo_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "demo1: cdev add failed\n");
goto err_cdev_add;
}
printk(KERN_INFO "demo1: driver initialized successfully\n");
return 0;
// 错误处理
err_cdev_add:
cdev_del(&demo_cdev);
err_register:
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 模块退出函数
static void __exit demo_exit(void)
{
// 1. 从系统删除字符设备
cdev_del(&demo_cdev);
// 2. 注销设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "demo1: driver removed\n");
}
// 模块声明
module_init(demo_init); // 指定初始化函数
module_exit(demo_exit); // 指定退出函数
MODULE_LICENSE("GPL"); // 许可证
MODULE_AUTHOR("Your Name"); // 作者
MODULE_DESCRIPTION("Demo Driver"); // 描述
5.2 配置编译系统
修改Kconfig文件
# drivers/char/Kconfig 中添加:
config DEMO_DRIVER
tristate "This is a demo_driver"
default y
help
This is a demo_driver
修改Makefile文件
# drivers/char/Makefile 中添加:
obj-$(CONFIG_DEMO_DRIVER) += demo_driver.o
5.3 编译和测试
编译步骤:
bash
# 1. 进入内核源码目录
cd ~/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
# 2. 配置内核(选择Demo driver)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 进入:Device Drivers → Character devices → Demo test driver
# 选择:<M> 编译为模块 或 <*> 编译进内核
# 3. 编译模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
# 或编译特定模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- M=drivers/char modules
测试应用程序:
/* demo_app.c - 测试程序 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(void)
{
int fd;
char buf[32] = "Hello Driver";
char read_buf[32] = {0};
// 1. 打开设备
fd = open("/dev/demo1", O_RDWR);
if (fd < 0) {
perror("Open device failed");
return -1;
}
printf("Device opened successfully, fd=%d\n", fd);
// 2. 写入数据
if (write(fd, buf, strlen(buf)) < 0) {
perror("Write failed");
close(fd);
return -1;
}
printf("Write: %s\n", buf);
// 3. 读取数据
if (read(fd, read_buf, sizeof(read_buf)) < 0) {
perror("Read failed");
close(fd);
return -1;
}
printf("Read: %s\n", read_buf);
// 4. 关闭设备
close(fd);
printf("Device closed\n");
return 0;
}
手动创建设备节点:
# 在开发板上执行:
# 1. 加载驱动模块
insmod demo_driver.ko
# 2. 查看分配的设备号
cat /proc/devices | grep demo1
# 3. 创建设备节点(假设主设备号是255)
mknod /dev/demo1 c 255 0
# 参数解释:
# /dev/demo1 - 设备节点路径(应用程序open的参数)
# c - 字符设备类型
# 255 - 主设备号(与驱动中DEV_MAJOR一致)
# 0 - 次设备号(与驱动中DEV_MINOR一致)
# 4. 修改权限
chmod 666 /dev/demo1 # 让所有用户可读写
# 5. 编译运行测试程序
arm-linux-gnueabihf-gcc demo_app.c -o demo_app
./demo_app
六、开发工具使用
6.1 ctags代码跳转工具
# 在内核源码目录生成索引
ctags -R
# Vim中使用:
# ctrl + ] - 跳转到定义
# ctrl + o - 跳转回之前位置
# :ts - 显示所有匹配的标签
# :tn/:tp - 下一个/上一个匹配
6.2 调试技巧
- 查看内核打印信息:
dmesg | tail -20 # 查看最后20条内核日志
dmesg -c # 查看并清空日志
cat /proc/kmsg & # 实时查看内核消息
- 查看已注册设备:
cat /proc/devices # 查看所有注册的设备
ls -l /dev/ # 查看设备节点
lsmod # 查看已加载模块
七、驱动框架总结
7.1 标准字符设备驱动流程
初始化阶段:
1. 定义file_operations结构体
2. 分配设备号(静态或动态)
3. 初始化cdev结构体
4. 添加cdev到系统
5. 创建设备节点(手动或自动)
运行阶段:
1. 应用程序调用open()
2. 内核查找对应驱动
3. 调用驱动的open函数
4. 后续read/write/ioctl操作
5. 调用close释放资源
退出阶段:
1. 删除cdev
2. 注销设备号
3. 删除设备节点
7.2 关键数据结构
-
file_operations:定义设备操作函数
-
cdev:字符设备内核结构
-
inode:文件系统索引节点
-
file:打开文件的结构体
7.3 常用函数
// 设备号操作
MKDEV(major, minor) // 生成设备号
MAJOR(dev_t dev) // 提取主设备号
MINOR(dev_t dev) // 提取次设备号
// 设备注册
register_chrdev_region() // 静态注册设备号
alloc_chrdev_region() // 动态分配设备号
unregister_chrdev_region() // 注销设备号
// 字符设备操作
cdev_init() // 初始化cdev
cdev_add() // 添加cdev到系统
cdev_del() // 删除cdev
// 内核打印
printk(KERN_LEVEL "message") // 内核打印函数
八、常见问题解决
8.1 设备号冲突
# 查看已使用的设备号
cat /proc/devices
# 解决方法:
# 1. 选择未使用的设备号
# 2. 使用动态分配:alloc_chrdev_region()
8.2 权限问题
# 创建设备节点后设置权限
sudo chmod 666 /dev/demo1
# 或者在驱动中设置默认权限
# 使用class_create()和device_create()自动创建设备节点
8.3 模块加载失败
# 查看详细错误
dmesg | tail -20
# 常见错误:
# 1. 设备号被占用
# 2. 符号未导出
# 3. 内存分配失败