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

发布时间:2026/7/8 15:21:14
嵌入式高手都在偷偷用的“第27条”:用 section 属性把代码搬出 Flash,让关键函数在 RAM 里飞起来 该文章同步至OneChan你有没有遇到过某个高频中断函数每次执行前 CPU 都要插入好几个 Flash 等待周期结果抖动大到通信总是丢包这是资深工程师压箱底的编程技巧系列第二十七篇。前面我们学会了用malloc属性优化指针别名分析用assume和unreachable向编译器注入不变量用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){// 读取编码器intposread_encoder();// PID 计算intoutputpid_calculate(pos);// 写入 PWMset_pwm(output);// 清除中断标志TIM2-SR~TIM_SR_UIF;}编译后TIM2_IRQHandler不会被放入.text段而是被放入一个叫.ramfunc的段。你可以通过查看.map文件确认.ramfunc 0x20000000 0x48 load address 0x08001000Step 2修改链接脚本为.ramfunc段分配正确的地址在链接脚本通常是.ld文件中你需要在SECTIONS里添加类似这样的定义SECTIONS { /* Flash 区域 */ .text : { *(.text) *(.text.*) } FLASH /* RAM 代码段加载在 Flash运行在 RAM */ .ramfunc : { . ALIGN(4); __ramfunc_start__ .; *(.ramfunc) . ALIGN(4); __ramfunc_end__ .; } RAM ATFLASH /* 其他段... */ }关键语法RAM ATFLASH这告诉链接器.ramfunc段的**运行地址VMA**在 RAM**加载地址LMA**在 Flash。启动时需要从 Flash 拷贝到 RAM。__ramfunc_start__和__ramfunc_end__是我们自定义的符号启动代码会用到它们。Step 3启动代码中执行拷贝在你的启动文件startup.s或system.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(dstend){*dst*src;}}voidSystemInit(void){// ... 其他初始化 ...copy_ramfuncs();// ...}注意不同工具链中加载地址的符号名可能略有不同。GCC 的链接器在RAM ATFLASH时通常会自动生成__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_value0x12345678;然后在链接脚本中把.calib_data固定在 Flash 的某个已知地址方便通过 JTAG 或 Bootloader 单独更新校准区。3. 配合used和KEEP保护关键段如第十六招所讲标记了自定义段的函数可能被垃圾回收。在链接脚本中使用KEEP.ramfunc : { KEEP(*(.ramfunc)) } RAM ATFLASH同时在源代码中加上__attribute__((used, section(.ramfunc)))双保险。4. 与noinline联合确保完整函数落入段中如果编译器将.ramfunc中的函数内联到调用者它就不再出现在.ramfunc段了。加上noinline可以确保它独立存在并被正确搬运__attribute__((section(.ramfunc),noinline))voidcritical_fn(void){/* ... */}四、留两个问题给你思考现在请你停下来思考这两个实际问题如果把一个函数放到了.ramfunc段但忘了在启动代码中拷贝它第一次调用时会发生什么怎么调试这种问题.ramfunc段的大小如果超出了链接脚本中分配的 RAM 区域链接阶段会发生什么如何优雅地检查并防止这种溢出想清楚这两个问题你就能在项目中安全地部署 RAM 代码而不是抱着侥幸心理祈祷它别崩。五、总结与思考题回答核心总结__attribute__((section(name)))将函数或变量放入自定义段配合链接脚本实现 Flash→RAM 搬运。主要用途高频 ISR 去 Flash 等待、外部存储器初始化代码、出厂校准数据固定位置。实现链路源码标记段 → 链接脚本RAM ATFLASH→ 启动代码拷贝。注意事项需要确保拷贝在调用前完成与used、noinline、KEEP配合防优化。思考题回答问题1忘记拷贝.ramfunc会怎样调用.ramfunc中的函数时CPU 会跳转到 RAM 中的地址取指但该地址的内容未被初始化可能是全 0、全 1 或随机值。后果通常是立即触发 HardFault 或 UsageFault执行未定义指令。调试方法是在调试器中查看 RAM 区域的内容确认是否已被正确拷贝。在copy_ramfuncs()中放置断点确认它被调用且拷贝了正确的字节数。在.map文件中检查符号值确认__ramfunc_start__和加载地址符合预期。临时在该函数入口点设置硬件断点观察 PC 是否跳转到了正确地址。问题2.ramfunc段超出 RAM 分配区域怎么办链接器会报错“section.ramfunc will not fit in regionRAM” 或类似信息。预防措施包括在链接脚本中使用ASSERT指令主动检查ASSERT(SIZEOF(.ramfunc) LENGTH(RAM), .ramfunc section too large for RAM!)限制放入 RAM 的函数的总体积定期在 CI 中运行size命令检查段大小。如果确实需要大量 RAM 代码考虑扩大 RAM 分配区、启用指令缓存以减少搬移需求或优化算法减小代码体积。好了第 27 招我们就彻底吃透了。从今天起检查你项目中那些对延迟敏感的函数让section属性帮它们脱离 Flash 的“慢车道”在 RAM 中享受零等待的极速。如果今天的内容让你对代码布局有了更底层的理解欢迎转发和点赞。下一篇我们继续挖用__attribute__((section(.noinit)))保留复位后不被清零的变量。咱们不见不散