目录
什么是Linux驱动
驱动的作用
驱动是内核空间和用户空间之间的桥梁:
- 用户程序 → 驱动 → 硬件设备
- 让内核能够识别和控制硬件
- 提供统一的接口给用户程序
两种空间对比
| 特性 | 用户空间 | 内核空间(驱动) |
|---|---|---|
| 权限 | 受限 | 完全(可以访问所有硬件) |
| 失败影响 | 只影响当前程序 | 可能导致系统崩溃 |
| 调试难度 | 容易 | 困难 |
| API | 标准C库 | 内核API |
驱动开发的基本流程
流程图
1. 需求分析
↓
2. 选择驱动类型
↓
3. 编写驱动代码
↓
4. 编写Makefile
↓
5. 编译模块
↓
6. 加载测试
↓
7. 调试优化
详细步骤
步骤1:需求分析
问自己这些问题:
- 要控制什么硬件?(LED、传感器、串口等)
- 硬件如何连接?(GPIO、I2C、SPI、UART等)
- 需要什么功能?(读取数据、写入数据、控制等)
- 用户程序如何使用?(文件接口、ioctl等)
步骤2:选择驱动类型
字符设备驱动(最常用)
- 像文件一样操作设备
- 示例:LED、传感器、简单的控制接口
- 适用:顺序访问、数据流
块设备驱动
- 以块为单位读写
- 示例:磁盘、U盘、SD卡
- 适用:存储设备
网络设备驱动
- 处理网络数据包
- 示例:网卡
- 适用:网络接口
步骤3:编写驱动代码
核心结构:
// 1. 头文件
#include <linux/module.h>
#include <linux/fs.h>
// ... 其他需要的头文件
// 2. 定义设备结构
struct my_device {
dev_t dev_num; // 设备号
struct cdev cdev; // 字符设备
struct class *class; // 设备类
struct device *device; // 设备
// ... 你的私有数据
};
// 3. 实现文件操作函数
static int my_open(struct inode *inode, struct file *file) {
// 打开设备时的操作
return 0;
}
static int my_release(struct inode *inode, struct file *file) {
// 关闭设备时的操作
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf,
size_t len, loff_t *off) {
// 读取数据
// 注意:必须用 copy_to_user() 传给用户空间
return bytes_read;
}
static ssize_t my_write(struct file *file, const char __user *buf,
size_t len, loff_t *off) {
// 写入数据
// 注意:必须用 copy_from_user() 从用户空间接收
return bytes_written;
}
// 4. 定义文件操作结构
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
// 5. 初始化函数
static int __init my_init(void) {
// 分配设备号
// 创建cdev
// 创建class和device
// 初始化硬件
return 0;
}
// 6. 清理函数
static void __exit my_exit(void) {
// 释放资源
// 销毁设备
}
// 7. 模块注册
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("My Device Driver");
步骤4:编写Makefile
obj-m += my_driver.o
# 内核目录
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
# 编译
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
# 清理
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
步骤5:编译模块
make
步骤6:加载测试
# 加载
sudo insmod my_driver.ko
# 查看日志
dmesg | tail
# 测试
cat /dev/my_device
echo "test" > /dev/my_device
# 卸载
sudo rmmod my_driver
核心概念解析
1. 模块(Module)
Linux驱动的执行形式,可以动态加载和卸载。
关键宏:
module_init()- 指定初始化函数module_exit()- 指定清理函数
示例:
static int __init my_init(void) {
printk(KERN_INFO "Driver loaded\n");
return 0;
}
static void __exit my_exit(void) {
printk(KERN_INFO "Driver unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
2. 设备号(Device Number)
每个设备都有唯一的编号。
组成:
主设备号:次设备号
例如:240:0
- 主设备号:标识驱动程序
- 次设备号:标识具体设备
分配方法:
// 方法1:动态分配(推荐)
dev_t dev_num;
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
// 方法2:静态分配
dev_t dev_num = MKDEV(240, 0);
register_chrdev_region(dev_num, 1, "my_device");
3. cdev(字符设备)
内核管理字符设备的结构。
struct cdev my_cdev;
// 初始化
cdev_init(&my_cdev, &fops);
// 添加到系统
cdev_add(&my_cdev, dev_num, 1);
// 删除
cdev_del(&my_cdev);
4. class和device
用于在/dev目录下创建设备节点。
// 创建类
struct class *my_class = class_create(THIS_MODULE, "my_class");
// 创建设备(自动创建 /dev/my_device)
struct device *my_device = device_create(
my_class, NULL, dev_num, NULL, "my_device");
// 销毁
device_destroy(my_class, dev_num);
class_destroy(my_class);
5. 用户空间和内核空间数据传输
重要:不能直接用指针传递!
错误做法:
// 危险!会导致系统崩溃
char *user_buf = (char *)file->private_data;
kernel_buf[0] = user_buf[0]; // 错误!
正确做法:
// 从用户空间读取数据
if (copy_from_user(kernel_buf, user_buf, len) != 0) {
return -EFAULT; // 读取失败
}
// 写入用户空间
if (copy_to_user(user_buf, kernel_buf, len) != 0) {
return -EFAULT; // 写入失败
}
6. 并发控制
驱动可能被多个程序同时访问,需要保护共享数据。
互斥锁:
#include <linux/mutex.h>
static DEFINE_MUTEX(my_mutex);
static int my_open(...) {
mutex_lock(&my_mutex);
// 访问共享数据
mutex_unlock(&my_mutex);
}
一步步实战
实战1:最简单的驱动(Hello World)
目标:创建一个简单的驱动,可以读写字符串。
完整代码:
// hello_driver.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "hello"
static int dev_major = 0;
static struct class *hello_class;
static struct device *hello_device;
static char *message_buffer;
static int message_len = 0;
// 打开设备
static int hello_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Hello: Device opened\n");
return 0;
}
// 关闭设备
static int hello_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "Hello: Device closed\n");
return 0;
}
// 读取数据
static ssize_t hello_read(struct file *file, char __user *buf,
size_t len, loff_t *off) {
printk(KERN_INFO "Hello: Reading\n");
if (*off >= message_len)
return 0; // EOF
if (len > message_len - *off)
len = message_len - *off;
if (copy_to_user(buf, message_buffer + *off, len) != 0)
return -EFAULT;
*off += len;
return len;
}
// 写入数据
static ssize_t hello_write(struct file *file, const char __user *buf,
size_t len, loff_t *off) {
printk(KERN_INFO "Hello: Writing\n");
kfree(message_buffer);
message_buffer = kmalloc(len, GFP_KERNEL);
if (!message_buffer)
return -ENOMEM;
if (copy_from_user(message_buffer, buf, len) != 0) {
kfree(message_buffer);
return -EFAULT;
}
message_len = len;
*off = 0;
return len;
}
// 文件操作结构
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
};
// 模块初始化
static int __init hello_init(void) {
dev_t dev;
printk(KERN_INFO "Hello: Initializing\n");
// 分配设备号
if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0) {
printk(KERN_ALERT "Hello: Failed to allocate major number\n");
return -1;
}
dev_major = MAJOR(dev);
// 创建字符设备
cdev_init(&hello_cdev, &fops);
hello_cdev.owner = THIS_MODULE;
if (cdev_add(&hello_cdev, dev, 1) < 0) {
unregister_chrdev_region(dev, 1);
return -1;
}
// 创建类和设备
hello_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(hello_class)) {
cdev_del(&hello_cdev);
unregister_chrdev_region(dev, 1);
return PTR_ERR(hello_class);
}
hello_device = device_create(hello_class, NULL, dev, NULL, DEVICE_NAME);
if (IS_ERR(hello_device)) {
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev, 1);
return PTR_ERR(hello_device);
}
printk(KERN_INFO "Hello: Device created successfully\n");
printk(KERN_INFO "Hello: Major number: %d\n", dev_major);
printk(KERN_INFO "Hello: Device: /dev/%s\n", DEVICE_NAME);
return 0;
}
// 模块清理
static void __exit hello_exit(void) {
dev_t dev = MKDEV(dev_major, 0);
printk(KERN_INFO "Hello: Cleaning up\n");
device_destroy(hello_class, dev);
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(dev, 1);
kfree(message_buffer);
printk(KERN_INFO "Hello: Goodbye!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Hello World Driver");
Makefile:
obj-m += hello_driver.o
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
测试:
# 编译
make
# 加载
sudo insmod hello_driver.ko
# 查看日志
dmesg | tail
# 写入测试
echo "Hello from userspace!" > /dev/hello
# 读取测试
cat /dev/hello
# 卸载
sudo rmmod hello_driver
实战2:LED控制驱动
目标:控制GPIO LED。
核心要点:
- 使用GPIO API
- 通过write控制开关
关键代码:
#include <linux/gpio.h>
static int led_gpio = 17; // GPIO 17
// 在init中
gpio_request(led_gpio, "led");
gpio_direction_output(led_gpio, 0); // 输出,初始低电平
// 在write中
if (copy_from_user(buf, user_buf, len) != 0)
return -EFAULT;
if (buf[0] == '1')
gpio_set_value(led_gpio, 1); // 亮
else
gpio_set_value(led_gpio, 0); // 灭
// 在exit中
gpio_free(led_gpio);
使用:
# 开灯
echo "1" > /dev/led
# 关灯
echo "0" > /dev/led
AI辅助开发策略
策略1:分步提问
❌ 错误方式:
"帮我写一个DS18B20驱动"
✅ 正确方式:
第一步:我要写DS18B20驱动,先教我驱动的基本框架结构
第二步:如何在驱动中实现GPIO时序控制?
第三步:如何实现read函数让用户读取温度?
...
策略2:渐进式学习
学习路径:
第1天:理解驱动概念
↓
第2天:Hello World驱动
↓
第3天:GPIO LED控制
↓
第4天:中断处理
↓
第5天:复杂设备(I2C/SPI)
↓
第6天:优化和调试
策略3:模板复制+修改
工作流:
- 找到类似的驱动模板
- 复制基本结构
- 用AI解释每部分
- 修改为自己的需求
- 测试和调试
示例:
你:给我一个字符驱动模板
AI: 给出模板...
你:解释一下module_init是什么?
AI: 详细解释...
你:我想在里面加一个全局变量,怎么加?
AI: 给出修改建议...
你:现在我想在read函数返回这个变量的值
AI: 给出代码...
策略4:让AI帮你调试
收集错误信息:
# 保存错误
dmesg > error_log.txt
# 提给AI
"我的驱动加载失败,错误信息如下:
[错误信息]
请帮我分析问题"
策略5:验证理解
让AI出题:
你:刚才学了copy_to_user,你给我出一个题测试理解
AI: 给出练习题...
你:我的答案是...
AI: 评价你的答案...
常见问题与调试
问题1:模块加载失败
症状:
$ sudo insmod my_driver.ko
insmod: ERROR: could not insert module
调试步骤:
-
查看内核日志
dmesg | tail -
常见原因:
- 设备号冲突
- GPIO已被占用
- 内存分配失败
- 权限问题
解决方法:
// 在所有可能失败的地方检查返回值
if (ret < 0) {
printk(KERN_ERR "Failed at line %d: %d\n", __LINE__, ret);
return ret;
}
问题2:程序崩溃或死机
可能原因:
- 访问空指针
- 忘记加互斥锁
- 错误的数据传输(直接用指针)
- 无限循环
预防:
// 1. 检查指针
if (!ptr) {
printk(KERN_ERR "Null pointer\n");
return -EINVAL;
}
// 2. 使用互斥锁
mutex_lock(&my_lock);
// 访问共享数据
mutex_unlock(&my_lock);
// 3. 正确的数据传输
if (copy_to_user(user_buf, kernel_buf, len) != 0)
return -EFAULT;
问题3:read/write返回错误
检查清单:
// read应该返回实际读取的字节数
return bytes_read;
// write应该返回实际写入的字节数
return bytes_written;
// 错误时返回负数
return -EFAULT; // 或其他错误码
调试技巧
1. 使用printk打印调试信息
printk(KERN_INFO "Function: %s, Line: %d\n", __func__, __LINE__);
printk(KERN_DEBUG "Variable value: %d\n", my_var);
2. 查看设备状态
# 查看设备节点
ls -l /dev/my_device
# 查看加载的模块
lsmod | grep my_driver
# 查看模块信息
modinfo my_driver.ko
3. 使用strace跟踪系统调用
strace cat /dev/my_device
4. 检查内核配置
# 确认GPIO支持开启
grep GPIO /boot/config-$(uname -r)
学习资源
推荐书籍
- 《Linux设备驱动程序》(LDD3)- 经典教材
- 《深入理解Linux内核》
- 《Linux内核设计与实现》
在线资源
- Linux内核文档:https://www.kernel.org/doc/
- 内核源码:https://github.com/torvalds/linux
- 驱动示例:内核源码的
drivers/目录
实用命令速查
# 编译
make
# 加载模块
sudo insmod module.ko
# 卸载模块
sudo rmmod module
# 查看模块信息
lsmod | grep module
modinfo module.ko
# 查看内核日志
dmesg
dmesg -T # 带时间戳
dmesg | tail # 最后几行
dmesg -c # 清除日志
# 查看设备
ls -l /dev/
ls -l /sys/class/
# GPIO操作
gpiofind gpiochip0 0 # 查找GPIO
gpioset gpiochip0 17=1 # 设置GPIO
gpioget gpiochip0 17 # 读取GPIO
总结
用AI写驱动的正确姿势
✅ 这样做:
- 先理解基本概念
- 用AI获取模板和解释
- 分步提问,逐步完善
- 每写一段代码就理解原理
- 测试、调试、修改、再测试
- 记录遇到的问题和解决方法
❌ 不要这样做:
- 直接复制粘贴不问原理
- 不测试就认为完成了
- 遇到错误就全部重来
- 不查看内核日志
- 不理解API就乱用
记住
- 理解比速度更重要
- 测试比写代码更重要
- 原理比结果更重要
- AI是工具,你是司机
开始写第一个驱动吧!遇到问题就问AI,但要确保你理解每个答案。

1420

被折叠的 条评论
为什么被折叠?



