2. 核间通信机制(IPC)

好,咱们进入正题。多核系统里最让人头疼的是什么?我个人觉得,就是怎么让几个核好好说话、别打架。说白了,IPC(Inter-Processor Communication)就是解决这个问题的。

TC2xx 系列有多个核,每个核都有自己的小算盘(本地资源)。但有些东西是共享的,比如内存、外设。这时候就需要一套规矩,让它们有序地交换数据、同步动作。这套规矩,就是 IPC。

2.1 共享内存:最直接的通信方式

共享内存,顾名思义,就是一块大家都能读写的内存区域。这是最原始、最粗暴,但也是最高效的方式。

怎么用?

你想想看,Core0 往地址 0x60000000 写了个数据,Core1 直接从同一个地址读出来。这不就通信了吗?

核心要点:共享内存本身不提供任何同步机制。谁先写、谁后读,全靠程序员自己控制。否则就会出现「数据竞争」——Core0 还没写完,Core1 就读了个半成品。

TC2xx 中的实现:

  • 所有核都能访问全局内存(如 LMU、DSPR 的一部分)
  • 每个核有自己的本地内存(如 PSPR、DSPR),但通过全局地址映射也能被其他核访问
  • 访问速度有差异:读本地内存最快,读其他核的本地内存会慢一些

我的经验:我在做电机控制项目时,Core0 负责 FOC 算法,Core1 负责通信协议。我们就在 LMU 里划了一块区域,Core0 把电流、转速数据写进去,Core1 定时读取。但注意,一定要加保护!我一开始没加,结果 Core0 更新到一半,Core1 读了个错误数据,电机抖了一下...嗯,从那以后我再也不敢裸用共享内存了。

2.2 信号量:给共享资源上把锁

共享内存的问题在于,多个核同时读写同一块区域时,数据会乱。信号量就是用来解决这个问题的——它本质上是一个「令牌」,谁拿到令牌,谁才能操作共享资源。

TC2xx 的信号量机制:

TC2xx 提供了硬件信号量单元,位于 SRI 总线上。每个信号量就是一个 32 位的寄存器,支持原子操作(Test and Set)。

// 伪代码:获取信号量
while (1) {
    if (TryLock(Semaphore_ID) == SUCCESS) {
        // 拿到锁了,操作共享资源
        break;
    }
    // 没拿到,继续等(或者做点别的事)
}

// 释放信号量
Unlock(Semaphore_ID);

关键特性:

  • 原子操作:硬件保证 Test and Set 不会被中断,不会出现两个核同时拿到锁的情况
  • 忙等待 vs 阻塞:TC2xx 的信号量获取是忙等待的(spinlock)。如果等待时间短,这没问题;如果长,会浪费 CPU 周期
  • 优先级反转:低优先级核拿着锁,高优先级核在等,结果低优先级核被其他任务抢占了...嗯,这是个经典问题

避坑指南:我曾经在一个项目中,Core0 和 Core1 都用信号量保护同一个缓冲区。Core0 优先级高,Core1 优先级低。结果 Core1 拿到锁后,被一个中断抢占了,Core0 一直在等锁,系统响应变慢。后来我用了「优先级继承」协议才解决。所以,信号量不是万能的,用之前要想清楚优先级关系。

2.3 邮箱:异步通信的好帮手

共享内存适合大数据量传输,但需要同步。信号量适合保护资源,但会阻塞。那有没有一种方式,既能传递消息,又不阻塞发送方?邮箱(Mailbox)就是干这个的。

邮箱的工作原理:

邮箱本质上是一个消息队列。发送方把消息丢进邮箱,然后就可以去干别的事了。接收方在合适的时候来取消息。如果邮箱满了,发送方可以选择等待或者丢弃。

TC2xx 中的邮箱实现:

TC2xx 没有专门的硬件邮箱模块,但我们可以用软件实现。通常的做法是:

  • 在共享内存中定义一个环形缓冲区(Ring Buffer)
  • 用信号量保护缓冲区的读写指针
  • 发送方写数据,更新写指针
  • 接收方读数据,更新读指针
  • 通过中断通知对方:有消息了!
// 邮箱结构体示例
typedef struct {
    uint32_t buffer[MAILBOX_SIZE];
    volatile uint32_t write_idx;
    volatile uint32_t read_idx;
    uint32_t sem_id;  // 保护用的信号量
} Mailbox_t;

// 发送消息
void Mailbox_Send(Mailbox_t *mb, uint32_t msg) {
    Lock(mb->sem_id);
    uint32_t next = (mb->write_idx + 1) % MAILBOX_SIZE;
    if (next != mb->read_idx) {  // 没满
        mb->buffer[mb->write_idx] = msg;
        mb->write_idx = next;
    }
    Unlock(mb->sem_id);
    // 触发接收核的中断(通过 SRC 寄存器)
    TriggerInterrupt(CORE1_MAILBOX_IRQ);
}

// 接收消息(在中断或轮询中调用)
uint32_t Mailbox_Receive(Mailbox_t *mb) {
    uint32_t msg = 0;
    Lock(mb->sem_id);
    if (mb->read_idx != mb->write_idx) {  // 非空
        msg = mb->buffer[mb->read_idx];
        mb->read_idx = (mb->read_idx + 1) % MAILBOX_SIZE;
    }
    Unlock(mb->sem_id);
    return msg;
}

我的建议:邮箱的大小要根据消息频率和接收方的处理速度来定。太小了容易丢消息,太大了浪费内存。我一般会留 2 倍于峰值消息数的空间。另外,中断触发要小心——如果接收方处理不过来,中断风暴会搞死系统。可以考虑用「阈值中断」,攒够一定数量再通知。

2.4 三种方式的对比与选择

特性 共享内存 信号量 邮箱
数据量 大(任意大小) 小(仅锁状态) 中(固定大小消息)
同步方式 需外部同步 自带同步 异步(可配合中断)
阻塞性 无(但需保护) 忙等待 发送方不阻塞
实现复杂度 低(硬件支持)
典型场景 大数据共享(如传感器数据) 保护临界区 命令/事件传递

怎么选?

  • 如果你要传几百字节的数据,用共享内存 + 信号量保护
  • 如果你只是要告诉另一个核「数据准备好了」,用邮箱发个事件
  • 如果你要保护一个外设寄存器,用信号量就够了

总结一下:IPC 没有银弹。我做了这么多年嵌入式,每个项目都是三种方式混着用。关键是要理解它们的优缺点,然后根据实际场景做取舍。记住一点:简单就是美。能用信号量解决的,别搞复杂的邮箱;能用共享内存的,别搞消息队列。过度设计是嵌入式开发的大忌。

嗯,这一节就到这里。下一节我们会讲 TC2xx 的中断系统,以及如何用它来触发核间通信。到时候我会分享一个我踩过的坑——中断优先级设置不当导致死锁,挺有意思的。