3. 共享资源与竞态条件
好,咱们进入第三章。这一章要聊的东西,说白了就是多核编程里最让人头疼的问题——共享资源和竞态条件。
我在TC2xx上做项目那会儿,第一次遇到竞态条件,debug了整整两天。最后发现就是一个全局变量被两个核同时写了。嗯,从那以后,我对共享资源就特别敏感。
3.1 什么是共享资源?
共享资源,顾名思义,就是能被多个任务或者多个核同时访问的东西。在TC2xx这种多核架构里,共享资源无处不在。
常见的共享资源包括:
- 全局变量:比如一个用于统计的计数器,或者一个状态标志位
- 外设寄存器:比如ADC的转换结果寄存器,或者CAN模块的发送缓冲区
- 内存缓冲区:比如两个核之间传递数据的环形队列
- 硬件描述符:比如DMA的描述符链表
你想想看,在单核系统里,这些资源天然就是安全的。因为同一时刻只有一个任务在跑。但到了多核环境,情况就完全不一样了。
核心观点:在TC2xx中,CPU0、CPU1、CPU2共享同一套外设和大部分内存空间。这意味着,任何一个核都可能在任何时刻访问同一个地址。
3.2 竞态条件——那个让你头疼的家伙
竞态条件,英文叫Race Condition。我习惯叫它「抢跑问题」。
什么叫竞态条件?简单说就是:多个执行单元同时访问共享资源,而最终结果取决于它们执行的先后顺序。这个顺序是不确定的,所以结果也是不确定的。
举个例子,你就能明白了:
// 假设这是一个全局计数器,两个核都在用
volatile uint32_t shared_counter = 0;
// Core 0 执行
shared_counter++;
// Core 1 执行
shared_counter++;
表面上看,两个核各加一次,结果应该是2。但实际运行中,结果可能是1,也可能是2。
为什么会这样?
因为 shared_counter++ 在底层其实是三步操作:
- 从内存读取值到寄存器
- 在寄存器里加1
- 把结果写回内存
如果Core 0刚读完值,还没写回去,Core 1也来读。那Core 1读到的就是旧值。两个核都加1,最后写回去的都是1。结果就是1,而不是2。
注意:这种bug非常隐蔽。它不会每次都出现,可能跑一万次才出一次。我在项目中遇到过,产品在实验室测了三天没问题,一上产线就随机死机。最后定位到就是竞态条件。
3.3 为什么多核环境下必须同步?
这个问题其实可以换个角度问:如果不做同步,会发生什么?
我总结了三个最典型的后果:
| 后果 | 具体表现 | 严重程度 |
|---|---|---|
| 数据不一致 | 共享变量值错误,逻辑判断出错 | 高 |
| 外设状态混乱 | 两个核同时配置同一个外设寄存器,导致配置被覆盖 | 极高 |
| 死锁或活锁 | 资源被错误占用,系统卡死 | 致命 |
你想想看,在汽车电子里,如果因为竞态条件导致一个传感器数据读错了,那后果可能是灾难性的。所以TC2xx这种车规级芯片,对同步机制的要求非常严格。
3.4 共享资源的分类
我个人习惯把共享资源分成三类,这样处理起来思路更清晰:
- 只读资源:比如查表用的常量数组。多个核同时读没问题,不需要同步。
- 独占资源:比如某个外设的配置寄存器。同一时刻只能有一个核操作。
- 读写资源:比如全局变量。读和写都可能冲突,需要保护。
我的建议:在设计阶段就把共享资源列个清单,标清楚每个资源的类型和访问权限。别等到代码写完了再回头找问题,那时候就晚了。
3.5 一个真实的例子
我记得在TC275的项目里,CPU0负责采集ADC数据,CPU1负责做控制算法。它们共享一个数据缓冲区。
最初的设计是这样的:
// 共享缓冲区
uint16_t adc_buffer[64];
volatile uint8_t buffer_ready = 0;
// CPU0:采集完成,置标志
adc_buffer[0] = ADC_RESULT;
buffer_ready = 1;
// CPU1:检测到标志,读取数据
if(buffer_ready) {
process_data(adc_buffer[0]);
buffer_ready = 0;
}
看起来没问题对吧?但实际跑起来,CPU1偶尔会读到错误数据。
原因就是:CPU0写buffer和写标志之间,CPU1可能已经读到标志了。CPU1读到标志为1,但buffer里的数据还没写完。这就是典型的竞态条件。
解决办法?嗯,我们后面会讲到。这里先留个悬念。
3.6 小结
这一章我们聊了:
- 共享资源的定义和常见类型
- 竞态条件的本质——执行顺序的不确定性
- 为什么多核环境必须做同步
- 共享资源的分类方法
说白了,多核编程的核心挑战就是管理好这些共享资源。你躲不开它,只能正面解决它。下一章,我们就来聊聊TC2xx提供了哪些同步机制。
嗯,做好准备,后面才是真正的硬核内容。