Linux 驱动开发完整指南:从入门到实践#
一、驱动开发核心概念与关系#
1.1 四大组件的关系图#
┌─────────────────────────────────────────┐
│ 用户空间应用程序
└─────────────┬───────────────────────────┘
│ 系统调用 (open/read/write)
┌─────────────▼───────────────────────────┐
│ 字符设备驱动框架 ← 提供标准接口
├─────────────────────────────────────────┤
│ GPIO 子系统 ← 抽象硬件操作
├─────────────────────────────────────────┤
│ 中断处理机制 ← 处理异步事件
└─────────────┬───────────────────────────┘
│ 硬件寄存器操作
┌─────────────▼───────────────────────────┐
│ 设备树 (DTS) ← 硬件描述层
└─────────────┬───────────────────────────┘
│ 描述硬件连接和配置
┌─────────────▼───────────────────────────┐
│ 物理硬件
└─────────────────────────────────────────┘
1.2 组件间的关系说明#
组件 |
角色 |
依赖关系 |
修改顺序 |
|---|---|---|---|
设备树 |
硬件描述层 |
独立存在,描述硬件连接 |
第 1 步 |
GPIO 驱动 |
硬件抽象层 |
依赖设备树获取硬件信息 |
第 2 步 |
中断驱动 |
事件处理层 |
依赖 GPIO 驱动和硬件 |
第 3 步 |
字符驱动 |
接口提供层 |
依赖所有底层组件 |
第 4 步 |
1.3 开发顺序逻辑#
二、完整的驱动开发步骤#
2.1 分析硬件与规划#
硬件分析要点#
# 1. 查看硬件连接(以 LED 为例)
LED1 → GPIOA.10 (低电平点亮)
BUTTON1 → GPIOC.13 (按下为低电平)
# 2. 确定所需资源
- GPIO:2 个(输入和输出各 1)
- 中断:1 个(按键中断)
- 内存:少量(用于状态缓存)
# 3. 设计数据结构
struct led_button_device {
// GPIO
struct gpio_desc *led_gpio;
struct gpio_desc *button_gpio;
// 中断
int irq;
// 同步机制
struct mutex lock;
wait_queue_head_t wait_queue;
// 设备状态
atomic_t button_pressed;
u8 led_state;
};
项目规划文件#
# project_plan.txt
硬件:
- LED: GPIOA.10, 低电平有效
- 按键: GPIOC.13, 下降沿触发
软件需求:
- 字符设备:/dev/led_button
- 功能:
1. 控制 LED 开关
2. 读取按键状态
3. 支持阻塞/非阻塞读取
4. 支持 poll 和异步通知
设备树节点:
- 名称:led_button
- 兼容性:custom,led-button-v1
- 属性:led-gpio, button-gpio
2.2 设备树配置#
2.2.1 创建设备树节点#
// custom-led-button.dts - 设备树源文件
/dts-v1/;
/plugin/; // 声明为设备树覆盖(可动态加载)
/**
* 设备树覆盖片段
* @fragment@0: 第一个片段
* @target-path: 目标路径,这里挂载到根节点
*/
&{/} {
// 在根节点下添加我们的设备
led_button_device {
/**
* compatible 属性:驱动匹配的关键
* 格式:"制造商,设备型号"
* 可以有多个,按优先级排序
*/
compatible = "custom,led-button-v1", "generic-led-button";
/**
* status 属性:设备状态
* "okay" - 启用设备
* "disabled" - 禁用设备
* "fail" - 设备故障
* "fail-sss" - 特定故障
*/
status = "okay";
/**
* LED GPIO 配置
* &gpioa: 引用 GPIOA 控制器节点
* 10: 引脚编号
* GPIO_ACTIVE_LOW: 低电平有效
*/
led-gpios = <&gpioa 10 GPIO_ACTIVE_LOW>;
/**
* 按键 GPIO 配置
* GPIOC.13, 高电平有效
* 添加中断属性
*/
button-gpios = <&gpioc 13 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)>;
/**
* 中断配置
* interrupt-parent: 中断父控制器
* interrupts: 中断号和触发方式
* IRQ_TYPE_EDGE_FALLING: 下降沿触发
* IRQ_TYPE_EDGE_RISING: 上升沿触发
* IRQ_TYPE_EDGE_BOTH: 双边沿触发
* IRQ_TYPE_LEVEL_HIGH: 高电平触发
* IRQ_TYPE_LEVEL_LOW: 低电平触发
*/
interrupt-parent = <&gpioc>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
/**
* 自定义属性示例
* 驱动可以根据需要解析这些属性
*/
debounce-interval = <20>; // 消抖时间 20ms
default-brightness = <128>; // 默认亮度 50%
/**
* 标签和别名(可选)
* 标签: 在其他地方可以用 &led_button 引用
* 别名: 在 /proc/device-tree/aliases 中创建快捷方式
*/
label = "user_button";
linux,name = "my_button";
};
};
2.2.2 编译和加载设备树#
#!/bin/bash
# compile_dts.sh - 设备树编译脚本
# 1. 编译设备树覆盖
echo "编译设备树..."
dtc -@ -I dts -O dtb -o custom-led-button.dtbo custom-led-button.dts
# 2. 检查语法
echo "检查设备树语法..."
dtc -I dtb -O dts custom-led-button.dtbo > /dev/null
# 3. 加载到开发板(方法 1:动态加载)
echo "加载设备树覆盖..."
mkdir -p /config/device-tree/overlays/led_button
cp custom-led-button.dtbo /config/device-tree/overlays/led_button/dtbo
# 4. 验证加载结果
echo "验证设备树节点..."
if [ -d /proc/device-tree/led_button_device ]; then
echo "✅ 设备树节点创建成功"
# 查看节点属性
echo "设备树节点信息:"
cat /proc/device-tree/led_button_device/compatible
echo ""
hexdump -C /proc/device-tree/led_button_device/led-gpios
else
echo "❌ 设备树节点创建失败"
fi
# 5. 或者方法 2:替换完整设备树
echo "方法 2:替换完整设备树"
# a. 反编译当前设备树
dtc -I dtb -O dts /boot/board.dtb > current.dts
# b. 添加我们的节点到 current.dts
# c. 重新编译
dtc -I dts -O dtb -o new-board.dtb current.dts
# d. 替换并重启
# cp new-board.dtb /boot/board.dtb
# reboot
2.2.3 设备树调试技巧#
# 查看系统中所有设备树节点
find /proc/device-tree -type f -name "*" | sort
# 查看特定节点
ls -la /proc/device-tree/led_button_device/
# 查看节点属性
cat /proc/device-tree/led_button_device/compatible
cat /proc/device-tree/led_button_device/status
# 查看 GPIO 属性(二进制格式)
hexdump -C /proc/device-tree/led_button_device/led-gpios
# 使用 oftree 工具(如果安装)
oftree -p /proc/device-tree -f led_button_device
# 动态修改属性(调试用)
echo 1 > /proc/device-tree/led_button_device/status
2.3 实现 GPIO 驱动#
2.3.1 驱动源码:gpio_core.c#
// gpio_core.c - GPIO 核心操作实现
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio/consumer.h>
#include <linux/err.h>
#include <linux/slab.h>
#include <linux/of.h>
#include <linux/platform_device.h>
/**
* struct gpio_control - GPIO 控制结构体
* 封装 GPIO 操作相关的数据和状态
*/
struct gpio_control {
struct device* dev; // 关联的设备
struct gpio_desc* led_gpio; // LED GPIO 描述符
struct gpio_desc* button_gpio; // 按键 GPIO 描述符
int led_state; // LED 当前状态
int button_state; // 按键当前状态
struct mutex lock; // 保护并发访问
};
/**
* gpio_setup() - 初始化 GPIO
* @dev: 设备指针
* @gc: GPIO 控制结构体指针
*
* 从设备树获取 GPIO 配置并进行初始化
* 返回值:成功返回 0,失败返回错误码
*/
static int gpio_setup(struct device* dev, struct gpio_control* gc) {
int ret = 0;
pr_info("%s: 开始初始化 GPIO\n", __func__);
// 1. 获取 LED GPIO
gc->led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(gc->led_gpio)) {
ret = PTR_ERR(gc->led_gpio);
dev_err(dev, "无法获取 LED GPIO: %d\n", ret);
// 尝试备用名称
gc->led_gpio = devm_gpiod_get(dev, "led-gpio", GPIOD_OUT_LOW);
if (IS_ERR(gc->led_gpio)) {
ret = PTR_ERR(gc->led_gpio);
dev_err(dev, "无法获取 LED GPIO(备用名): %d\n", ret);
return ret;
}
}
// 2. 获取按键 GPIO
gc->button_gpio = devm_gpiod_get(dev, "button", GPIOD_IN);
if (IS_ERR(gc->button_gpio)) {
ret = PTR_ERR(gc->button_gpio);
dev_err(dev, "无法获取按键 GPIO: %d\n", ret);
return ret;
}
// 3. 配置 GPIO 属性(可选)
// 设置 LED GPIO 标签
gpiod_set_consumer_name(gc->led_gpio, "user_led");
// 设置按键 GPIO 标签
gpiod_set_consumer_name(gc->button_gpio, "user_button");
// 4. 初始化状态
gc->led_state = 0; // 初始关闭
gc->button_state = gpiod_get_value(gc->button_gpio);
// 5. 初始化互斥锁
mutex_init(&gc->lock);
dev_info(dev, "GPIO 初始化成功\n");
dev_info(dev, " LED GPIO: %s, 状态: %s\n", desc_to_gpio(gc->led_gpio) ? "有效" : "无效",
gc->led_state ? "亮" : "灭");
dev_info(dev, " 按键 GPIO: %s, 状态: %s\n",
desc_to_gpio(gc->button_gpio) ? "有效" : "无效",
gc->button_state ? "按下" : "释放");
return 0;
}
/**
* gpio_led_set() - 控制 LED
* @gc: GPIO 控制结构体指针
* @state: 目标状态 (1=亮, 0=灭)
*
* 线程安全的 LED 控制函数
*/
int gpio_led_set(struct gpio_control* gc, int state) {
int ret = 0;
if (!gc || !gc->led_gpio) {
pr_err("%s: 无效参数或 GPIO 未初始化\n", __func__);
return -EINVAL;
}
mutex_lock(&gc->lock);
if (gc->led_state != state) {
/**
* gpiod_set_value() - 设置 GPIO 输出值
* @gpio: GPIO 描述符
* @value: 输出值 (0/1)
* 注意:对于 ACTIVE_LOW 的 GPIO,值会反转
*/
gpiod_set_value(gc->led_gpio, state);
gc->led_state = state;
dev_dbg(gc->dev, "LED 状态改变: %s\n", state ? "亮" : "灭");
}
mutex_unlock(&gc->lock);
return ret;
}
/**
* gpio_led_get() - 获取 LED 状态
* @gc: GPIO 控制结构体指针
*
* 返回 LED 当前状态
*/
int gpio_led_get(struct gpio_control* gc) {
int state;
if (!gc) {
return -EINVAL;
}
mutex_lock(&gc->lock);
state = gc->led_state;
mutex_unlock(&gc->lock);
return state;
}
/**
* gpio_button_get() - 获取按键状态
* @gc: GPIO 控制结构体指针
*
* 读取 GPIO 引脚的实际电平
*/
int gpio_button_get(struct gpio_control* gc) {
int state;
if (!gc || !gc->button_gpio) {
return -EINVAL;
}
/**
* gpiod_get_value() - 读取 GPIO 输入值
* @gpio: GPIO 描述符
* 返回值:GPIO 当前电平 (0/1)
*/
state = gpiod_get_value(gc->button_gpio);
mutex_lock(&gc->lock);
gc->button_state = state;
mutex_unlock(&gc->lock);
return state;
}
/**
* gpio_led_toggle() - 翻转 LED 状态
* @gc: GPIO 控制结构体指针
*/
int gpio_led_toggle(struct gpio_control* gc) {
int new_state;
if (!gc) {
return -EINVAL;
}
mutex_lock(&gc->lock);
new_state = !gc->led_state;
gpiod_set_value(gc->led_gpio, new_state);
gc->led_state = new_state;
dev_dbg(gc->dev, "LED 翻转: %s\n", new_state ? "亮" : "灭");
mutex_unlock(&gc->lock);
return new_state;
}
/**
* gpio_cleanup() - 清理 GPIO 资源
* @gc: GPIO 控制结构体指针
*
* 注意:我们使用 devm_gpiod_get() 申请的资源会自动释放
* 这里只需要销毁互斥锁
*/
void gpio_cleanup(struct gpio_control* gc) {
if (gc) {
mutex_destroy(&gc->lock);
dev_info(gc->dev, "GPIO 资源已清理\n");
}
}
EXPORT_SYMBOL(gpio_setup);
EXPORT_SYMBOL(gpio_led_set);
EXPORT_SYMBOL(gpio_led_get);
EXPORT_SYMBOL(gpio_button_get);
EXPORT_SYMBOL(gpio_led_toggle);
EXPORT_SYMBOL(gpio_cleanup);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("通用 GPIO 控制模块");
MODULE_AUTHOR("Driver Developer");
2.3.2 GPIO 调试方法#
# 1. 查看 GPIO 状态
cat /sys/kernel/debug/gpio
# 2. 使用 GPIO Sysfs 接口(旧方式)
# 导出 GPIO
echo 10 > /sys/class/gpio/export
echo 13 > /sys/class/gpio/export
# 查看 GPIO 信息
ls /sys/class/gpio/gpio10/
cat /sys/class/gpio/gpio10/direction
cat /sys/class/gpio/gpio10/value
# 3. 使用 libgpiod 工具(新方式)
# 安装工具
sudo apt-get install gpiod
# 查看 GPIO 信息
gpiodetect
gpioinfo
gpiofind "LED"
gpioget gpiochip0 10
gpioset gpiochip0 10=1
# 4. 在内核中添加调试信息
echo "file gpio_core.c +p" > /sys/kernel/debug/dynamic_debug/control
2.4 实现中断驱动#
2.4.1 驱动源码:irq_handler.c#
// irq_handler.c - 中断处理实现
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/timer.h>
#include <linux/atomic.h>
/**
* struct interrupt_data - 中断处理数据结构
* 管理中断相关的状态和资源
*/
struct interrupt_data {
struct device* dev; // 关联的设备
int irq_number; // 中断号
atomic_t irq_count; // 中断计数(原子操作)
struct timer_list debounce_timer; // 消抖定时器
struct work_struct work; // 工作队列项
struct workqueue_struct* wq; // 工作队列
// 状态标志
unsigned long last_irq_time; // 上次中断时间
atomic_t button_pressed; // 按键状态
wait_queue_head_t wait_queue; // 等待队列
// 回调函数
void (*callback)(void* data); // 用户回调
void* callback_data; // 回调数据
};
/**
* button_work_handler() - 工作队列处理函数
* @work: 工作队列项
*
* 在进程上下文中处理中断的耗时操作
* 可以安全地使用可能引起休眠的函数
*/
static void button_work_handler(struct work_struct* work) {
struct interrupt_data* idata = container_of(work, struct interrupt_data, work);
dev_info(idata->dev, "工作队列处理:按键事件\n");
// 可以执行耗时操作
// msleep(10); // 如果需要可以休眠
// 更新系统状态
sysfs_notify(&idata->dev->kobj, NULL, "button");
// 调用用户回调(如果设置)
if (idata->callback) {
idata->callback(idata->callback_data);
}
// 唤醒等待进程
wake_up_interruptible(&idata->wait_queue);
}
/**
* button_debounce_timer() - 消抖定时器回调
* @timer: 定时器指针
*
* 用于按键消抖,定时器到期后读取稳定的按键状态
*/
static void button_debounce_timer(struct timer_list* timer) {
struct interrupt_data* idata = from_timer(idata, timer, debounce_timer);
int state;
// 读取稳定的 GPIO 状态
state = gpio_button_get(idata->gpio_control);
if (state != atomic_read(&idata->button_pressed)) {
atomic_set(&idata->button_pressed, state);
// 调度工作队列处理
queue_work(idata->wq, &idata->work);
dev_dbg(idata->dev, "消抖完成,按键状态: %s\n", state ? "按下" : "释放");
}
}
/**
* button_irq_handler() - 中断处理函数(上半部)
* @irq: 中断号
* @dev_id: 设备标识符
*
* 中断上半部,处理要尽可能快
* 返回值:IRQ_WAKE_THREAD 表示需要下半部处理
*/
static irqreturn_t button_irq_handler(int irq, void* dev_id) {
struct interrupt_data* idata = dev_id;
unsigned long current_time = jiffies;
// 原子操作增加中断计数
atomic_inc(&idata->irq_count);
// 简单的时间戳记录
idata->last_irq_time = current_time;
/**
* 中断消抖逻辑:
* 1. 取消之前的定时器(如果还在运行)
* 2. 重新启动定时器
* 这样可以避免按键抖动引起的多次中断
*/
del_timer(&idata->debounce_timer);
// 设置定时器 20ms 后触发消抖检查
mod_timer(&idata->debounce_timer, jiffies + msecs_to_jiffies(20));
dev_dbg(idata->dev, "中断触发 #%d\n", atomic_read(&idata->irq_count));
/**
* 返回 IRQ_HANDLED 表示中断已处理
* 如果使用线程化中断,可以返回 IRQ_WAKE_THREAD
*/
return IRQ_HANDLED;
}
/**
* threaded_irq_handler() - 线程化中断处理函数(下半部)
* @irq: 中断号
* @dev_id: 设备标识符
*
* 线程化中断的下半部,可以执行耗时操作
*/
static irqreturn_t threaded_irq_handler(int irq, void* dev_id) {
struct interrupt_data* idata = dev_id;
// 在线程上下文中可以安全地休眠
// msleep(1);
// 执行实际的处理工作
button_work_handler(&idata->work);
return IRQ_HANDLED;
}
/**
* interrupt_setup() - 初始化中断处理
* @dev: 设备指针
* @gpio_desc: 按键 GPIO 描述符
* @idata: 中断数据结构指针
*
* 返回值:成功返回 0,失败返回错误码
*/
int interrupt_setup(struct device* dev, struct gpio_desc* button_gpio,
struct interrupt_data* idata) {
int ret, irq;
if (!dev || !button_gpio || !idata) {
return -EINVAL;
}
// 初始化数据结构
idata->dev = dev;
atomic_set(&idata->irq_count, 0);
atomic_set(&idata->button_pressed, 0);
// 1. 获取中断号
irq = gpiod_to_irq(button_gpio);
if (irq < 0) {
dev_err(dev, "无法获取 GPIO 中断号: %d\n", irq);
return irq;
}
idata->irq_number = irq;
// 2. 初始化等待队列
init_waitqueue_head(&idata->wait_queue);
// 3. 初始化定时器(用于消抖)
timer_setup(&idata->debounce_timer, button_debounce_timer, 0);
// 4. 创建工作队列
idata->wq = create_singlethread_workqueue("button_wq");
if (!idata->wq) {
dev_err(dev, "无法创建工作队列\n");
return -ENOMEM;
}
// 5. 初始化工作
INIT_WORK(&idata->work, button_work_handler);
// 6. 注册中断处理函数
dev_info(dev, "注册中断 %d (GPIO: %d)\n", irq, desc_to_gpio(button_gpio));
/**
* 方法1:普通中断 + 工作队列
*/
ret = request_irq(irq, button_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"button_irq", idata);
/**
* 方法2:线程化中断(推荐,更简单)
* ret = request_threaded_irq(irq, button_irq_handler,
* threaded_irq_handler,
* IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | IRQF_ONESHOT,
* "button_irq", idata);
*/
if (ret) {
dev_err(dev, "无法注册中断: %d\n", ret);
destroy_workqueue(idata->wq);
return ret;
}
// 7. 启用中断(默认就是启用的)
enable_irq(irq);
dev_info(dev, "中断初始化成功 (IRQ: %d)\n", irq);
return 0;
}
/**
* interrupt_cleanup() - 清理中断资源
* @idata: 中断数据结构指针
*/
void interrupt_cleanup(struct interrupt_data* idata) {
if (!idata) {
return;
}
// 1. 禁用中断
if (idata->irq_number > 0) {
disable_irq(idata->irq_number);
}
// 2. 释放中断
free_irq(idata->irq_number, idata);
// 3. 删除定时器
del_timer_sync(&idata->debounce_timer);
// 4. 销毁工作队列
if (idata->wq) {
flush_workqueue(idata->wq);
destroy_workqueue(idata->wq);
}
dev_info(idata->dev, "中断资源已清理\n");
}
/**
* interrupt_wait_event() - 等待中断事件
* @idata: 中断数据结构指针
* @timeout_ms: 超时时间(毫秒)
*
* 供应用程序调用,等待按键事件
*/
int interrupt_wait_event(struct interrupt_data* idata, int timeout_ms) {
int ret;
long timeout_jiffies = msecs_to_jiffies(timeout_ms);
/**
* wait_event_interruptible_timeout() - 可中断的超时等待
* @wq: 等待队列头
* @condition: 等待条件(当为真时返回)
* @timeout: 超时时间(jiffies)
* 返回值:>0 条件满足,0 超时,<0 被信号中断
*/
ret = wait_event_interruptible_timeout(
idata->wait_queue, atomic_read(&idata->button_pressed) != 0, timeout_jiffies);
return ret;
}
EXPORT_SYMBOL(interrupt_setup);
EXPORT_SYMBOL(interrupt_cleanup);
EXPORT_SYMBOL(interrupt_wait_event);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("通用中断处理模块");
MODULE_AUTHOR("Driver Developer");
2.4.2 中断调试技巧#
#!/bin/bash
# interrupt_debug.sh - 中断调试脚本
echo "=== 中断系统状态 ==="
# 1. 查看所有中断统计
echo "1. 系统中断统计:"
cat /proc/interrupts | grep -E "(button|gpio|irq)"
# 2. 查看特定中断信息
echo -e "\n2. 按键中断详情:"
IRQ_NUM=$(cat /proc/interrupts | grep button | awk '{print $1}' | sed 's/://')
if [ ! -z "$IRQ_NUM" ]; then
echo "中断号: $IRQ_NUM"
cat /proc/irq/$IRQ_NUM/spurious
cat /proc/irq/$IRQ_NUM/affinity_hint
fi
# 3. 查看中断亲和性(多核CPU)
echo -e "\n3. CPU 中断分布:"
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
cpu_num=$(basename $cpu | sed 's/cpu//')
echo -n "CPU$cpu_num: "
cat $cpu/topology/thread_siblings_list
done
# 4. 动态调整中断亲和性
echo -e "\n4. 调整中断亲和性示例:"
# 将中断绑定到 CPU0
# echo 1 > /proc/irq/$IRQ_NUM/smp_affinity
# 5. 监控中断频率
echo -e "\n5. 实时监控中断(按 Ctrl+C 停止):"
watch -n 1 "cat /proc/interrupts | head -20"
# 6. 使用 ftrace 跟踪中断
echo -e "\n6. 使用 ftrace 跟踪中断流:"
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo "handle_irq_event*" > /sys/kernel/debug/tracing/set_ftrace_filter
echo "request_threaded_irq" >> /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 等待中断发生...
sleep 2
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace > /tmp/interrupt_trace.txt
echo "跟踪结果保存到: /tmp/interrupt_trace.txt"
2.5 实现字符设备驱动#
2.5.1 驱动源码:char_device.c#
// char_device.c - 字符设备驱动实现
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/poll.h>
#include <linux/fcntl.h>
#include <linux/slab.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/version.h>
/**
* struct char_device_data - 字符设备私有数据
* 管理字符设备的所有状态和资源
*/
struct char_device_data {
struct cdev cdev; // 字符设备结构
struct device* device; // 关联的设备
dev_t devno; // 设备号
// 同步机制
struct mutex mutex; // 保护设备操作
struct rw_semaphore rwsem; // 读写信号量
wait_queue_head_t read_queue; // 读等待队列
wait_queue_head_t write_queue; // 写等待队列
// 缓冲区管理
char* buffer; // 数据缓冲区
size_t buffer_size; // 缓冲区大小
size_t data_len; // 当前数据长度
loff_t read_pos; // 读位置
loff_t write_pos; // 写位置
// 异步通知
struct fasync_struct* fasync_queue;
// 下层组件引用
struct gpio_control* gpio_ctrl;
struct interrupt_data* irq_data;
// 设备状态
unsigned int open_count; // 打开计数
bool is_open; // 打开状态
atomic_t available; // 数据可用标志
};
/**
* char_device_open() - 打开设备
* @inode: 设备 inode
* @filp: 文件结构指针
*
* 当应用程序调用 open() 时触发
*/
static int char_device_open(struct inode* inode, struct file* filp) {
struct char_device_data* cdata;
// 从 inode 获取设备私有数据
cdata = container_of(inode->i_cdev, struct char_device_data, cdev);
// 检查设备是否已经打开(可选)
mutex_lock(&cdata->mutex);
if (cdata->is_open) {
mutex_unlock(&cdata->mutex);
pr_warn("设备已经被打开\n");
return -EBUSY;
}
// 更新状态
cdata->open_count++;
cdata->is_open = true;
// 将私有数据保存到文件结构中
filp->private_data = cdata;
mutex_unlock(&cdata->mutex);
pr_info("设备已打开,打开计数: %u\n", cdata->open_count);
return 0;
}
/**
* char_device_release() - 关闭设备
* @inode: 设备 inode
* @filp: 文件结构指针
*
* 当应用程序调用 close() 时触发
*/
static int char_device_release(struct inode* inode, struct file* filp) {
struct char_device_data* cdata = filp->private_data;
if (!cdata) {
return -ENODEV;
}
mutex_lock(&cdata->mutex);
// 清除异步通知队列
if (cdata->fasync_queue) {
fasync_helper(-1, filp, 0, &cdata->fasync_queue);
}
// 更新状态
cdata->open_count--;
if (cdata->open_count == 0) {
cdata->is_open = false;
// 清理缓冲区
cdata->data_len = 0;
cdata->read_pos = 0;
cdata->write_pos = 0;
}
mutex_unlock(&cdata->mutex);
pr_info("设备已关闭,剩余打开计数: %u\n", cdata->open_count);
return 0;
}
/**
* char_device_read() - 读取设备数据
* @filp: 文件结构指针
* @buf: 用户空间缓冲区
* @count: 请求读取的字节数
* @f_pos: 文件位置指针
*
* 支持阻塞和非阻塞读取
*/
static ssize_t char_device_read(struct file* filp, char __user* buf, size_t count,
loff_t* f_pos) {
struct char_device_data* cdata = filp->private_data;
ssize_t retval = 0;
size_t available;
if (!cdata) {
return -ENODEV;
}
// 非阻塞模式检查
if (filp->f_flags & O_NONBLOCK) {
if (!atomic_read(&cdata->available)) {
return -EAGAIN;
}
} else {
// 阻塞等待数据可用
retval = wait_event_interruptible(cdata->read_queue, atomic_read(&cdata->available));
if (retval) {
return retval; // 被信号中断
}
}
// 获取读信号量(允许并发读)
down_read(&cdata->rwsem);
// 计算可用数据量
available = cdata->data_len - cdata->read_pos;
if (available == 0) {
up_read(&cdata->rwsem);
return 0; // EOF
}
// 限制读取数量
if (count > available) {
count = available;
}
// 复制数据到用户空间
if (copy_to_user(buf, cdata->buffer + cdata->read_pos, count)) {
up_read(&cdata->rwsem);
return -EFAULT;
}
// 更新位置
cdata->read_pos += count;
if (cdata->read_pos >= cdata->data_len) {
// 所有数据已读完
cdata->read_pos = 0;
cdata->write_pos = 0;
cdata->data_len = 0;
atomic_set(&cdata->available, 0);
}
up_read(&cdata->rwsem);
// 唤醒可能的写等待
wake_up_interruptible(&cdata->write_queue);
pr_debug("读取 %zu 字节,剩余 %zu 字节\n", count, cdata->data_len - cdata->read_pos);
return count;
}
/**
* char_device_write() - 写入设备数据
* @filp: 文件结构指针
* @buf: 用户空间数据缓冲区
* @count: 写入字节数
* @f_pos: 文件位置指针
*
* 支持阻塞和非阻塞写入
*/
static ssize_t char_device_write(struct file* filp, const char __user* buf, size_t count,
loff_t* f_pos) {
struct char_device_data* cdata = filp->private_data;
ssize_t retval = 0;
size_t free_space;
if (!cdata) {
return -ENODEV;
}
// 参数检查
if (count == 0) {
return 0;
}
if (count > cdata->buffer_size) {
count = cdata->buffer_size;
}
// 阻塞等待缓冲区空间(如果不是非阻塞模式)
if (!(filp->f_flags & O_NONBLOCK)) {
retval = wait_event_interruptible(cdata->write_queue,
cdata->data_len + count <= cdata->buffer_size);
if (retval) {
return retval;
}
} else {
// 非阻塞模式检查空间
if (cdata->data_len + count > cdata->buffer_size) {
return -EAGAIN;
}
}
// 获取写信号量(独占访问)
down_write(&cdata->rwsem);
// 计算可用空间
free_space = cdata->buffer_size - cdata->data_len;
if (count > free_space) {
count = free_space;
}
// 复制用户数据到内核缓冲区
if (copy_from_user(cdata->buffer + cdata->write_pos, buf, count)) {
up_write(&cdata->rwsem);
return -EFAULT;
}
// 更新缓冲区状态
cdata->write_pos += count;
cdata->data_len += count;
// 如果缓冲区已满,更新写位置
if (cdata->write_pos >= cdata->buffer_size) {
cdata->write_pos = 0;
}
up_write(&cdata->rwsem);
// 设置数据可用标志
atomic_set(&cdata->available, 1);
// 唤醒可能的读等待
wake_up_interruptible(&cdata->read_queue);
// 发送异步通知(如果有注册)
if (cdata->fasync_queue) {
kill_fasync(&cdata->fasync_queue, SIGIO, POLL_IN);
}
pr_debug("写入 %zu 字节,缓冲区使用率: %zu/%zu\n", count, cdata->data_len,
cdata->buffer_size);
return count;
}
/**
* char_device_ioctl() - 设备控制命令
* @filp: 文件结构指针
* @cmd: 命令号
* @arg: 命令参数
*
* 实现自定义的设备控制命令
*/
static long char_device_ioctl(struct file* filp, unsigned int cmd, unsigned long arg) {
struct char_device_data* cdata = filp->private_data;
int retval = 0;
int value;
if (!cdata) {
return -ENODEV;
}
// 检查命令权限
if (_IOC_TYPE(cmd) != 'L') {
return -ENOTTY;
}
mutex_lock(&cdata->mutex);
switch (cmd) {
case LED_ON:
if (cdata->gpio_ctrl) {
retval = gpio_led_set(cdata->gpio_ctrl, 1);
}
break;
case LED_OFF:
if (cdata->gpio_ctrl) {
retval = gpio_led_set(cdata->gpio_ctrl, 0);
}
break;
case LED_TOGGLE:
if (cdata->gpio_ctrl) {
retval = gpio_led_toggle(cdata->gpio_ctrl);
}
break;
case GET_BUTTON_STATE:
if (cdata->gpio_ctrl) {
value = gpio_button_get(cdata->gpio_ctrl);
retval = put_user(value, (int __user*)arg);
}
break;
case WAIT_FOR_BUTTON:
if (cdata->irq_data) {
retval = interrupt_wait_event(cdata->irq_data, arg);
}
break;
case GET_BUFFER_INFO: {
struct buffer_info info;
info.size = cdata->buffer_size;
info.used = cdata->data_len;
info.free = cdata->buffer_size - cdata->data_len;
retval = copy_to_user((void __user*)arg, &info, sizeof(info)) ? -EFAULT : 0;
break;
}
default: retval = -ENOTTY; break;
}
mutex_unlock(&cdata->mutex);
return retval;
}
/**
* char_device_poll() - 轮询设备状态
* @filp: 文件结构指针
* @wait: 轮询表
*
* 支持 select() 和 poll() 系统调用
*/
static __poll_t char_device_poll(struct file* filp, poll_table* wait) {
struct char_device_data* cdata = filp->private_data;
__poll_t mask = 0;
if (!cdata) {
return EPOLLERR;
}
// 注册等待队列
poll_wait(filp, &cdata->read_queue, wait);
poll_wait(filp, &cdata->write_queue, wait);
// 检查可读状态
if (atomic_read(&cdata->available)) {
mask |= EPOLLIN | EPOLLRDNORM;
}
// 检查可写状态
if (cdata->data_len < cdata->buffer_size) {
mask |= EPOLLOUT | EPOLLWRNORM;
}
// 检查错误状态
if (!cdata->is_open) {
mask |= EPOLLERR;
}
return mask;
}
/**
* char_device_fasync() - 异步通知支持
* @fd: 文件描述符
* @filp: 文件结构指针
* @on: 启用/禁用标志
*
* 支持异步通知(信号驱动 I/O)
*/
static int char_device_fasync(int fd, struct file* filp, int on) {
struct char_device_data* cdata = filp->private_data;
if (!cdata) {
return -ENODEV;
}
return fasync_helper(fd, filp, on, &cdata->fasync_queue);
}
/**
* char_device_mmap() - 内存映射支持
* @filp: 文件结构指针
* @vma: 虚拟内存区域
*
* 将设备缓冲区映射到用户空间
*/
static int char_device_mmap(struct file* filp, struct vm_area_struct* vma) {
struct char_device_data* cdata = filp->private_data;
unsigned long size = vma->vm_end - vma->vm_start;
if (!cdata || !cdata->buffer) {
return -ENODEV;
}
// 检查映射大小
if (size > cdata->buffer_size) {
return -EINVAL;
}
// 将内核缓冲区映射到用户空间
return remap_pfn_range(vma, vma->vm_start, virt_to_phys(cdata->buffer) >> PAGE_SHIFT, size,
vma->vm_page_prot);
}
/**
* 文件操作结构体
* 定义设备支持的所有操作
*/
static const struct file_operations char_device_fops = {
.owner = THIS_MODULE,
.open = char_device_open,
.release = char_device_release,
.read = char_device_read,
.write = char_device_write,
.unlocked_ioctl = char_device_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.poll = char_device_poll,
.fasync = char_device_fasync,
.mmap = char_device_mmap,
.llseek = no_llseek,
};
/**
* char_device_create() - 创建字符设备
* @parent: 父设备
* @gpio_ctrl: GPIO 控制结构
* @irq_data: 中断数据结构
* @buffer_size: 缓冲区大小
*
* 返回值:成功返回设备指针,失败返回 ERR_PTR
*/
struct char_device_data* char_device_create(struct device* parent,
struct gpio_control* gpio_ctrl,
struct interrupt_data* irq_data,
size_t buffer_size) {
struct char_device_data* cdata;
int ret;
dev_t devno;
if (!parent) {
return ERR_PTR(-EINVAL);
}
// 1. 分配设备数据结构
cdata = devm_kzalloc(parent, sizeof(*cdata), GFP_KERNEL);
if (!cdata) {
return ERR_PTR(-ENOMEM);
}
// 2. 分配缓冲区
cdata->buffer = devm_kzalloc(parent, buffer_size, GFP_KERNEL);
if (!cdata->buffer) {
return ERR_PTR(-ENOMEM);
}
cdata->buffer_size = buffer_size;
cdata->data_len = 0;
cdata->read_pos = 0;
cdata->write_pos = 0;
cdata->gpio_ctrl = gpio_ctrl;
cdata->irq_data = irq_data;
cdata->device = parent;
cdata->open_count = 0;
cdata->is_open = false;
atomic_set(&cdata->available, 0);
// 3. 初始化同步机制
mutex_init(&cdata->mutex);
init_rwsem(&cdata->rwsem);
init_waitqueue_head(&cdata->read_queue);
init_waitqueue_head(&cdata->write_queue);
// 4. 动态分配设备号
ret = alloc_chrdev_region(&devno, 0, 1, "led_button");
if (ret < 0) {
dev_err(parent, "无法分配设备号: %d\n", ret);
return ERR_PTR(ret);
}
cdata->devno = devno;
// 5. 初始化字符设备
cdev_init(&cdata->cdev, &char_device_fops);
cdata->cdev.owner = THIS_MODULE;
// 6. 添加到系统
ret = cdev_add(&cdata->cdev, devno, 1);
if (ret) {
dev_err(parent, "无法添加字符设备: %d\n", ret);
unregister_chrdev_region(devno, 1);
return ERR_PTR(ret);
}
// 7. 创建设备节点(可选,udev 会自动创建)
cdata->device = device_create(class_led_button, parent, devno, NULL, "led_button");
if (IS_ERR(cdata->device)) {
dev_err(parent, "无法创建设备节点\n");
cdev_del(&cdata->cdev);
unregister_chrdev_region(devno, 1);
return ERR_CAST(cdata->device);
}
dev_info(parent, "字符设备创建成功,主设备号: %d,次设备号: %d\n", MAJOR(devno),
MINOR(devno));
dev_info(parent, "设备节点: /dev/led_button\n");
return cdata;
}
/**
* char_device_destroy() - 销毁字符设备
* @cdata: 字符设备数据指针
*/
void char_device_destroy(struct char_device_data* cdata) {
if (!cdata) {
return;
}
// 1. 删除设备节点
if (!IS_ERR_OR_NULL(cdata->device)) {
device_destroy(class_led_button, cdata->devno);
}
// 2. 删除字符设备
cdev_del(&cdata->cdev);
// 3. 释放设备号
unregister_chrdev_region(cdata->devno, 1);
// 4. 销毁同步机制
mutex_destroy(&cdata->mutex);
dev_info(cdata->device, "字符设备已销毁\n");
}
EXPORT_SYMBOL(char_device_create);
EXPORT_SYMBOL(char_device_destroy);
// 创建设备类(模块全局)
static struct class* class_led_button;
static int __init char_device_init(void) {
// 创建设备类
class_led_button = class_create(THIS_MODULE, "led_button");
if (IS_ERR(class_led_button)) {
pr_err("无法创建设备类\n");
return PTR_ERR(class_led_button);
}
pr_info("字符设备模块初始化成功\n");
return 0;
}
static void __exit char_device_exit(void) {
// 销毁设备类
if (class_led_button) {
class_destroy(class_led_button);
}
pr_info("字符设备模块卸载成功\n");
}
module_init(char_device_init);
module_exit(char_device_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("通用字符设备驱动框架");
MODULE_AUTHOR("Driver Developer");
2.6 整合所有组件的主驱动#
2.7 编译和测试#
2.7.1 完整的 Makefile#
# Makefile - 驱动编译配置
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
# 驱动模块列表
obj-m := led_button_driver.o
led_button_driver-objs := \
gpio_core.o \
irq_handler.o \
char_device.o \
led_button_driver.o
# 内核模块编译标志
ccflags-y := -I$(src)/include -DDEBUG
ccflags-y += -Wno-declaration-after-statement
# 用户空间测试程序
TEST_PROGS := led_button_test
all: modules test_apps
modules:
@echo "编译内核模块..."
$(MAKE) -C $(KDIR) M=$(PWD) modules
test_apps: $(TEST_PROGS)
led_button_test: test/led_button_test.c
@echo "编译测试程序..."
$(CC) -o $@ $< -lpthread
clean:
@echo "清理编译文件..."
$(MAKE) -C $(KDIR) M=$(PWD) clean
rm -f $(TEST_PROGS)
rm -f *.o *.ko *.mod.c *.mod.o *.order *.symvers
install: modules
@echo "安装驱动模块..."
sudo cp led_button_driver.ko /lib/modules/$(shell uname -r)/kernel/drivers/misc/
sudo depmod -a
load: modules
@echo "加载驱动模块..."
sudo insmod led_button_driver.ko
sudo dmesg | tail -10
unload:
@echo "卸载驱动模块..."
sudo rmmod led_button_driver
sudo dmesg | tail -10
reload: unload load
# 调试目标
debug:
@echo "启用调试输出..."
echo "module led_button_driver +p" | sudo tee /sys/kernel/debug/dynamic_debug/control
.PHONY: all modules clean install load unload reload debug
2.7.2 测试应用程序#
// test/led_button_test.c - 完整的测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <pthread.h>
#include <sys/ioctl.h>
// 定义与驱动一致的 IOCTL 命令
#define LED_ON _IO('L', 1)
#define LED_OFF _IO('L', 2)
#define LED_TOGGLE _IO('L', 3)
#define GET_BUTTON_STATE _IOR('L', 4, int)
#define WAIT_FOR_BUTTON _IOW('L', 5, int)
#define GET_BUFFER_INFO _IOR('L', 6, struct buffer_info)
struct buffer_info {
size_t size;
size_t used;
size_t free;
};
// 全局变量
static volatile int running = 1;
static int fd = -1;
// 信号处理函数
void signal_handler(int sig) {
printf("\n收到信号 %d,准备退出...\n", sig);
running = 0;
}
// 异步通知回调
void async_callback(int sig) {
char buffer[256];
int n;
if (sig == SIGIO) {
n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("[异步通知] 收到数据: %s\n", buffer);
}
}
}
// 测试用例 1:基本 LED 控制
void test_led_control(void) {
printf("\n=== 测试 LED 控制 ===\n");
// 打开 LED
if (ioctl(fd, LED_ON) < 0) {
perror("LED_ON 失败");
} else {
printf("✓ LED 已打开\n");
sleep(1);
}
// 关闭 LED
if (ioctl(fd, LED_OFF) < 0) {
perror("LED_OFF 失败");
} else {
printf("✓ LED 已关闭\n");
sleep(1);
}
// 翻转 LED
for (int i = 0; i < 5; i++) {
if (ioctl(fd, LED_TOGGLE) < 0) {
perror("LED_TOGGLE 失败");
break;
}
printf("✓ LED 翻转 %d\n", i + 1);
usleep(500000); // 500ms
}
}
// 测试用例 2:按键状态读取
void test_button_read(void) {
int state;
printf("\n=== 测试按键读取 ===\n");
printf("请按下按键(按 Ctrl+C 跳过)...\n");
for (int i = 0; i < 10 && running; i++) {
if (ioctl(fd, GET_BUTTON_STATE, &state) < 0) {
perror("GET_BUTTON_STATE 失败");
break;
}
printf(" 按键状态: %s\n", state ? "按下" : "释放");
usleep(500000); // 500ms
}
}
// 测试用例 3:阻塞等待按键
void test_button_wait(void) {
int timeout = 5000; // 5 秒
printf("\n=== 测试阻塞等待按键 ===\n");
printf("请在 %d 毫秒内按下按键...\n", timeout);
if (ioctl(fd, WAIT_FOR_BUTTON, &timeout) < 0) {
if (errno == ETIMEDOUT) {
printf("✗ 等待超时,按键未按下\n");
} else {
perror("WAIT_FOR_BUTTON 失败");
}
} else {
printf("✓ 检测到按键按下!\n");
}
}
// 测试用例 4:读写数据
void test_data_rw(void) {
char write_buf[64];
char read_buf[64];
int n;
printf("\n=== 测试数据读写 ===\n");
// 写入数据
snprintf(write_buf, sizeof(write_buf), "测试数据,时间戳: %ld", time(NULL));
n = write(fd, write_buf, strlen(write_buf));
if (n < 0) {
perror("写入失败");
} else {
printf("✓ 写入 %d 字节: %s\n", n, write_buf);
}
// 读取数据
n = read(fd, read_buf, sizeof(read_buf) - 1);
if (n < 0) {
perror("读取失败");
} else {
read_buf[n] = '\0';
printf("✓ 读取 %d 字节: %s\n", n, read_buf);
}
}
// 测试用例 5:poll 机制
void test_poll(void) {
struct pollfd pfd;
int ret;
printf("\n=== 测试 poll 机制 ===\n");
pfd.fd = fd;
pfd.events = POLLIN | POLLOUT;
pfd.revents = 0;
printf("等待设备可读或可写(3秒超时)...\n");
ret = poll(&pfd, 1, 3000);
if (ret < 0) {
perror("poll 失败");
} else if (ret == 0) {
printf("✗ poll 超时\n");
} else {
printf("✓ poll 返回 %d\n", ret);
if (pfd.revents & POLLIN) {
printf(" 设备可读\n");
}
if (pfd.revents & POLLOUT) {
printf(" 设备可写\n");
}
if (pfd.revents & POLLERR) {
printf(" 设备错误\n");
}
}
}
// 测试用例 6:异步通知
void* async_thread(void* arg) {
struct sigaction sa;
int oflags;
// 设置信号处理
memset(&sa, 0, sizeof(sa));
sa.sa_handler = async_callback;
sa.sa_flags = 0;
sigaction(SIGIO, &sa, NULL);
// 设置异步通知
fcntl(fd, F_SETOWN, getpid());
oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, oflags | FASYNC);
printf("异步通知已启用,等待信号...\n");
// 等待信号
while (running) {
pause();
}
return NULL;
}
// 主测试函数
int main(int argc, char** argv) {
pthread_t async_tid;
int test_mode = 0;
// 解析命令行参数
if (argc > 1) {
if (strcmp(argv[1], "all") == 0) {
test_mode = 0; // 全部测试
} else if (strcmp(argv[1], "led") == 0) {
test_mode = 1; // 只测试 LED
} else if (strcmp(argv[1], "button") == 0) {
test_mode = 2; // 只测试按键
} else if (strcmp(argv[1], "async") == 0) {
test_mode = 3; // 只测试异步
}
}
// 设置信号处理
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// 打开设备
printf("打开设备 /dev/led_button...\n");
fd = open("/dev/led_button", O_RDWR);
if (fd < 0) {
perror("无法打开设备");
return EXIT_FAILURE;
}
printf("✓ 设备打开成功 (fd=%d)\n", fd);
// 运行测试用例
switch (test_mode) {
case 0: // 全部测试
test_led_control();
test_button_read();
test_button_wait();
test_data_rw();
test_poll();
// 异步测试在后台运行
pthread_create(&async_tid, NULL, async_thread, NULL);
sleep(5);
running = 0;
pthread_join(async_tid, NULL);
break;
case 1: // 只测试 LED
test_led_control();
break;
case 2: // 只测试按键
test_button_read();
test_button_wait();
break;
case 3: // 只测试异步
pthread_create(&async_tid, NULL, async_thread, NULL);
printf("异步测试运行中,按 Ctrl+C 退出...\n");
while (running) {
sleep(1);
}
pthread_join(async_tid, NULL);
break;
}
// 清理
close(fd);
printf("\n测试完成!\n");
return EXIT_SUCCESS;
}
2.7.3 自动化测试脚本#
#!/bin/bash
# test_all.sh - 完整的驱动测试脚本
set -e # 遇到错误立即退出
echo "=== LED 按键驱动完整测试 ==="
echo "开始时间: $(date)"
echo ""
# 1. 检查环境
echo "1. 检查编译环境..."
if [ ! -f "/lib/modules/$(uname -r)/build/Makefile" ]; then
echo "错误: 内核构建目录不存在"
exit 1
fi
# 2. 清理旧文件
echo "2. 清理旧编译文件..."
make clean
# 3. 编译驱动
echo "3. 编译驱动模块..."
make modules
if [ ! -f "led_button_driver.ko" ]; then
echo "错误: 驱动编译失败"
exit 1
fi
echo "✓ 驱动编译成功"
# 4. 编译测试程序
echo "4. 编译测试程序..."
make test_apps
if [ ! -f "led_button_test" ]; then
echo "错误: 测试程序编译失败"
exit 1
fi
echo "✓ 测试程序编译成功"
# 5. 加载驱动
echo "5. 加载驱动模块..."
sudo rmmod led_button_driver 2>/dev/null || true
sudo insmod led_button_driver.ko
# 检查是否加载成功
if ! lsmod | grep -q led_button_driver; then
echo "错误: 驱动加载失败"
dmesg | tail -20
exit 1
fi
echo "✓ 驱动加载成功"
# 检查设备节点
sleep 1
if [ ! -e "/dev/led_button" ]; then
echo "警告: 设备节点未自动创建,尝试手动创建..."
sudo mknod /dev/led_button c $(cat /proc/devices | grep led_button | awk '{print $1}') 0
sudo chmod 666 /dev/led_button
fi
if [ -e "/dev/led_button" ]; then
echo "✓ 设备节点创建成功: /dev/led_button"
else
echo "错误: 设备节点创建失败"
exit 1
fi
# 6. 运行基本测试
echo "6. 运行基本功能测试..."
echo ""
sudo ./led_button_test led
echo "✓ LED 控制测试通过"
echo ""
sudo ./led_button_test button
echo "✓ 按键测试通过"
# 7. 运行高级测试
echo ""
echo "7. 运行高级功能测试..."
echo ""
sudo ./led_button_test all &
TEST_PID=$!
# 等待测试完成
sleep 10
if ps -p $TEST_PID > /dev/null; then
echo "测试超时,强制终止..."
kill -9 $TEST_PID 2>/dev/null
else
wait $TEST_PID
echo "✓ 完整测试通过"
fi
# 8. 压力测试
echo ""
echo "8. 运行压力测试..."
for i in {1..100}; do
sudo ./led_button_test led >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo "压力测试失败,第 $i 次迭代"
break
fi
echo -n "."
if [ $(($i % 50)) -eq 0 ]; then
echo " $i"
fi
done
echo ""
echo "✓ 压力测试通过(100 次迭代)"
# 9. 查看内核日志
echo ""
echo "9. 查看内核日志..."
echo "最后 20 条内核消息:"
dmesg | tail -20
# 10. 检查系统状态
echo ""
echo "10. 检查系统状态..."
echo "中断统计:"
cat /proc/interrupts | grep -E "(button|led_button)"
echo ""
echo "驱动信息:"
modinfo led_button_driver.ko | grep -E "(version|author|description)"
# 11. 清理
echo ""
echo "11. 清理测试环境..."
sudo rmmod led_button_driver
echo "✓ 驱动已卸载"
make clean
echo "✓ 编译文件已清理"
echo ""
echo "=== 所有测试完成 ==="
echo "结束时间: $(date)"
echo "测试结果: ✅ 通过"
三、调试技巧和问题排查#
3.1 分层调试方法#
#!/bin/bash
# debug_driver.sh - 分层调试脚本
echo "=== 驱动分层调试 ==="
echo ""
# 第 1 层:设备树调试
echo "1. 检查设备树..."
if [ -d "/proc/device-tree/led_button_device" ]; then
echo "✓ 设备树节点存在"
echo " 兼容性: $(cat /proc/device-tree/led_button_device/compatible)"
echo " 状态: $(cat /proc/device-tree/led_button_device/status)"
else
echo "✗ 设备树节点不存在"
echo " 尝试重新加载设备树..."
dtc -I dtb -O dts /boot/board.dtb > /tmp/current.dts
if grep -q "led_button_device" /tmp/current.dts; then
echo " 设备树节点已定义但未生效"
else
echo " 设备树节点未定义"
fi
fi
echo ""
# 第 2 层:GPIO调试
echo "2. 检查 GPIO 状态..."
if [ -f "/sys/kernel/debug/gpio" ]; then
echo "GPIO 状态:"
grep -E "(led|button)" /sys/kernel/debug/gpio || echo " 未找到相关 GPIO"
else
echo "✗ GPIO 调试文件不存在"
fi
echo ""
# 第 3 层:中断调试
echo "3. 检查中断状态..."
echo "中断统计:"
if grep -q "button" /proc/interrupts; then
grep "button" /proc/interrupts
IRQ_NUM=$(grep "button" /proc/interrupts | awk '{print $1}' | sed 's/://')
echo " 中断号: $IRQ_NUM"
if [ -f "/proc/irq/$IRQ_NUM/spurious" ]; then
echo " 假中断计数: $(cat /proc/irq/$IRQ_NUM/spurious)"
fi
else
echo "✗ 未找到按键中断"
fi
echo ""
# 第 4 层:字符设备调试
echo "4. 检查字符设备..."
if [ -e "/dev/led_button" ]; then
echo "✓ 设备节点存在"
echo " 设备号: $(ls -l /dev/led_button | awk '{print $5,$6}')"
echo " 权限: $(ls -l /dev/led_button | awk '{print $1}')"
# 测试打开设备
echo " 测试打开设备..."
if timeout 1 cat /dev/led_button 2>&1 | grep -q "设备"; then
echo "✓ 设备可正常访问"
else
echo "✗ 设备访问失败"
fi
else
echo "✗ 设备节点不存在"
echo " 尝试创建设备节点..."
MAJOR=$(grep led_button /proc/devices | awk '{print $1}')
if [ ! -z "$MAJOR" ]; then
sudo mknod /dev/led_button c $MAJOR 0
sudo chmod 666 /dev/led_button
echo " 已创建设备节点"
else
echo " 无法确定主设备号"
fi
fi
echo ""
# 第 5 层:内核日志分析
echo "5. 分析内核日志..."
echo "最后 50 条相关日志:"
dmesg | grep -E "(led_button|gpio|irq)" | tail -50
echo ""
echo "=== 调试完成 ==="
3.2 常见问题排查表#
问题现象 |
可能原因 |
排查步骤 |
解决方法 |
|---|---|---|---|
insmod 失败 |
内核版本不匹配 |
查看 dmesg 错误信息 |
使用正确内核编译 |
依赖缺失 |
检查内核版本 |
按顺序加载依赖模块 |
|
符号未导出 |
查看模块依赖 |
导出需要的符号 |
|
设备节点不存在 |
驱动未注册成功 |
检查 |
手动 mknod 创建设备 |
udev 规则问题 |
查看内核日志 |
修改 udev 规则 |
|
权限问题 |
检查 devtmpfs |
检查驱动注册代码 |
|
GPIO 无法控制 |
GPIO 号错误 |
检查设备树配置 |
修正设备树 GPIO 号 |
方向设置错误 |
使用 |
确认 GPIO 方向 |
|
时钟未使能 |
查看硬件手册 |
检查时钟配置 |
|
中断不触发 |
中断号错误 |
查看 |
修正中断配置 |
触发方式错误 |
检查设备树 interrupts 属性 |
检查中断控制器 |
|
中断被屏蔽 |
使用示波器验证信号 |
验证硬件连接 |
|
读写操作阻塞 |
缓冲区满/空 |
检查缓冲区状态 |
调整缓冲区大小 |
等待队列未唤醒 |
查看等待队列 |
正确使用唤醒函数 |
|
锁未释放 |
使用 lockdep 检查死锁 |
修复锁的使用 |
|
系统崩溃/死锁 |
内存越界 |
使用 kasan 检查内存 |
修复内存访问 |
中断上下文错误 |
分析内核转储 |
避免中断中睡眠 |
|
锁使用不当 |
使用 lockdep |
正确使用锁 |
3.3 性能优化技巧#
// 1. 使用 DMA 缓冲区减少拷贝
static int optimize_with_dma(struct device* dev) {
dma_addr_t dma_handle;
void* buffer;
// 分配 DMA 缓冲区
buffer = dma_alloc_coherent(dev, BUFFER_SIZE, &dma_handle, GFP_KERNEL);
if (!buffer) {
return -ENOMEM;
}
// 使用 DMA 传输
dma_map_single(dev, buffer, BUFFER_SIZE, DMA_TO_DEVICE);
// ... 操作完成后
dma_unmap_single(dev, dma_handle, BUFFER_SIZE, DMA_TO_DEVICE);
dma_free_coherent(dev, BUFFER_SIZE, buffer, dma_handle);
return 0;
}
// 2. 优化中断处理延迟
static irqreturn_t low_latency_irq_handler(int irq, void* dev_id) {
struct device_data* data = dev_id;
// 快速处理:只记录时间戳
data->irq_timestamp = ktime_get_ns();
// 延迟处理放到工作队列
queue_work(system_highpri_wq, &data->work);
return IRQ_HANDLED;
}
// 3. 使用 RCU 保护只读数据
static struct config_data* global_config;
static void update_config(struct config_data* new_config) {
struct config_data* old_config;
rcu_read_lock();
old_config = rcu_dereference(global_config);
rcu_assign_pointer(global_config, new_config);
synchronize_rcu();
kfree(old_config);
rcu_read_unlock();
}
// 4. 批量处理减少系统调用开销
static ssize_t batch_write(struct file* filp, const char __user* buf, size_t count,
loff_t* f_pos) {
struct iov_iter iter;
struct iovec iov = {
.iov_base = (void __user*)buf,
.iov_len = count,
};
iov_iter_init(&iter, WRITE, &iov, 1, count);
return drv_write_iter(filp, &iter, f_pos);
}
四、总结和最佳实践#
4.1 开发流程回顾#
硬件分析(1-2 天)
查看原理图和硬件手册
确定硬件连接和接口
规划所需的软件资源
设备树配置(1 天)
编写设备树节点
定义硬件属性和中断
编译和加载设备树
GPIO 驱动实现(1-2 天)
实现 GPIO 初始化和控制
添加必要的同步机制
测试基本的硬件控制
中断驱动实现(1-2 天)
注册中断处理函数
实现中断消抖和下半部处理
测试中断触发和处理
字符设备实现(2-3 天)
实现文件操作接口
添加高级功能(poll、异步通知等)
测试用户空间接口
整合和测试(2-3 天)
整合所有组件
编写测试程序
进行完整的功能和压力测试
优化和调试(持续)
性能优化
稳定性测试
问题修复
4.2 关键注意事项#
内存管理
使用
devm_系列函数自动管理资源检查所有内存分配的错误情况
避免内存泄漏和悬空指针
并发控制
正确使用锁保护共享数据
区分中断上下文和进程上下文
避免死锁和优先级反转
错误处理
所有函数都要有适当的错误处理
提供有意义的错误信息
实现完整的资源清理
兼容性
支持多种硬件变体
保持向后兼容性
遵循内核编码规范
4.3 推荐的调试工具#
工具 |
用途 |
示例命令 |
|---|---|---|
dmesg |
查看内核日志 |
|
strace |
跟踪系统调用 |
|
perf |
性能分析 |
|
ftrace |
内核函数跟踪 |
|
kgdb |
内核调试 |
|
sysrq |
系统请求 |
|
/proc |
系统信息 |
|
/sys |
内核对象 |
|
4.4 下一步学习建议#
深入学习内核机制
内存管理(slab、vmalloc、DMA)
进程调度和实时性
电源管理和休眠唤醒
掌握更多驱动类型
网络设备驱动
USB 设备驱动
输入子系统驱动
显示和 GPU 驱动
性能优化技巧
使用 DMA 和零拷贝
优化中断延迟
多核并行处理
参与开源社区
阅读内核源码
提交补丁和修复
参与邮件列表讨论
通过这个完整的驱动开发流程,您应该能够:
✅ 理解驱动开发的整体架构
✅ 掌握设备树、GPIO、中断、字符设备的开发方法
✅ 能够独立开发和调试完整的驱动
✅ 具备解决实际问题的能力