💡 阅读提示:本文将带你彻底搞懂分散加载文件,学会把关键变量塞进内部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占用0768KB
编译状态链接失败成功

三、进阶:分配网络缓冲到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工程,试着把最大的数组挪到外部内存去吧。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐