第1章:嵌入式系统基础回顾:MCU选型与资源规划、Bootloader与App分区设计、内存映射与保护机制

1.1 MCU选型:别只看主频和Flash大小

做血压计,MCU选型是第一道坎。很多人上来就盯着主频看,觉得越高越好。其实不然。

我个人习惯,先列三个硬指标:

  • ADC精度:血压计的核心是压力传感器,至少需要12位ADC。我见过有人用10位的,结果压力曲线毛刺多到没法看,最后还得加外部ADC,成本反而上去了。
  • 低功耗能力:血压计不是一直工作的。测量时几十毫安,待机时得做到微安级。我记得有一次选了一颗待机电流标称2μA的芯片,结果实际跑起来有8μA,电池续航直接砍半。
  • Flash与RAM:别只看总容量,要看分区够不够用。后面要放Bootloader、App、参数区、日志区,每个区域都得留余量。

我的经验法则:选MCU时,Flash至少留30%余量,RAM留50%。你永远不知道后面会不会加功能。

举个例子,我常用的STM32G0系列,主频64MHz,12位ADC,待机电流0.5μA,Flash从32KB到256KB可选。做入门级血压计,64KB Flash + 8KB RAM就够用。但要是想做带蓝牙的,至少得128KB Flash起步。

1.2 Bootloader与App分区:两个世界,一个芯片

Bootloader和App分区,说白了就是让芯片里住着两个程序。一个负责升级,一个负责干活。

分区设计其实很简单,但坑不少。我直接给个典型布局:

区域 起始地址 大小 用途
Bootloader 0x08000000 16KB 启动、校验、升级
App 0x08004000 96KB 业务逻辑
参数区 0x0801C000 4KB 校准参数、用户设置
日志区 0x0801D000 12KB 运行日志、错误记录

为什么要这么分?

  • Bootloader放最前面:芯片上电默认从0x08000000执行。Bootloader先跑,检查App是否有效,有效就跳转,无效就等升级。
  • App紧跟着:跳转地址要固定,不能随便改。我见过有人把App地址改了,结果Bootloader跳过去直接跑飞。
  • 参数区单独放:升级App时,参数不能丢。用户校准一次不容易,总不能每次升级都重新校准吧?
  • 日志区放最后:用来记录异常信息,方便排查问题。

小技巧:Bootloader里放一个版本号,App里也放一个。升级时先比较版本号,避免重复升级或者降级。我曾经遇到过用户反复刷同一个版本,结果Flash磨损了。

1.3 内存映射与保护机制:别让App踩了Bootloader的脚

内存映射,说白了就是给每个模块分配好地址范围。谁该用哪块,清清楚楚。

但光分配还不够,得加保护。不然App跑飞了,直接往Bootloader区域写数据,下次开机就变砖了。

我常用的保护手段:

  • MPU(内存保护单元):Cortex-M0+以上内核基本都有。把Bootloader区域设为只读,App区域设为可读写。App想写Bootloader?直接触发HardFault。
  • Flash写保护:MCU的Flash控制器一般都有写保护寄存器。Bootloader启动后,先把自己的区域锁死,再跳转到App。这样App就算想写也写不了。
  • 看门狗:App跑飞了,看门狗超时复位,系统回到Bootloader。Bootloader再检查App是否正常,不正常就进入升级模式。

注意:MPU配置要在特权模式下做。我刚开始做的时候,在用户模式下配MPU,结果配了半天没反应。后来才发现,MPU寄存器只能在特权模式访问。

代码示例,Bootloader跳转到App前的MPU配置:

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    
    // 禁用MPU
    HAL_MPU_Disable();
    
    // 配置Bootloader区域:0x08000000 - 0x08003FFF,只读
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x08000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
    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_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    // 配置App区域:0x08004000 - 0x0801BFFF,可读写
    MPU_InitStruct.BaseAddress = 0x08004000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_96KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.Number = MPU_REGION_NUMBER1;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    // 使能MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

这段代码在Bootloader里执行,配置完MPU后再跳转到App。App运行期间,Bootloader区域是只读的,App想写也写不了。

1.4 避坑指南:我踩过的那些坑

做血压计固件这么多年,踩过的坑不少。挑几个典型的说说:

  • 中断向量表偏移:App的起始地址不是0x08000000,所以中断向量表也得跟着偏移。我见过有人忘了改SCB->VTOR,结果一进中断就跑飞。
  • Flash擦除时间:擦除一页Flash要几十毫秒,如果这时候血压计正在测量,压力数据就丢了。所以升级操作一定要避开测量过程。
  • 参数区备份:参数区写一半断电了怎么办?我习惯用双备份,写之前先备份,写完后校验,校验通过再删除备份。这样就算断电,下次启动也能恢复。

总结一下:MCU选型别只看参数,要结合实际需求。分区设计要留余量,保护机制要到位。这些基础打好了,后面的固件升级和安全机制才能稳稳当当。

嗯,这一章就到这里。下一章我们聊聊固件升级的通信协议设计,包括数据包格式、校验和重传机制。到时候我会分享一个我在项目中用过的实用协议栈。