3、MCU选型与启动流程:主流医疗级MCU对比、启动文件分析、时钟树配置、堆栈设置

好,咱们进入第三章。这一章我打算聊聊MCU选型和启动流程。说实话,这是整个OTA设计的地基。地基没打好,后面再漂亮的升级方案都是空中楼阁。我在医疗贴片项目里吃过这个亏,所以今天把经验掰开揉碎了讲给你听。

3.1 主流医疗级MCU对比:STM32、NXP、TI

医疗贴片对MCU的要求很明确:低功耗、高可靠性、小封装、还得有足够的安全特性。市面上主流的就这三家——ST、NXP、TI。我三个平台都用过,各有各的脾气。

特性 STM32 (如L4/U5系列) NXP (如LPC55xx/K32W) TI (如MSP430/CC26xx)
功耗 优秀,有超低功耗模式 良好,动态功耗控制不错 极低,MSP430是标杆
安全特性 硬件加密、TrustZone (U5) 内置PUF、安全启动 AES加速器、安全密钥存储
OTA支持 双Bank Flash,方便 支持双区,但需注意映射 Flash较小,需外扩
生态系统 最丰富,HAL/LL库成熟 MCUXpresso,上手略复杂 CCS,资料相对少
典型封装 BGA、QFN,适合贴片 QFN、WLCSP QFN、TSSOP

我个人习惯:如果项目对功耗要求极其苛刻,比如纽扣电池用一年,我会优先考虑TI的MSP430。但如果你需要跑复杂的OTA协议栈,比如带差分升级、加密校验,那STM32的生态能帮你省不少时间。NXP的LPC55系列在安全启动方面做得很好,适合需要通过FDA认证的医疗设备。

我的经验:别只看数据手册上的功耗数字。实际项目中,MCU在OTA升级时的峰值电流往往比睡眠电流更关键。我曾经选了一颗睡眠电流只有1uA的芯片,结果升级时电流飙到50mA,电池电压瞬间被拉低,导致升级失败。嗯,这个坑我替你们踩过了。

3.2 启动文件分析:从Reset到main()

启动文件,说白了就是MCU上电后执行的第一段代码。很多工程师觉得这是芯片厂商写好的,不用管。但做OTA时,你不得不跟它打交道——因为你要修改中断向量表,要管理启动地址。

以STM32为例,启动文件(startup_stm32l4xx.s)主要干了这几件事:

  1. 设置堆栈指针:从向量表的第一个字加载SP值
  2. 复位处理:调用SystemInit()初始化时钟
  3. 数据段搬运:把RW段从Flash搬到RAM
  4. BSS段清零:把未初始化的全局变量清零
  5. 跳转到main():调用C库的__main,最终进入你的代码

你想想看,如果OTA升级后,新的固件放在不同的Flash地址,那向量表的位置就变了。这时候你需要手动修改SCB->VTOR寄存器。我见过有人直接在启动文件里硬编码向量表地址,结果升级一次改一次,太痛苦了。

OTA关键点:启动文件中的向量表偏移必须在系统启动早期设置。我建议在SystemInit()函数里,根据当前运行的固件分区,动态计算VTOR的值。这样无论固件在Bank0还是Bank1,都能正确响应中断。

3.3 时钟树配置:别让时钟成为瓶颈

时钟树配置,嗯,这是个容易翻车的地方。医疗贴片对时钟精度有要求——比如心电采样需要精确的时基,OTA通信需要稳定的波特率。

我一般遵循这个原则:

  • 系统时钟:用外部晶振,精度高。贴片空间有限,我常用8MHz或16MHz的晶振
  • RTC时钟:用32.768kHz的晶振,或者内部低速RC。但内部RC温漂大,做长时间计时会不准
  • 外设时钟:能关就关,省电。OTA升级时才打开必要的通信外设

举个例子,STM32L4的时钟树配置代码:

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  // 配置HSE为8MHz,PLL倍频到80MHz
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 1;
  RCC_OscInitStruct.PLL.PLLN = 20;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;
  RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
  RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
  HAL_RCC_OscConfig(&RCC_OscInitStruct);

  // 配置AHB、APB分频
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
  HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4);
}
注意:OTA升级时,如果切换了时钟源(比如从HSE切到HSI),一定要重新配置所有外设的时钟分频。我遇到过升级后UART波特率对不上的情况,查了半天才发现是时钟源变了,但UART的时钟分频没更新。

3.4 堆栈设置:别让堆栈溢出毁了你的OTA

堆栈设置,看似简单,实则暗藏杀机。医疗贴片的RAM通常只有几十KB,堆栈分配不合理,很容易在OTA升级过程中崩溃。

栈(Stack):用于局部变量、函数调用。我建议至少分配2KB,如果OTA协议栈比较复杂,比如用到mbedTLS做签名验证,栈可能需要4KB甚至更多。

堆(Heap):用于动态内存分配。说实话,在医疗贴片这种资源受限的设备上,我尽量不用动态分配。malloc/free容易产生碎片,而且一旦分配失败,整个系统就挂了。OTA升级时,我倾向于用静态缓冲区。

在启动文件中,堆栈大小是这样定义的:

Stack_Size      EQU     0x00001000   ; 4KB栈
                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

Heap_Size       EQU     0x00000200   ; 512字节堆
                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit
我的建议:在调试阶段,把栈填满0xCC(或者0xDEAD),然后运行一段时间,检查栈空间的使用情况。我曾经用这个方法发现一个递归函数吃掉了1.5KB的栈,优化后降到了200字节。嗯,有时候问题就藏在这些不起眼的地方。

另外,OTA升级时,Bootloader和App的堆栈设置要协调好。Bootloader的栈可以小一点,因为它的任务简单。但App的栈一定要留足余量,特别是当你在升级过程中还要处理蓝牙通信、Flash擦写、CRC校验这些并发任务时。

好了,这一章就到这里。MCU选型是战略决策,启动流程是战术细节,两者都马虎不得。下一章我们聊聊Bootloader的设计——那才是OTA的核心战场。