ARTICLE DETAIL

资讯详情

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

STM32F4分散加载文件SCT详解:从默认配置到高级定制

STM32F4分散加载文件SCT详解:从默认配置到高级定制 1. SCT文件在STM32F4工程里的角色1.1 先把它当成一张城市用地规划图做STM32F4项目比如基于STM32F4的音频信号采集与实时FFT频谱分析系统、多路定时器输入捕获、DAC波形发生这一类工程时如果你一直用Keil的默认配置可能从来都没主动打开过那个后缀叫.sct的文件。大多数情况下这完全没问题Keil会按照你Target页面里填的IROM地址和IRAM地址自动生成一份“够用”的SCT。可一旦你开始碰Bootloader、想把大数组放到外部SDRAM、想把某些代码放到RAM里跑SCT文件就是你绕不开的那道坎。把SCT文件理解为一张城市用地规划图最贴切。芯片里的Flash和RAM就是这座城市的地皮代码段、数据段、堆栈、堆分别是要落地的建筑。规划图规定了哪块地建什么、哪些建筑必须挨着大马路特定地址、哪些区域之间必须留出消防通道对齐、预留空间。没有这张图编译器虽然也能把程序构建出来但建筑落在哪里就完全随机了出了问题你连排查方向都没有。1.2 加载地址与执行地址差在哪理解SCT之前必须先分清两个概念加载地址Load Address和执行地址Execution Address。以STM32F407为例程序烧录后躺在0x08000000开始的Flash里CPU运行时通常也直接从Flash取指令这种情况下加载地址等于执行地址。但在某些场景下这两者必须分开。比如我把一段Flash擦写函数放在RAM里执行那么它的机器码仍然存储在Flash加载地址在Flash程序启动后由启动代码把它拷贝到RAM执行地址在RAMCPU再从RAM去取指令。这种“Flash睡大觉RAM里干活”的模式就是分散加载的核心思路。SCT文件所做的就是把每个段Section在加载视图和执行视图中的位置都明确写清楚同时告诉链接器什么时候该拷贝、哪些段要零初始化、哪些段只想保留地址不希望启动代码碰它。把它学透之后你再也不是只会在Target选项卡里改个起始地址的新手。2. 手把手读懂Keil自动生成的默认SCT2.1 拿到一份真实的F407 SCT文件在Keil MDK工程里打开Options for Target - Linker可以看到链接器用到的SCT文件路径。默认情况下取消勾选“Use Memory Layout from Target Dialog”之后这个文件才可以被手动编辑。STM32F407工程里Keil生成的最典型SCT长这样; *************************************************************** ; *** Scatter-Loading Description File generated by uVision *** ; *************************************************************** LR_IROM1 0x08000000 0x00100000 { ; load region size_1MB ER_IROM1 0x08000000 0x00100000 { ; load address execution address *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { ; RW data .ANY (RW ZI) } }这段文本看着不长信息量却很大。我们可以把它拆成两大块加载区Load Region和执行区Execution Region。加载区用LR_开头命名执行区用ER_或自定义名字命名。STM32F407的Flash容量是1MB从0x08000000开始所以加载区的大小限制写的是0x00100000。RAM从0x20000000开始共128KB所以RW_IRAM1的大小是0x00020000。2.2 加载区里的“两层结构”SCT文件使用了花括号嵌套外层花括号定义的是加载区内层花括号定义的是该加载区下的执行区。在我的理解里加载区回答的问题是“这些数据物理上存在哪里”执行区回答“运行时这些东西在哪里生效”。LR_IROM1 0x08000000 0x00100000这一行声明了一个名为LR_IROM1的加载区起始地址是0x08000000最大容量为0x00100000。编译出的所有RO只读内容、RW初始值都会被分配到这个范围内。内层ER_IROM1是一个执行区它的地址与加载地址相同所以这一块代码在Flash里直接运行不需要启动拷贝。如果你看到某个执行区地址和加载区地址不同比如ER_RAMCODE 0x20002000出现在LR_IROM1内部意味着这块内容“加载在Flash执行在RAM”启动代码要做一次搬运。这一点在后面实战案例里会展开。2.3 选择器行各自的含义花括号内部每一行都是对输入段的筛选规则。*.o (RESET, First)是优先级最高的一行它把所有目标文件里的RESET段收集起来放执行区的最前面。RESET段存放的是初始栈指针和中断向量表必须放在Flash起始位置CPU上电后第一件事就是从这里取栈指针和复位向量。*(InRoot$$Sections)是很多新手看不懂的一行。这行是为了满足__main库函数的特殊要求。启动代码在调用main之前要执行一段运行时环境初始化包括把RW数据从Flash拷贝到RAM、把ZI段清零、建立堆栈等。这些初始化库函数被放在名为InRoot$$Sections的段中它们必须运行在加载地址等于执行地址的“根区域”里否则CPU一开始连拷贝动作都没法执行程序就起不来了。.ANY (RO)和.ANY (XO)是兜底规则。.ANY和*.o不同的地方在于它的优先级更低前面没有匹配上的RO段只读代码和只读常量、XO段仅执行代码最后由它收编。这也是为什么你随便加一个C文件代码都能被分配进Flash因为编译器不会让任何段落单。2.4 RW_IRAM1里藏着启动拷贝的秘密RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) }定义的是RAM区域。RW段是初始值非零的全局变量和静态变量ZI段是初始值为零或未初始化的变量。编译后RW段在Flash里存有一份初始值的映像启动时__main会把这部分数据从Flash搬运到RAM的0x20000000处ZI段则只占用RAM空间启动时被清成全零。这里要注意链接器是把RW和ZI放在同一个执行区里连续排布的然后启动代码根据链接器生成的符号知道从哪里拷贝、拷多长、清零多少。如果RAM区域规划出了问题比如RW_IRAM1太小链接时就会看到“Execution region RW_IRAM1 size overflowed”的报错直接把工程Failed。很多时候我们点编译看到这个错第一反应是怀疑代码太大其实根因往往是SCT默认只给了RAM区128KB而你塞进了一堆大缓冲。3. 实战按需求定制SCT文件3.1 修改SCT的正确操作路径在Keil中修改SCT文件有两大原则第一取消勾选“Use Memory Layout from Target Dialog”否则你改了也不会生效再次编译会被自动生成的SCT覆盖第二改完SCT后Target选项卡里的IROM、IRAM设置只能当作参考实际链接布局以SCT为准。具体操作顺序是打开Options for Target - Linker。把“Use Memory Layout from Target Dialog”前面的勾去掉。正常勾选“Scatter File”在输入框里填写你要用的.sct文件路径也可以点击旁边的“Edit”直接编辑当前生效的SCT。改完保存重新编译建议先Clean Target再Build。从这一刻起Target页面里的地址配置就不再生效了。实际上很多人踩过这个坑在Target页面把IROM1起始地址改成0x08008000却发现编译出的工程还是从0x08000000开始输出原因就是SCT文件仍然按旧的地址链接。你要么一直用自动生成模式要么全手动SCT不要混着来。3.2 把Flash操作函数放到RAM里跑先看一个我实际用过的案例做IAP在线升级时需要在应用里执行Flash擦写操作。如果擦写函数本身放在Flash里擦除过程中CPU从Flash取指令可能会被卡住因为Flash控制器正在执行擦除命令同一时刻没法同时响应指令取指。稳妥做法是把擦写函数整体搬到RAM执行。在代码里给函数指定段名以AC6或GCC编译器为例#if defined(__ARMCC_VERSION) || defined(__CC_ARM) #define RAMFUNC __attribute__((section(RAM_CODE), noinline)) #elif defined(__GNUC__) #define RAMFUNC __attribute__((section(.ramfunc), noinline)) #endif RAMFUNC void flash_erase_sector(uint32_t sector) { // 这里放实际的擦除流程比如操作FLASH_CR、FLASH_SR等 }然后在SCT文件中单独划出一块RAM_CODE执行区LR_IROM1 0x08000000 0x00100000 { ER_IROM1 0x08000000 0x00100000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x0001F000 { ; 主RAM区域留出4KB给RAM_CODE .ANY (RW ZI) } RW_RAMCODE 0x2001F000 UNINIT ALIGN 2 0x1000 { *.o (RAM_CODE) } }这里有几个细节值得说。RW_RAMCODE起始地址我设在0x2001F000这样既不让RAM_CODE和普通数据混杂预留的大小也足够放几个擦写函数。UNINIT的意思是告诉链接器这块区域不要做零初始化。由于函数代码不是普通变量启动时我们需要由代码自己把函数机器码拷贝到RAM或者更常见的是直接把这块区域交给__main的RW拷贝机制。如果你想让Keil启动代码自动完成拷贝就不要加UNINIT链接器会在Flash里为这个执行区单独分配一块加载空间上电后由__scatterload把机器码从Flash拷到RAM。我习惯上不用UNINIT让它走标准RW拷贝流程这样最省心。还有一个容易忽略的点放到RAM里的函数如果调用了别的函数被调的也要确保在RAM区或在Flash中可执行。如果被调函数在Flash里擦写期间去调它一样会有风险。所以放到RAM里的代码段最好是自包含的或者你把它依赖的工具函数一并放进去不要只搬了一层壳。3.3 把大数组放到外部SDRAM以STM32F429这类带FMC外部存储控制器的芯片为例。做音频采集和FFT频谱分析时需要连续存放4096点甚至8192点的float数据一个float占4字节4096点就是16KB再加上双缓冲、窗函数表内部128KB或192KB的RAM很容易被吃穿。此时最自然的想法是把大数组放到外部SDRAM。SCT文件增加一个外部SDRAM执行区RW_IRAM1 0x20000000 0x00030000 { .ANY (RW ZI) } RW_SDRAM 0xD0000000 0x00800000 UNINIT { *.o (SDRAM_DMA_BUF) }代码里这样指定段__attribute__((section(SDRAM_DMA_BUF), aligned(32))) float fft_input[4096];这里有两个关键词必须理解清楚。一个是UNINITSDRAM上电后内容不确定而启动代码在RAM初始化阶段如果去清零这块区域一旦FMC还没初始化CPU访问SDRAM地址会直接HardFault。标注了UNINIT后启动代码不会碰这块地址等到main里把SDRAM控制器初始化好再由应用代码自己填数据。另一个是aligned(32)。FFT运算、DMA传输都对内存对齐有要求Cortex-M4的DMA外设要求缓冲区地址按数据宽度对齐FFT库更是要求缓冲区严格对齐到4字节以上。在SDRAM区里定义数组时显式加对齐属性可以省掉一堆运行期对齐操作的麻烦。这种用法在做实时频谱分析项目时非常实用。双缓冲采集中一个缓冲区在DMA搬运另一个在CPU做FFT两个大块头都扔到SDRAM内部RAM留给堆栈和实时性要求高的变量。我在F429上实测把4096点FFT输入输出都放到SDRAM后系统整体跑得很稳CPU访问SDRAM虽然比内部SRAM慢一些但对这种批次型的信号处理场景影响不大。3.4 Bootloader与App的地址偏移另一个高频需求是Bootloader加App结构。假设Bootloader占据Flash开头32KBApp从0x08008000开始。在App工程的SCT里这样改LR_IROM1 0x08008000 0x000F8000 { ER_IROM1 0x08008000 0x000F8000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } }同时在main函数的前几行设置中断向量表偏移SCB-VTOR 0x08008000;这行的意义是告诉CPU中断向量表已经从Flash起始地址挪到了0x08008000处。STM32F4的向量表偏移量有硬件对齐要求具体看参考手册通常要求按中断向量个数向上取整对齐比如0x400的倍数。需要强调的是VTOR设置必须在任何依赖中断的功能开启之前完成包括SysTick、串口中断等否则中断一进来就找错向量表程序直接飞掉。还经常有人出现这种情况App能跑起来但一切换到某个中断就HardFault。排查了一圈最后发现Bootloader在跳转前把外设中断关了而App没有重新初始化表现倒是很像向量表问题实际上两边中断向量表配置都要检查到位。SCT保证了地址布局正确但外设状态切换属于运行期协作问题光靠SCT解决不了。3.5 用后备SRAM保存关键掉电数据STM32F4系列还有一小块4KB的后备SRAM地址在0x40024000主电源断电后由VBAT引脚供电里面的数据不会丢。很多设备用它保存校准参数、运行状态计数、非法掉电标志比外部EEPROM速度更快也不用担心擦写寿命问题。在SCT里把它单独划出来RW_BKPSRAM 0x40024000 0x00001000 UNINIT { *.o (BKPSRAM) }代码里这样定义变量__attribute__((section(BKPSRAM))) uint32_t boot_count;然后在使用前使能备份域访问权限__HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess();使用UNINIT非常重要因为后备SRAM启动时不应该被清零否则掉电保存就失去意义了。即使标明UNINIT编译时链接器还是会为这个段保留地址只是不生成初始化动作这样掉电重启后变量内容仍然存在。要注意的是这个区域地址在0x4002_4000附近属于外设地址空间并不是常规RAM区间如果在调试时访问它会被Debugger特殊处理需要使能备份访问后才能正常读写否则读出来全是0xFF。4. SCT与启动代码、链接器符号的配合4.1 __main和__scatterload到底做了什么很多工程师虽然熟悉main函数但很少去想main之前发生了什么。在Keil/ARMCC环境下Reset_Handler在执行完SystemInit后跳转到的是__main而不是直接跳进我们写的那个main。__main内部会调用__scatterload这一步就是真正执行分散加载的地方。链接器根据SCT文件内部每个执行区的描述为每一个带加载地址和执行地址的区域生成拷贝任务。__scatterload把这些任务依次执行把初始化数据从Flash拷到RAM、把ZI段清零、然后跳转到__rt_entry完成堆栈和库函数初始化最后才进入C语言的main。SCT里哪些区域需要拷贝、哪些不需要就是这个阶段的执行依据。如果你把SCT中某个执行区域配置得过于复杂或者漏了某个区域可能导致启动阶段就直接HardFault。比如上电时FMC外设还没初始化而SCT里定义了从Flash拷贝数据到SDRAM执行区那么__scatterload在拷贝时访问SDRAM地址就会触发总线错误。前面说的用UNINIT规避这个问题本质上就是在告诉__scatterload这块不要你管。4.2 Image$$和Load$$符号怎么用链接器在生成SCT布局时会为每个执行区生成一些特殊符号格式一般是Image$$区名$$Base、Image$$区名$$Length、Load$$区名$$Base。这些符号暴露给C代码后就可以在运行期拿到区域地址。比如我在代码里可以通过下面方式获取RW_IRAM1的起始地址和长度extern int Image$$RW_IRAM1$$Base; extern int Image$$RW_IRAM1$$Length; uint32_t ram_base (uint32_t)Image$$RW_IRAM1$$Base; uint32_t ram_size (uint32_t)Image$$RW_IRAM1$$Length;这在调试和分析内存使用情况时非常有用。我曾经在排查堆栈溢出问题时就是在main里把这个起始地址和长度打印出来再结合栈指针SP的实际值判断栈有没有越过RW段的边界。SCT和启动代码、链接器符号三者配合好了你就等于是从链接层看到了整个内存地图很多“神秘崩溃”都能从这个层面找到线索。5. 高频踩坑与排查方法5.1 编译报错对照速查表实际调试中SCT相关的编译错误最常出现下面几种报错信息常见原因解决思路Region LR_IROM1 size overflowedFlash容量不够或加载区地址/大小设置不匹配检查代码体积或把部分只读数据挪到外部存储Execution region RW_IRAM1 size overflowedRAM区域的RWZI数据超过设置空间减少全局缓冲或把大数组放入SDRAMUndefined symbol Image$$...$$Base使用的执行区名称与SCT中定义不一致核对符号名大小写及区名拼写L6218E: Undefined symbol __mainInRoot$$Sections没有被正确包含检查是否使用了非标准启动文件Error: L6406E: No space in execution regions输入段没有匹配到任何执行区域检查选择器写法确认段名正确Error: L6407E: Sections of aggregate size ... could not fit某个指定段放不进对应区域扩大对应区域或把指定段移到别的执行区看到这几类错误别慌第一步先打开编译生成的.map文件在里面搜索关键段名比如RESET、RAM_CODE、SDRAM_DMA_BUF看链接器到底把段放到了哪里、占用了多大空间。Map文件是排查SCT布局问题最直接的工具比对着报错瞎猜效率高得多。5.2 运行时HardFault的排查思路SCT设置错误不总是出现在编译期更多时候是编译通过一运行就崩。我遇到过的典型场景是把变量放到SDRAM之后启动时忘记加UNINIT结果__scatterload去访问还没初始化的SDRAM控制器上电瞬间直接HardFault。这种问题光靠看代码很难发现因为代码逻辑完全正确。排查思路是按阶段定位在Reset_Handler入口打断点全速跑到这个断点确认上电复位是否正常。单步跟踪SystemInit看它是否正常完成时钟配置和外设初始化。进入__main后继续单步看是执行到哪一步崩的。如果是跳到某个外部SDRAM相关的拷贝动作时崩基本可以确定是SDRAM未初始化或UNINIT缺失。用Keil的寄存器窗口查看PC指针当时停在什么地址如果是0xD0000000附近就能确定是在访问SDRAM。另外还有一类运行期问题堆和栈和某个大数据区挤在一起。Cortex-M4的堆栈是由启动文件和库函数设置的默认情况栈顶就是RAM的起始地址加RAM大小。如果你在SCT里把RAM区大小改小了而启动文件里的堆栈尺寸没同步更新栈就可能跑到SCT定义区域之外被别的段覆盖。这种问题通常表现为程序时好时坏、调用层级一深就死机。排查方法有几种在main里打印SP寄存器值和数组地址对比栈底和SCT区域关系或者直接把启动文件里Stack_Size调大更彻底的是在SCT中单独划分一个STACK执行区并配合__user_initial_stackheap来实现但一般中小型工程用不到那么复杂。5.3 几个实用小习惯经历过几次SCT文件相关的折腾后我总结出几个能救命的小习惯。第一个习惯是每次改SCT之前先在Map文件里给当前工程留一份“基线地图”。编译后打开.map文件记录当前各区域大小和关键符号地址。改完SCT再对比差异如果出现莫名其妙的段迁移看map马上就能定位。第二个习惯是尽量少用绝对地址段多用带名字的section配合SCT区域统一管理。直接写__attribute__((section(.ARM.__at_0x20001000)))虽然省事但很容易在后续增删变量时发生地址冲突。用SCT配合段名整个工程的内存分配一目了然后续加功能也能有序扩展。第三个习惯是始终把启动文件和SCT文件放在版本管理里。很多人工程目录里只有源码和工程文件但SCT文件丢失后Keil会自动按Target配置重新生成一份默认的你的特殊布局规划就全没了。把这个文件纳入版本管理换电脑、换工程时都不怕。我自己在F407和F429上折腾SCT其实最开始也是因为一次IAP升级觉得Flash擦写老是不稳定查来查去发现就是擦写函数在Flash里执行引发的。把函数弄到RAM后问题立刻消失从那以后SCT就不再是一个“看了就困”的配置文件而是一张能帮我在链接层面解决问题的最底层地图。后来做大数组和外部SDRAM也是靠SCT把内存架构理清楚的。这个文件你越早吃透后面碰到的很多疑难杂症就越少。
返回列表