为什么要写 Linux 驱动

跑 Linux 的 SoC 越来越多,从树莓派到瑞芯微全志,都要接各种外设:传感器、显示屏、SPI 屏、CAN 收发器。内核已经自带了大部分常见驱动,但你板子上的新外设、自定义硬件,就得自己写驱动。驱动说白了就是:把硬件寄存器的读写封装成 Linux 标准接口(/dev/xxx、sysfs),让用户态应用像操作文件一样操作硬件。

写驱动 = 在内核态写代码。内核里一个空指针就能让整个系统 panic,比 MCU 上写程序严格得多。
Linux 驱动配图:中央开发板周围空白圆角节点以光线连接
驱动 = 内核和硬件之间的翻译层

这篇从 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);