目录
一、驱动开发导论
驱动程序(Device Driver)是操作系统内核中直接与硬件交互的代码模块。它向上为应用层提供统一的软件接口(如 open/read/write/ioctl),向下通过寄存器、总线协议与硬件通信。驱动开发是嵌入式 Linux 的核心技能。
1.1 为什么需要驱动
| 无驱动 | 有驱动 |
|---|---|
| 每个应用都要直接操作硬件寄存器 | 驱动封装硬件细节,应用只需调用 open/read/write |
| 硬件变化时所有应用都要改 | 只改驱动,应用不变 |
| 无并发保护,多进程同时操作硬件会崩溃 | 内核提供互斥锁、信号量保护 |
| 无法统一接口(每个硬件 API 不同) | 遵循 Linux VFS 统一接口 |
1.2 驱动的三大类型
Linux 内核将驱动分为三大类,每类有不同的数据结构、操作接口和注册 API:
- 字符设备(Character Device)——最常见,字节流式顺序读写,通过
cdev+file_operations注册,节点在/dev/xxx。几乎所有 MCU 外设(UART、I2C、SPI、GPIO、ADC、传感器)都属于此类。 - 块设备(Block Device)——随机访问、固定大小块(通常 512B),通过
gendisk+request_queue注册,节点在/dev/xxx。典型设备:硬盘、SSD、SD 卡、U盘、eMMC、NAND Flash。块设备有缓存层和调度层,支持队列、合并、重排 I/O。 - 网络设备(Network Device)——面向网络协议栈,无文件节点(不在 /dev),通过
net_device+sk_buff收发数据包,在ifconfig/ip link中显示。典型设备:以太网网卡(eth0)、WiFi、4G/5G 模块、虚拟网络接口。
二、Linux 内核驱动模型
Linux 内核通过 设备模型(Device Model)统一管理所有硬件设备。核心概念是 kobject(内核对象)——每个设备、驱动、总线、类都是一个 kobject,形成一棵层次化的设备树。这棵树映射到用户空间就是 /sys 目录。
2.1 四大核心概念
| 概念 | 内核结构体 | /sys 路径 | 含义 |
|---|---|---|---|
| Bus(总线) | bus_type | /sys/bus | 设备连接的总线类型(platform/i2c/spi/pcie) |
| Device(设备) | device | /sys/devices | 具体的硬件实例(GPIO 控制器、传感器芯片) |
| Driver(驱动) | device_driver | /sys/drivers | 操作设备的代码(probe/remove) |
| Class(类) | class | /sys/class | 按功能分类(input/net/block),把设备聚合 |
2.2 总线-设备-驱动绑定过程
// ① 设备注册(内核启动或设备树解析时自动触发) platform_device_register(&my_pdev); // → 挂到 /sys/bus/platform/devices/my_device/ // ② 驱动注册(modprobe 时调用) platform_driver_register(&my_pdriver); // → 挂到 /sys/bus/platform/drivers/my_driver/ // ③ Bus 自动匹配(platform_match) // 匹配规则:of_match_table (设备树 compatible) → acpi_match → id_table // ④ 匹配成功 → 调用 driver.probe() // probe 中申请资源、注册字符设备、导出 sysfs 属性… // ⑤ 匹配失败 → 设备留在总线上等待,驱动被卸载 # 用户空间可以看到绑定结果: # ls /sys/bus/platform/devices/ | grep my_device # ls /sys/bus/platform/devices/my_device/driver → 若有输出说明已绑定 # 手动解绑:echo my_device > /sys/bus/platform/drivers/my_driver/unbind # 手动绑定:echo my_device > /sys/bus/platform/drivers/my_driver/bind
2.3 常用设备类型与对应子系统
| 子系统 | 总线类型 | 设备例子 | 注册函数 |
|---|---|---|---|
| Platform | platform | GPIO 控制器、I2C 控制器、片上外设 | platform_driver_register() |
| I2C | i2c | 温度传感器、EEPROM、ADC | i2c_driver_register() |
| SPI | spi | OLED 屏幕、Flash、高速 ADC | spi_driver_register() |
| USB | usb | U盘、鼠标、USB 转串口 | usb_driver_register() |
| PCIe | pci | 网卡、显卡、FPGA 加速卡 | pci_register_driver() |
platform 是最基础的总线——SoC 上所有不插在 PCI/USB 总线上的外设(GPIO、I2C/SPI 控制器、UART、Timer)都走 platform。
几乎所有 SoC 外设驱动都是 platform_driver。
三、内核模块基础
Linux 内核模块(Loadable Kernel Module, LKM)是可以动态加载/卸载的驱动代码。相比直接编译进内核,模块有以下优势:
- 热插拔:系统运行时加载/卸载,无需重启
- 节省内存:只加载需要的驱动
- 便于开发:修改后重新编译加载即可测试
// hello.c —— 最简单的内核模块 #include <linux/module.h> #include <linux/init.h> static int __init hello_init(void) { printk(KERN_INFO "Hello, Linux Driver! module loaded\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "Hello module unloaded\n"); } module_init(hello_init); // insmod 时调用 module_exit(hello_exit); // rmmod 时调用 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple hello module"); # 编译命令(Makefile) obj-m += hello.o KDIR := /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(PWD) modules # 加载/卸载 sudo insmod hello.ko dmesg | tail -5 # 查看 printk 输出 sudo rmmod hello
3.1 模块参数与符号导出
// 带参数的模块 #include <linux/moduleparam.h> static int rate = 9600; module_param(rate, int, 0644); MODULE_PARM_DESC(rate, "Baud rate (default 9600)"); // insmod hello.ko rate=115200 // 导出符号供其他模块使用 void my_driver_func(int val) { ... } EXPORT_SYMBOL(my_driver_func); // 允许 GPL 模块使用 EXPORT_SYMBOL_GPL(my_driver_func); 仅允许 GPL 模块使用
四、字符设备驱动
字符设备驱动是最常见的驱动类型,遵循 Linux VFS(虚拟文件系统)接口。应用空间通过 open/read/write/ioctl 操作设备文件(/dev/xxx),内核通过 file_operations 结构体将调用分发到驱动实现。
// led_chardev.c —— 一个最简字符设备驱动 #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/uaccess.h> #define DEVICE_NAME "led_chrdev" #define BUF_SIZE 1024 static int major; static struct cdev led_cdev; static struct device *led_device; static struct class *led_class; static char kernel_buf[BUF_SIZE]; static int open_count; static int led_open(struct inode *inode, struct file *filp) { open_count++; printk(KERN_INFO "LED device opened (count=%d)\n", open_count); return 0; } static int led_release(struct inode *inode, struct file *filp) { printk(KERN_INFO "LED device closed\n"); return 0; } static ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { int bytes_to_copy = min(count, (size_t)(BUF_SIZE - *offset)); if (bytes_to_copy == 0) return 0; copy_to_user(buf, kernel_buf + *offset, bytes_to_copy); *offset += bytes_to_copy; return bytes_to_copy; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) { int bytes_to_copy = min(count, (size_t)(BUF_SIZE - *offset)); if (bytes_to_copy == 0) return -EFAULT; copy_from_user(kernel_buf + *offset, buf, bytes_to_copy); *offset += bytes_to_copy; return bytes_to_copy; } static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case 0x1001: printk(KERN_INFO "LED ON, value=%lu\n", arg); break; case 0x1002: printk(KERN_INFO "LED OFF, value=%lu\n", arg); break; default: return -ENOTTY; } return 0; } static const struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .release = led_release, .read = led_read, .write = led_write, .unlocked_ioctl = led_ioctl, }; static int __init led_init(void) { dev_t dev_num = MKDEV(0, 0); major = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); cdev_init(&led_cdev, &led_fops); led_cdev.owner = THIS_MODULE; cdev_add(&led_cdev, dev_num, 1); led_class = class_create(DEVICE_NAME); led_device = device_create(led_class, NULL, dev_num, NULL, DEVICE_NAME); printk(KERN_INFO "LED char device registered, major=%d\n", major); return 0; } static void __exit led_exit(void) { dev_t dev_num = MKDEV(major, 0); device_destroy(led_class, dev_num); class_destroy(led_class); cdev_del(&led_cdev); unregister_chrdev_region(dev_num, 1); printk(KERN_INFO "LED char device unregistered\n"); } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name");
•
echo "hello" > /dev/led_chrdev 写入•
cat /dev/led_chrdev 读取•
dmesg | tail 查看 printk•
ls -la /dev/led_chrdev 查看设备文件
五、平台驱动
平台驱动(Platform Driver)是 Linux 为 SoC 片上外设设计的统一驱动框架。SoC 上的 GPIO、I2C、SPI 控制器、UART 等都是"平台设备"——它们不插在 PCI/USB 总线上,而是直接通过地址映射连接 CPU。
static const struct of_device_id my_of_match[] = { { .compatible = "vendor,my-device" }, { } }; MODULE_DEVICE_TABLE(of, my_of_match); static int my_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; // 1. 获取 IO 内存资源 struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -ENODEV; // 2. 请求并映射 IO 内存 my_base = devm_ioremap_resource(dev, res); if (IS_ERR(my_base)) return PTR_ERR(my_base); // 3. 获取 IRQ my_irq = platform_get_irq(pdev, 0); if (my_irq < 0) return my_irq; // 4. 注册中断处理 ret = devm_request_irq(dev, my_irq, my_irq_handler, 0, "my-irq", my_dev); // 5. 创建设备类与节点 my_class = class_create(DEVICE_NAME); my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME); return 0; } static int my_remove(struct platform_device *pdev) { device_destroy(my_class, MKDEV(major, 0)); class_destroy(my_class); // devm 资源自动释放,无需手动 iounmap/free_irq return 0; } static struct platform_driver my_driver = { .probe = my_probe, .remove = my_remove, .driver = { .name = "my_driver", .of_match_table = my_of_match, }, }; module_platform_driver(my_driver); // 自动生成 init/exit
六、设备树绑定
设备树(Device Tree, DT)是 Linux 内核描述硬件的标准方式。驱动不需要硬编码硬件地址/中断号,而是通过设备树获取。设备树用 DTS(文本)编写,经 DTC 编译为 DTB(二进制),内核启动时解析。
// 驱动中通过 compatible 匹配设备树节点 static const struct i2c_device_id tmp102_id[] = { { "tmp102", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, tmp102_id); static const struct of_device_id tmp102_of_match[] = { { .compatible = "ti,tmp102" }, { } }; static int tmp102_probe(struct i2c_client *client, const struct i2c_device_id *id) { printk(KERN_INFO "TMP102 probed at addr 0x%02x\n", client->addr); // 从设备树读取属性 if (client->dev.of_node) { int irq = of_irq_get(client->dev.of_node, 0); dev_info(&client->dev, "IRQ = %d\n", irq); } return 0; } static struct i2c_driver tmp102_driver = { .driver = { .name = "tmp102", .of_match_table = tmp102_of_match, }, .probe = tmp102_probe, .remove = tmp102_remove, .id_table = tmp102_id, }; module_i2c_driver(tmp102_driver);
七、GPIO 驱动实战
GPIO 是最基础的外设,几乎每个 SoC 都有。Linux 内核提供了两套 GPIO 操作 API:
gpiolib:底层 GPIO 控制器操作(request/free/direction_set)gpiod:推荐的高层 API(get/set_value,支持设备树)
// led_gpio.c —— LED GPIO 平台驱动 #include <linux/gpio/consumer.h> #include <linux/platform_device.h> #include <linux/fs.h> #include <linux/cdev.h> static struct gpio_desc *led_gpio; static int major; static struct cdev led_cdev; static long led_ioctl(struct file *f, unsigned int cmd, unsigned long arg) { switch (cmd) { case 0x1001: gpiod_set_value(led_gpio, 1); break; // LED ON case 0x1002: gpiod_set_value(led_gpio, 0); break; // LED OFF default: return -ENOTTY; } return 0; } static const struct file_operations led_fops = { .owner = THIS_MODULE, .unlocked_ioctl = led_ioctl, }; static int led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; dev_t dev_num; // 获取 GPIO(设备树中 led-gpios 属性) led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio); // 注册字符设备 alloc_chrdev_region(&dev_num, 0, 1, "led_gpio"); major = MAJOR(dev_num); cdev_init(&led_cdev, &led_fops); cdev_add(&led_cdev, dev_num, 1); dev_info(dev, "LED GPIO driver probed, major=%d\n", major); return 0; } static int led_remove(struct platform_device *pdev) { dev_t dev_num = MKDEV(major, 0); cdev_del(&led_cdev); unregister_chrdev_region(dev_num, 1); gpiod_set_value(led_gpio, 0); return 0; } static const struct of_device_id led_of_match[] = { { .compatible = "vendor,led-gpio" }, { } }; static struct platform_driver led_driver = { .probe = led_probe, .remove = led_remove, .driver = { .name = "led_gpio", .of_match_table = led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE("GPL");
八、I2C 驱动实战
I2C 是嵌入式中最常用的串行总线,连接温度传感器、EEPROM、ADC/DAC 等外设。Linux I2C 子系统分三层:Adapter(控制器驱动)、Client(设备实例)、Driver(设备驱动)。
// tmp102.c —— TI TMP102 温度传感器驱动 #include <linux/i2c.h> #include <linux/hwmon.h> #include <linux/hwmon-sysfs.h> struct tmp102_data { struct device *hwmon_dev; }; static int tmp102_read_temp(struct i2c_client *client) { s32 temp_raw = i2c_smbus_read_word_data(client, 0x00); if (temp_raw < 0) return temp_raw; // 高字节整数,低字节小数(0.0625°C/LSB) return (temp_raw >> 4) * 625; // 单位 milli-°C } static ssize_t show_temp_input(struct device *dev, struct device_attribute *attr, char *buf) { struct tmp102_data *data = dev_get_drvdata(dev); struct i2c_client *client = to_i2c_client(dev->parent); int temp = tmp102_read_temp(client); return sprintf(buf, %d\n, temp); } static DEVICE_ATTR(temp1_input, 0444, show_temp_input, NULL); static int tmp102_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct tmp102_data *data; data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; data->hwmon_dev = devm_hwmon_device_register_with_groups( &client->dev, "tmp102", data, NULL); sysfs_create_file(&client->dev.kobj, &dev_attr_temp1_input.attr); i2c_set_clientdata(client, data); dev_info(&client->dev, "TMP102 probed at 0x%02x\n", client->addr); return 0; } static int tmp102_remove(struct i2c_client *client) { sysfs_remove_file(&client->dev.kobj, &dev_attr_temp1_input.attr); return 0; } static const struct i2c_device_id tmp102_id[] = { { "tmp102", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, tmp102_id); static const struct of_device_id tmp102_of_match[] = { { .compatible = "ti,tmp102" }, { } }; static struct i2c_driver tmp102_driver = { .driver = { .name = "tmp102", .of_match_table = tmp102_of_match, }, .probe = tmp102_probe, .remove = tmp102_remove, .id_table = tmp102_id, }; module_i2c_driver(tmp102_driver); MODULE_LICENSE("GPL"); # 用户空间读取温度 cat /sys/class/hwmon/hwmon0/temp1_input # 输出 23125 → 23.125°C
九、SPI 驱动实战
SPI 比 I2C 速度更快但需要更多引脚(CS/SCK/MOSI/MISO)。Linux SPI 子系统架构与 I2C 类似:Controller Driver 管理硬件,Device Driver 实现外设协议。SPI 支持 Mode 0-3(CPOL/CPHA 组合)和不同位宽。
9.1 SPI Device Driver 代码示例
// spi_oled.c —— SPI 驱动 SSD1306 OLED 屏幕 #include <linux/spi/spi.h> #define OLED_CMD 0x00 // DC 低:命令 #define OLED_DATA 0x40 // DC 高:数据 static struct gpio_desc *dc_gpio; // DC 引脚 static struct gpio_desc *rst_gpio; // RST 引脚 // 发送一条命令 static int oled_write_cmd(struct spi_device *spi, u8 cmd) { gpiod_set_value(dc_gpio, 0); // DC=低 return spi_write(spi, &cmd, 1); } // 发送数据 static int oled_write_data(struct spi_device *spi, const u8 *buf, int len) { gpiod_set_value(dc_gpio, 1); // DC=高 return spi_write(spi, buf, len); } // probe:从设备树获取 GPIO,初始化 OLED static int oled_probe(struct spi_device *spi) { // ① 获取 DC 和 RST GPIO(devm_ 自动释放) dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_HIGH); rst_gpio = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH); // ② 硬件复位 gpiod_set_value(rst_gpio, 0); msleep(10); gpiod_set_value(rst_gpio, 1); msleep(10); // ③ 初始化 OLED(发送一系列命令) oled_write_cmd(spi, 0xAE); // 关闭显示 oled_write_cmd(spi, 0xD5); // 设置时钟分频 oled_write_cmd(spi, 0x80); // ... 更多初始化命令 oled_write_cmd(spi, 0xAF); // 开启显示 return 0; } static DEVICE_ATTR(compatible, 0444, ...); // SPI 设备匹配表 static const struct of_device_id oled_of_match[] = { { .compatible = "vendor,ssd1306-oled" }, { } }; MODULE_DEVICE_TABLE(of, oled_of_match); static struct spi_driver oled_driver = { .probe = oled_probe, .remove = oled_remove, .driver = { .name = "ssd1306_oled", .of_match_table = oled_of_match, }, }; module_spi_driver(oled_driver); // 自动生成 init/exit
• SPI 速度快(几十 MHz vs I2C 400KHz)、支持全双工,但需要更多 GPIO
• I2C 只需 2 根线(SCL/SDA)、支持多主多从、有 ACK 机制,但速度慢
• OLED、LCD、ADC/DAC、Flash(SPI Nor)→ 选 SPI;EEPROM、小传感器 → 选 I2C
十、中断与下半部
中断驱动硬件通知 CPU 有事件发生。Linux 中断处理分两阶段:上半部(hardirq,原子上下文)和 下半部(softirq/tasklet/workqueue,可睡眠)。上半部只做标记,将耗时工作推给下半部。
10.1 原子上下文 vs 睡眠上下文
| 上下文 | 位置 | 能否睡眠 | 可用 API |
|---|---|---|---|
| HardIRQ(硬中断) | irqreturn_t handler | ❌ 绝对不能 | spinlock、atomic_t、kmalloc(GFP_ATOMIC) |
| softirq / tasklet | 下半部(原子型) | ❌ 不能 | spinlock、atomic_t |
| workqueue | 下半部(可睡眠型) | ✅ 可以 | mutex、kmalloc(GFP_KERNEL)、msleep |
| 进程上下文 | file_operations | ✅ 可以 | 所有 API |
10.2 按键中断驱动完整示例
// key_irq.c —— 按键中断 + workqueue 处理 #include <linux/interrupt.h> #include <linux/workqueue.h> static int key_irq; // 中断号 static struct gpio_desc *key_gpio; 按键 GPIO // 工作队列:在进程上下文处理耗时工作 static struct work_struct key_work; // 下半部处理(可睡眠) static void key_work_func(struct work_struct *work) { // 读取按键状态(需要 debounce) msleep(20); // 消抖,这里可以睡眠! int val = gpiod_get_value(key_gpio); if (val == 0) { // 按键按下,执行业务逻辑 dev_info(dev, "Key pressed!\n"); } } // 上半部 HardIRQ(原子上下文,绝对不能睡眠!) static irqreturn_t key_irq_handler(int irq, void *dev_id) { // ① 读取 GPIO 电平,确定是按下还是释放 // ② 只是标记一下,然后把活推给 workqueue schedule_work(&key_work); // 投递到系统工作队列 return IRQ_HANDLED; // 中断已处理 } // probe 中注册中断 static int my_probe(struct platform_device *pdev) { key_gpio = devm_gpiod_get(&pdev->dev, "key", GPIOD_IN); key_irq = gpiod_to_irq(key_gpio); // GPIO → 中断号 // 初始化工作队列 INIT_WORK(&key_work, key_work_func); // 注册中断(devm_ 自动释放) devm_request_irq(&pdev->dev, key_irq, key_irq_handler, IRQF_TRIGGER_FALLING, "key_irq", NULL); return 0; } // remove 中取消 workqueue 并等待完成 static int my_remove(struct platform_device *pdev) { cancel_work_sync(&key_work); // 等 work 真正跑完 return 0; }
① workqueue——最常用,可睡眠,有自己的线程池。绝大多数场景都用它。
② tasklet——不能睡眠,比 workqueue 更早执行。需要很高优先级、且绝对不会睡眠时用。
③ softirq——内核内部使用,驱动一般不直接操作。
④ 不要在 HardIRQ 中做任何可能耗时的事情——会拖慢系统响应、影响实时性。
十一、DMA 与内存管理
DMA(Direct Memory Access)让外设直接访问内存,无需 CPU 介入,大幅提升吞吐量。DMA 缓冲区需要特殊内存属性(连续、cache-safe),用 dma_alloc_coherent() 分配。
11.1 DMA vs CPU 拷贝对比
| CPU memcpy | DMA | |
|---|---|---|
| CPU 占用 | 100%(阻塞 CPU) | 0%(CPU 可并行执行) |
| 延迟 | 低(缓存命中快) | 较高(需要 DMA 握手) |
| 吞吐量 | 受限 CPU 频率 | 受限于总线带宽(通常更高) |
| 适用场景 | 小块数据、低频率 | 大块连续数据、高频率(音频/视频/高速采集) |
11.2 DMA 缓冲区分配示例
// dma_example.c —— SPI + DMA 发送 #include <linux/dma-mapping.h> #include <linux/spi/spi.h> #define DMA_BUF_SIZE 4096 static struct dma_chan *rx_chan; static struct dma_chan *tx_chan; static void *dma_buf; static dma_addr_t dma_handle; // probe 中分配 DMA 缓冲区 static int my_probe(struct platform_device *pdev) { // ① 分配 coherent 内存(CPU 和 DMA 都能访问,cache-safe) dma_buf = dma_alloc_coherent(dev, DMA_BUF_SIZE, &dma_handle, GFP_KERNEL); if (!dma_buf) return -ENOMEM; // ② 获取 DMA 通道 tx_chan = dma_request_slave_channel(dev, "tx"); rx_chan = dma_request_slave_channel(dev, "rx"); return 0; } // remove 中释放(不用 devm_ 就手动释放) static int my_remove(struct platform_device *pdev) { dma_free_coherent(dev, DMA_BUF_SIZE, dma_buf, dma_handle); dma_release_channel(tx_chan); dma_release_channel(rx_chan); return 0; } // DMA 传输数据 static void dma_transfer(struct device *dev) { struct dma_async_tx_descriptor *tx_desc; // 准备 DMA 描述符 tx_desc = dmaengine_prep_slave_single(tx_chan, dma_handle, DMA_BUF_SIZE, DMA_MEM_TO_DEV, 0); // 提交并启动 DMA dma_submit(tx_desc); dma_async_issue_pending(tx_chan); // 等 DMA 完成(也可以用回调异步通知) dma_sync_wait(tx_chan, tx_desc->cookie); }
①
dma_alloc_coherent() ≠ kmalloc()——DMA buffer 必须物理连续且 cache-coherent② 如果需要多次小传输,可以用
dma_map_single() / dma_unmap_single() 动态映射 kmalloc 内存③ 64 位地址需要在设备树中声明
dma-coherent 属性,并在驱动中设置 dma_set_mask(dev, DMA_BIT_MASK(64))
十二、内核-用户空间接口
驱动需要向用户空间暴露操作接口,Linux 提供四种方式:ioctl、sysfs、procfs、debugfs。它们的定位和使用场景各有不同,下图展示了四者的对比:
12.1 四种接口对比
| 接口 | 文件位置 | 特点 | 适用场景 |
|---|---|---|---|
| ioctl | /dev/xxx | 灵活、可传结构体、需要定义 cmd | 设备控制(开/关/重置/配置) |
| sysfs | /sys/class/xxx/ | 单值属性、可脚本化、有 show/store | 导出状态/配置参数(只读或单值读写) |
| procfs | /proc/driver/xxx | 历史悠久、可输出多行文本 | 导出运行时信息、全局状态 |
| debugfs | /sys/kernel/debug/xxx | 调试专用、内核可裁掉 | 临时调试、不应在生产环境依赖 |
12.2 sysfs 属性导出示例
// sysfs_ex.c —— 用 sysfs 导出 LED 亮度属性 #include <linux/sysfs.h> static int led_brightness = 50; // 当前亮度 0-100 // show:驱动 → 用户空间(读取属性) static ssize_t brightness_show(struct device *dev, struct device_attribute *attr, char *buf) { return sprintf(buf, "%d\n", led_brightness); } // store:用户空间 → 驱动(写入属性) static ssize_t brightness_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { kstrtoint(buf, 0, &led_brightness); if (led_brightness < 0) led_brightness = 0; if (led_brightness > 100) led_brightness = 100; return count; } // DEVICE_ATTR 宏自动生成 show/store static DEVICE_ATTR(brightness, 0644, brightness_show, brightness_store); // probe 中创建 sysfs 属性 device_create_file(dev, &dev_attr_brightness); // remove 中删除 device_remove_file(dev, &dev_attr_brightness); // 用户空间访问: // cat /sys/class/mydev/mydev0/brightness ← 读取 // echo 80 > /sys/class/mydev/mydev0/brightness ← 写入
12.3 ioctl 示例
// ioctl_ex.c —— ioctl 实现 LED 控制 #include <linux/ioctl.h> // 定义 ioctl 命令(_IO / _IOR / _IOW / _IOWR 宏) #define LED_MAGIC 'L' #define LED_ON _IO(LED_MAGIC, 1) #define LED_OFF _IO(LED_MAGIC, 2) #define LED_SET _IOW(LED_MAGIC, 3, int) static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case LED_ON: gpiod_set_value(led_gpio, 1); break; case LED_OFF: gpiod_set_value(led_gpio, 0); break; case LED_SET: led_brightness = (int)arg; break; default: return -ENOTTY; // 不支持的命令 } return 0; } static const struct file_operations led_fops = { .owner = THIS_MODULE, .unlocked_ioctl = led_ioctl, // ... open/release/read/write };
① 有状态/配置类属性 → sysfs(脚本友好)
② 复杂控制/传结构体 → ioctl(灵活但需定义命令)
③ 运行时调试信息 → procfs 或 debugfs(debugfs 可裁掉)
④ 不要用 procfs 做 sysfs 的事——内核社区已明确:新代码优先 sysfs
十三、驱动编译与构建
Linux 内核驱动使用 Kbuild 构建系统,类似 Kconfig 配置。编译分为 模块编译(.ko 可动态加载)和 内建编译(直接编进内核 Image)。下面是最常见的外部模块编译流程:
13.1 Makefile 模板(外部模块)
# Makefile —— 驱动编译入口 # 指定驱动名,obj-m 表示编译为模块(.ko) # obj-y 表示编进内核(built-in) obj-m += my_led_driver.o # 多文件驱动(两个 .c 文件编译进同一个 .ko) # my_led_driver-objs = led_main.o led_gpio.o # 内核源码目录(交叉编译时要指定) # KDIR := /lib/modules/$(shell uname -r)/build # 本机编译 KDIR := $(HOME)/linux-source/5.15.0 # 或你的内核源码树 # 交叉编译工具链(可选,本机编译可省略) # CROSS_COMPILE := arm-linux-gnueabihf- all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean # 编译后得到:my_led_driver.ko # scp my_led_driver.ko root@target:/lib/modules/$(uname -r)/ # modprobe my_led_driver
13.2 Kconfig 选项(如果驱动要进内核源码树)
# drivers/my_drivers/Kconfig config MY_LED_DRIVER tristate "My LED GPIO Driver" depends on GPIOLIB help Say Y here to enable the GPIO LED driver. If unsure, say M (module). # drivers/my_drivers/Makefile obj-$(CONFIG_MY_LED_DRIVER) += led_driver.o # 编译步骤: # 1. make menuconfig → Device Drivers → My Drivers → 选 M 或 Y # 2. make modules (如果选 M) # 3. 或 make Image (如果选 Y,编进内核)
13.3 交叉编译实战(树莓派 4B 为例)
# Step 1: 获取内核源码(版本必须和目标机 uname -r 一致) git clone --depth=1 -b rpi-5.15.y https://github.com/raspberrypi/linux # Step 2: 安装工具链 sudo apt install gcc-aarch64-linux-gnu # Step 3: 使用目标机 defconfig KERNEL=kernel8 make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bcm2711_defconfig # Step 4: 编译模块(-j 并行加速) make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) modules_prepare # Step 5: 编译你的驱动(在驱动目录下 make) make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -C /path/to/linux M=$(pwd) modules # Step 6: 验证驱动内核版本 modinfo my_driver.ko | grep vermagic # Step 7: 部署到目标机 scp my_driver.ko pi@raspberrypi:/tmp/ ssh pi@raspberrypi "sudo insmod /tmp/my_driver.ko"
① Kernel version mismatch——.ko 的 vermagic 必须和目标机内核精确一致(包括 CONFIG 选项),否则 modprobe 拒绝加载
② extern "C"——驱动代码用 C 写,C++ 调用内核 API 需要 extern "C"
③ 不能用 C 标准库——驱动不能 include 标准 C 头文件(stdlib/stdio/malloc),用内核替代(kmalloc/printk)
④ -Werror——内核开启了警告即错误,编译警告要及时修
十四、驱动调试
内核驱动调试比应用更困难——没有 gdb 直接调试内核代码,需要依赖 printk、ftrace、kprobe、kgdb 等工具。调试的基本原则是:先 printk 看日志 → 再 ftrace 追踪调用 → 必要时上 KGDB。
14.1 printk 使用技巧
// 基本 printk(优先用 pr_info/pr_err 封装) printk(KERN_INFO "led: probe ok, base=%#x\n", base_addr); pr_info("led: probe ok\n"); // 自动加函数名前缀 pr_err("led: probe failed: %d\n", ret); // 动态调试(dynamic debug)——生产环境保留但不输出 // 代码中用 dev_dbg / dev_vdbg dev_dbg(dev, "probe called\n"); // 默认不输出 // 运行时开启: # echo 'file my_driver.c +p' > /sys/kernel/debug/dynamic_debug/control # echo 'func my_probe +p' > /sys/kernel/debug/dynamic_debug/control // 查看内核日志: dmesg | tail -50 # 最近 50 行 dmesg -w # 实时追踪(tail -f) dmesg -T # 带人类可读时间戳 cat /var/log/kern.log # 持久化日志(syslog) // 日志级别控制: # cat /proc/sys/kernel/printk → 查看当前级别 # echo '8' > /proc/sys/kernel/printk → 显示所有级别的 printk
14.2 ftrace 追踪函数调用
# ftrace 无需改源码,运行时追踪函数调用链 # ① 查看可用追踪器 cat /sys/kernel/tracing/available_tracers # ② 函数图追踪(看调用关系和耗时) cd /sys/kernel/tracing echo function_graph > current_tracer echo 1 > tracing_on # 执行你的操作… echo 0 > tracing_on cat trace # ③ 只追踪你关心的函数 echo my_probe > set_ftrace_filter echo my_remove > set_ftrace_filter echo function > current_tracer # 加载/卸载模块,然后 cat trace 看结果 # ④ trace-cmd 工具(更强大) sudo trace-cmd record -p function_graph -g my_probe -g my_remove sudo trace-cmd report trace.dat # 生成可读报告 # ⑤ perf(另一个选择) sudo perf record -g -- sleep 5 # 5秒调用栈采样 sudo perf report # 交互式分析
14.3 Oops 分析实战
# Oops 是内核的"段错误",dmesg 里会有完整调用栈 # Step 1: 找 dmesg 里的关键行 dmesg | grep -A 30 -i "oops\|panic\|BUG:" # Step 2: 看 Call trace(调用栈回溯) # 找到你的驱动函数名,它是崩溃的触发点 # Step 3: addr2line 解析地址 → 源码行号 # Call trace 里类似: # [] my_probe+0x50/0x100 # 0x50 是函数内偏移,0x100 是函数总大小 # 编译时用 -g 保留调试信息 addr2line -e my_driver.ko 0x50 -f # 输出:my_probe # /home/dev/my_driver.c:42 # Step 4: objdump 反汇编看那行代码 objdump -d my_driver.ko | grep -A5 -B5 "call trace 里的地址"
① 先用 printk 定位大方向(哪个函数、哪个条件),再上 ftrace
② printk 不要太频繁——会改变时序、掩盖竞态条件
③ dmesg 缓冲有限(默认环形 64KB),加
log_buf_len=2M 内核启动参数扩大缓冲④ KGDB 需要串口或 JTAG 连接,适合开发阶段,但生产环境一般不用
十五、常见问题与故障排查
驱动开发中遇到的问题千奇百怪,但掌握正确的排查方法论可以事半功倍。核心思路是:先看现象 → 缩小范围 → 验证假设 → 定位根因。以下是常见故障分类和排查要点:
15.1 模块加载/卸载类问题
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| insmod 报 "Unknown symbol" | 依赖模块未加载 / 符号未 EXPORT | modprobe 替代 insmod;检查依赖模块 lsmod |
| modprobe 报 "Module version mismatch" | 驱动编译内核版本 ≠ 运行内核版本 | 检查 uname -r 和驱动的 vermagic |
| rmmod 报 "Module is in use" | 有进程打开了设备文件 | fuser /dev/xxx 或 lsof /dev/xxx |
| rmmod 成功但设备节点还在 | 驱动中 class_create 但没 class_destroy | 补 remove 中的清理代码,或检查 devm_ 是否正确使用 |
15.2 probe 失败类问题
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| dmesg 显示 "probe failed" | compatible 字符串不匹配 | 检查 DTS 的 compatible 和驱动 of_match_table 是否一致 |
| "Failed to request GPIO" | GPIO 已被其他驱动占用 / 设备树 gpios 属性写错 | cat /sys/kernel/debug/gpio 查看占用 |
| "Failed to request IRQ" | 中断号冲突 / 设备树 interrupts 属性错误 | cat /proc/interrupts 查看中断表 |
| "Failed to ioremap" | 物理地址范围不对 / 地址被其他驱动映射 | 检查 DTS reg 属性的起始地址和长度 |
15.3 运行时崩溃类问题(Oops / Panic)
| 典型日志关键词 | 最可能原因 | 修复方向 |
|---|---|---|
| "Unable to handle kernel NULL pointer dereference" | 空指针解引用 | probe 返回值未检查;指针在 remove 后被引用 |
| "BUG: unable to handle kernel paging request" | use-after-free / 越界 | 用 kfree 释放后又访问;缓冲区边界计算错误 |
| "kernel stack overflow" | 内核栈使用超过 8KB | 大数组改用 kmalloc;递归太深;spinlock 嵌套 |
| "INFO: task xxx blocked for more than 120 seconds" | 死锁 / mutex 在原子上下文 | HardIRQ/tasklet 中不能用 mutex;锁顺序要一致 |
① devm_ 前缀的 API 自动管理资源——probe 中用了 devm_ioremap/devm_gpio_request,remove 中不用手动清理
② 原子上下文(HardIRQ / spinlock / tasklet)绝对不能睡眠——不能用 mutex、不能调用可能阻塞的函数
③ DMA 缓冲区用 dma_alloc_coherent(),不能用 kmalloc——需要物理连续且 cache-safe
十六、完整实战:从设备树到用户测试
把前面所有知识点串起来,完成一个完整的 LED + TMP102 温度传感器驱动 项目。
驱动开发核心要点
- 驱动分层:应用→VFS→驱动→硬件,
file_operations是关键接口 - 模块加载:modprobe 自动解析依赖,insmod 不解析
- 平台驱动:platform_driver + probe/remove,devm_ 自动管理资源
- 设备树:compatible 是驱动与设备匹配的桥梁,reg/interrupts/gpios 传递硬件信息
- 字符设备:cdev_init + cdev_add + alloc_chrdev_region + class_create
- I2C/SPI:Adapter Driver 管硬件,Device Driver 管协议,Core 协调两者
- 中断:HardIRQ 原子上下文不能睡眠 → 标记;Workqueue 可睡眠 → 处理耗时逻辑
- DMA:
dma_alloc_coherent()分配连续 cache-safe 内存,不能用 kmalloc - 用户接口:ioctl 灵活、sysfs 可脚本化、debugfs 调试专用
- 调试:printk→ftrace→kprobe→KGDB,addr2line 解析 Oops 地址
- 原子 vs 睡眠:HardIRQ/spinlock/rwlock/tasklet 不能睡眠;mutex/workqueue 可以睡眠
- 资源管理:devm_ 前缀的 API 自动释放,remove 中不用手动清理