3. Linux驱动开发基础
在 RISC-V Linux 平台上做驱动适配,第一步不是急着写寄存器,而是先理解 Linux 驱动框架的“骨架”。内核模块机制决定了代码如何进入内核,字符设备与文件操作接口决定了用户空间如何访问硬件,平台设备与设备树决定了硬件资源如何被描述和匹配,而并发控制则决定了多核 RISC-V 上驱动是否稳定。本节把这五块串成一条完整的驱动开发主线。
一、内核模块机制:驱动的装载与卸载
Linux 驱动通常以可加载内核模块(LKM)形式存在,也可以直接编译进内核。模块机制让驱动可以动态插入、卸载,便于调试和裁剪。一个最小模块包含入口、出口和许可证声明:
#include <linux/module.h>
#include <linux/init.h>
static int __init demo_init(void)
{
pr_info("demo module init\n");
return 0;
}
static void __exit demo_exit(void)
{
pr_info("demo module exit\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("riscv-driver");
MODULE_DESCRIPTION("RISC-V Linux demo module");
对应的 Makefile 需要指向内核构建目录,RISC-V 交叉编译时通过 ARCH=riscv 与 CROSS_COMPILE 指定工具链:
obj-m += demo.o
KDIR := /path/to/linux
all:
make -C $(KDIR) M=$(PWD) ARCH=riscv \
CROSS_COMPILE=riscv64-linux-gnu- modules
clean:
make -C $(KDIR) M=$(PWD) clean
加载使用 insmod demo.ko,卸载使用 rmmod demo,查看日志用 dmesg | tail。模块参数通过 module_param() 暴露,例如 module_param(debug, int, 0644),可在加载时传入 insmod demo.ko debug=1。需要注意:模块只能使用内核导出的符号,用户空间库函数如 printf、malloc 不可用,应使用 pr_info、kmalloc 等内核接口。
二、字符设备与文件操作接口
字符设备是驱动适配中最常见的类型,它以字节流方式访问硬件,用户空间通过 /dev/xxx 节点调用 open/read/write/ioctl/close。内核侧用 file_operations 结构体把系统调用映射到驱动函数:
static int demo_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t demo_read(struct file *filp, char __user *buf,
size_t len, loff_t *off)
{
return simple_read_from_buffer(buf, len, off,
"hello riscv\n", 12);
}
static const struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.read = demo_read,
};
设备号由主设备号和次设备号组成,注册方式有静态 register_chrdev_region() 和动态 alloc_chrdev_region()。现代驱动更推荐使用 cdev_init() + cdev_add() 绑定 file_operations,再配合 class_create() 与 device_create() 自动生成 /dev 节点,避免手工 mknod。用户空间读写时,驱动必须使用 copy_to_user() 和 copy_from_user(),不能直接解引用用户指针。
三、平台设备、设备树与匹配机制
RISC-V SoC 中大量外设是“平台设备”,它们不通过总线枚举,而是由设备树描述。设备树节点提供寄存器基地址、中断号、时钟、引脚等资源,驱动通过 of_match_table 与 compatible 属性匹配:
static const struct of_device_id demo_of_match[] = {
{ .compatible = "vendor,demo-uart" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "demo-uart",
.of_match_table = demo_of_match,
},
};
module_platform_driver(demo_driver);
设备树片段如下,reg 描述寄存器范围,interrupts 描述中断,clocks 描述时钟源:
demo_uart: serial@10000000 {
compatible = "vendor,demo-uart";
reg = <0x0 0x10000000 0x0 0x1000>;
interrupts = <10>;
clocks = <&uart_clk>;
status = "okay";
};
驱动 probe 中常用 platform_get_resource() 取寄存器,devm_ioremap_resource() 做映射,platform_get_irq() 取中断,devm_clk_get() 取时钟。使用 devm_ 前缀的资源会自动释放,能显著减少出错路径。
四、并发控制:多核 RISC-V 下的稳定性关键
RISC-V 多核平台中,驱动可能同时被多个 CPU 上的进程、中断处理程序访问,共享数据必须保护。常见手段如下表:
| 机制 | 适用场景 | 是否可睡眠 | 典型接口 |
|---|---|---|---|
| 自旋锁 | 短临界区、中断上下文 | 否 | spin_lock_irqsave / spin_unlock_irqrestore |
| 互斥锁 | 可能睡眠的进程上下文 | 是 | mutex_lock / mutex_unlock |
| 原子操作 | 计数器、标志位 | 否 | atomic_inc / atomic_read |
| 完成量 | 等待某事件完成 | 是 | wait_for_completion / complete |
| RCU | 读多写少链表 | 读可睡眠 | rcu_read_lock / synchronize_rcu |
常见误区:在中断处理程序中调用 mutex_lock() 或 copy_to_user(),会导致睡眠甚至崩溃;自旋锁持有期间调用可能睡眠的函数,会造成死锁;忘记 spin_lock_irqsave 保存中断状态,可能破坏中断上下文。RISC-V 上还需注意内存屏障与 wmb()/rmb() 的使用。
五、案例与复盘:一个虚拟字符设备驱动
假设我们要为 RISC-V 开发板上的一个虚拟传感器写驱动:设备树节点 compatible = "vendor,virt-sensor",寄存器 0x10001000,中断 11。驱动 probe 中映射寄存器、申请中断、注册字符设备 /dev/virt_sensor。read 返回最近一次采样值,ioctl 用于启动/停止采样。中断处理中读取寄存器并更新 atomic_t 缓存,read 时用 copy_to_user 返回。整个过程用 mutex 保护配置状态,用 spin_lock_irqsave 保护中断与 read 共享的缓存。
调试建议:先用 cat /proc/devices 确认设备号,用 ls /sys/class/virt_sensor/ 确认 class 注册,用 cat /proc/interrupts 确认中断触发次数,用 dmesg -w 实时观察 probe 与 remove 日志。若 probe 未执行,优先检查设备树 status 与 compatible 是否匹配。
掌握模块机制、字符设备、平台设备、设备树、文件操作接口与并发控制这六项基础后,后续的 GPIO、I2C、SPI、DMA 等驱动适配都可以套用同一套结构:设备树描述资源,平台驱动匹配并初始化,字符或子系统接口暴露给用户空间,并发控制保证多核稳定。