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 的中断系统,以及如何用它来触发核间通信。到时候我会分享一个我踩过的坑——中断优先级设置不当导致死锁,挺有意思的。