1. 为什么需要手动控制函数存放位置

在嵌入式开发中,内存资源往往非常有限。就拿常见的STM32系列MCU来说,内部SRAM通常只有几十KB到几百KB,而Flash空间也不过几百KB到几MB。但现代嵌入式应用越来越复杂,比如工业控制中的电机驱动算法、图像处理中的卷积运算,这些函数体积庞大,直接放内部SRAM根本装不下。

我第一次遇到这个问题是在开发数控机床控制器时。当时有个运动轨迹规划算法,编译后有80多KB,而板载SRAM总共才128KB。如果硬塞进SRAM,其他变量和栈空间就不够了。这时候就需要借助外部SDRAM——它的容量通常是SRAM的几十倍(比如32MB),虽然访问速度慢一些,但至少能装下这些"大块头"函数。

GCC的__attribute__((section))就是解决这个问题的金钥匙。通过它,我们可以精确控制每个函数的存放位置。比如:

// 将函数强制放入SDRAM区域
void heavy_algorithm() __attribute__((section("SDRAM_FUNC1")));

实际项目中,我常用这种方式处理三类函数:

  1. 体积庞大的算法函数(如FFT、PID控制)
  2. 低频调用的初始化函数(如设备启动配置)
  3. 需要动态更新的代码(如通过OTA升级的功能模块)

2. 链接器脚本的魔法配合

光有编译器指令还不够,要让函数真正落地到SDRAM,必须配合链接器脚本(.ld文件)。这就像盖房子——编译器负责生产砖块(生成代码),链接器则是建筑师,决定每块砖放在哪里。

一个典型的链接器脚本包含两个关键部分:

MEMORY {
    /* 定义SDRAM的起始地址和大小 */
    SDRAM (rwx) : ORIGIN = 0xC0000000, LENGTH = 32M  
}

SECTIONS {
    /* 创建自定义段并指定存放位置 */
    .SDRAM_FUNC1 : {
        KEEP(*(SDRAM_FUNC1))  /* 收集所有标记为SDRAM_FUNC1的函数 */
    } > SDRAM
}

这里有几个容易踩的坑:

  • 地址对齐:SDRAM的起始地址必须按芯片手册要求对齐(比如4字节对齐)
  • 属性设置:rwx表示可读可写可执行,缺少任何属性都会导致运行时错误
  • KEEP关键字:防止链接器优化掉未被显式调用的函数

我曾经调试过一个诡异的问题:函数明明放进了SDRAM,但一调用就hardfault。最后发现是链接器脚本漏了x(可执行)属性,处理器无法执行那段代码。

3. SDRAM初始化的生死时速

把函数放进SDRAM有个重要前提——SDRAM必须先初始化!这就像把家具搬进毛坯房前,得先通水电。很多新手会忽略这一点,导致程序一启动就崩溃。

正确的初始化顺序应该是:

void Reset_Handler(void) {
    SystemInit();   // 初始化时钟和SDRAM控制器
    __libc_init_array();  // C++全局对象构造
    main();         // 进入主程序
}

在SystemInit()中必须包含SDRAM初始化代码。以STM32H7系列为例:

void HAL_SDRAM_Init() {
    /* 配置时序参数 */
    hsdram.Instance = FMC_SDRAM_DEVICE;
    hsdram.Init.SDBank = FMC_SDRAM_BANK1;
    hsdram.Init.ColumnBitsNumber = FMC_SDRAM_COLUMN_BITS_NUM_9;
    hsdram.Init.RowBitsNumber = FMC_SDRAM_ROW_BITS_NUM_13;
    /* 更多配置... */
    HAL_SDRAM_Init(&hsdram, &timing);
    
    /* 发送SDRAM初始化序列 */
    __IO uint32_t tmpmrd = 0;
    HAL_SDRAM_SendCommand(&hsdram, &command, 0x1000);
}

我曾经在电机控制项目中发现,如果SDRAM初始化耗时过长(>50ms),会导致电机启动抖动。后来通过预加载初始化参数和优化刷新率,把时间压缩到了20ms以内。

4. 性能优化的平衡艺术

把函数放SDRAM确实能节省宝贵的内存空间,但性能代价有多大?我用Cortex-M7做了组实测:

存储位置平均执行周期相对速度
Flash1250 cycles基准
SRAM1180 cycles快6%
SDRAM1875 cycles慢50%

测试代码:

// 在SRAM中执行
__attribute__((section(".fast_code"))) void test_func() {
    for(int i=0; i<1000; i++) {
        asm("nop");
    }
}

// 在SDRAM中执行
__attribute__((section("SDRAM_FUNC1"))) void test_func_sdram() {
    for(int i=0; i<1000; i++) {
        asm("nop");
    }
}

通过DWT周期计数器测量发现:

  • 对于实时性要求高的函数(如PWM中断服务),放SRAM更合适
  • 对于计算密集但非实时的函数(如日志处理),放SDRAM更划算

有个取巧的办法:对性能关键的部分函数使用__attribute__((hot))提示编译器优化,再配合-falign-functions=4对齐指令,可以部分抵消SDRAM的延迟。

5. 跨平台开发的兼容性技巧

GCC的__attribute__语法在其他编译器(如IAR、Keil)上不兼容。为了代码可移植性,我通常这样封装:

#if defined(__GNUC__)
    #define SDRAM_FUNC __attribute__((section("SDRAM_FUNC1")))
#elif defined(__ICCARM__)
    #define SDRAM_FUNC _Pragma("location=\"SDRAM_FUNC1\"")
#else
    #define SDRAM_FUNC
#endif

// 使用宏定义修饰函数
SDRAM_FUNC void motor_control() {
    // ...
}

在Keil中则需要修改scatter文件:

LR_IROM1 0x08000000 0x00200000 {
    ER_IROM1 0x08000000 0x00200000 {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }
    RW_IRAM1 0x20000000 0x00060000 {
        .ANY (+RW +ZI)
    }
    RW_SDRAM 0xC0000000 0x02000000 {
        motor_control.o (+RO)  /* 指定目标文件中的代码段 */
    }
}

6. 调试与验证实战

验证函数是否真的放到了指定位置,我常用这几个方法:

方法一:objdump查看符号表

arm-none-eabi-objdump -t firmware.elf | grep SDRAM_FUNC1

输出示例:

c0000120 g     F SDRAM_FUNC1    00000064 motor_control

方法二:map文件分析 在链接器选项中加入-Wl,-Map=firmware.map,然后搜索段名:

.SDRAM_FUNC1   0xc0000000    0x4000
 *(SDRAM_FUNC1)
 .SDRAM_FUNC1  0xc0000000    0x64 motor_control.o

方法三:运行时地址检查

printf("函数地址: %p\n", (void*)motor_control);
// 输出应为0xC000xxxx范围内的地址

遇到过一个棘手bug:函数地址正确,但执行出错。最后发现是链接器脚本中SDRAM的长度设小了,导致函数被截断。

7. 高级技巧:动态加载与更新

SDRAM相比Flash有个独特优势——可以运行时修改代码。这在需要现场升级算法的场景非常有用。具体实现步骤:

  1. 将函数编译为独立的目标文件
  2. 通过串口或网络接收新代码
  3. 擦写SDRAM中的函数区域
  4. 跳转到新函数执行

示例代码框架:

// 定义函数指针类型
typedef void (*upgradable_func)(void);

// 原始函数(预留空间)
__attribute__((section("UPGRADE_AREA"))) 
uint8_t func_buffer[1024];

// 动态加载新函数
void load_new_code(uint8_t* new_bin) {
    memcpy(func_buffer, new_bin, 1024);
    __DSB();  // 数据同步屏障
    __ISB();  // 指令同步屏障
}

// 调用新函数
upgradable_func func = (upgradable_func)func_buffer;
func();

注意要处理缓存一致性问题,ARM芯片需要调用SCB_CleanDCache_by_Addr()清理缓存。

8. 真实案例:工业机器人控制系统

在某六轴机器人项目中,我们遇到运动学解算函数占用过多SRAM的问题。最终解决方案:

  1. 将逆运动学求解器放入SDRAM
__attribute__((section("SDRAM_FUNC1"))) 
void inverse_kinematics(float* joints, const float* pose);
  1. 优化链接器脚本,为SDRAM划分不同区域:
MEMORY {
    SDRAM_CODE (rx) : ORIGIN = 0xC0000000, LENGTH = 1M
    SDRAM_DATA (rw) : ORIGIN = 0xC0100000, LENGTH = 7M
}
  1. 在启动阶段预加载常用参数到Cache:
void warmup_cache() {
    // 主动访问函数触发缓存加载
    float test_pose[6] = {0};
    float test_joints[6];
    inverse_kinematics(test_joints, test_pose);
}

优化后的结果:

  • SRAM占用从98%降至65%
  • 实际运行性能仅下降8%(得益于Cache优化)
  • 支持了在线算法更新功能

更多推荐