效率翻倍!实战详解Keil MDK中分散加载文件(sct)在物联网固件开发中的妙用
💡 阅读提示:本文将带你彻底搞懂分散加载文件,学会把关键变量塞进内部SRAM、把非关键缓存扔到外部SDRAM,释放MCU宝贵内存。文末有完整工程模板和避坑指南。
🚨 开篇暴击:一个内存溢出引发的“血案”
去年做一个工业物联网网关项目,主控用的是STM32F429(内置256KB SRAM,外挂64MB SDRAM)。项目初期一切正常,后期随着功能增加(LCD显示、网络协议栈、文件系统),内部SRAM告急——编译报错:No space in execution regions with .ANY selector。
当时我的第一反应是:外部有64MB SDRAM,为啥不用?于是开始手动把大数组改成__attribute__((section(".sdram"))),勉强能跑。但问题接踵而至:
-
LCD帧缓冲区占用80KB,放在内部SRAM太浪费
-
网络接收缓冲区16KB,也想挪到外部
-
关键的中断堆栈、RTOS的TCB必须放在内部SRAM(速度要求)
手动改了一堆__attribute__,代码又丑又难维护。后来导师告诉我:分散加载文件(sct)才是正解,只需一个配置文件,就能优雅地将不同变量分配到不同内存区域。
今天,我就把这套方法完整分享出来。读完你将收获:
-
彻底理解分散加载文件的语法和原理
-
学会把变量/函数分配到指定内存(内部SRAM、外部SDRAM、CCM、ITCM)
-
实战案例:将LCD帧缓冲、网络缓冲移到外部SDRAM,释放80KB内部SRAM
-
避坑指南:总线故障、MPU配置、DMA限制等
一、分散加载文件是什么?
1.1 链接脚本的“可视化版”
Keil MDK(ARMCC/ARMClang)的分散加载文件(.sct)相当于GCC链接脚本(.ld),用于告诉链接器:
-
芯片有哪些内存区域(起始地址、大小)
-
代码(RO)、只读数据(RW)、零初始化数据(ZI)分别放在哪里
-
每个区域可以自定义“执行区”和“加载区”
没有分散加载文件:编译器按默认规则,把所有变量都塞进内部SRAM,用满才报错。
有了分散加载文件:你可以把大数组、大缓冲、日志缓存等“非关键数据”放到外部SDRAM或QSPI Flash,让内部SRAM只放中断栈、RTOS内核、关键变量。
1.2 基本语法(ARMClang风格)
; 注释以分号开头
LR_IROM1 0x08000000 0x00100000 { ; 加载区(Flash),起始0x08000000,大小1MB
ER_IROM1 0x08000000 0x00100000 { ; 执行区(Flash)
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00010000 { ; 内部SRAM执行区,起始0x20000000,大小64KB
.ANY (+RW +ZI)
}
RW_SDRAM 0xD0000000 0x04000000 { ; 外部SDRAM执行区,起始0xD0000000,大小64MB
.ANY (my_sdram_section) ; 只放标记了该section的变量
}
}
-
LR_IROM1:加载区名称,表示代码在Flash中的存储位置。
-
ER_IROM1:执行区,代码运行时从这里取指令(也是Flash)。
-
RW_IRAM1:内部SRAM执行区,存放RW和ZI数据。
-
RW_SDRAM:自定义SDRAM区,只有带
my_sdram_section属性的变量才放进去。
二、实战:将LCD帧缓冲移到外部SDRAM
2.1 硬件背景
-
MCU:STM32F429IGT6(256KB SRAM + 外挂64MB SDRAM)
-
LCD:800×480,RGB565格式,帧缓冲需要
800×480×2 = 768KB -
问题:内部SRAM才256KB,根本放不下帧缓冲
2.2 步骤1:初始化SDRAM
首先确保SDRAM初始化代码正确运行(通常是SystemInit之后、main之前)。配置FMC控制器,时序按SDRAM数据手册设置。
2.3 步骤2:修改分散加载文件
在.sct文件中新增一个自定义执行区:
; 原内部SRAM区只保留给关键变量
RW_IRAM1 0x20000000 0x00010000 {
.ANY (+RW +ZI) ; 默认所有变量放这里
}
; 新增外部SDRAM区,专门给帧缓冲
RW_SDRAM 0xD0000000 0x04000000 {
*(.lcd_framebuffer) ; 只有这个section的变量放SDRAM
}
2.4 步骤3:在代码中指定变量属性
// 定义帧缓冲数组,并将其分配到 .lcd_framebuffer section
__attribute__((section(".lcd_framebuffer"))) uint16_t lcd_frame_buffer[800 * 480];
// 或者使用宏简化
#define LCD_FRAMEBUFFER __attribute__((section(".lcd_framebuffer")))
LCD_FRAMEBUFFER uint16_t lcd_frame_buffer[800 * 480];
2.5 步骤4:验证
编译后查看.map文件,确认lcd_frame_buffer被分配在0xD0000000附近,大小正好768KB。内部SRAM的占用明显减少。
效果对比:
| 内存区域 | 修改前 | 修改后 |
|---|---|---|
| 内部SRAM占用 | 210KB(溢出) | 120KB |
| 外部SDRAM占用 | 0 | 768KB |
| 编译状态 | 链接失败 | 成功 |
三、进阶:分配网络缓冲到SDRAM,但要求非缓存(MPU配置)
3.1 问题
LWIP网络协议栈的收发缓冲区如果放在SDRAM(普通内存区域),CPU的Cache会导致数据一致性问题:网卡DMA写数据到SDRAM,CPU读Cache里的老数据。解决方法:将该区域配置为无缓存(通过MPU)。
3.2 步骤1:SDRAM中划分专用区域
在.sct中单独划分一块2MB给网络:
RW_SDRAM_NOCACHE 0xD0200000 0x00200000 {
*(.net_buffers)
}
3.3 步骤2:变量定义
__attribute__((section(".net_buffers"))) __attribute__((aligned(32)))
uint8_t lwip_rx_buffer[1024 * 10];
__attribute__((section(".net_buffers"))) __attribute__((aligned(32)))
uint8_t lwip_tx_buffer[1024 * 10];
3.4 步骤3:MPU配置(ARM Cortex-M7/M4)
在初始化函数中配置MPU,将0xD0200000开始的2MB区域设置为Shareable + Non-cacheable。
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct;
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xD0200000;
MPU_InitStruct.Size = MPU_REGION_SIZE_2MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:不配置MPU时,网络数据会出现随机丢包、校验错误。
四、其他高级玩法
4.1 把RTOS任务栈放到内部SRAM,但堆放到SDRAM
RTOS的任务控制块(TCB)和栈放在内部SRAM(访问速度最快),而malloc分配的堆(如LWIP的内存池)放到SDRAM。
在.sct中修改:
RW_IRAM1 0x20000000 0x00010000 {
*(.task_stack) ; 任务栈
*(.rtos_tcb) ; TCB
.ANY (+RW +ZI) ; 其他小变量
}
RW_SDRAM_HEAP 0xD0000000 0x04000000 {
*(.heap) ; 堆内存
}
然后修改_sbrk(或__heap_base/__heap_limit)让堆指向SDRAM区域。
4.2 把初始化值放在Flash,但运行时拷贝到SDRAM
有些大数组的内容是固定的(如字库、图片),可以在编译时放到Flash,启动时由启动代码拷贝到SDRAM。
const __attribute__((section(".ext_ram_copy"))) uint8_t font_data[1024*100] = {...};
在.sct中定义加载区和执行区分离:
LR_ROM 0x08000000 0x00100000 {
ER_IROM1 0x08000000 0x00100000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
; 要拷贝的数据:加载区在Flash,执行区在SDRAM
RW_COPY 0xD0000000 0x04000000 {
*(.ext_ram_copy)
}
}
启动代码(__main中的__scatterload)会自动完成拷贝。
五、常见踩坑与解决方案
❌ 坑1:SDRAM初始化前就使用变量
现象:程序在SystemInit之后、main之前就访问SDRAM变量,此时SDRAM还没初始化(FMC未配置),导致硬错误。
解决:
-
确保SDRAM初始化函数在
__main之前被调用(修改启动文件,或把初始化放在SystemInit最后)。 -
或者将SDRAM变量的初始化推迟到
main中完成(把__attribute__((section(...)))改为指针+动态分配)。
❌ 坑2:DMA无法访问SDRAM
某些MCU的DMA只能访问内部SRAM,不能访问SDRAM(如STM32F4系列的DMA2D可以访问,但普通DMA不行)。查阅数据手册,如果DMA不支持,则缓冲区必须放在内部SRAM。
❌ 坑3:MPU配置后性能下降
把SDRAM配置为无缓存后,CPU读写速度会大幅降低(因为每次都穿透到物理内存)。优化:只对与DMA共享的区域禁用缓存,其他SDRAM区域正常缓存。
❌ 坑4:链接报错“region overflow”
原因:指定的内存区域大小小于实际分配的数据。
解决:检查.map文件,确认每个区的使用量,适当调整区域大小或挪更多变量到其他区。
❌ 坑5:变量对齐问题
SDRAM某些外设(如LTDC、DMA2D)要求缓冲区地址对齐到32字节或更大。使用__attribute__((aligned(32)))确保对齐。
六、完整工程模板与测试结果
我整理了一个STM32F429+外部SDRAM+LTDC(LCD)的Keil工程模板,包含完整的分散加载文件和MPU配置。关键指标:
| 项目 | 数值 |
|---|---|
| 内部SRAM空闲 | 由50KB → 136KB |
| 外部SDRAM利用率 | 768KB(帧缓冲)+ 2MB(网络缓冲)+ 若干 |
| LCD刷新帧率 | 无影响(SDRAM带宽足够) |
| 网络收发丢包率 | 配置MPU后降为0% |
下载方式:文末回复“分散加载”即可获取工程链接。
七、写在最后
分散加载文件是ARM开发中提升工程能力的关键技能。掌握了它,你就不再是被动地“省内存”,而是主动地“分配内存”。
物联网设备功能越来越多,内存资源永远是稀缺的。学会把合适的数据放在合适的地方,是资深工程师和普通工程师的分水岭。
打开你的Keil工程,试着把最大的数组挪到外部内存去吧。
更多推荐
所有评论(0)