3. 环境应力筛选基础

固件优化,说白了就是三件事:让代码更小、跑得更快、功耗更低。这三件事往往互相打架——你优化了体积,可能牺牲了速度;你追求低功耗,可能拖慢了响应。我在项目中踩过不少这样的坑,今天就把这些经验掰开揉碎讲给你听。

核心原则:固件优化不是“一刀切”,而是根据产品需求做权衡。工业控制优先看实时性,电池供电优先看功耗,Flash受限的芯片优先看体积。

3.1 代码体积优化

代码体积优化,主要针对Flash或ROM空间紧张的MCU。比如你用的芯片只有64KB Flash,功能却越加越多,这时候就得想办法“挤一挤”。

3.1.1 编译器优化选项

这是最直接的手段。我建议你先把编译器的优化等级调起来:

优化选项 效果 风险
-Os(GCC) 以体积最小为目标 调试信息可能不准确
-Oz(Clang) 比-Os更激进地压缩 可能增加执行时间
--function-sections + --gc-sections 删除未调用的函数 需确认链接脚本支持

嗯,这里要注意:不要在生产固件里用-O0。我曾经见过一个团队,Debug版固件能跑,Release版一上电就死机——就是因为-O2优化把某个未初始化的变量给优化掉了。所以,优化等级要逐级测试。

3.1.2 手动代码瘦身技巧

编译器能做的有限,真正的大头还得靠你自己动手。

  • 去掉冗余的printf和调试代码——用条件编译控制,发布版直接剪掉。
  • 合并相似的函数——比如两个驱动函数只有寄存器地址不同,可以用参数化函数替代。
  • 使用查表法代替复杂计算——比如CRC校验、三角函数,预计算好存成常量数组。
  • 避免大块的全局变量——能用uint8_t就别用uint32_t,能位域就别用结构体。
我的习惯:每次提交代码前,用size命令看一下.text/.data/.bss段的变化。如果某次提交体积暴涨,立刻回查。

3.1.3 链接脚本优化

链接脚本很多人不碰,其实这里能省不少空间。比如:

/* 将只读常量放到Flash末尾,避免对齐浪费 */
.rodata : ALIGN(4)
{
  *(.rodata)
  *(.rodata*)
} > FLASH

/* 将未初始化变量放到RAM末尾 */
.bss (NOLOAD) :
{
  *(.bss)
  *(.bss*)
  *(COMMON)
} > RAM

我曾经在一个项目中,通过调整链接脚本的段对齐方式,硬生生省出了2KB空间——够塞一个Bootloader了。

3.2 执行效率优化

执行效率,说白了就是“响应够不够快”。工业现场、电机控制、通信协议栈,这些场景对实时性要求极高。

3.2.1 算法层面的优化

别一上来就调汇编,先看看算法能不能简化。

  • 用移位代替乘除——比如x * 8写成x << 3,x / 16写成x >> 4。
  • 用查表代替实时计算——比如PID参数、滤波系数,提前算好存成数组。
  • 减少循环内的函数调用——把函数体inline展开,或者用宏替代。
  • 使用DMA搬运数据——别让CPU干苦力活。
避坑指南:我曾经把一个for循环里的if判断提到循环外面,结果代码体积没变,执行时间反而增加了——因为编译器已经做了循环展开优化。所以,先测再改,别凭感觉。

3.2.2 中断与任务调度优化

中断响应时间是实时系统的命门。我建议你遵循以下原则:

  1. 中断服务函数要短——只做标志位设置或数据搬运,复杂处理放到主循环或任务里。
  2. 合理设置中断优先级——高频率中断(如定时器)优先级别太高,别把低优先级中断饿死。
  3. 使用临界区保护共享数据——但临界区要短,别关中断太久。
  4. RTOS任务优先级不要超过5级——级数越多,调度开销越大。

3.2.3 内存访问优化

MCU访问RAM比Flash快,访问Cache比RAM快。所以:

  • 热点数据放到RAM里——比如中断用的变量、频繁调用的函数。
  • 使用__attribute__((section(".ramfunc")))——把关键函数放到RAM执行。
  • 避免跨页访问——某些MCU跨Flash页访问会有等待周期。

3.3 低功耗测试模式设计

低功耗设计,不只是硬件的事。固件如果写不好,再好的硬件也省不了电。

3.3.1 低功耗模式选择

不同MCU的低功耗模式差异很大,但大体分这几类:

模式 典型电流 唤醒时间 适用场景
Run mA级 - 正常处理
Sleep 几百μA 几μs 空闲等待
Deep Sleep 几μA 几十μs 周期性唤醒
Shutdown nA级 ms级 长时间休眠

嗯,这里要注意:不是所有外设都能在低功耗模式下工作。比如你要用UART唤醒,就得选支持“UART唤醒”的Sleep模式,而不是Deep Sleep。

3.3.2 固件层面的低功耗策略

我总结了一套“三步走”策略:

  1. 空闲时立即休眠——别让CPU空转。用WFI或WFE指令,或者RTOS的IDLE钩子。
  2. 按需开启外设时钟——不用就关掉。比如ADC、SPI、定时器,用完立刻__HAL_RCC_xxx_CLK_DISABLE()。
  3. 降低主频——如果任务不紧急,把系统时钟降到最低。比如从72MHz降到8MHz,功耗能降一个数量级。
我的习惯:在低功耗测试模式下,我会把GPIO全部配置成“模拟输入”或“上拉输入”,避免浮空引脚漏电。这个细节,很多人会忽略。

3.3.3 低功耗测试模式代码示例

下面是一个典型的低功耗测试模式代码框架:

void EnterLowPowerTestMode(void)
{
    // 1. 保存关键寄存器
    uint32_t saved_clock = SystemCoreClock;

    // 2. 关闭所有非必要外设时钟
    __HAL_RCC_GPIOA_CLK_DISABLE();
    __HAL_RCC_GPIOB_CLK_DISABLE();
    __HAL_RCC_USART1_CLK_DISABLE();
    // ... 其他外设

    // 3. 配置唤醒源(比如RTC闹钟)
    HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);

    // 4. 进入Deep Sleep模式
    HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);

    // 5. 唤醒后恢复时钟
    SystemClock_Config();
    SystemCoreClock = saved_clock;

    // 6. 重新初始化必要外设
    MX_USART1_UART_Init();
    // ...
}

这个代码看起来简单,但我在项目中遇到过一个问题:唤醒后外设时钟没恢复,导致UART发不出数据。所以,唤醒后的时钟和外设重初始化,一定要做完整。

3.3.4 低功耗测试的测量方法

你写好了低功耗代码,怎么验证?别光看数据手册。我建议你:

  • 用精密万用表串在电源回路里——测平均电流。
  • 用示波器看电流波形——看有没有异常尖峰。
  • 用逻辑分析仪抓唤醒事件——确认唤醒源是否按预期触发。
避坑指南:我曾经测一个低功耗产品,Deep Sleep模式电流标称2μA,实测却有50μA。查了两天,最后发现是板子上一个上拉电阻没焊——GPIO浮空导致漏电。所以,硬件和固件要一起排查。

3.4 本章知识体系

下面这张图,把固件优化的三个维度串起来了:

固件优化策略知识体系 固件优化策略 代码体积优化 执行效率优化 低功耗测试模式 编译器选项 手动瘦身 链接脚本优化 算法优化 中断调度 内存访问 模式选择 固件策略 测量验证 三者互相制约,需根据产品需求做权衡

这张图把三个优化方向拆开了。你想想看,实际项目中,这三个方向往往是同时进行的。比如你为了省电降低了主频,那执行效率就得靠算法优化来补;你为了省Flash空间去掉了调试打印,那低功耗测试时就得靠硬件手段来测量。

总结一句话:固件优化没有银弹。编译器、算法、链接脚本、低功耗模式,每个环节都要动手试、上仪器测。我做了十几年嵌入式,最深的体会就是——别信数据手册,信你的示波器和万用表。