8、设备驱动:SMP安全的驱动编写、DMA一致性、设备树与多核绑定
好,我们进入第八章。这一章讲的是设备驱动在多核环境下的那些“坑”和“招”。说实话,驱动开发在单核时代还算单纯,但一上SMP,很多老经验就不灵了。我当年第一次把单核驱动移植到双核上,跑起来直接死机,查了三天才发现是全局变量没加保护。嗯,咱们今天就把它聊透。
8.1 SMP安全的驱动编写:别让两个核同时碰一个寄存器
先问一个问题:两个CPU核同时写一个设备寄存器,会发生什么?
答案很残酷——数据错乱,设备状态不可预测。这就是驱动必须考虑SMP安全的原因。
我个人习惯把驱动中的共享资源分成三类:
- 全局变量:比如设备状态标志、缓冲区指针
- 硬件寄存器:多个核可能同时访问的MMIO区域
- DMA描述符链:多个核提交DMA请求时可能冲突
针对这些,RT-Thread提供了几种保护手段:
| 保护手段 | 适用场景 | 性能开销 |
|---|---|---|
| 自旋锁(spinlock) | 中断上下文、短临界区 | 低 |
| 互斥量(mutex) | 线程上下文、可能睡眠 | 中 |
| 原子操作 | 简单计数器、标志位 | 极低 |
| RCU(读-拷贝-更新) | 读多写少的场景 | 读无锁 |
举个例子,一个简单的UART驱动,发送缓冲区索引需要保护:
/* 不安全的写法 */
static int tx_index = 0;
void uart_send(char c) {
buffer[tx_index++] = c; // 两个核同时执行,tx_index会乱
}
/* SMP安全的写法 */
static int tx_index = 0;
static struct rt_spinlock tx_lock;
void uart_send(char c) {
rt_spin_lock(&tx_lock);
buffer[tx_index++] = c;
rt_spin_unlock(&tx_lock);
}
这里有个细节:自旋锁在单核上其实是个空操作,但多核下必须加。我建议你从一开始就加上,别等出问题再补。
8.2 DMA一致性:缓存和内存的“同步战争”
DMA一致性,说白了就是CPU的缓存和DMA引擎看到的内存数据不一样。为什么会这样?
CPU有L1/L2缓存,写数据时可能先写到缓存里,还没刷回主存。DMA引擎直接读写主存,它看不到缓存里的最新数据。这就出问题了。
我在项目中遇到过这样一个案例:网卡驱动用DMA接收数据包,CPU从缓冲区读数据时总是读到旧数据。查了半天,发现是DMA已经把数据写到了主存,但CPU的缓存里还是老版本。解决办法就是做缓存一致性操作。
RT-Thread中处理DMA一致性的常用方法:
- 使用一致性DMA缓冲区:分配时指定为非缓存区域,CPU和DMA都直接操作主存。
- 手动缓存刷新:在DMA传输前后调用缓存维护函数。
- 硬件自动一致性:有些SoC支持硬件缓存一致性协议(如CCI-400),驱动无需额外操作。
代码示例——分配一致性DMA缓冲区:
#include <rtdevice.h>
/* 分配一个DMA安全的缓冲区 */
void *dma_buf = rt_dma_alloc(1024, RT_DMA_CONSISTENT);
if (!dma_buf) {
rt_kprintf("DMA buffer allocation failed\n");
return -ENOMEM;
}
/* 使用完毕后释放 */
rt_dma_free(dma_buf);
如果你用的是手动刷新方式,记住这个原则:
- DMA写入数据前:CPU要调用缓存无效化(invalidate),确保读到的是DMA写的最新数据。
- DMA读取数据前:CPU要调用缓存清理(clean),把缓存中的数据刷回主存。
8.3 设备树与多核绑定:告诉系统“这个设备归哪个核管”
设备树(Device Tree)在嵌入式Linux里用得很多,RT-Thread也支持。它的作用就是描述硬件拓扑,告诉内核“有哪些设备、挂在哪里、中断号是多少”。
在多核环境下,设备树还能做一件事:指定某个设备的中断由哪个CPU核处理。这叫中断亲和性(IRQ affinity)。
举个例子,一个千兆网卡的中断如果总是被CPU0处理,那CPU0的负载会很高,其他核却闲着。我们可以通过设备树把中断分散到不同核上:
/* 设备树片段:将网卡中断绑定到CPU1 */
ethernet@40000000 {
compatible = "vendor,eth";
reg = <0x40000000 0x1000>;
interrupts = <33 2>;
interrupt-affinity = <1>; /* 绑定到CPU1 */
};
在RT-Thread中,解析设备树并设置中断亲和性的流程大致如下:
- 解析设备树节点,获取interrupt-affinity属性。
- 调用rt_hw_interrupt_set_affinity()函数,将中断路由到指定CPU。
- 在中断处理函数中,检查当前运行的CPU核是否与绑定一致。
我曾经调试过一个音频驱动,播放时总是有杂音。后来发现是中断在两个核之间来回跳,导致DMA缓冲区被交替访问。用设备树把中断固定到一个核上,问题就解决了。
8.4 避坑指南:我踩过的几个雷
最后分享几个实战中容易忽略的点:
- DMA描述符的缓存问题:描述符链本身也可能被缓存,记得在提交DMA前刷新描述符内存。
- 中断嵌套:高优先级中断可能打断低优先级中断,如果它们共享同一个设备寄存器,必须用自旋锁保护。
- 设备树中的中断号:不同SoC的中断号编码方式不同,有的从0开始,有的从32开始。一定要看芯片手册确认。
- 一致性内存的分配时机:不要在中断处理函数里分配DMA缓冲区,因为分配过程可能阻塞。应该在初始化时预分配好。
嗯,这一章的内容就到这里。驱动开发是个细致活,SMP环境下更是如此。记住一句话:能加锁的地方别省,能固定绑定的别乱跑。下一章我们会聊调度器在多核下的实现,那才是真正的重头戏。