4. 多核间通信机制与共享内存
在 Stellar 这类多核实时 MCU 中,多个 Cortex-M7 内核与 GTM、HSE 等外设并行工作,核间通信(IPC)不是“可选项”,而是系统能否正确协同的基础。Stellar 提供的核间通信硬件主要有三类:MSCM(Multi-Core System Control Module)、MU(Messaging Unit)和 IPC 模块。理解它们的定位差异,比记住寄存器更重要。
MSCM 是全局系统控制与状态中枢,负责核间中断触发、核 ID 识别、复位与启动控制,以及各核共享资源的访问仲裁。它不搬运数据,只负责“通知”和“管理”。MU 则是真正意义上的消息通道,每个 MU 实例通常包含若干发送/接收寄存器组,支持中断与轮询两种模式,适合传递短报文、命令字和握手信号。IPC 模块在部分 Stellar 型号中提供更丰富的通道与触发源,可把外设事件直接路由到指定内核。三者的关系可以概括为:MSCM 管“谁在运行、谁被通知”,MU/IPC 管“消息怎么过去”。
共享内存布局是核间数据交换的物理基础。Stellar 通常把 SRAM 划分为若干区域,其中一部分被指定为多核共享区。典型做法是在链接脚本中显式定义共享段,例如把 0x24000000 起始的 64 KB 划为共享 SRAM,Core 0 与 Core 1 的链接脚本都保留同一地址范围,且不将其纳入各自的栈或堆。共享区内部再细分为控制块与数据块:控制块存放环形缓冲区的读写指针、序号和状态标志,数据块存放实际报文。控制块必须按缓存行对齐,避免两个核的指针落在同一缓存行上产生伪共享。
缓存一致性是共享内存最容易出错的地方。Cortex-M7 带有 D-Cache,若 Core 0 写入共享数据后仅停留在自己的 D-Cache 中,Core 1 读到的仍是旧值。处理原则是:写方在更新数据后执行 Clean(写回),读方在读取前执行 Invalidate(无效化),并且顺序不能颠倒。对于环形缓冲区,通常把指针更新放在数据写入之后,并在更新指针前对数据区做 Clean;读方先 Invalidate 数据区,再读取指针。若共享区被配置为 Write-Through 或 Non-Cacheable,可省去显式维护,但会牺牲访问性能,需要根据实时性要求权衡。
无锁环形缓冲区是核间数据交换最常用的结构。它的核心思想是:生产者只写写指针,消费者只读写指针,双方通过内存屏障和缓存维护保证可见性,而不使用互斥锁。下面给出一个单生产者单消费者(SPSC)的简化实现示例,参数为:缓冲区 256 字节,元素类型为 32 位字,控制块位于共享 SRAM 起始处。
typedef struct {
volatile uint32_t head; // 生产者写指针
volatile uint32_t tail; // 消费者读指针
uint32_t buf[64]; // 256 字节数据区
} ring_t;
// 生产者:Core 0
bool ring_push(ring_t *r, uint32_t v) {
uint32_t next = (r->head + 1) & 63;
if (next == r->tail) return false; // 满
r->buf[r->head] = v;
__DSB(); // 数据先可见
SCB_CleanDCache_by_Addr(&r->buf[r->head], 4);
r->head = next;
__DSB();
return true;
}
// 消费者:Core 1
bool ring_pop(ring_t *r, uint32_t *v) {
if (r->tail == r->head) return false; // 空
SCB_InvalidateDCache_by_Addr(&r->buf[r->tail], 4);
*v = r->buf[r->tail];
__DSB();
r->tail = (r->tail + 1) & 63;
return true;
}
这段代码中有几个关键点值得注意。第一,head 和 tail 声明为 volatile,防止编译器把它们缓存在寄存器中。第二,__DSB() 保证数据写入先于指针更新对外可见。第三,Clean 和 Invalidate 只针对实际访问的 4 字节,而不是整个缓冲区,减少维护开销。第四,容量为 64 个元素时,实际可用 63 个,因为需要留一个空位区分“满”和“空”。
实际调试时,可以用 MU 发送一个递增序号,Core 1 收到后回送相同序号,Core 0 比对序号是否连续。若出现跳变或重复,优先检查缓存维护是否遗漏、指针是否对齐到缓存行、以及是否在中断上下文中发生了重入。共享内存的地址、大小和缓存属性一旦确定,就应在两个核的链接脚本和初始化代码中保持一致,任何一侧改动都必须同步更新,否则会出现难以定位的数据错乱。