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;
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();
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
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;
}
}
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 字节,无填充,访问效率高