基于 RTOS 信号量实现多核(AMP)架构下外设互斥访问的陷阱与修正
👁 1 阅读 · 2026-08-27 · 嵌入式
在非对称多处理(AMP)架构中,多个内核共享同一外设时,仅依赖 RTOS 信号量进行互斥往往隐藏着致命陷阱:信号量状态不一致、缓存一致性问题、以及中断上下文中的死锁。本文深入剖析这些陷阱的根源,并给出基于硬件自旋锁、内存屏障及核间中断(IPI)的修正方案,附完整可运行的 STM32H7 双核示例代码,帮助开发者构建真正安全的外设访问机制。
# 基于 RTOS 信号量实现多核(AMP)架构下外设互斥访问的陷阱与修正
## 引言
在嵌入式多核系统中,非对称多处理(AMP)架构因其灵活性和实时性被广泛采用,例如 STM32H7 系列的双核(Cortex-M7 + Cortex-M4)平台。然而,当两个核共享 UART、ADC 或 Flash 控制器等外设时,互斥访问成为关键问题。许多开发者直接使用 RTOS 信号量(如 FreeRTOS 的 `xSemaphoreTake`)来实现互斥,但在 AMP 架构下,这种做法往往导致系统崩溃或数据损坏。本文将从底层原理出发,剖析陷阱,并提供经过验证的修正方案。
## 陷阱一:信号量状态不一致
### 原理
RTOS 信号量(如 FreeRTOS 的互斥量)通常基于内核维护的变量和链表。在单核系统中,内核通过关闭中断或调度器来保证原子性。但在 AMP 架构下,每个核运行独立的 RTOS 实例,信号量对象在各自的内存空间中独立存在。若两个核共享一个信号量变量,则对它的访问(如 `xSemaphoreGive`)并非原子操作,可能导致状态错乱。
### 示例
假设 M7 核和 M4 核都执行以下代码:
```c
// 共享信号量(错误做法)
SemaphoreHandle_t sharedSem;
void shared_peripheral_access(void) {
if (xSemaphoreTake(sharedSem, portMAX_DELAY) == pdTRUE) {
// 访问共享外设
UART_SendData(...);
xSemaphoreGive(sharedSem);
}
}
```
当 M7 核正在执行 `xSemaphoreTake` 时,M4 核可能同时执行 `xSemaphoreGive`,导致信号量计数被破坏,最终两个核同时进入临界区。
### 修正方案
必须使用硬件提供的原子操作或专用互斥机制。例如,在 Cortex-M 系列中,可以使用 `LDREX`/`STREX` 指令实现自旋锁,或者使用硬件信号量(如 STM32H7 的 HSEM 外设)。
## 陷阱二:缓存一致性问题
### 原理
现代 MCU (如 STM32H7)具有多级缓存(L1-Cache),且各核的缓存可能不一致。当 M7 核写一个共享变量(如信号量状态),该数据可能只存在于 M7 的 L1 Cache 中,而 M4 核读取时可能从主存获取旧值,导致互斥失效。
### 示例
```c
// 共享标志(错误做法)
volatile uint32_t lock_flag = 0;
void acquire_lock(void) {
while (__LDREXW(&lock_flag) == 1) {
// 等待
}
__STREXW(1, &lock_flag); // 可能失败,因为缓存未更新
}
```
### 修正方案
- 使用内存屏障指令(`__DSB()` 和 `__DMB()`)确保数据可见性。
- 将共享变量声明为 `volatile` 并放置在非缓存区域(如 `__attribute__((section(".noncacheable")))`)。
- 或者使用硬件信号量(HSEM),它由硬件保证原子性和一致性。
## 陷阱三:中断上下文中的死锁
### 原理
在 RTOS 中,信号量操作可能阻塞任务。如果在中断服务程序(ISR)中调用 `xSemaphoreTake` 并设置阻塞时间,会导致死锁或系统崩溃。在 AMP 架构下,若一个核在 ISR 中获取信号量,而另一个核持有该信号量并等待前一个核释放,则可能形成循环等待。
### 示例
```c
void UART_ISR(void) {
// 错误:在 ISR 中获取信号量
if (xSemaphoreTake(sharedSem, 100) == pdTRUE) {
// 处理数据
xSemaphoreGive(sharedSem);
}
}
```
### 修正方案
在 ISR 中应使用非阻塞的 `xSemaphoreTakeFromISR`,并立即返回。对于 AMP 架构,推荐使用硬件信号量,因为其获取操作是原子的且不会阻塞。
## 修正实践:基于硬件信号量(HSEM)的互斥实现
### 原理
STM32H7 系列内置硬件信号量(HSEM),提供 32 个独立信号量,支持原子获取/释放,并可通过中断通知。它不依赖 RTOS,天然适用于多核。
### 配置步骤
1. 在 CubeMX 中启用 HSEM,并分配一个信号量(如 HSEM0)。
2. 初始化 HSEM:
```c
void HSEM_Init(void) {
HAL_HSEM_Init();
// 设置信号量初始值为1(可用)
HAL_HSEM_Set(HSEM_ID_0, 1);
}
```
3. 获取信号量(非阻塞):
```c
uint8_t HSEM_Acquire(void) {
if (HAL_HSEM_Take(HSEM_ID_0, 0) == HAL_OK) {
return 1; // 成功
}
return 0;
}
```
4. 释放信号量:
```c
void HSEM_Release(void) {
HAL_HSEM_Release(HSEM_ID_0, 0);
}
```
### 完整示例(双核共享 UART)
以下代码在 M7 和 M4 核上运行,通过 HSEM 保护 UART 发送:
```c
// 公共头文件(两个核共用)
#include "stm32h7xx_hal.h"
#define UART_HSEM_ID 0
void UART_SendString(const char *str) {
// 获取硬件信号量,自旋等待
while (HAL_HSEM_Take(UART_HSEM_ID, 0) != HAL_OK) {
// 可加超时或让出 CPU
__NOP();
}
// 临界区:发送数据
HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), 1000);
// 释放信号量
HAL_HSEM_Release(UART_HSEM_ID, 0);
}
// M7 核任务
void M7_Task(void *arg) {
while (1) {
UART_SendString("M7: Hello\n");
HAL_Delay(100);
}
}
// M4 核任务
void M4_Task(void *arg) {
while (1) {
UART_SendString("M4: World\n");
HAL_Delay(200);
}
}
```
## 注意事项
- **内存屏障**:在使用 HSEM 时,硬件已保证一致性,但若在临界区中访问普通共享变量,仍需在进入和退出时添加 `__DMB()` 确保顺序。
- **超时处理**:自旋等待会浪费 CPU,建议在 RTOS 中结合 `vTaskDelay` 或使用 HSEM 的中断功能。
- **信号量数量**:HSEM 只有 32 个,合理规划,避免耗尽。
- **调试**:使用调试器观察 HSEM 状态,确保没有死锁。
## 总结
在 AMP 架构下,RTOS 信号量并非可靠的互斥工具,其陷阱源于原子性、缓存一致性和中断上下文。通过使用硬件信号量(如 STM32H7 的 HSEM),结合内存屏障和正确的临界区设计,可以构建安全的外设访问机制。开发者应深入理解硬件特性,而非盲目依赖软件抽象,这是嵌入式多核开发的关键。