FreeRTOS on STM32H723¶
在 STM32H723 上运行 FreeRTOS 与 F1/F4 有显著不同:564KB 多区域 SRAM 需要合理分配给堆和栈,L1 Cache 与 DMA 的一致性问题交织着 RTOS 的任务通信,更高的主频也意味着更多的优化空间和更多的陷阱。本页记录在 H723 上使用 FreeRTOS 的关键配置和注意事项。
CubeMX 配置要点¶
启用 FreeRTOS¶
- Middleware → FREERTOS → Interface 选择 CMSIS_V2
- SYS → Timebase Source 改为 TIM6 或 TIM7(不能用 SysTick)
必须修改 HAL 时基!
FreeRTOS 占用了 SysTick 作为调度器心跳。如果 HAL 时基也用 SysTick,HAL_Delay() 在 RTOS 启动后行为异常(可能永远不返回或不计时)。
H723 有充足的定时器资源,建议用 TIM6(基本定时器,不占用 PWM 通道)。
关键参数配置¶
在 FREERTOS → Config parameters 中:
| 参数 | F103 建议值 | H723 建议值 | 说明 |
|---|---|---|---|
TOTAL_HEAP_SIZE |
10240~15360 | 65536~131072 | H723 有 564KB SRAM,堆可以给大方 |
TICK_RATE_HZ |
1000 | 1000 | 1ms tick,通常无需改 |
MAX_PRIORITIES |
7 | 16 | H7 项目通常更复杂,多留优先级 |
MINIMAL_STACK_SIZE |
128 | 256 | M7 内核上下文更大 |
TIMER_TASK_STACK_DEPTH |
256 | 512 | 定时器守护任务栈 |
IDLE_TASK_STACK_DEPTH |
128 | 256 | 空闲任务栈 |
CHECK_FOR_STACK_OVERFLOW |
Option2 | Option2 | 始终启用栈溢出检测 |
USE_MALLOC_FAILED_HOOK |
1 | 1 | 内存分配失败钩子 |
Memory Management scheme |
heap_4 | heap_4 | 默认即可 |
堆大小粗算
H723 常见分配:
- AXI-SRAM 256KB 给 FreeRTOS 堆 + DMA 缓冲区
- DTCM 128KB 给全局变量和任务栈(CPU 零等待访问)
- SRAM1/SRAM2 32KB 给以太网/USB DMA
FreeRTOS 堆可以给到 64~128KB,按需调整。
内存分配策略(核心问题)¶
堆放在哪个 SRAM?¶
这是 H723 + FreeRTOS 最关键的决策:
| 方案 | 堆的位置 | 优点 | 缺点 |
|---|---|---|---|
| 方案 A(推荐) | AXI-SRAM (0x2400 0000) |
DMA 安全,一切正常 | CPU 访问稍慢于 DTCM |
| 方案 B | DTCM (0x2000 0000) |
CPU 最快 | ❌ 队列/信号量缓冲区如果被 DMA 操作会出问题 |
| 方案 C | 混合(heap_5) | 充分利用所有 SRAM | 配置复杂 |
推荐方案 A:堆放在 AXI-SRAM
对于大多数项目,将 FreeRTOS 堆整体放在 AXI-SRAM 是最稳妥的选择。虽然 CPU 访问 AXI-SRAM 不如 DTCM 快,但有 D-Cache 加速后差距很小,且完全避免了 DMA 问题。
修改链接脚本¶
CubeMX 默认可能将堆放在 DTCM。需要修改 .ld 链接脚本:
/* STM32H723ZITX_FLASH.ld */
MEMORY
{
DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 256K /* AXI-SRAM */
RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 32K /* SRAM1+SRAM2 */
RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 16K /* SRAM4 */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
}
/* 将 _estack 和默认 RAM 指向 DTCM(主栈+全局变量) */
_estack = ORIGIN(DTCMRAM) + LENGTH(DTCMRAM);
/* 关键:在 sections 中添加,让 FreeRTOS 堆分配在 AXI-SRAM */
.freertos_heap (NOLOAD) :
{
. = ALIGN(32);
__freertos_heap_start = .;
. = . + 131072; /* 128KB FreeRTOS heap */
__freertos_heap_end = .;
} > RAM_D1
然后在 FreeRTOSConfig.h 或 CubeMX 配置中:
/* 告诉 FreeRTOS 堆的位置和大小 */
#define configTOTAL_HEAP_SIZE ((size_t)131072) /* 128KB */
/* 如果用 heap_4,需要在 port.c 中确认 ucHeap 数组的位置 */
更简单的做法
如果不想改链接脚本,可以在 FreeRTOSConfig.h 中直接用 configAPPLICATION_ALLOCATED_HEAP 宏,自己声明堆数组并指定 section:
/* FreeRTOSConfig.h 中 */
#define configAPPLICATION_ALLOCATED_HEAP 1
/* 某个 .c 文件中 */
__attribute__((section(".RAM_D1"), aligned(32)))
uint8_t ucHeap[configTOTAL_HEAP_SIZE];
这样 FreeRTOS 的堆就确定在 AXI-SRAM 中了。
DMA + 队列的 Cache 问题¶
典型问题场景¶
graph LR
ISR["DMA 中断<br>数据到达 RAM"] -->|"队列发送"| Q[(FreeRTOS Queue)]
Q -->|"队列接收"| TASK["处理任务<br>读取数据"]
style ISR fill:#ff6b6b,color:#fff
style TASK fill:#51cf66,color:#fff
在 H723 上,这个流程暗藏 Cache 一致性问题:
- DMA 将数据写入 AXI-SRAM 中的缓冲区
- 但 CPU 的 D-Cache 中可能还缓存着旧数据
- 中断回调中读取缓冲区 → 读到旧数据
- 通过队列发送出去 → 任务收到错误数据
正确的处理流程¶
/* DMA 接收完成回调 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
/* ① 先 Invalidate Cache,确保从 RAM 读到最新数据 */
SCB_InvalidateDCache_by_Addr((uint32_t*)uart_dma_buf,
sizeof(uart_dma_buf));
/* ② 现在可以安全地通过队列发送 */
UartMsg_t msg;
msg.len = rx_size;
memcpy(msg.data, uart_dma_buf, rx_size);
osMessageQueuePut(UartQueueHandle, &msg, 0, 0);
/* ③ 重启 DMA 接收 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_dma_buf,
sizeof(uart_dma_buf));
}
}
/* 处理任务(读出来已经是正确数据了) */
void UART_Process_Task(void *argument)
{
UartMsg_t msg;
for (;;)
{
if (osMessageQueueGet(UartQueueHandle, &msg, NULL,
osWaitForever) == osOK)
{
// msg.data 中是正确的数据
process_data(msg.data, msg.len);
}
}
}
DMA 发送的 Cache 处理¶
/* 任务中准备数据,通过 DMA 发送 */
void UART_Send_Task(void *argument)
{
for (;;)
{
// 从队列获取待发送数据
TxMsg_t tx;
if (osMessageQueueGet(TxQueueHandle, &tx, NULL,
osWaitForever) == osOK)
{
memcpy(uart_dma_tx_buf, tx.data, tx.len);
/* 发送前 Clean Cache:确保 CPU 写的数据刷到 RAM */
SCB_CleanDCache_by_Addr((uint32_t*)uart_dma_tx_buf,
sizeof(uart_dma_tx_buf));
/* 用信号量等待上次发送完成 */
osSemaphoreAcquire(UartTxSemHandle, osWaitForever);
HAL_UART_Transmit_DMA(&huart1, uart_dma_tx_buf, tx.len);
}
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
osSemaphoreRelease(UartTxSemHandle);
}
任务栈放哪里?¶
DTCM 的优势¶
任务栈是 CPU 高频访问的区域(每次函数调用、局部变量都在栈上)。DTCM 是 CPU 零等待访问,非常适合放栈。
静态分配法:指定栈到 DTCM¶
/* 在 DTCM 中静态分配任务栈 */
__attribute__((section(".dtcm_noinit")))
static uint32_t led_task_stack[256]; // 1KB
__attribute__((section(".dtcm_noinit")))
static uint32_t uart_task_stack[512]; // 2KB
__attribute__((section(".dtcm_noinit")))
static StaticTask_t led_task_tcb;
__attribute__((section(".dtcm_noinit")))
static StaticTask_t uart_task_tcb;
/* 使用静态分配创建任务 */
const osThreadAttr_t led_attr = {
.name = "LEDTask",
.cb_mem = &led_task_tcb,
.cb_size = sizeof(led_task_tcb),
.stack_mem = led_task_stack,
.stack_size = sizeof(led_task_stack),
.priority = (osPriority_t) osPriorityNormal,
};
osThreadNew(LED_Task, NULL, &led_attr);
混合策略(推荐)
- 任务栈:静态分配在 DTCM(CPU 快速访问)
- FreeRTOS 堆:放在 AXI-SRAM(队列、信号量、动态对象)
- DMA 缓冲区:放在 AXI-SRAM 或 SRAM1/SRAM2(按域选择)
这样既保证了任务切换的速度,又确保了 DMA 的正确性。
Cache 与互斥量/信号量¶
好消息¶
FreeRTOS 的队列、信号量、互斥量的内核数据结构不涉及 DMA,是纯 CPU 操作。所以它们本身不存在 Cache 一致性问题——Cache 对相同地址的读写是透明的。
只有当任务间传递的数据来自 DMA 缓冲区时,才需要在读取 DMA 数据时做 Cache 维护。
简化规则¶
| 操作 | 需要 Cache 维护? | 说明 |
|---|---|---|
osMessageQueuePut/Get |
❌(队列本身不需要) | 队列是 CPU 到 CPU |
osSemaphoreAcquire/Release |
❌ | 纯内核操作 |
osMutexAcquire/Release |
❌ | 纯内核操作 |
| 中断中读取 DMA 缓冲区 | ✅ Invalidate | DMA 写的数据需要刷新 Cache |
| 任务中填充 DMA 发送缓冲区 | ✅ Clean | CPU 写的数据需要写回 RAM |
| 任务中直接读写全局变量 | ❌ | Cache 对 CPU 读写透明 |
H7 特有的 FreeRTOS 配置¶
中断优先级¶
H723 使用 4 位优先级(0~15),与 F1/F4 相同。FreeRTOS 配置不变:
/* FreeRTOSConfig.h(CubeMX 自动生成) */
#define configPRIO_BITS 4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
启用 FPU 上下文保存¶
H723 有双精度 FPU,如果任务中使用浮点运算,FreeRTOS 需要在任务切换时保存/恢复 FPU 寄存器:
FPU 上下文增大栈消耗
启用 FPU 后,每个使用浮点的任务在切换时需要额外保存 FPU 寄存器(约 132 字节)。因此 H7 上任务栈需要比 F1 上更大。这就是建议 MINIMAL_STACK_SIZE 设为 256 Words 的原因。
MPU + FreeRTOS¶
FreeRTOS 支持与 MPU 配合实现内存保护,但这属于进阶用法。对于一般项目,MPU 主要用于设置 DMA 缓冲区为 Non-cacheable:
/* 在 main() 中,MX_FREERTOS_Init 之前调用 */
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
/* Region 0: AXI-SRAM 中 DMA 缓冲区域设为 Non-cacheable */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x24000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; // 前 64KB 给 DMA
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.SubRegionDisable = 0x00;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* Region 1: AXI-SRAM 剩余部分 Cacheable(给 FreeRTOS 堆) */
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.BaseAddress = 0x24010000; // 64KB 之后
MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
完整工程模板¶
推荐内存布局¶
graph TB
subgraph "DTCM 128KB (0x2000 0000)"
STACK["主栈 (MSP) 4KB"]
GLOBAL["全局/静态变量"]
TASK_STACK["任务栈<br>(静态分配)"]
end
subgraph "AXI-SRAM 256KB (0x2400 0000)"
DMA_BUF["DMA 缓冲区 64KB<br>(Non-cacheable via MPU)"]
HEAP["FreeRTOS 堆 128KB<br>(Cacheable)"]
AXI_FREE["剩余 64KB"]
end
subgraph "SRAM1+2 32KB (0x3000 0000)"
ETH_BUF["以太网 DMA 缓冲区"]
USB_BUF["USB DMA 缓冲区"]
end
subgraph "SRAM4 16KB (0x3800 0000)"
BDMA_BUF["BDMA 缓冲区"]
LP_DATA["低功耗保持数据"]
end
main.c 初始化顺序¶
int main(void)
{
/* 1. MPU 配置(必须在 Cache 启用之前) */
MPU_Config();
/* 2. 启用 Cache */
SCB_EnableICache();
SCB_EnableDCache();
/* 3. HAL 初始化 */
HAL_Init();
/* 4. 系统时钟配置(550 MHz) */
SystemClock_Config();
/* 5. 外设初始化 */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// ... 其他外设
/* 6. FreeRTOS 初始化并启动 */
MX_FREERTOS_Init();
osKernelStart();
/* 不应该执行到这里 */
while (1) {}
}
性能优化¶
充分利用 H7 的算力¶
| 优化手段 | 方法 | 效果 |
|---|---|---|
| 关键代码放 ITCM | __attribute__((section(".itcm_text"))) |
零等待指令获取 |
| 频繁数据放 DTCM | 全局变量/任务栈放 DTCM | 零等待数据访问 |
| 启用 D-Cache | SCB_EnableDCache() |
大幅加速 AXI-SRAM 访问 |
| 使用 DMA | 减少 CPU 搬运数据 | CPU 专注计算 |
| DSP 指令 | CMSIS-DSP 库 | FFT、滤波、矩阵运算加速 |
| 提高 Tick 精度 | TICK_RATE_HZ = 1000 足够 |
不建议更高,徒增切换开销 |
关键函数放 ITCM 示例¶
/* 将 PID 计算放入 ITCM,CPU 零等待执行 */
__attribute__((section(".itcm_text")))
float PID_Calculate(PID_t *pid, float setpoint, float measured)
{
float error = setpoint - measured;
pid->integral += error;
float derivative = error - pid->prev_error;
pid->prev_error = error;
return pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative;
}
常见问题¶
FreeRTOS 创建任务失败,但 H723 有 564KB SRAM?
FreeRTOS 堆可能只配置在了 DTCM 的一小块区域。检查:
configTOTAL_HEAP_SIZE是否够大- 堆是否放在了正确的 SRAM 区域(推荐 AXI-SRAM)
- 是否使用了
configAPPLICATION_ALLOCATED_HEAP并指定 section
任务中使用 DMA 通信,数据偶尔出错?
Cache 一致性问题。按以下检查清单排查:
- DMA 缓冲区是否在 AXI-SRAM 或 SRAM1/2(不在 DTCM)?
- 缓冲区是否 32 字节对齐?
- DMA 接收后是否调用了
SCB_InvalidateDCache_by_Addr()? - DMA 发送前是否调用了
SCB_CleanDCache_by_Addr()? - 或者是否通过 MPU 将缓冲区设为 Non-cacheable?
H7 上 FreeRTOS 任务栈应该给多大?
由于 Cortex-M7 上下文(含 FPU 寄存器)比 M3/M4 大,建议:
| 任务类型 | M3/M4 栈大小 | M7 栈大小 |
|---|---|---|
| 简单 GPIO | 128 Words | 256 Words |
| 串口通信 | 256 Words | 384 Words |
| 浮点 + printf | 512 Words | 768 Words |
| 复杂算法 | 512+ Words | 1024+ Words |
以太网 + LwIP + FreeRTOS 怎么配?
关键注意点:
- LwIP 缓冲区必须在 SRAM1/SRAM2(D2 域),ETH DMA 只能访问 D2
- CubeMX 中启用 LwIP 会自动要求 FreeRTOS
- LwIP 线程栈建议 ≥ 1024 Words
MEM_SIZE(LwIP 堆)建议 ≥ 16KB