跳转至

FreeRTOS on STM32H723

在 STM32H723 上运行 FreeRTOS 与 F1/F4 有显著不同:564KB 多区域 SRAM 需要合理分配给堆和栈,L1 Cache 与 DMA 的一致性问题交织着 RTOS 的任务通信,更高的主频也意味着更多的优化空间和更多的陷阱。本页记录在 H723 上使用 FreeRTOS 的关键配置和注意事项。


CubeMX 配置要点

启用 FreeRTOS

  1. Middleware → FREERTOS → Interface 选择 CMSIS_V2
  2. SYS → Timebase Source 改为 TIM6TIM7(不能用 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 一致性问题:

  1. DMA 将数据写入 AXI-SRAM 中的缓冲区
  2. 但 CPU 的 D-Cache 中可能还缓存着旧数据
  3. 中断回调中读取缓冲区 → 读到旧数据
  4. 通过队列发送出去 → 任务收到错误数据

正确的处理流程

/* 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 寄存器:

/* CubeMX 自动处理,但需确认以下宏定义 */
#define configENABLE_FPU  1

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 的一小块区域。检查:

  1. configTOTAL_HEAP_SIZE 是否够大
  2. 堆是否放在了正确的 SRAM 区域(推荐 AXI-SRAM)
  3. 是否使用了 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