为什么要写 Linux 驱动
跑 Linux 的 SoC 越来越多,从树莓派到瑞芯微全志,都要接各种外设:传感器、显示屏、SPI 屏、CAN 收发器。内核已经自带了大部分常见驱动,但你板子上的新外设、自定义硬件,就得自己写驱动。驱动说白了就是:把硬件寄存器的读写封装成 Linux 标准接口(/dev/xxx、sysfs),让用户态应用像操作文件一样操作硬件。
写驱动 = 在内核态写代码。内核里一个空指针就能让整个系统 panic,比 MCU 上写程序严格得多。
这篇从 Hello World 模块写到一个能和用户态 ioctl 通信的字符设备驱动。
内核模块骨架
内核模块(.ko)可以动态加载进内核,不用重新编译整内核。最骨架的模块长这样:
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init(void) {
printk(KERN_INFO "hello driver loaded\n");
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO "hello driver unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("hello world driver");
用交叉编译工具链编出 .ko,insmod 加载、rmmod 卸载,dmesg 看打印。
字符设备:最常用的一类
字符设备是"按字节流"访问的设备,比如 LED、传感器、自定义板卡。要实现 open/read/write/ioctl 这几个文件操作。
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
static int __init my_init(void) {
alloc_chrdev_region(&dev, 0, 1, "mydev");
cdev_init(&cdev, &fops);
cdev_add(&cdev, dev, 1);
class_create(THIS_MODULE, "mydev");
device_create(mycls, NULL, dev, NULL, "mydev0");
return 0;
}
// 用户态就能 open("/dev/mydev0", O_RDWR)
设备树:描述硬件
现代 ARM 内核不用硬编码板级信息,而是用设备树(DTS)描述:这个板子上有什么芯片、挂在哪个总线、中断号多少、寄存器基址多少。驱动通过 of_match_table 匹配 DTS 节点。
// myboard.dts
myled@40010000 {
compatible = "myorg,myled-1.0";
reg = <0x40010000 0x1000>;
interrupts = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>;
status = "okay";
};
// 驱动里
static const struct of_device_id myled_of_match[] = {
{ .compatible = "myorg,myled-1.0" },
{}
};
MODULE_DEVICE_TABLE(of, myled_of_match);
ioctl:和用户态对话
read/write 适合流式数据,"设置参数、读状态"这种命令式操作就用 ioctl。
// 驱动侧
long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case MY_LED_ON: writel(1, reg_base + 0x0); break;
case MY_LED_OFF: writel(0, reg_base + 0x0); break;
case MY_LED_GET: {
uint32_t v = readl(reg_base + 0x0);
copy_to_user((void __user *)arg, &v, sizeof(v));
break;
}}
return 0;
}
// 用户态
ioctl(fd, MY_LED_ON);
ioctl(fd, MY_LED_GET, &val);
中断与并发
驱动要响应硬件中断,要用 top half + bottom half 设计:中断顶半部只做最少的事(读状态、关中断源),把耗时工作推迟到底半部(tasklet / workqueue)。
并发要点
- 自旋锁短临界区、不能睡眠。
- 互斥锁可睡眠、长临界区。
- 原子变量计数器类。
- 避免死锁固定锁获取顺序。
调试手段
// 1. printk + dmesg:最基础
// 2. dev_dbg:开动态调试
// 3. ftrace:看函数调用时序
// 4. gdb + kgdb:在线调试(要提前配内核)
// 5. /proc、/sys:把内部状态暴露出来
// 6. 板级验证:先在 QEMU 上跑通再上真板
写驱动最难的不是写代码,是查"为什么寄存器读回来是 0"。十有八九是时钟没开、引脚复用没配、电源没上电。
把这套骨架跑通,接新外设就是查手册、写 DTS、填 read/write 的事了。