搞嵌入式这几年我见过太多在Keil里栽在IROM1和IRAM1上的案例了。这两个参数就藏在Options for Target的Target标签页里很多朋友打开工程以后看都不看或者干脆从别人的工程里复制一份配置直接用。程序小的时候无所谓一旦代码规模上来、功能一多各种诡异问题就接踵而至程序下进去跑不起来、全局变量莫名被清零、硬件仿真一进中断就死机……排查半天最后发现根子居然在IROM1和IRAM1上。这篇文章准备把这个话题彻底讲透包括这两项到底管什么、为什么总有人填错、如何根据芯片型号和实际工程需求把数值算对以及我在实际项目中踩过的一些坑和对应的排查思路。无论你是刚接触STM32的新手还是已经写过不少工程但没仔细研究过链接配置的开发者这篇都值得花几分钟看完。1. 这两行配置到底是什么为什么总有人填错1.1 先说清楚Flash和RAM的分工你就懂IROM1和IRAM1了STM32内部的存储器大致分两类一类是Flash掉电不丢用来存放代码和只读常量另一类是RAM掉电即失用来存放全局变量、局部变量、堆栈这些运行时数据。Keil工程里的IROM1和IRAM1本质上是告诉链接器“你写写画画的时候能用Flash和RAM的哪些地址范围”。为了方便理解可以把Flash想象成书架RAM想象成办公桌。你的程序源码就是一本本书书只能放上书架才能长期保存运行时需要临时查阅的草稿纸、计算器就放在办公桌上随手拿随手放。IROM1就是给编译器划定了“书架上有哪些格子可以用”IRAM1则是告诉编译器“这张办公桌有多大”。对绝大多数STM32来说内部Flash的起始地址固定是0x08000000内部RAM的起始地址固定是0x20000000。这是芯片设计时定好的地址映射不会因为换型号就变。需要变的是这两个起始地址往后的“可用范围”也就是Size字段。这一块背后的计算逻辑其实非常直接0x08000000加上Flash容量就是Flash地址范围的终点。0x20000000加上RAM容量就是RAM地址范围的终点。举个具体例子STM32F103C8T6Flash是64KBRAM是20KB。那么IROM1起始地址0x08000000大小0x1000064KB。IRAM1起始地址0x20000000大小0x500020KB。只要这两个Size不超过芯片物理范围配置就是基本合理的。超过物理范围就相当于告诉编译器“书架比实际大”链接器会把内容安排在根本不存在的格子里程序运行时不炸才怪。1.2 填错参数后的两种典型“翻车现场”很多人以为IROM1和IRAM1填错了编译器会立刻报错实际上真不是这样。它们翻车的方式往往很隐蔽这也是这几个配置特别坑的原因。第一种是IROM1填得比芯片实际Flash大。编译可能正常通过因为从链接器的角度看这只是给它画了一个更大的可用范围地址空间依然连续但生成的HEX或BIN文件一旦超出物理Flash容量下载器烧录时会直接失败或者烧录进去后程序在启动阶段就跑到无效地址上去执行表现为“上电没反应”或“偶尔能跑、复位就死”。如果你手上的工具不检查烧录地址这个问题甚至会一路带到产线上去到那时候再排查就十分被动了。第二种是IRAM1填得比实际RAM大。这种情况下最典型的表现是程序能编译下载正常但运行一段时间后某些全局变量会被莫名其妙地清零或者一进中断服务函数就HardFault。原因在于编译器把变量安排到了物理上不存在的RAM区间CPU一旦访问这些地址就会触发总线错误。这种问题往往不是必现的可能跑几秒、几分钟甚至几小时才出现但它的出现频率和你的栈深度、局部变量分配方式直接相关排查起来极其痛苦。还有一类常见错误是把IRAM1整体改小或改错起始地址导致堆栈区被压缩递归调用稍微深一点就栈溢出。栈溢出初期也是“偶尔死机”后面会越来越频繁最后进程彻底跑不起来。这类问题最大的迷惑性在于它不会在链接阶段报错容易被人当成硬件问题去排查白白浪费大量时间。2. 手把手把IROM1和IRAM1填对2.1 第一步按芯片型号查容量别靠记忆不同型号的STM32Flash和RAM容量差异很大。哪怕是同一个系列后缀不同容量也可能完全不同。做配置之前先对照芯片丝印和官方数据手册确认两个关键数字Flash总容量、RAM总容量。我整理了一份常见型号的对照表可以当作日常开发的快速参考芯片型号Flash大小Flash起始地址RAM大小RAM起始地址STM32F103C8T664KB (0x10000)0x0800000020KB (0x5000)0x20000000STM32F103RCT6256KB (0x40000)0x0800000048KB (0xC000)0x20000000STM32F103ZET6512KB (0x80000)0x0800000064KB (0x10000)0x20000000STM32F407VET6512KB (0x80000)0x08000000128KB (0x20000)0x20000000STM32F407ZGT61MB (0x100000)0x08000000128KB (0x20000)0x20000000STM32H743VIT62MB (0x200000)0x08000000512KB (0x80000)0x20000000这里要特别强调一下F407系列还带一块64KB的CCM RAM起始地址是0x10000000默认情况下Keil不会把它算进IRAM1因为这块RAM只能由CPU内核访问不能给DMA外设使用。后面我会单独讲它怎么处理。另外同一型号的芯片也可能存在批次差异。网上有传言说某些C8T6芯片实际Flash是128KB但我个人的建议是不要赌按数据手册写的来。你赌赢了多出来的空间用起来提心吊胆赌输了量产阶段批量出问题付出的代价远大于省下的那颗芯片钱。做产品不是做实验稳一点永远没错。2.2 第二步打开Options for Target把参数填进去确认了芯片容量接下来就是实际操作。具体路径是Project菜单 - Options for Target - 第二个标签页Target。在这个页面里你需要关注几个区域IROM1区域勾选Read-Only Memory Area下面的复选框在Start框填0x08000000在Size框填上一步查到的Flash大小对应的十六进制数值。IRAM1区域勾选Read-Write Memory Area下面的复选框在Start框填0x20000000在Size框填RAM大小对应的十六进制数值。右上角的Code Generation区域里把Use MicroLIB勾上。这个小选项的作用是用精简版C库替换标准C库能明显减少RAM和Flash占用特别是当你用printf这类输出函数的时候不勾它RW-data和ZI-data往往会涨得让你怀疑人生。这里有件事一定要提醒Target页面底部有个IRAM2区域偶尔会被误用。很多人以为把CCM RAM的地址填进去就能白赚64KB空间结果发现程序居然还能运行就放着不管了。实际上链接器分配变量时并不会区分这块内存能不能被DMA访问如果你把需要做DMA传输的缓冲区放到了CCM RAM里轻则数据收不到重则进入硬件错误中断这种问题非常难查。所以没有充分把握的情况下IRAM2建议保持空白。配置完成后的样子应该是这样的以STM32F103C8T6为例IROM1: Start 0x08000000 Size 0x10000 IRAM1: Start 0x20000000 Size 0x5000很多从别的工程复制配置的人最容易在这里翻车。比如从F103ZET6的工程复制过来默认还是0x80000和0x10000用到C8T6上程序能编译部分地址却已经超出芯片物理范围结果就是下载报错或者运行异常。所以每次新建工程、更换芯片都建议先回来核对一下这一页。2.3 第三步第三步是核对Linker设置改了却没生效多半是它Target页填好了还需要去看一下Linker标签页。正常情况下Keil会勾选一个选项叫Use Memory Layout from Target Dialog意思是链接器直接使用Target页里你刚填的IROM1和IRAM1配置自动生成分散加载文件。这个勾选状态是配置生效的关键。如果你在Target页改了半天编译后查看Map文件发现变量和代码的地址还是老样子多半就是Linker页里这个选项没勾上或者工程里手动指定了一个分散加载文件Scatter File它会把Target页的配置完全覆盖掉。我对这种场景比较熟悉因为有些进阶工程为了把变量放到外部SRAM或者做复杂的地址布局会取消这个勾选手工维护一个.sct文件。这时你去改Target页相当于改了一份永远不会被读取的配置自然没有任何效果。判断方法很简单如果勾选了Use Memory Layout from Target Dialog一切以Target页为准。如果没勾选Target页的IROM1和IRAM1只是“建议”真正的布局全看右边Scatter File里指定的文件。很多人在网上求助“为什么我改了IROM1/IRAM1没反应”绝大多数都是这个原因。排查思路并不复杂但确实容易被忽略。2.4 用编译输出反向验证心里更有底填完配置以后编译一下看Build Output窗口里的Program Size就能大致估算出当前程序对Flash和RAM的占用情况。Keil输出的格式是这样的Program Size: Code30000 RO-data1000 RW-data500 ZI-data8000这里每个字段的含义需要搞清楚Code程序代码占用的Flash空间。RO-data只读常量比如const数组、字符串字面量也放在Flash。RW-data有初值的全局变量。初值存在Flash里启动时会被拷贝到RAM所以它同时占用Flash和RAM。ZI-data零初始化变量比如未赋初值的全局变量、静态变量只占用RAM。所以粗略估算公式是Flash占用 Code RO-data RW-dataRAM占用 RW-data ZI-data注意RAM占用还要加上启动文件里定义的堆栈大小。Keil自带的启动文件默认Stack_Size是0x4001KB、Heap_Size是0x200512B它们不会被算进ZI-data里但实际运行时确实占RAM。如果用到了FreeRTOS这类系统任务栈会以全局数组的形式分配这些数组已经包含在ZI-data里了不需要重复计算。举个例子我早期做F103C8T6的一个项目时编译输出如下Program Size: Code42000 RO-data1200 RW-data600 ZI-data8500用上面的公式算一下Flash占用 42000 1200 600 43800字节约42.7KB而芯片Flash是64KB剩余空间充足。RAM占用 600 8500 9100字节约8.9KB再加上Stack 1KB和Heap 0.5KB总共约10.4KB芯片RAM是20KB剩余将近一半配置合理。把计算后的值和IROM1/IRAM1配置的大小比较一下基本就能确认参数是否填得合理。如果你的Flash占用已经非常接近甚至超过配置值链接器会在编译时直接报错提示区域溢出。这个报错不需要太紧张它反而是个好信号至少比“编译通过但运行崩溃”好查得多。3. 进阶场景BootLoader预留、CCM RAM和分散加载3.1 需要BootLoader时IROM1起始地址怎么算很多产品都需要IAP在线升级也就是BootLoader App的结构。这时App固件不能占满整个Flash的起始地址因为起始区域是留给BootLoader的。IROM1的Start字段就需要往后挪。具体怎么挪取决于BootLoader本身占用多大空间。比如我常用的做法是BootLoader编译出来后看Program Size假设它占用了约8KB的Flash那我会按Flash扇区大小对齐来预留。对STM32F103中容量芯片来说一页是1KB对齐8KB就是0x2000对STM32F4系列来说扇区最小也有16KBBootLoader哪怕只有5KB也得预留16KB。这里最怕出现的情况是BootLoader和App都用了0x08000000作为起始地址打包给产线烧录后App把BootLoader覆盖了设备直接变成砖头。产品开发中这种低级错误并不少见所以上产线之前一定要在工程里确认App的IROM1起始地址确实和BootLoader预留区域错开了。假设BootLoader预留16KBApp的IROM1应该这样填项目值说明Start0x080040000x08000000 0x400016KBSize0xC0000x10000 - 0x4000即64KB总Flash减去16KB末尾地址0x0800FFFF0x08004000 0xC000 - 1如果你在此基础上加更多功能发现App空间不够优先考虑优化BootLoader大小而不是直接把Start往前挪覆盖BootLoader区域。升级功能是整个产品的最后防线一旦被破坏后续所有维护都会非常被动。3.2 F4系列的CCM RAM到底要不要用F4系列比如STM32F407的CCM RAM是一个很容易让人纠结的话题。它是64KB起始地址0x10000000速度很快和CPU同频访问用来放临界数据或高频中断里要操作的标志位效果确实很好。但前面也说了它不能被DMA访问也不能用于以太网、USB等外设的缓冲区。Keil对F407的默认Device配置里IRAM1通常是0x20000000开始的128KBCCM RAM需要自己在IRAM2里手动加。我个人建议在多数项目里不要轻易把CCM RAM加入自动分配范围。因为一旦加入链接器会在它认为合适的时候把你的变量塞进去而你很难保证所有变量都不会被DMA操作。如果需要充分利用CCM RAM更稳妥的做法是手工指定地址。比如我常在代码里这样用__attribute__((at(0x10000000))) uint8_t fast_buffer[1024];这句的含义是把fast_buffer这个数组固定放在CCM RAM开头只有明确清楚这个变量用途的自己能控制它。注意使用__attribute__语法之前在Keil的Target页里最好不要把IRAM2填上CCM地址否则会出现“同一段地址被链接器管理又被编译器硬性分配”的冲突。二者选其一别混着来。还有一个和CCM相关的坑CCM RAM不支持DMA那DMA的缓冲区放哪里答案是最普通的SRAM也就是0x20000000开始的区域。设计数据结构时需要DMA访问的缓冲区要明确标注别让编译器自动分配到奇怪的地方。3.3 分散加载文件sct和IROM1/IRAM1的关系前面提过Linker页的Scatter File选项这里再展开讲一下。Keil在勾选Use Memory Layout from Target Dialog时会根据Target页的IROM1/IRAM1自动生成一个分散加载描述。这个描述本质上就是告诉链接器可用的执行区域有哪些、分别从哪个地址开始、大小是多少。你手动指定一个.sct文件后IROM1/IRAM1的配置就被旁路了。手动维护sct文件常见于外扩SRAM的场景。STM32的FSMC或FMC接口可以外挂一片SRAM地址比如是0x68000000。默认Target页里没有这个区域想用就必须在.sct文件里手动加一个执行区域。这时候工程里通常要把Use Memory Layout from Target Dialog取消勾选并在Scatter File路径里指定你的sct文件。我见过有工程师在Target页里把IRAM2填上0x68000000试图让链接器把变量放到外部SRAM结果发现程序一跑就死原因是外部SRAM的初始化代码在链接器分配完成前根本没法执行变量区和使用它的代码时序对不上。这种情况下手工控制sct文件才是正路。所以当你的工程涉及外部存储器时建议花点时间把sct的语法和结构弄明白这比在Target页里瞎试靠谱得多。4. 常见问题与排查经验速查4.1 一张表对照症状找根因很多读者遇到“程序不稳定”时第一反应是查代码逻辑、查硬件电路但问题可能就出在链接配置上。这里把我在实际项目和社区答疑中见到的典型症状整理成一张速查表方便大家对照症状可能原因优先排查方向编译报错L6220E提示Execution Region溢出Flash或RAM实际占用超出配置范围看Program Size确认是Flash溢出还是RAM溢出能编译烧录时报错或写不进去IROM1/IRAM1设置超过了芯片物理容量对照芯片型号查Flash/RAM大小全局变量在运行中悄悄被清零IRAM1配置超出物理RAM变量落在无效地址检查IRAM1的Size测量变量实际地址一进中断或调用稍深就死机栈空间不足或栈溢出到了无效区域检查Stack Size、IRAM1大小和ZI-data总量Target页改了IROM1/IRAM1没有任何效果Linker页没有勾选Use Memory Layout from Target Dialog检查Linker页的Scatter File选项App程序把BootLoader覆盖了两个工程的IROM1起始地址一样核对App工程的Start和Size4.2 排查时重点看哪几个地方如果你排查的问题确实和IROM1/IRAM1相关有三个信息源是绕不开的。第一个是Build Output窗口的Program Size。它能帮你快速判断当前编译产物有多大。注意别只看CodeRW-data和ZI-data的联动关系才是定位RAM问题的关键。第二个是Map文件。在编译完成后.map文件会详细列出每一个Execution Region的起始地址和占用大小。用文本编辑器打开搜索Execution Region关键字可以看到类似这样的内容Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00002400, Max: 0x00005000, ...)这里的Size是当前实际占用Max是你IRAM1里配置的大小两者一对比空间余量一目了然。如果Size很接近Max说明RAM已经吃紧优化代码或调大IRAM1之前先想想哪个方向更靠谱。第三个是烧录器的日志。使用ST-Link或者J-Link烧录时日志会明确显示写入地址范围和文件大小。如果写入范围超过了芯片Flash物理范围日志里通常会有警告或错误。遇到这种提示别再怀疑烧录器坏了回头检查IROM1吧。4.3 几条值得记下来的避坑经验开发过程中踩过坑之后我发现有几个习惯能大幅降低这类问题的排查成本。第一新建工程时先在Keil的Device列表里选中自己用的芯片型号接着在Target页点一下小扳手图标旁边的Default按钮。Keil会自动根据器件数据库填充一组默认的IROM1/IRAM1值这个值通常和芯片规格书一致非常稳妥。在此基础上做少量调整比从一个陌生工程导过来复制粘贴可靠得多。第二配置完IROM1/IRAM1以后最好在工程注释或者README里写一句“本工程基于STM32F103C8T6Flash 64KBRAM 20KB如果换芯片请重新核对”。这种备注看起来有点啰嗦但它能在几个月后你接手旧工程时快速判断配置是否还能用省去重新查手册的时间。第三对于RAM比较紧张的MCU全局大数组优先用const放到Flash比如字库、图标、曲线表这类只读数据不要图省事定义成全局变量。一个几千字节的const数组放在Flash里对RAM的压力几乎为零而同样的数组放RAM很可能直接把可用空间吃穿。配合上面讲的Program Size估算方法你很快就能算出最优的数据放置策略。第四不要贪心把IRAM1的Size填满。建议留5%-10%的余量给堆栈和动态内存。没有余量的工程任何一次偶然的函数嵌套变深都可能触发栈溢出而这个时机的随机性会让所有人崩溃。宁可稍微控制一下全局变量的规模也要保证配置有合理的喘息空间。这些经验听起来都很基础但很多工程事故恰恰就是基础环节的细节没处理好。每次配置前多想一步“这个值在这个芯片上是否合理”就能少走很多弯路。