3、多核通信机制(下):硬件队列(HWQ)、DMA引擎在多核间的数据搬运、消息传递协议设计
好,我们接着聊多核通信。上一节我们讲了共享内存和核间中断,那是基础。但说实话,在真正的多核DSP系统里,光靠这两样东西,性能是上不去的。你想想看,每个核都在抢总线、抢内存,中断一多,CPU全花在上下文切换上了。
那怎么办?硬件来帮忙。今天要讲的三个东西——硬件队列(HWQ)、DMA引擎、消息传递协议——就是解决这个问题的关键。我在好几个项目里都吃过亏,后来才明白,通信机制的设计,直接决定了系统的吞吐量。
3.1 硬件队列(HWQ):比软件队列快一个数量级
先说说硬件队列。说白了,这就是在芯片里用寄存器或SRAM实现的一个FIFO。跟软件队列比,它不需要加锁,不需要原子操作,甚至不需要CPU参与入队出队。
核心优势:硬件队列的入队和出队操作,通常只需要一个写寄存器或读寄存器的指令。延迟在1-2个时钟周期。而软件队列,哪怕用无锁设计,也得几十个周期。
我习惯把硬件队列分成两类:
- 生产者-消费者队列:一个核写,另一个核读。典型应用是任务分发。
- 多生产者多消费者队列:多个核同时写,多个核同时读。这种设计更复杂,但灵活性高。
举个例子。我在做一款雷达信号处理芯片时,有4个DSP核。每个核处理完一帧数据后,要把结果发给下一个核。如果用共享内存加标志位,光同步就得花掉5%的CPU时间。后来换成硬件队列,每个核只负责往队列里丢指针,下一个核从队列里取。CPU占用率直接降到0.5%以下。
我的经验:硬件队列的深度不要设太大。16-32个条目通常就够了。太深了浪费面积,太浅了容易溢出。我曾经为了省面积,把队列深度设成8,结果在高负载下频繁丢数据。后来改成16,问题就解决了。
3.2 DMA引擎:让数据自己跑起来
接下来是DMA引擎。这个大家应该不陌生。但在多核场景下,DMA的角色远不止“搬数据”这么简单。
你想想看,如果每个核都要通过DMA从共享内存里取数据,那DMA的带宽就成了瓶颈。更麻烦的是,DMA传输完成后,怎么通知目标核?
我见过一个方案:DMA传输完成后,直接触发一个核间中断。但问题是,中断处理本身有开销。如果数据量小、传输频繁,中断的开销可能比数据搬运本身还大。
那怎么办?我建议用DMA链式传输加硬件队列的组合。
具体做法是这样的:
- DMA配置成链式模式,一次配置多个传输描述符(descriptor)。
- 每个描述符指定源地址、目的地址、传输长度。
- 最后一个描述符完成后,DMA自动把完成状态写入硬件队列。
- 目标核轮询硬件队列,发现新条目后直接处理。
这样做的好处是:CPU完全不用管DMA的调度。DMA自己跑完一个链,就把结果通知给目标核。目标核也不用处理中断,轮询硬件队列的开销几乎可以忽略。
注意:DMA链式传输的描述符要放在内部SRAM里,不要放在DDR里。否则DMA每次取描述符都要走DDR总线,延迟会很大。我曾经因为这个原因,DMA吞吐量直接腰斩。后来把描述符挪到L2 SRAM里,问题才解决。
3.3 消息传递协议设计:别让协议成为瓶颈
有了硬件队列和DMA,消息传递协议的设计就变得简单了。但简单不等于随便。我见过太多团队,把协议设计得过于复杂,结果性能还不如裸写共享内存。
我个人习惯把消息分成三类:
| 消息类型 | 长度 | 传输方式 | 典型场景 |
|---|---|---|---|
| 控制消息 | 4-16字节 | 硬件队列直接传递 | 启动/停止任务、状态查询 |
| 数据消息 | 64字节-4KB | DMA搬运 + 硬件队列通知 | 图像块、音频帧、FFT结果 |
| 大数据消息 | 4KB以上 | DMA链式传输 + 硬件队列通知 | 整帧图像、批量数据 |
控制消息直接用硬件队列传。因为硬件队列本身就在寄存器里,延迟极低。数据消息和大数据消息,用DMA搬,搬完往硬件队列里丢一个描述符。目标核看到描述符,就知道数据在哪、有多大。
这里有个细节:消息头部的设计。我建议每个消息至少包含以下字段:
- 消息ID:标识消息类型
- 源核ID:谁发的
- 目标核ID:发给谁
- 数据长度:有效数据大小
- 数据指针:指向共享内存中的数据
- 时间戳:可选,用于调试和性能分析
为什么要加时间戳?嗯,我在调试一个多核系统时,发现数据偶尔会乱序。加了时间戳后,一眼就看出是DMA传输顺序的问题。没有时间戳,你根本不知道是哪个环节出了错。
避坑指南:我曾经设计过一个协议,消息头部里放了太多信息,结果一个控制消息要传32字节。硬件队列的条目宽度只有64位,一个消息要分4次才能传完。后来我把消息头部精简到8字节,剩下的信息通过DMA传。性能提升了一倍。
3.4 整体架构:一张图看懂
说了这么多,我们来画一张图,把这三个东西串起来。
这张图展示的就是我常用的架构。每个DSP核通过硬件队列发送消息头部。消息头部里包含数据指针和长度。DMA引擎根据消息头部的内容,自动从共享内存里搬数据。搬完后,DMA往硬件队列里写一个完成通知。目标核轮询硬件队列,拿到完成通知后,就知道数据已经准备好了。
整个过程,CPU只参与了消息头部的写入和完成通知的读取。数据搬运完全由DMA负责。这就是多核通信的“零拷贝”思想——数据只搬一次,CPU不碰数据本身。
我的建议:在设计消息传递协议时,一定要考虑流控。如果生产者核发消息太快,消费者核处理不过来,硬件队列会溢出。我常用的做法是:在消息头部里加一个“信用”字段。消费者核处理完一条消息后,通过另一个硬件队列返还一个信用。生产者核只有在有信用时才能发消息。这样就能天然地实现背压。
好了,多核通信机制就讲到这里。硬件队列、DMA引擎、消息传递协议,这三样东西组合起来,基本能覆盖90%的多核通信场景。剩下的10%,比如跨芯片通信、非对称多核架构,我们后面再聊。