ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F4的.sct分散加载文件深度解析:从内存映射到实际调试

STM32F4的.sct分散加载文件深度解析:从内存映射到实际调试 做嵌入式开发这些年有个文件一直很“玄学”——平时根本不用管它但一旦程序跑飞、变量莫名被清零、或者要做Bootloader跳转最后查来查去罪魁祸首往往是它。这个文件就是Keil MDK工程里那个后缀为.sct的分散加载文件。尤其用STM32F4系列时Flash大、SRAM也不小还带一块只能内核访问的CCM RAM如果只把sct当成“编译器自动生成的破文件”而不去理解它那你在做内存规划、代码重定位、外扩SDRAM的时候一定会付出惨痛代价。这篇文章我打算把STM32F4的sct文件从头到尾讲透不光是语法层面的解释更多是我实际调试中踩过的坑、总结出的排查思路。适合这类读者已经能熟练点灯、会配置外设中断但一打开sct文件就头皮发麻的人或者正在做Bootloader、想把关键代码和数据放到指定内存区域、却不知道从哪里下手的人。1. 为什么搞懂sct文件能救你一命先说个我自己的经历。有次做F407的音频采集DMA采完数据之后在中断里做FFT结果算出来的频谱全是乱的。我一度以为是ADC配置错了反复查DMA、查触发源浪费了大半天。后来灵机一动看了一眼工程里的sct文件才发现某个大数组被放到了0x10000000开头的CCM RAM区。这地方DMA控制器根本访问不到搬运过来的数据全是无效的。问题不在驱动而在内存放错了位置。这就是sct文件的第一个价值它决定了你的变量、代码、堆栈最终落在物理地址的哪个位置。程序能不能跑看起来是逻辑问题但很多时候是“东西放错了地方”的问题。再往深一层说MDK里.sct文件对应的是ARM的armlink链接器使用的分散加载描述文件英文叫Scatter File。它存在的意义是告诉链接器你的程序分几个区域加载、分几个区域执行、哪些数据要初始化、哪些数据要清零。听起来复杂但你可以把它理解成一张“仓库货架图”。编译产物RO、RW、ZI段就是一堆货物sct文件规定了哪些货放到哪个货架货架分别在哪面墙、多大面积。货架放错了拿货时就找不到面积写小了货塞不下链接直接报错面积写大了浪费宝贵的RAM。跟GCC系的ld链接脚本对比可能更好理解stm32f407x.ld里那些MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1M }的写法在Keil里就被翻译成了sct文件里的加载域和执行域。所以你在网上查资料时会遇到“分散加载”“Scatter File”“链接脚本”“内存映射”这些词本质都在说同一件事。理解了这层关系你就明白不是非要用sct才能做嵌入式开发像MDK默认情况下sct是自动生成的你确实可以一辈子不碰它。但如果你想回答这三个问题——我的数组到底在哪个地址为什么这个函数执行特别慢App程序凭什么能从Bootloader跳过去——你就绕不开sct。它不是事后的分析工具而是你在写代码之前就该规划好的东西。2. STM32F4的内存地形硬件地图先画出来在逐行拆解sct之前我们必须先把STM32F4的内存地形刻在脑子里。没有这张地图看sct就像拿着仓库平面图却不知道仓库本身长什么样。以最常见的STM32F407VE为例它有三块主要的内存区域区域起始地址大小总线接口特点Flash0x08000000512KBVE/1MBVGI-Bus / D-Bus / S-Bus掉电不丢存放代码和只读数据SRAM0x20000000128KB系统总线BusMatrix主存储区DMA可以访问CPU和外设都走这里CCM RAM0x1000000064KB内核私有数据总线只有CPU能访问DMA和外设都够不着第一块Flash不用多说代码的归宿。要注意F4的Flash是支持指令预取和ART加速的所以代码跑在Flash里通常不比RAM慢多少没必要一上来就想着“把代码挪到RAM跑更快”。第二块SRAM是主战场。所有的全局变量、堆栈、DMA缓冲区默认都在这里。因为它挂在BusMatrix上CPU、DMA1、DMA2、以太网MAC等外设都能访问。F4的内存映射里0x20000000往前还有一段位带别名区但那是另一个话题普通应用可以不关心。第三块CCM RAM是F4系列一个非常有特色的东西也是最容易坑人的地方。它叫Core Coupled Memory直连内核不需要经过总线矩阵所以CPU访问它延迟低、速度跟内核同频。但也正因为如此DMA控制器根本看不见它。我见过不止一个新手把大数组定义到CCM之后DMA传输的数据全是乱的还以为是DMA配置错了。这三块之外F4还有一大片外设寄存器区0x40000000起以及FSMC/SDRAM控制器映射的外部存储区F407的Bank1映射到0x60000000开始F429的FMC SDRAM Bank映射到0xC0000000。这些区域虽然不会出现在默认的sct文件里但当你外扩SDRAM、NOR Flash、LCD显存的时候你就需要手动新增执行域把它们纳入“仓库地图”。搞懂这些之后再回头看默认生成的sct你会觉得它其实很直白Flash用来放代码ROSRAM用来放变量RW和ZICCM RAM默认空着不用。你改sct无非就是在这张地图上重新划分势力范围。3. 一个最普通的F4工程sct文件里到底写了什么现在咱们打开一个默认的STM32F407 Keil工程找到Options for Target - Linker - Scatter File默认会勾选Use Memory Layout from Target Dialog相当于MDK根据你在Target页填的ROM/RAM地址自动生成sct。如果你把它取消勾选旁边会出现一个Edit按钮点开就能看到完整的自动生成内容。以F407VE为例默认大概长这样LR_IROM1 0x08000000 0x00080000 { ; 加载域从Flash起始地址开始长度512KB ER_IROM1 0x08000000 0x00080000 { ; 执行域同样在Flash *.o (RESET, First) ; 中断向量表必须放在最前面 *(InRoot$$Sections) .ANY (RO) ; 所有只读段代码常量 } RW_IRAM1 0x20000000 0x00020000 { ; 执行域SRAM起始地址128KB .ANY (RW ZI) ; 可读写数据和零初始化数据 } }看着是不是很简单但这五行话背后藏着不少门道。先说LR_IROM1。LR是Load Region的缩写表示加载域。程序烧录到Flash里的原始形态都归它管。括号里第一个数字0x08000000是Flash的起始地址第二个0x00080000是大小。注意这里的大小是“最大可用空间”不是实际占用。你可以把它理解成给这个货架划定的边界货物没装满是允许的超了链接器就报错。ER_IROM1里的ER是Execution Region执行域。加载域描述的是“程序待着的地方”执行域描述的是“程序跑起来之后各部分待的地方”。对于不重定位的代码加载域和执行域地址一样都在Flash。ER_IROM1里第一行*.o (RESET, First)是整个文件的灵魂。它告诉链接器把目标文件里名为RESET的段放到这个执行域的最前面。RESET段存放的就是中断向量表也就是startup_stm32f407xx.s里的那块数据包含初始堆栈指针和所有中断处理函数入口。为什么必须放最前因为Cortex-M内核上电后固定从0x08000000读取初始SP从0x08000004读取复位入口。向量表不放在这里CPU上电第一步就找不着北。*(InRoot$$Sections)这行很多人看不懂它其实是C库初始化代码需要的特殊段保证__main、启动拷贝逻辑等关键函数被放在Root区域也就是能够被上电后立即找到的地方。.ANY (RO)是“分配主力”。.ANY是armlink的一个通配符表示“如果前面没有更具体的匹配就用它来兜底”。RO表示只读段也就是代码段和const常量。所以这一行的意思是所有没被特殊指定的代码和常量都往这个Flash执行域里塞。这也是为什么你的代码基本都落在0x08000000附近的原因。再来看RW_IRAM1。0x20000000是SRAM起始地址0x00020000是128KB大小。RW表示可读写的数据段ZI表示零初始化段。RW数据是那些有初值的全局变量比如uint32_t counter 5;编译器会给它在Flash里存一份初值启动时由__main里的拷贝逻辑搬到RAM里。ZI数据是没初值的比如uint8_t buffer[1024];上电后直接清零。这两部分都放在SRAM里逻辑上很好理解Flash不能随便写RAM才能做变量。这个默认sct里其实还藏着两个隐形的执行域叫ARM_LIB_HEAP和ARM_LIB_STACK。你可能没在sct文件里见到它们但打开生成的.map文件一定会看到类似这样的条目ARM_LIB_HEAP 0x2007F800 Size 0x800 ; 堆 ARM_LIB_STACK 0x2007F000 Size 0x800 ; 栈这是C库自己管理堆栈的区域默认从RAM的末尾往下长。堆用于malloc栈用于函数调用和局部变量。用microLIB时这些区域不会单独显示但栈依然是启动文件里Stack_Size那个值指定的空间本质逻辑一样。如果你在sct文件里新增了一个执行域想放数组又没有给C库的堆栈预留足够空间就可能出现栈溢出覆盖到变量区的问题。这个细节后面讲坑的时候再展开。4. 动手改sct的四种高频实战场景理解了默认sct的意思接下来就可以谈怎么改了。我不主张没事乱改sct但下面这几个场景是你迟早要遇到的。4.1 把关键数据或热代码放到CCM RAMSTM32F407的CCM RAM是最容易被忽视的64KB快速内存。CPU访问它不需要经过总线矩阵所以延迟低、速度稳定。适合放两类东西一是中断服务函数里频繁读写的关键标志或小数组二是对时间敏感的热代码比如音频解码的某个循环体。要在sct里给CCM RAM开一个区域写法很简单RW_IRAM2 0x10000000 0x00010000 { *(.ccm_ram) *(.ccm_ram*) }然后在代码里把变量指到对应段__attribute__((section(.ccm_ram))) volatile uint32_t g_irq_flag; __attribute__((section(.ccm_ram))) float fft_input[256];编译后用调试器看内存你会看到这些变量的地址以0x1000开头。实测下来在某些内存带宽争用激烈的场合比如DMA大量搬运CPU密集计算同时进行把热点数据放CCM确实能感受到差异。但千万记住DMA不能访问CCM RAM。ADC采集缓冲区、SPI收发缓冲区、以太网描述符这类要跟外设打交道的数据绝对不能放进来。4.2 划一块独立“物理地址固定”的缓冲区有些场景下你想让某个大数组固定在某一个特定地址可能是为了给DMA描述符用可能是为了两个程序比如Bootloader和App通过约定地址共享数据也可能只是为了调试时看内存方便。除了用__attribute__((at(address)))这种“霸道”的定点方式更优雅的做法是在sct里定义一个专门的执行域然后把特定section放进去。举个例子你想把0x2001C000开始的地方划出16KB作为系统共享内存区RW_IRAM_SHARE 0x2001C000 0x00004000 { *(.shared_mem) }代码里这样定义变量__attribute__((section(.shared_mem), zero_init)) uint8_t shared_buf[0x4000];这样做的好处是地址集中管理以后想调整位置只需要改sct一个地方不用到源码里改at地址。这在Bootloader和App约定升级标志、版本信息时特别好用。两个工程在各自的sct里声明同一个地址区域数据结构体定义一致就能实现跨程序通信。4.3 Bootloader App的地址重映射做IAP升级时sct文件的重要性体现得最淋漓尽致。App程序不能从0x08000000跑因为那里住着Bootloader。假设你给App分配从0x08010000开始、大小0x00070000的空间那么App工程的sct要改成这样LR_IROM1 0x08010000 0x00070000 { ER_IROM1 0x08010000 0x00070000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } }光改链接地址还不够这是最容易被坑的地方。App启动第一件事要设置向量表偏移在SystemInit()或main()最前面加SCB-VTOR 0x08010000;否则中断来了CPU还是去0x08000000找向量表结果跳到Bootloader的向量表然后进了一个错误的中断处理函数表面上看起来就是“程序跑起来但一进中断就卡死”。此外还要注意Bootloader跳转到App前要确认App的合法性常见方式是检查0x08010000处的前两个字第一个字必须是合法的栈顶地址在SRAM范围内第二个字是复位入口地址。如果不合法就不能跳。这部分代码逻辑不在sct里但地址参数全部由sct规划决定。4.4 外扩SDRAM/外部存储器的执行域分配如果你用的是F429/F439这种带FMC的型号外扩了一片SDRAM想让大数组直接落在SDRAM里同样要靠sct。SDRAM的地址在FMC映射到0xC0000000假设外扩32MBRW_IRAM3 0xC0000000 0x02000000 { *(.sdram) }然后代码里__attribute__((section(.sdram), zero_init)) uint8_t lcd_frame_buffer[800*480*3];但是这里有一个巨大的隐藏前提SDRAM必须先把FMC和SDRAM控制器初始化好才能访问。链接器不会帮你初始化硬件。这意味着你不能在启动阶段让C库自动把RW初值拷贝到SDRAM也不能让ZI清零逻辑碰SDRAM。所以一般不会把带初值的RW数据放SDRAMZI可以但要确保访问之前你已经调用了FMC_SDRAM的初始化函数。最稳妥的写法是像上面那样用zero_init并且专门写一个SDRAM_Init()函数在main里第一件事就调用之后才能用这块内存。如果你在main之前的全局对象构造或C库初始化阶段就去访问SDRAM大概率直接HardFault。5. 改sct最容易翻车的三个坑这一节我原本不想写因为每个坑说起来都是血泪史。但既然标题叫“理解sct文件”不把坑写透等于没理解。下面这三个问题是我见过以及亲身踩过的最高频的三个雷区。5.1 新增执行域后变量没被初始化读出来是垃圾值有次我想把一个大数组放到内部SRAM尾部专用的区域于是参考网上的写法加了执行域和section也链接成功了。结果程序运行起来数组里全是随机数而且第一次读和第二次读还不一样。仔细排查之后发现这个数组我用的是普通定义方式__attribute__((section(.my_buf))) uint8_t big_buf[4096];它默认会被当成ZI处理吗不一定。如果编译器认为这个section的属性是RWC库启动初始化时会把Flash里的一段初值拷到RAM里如果没初值理论上应当清零但只有链接器明确知道这个区域需要在启动阶段初始化时才会生成清零代码。问题在于我新增的执行域没有告诉链接器如何处理ZI段或者使用了.ANY (RW)把普通RW数据混进来了。实际排查时我用调试器看了big_buf的地址发现它确实在我的新执行域里但那个区域在启动时没有被__main的拷贝/清零逻辑覆盖到。产生这个问题的根源是__main在进入main之前会根据sct文件里的执行域信息做三件事——把加载域的RW数据拷贝到执行域把执行域的ZI段清零然后跳转__rt_entry。问题是如果你用.ANY (RW)匹配了某些段或某些带初始值的变量被放进新增区域而链接器未能生成对应的拷贝指令数据自然不会正确。完整的排查链路是这样的打开调试器看变量地址是否落在了预期执行域范围内。打开.map文件搜索该变量名字看它被分配在哪个Execution Region。查看该Execution Region的Type如果显示RW确认加载域里有没有对应的初始化数据如果显示ZI确认启动阶段有没有对这段地址执行清零。在__main入口打断点单步到__scatterload_copy和__scatterload_zeroinit看有没有处理到你的区域。如果发现没处理最简单的修法是避免在自定义区域放带初值的RW数据尽量用zero_init修饰并且在第一次使用前手动memset。或者更稳妥的是不要自己声明一个从来没定义过的section而是把变量放在现有执行域里用地址规划实现分区。这告诉我一个原则新增执行域时不要在sct里用太多.ANY通配。.ANY会吸引大量你没想放到这里的段导致莫名其妙加大RAM占用。要用完整名字精确匹配比如*(.my_buf)不要用.ANY (RW)去覆盖全工程。5.2 CCM RAM和DMA一个让数据全乱的大坑我在第一节讲过自己的经历这里再展开一次。当时我在sct里加了RW_IRAM2放音频缓冲区看到CCM RAM有64KB觉得不用白不用就把ADC的DMA缓冲区也塞进去了。结果就是采出来的波形乱得没法看。排查那个问题时我一度怀疑DMA配置、怀疑ADC采样时间、怀疑触发电平最后用调试器看内存才发现——DMA搬运确实在“运行”但CCM RAM区域的值从头到尾没有变化。原因很简单DMA控制器挂在BusMatrix上它访问存储器的路径里没有CCM RAM。你给它一个0x1000xxxx的地址它根本不知道往哪里读写。判断标准其实很直接凡是外设会访问的内存DMA源/目标、USB缓冲区、以太网描述符、LTDC显存描述符等一律放0x20000000的SRAM区。只有纯CPU计算的变量、中断标志、热代码可以放CCM。修改方法就不用多说了把缓冲区定义到普通SRAMsct里那段CCM区域只保留int类型的标志位和临时数组就好。5.3 Bootloader跳转App后跑飞地址偏移漏改了一处做Bootloader时最典型的症状是单独烧App裸机程序一切正常但从Bootloader跳过去就死或者一进中断就复位。99%的情况是地址重映射没做全。我整理过一份检查清单每次做App工程都会逐项确认链接器地址sct里的LR_IROM1和ER_IROM1起始地址改成App的偏移比如0x08010000。这个没改整个App的符号地址都是错的跳过去当然跑不了。烧录起始地址下载算法Flash Download里Programming Start Address必须改成0x08010000否则烧进去还是在0x08000000。VTOR偏移SCB-VTOR 0x08010000必须在早期执行虽然现在的F4也支持在C代码里设置但一定要在中断使能之前。中断向量表本身App的startup文件里的向量表会被链接器放到新地址这个不用担心但跳转之后CPU会从新的向量表取地址所以VTOR与链接地址必须一致。Bootloader跳转逻辑跳转前要把全局中断关干净把SysTick、外设中断都停掉跳转时用函数指针去调用App的复位地址((void(*)(void))*(volatile uint32_t*)0x08010004)()。有一次我只改了sct和烧录地址忘了在App里设置VTOR结果Bootloader跳过去后连续点几百毫秒才进一次中断一进就复位。后来在main第一行加了VTOR设置问题立刻消失。后来我养成了习惯写App工程时上来第一件事就是设置VTOR并且在sct文件里用注释标明偏移量防止过半年回来自己都忘了为什么是这个地址。如果说上面三个坑有什么共性那就是改sct不是改完就结束你必须验证内存的实际分布是否符合预期。怎么验证看.map文件、看调试器Memory窗口、看启动汇编的拷贝地址这些手段缺一不可。6. 调试与验证怎么确认你的sct真的按预期工作改完sct之后我从不直接信心满满地全速运行。下面这些验证步骤是我的固定流程每一步都能帮你省下两小时的排查时间。6.1 看Build Output里的Program Size编译完成后的输出窗口会显示Program Size: Code12340 RO-data456 RW-data78 ZI-data4096注意这里的RW-data ZI-data通常对应你所有执行域的空间占用但不区分具体落在哪块RAM。如果ZI数据比预期大很多就要怀疑是不是.ANY (RW ZI)把你的大数组全塞进SRAM了而你在sct里新增的区域根本没被使用。这时候就要去看.map文件。6.2 学会看.map文件里的Execution Region.map文件是链接器的“收货清单”。打开之后找到类似这样的段落Execution Region RW_IRAM1 (Base: 0x20000000, Size: 0x00012345, Max: 0x00020000, %used: 35.7%) Execution Region RW_IRAM2 (Base: 0x10000000, Size: 0x00000000, Max: 0x00010000, %used: 0.0%)如果你辛辛苦苦在sct里加了RW_IRAM2结果Size: 0x00000000说明没有任何变量放进去。这时候去搜你的section名字比如搜.ccm_ram看它到底被哪个执行域收编了。我见过有人变量名拼写和section名字不一致导致区域空置的情况链接器不会报错但功能就是不对。看符号地址也在这里搜索。比如你搜g_irq_flag能看到类似g_irq_flag 0x10000000 Data 4 main.o地址是0x10000000就对了。如果发现它落在0x20000000说明sct里的匹配规则没生效赶紧去查section名称。6.3 调试器里现场验证内存分布Keil的调试器里View - Memory Windows - Memory 1可以直接输入地址观察。比如你在sct里划了RW_IRAM_SHARE 0x2001C000就输入0x2001C000看数据区域能不能被正确清零或写入。这个方法尤其适合验证Bootloader和App共享内存是否真的在同一块物理地址。还有一个很少人用但极好用的技巧在startup_stm32f407xx.s的Reset_Handler里__main调用之前打断点。这时候C库的分散加载还没有完全跑完你可以看SP的初始值、看VTOR指向哪里也能间接判断向量表有没有放对位置。如果SP初始值不是RAM范围内的合法地址多半是sct里执行域起始地址写错了。6.4 对比改动前后的map文件如果是在维护旧项目我强烈建议改sct之前先编译一次把.map文件备份一份。改完再编译一次然后对比两个map文件里Execution Region的变化。有时候你只是改了一行地址结果某个大数组从一个RAM区“漂移”到另一个RAM区这种问题光靠脑子想是想不出来的必须靠对比。我甚至会把map文件里所有符号的地址列出来写个小脚本排序检查有没有哪个变量落到了不该落的位置。这个方法很土但非常有效。就我个人经验来说理解sct文件最难的不是语法而是建立“内存视角”。写代码时习惯性地把变量抽象成一个个int、float但它们在硬件上都是躺在某个物理地址上的字节。而sct文件就是那个帮你决定“躺哪”的管家。最后分享一个小习惯我每次新建工程都会在sct文件头部加一段注释写清楚ROM/RAM分配、Bootloader偏移、哪些区域是给DMA用的、哪些区域绝对不能放DMA。等过几个月回头维护代码看到这些注释真的会感谢当时的自己。如果你也想改sct去实现什么功能先别急着抄网上的代码打开你的.map文件看看现状改完再对照着验证一遍你会少吃很多苦头。
返回列表