嵌入式高手都在偷偷用的“第27条”:用 section 属性把代码搬出 Flash,让关键函数在 RAM 里飞起来

该文章同步至OneChan

你有没有遇到过:某个高频中断函数,每次执行前 CPU 都要插入好几个 Flash 等待周期,结果抖动大到通信总是丢包?

这是资深工程师压箱底的编程技巧系列第二十七篇。前面我们学会了用malloc属性优化指针别名分析,用assumeunreachable向编译器注入不变量,用warn_unused_result强制检查返回值。今天这一招,解决的是嵌入式系统中一个非常底层的痛点——Flash 太慢,CPU 太快,高频代码需要搬到 RAM 里跑,否则 Flash 的等待周期会吃掉你所有的实时性能。

它就是嵌入式开发者必须掌握的链接期布局利器:

__attribute__((section("段名")))

配合链接脚本和启动代码,这个属性可以把任意函数或变量从默认的 Flash 段中“摘”出来,放到你指定的 RAM 地址上。在启动时,由拷贝表自动将代码从 Flash 搬运到 RAM,之后 CPU 就从高速 RAM 中取指执行,彻底消除 Flash 等待。


一、这东西到底是干什么用的?

简单说:__attribute__((section("name")))告诉编译器,不要把这个函数或变量放入默认的.text.data段,而是放入一个你自定义的、名叫name的段。然后你在链接脚本中为这个段指定加载地址(Flash)和运行地址(RAM),启动代码在进入main()之前将其拷贝到 RAM。

它的声明:

__attribute__((section(".ramfunc")))voidcritical_isr(void){// 这段代码将在 RAM 中执行}

为什么要这么做?现代 MCU(如 Cortex-M7 在 400MHz+)的 CPU 速度远超 Flash 读取速度。Flash 通常需要插入等待周期(如 5-7 个 WS)。对于普通代码,指令缓存可以掩盖大部分延迟。但对于那些每次执行都不重复、每次都触发缓存缺失的高频中断,或者严格要求确定执行时间的控制算法,Flash 的等待周期是致命的。搬到 RAM 后,取指零等待,执行时间变得极短且完全可预测。

与普通变量在 RAM 中不同:全局变量本来就在 RAM(.data.bss),函数代码默认在 Flash。我们需要的是把代码也放进 RAM,并且链接器自动处理加载地址和运行地址的差异。


二、上硬菜,直接看怎么用

Step 1:标记关键函数并放入自定义段

假设你有一个 100kHz 的定时器中断,里面执行一个快速 PID 控制:

// pid_control.c__attribute__((section(".ramfunc")))voidTIM2_IRQHandler(void){// 读取编码器intpos=read_encoder();// PID 计算intoutput=pid_calculate(pos);// 写入 PWMset_pwm(output);// 清除中断标志TIM2->SR&=~TIM_SR_UIF;}

编译后,TIM2_IRQHandler不会被放入.text段,而是被放入一个叫.ramfunc的段。你可以通过查看.map文件确认:

.ramfunc 0x20000000 0x48 load address 0x08001000

Step 2:修改链接脚本,为.ramfunc段分配正确的地址

在链接脚本(通常是.ld文件)中,你需要在SECTIONS里添加类似这样的定义:

SECTIONS { /* Flash 区域 */ .text : { *(.text) *(.text.*) } >FLASH /* RAM 代码段:加载在 Flash,运行在 RAM */ .ramfunc : { . = ALIGN(4); __ramfunc_start__ = .; *(.ramfunc) . = ALIGN(4); __ramfunc_end__ = .; } >RAM AT>FLASH /* 其他段... */ }

关键语法>RAM AT>FLASH:这告诉链接器,.ramfunc段的**运行地址(VMA)**在 RAM,**加载地址(LMA)**在 Flash。启动时需要从 Flash 拷贝到 RAM。

__ramfunc_start____ramfunc_end__是我们自定义的符号,启动代码会用到它们。

Step 3:启动代码中执行拷贝

在你的启动文件(startup.ssystem.c)中,需要在进入main()之前,把.ramfunc的内容从 Flash 搬运到 RAM。这通常在 BSS 清零之后、调用构造函数之前完成:

// system.cexternuint32_t__ramfunc_start__;externuint32_t__ramfunc_end__;externuint32_t__ramfunc_loadaddr__;// 链接器可能自动生成此符号,或需显式定义voidcopy_ramfuncs(void){uint32_t*src=&__ramfunc_loadaddr__;uint32_t*dst=&__ramfunc_start__;uint32_t*end=&__ramfunc_end__;while(dst<end){*dst++=*src++;}}voidSystemInit(void){// ... 其他初始化 ...copy_ramfuncs();// ...}

注意:不同工具链中,加载地址的符号名可能略有不同。GCC 的链接器在>RAM AT>FLASH时通常会自动生成__ramfunc_loadaddr__(实际符号名可能是__ramfunc_load_start__或类似,取决于版本)。你可以在.map文件中查找准确的符号名。

Step 4:在主程序中正常调用

完成上述设置后,你在代码中正常调用TIM2_IRQHandler()即可。CPU 会自动从 RAM 中的地址取指执行,不需要任何特殊调用语法。


三、举一反三,section的这些用法你必须掌握

1. 将启动时执行的加载程序放入 RAM

某些 MCU 在初始化 SDRAM 或配置外部存储器时,加载代码本身必须已经在 RAM 中运行(因为 Flash 可能尚未配置加速或正在编程)。可以将这些函数标记为.ramfunc

__attribute__((section(".ramfunc")))voidsdram_init(void){// 配置 SDRAM 控制器}

2. 将默认配置数据放入特定 Flash 区

不仅是函数,变量也可以放入自定义段,用于保存校准值或序列号:

__attribute__((section(".calib_data")))constuint32_tcalibration_value=0x12345678;

然后在链接脚本中把.calib_data固定在 Flash 的某个已知地址,方便通过 JTAG 或 Bootloader 单独更新校准区。

3. 配合usedKEEP保护关键段

如第十六招所讲,标记了自定义段的函数可能被垃圾回收。在链接脚本中使用KEEP

.ramfunc : { KEEP(*(.ramfunc)) } >RAM AT>FLASH

同时在源代码中加上__attribute__((used, section(".ramfunc")))双保险。

4. 与noinline联合,确保完整函数落入段中

如果编译器将.ramfunc中的函数内联到调用者,它就不再出现在.ramfunc段了。加上noinline可以确保它独立存在并被正确搬运:

__attribute__((section(".ramfunc"),noinline))voidcritical_fn(void){/* ... */}

四、留两个问题给你思考

现在请你停下来,思考这两个实际问题:

  1. 如果把一个函数放到了.ramfunc段,但忘了在启动代码中拷贝它,第一次调用时会发生什么?怎么调试这种问题?
  2. .ramfunc段的大小如果超出了链接脚本中分配的 RAM 区域,链接阶段会发生什么?如何优雅地检查并防止这种溢出?

想清楚这两个问题,你就能在项目中安全地部署 RAM 代码,而不是抱着侥幸心理祈祷它别崩。


五、总结与思考题回答

核心总结:


思考题回答

问题1:忘记拷贝.ramfunc会怎样?

调用.ramfunc中的函数时,CPU 会跳转到 RAM 中的地址取指,但该地址的内容未被初始化,可能是全 0、全 1 或随机值。后果通常是立即触发 HardFault 或 UsageFault(执行未定义指令)。调试方法是:

问题2:.ramfunc段超出 RAM 分配区域怎么办?

链接器会报错:“section.ramfunc' will not fit in regionRAM`” 或类似信息。预防措施包括:


好了,第 27 招我们就彻底吃透了。从今天起,检查你项目中那些对延迟敏感的函数,让section属性帮它们脱离 Flash 的“慢车道”,在 RAM 中享受零等待的极速。

如果今天的内容让你对代码布局有了更底层的理解,欢迎转发和点赞。下一篇我们继续挖:__attribute__((section(".noinit")))保留复位后不被清零的变量。咱们不见不散!