3. FreeRTOS内核启动与第一个多任务工程

STM32 的存储器架构,说白了就是 CPU 和内存、Flash 之间的数据高速公路。这条路修得好不好,直接决定了你的程序跑得快不快。我见过不少开发者,代码写得挺漂亮,但一上 RTOS 就卡顿,查到最后发现是存储器访问没优化好。

这一章,咱们就聊聊怎么把这条路修通、修快。

3.1 Flash 预取与缓存

STM32 的 Flash 访问速度通常比 CPU 主频慢。比如 STM32F4 跑 168MHz,Flash 却只能跑到 30MHz 左右。CPU 去读 Flash,就得等,这就是所谓的「等待周期」。

怎么解决?靠预取和缓存。

3.1.1 预取缓冲区

预取缓冲区会提前把 CPU 可能要用的指令从 Flash 里读出来。CPU 执行当前指令时,预取器已经在读下一条了。这样,CPU 就不用干等。

我个人习惯,只要不是极端低功耗场景,都会把预取打开。配置很简单:

// 开启 Flash 预取
FLASH->ACR |= FLASH_ACR_PRFTEN;

// 设置等待周期(以 STM32F4 为例,168MHz 下需要 5 个等待周期)
FLASH->ACR |= FLASH_ACR_LATENCY_5WS;
小提示:等待周期数不是随便设的。设少了,Flash 读数据会出错;设多了,性能白白浪费。查你芯片的参考手册,按主频和电压查表。

3.1.2 指令缓存与数据缓存

有些 STM32 系列(比如 F7、H7)带 L1 缓存。指令缓存(I-Cache)和数据缓存(D-Cache)各 4KB 或 16KB。缓存命中时,CPU 一个周期就能拿到数据。

我在一个图像处理项目里遇到过:没开缓存时,从 Flash 读一条 32 位指令要 5 个周期;开了 I-Cache 后,大部分指令都在缓存里,平均 1.1 个周期就搞定。性能提升了 4 倍多。

开启缓存的代码:

// 使能 I-Cache 和 D-Cache(Cortex-M7 内核)
SCB_EnableICache();
SCB_EnableDCache();
注意:D-Cache 开了之后,DMA 和 CPU 访问同一块内存时会有缓存一致性问题。我曾经被这个坑过——DMA 把数据写进了内存,CPU 读的却是缓存里的旧数据。解决办法是在 DMA 传输前后做缓存清理或无效化操作。

3.2 SRAM 分区策略

STM32 的 SRAM 通常分成好几块。比如 STM32F407 有 192KB SRAM:112KB 的主 SRAM(SRAM1)、16KB 的 SRAM2、64KB 的 CCM SRAM(内核耦合存储器)。

这些分区各有特点:

分区 大小 总线 特点 推荐用途
SRAM1(主 SRAM) 112KB AHB 总线矩阵 DMA 可访问,所有外设都能用 全局变量、堆、栈、DMA 缓冲区
SRAM2 16KB AHB 总线矩阵 DMA 可访问,与 SRAM1 并行 以太网、USB 缓冲区
CCM SRAM 64KB 直接连内核 CPU 独占,DMA 不可访问 中断服务程序栈、RTOS 内核数据

怎么分?我建议这样:

  • CCM SRAM:放 FreeRTOS 的任务栈和内核控制块。因为 CPU 访问它没有总线竞争,实时性最好。
  • SRAM1:放全局变量、堆、以及大部分任务栈。
  • SRAM2:放 DMA 缓冲区,尤其是和 USB、以太网相关的。

在链接脚本里指定分区:

/* STM32F407 链接脚本片段 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
    SRAM1 (xrw) : ORIGIN = 0x20000000, LENGTH = 112K
    SRAM2 (xrw) : ORIGIN = 0x2001C000, LENGTH = 16K
    CCMRAM (xrw): ORIGIN = 0x10000000, LENGTH = 64K
}

/* 将 FreeRTOS 内核数据放到 CCM */
.os_kernel (NOLOAD) : {
    *(.os_kernel)
} > CCMRAM
关键点:CCM SRAM 虽然快,但 DMA 访问不了。如果你把 DMA 缓冲区放进去,DMA 传输会直接失败。这个坑我踩过,排查了一整天。

3.3 DMA 传输机制

DMA(直接存储器访问)让外设和内存之间直接传数据,不用 CPU 插手。CPU 只需要发起传输,然后就可以去干别的事。传输完了,DMA 会发个中断通知你。

STM32 的 DMA 控制器有多个流(Stream)和通道(Channel)。每个流可以配置优先级、传输方向、数据宽度等。

3.3.1 DMA 配置示例

下面是一个用 DMA 从 USART 接收数据的例子:

// 配置 DMA1 流 5,通道 4(USART2 RX)
DMA_Stream_TypeDef *dma_stream = DMA1_Stream5;

// 使能 DMA 时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;

// 等待 DMA 流就绪
while (dma_stream->CR & DMA_SxCR_EN);

// 配置外设地址(USART2 数据寄存器)
dma_stream->PAR = (uint32_t)&(USART2->DR);

// 配置内存地址(接收缓冲区)
dma_stream->M0AR = (uint32_t)rx_buffer;

// 配置传输数量
dma_stream->NDTR = RX_BUFFER_SIZE;

// 配置控制寄存器
dma_stream->CR = DMA_SxCR_CHSEL_1          // 通道 4
                | DMA_SxCR_PL               // 最高优先级
                | DMA_SxCR_MINC             // 内存地址递增
                | DMA_SxCR_TCIE             // 传输完成中断使能
                | DMA_SxCR_EN;              // 使能 DMA

3.3.2 双缓冲区模式

对于高速数据流(比如 ADC 连续采样、摄像头数据),我推荐用双缓冲区模式。DMA 在往缓冲区 A 写数据时,CPU 可以处理缓冲区 B 的数据。两个缓冲区轮流切换,没有等待时间。

// 配置双缓冲区模式
dma_stream->CR |= DMA_SxCR_DBM;            // 使能双缓冲区
dma_stream->M1AR = (uint32_t)rx_buffer_2;  // 设置第二个缓冲区地址

// 在中断中切换缓冲区
void DMA1_Stream5_IRQHandler(void)
{
    if (DMA1->HISR & DMA_HISR_TCIF5) {
        // 当前使用的缓冲区索引
        uint32_t active_buf = (dma_stream->CR & DMA_SxCR_CT) ? 1 : 0;
        
        // 处理非活动缓冲区
        process_buffer(active_buf ^ 1);
        
        // 清除中断标志
        DMA1->HIFCR = DMA_HIFCR_CTCIF5;
    }
}
经验之谈:DMA 中断服务程序要尽量短。我一般只在中断里设置一个标志位,数据处理放到任务里做。这样不会阻塞其他中断。

3.4 代码与数据的存储布局优化

代码和数据怎么放,对性能影响很大。核心原则是:频繁访问的放快的地方,不常用的放慢的地方。

3.4.1 代码布局

中断服务程序、RTOS 调度器、高频调用的函数,这些应该放在 Flash 的起始地址。因为 Flash 预取器对连续地址的访问效率最高。

在链接脚本里可以控制函数放置顺序:

SECTIONS
{
    /* 高频代码段 */
    .fast_code : {
        *(.isr_vector)      // 中断向量表
        *(.text.SysTick_Handler)
        *(.text.xPortSysTickHandler)
        *(.text.HardFault_Handler)
    } > FLASH
    
    /* 普通代码段 */
    .text : {
        *(.text*)
    } > FLASH
}

3.4.2 数据布局

频繁读写的全局变量,建议放到 SRAM 里。如果芯片有 TCM(紧耦合内存),那就更好了。

对于只读的常量数据(比如查表用的数组),可以放到 Flash 里,不占 SRAM 空间:

// 放到 Flash 的常量数据
const uint16_t sine_table[256] __attribute__((section(".rodata"))) = {
    // ... 正弦波查表数据
};

// 放到 SRAM 的频繁读写变量
volatile uint32_t system_tick __attribute__((section(".sram_data")));

3.4.3 对齐优化

CPU 访问对齐的数据最快。比如 32 位变量最好放在 4 字节对齐的地址上。编译器默认会做对齐,但结构体里字段的顺序会影响对齐效率。

我曾经优化过一个通信协议栈,把结构体字段按大小从大到小排列,减少了填充字节,性能提升了 15%。

// 不好的布局(有填充字节)
typedef struct {
    uint8_t  flag;      // 1 字节
    uint32_t counter;   // 4 字节,需要 3 字节填充
    uint16_t value;     // 2 字节
} __attribute__((packed)) BadLayout;  // 总大小 7 字节,但访问效率低

// 好的布局(按大小降序排列)
typedef struct {
    uint32_t counter;   // 4 字节
    uint16_t value;     // 2 字节
    uint8_t  flag;      // 1 字节
} GoodLayout;           // 总大小 7 字节,无填充,访问效率高
总结一下:存储器优化不是玄学,是实实在在的工程实践。Flash 预取和缓存能减少指令读取延迟;SRAM 分区能避免总线竞争;DMA 能解放 CPU;合理的布局能让数据访问更高效。把这些做好了,你的 STM32 项目性能至少能提升 30%。
STM32 存储器架构与性能优化全景图 Cortex-M 内核 I-Cache / D-Cache 预取缓冲区 Flash 主存储 代码 + 只读数据 SRAM1 / SRAM2 全局变量 + 堆栈 + DMA CCM SRAM RTOS 内核 + 中断栈 DMA 控制器 外设 ↔ 内存 Flash 预取使能 SRAM 分区 + 链接脚本 DMA 双缓冲区 性能提升 30%+ | 实时性增强 | 功耗降低