ARTICLE DETAIL

资讯详情

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

STM32F4 sct文件详解:从默认配置到CCM RAM与BootLoader修改

STM32F4 sct文件详解:从默认配置到CCM RAM与BootLoader修改 第一次真正认真看sct文件不是因为我想学而是因为程序跑飞了。当时做一个基于STM32F4的音频采集与FFT频谱分析小项目ADC的DMA缓冲数组死活采不到数据进调试界面看内存全是0。折腾了半天最后在.map文件里发现那个数组被链接器塞进了CCM RAM0x10000000而F4系列里这块RAM是走内核直连总线的DMA外设根本访问不到。就是从那次起我意识到只要想在STM32F4上精细控制内存布局就必须把sct文件这件事彻底搞明白。这篇文章就专门聊聊我对STM32F4的sct文件的理解和实操经验包括默认文件每一行的含义、几种最常见的修改方案、以及改完sct之后怎么确认代码和数据真的落在预期位置。内容适合已经能点亮LED、跑过几个简单工程但一碰到内存布局问题就头疼的开发者。看完你会知道这个后缀为.sct的文件并不是什么高深魔法它只是一张告诉链接器“什么东西放哪里”的施工图纸。1. 什么情况下你需要开始认真对待sct文件1.1 默认情况下你其实一直在用sct文件在Keil MDK里新建一个STM32F4工程编译链接时sct文件是躲不掉的。只要工程选项里的Linker页面保持默认也就是勾选了“Use Memory Layout from Target Dialog”IDE就会根据你在Target页面填的IROM1、IRAM1、IRAM2参数在每次编译时自动生成一份sct文件。这份临时文件不会出现在你的源码目录里所以你平时根本看不到它但链接器确实拿着它在干活。很多人第一次“见到”sct文件是因为手动把工程选项里的“Use Memory Layout from Target Dialog”取消勾选然后在Linker页面指定了一个自定义.sct路径。这一刻开始Target页面的内存配置就不再被链接器采纳了sct文件成了唯一的内存布局依据。这也是一个非常容易出错的分水岭你以为改了Target里的RAM大小就有用实际上链接器已经不听那个配置了。1.2 哪些场景会逼着你去改sct文件我总结了一下平时做F4开发遇到需要碰sct的情况基本逃不出下面这几类第一类BootLoader和App分区。你不想让App从0x08000000开始烧而是从0x08004000或者更后面的偏移地址开始那么App工程的sct文件里IROM1的起始地址和大小必须跟着改否则链接器还是按从头开始的布局生成代码。第二类把某个函数放到RAM里执行。比如Flash擦写期间的IAP关键代码、对时序敏感的中断处理函数或者你实在无法容忍Flash读取的等待周期都需要把代码放到RAM里跑。这种场景下代码的加载地址在Flash执行地址在RAM必须通过sct文件单独划出一块RAM执行区并让启动代码完成搬运。第三类使用外部SDRAM或者外部NOR Flash。大屏显存、音频数据缓冲这类动辄几百KB甚至几MB的数据片上RAM肯定装不下。你要么把数据放在外部SDRAM要么把只读资源放在外部Flash这些都需要在sct里定义额外的加载区或执行区。第四类想利用F4片上那块特殊的CCM RAM。STM32F407、F411等系列有一块64KB的CCM RAM地址在0x10000000它挂在内核私有总线上CPU访问速度比普通SRAM快但DMA和大部分外设够不到。很多新手在默认情况下根本不知道这块RAM的存在直到你需要在CPU高频访问和大缓冲之间做权衡时才会想把它用起来。第五类链接报错。最常见的就是L6220E某个执行区域的大小超过了限制。翻译成人话就是RAM或者Flash塞不下了。这时打开.map文件一看某个大数组占了半块RAM那你就得考虑给它挪个位置。1.3 sct文件理解不到位会带来什么后果如果只是照抄网上的一份sct模板运气好可能没什么问题运气不好就是程序上电就进HardFault或者某个全局变量初始值反复不对又或者代码跑起来之后莫名其妙被篡改。这些问题有个共同特点不报错但行为诡异。我见过一个工程把一个512字节的DMA缓冲区用__attribute__((section(dma_buf)))声明了一下却在sct里只加了ANY(.ANY(RWZI))结果段名没匹配上链接器也只在map里留了一行警告程序单独跑DMA看不出毛病一开中断就丢数据折腾了好几天。所以真正理解sct文件不是为了显得专业而是为了在内存布局这个环节上拥有确定性和掌控感。2. sct文件在编译链接中到底扮演什么角色2.1 从C源码到hex文件链接器做了最后一步STM32F4的编译流程大致是预编译、编译、汇编、链接。前面的步骤把C代码翻译成一个个目标文件.o每个目标文件里有代码段、只读数据段、已初始化数据段、未初始化数据段。到链接这一步编译器工具链需要决定这些零散的段最终落在芯片存储空间的哪个地址上。对于GCC工具链这个决策写在.ld链接脚本里对于Keil使用的ARM编译器armcc或者armclang这个决策就写在.sct文件里。sct文件是ARM的分散加载描述文件它的全称是Scatter-Loading Description File。从“Scatter”这个词就能看出来它的核心作用是把程序各种段“分散”到不同存储区域而不是一股脑全塞在一起。2.2 加载区Load Region和执行区Execution Region是理解sct的钥匙sct文件最核心的两个概念一个是加载区Load Region一个是执行区Execution Region。加载区描述的是程序烧录后在非易失存储器里的停放位置。对于STM32F4来说这个位置通常就是内部Flash也就是0x08000000开头的地址区间。执行区描述的是程序运行时代码和数据在内存里的实际地址。对于大部分只读代码和常量加载地址和执行地址是同一个地方。CPU直接从Flash取指令不需要把代码拷贝到RAM里。但对于RW数据情况就不同了已初始化的全局变量初值在Flash里但变量运行时必须待在RAM里所以启动代码要负责在main执行之前把Flash里的初值复制到RAM中的对应位置。ZI数据也就是未初始化变量则只需要在RAM里清零就可以了。这个过程在ARM的C库启动流程里由__main函数中的__scatterload来完成。它会遍历sct文件里所有加载区和执行区凡遇到加载地址不等于执行地址的区域就自动生成一段复制代码把数据搬过去。所以当你把一段代码放到RAM执行时其实不需要自己写拷贝逻辑只要能正确地在sct里定义出加载区和执行区的地址链接器和启动代码会帮你完成剩下的工作。2.3 三个关键词RO、RW、ZIARM编译器把输出内容按属性分成三类理解这三类是看懂sct语法的前提。ROReadOnly包括代码和只读常量。在F4上它们默认放在Flash里加载地址等于执行地址程序直接读取。RWReadWrite是已经初始化了的全局变量和静态变量比如uint8_t flag 1;。初值放在Flash运行时要复制到RAM所以RW数据需要同时占据Flash的一部分空间和RAM的一部分空间。ZIZero Initialized是未初始化或者显式初始化为0的全局变量比如大数组uint8_t buffer[1024];它不占用Flash只在RAM里占空间启动时被清零。这里有一个初学者很容易忽略的点工程编译后Build Output里那一行“Program Size: Codexxxx RO-dataxxxx RW-dataxxxx ZI-dataxxxx”其中RW-data和ZI-data共同决定了RAM占用RO-data和Code共同决定了Flash占用。而RW-data既有初值占Flash的一部分又会在运行时占据RAM。所以当你看到RW-data特别大时说明Flash和RAM都被消耗了不是只占一份。3. 逐行拆解STM32F4默认生成的sct文件3.1 一个典型的默认sct长什么样下面这个示例对应一颗512KB Flash、128KB SRAM加64KB CCM RAM的F4芯片比如STM32F407VE系列的常见配置KEIL生成的默认sct文件风格大致如下; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00080000 { ; load region size_region ER_IROM1 0x08000000 0x00080000 { ; load address execution address *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { ; RW data .ANY (RW ZI) } RW_IRAM2 0x10000000 0x00010000 { ; CCM RAM .ANY (RW ZI) } }如果你的Target页面没有配置第二块IRAM那么RW_IRAM2那段就不会出现。不同容量、不同型号的F4Flash大小和RAM大小不一样IROM和IRAM的数值会跟着变但语法结构是完全一样的。3.2 LR_IROM1与ER_IROM1程序主体所在的加载区先看最外面的大括号它定义了一个叫LR_IROM1的加载区。LR是Load Region的缩写IROM1是KEIL在Target页面里给片内Flash起的默认名字。后面跟着两个数值第一个0x08000000是加载区的起始地址也就是STM32F4内部Flash的基地址第二个0x00080000是这片区域的可用大小对应512KB。这里必须基于你芯片的实际Flash容量来写写大了链接器不会拦你但烧录下载时会出问题。下一层的大括号定义了一个叫ER_IROM1的执行区。ER是Execution Region的缩写。它从0x08000000开始大小同样是0x00080000。因为这是只读代码区域加载地址和执行地址相同所以程序直接从Flash执行不需要额外搬运。ER_IROM1里第一行是*.o (RESET, First)。这行的意思是在所有目标文件里寻找名为RESET的段也就是启动文件startup_stm32f4xx.s里定义的向量表段并且强制把它放在这个执行区的最前面。First是一个很关键的属性因为Cortex-M内核上电后会从0x08000000读取初始堆栈指针从0x08000004读取复位向量向量表必须位于这个地址否则芯片一上电就跑飞。第二行*(InRoot$$Sections)匹配的是ARM C库中必须放在根区的特殊段比如__main、__scatterload等启动相关代码。这类段不能被随意搬到RAM里否则整个C运行环境的初始化顺序会被打乱。第三行.ANY (RO)是一个通配匹配表示把前面没有被精确匹配到的所有只读内容包括普通代码和常量都放到这个区域。第四行.ANY (XO)负责匹配Execute-Only段这类代码只能执行不能读取一般出现在某些安全库或者特殊优化场景平时用得少但默认模板都会带上。3.3 RW_IRAM1与RW_IRAM2运行时变量存放区ER_IROM1后面的RW_IRAM1地址从0x20000000开始大小0x00020000对应F4的128KB普通SRAM。里面只有一行.ANY (RW ZI)意思很直白把工程里所有需要读写的已初始化数据RW和需要清零的未初始化数据ZI都分配到这块RAM里。RW_IRAM2则是第二块RAM区域地址0x10000000大小0x00010000对应CCM RAM。这个区域的语法和RW_IRAM1一模一样但地址在F4的内存映射里属于特殊位置。Cortex-M4内核可以通过系统总线直接访问它速度比访问普通SRAM还快但DMA、以太网MAC、USB OTG等外设无法访问。默认工程如果没用到CCM链接器不会主动把数据放进去因为编译器没法自动判断哪些变量会被DMA访问贸然放入CCM可能会导致外设和CPU数据不一致。所以默认sct里即使写了这个区域一般也只有当你改变量放进某个明确section后才会真正被使用。3.4 “Target对话框配置”和“sct文件”的关系Keil里有个经典迷惑操作在Target页面改大RAM发现编译后还是报RAM溢出。这是因为只要Linker页面勾选了“Use Memory Layout from Target Dialog”每次编译其实都是根据Target页面动态生成一份sct文件你在Target页面改了IRAM大小新生成的sct就会跟着变。但如果你手动指定了一份自定义sct那Target页面的IRAM、IROM配置对链接就不再生效了。调试下载时Target页面里的RAM地址还可能影响初始化脚本但最终决定程序布局的一定是sct。我个人的习惯是开始阶段先用默认配置让KEIL自动生成sct跑通基础工程之后再决定要不要手动接管。一旦手动接管就要把sct文件纳入版本管理并且每次修改都同步检查.map文件否则很容易出现Target设置和实际布局“两张皮”的问题。4. 实操三种最常见的sct修改方案4.1 方案一把指定变量放到CCM RAM使用CCM RAM的常规思路是先定义一个自定义段然后把变量放进这个段最后在sct文件里为这个段分配空间。假设工程里有一个经常被CPU高频访问的FFT中间缓冲区想放到CCM里提速而另一个32字节的控制结构想让DMA访问则必须留在普通SRAM。代码可以这样写__attribute__((section(.ccm_ram))) float fft_buffer[1024]; __attribute__((section(.sram_data))) uint32_t dma_ctrl[8];然后在sct文件里把RW_IRAM2那个区域改成RW_IRAM2 0x10000000 0x00010000 { .ccm_ram (RW ZI) .ANY (RW ZI) }.ccm_ram (RW ZI)的写法表示在RW_IRAM2执行区内优先匹配所有具有.ccm_ram段名的数据。链接器会先为精确匹配的段分配空间剩下的空间再用.ANY去填其他RW和ZI数据。如果你希望这个区域只放CCM专用数据不放其他任何数据那可以把.ANY那一行删掉改成只保留.ccm_ram (RW ZI)。这里有一个很容易踩的坑如果你定义了__attribute__((section(.ccm_ram)))但sct文件里没有写任何匹配这个段名的规则链接器并不会报致命错误很多时候只会给一个L6314W警告提示“no section matches pattern”。然后这个变量就被扔到了默认的RW区CCM完全没有被用到。所以定义段之后一定要去Build Output窗口看有没有对应的警告再去.map文件里确认变量的实际地址。4.2 方案二把某个函数放到RAM执行有些场景需要在RAM里执行代码典型的有两类一是在Flash擦写期间CPU需要从Flash取指如果正在擦写的区域就是当前正在执行的代码所在区域会直接导致总线错误二是某些中断服务函数要求极低延迟不希望每次取指都经过Flash接口的等待周期。假设我想把一个叫Flash_Erase_Handler的函数放到RAM执行代码可以写成__attribute__((section(.ramfunc))) void Flash_Erase_Handler(void) { // 擦写Flash的关键操作 }sct文件里需要增加一个RAM执行区的定义。注意这个执行区的加载地址在Flash执行地址在RAM所以它不能再被放到ER_IROM1里而应该作为一个独立的执行区同时仍然位于LR_IROM1这个大加载区之内。示例LR_IROM1 0x08000000 0x00080000 { ER_IROM1 0x08000000 0x00080000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RAM_FUNC 0x20000000 0x00010000 { .ramfunc (RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (RW ZI) } RW_IRAM2 0x10000000 0x00010000 { .ANY (RW ZI) } }关于这个配置有两点需要特别强调。第一RAM_FUNC执行区的起始地址是0x20000000但RW_IRAM1执行区的起始地址也写着0x20000000两者在地址空间上是重叠的。这是ARM分散加载允许的多个执行区可以“紧挨着”或“部分重叠”只要实际占用的段没有互相冲突。链接器会先放置精确匹配的段。如果RAM_FUNC里的函数实际只占了几百字节链接器在放置.ANY (RW ZI)时不会越过RAM_FUNC占用的地址范围。但是如果链接器发现RW数据需要占用0x20000000到0x2000FFFF而你的ramfunc也放在0x20000000就可能报L6238E错误提示区域放置失败。出现这种问题时最简单的办法是把RAM_FUNC起始地址改到RAM的尾部比如RAM_FUNC 0x20010000 0x00010000那样和RW_IRAM1就不会重叠了。不过需要注意的是片内SRAM地址空间是连续但物理上分成多个块的F4的0x20000000到0x2001FFFF总共128KB如果你把RAM_FUNC放在0x20010000它占用的就是末尾64KB前提是你芯片的SRAM总容量确实有那么大。第二当加载地址等于Flash地址而执行地址等于RAM地址时链接器会自动把.ramfunc段识别为“需要从加载区复制到执行区”。在main执行之前__main里的scatterload逻辑会完成复制不需要你在main函数里手动去做memcpy。但如果你使用的是armclang并且在启动流程里禁用了标准库初始化那这份搬运工作就得自己处理了。4.3 方案三BootLoader与App分区时的sct调整很多项目需要BootLoader加App的结构。BootLoader固定从0x08000000启动App从0x08004000或0x08008000开始。App工程的sct文件要改的地方很明确把IROM1的起始地址改成App的起始地址同时把大小改成不超过剩余Flash容量的值。LR_IROM1 0x08004000 0x0007C000 { ER_IROM1 0x08004000 0x0007C000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } RW_IRAM2 0x10000000 0x00010000 { .ANY (RW ZI) } }改完sct之后还有一件事不能漏修改向量表偏移。STM32F4的启动文件在SystemInit里会根据VECT_TAB_OFFSET设置SCB-VTOR。在system_stm32f4xx.c文件里默认VECT_TAB_OFFSET是0代表向量表就在Flash起始地址0x08000000。App工程必须把这个偏移改成0x4000也就是#define VECT_TAB_OFFSET 0x4000。如果只改sct不改VTOR中断来了CPU会从0x08000000读向量表读到的却是BootLoader的向量表中断自然就乱了。这种分区方案调试时还有一个额外经验下载App时Keil的Flash Download算法里Programming Algorithm大小和地址范围往往会覆盖整个Flash而实际你只想烧0x08004000之后的区域。为了安全可以单独建立一个下载配置或者在下载时不擦除整个芯片而是只擦除App所在的扇区。否则每次调试都会把BootLoader也冲掉非常影响效率。5. sct文件里的属性和隐藏规则看懂了才不容易翻车5.1 FIRST、LAST、ALIGN和FIXED除了默认模板里出现过的Firstsct文件里还有几个常用属性值得了解。Last和First相反表示把这个段放到执行区的最后位置。这种需求常见于CRC校验的场合你想在固件末尾放一个固定格式的校验值那么就可以让这个校验段排在所有代码和数据的最后面方便后期工具统一计算CRC。ALIGN用于对齐常见写法是ALIGN 4。Cortex-M处理器对32位访问有对齐要求而某些外设寄存器配置区域或者DMA描述符表也要满足对齐条件。如果在sct里为数据段保持ALIGN等于告诉链接器这个区域的起始地址必须按指定字节数对齐。FIXED表示执行区地址固定链接器不得做任何重定位。通常在把数据放到某个绝对地址、但不想让链接器自动优化挪动时使用。还有一个EMPTY它不代表有数据而是预留一片空间。这类用法在复杂系统里比较多比如你想在外部SDRAM里预留一块地址给BootLoader和App之间传递参数就可以在sct里定义一个EMPTY执行区表示这段空间虽然现在没有数据但也不允许链接器把其他数据塞进来。5.2 精确匹配和.ANY的优先级问题sct文件里的段选择器精确写的越具体优先级越高。比如startup_stm32f407xx.o (RESET, First)是精确匹配某个目标文件里的RESET段优先级最高*.o (RESET, First)是匹配所有目标文件里的RESET段优先级其次.ANY (RO)则是最低优先级的兜底匹配。很多人以为.ANY和*差不多其实有本质区别。*在一个执行区里匹配了某个段后就不会再被其他执行区匹配了。而.ANY的特点是当多个执行区里都写了.ANY时链接器会综合平衡放置尽量把各个区域的利用率拉均衡。所以默认模板里RW_IRAM1和RW_IRAM2各写一个.ANY (RW ZI)链接器会自动决定哪些RW数据放到普通SRAM哪些放到CCM。这种自动平衡大多数时候是好事但如果你希望某个特定变量必须放普通SRAM就必须用精确的段匹配而不能依赖.ANY。5.3 栈和堆其实也跟sct有关系打开启动文件startup_stm32f4xx.s会看到类似Stack_Size EQU 0x00000400和Heap_Size EQU 0x00000200的定义接着在代码里会定义AREA STACK, NOINIT, READWRITE这样的段。这些栈和堆段在链接时也会被分配到一个执行区里最终出现在.map文件中的ARM_LIB_STACK和ARM_LIB_HEAP标识下。当你手动调整sct里RAM区域的位置或大小时别忘了给栈和堆留空间。我见过一个案例某开发者把RW_IRAM1的大小从0x00020000改成了0x00010000但启动文件里栈大小设置的是0x00001000整个工程的RAM数据早就把空间塞满了栈被挤到越界区域都没人发现程序运行到某个深层函数调用时直接HardFault。这种问题不好查因为静态看代码没有任何逻辑错误。建议在改完sct后打开.map文件搜索ARM_LIB_STACK确认栈的起始地址和结束地址确实落在RAM区间内。5.4 加载区大小不等于芯片Flash大小还有一个坑是加载区的大小可以比Flash实际容量小但不能大。比如你的F4芯片Flash只有256KB地址范围是0x08000000到0x0803FFFF那LR_IROM1的大小最多写0x00040000。如果你误写成了0x00080000链接器可能不会立刻报错但生成的镜像一旦超过实际Flash容量下载时就会提示校验失败或者烧录到不存在的地址。同理RW区域大小也不能超过芯片物理RAM总容量。6. 常见链接错误和内存问题的排查经验6.1 从Build Output和.map文件定位问题每次编译完Build Output窗口都会显示“Program Size”和可能的警告信息。链接错误基本会直接弹出来比如L6220E是区域空间不足L6314W是某个段没有匹配到。这些信息虽然简短但对定位问题很有帮助。打开.map文件的方法很简单在Build Output窗口双击某个.axf文件或者直接在Output目录里用文本编辑器打开。.map文件里有“Memory Map of the image”一节里面的“Execution Region”分类会列出每个执行区的名称、起始地址、结束地址和占用大小。想确认某个变量到底放到了哪里就在.map文件里搜索变量名找到它所在的地址再看这个地址落在哪个执行区范围内。我自己的查错顺序通常是这样的先看Build Output里有没有L62开头的警告或错误再看Program Size行最后打开.map文件检查每个Execution Region的占用率。占用率如果超过90%就要警惕了因为栈还要占空间而且后续加功能很容易崩。6.2 几个高频报错的应对办法下面这种表是我自己在项目里顺手整理的每次遇到链接问题都会先对照一遍。错误信息常见原因优先排查方向Error: L6220E: Region RW_IRAM1 size exceeds limitRAM溢出检查大数组定义调整ZI数据布局考虑外部RAM或CCMError: L6314W: No section matches patternsct里定义了一个匹配不到的段名检查变量/函数是否真的编译进了工程段名拼写是否一致Error: L6238E: Cannot place section代码或数据指定的放置区域不满足条件检查执行区地址重叠、超出物理RAM范围、FIXED属性冲突Error: L6218E: Undefined symbol缺少某个函数或变量的定义先查代码链接问题不一定是sct的锅有一种很隐蔽的“L6314W”现象值得单独拿出来说你明明在源码里写了__attribute__((section(.my_section)))编译也没报错但sct里就是匹配不到。原因多半是编译器优化把这个变量优化掉了或者函数被内联了。解决方法是给变量加上volatile或者给函数加上__attribute__((used))强制保留这个段然后再去编译。6.3 上电后HardFault和变量初值错乱的血泪教训sct文件配置不当导致的HardFault最常见的三个原因我都遇到过。第一个原因是向量表没放对位置。sct里First写错了或者RESET段被分配到RAM区里CPU上电后从0x08000000读到的不是向量表自然跑飞。这种问题的排查方法是看反汇编窗口确认0x08000000处存放的确实是SP初始值和Reset_Handler地址。第二个原因是DMA缓冲区被链接器放进了CCM RAM。这个问题前面提过F4的DMA访问不了0x10000000这块区域。当你把一个大数组用__attribute__((section(.ccm_ram)))放进去之后如果又把这个数组的地址传给了DMADMA配置看起来一切正常但就是不会传数据。排查方法是查看.map文件确认缓冲区地址范围或者在调试器Memory窗口里看0x10000000区域的数据是否在变化。第三个原因是改变了加载地址却没有处理拷贝。前面说的__main自动搬运前提是你用了标准的ARM启动流程。如果工程里为了省空间关闭了MicroLIB以外的某些初始化或者自己在Reset_Handler里直接跳main那scatterload可能不会执行放在RAM执行区的函数永远没有被复制一调用就是执行RAM里的随机数据。这个情况排查起来比较费劲因为程序逻辑看起来完全正确。我自己在写IAP时遇到过后来统一在启动文件里保留标准启动流程才彻底解决。6.4 查看运行时实际布局的几个技巧调试阶段我想确认某段代码或者某个变量是否真的按预期放在指定位置一般会做三件事。第一在调试界面里用表达式窗口输入函数名或者数组名看它的地址。如果函数在RAM里地址应该落在0x20000000或0x10000000区间如果在Flash里落在0x08000000区间。第二打开.map文件搜索段名。比如想确认.ramfunc段是否真的被链接器识别搜索ramfunc能看到它所在的执行区名称和地址范围。第三在反汇编窗口里跳到某个RAM地址看里面的内容是不是对应的机器码。如果是一堆0x00或者0xFF说明scatterload没有把代码复制过来。还有一个防止未来踩坑的小技巧在sct文件里写注释记录每个区域的用途。虽然sct文件支持分号注释很多人却不写几个月后再看就完全不记得为什么要保留RW_IRAM2、为什么要给某个函数单独划RAM。写上几行注释后面维护时能省很多时间。7. 综合案例一个带FFT频谱分析的小工程该怎么规划内存7.1 典型音频采集与FFT工程的内存需求回到文章开头说到的FFT频谱分析项目。这个项目用的MCU是STM32F407VET6有512KB Flash、128KB普通SRAM和64KB CCM RAM。系统的核心流程是ADC通过DMA连续采集音频数据DMA缓冲做好双缓冲切换CPU对采集到的数据进行FFT得到频谱然后通过LCD显示波形和频谱柱状图。这个过程中内存需求大致是DMA缓冲数组必须放在普通SRAM因为DMA根本访问不了CCMFFT运算需要的输入缓冲区和旋转因子表可以被CPU大量访问放CCM能提速LCD的显存如果比较大片内RAM放不下只能考虑外部SDRAM或者缩减分辨率另外还有各种状态结构体和栈。7.2 针对这类工程的内存布局建议基于上面的需求我的建议是设置三段独立的sct区域LR_IROM1 0x08000000 0x00080000 { ER_IROM1 0x08000000 0x00080000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { .dma_buf (RW ZI) .ANY (RW ZI) } RW_IRAM2 0x10000000 0x00010000 { .fft_buf (RW ZI) .ANY (RW ZI) } }然后在代码里把DMA缓冲区放到.dma_buf段把FFT实时计算缓冲区放到.fft_buf段__attribute__((section(.dma_buf), aligned(32))) uint16_t adc_dma_buf[2][2048]; __attribute__((section(.fft_buf), aligned(16))) float fft_input[1024];这样链接器会优先把DMA缓冲区放在普通SRAMFFT缓冲区放在CCM剩下的空间再被其他RW和ZI数据使用。如果之后发现普通SRAM不够用而CCM还剩很多可以在sct里把RW_IRAM1中的.ANY (RW ZI)删掉强制所有常规变量也优先使用CCM但这必须非常谨慎因为一旦某个会被DMA访问的普通变量也落进CCM就又会出现之前踩过的坑。7.3 外部SDRAM的额外注意事项如果LCD分辨率高片内RAM放不下显存很多方案会给F407外扩一块SDRAM挂在FMC的Bank1上地址通常是0xC0000000。这时候sct里可以增加一个外部RAM的加载区比如LR_SDRAM 0xC0000000 0x00800000 { ER_SDRAM 0xC0000000 0x00800000 { .lcd_fb (RW ZI) .audio_cache (RW ZI) } }但这里有一个特别重要的前提SDRAM控制器必须在启动早期完成初始化否则在main函数之前scatterload试图把RW数据复制到SDRAM时SDRAM根本没有准备好程序就会卡死或者HardFault。通常的做法是先不要让任何自动搬运的RW数据指向SDRAM而是在main函数里手动初始化FMC和SDRAM然后用memcpy显式地把需要的缓冲区地址指向SDRAM。实际上对于外部SDRAM这类上电后需要软件初始化的器件更常见的方案是只把大数组声明成指针在运行到main之后动态分配或用静态指针指向SDRAM地址而不是通过sct直接进行自动加载。这一点新手非常容易踩坑。7.4 内存规划这件事越早做越好从项目的角度看sct文件规划其实应该在动手写功能代码之前就做完。你先想清楚哪些数据要高频访问、哪些数据要过DMA、哪些数据可能很大需要外部存储然后再把这些约束落到sct文件里。如果项目都写完了再去改sct就可能出现函数链接地址变动、变量地址变动带来的各种奇怪问题而且排查成本极高。我自己现在接手一个新工程第一件事就是打开默认sct和.map文件把芯片的Flash、SRAM、CCM、外设地址空间画在一张纸上标清哪些区域给代码、哪些给大数据、哪些给栈和堆。这个过程看着麻烦但能帮我在后续开发中少调很多程序。最后再分享一个个人习惯每改一次sct文件我都会另存一份带版本号的备份然后在Release Notes里记录改动原因。因为sct文件不像C代码那样有明确的函数逻辑一旦时间久了很容易忘记某个区域为什么存在。初期多花五分钟写注释和备份后期排查问题时能省五小时。
返回列表