作为一名嵌入式Linux开发者,我深知字符设备驱动开发是连接硬件与操作系统的关键桥梁。在嵌入式系统中,80%以上的外设控制都需要通过字符设备驱动来实现。本文将基于我多年的实战经验,深入剖析字符设备驱动的核心架构和开发流程。
在裸机开发中,我们通常直接操作硬件寄存器来控制外设。这种方式虽然简单直接,但在Linux系统中会面临诸多问题:
Linux内核通过设备驱动完美解决了这些问题,提供了标准化的硬件访问接口。
字符设备驱动在嵌入式系统中尤为重要,它具有以下特点:
一个完整的字符设备驱动包含以下关键部分:
设备号是驱动在内核中的唯一标识,由主设备号和次设备号组成:
c复制dev_t dev_num; // 32位无符号整数
int major = MAJOR(dev_num); // 主设备号(12位)
int minor = MINOR(dev_num); // 次设备号(20位)
动态申请设备号是嵌入式开发的首选方式:
c复制int alloc_chrdev_region(&dev_num, 0, 1, "my_driver");
通过struct cdev向内核注册驱动:
c复制struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops); // 绑定操作接口
cdev_add(&my_cdev, dev_num, 1); // 注册到内核
这是驱动与VFS的桥梁,定义了硬件操作的所有方法:
c复制static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
现代Linux驱动通常自动创建设备文件:
c复制struct class *my_class = class_create(THIS_MODULE, "my_class");
device_create(my_class, NULL, dev_num, NULL, "my_device");
这是驱动与硬件直接交互的部分,通常包括:
内核驱动必须使用专用内存分配函数:
| API | 特点 | 适用场景 |
|---|---|---|
| kmalloc | 分配物理连续内存 | 小内存块(<1页) |
| kzalloc | kmalloc+清零初始化 | 结构体等需要初始化的内存 |
| vmalloc | 分配虚拟连续内存 | 大内存块(>1页) |
| kfree | 释放kmalloc/kzalloc内存 | 驱动卸载时 |
使用示例:
c复制char *buf = kzalloc(128, GFP_KERNEL);
if (!buf) {
return -ENOMEM;
}
kfree(buf);
现代Linux驱动应使用gpiod API:
c复制// GPIO输出示例
struct gpio_desc *led = gpiod_get(NULL, "led-gpio", GPIOD_OUT_LOW);
gpiod_set_value(led, 1); // 输出高电平
gpiod_put(led); // 释放GPIO
Linux中断采用顶半部+底半部架构:
c复制// 顶半部(不可睡眠)
irqreturn_t irq_handler(int irq, void *dev_id) {
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
// 底半部(可延迟执行)
void tasklet_func(unsigned long data) {
// 处理耗时操作
}
// 初始化
tasklet_init(&my_tasklet, tasklet_func, 0);
request_irq(irq_num, irq_handler, IRQF_TRIGGER_FALLING, "my_irq", NULL);
典型LED连接方式:
code复制LED阳极 → 限流电阻(220Ω) → GPIO引脚
LED阴极 → GND
控制逻辑:
c复制static dev_t led_dev;
static struct cdev led_cdev;
static struct class *led_class;
static struct gpio_desc *led_gpio;
c复制static int led_ctrl(char state) {
switch(state) {
case '1': gpiod_set_value(led_gpio, 1); break;
case '0': gpiod_set_value(led_gpio, 0); break;
default: return -EINVAL;
}
return 0;
}
c复制static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) {
char kernel_buf;
if (copy_from_user(&kernel_buf, buf, 1))
return -EFAULT;
return led_ctrl(kernel_buf);
}
c复制static int __init led_init(void) {
// 1. 申请设备号
// 2. 注册字符设备
// 3. 创建设备类
// 4. 创建设备文件
// 错误处理需逆序释放资源
}
static void __exit led_exit(void) {
// 1. 销毁设备文件
// 2. 销毁设备类
// 3. 注销字符设备
// 4. 释放设备号
// 5. 释放GPIO
}
makefile复制obj-m += led_drv.o
KERNELDIR ?= /path/to/kernel
CROSS_COMPILE ?= arm-linux-gnueabihf-
ARCH ?= arm
bash复制# 加载驱动
insmod led_drv.ko
# 控制LED
echo 1 > /dev/led # 点亮
echo 0 > /dev/led # 熄灭
# 查看日志
dmesg | grep LED
# 卸载驱动
rmmod led_drv
c复制static struct tasklet_struct key_tasklet;
static int key_status = 0;
void key_tasklet_func(unsigned long data) {
int val = gpiod_get_value(key_gpio);
if (val == 0) { // 按键按下(低电平)
mdelay(15); // 去抖延时
if (gpiod_get_value(key_gpio) == 0)
key_status = 1;
} else {
key_status = 0;
}
}
irqreturn_t key_irq_handler(int irq, void *dev_id) {
tasklet_schedule(&key_tasklet);
return IRQ_HANDLED;
}
c复制static ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *pos) {
if (copy_to_user(buf, &key_status, 1))
return -EFAULT;
return 1;
}
bash复制# 加载驱动
insmod key_drv.ko
# 查看按键状态
cat /dev/key
# 监控中断
cat /proc/interrupts | grep key
# 卸载驱动
rmmod key_drv
驱动加载失败:
设备文件无法访问:
硬件无响应:
中断优化:
内存优化:
延时处理:
掌握基础字符设备驱动后,可以进一步学习:
在实际项目中,我经常遇到需要同时控制多个外设的情况。这时就需要深入理解Linux内核的并发机制,如互斥锁、信号量、完成量等同步原语的使用。特别是在中断上下文和进程上下文共享数据时,必须谨慎处理竞态条件。