第四章 ARINC 429 软件驱动开发:Linux 下 SPI 驱动框架、中断处理、DMA 传输、环形缓冲区设计
好,咱们进入正题。这一章要聊的是 ARINC 429 的软件驱动开发,具体是在 Linux 环境下,怎么把 SPI 驱动、中断处理、DMA 传输和环形缓冲区这几个东西串起来。说实话,这部分内容在航电系统里属于「硬骨头」,但啃下来之后,你会发现很多底层逻辑都是相通的。
4.1 Linux SPI 驱动框架:从设备树到数据传输
先说说 SPI 驱动框架。Linux 下的 SPI 驱动,说白了就是一套标准化的「对话协议」。你不需要从头造轮子,内核已经帮你搭好了骨架。
我个人习惯,第一步是看设备树(Device Tree)。设备树里会描述 SPI 控制器的寄存器地址、中断号、时钟频率,还有挂在上面的从设备。比如 ARINC 429 的收发器芯片,通常就是 SPI 从设备。
设备树节点示例:
arinc429: arinc429@0 {
compatible = "mycompany,arinc429-spi";
reg = <0>;
spi-max-frequency = <10000000>;
interrupt-parent = <&gpio>;
interrupts = <17 2>; // GPIO17, 下降沿触发
};
嗯,这里要注意:spi-max-frequency 别设太高。我在项目中遇到过,某次把频率设到 20MHz,结果数据老是丢包,查了半天发现是信号完整性出了问题。后来降到 10MHz,稳如老狗。
驱动代码里,核心是 spi_driver 结构体。你需要实现 probe 和 remove 函数。在 probe 里,我会做三件事:
- 分配并初始化私有数据结构(比如环形缓冲区指针)
- 注册中断处理函数
- 创建字符设备或 misc 设备,供用户空间访问
数据传输用 spi_message 和 spi_transfer。你可以把多个 transfer 组成一个 message,内核会帮你按顺序执行。举个例子,读 ARINC 429 数据时,先发一个字节的命令,再收四个字节的数据,这就是两个 transfer。
我的小技巧: 用 spi_async 替代 spi_sync。同步方式会阻塞当前线程,而异步方式配合完成回调,效率高得多。尤其是在航电系统里,实时性要求高,阻塞是大忌。
4.2 中断处理:别让 CPU 空转
中断处理是驱动开发的灵魂。ARINC 429 芯片通常会在收到一帧数据后拉高中断引脚,或者在发送完成时产生中断。
我曾经犯过一个错误:在中断处理函数里做太多事情。比如直接调用 spi_sync 去读数据。结果呢?中断上下文不允许睡眠,而 spi_sync 会触发调度,系统直接崩溃。教训深刻啊。
正确的做法是:
- 中断处理函数(上半部)只做最紧急的事:比如读取状态寄存器,清除中断标志,然后唤醒一个工作队列或 tasklet。
- 下半部(比如
work_struct或tasklet)里再发起 SPI 传输,读取数据。
避坑指南: 我曾经在 tasklet 里用了 msleep,结果系统直接死锁。tasklet 是在软中断上下文执行的,不能睡眠。要用 mdelay 或者干脆用工作队列。
代码结构大致是这样:
static irqreturn_t arinc429_irq_handler(int irq, void *dev_id)
{
struct arinc429_dev *dev = dev_id;
// 清除中断标志(通过 GPIO 或 SPI 命令)
// 调度工作队列
schedule_work(&dev->work);
return IRQ_HANDLED;
}
static void arinc429_work_handler(struct work_struct *work)
{
struct arinc429_dev *dev = container_of(work, struct arinc429_dev, work);
// 发起 SPI 传输,读取数据
// 将数据放入环形缓冲区
}
4.3 DMA 传输:让数据自己跑起来
SPI 传输如果每次都用 CPU 搬运数据,那 CPU 就啥也别干了。DMA 就是干这个的——让数据直接在 SPI 控制器和内存之间流动,CPU 只需要在传输完成时收个通知。
Linux 内核提供了 dmaengine 框架。你需要先申请一个 DMA 通道,然后配置源地址、目的地址、传输长度。对于 SPI 接收,源地址是 SPI 的 RX FIFO 地址,目的地址是内存中的环形缓冲区。
我记得第一次调 DMA 时,数据死活不对。后来发现是 DMA 的地址没有做 cache 一致性处理。ARM 架构下,DMA 和 CPU 看到的 cache 可能不一致。解决方案是用 dma_map_single 或 dma_alloc_coherent 分配内存。
关键代码片段:
struct dma_async_tx_descriptor *txd;
dma_cookie_t cookie;
txd = dmaengine_prep_slave_sg(chan, &sgt, 1, DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
txd->callback = dma_callback;
txd->callback_param = dev;
cookie = dmaengine_submit(txd);
dma_async_issue_pending(chan);
DMA 传输完成后,会调用回调函数。在回调里,你只需要更新环形缓冲区的写指针,然后唤醒等待数据的进程。CPU 几乎零负担。
4.4 环形缓冲区:无锁设计的艺术
环形缓冲区(Ring Buffer)是解决生产者和消费者问题的经典方案。在驱动里,中断下半部是生产者,把数据写进缓冲区;用户空间的 read 调用是消费者,从缓冲区读数据。
为什么用环形缓冲区?因为可以做到无锁。只要保证一个写指针和一个读指针,并且写指针只由生产者更新,读指针只由消费者更新,就不需要互斥锁。这在实时系统中至关重要。
我常用的结构体是这样的:
struct arinc429_ringbuf {
uint32_t *buf; // 数据缓冲区
int size; // 缓冲区大小(必须是2的幂)
int write_idx; // 写指针
int read_idx; // 读指针
};
注意 size 必须是 2 的幂。这样可以用位运算代替取模:idx & (size - 1)。位运算比取模快一个数量级,在中断上下文里尤其重要。
写数据时:
int ringbuf_write(struct arinc429_ringbuf *rb, uint32_t data)
{
int next = (rb->write_idx + 1) & (rb->size - 1);
if (next == rb->read_idx) {
return -ENOSPC; // 缓冲区满了
}
rb->buf[rb->write_idx] = data;
smp_wmb(); // 写屏障,确保数据写入后再更新指针
rb->write_idx = next;
return 0;
}
读数据时类似,只是方向相反。这里用 smp_wmb() 是为了防止编译器或 CPU 重排序。我在项目中遇到过,不加屏障时,消费者读到了旧数据,因为写指针先更新了,但数据还没写到内存。加了屏障后,问题解决。
我的经验: 环形缓冲区的大小要根据数据速率来定。ARINC 429 每帧 32 位,速率有 12.5kbps 和 100kbps 两种。如果缓冲区太小,高负载时会丢数据;太大又浪费内存。我一般设为 256 或 512 个条目,够用。
4.5 整合:一个完整的驱动流程
把上面几个模块串起来,整个驱动的工作流程是这样的:
- 设备树解析,注册 SPI 设备
- probe 函数初始化:分配环形缓冲区、注册中断、申请 DMA 通道
- ARINC 429 芯片收到数据,产生中断
- 中断上半部清除标志,调度工作队列
- 工作队列发起 DMA 传输,数据从 SPI FIFO 直接搬到环形缓冲区
- DMA 完成回调,更新写指针,唤醒用户空间进程
- 用户空间调用 read,从环形缓冲区读取数据
你看,整个过程中,CPU 只在中断上半部和 DMA 回调里做了极少量工作,大部分时间都在处理其他任务。这才是嵌入式系统该有的样子。
嗯,这一章的内容就到这里。下一章我们会聊 ARINC 429 数据解析和校验,包括奇偶校验、SDI、SSM 这些字段的处理。到时候再细说。