# 基于 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),结合内存屏障和正确的临界区设计,可以构建安全的外设访问机制。开发者应深入理解硬件特性,而非盲目依赖软件抽象,这是嵌入式多核开发的关键。