ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

嵌入式启动流程全解析:从复位向量到OTA升级的工程实战指南

嵌入式启动流程全解析:从复位向量到OTA升级的工程实战指南 1. 为什么要花大力气拆解启动流程做嵌入式固件这几年我最大的一个感受是很多线上问题、现场事故最后都归结到了启动过程上。应用逻辑跑飞了、外设初始化顺序不对、内存踩踏导致上电即死、OTA升级后设备变砖——这些问题看似五花八门真正的根源往往就藏在从复位向量到main函数之间的那几百行代码里。但现实是大多数工程师对启动流程的理解停留在复位后从0x08000000开始执行这种层面。能用调试器跑到main就算成功至于中间发生了什么、为什么要这样设计、哪些环节最容易出问题很少人能把整条链路完整讲清楚。这也是我在CSDN开这个付费专栏的初衷——把启动流程、故障定位、OTA升级这三块硬骨头掰开揉碎从原理到工程实践一条线讲透。这篇连载的上篇我已经重点做了启动流程的深度拆解课后留了几道思考题。今天这篇一方面把上篇的思考题完整解析一遍另一方面把故障定位的方法论和OTA升级的工程化方案做个系统梳理。三块内容看似独立实际是一条完整的技术链路不懂启动流程就不知道系统从哪里开始不会故障定位系统崩了只能干瞪眼不做OTA工程化固件迭代就只能靠下载器一台台烧。适合谁来读我觉得只要是做嵌入式开发、特别是做单片机或者带系统的固件开发、想往更高阶方向走的工程师这篇文章都值得花半小时认真看一遍。尤其是已经在做量产项目、遇到过现场问题却不知道怎么下手的人你踩过的坑大概率都在下面这些内容里。2. MCU与SoC启动流程从复位向量到main函数之间到底发生了什么2.1 裸机MCU启动的本质一张向量表和一个栈指针先看最简单的场景——裸机MCU比如STM32F103这种Cortex-M3内核的片子。上电之后硬件做的事情其实非常少读0x00000000处的初始栈指针MSP读0x00000004处的复位向量然后跳转过去执行。这个动作在ARM架构手册里有明确规定属于CPU的基本行为。但这里有个关键点硬件只负责这两个动作之后的一切都是软件的事。跳转到复位向量之后芯片厂商的启动文件比如startup_stm32f103xe.s就开始干活了。这段汇编代码做的事情按顺序大概是初始化栈空间把栈指针设置到RAM的合法区域拷贝.data段的数据从Flash到RAM清零.bss段可选配置系统时钟调用SystemInit函数做时钟和Flash等待状态的初始化跳转到C运行时库的__main最终进入main函数很多人以为启动文件是编译器自动生成的其实不是。它由芯片厂商提供不同厂商、不同内核的启动文件差异很大。比如Cortex-M0的启动文件和M3/M4的就不同M0的中断向量表更简单可用的中断也少得多。如果移植代码时忽略了这个差异经常会在莫名其妙的地方出问题。另外还有一个非常容易被忽略的细节看门狗。如果工程里默认开了独立看门狗IWDG而且你在进main之前消耗的时间过长——比如SystemInit里做了较耗时的时钟切换——看门狗可能已经溢出复位了。这个问题在调试器里几乎看不出来因为调试器默认暂停看门狗计数但现场设备上就是反复重启。我见过不止一个项目因为这个原因一上电就跑飞查了半天最后发现是启动文件里缺了喂狗。虽然裸机场景下看门狗通常由用户程序自己初始化但如果你用的是带Bootloader的方案这个问题会更突出——后面讲OTA的时候我会再提。2.2 SoC启动链路为什么要比MCU多那么多级说完MCU再看SoC。拿常见的i.MX6ULL、全志V3s这类带MMU、能跑Linux的应用处理器来说启动过程要比MCU复杂一个量级。SoC的典型启动链路是这样的ROM Code → BootROM固化在芯片内部 → SPL或叫MLO → U-Boot → Kernel → rootfsROM Code是芯片出厂时就固化在硅片内部的一段代码谁也改不了。它的职责是根据eFUSE或GPIO的电平状态决定从哪里加载下一级bootloader——比如SD卡、eMMC、NAND Flash、USB、网络等等。这段代码的主要作用是引导级的引导它的存在让芯片可以支持多种启动介质而且不需要修改芯片本身。然后是SPLSecondary Program Loader。这是U-Boot的一个精简版本主要目的是在DDR还没初始化之前用最小的代码量把DDR初始化好然后把完整的U-Boot从存储介质加载到内存里运行。为什么要分两步因为完整的U-Boot比较大几百KB到几MB而芯片内置的SRAM通常只有几十到几百KB装不下完整的U-Boot。所以先把SPL加载进SRAM运行由SPL初始化SDRAM/DDR再把完整的U-Boot搬到内存里跑。U-Boot起来之后会做更完整的板级初始化串口打印、网卡、eMMC/SD驱动、显示、命令行交互等。然后根据bootcmd环境变量把kernel镜像和设备树加载到内存指定地址跳转执行。kernel接管后完成虚拟内存、调度器、驱动模型等的初始化最后挂载根文件系统执行init进程。这条链路看上去很简单但实际调试时每一步都可能出问题。一个常见的情况SPL阶段串口没输出任何东西你连U-Boot都没进。这时要用调试器确认SPL有没有被执行——是加载地址不对还是DDR初始化就崩了。另一个典型问题U-Boot已经起来了但kernel启动到一半死掉这通常是设备树DTB与kernel版本不匹配或者kernel里对应外设驱动不全导致。2.3 RT-Thread系统启动初始化流程的独特性如果你用的是RT-Thread这种RTOS启动流程又是另一套逻辑。RT-Thread在keil/IAR/GCC下的启动过程大体是硬件启动文件把环境准备好之后进入RT-Thread的入口函数rtthread_startup然后在这个函数里完成一系列初始化最终启动调度器进入第一个线程。按顺序大概是这样关闭中断保证初始化过程不被抢占初始化硬件相关部分板级初始化、系统堆初始化初始化内核对象调度器、信号量、互斥锁、邮箱、消息队列等创建main线程或叫init线程让应用代码在main线程里跑启动调度器开始多任务调度有一个RT-Thread新手容易踩的坑在main函数里写一个死循环什么都不做以为RT-Thread会自动跑别的线程。实际上如果main线程里没有主动让出CPU或者阻塞等待它会把CPU占满其他优先级低于它的线程永远得不到调度。这就是为什么RT-Thread官方例程的main函数里通常要么有延时、要么有信号量等待——不是为了搞个示例而是为了让调度器有机会切换任务。还有一点RT-Thread的启动过程对中断比较敏感。如果在调度器启动之前就打开了中断而且此时刚好有中断触发中断处理函数里如果调用了依赖于调度器的API比如释放信号量、发送消息可能会导致系统崩溃。因为调度器还没初始化完内核对象还没准备好。这个属于时序问题排查起来很隐蔽建议你写板级初始化代码时明确中断开关的时机不要依赖厂商的默认实现。2.4 U-Boot启动流程中必须吃透的几个环节U-Boot作为SoC启动链路中的关键一环它的启动流程值得单独说。U-Boot从入口_start开始经历大致以下几个阶段设置CPU为SVC模式关闭中断和MMU保证硬件处于最原始的干净状态初始化串口早期串口用于打印最开始的启动日志初始化DDR控制器如果是从SPL跳转过来这一步可能已经做过了重定位U-Boot自身到DDR的高地址区域因为低地址区域后续要放kernel初始化板级硬件时钟、网卡、存储控制器、GPIO等进入main_loop等待用户按键进入命令行或者自动执行bootcmd理解U-Boot的关键在于理解它的两个阶段设计SPL阶段和U-Boot正式阶段。很多刚开始接触U-Boot的工程师会困惑为什么U-Boot源码里有两个board_init_f和board_init_r为什么有的函数执行两次原因就是分阶段初始化——第一阶段init_f在SRAM里运行做最小初始化第二阶段init_r在DDR里运行做完整初始化。实际调试中最常见的U-Boot故障有三种U-Boot起来后网卡不通无法通过tftp下载kernel。这个一般先查PHY地址对不对再看设备树里网卡节点的reg属性是否匹配。U-Boot能起来但无法加载kernel提示Bad Linux ARM zImage magic。这个通常是加载地址不对——kernel镜像要加载到内存的合适地址不同SoC有各自约定乱放肯定不行。加了新外设后U-Boot启动变慢甚至卡死。这个重点检查新设备的初始化是否放在了会阻塞的位置比如等待某个不稳定外设的握手信号。3. 启动阶段的故障定位方法论从现象到根因的完整排查链路3.1 建立启动阶段时间轴思维讲完原理下面进入故障定位环节。我认为做启动阶段的问题排查第一步不是拿示波器、不是接调试器而是在纸上画出系统从复位到正常运行的时间轴每一段标注出当前执行什么代码、输出什么信息、外设处于什么状态。为什么要画时间轴因为启动阶段的问题有一个共同特征信息量极少错误窗口很短。资源还没完全初始化日志系统可能还没起来网络栈可能还不可用你唯一能依赖的往往是一个串口、几行打印、一个LED或者调试器。如果没有时间轴意识你很容易在错误的时间点找错误的信息。比如之前帮一个客户排查农用设备控制板现象是上电没反应LED不亮。乍一看像是电源问题但实测电源输出正常、MCU没有明显短路。后来用调试器连上发现CPU已经跑在HardFault_Handler里了。再进一步从故障寄存器CFSR、HFSR里看到是总线错误访问了非法地址。顺着这个线索查下去——初始化外设时某个外设的时钟还没使能就写了它的寄存器导致总线错误。这就是典型的时间轴错位问题代码在A阶段启动了某个外设的时钟却在B阶段时钟还没起来时就开始操作那个外设的寄存器。画时间轴写清楚每一步的先后顺序这种问题其实很好避免。3.2 串口打印是启动阶段的第一排障手段但要用对方法串口打印是最朴素也最有效的启动排障手段。很多工程师用串口打印的方式是这样的在main入口打一行System Start然后在几个关键函数入口各打一行。这个思路没错但实际操作中经常遇到两个问题第一个问题是打印本身会改变启动时序。如果用的是阻塞式串口发送每条打印都可能耗时几十微秒到几毫秒在高波特率下还好波特率低时影响就很明显。如果做的是对时序敏感的外设初始化比如某些传感器上电后要求在一定时间内完成初始化打印日志反倒可能引入故障。我的建议是在排障阶段尽量使用调试串口不参与业务逻辑并且把关键日志放到初始化完成之后再集中打印。第二个问题是打印信息太泛滥真正有用的信息被淹没。启动排查阶段我习惯这么做每条关键打印都带一个编号和缩写比如[P1] CLK INIT OK、[P2] SDRAM SELF-TEST PASS、[P3] LOAD KERNEL FROM EMMC。这样一旦问题发生只需要看最后一个编号就知道卡在哪里。比一屏乱糟糟的日志高效得多。这个方法在排查U-Boot启动异常时尤为好用。U-Boot默认打印信息量很大但很多关键阶段并没有明确的标记。我会在board_init_f、board_init_r、main_loop这些关键函数里手动加带编号的打印重编一个排障版U-Boot跑一次就知道问题发生在哪一级。3.3 实战案例一个启动卡死问题从无任何输出到定位根因的完整过程分享一个我真实处理过的案例尽量还原完整的排查链路其中包括每一次尝试和反复而不是只给结论。现象描述一块基于Cortex-M7内核的板子上电后串口无任何输出调试器加载固件后可以正常跑说明固件本身没问题但断电重新上电就完全没反应。客户怀疑是硬件问题板子发过来让我帮忙看。第一步确认电源、时钟、复位。用示波器测各路电源的上升沿确认上电时序是否满足要求测复位引脚发现复位释放正常用频率计测外部晶振发现时钟输出正常。这说明硬件的最小系统大概率没问题。第二步用调试器挂上单步执行。从复位向量开始单步走发现代码停在了一个奇怪的地址不是HardFault_Handler也不是Default_Handler而是一个看起来像是随机的地址。查map文件这个地址在某个外设寄存器的地址范围内——说明代码访问了非法地址触发了总线错误但异常处理本身也有问题所以没有跳转到预期的地方。第三步回到启动文件检查栈指针。再看一遍硬件复位行为CPU复位后先从0x00000000取初始栈指针再从0x00000004取复位向量。问题就出在这里**0x00000000处的值**不是合法的RAM地址。用调试器读一下内存发现Flash的前4个字节全是0xFFFFFFFF——等于说Flash根本没烧录成功或者烧录的地址不对。第四步查烧录配置。打开烧录算法配置发现客户用的是外部QSPI Flash启动方案但烧录时把固件下载到了内部Flash的地址0x08000000而芯片实际是从外部QSPI的0x90000000启动的。烧录地址和启动地址不匹配导致CPU从QSPI Flash读到的全是0xFFFFFFFF初始化栈指针就失败了。复盘这个问题根因很简单但排查过程走了一些弯路。如果用启动时间轴的方法第一步就应该确认芯片实际是从哪个地址启动的启动介质上有没有合法数据。有了这个前提后面的排查会快很多。这个案例也说明很多启动问题不是代码写错了而是工程配置与硬件设计不一致这类问题仅靠看代码是看不出来的必须结合硬件实际情况一起分析。3.4 故障定位的常用工具箱从调试器到逻辑分析仪启动阶段的故障定位工具不在多关键是选对并用对。我个人最常用的组合是一个带SWD接口的调试器J-Link、DAP-Link都行、一个四通道以上的示波器、一个逻辑分析仪再加一个串口调试助手。调试器主要用于看CPU的执行状态停在哪里、PC指针是什么、堆栈回溯到哪个函数、寄存器状态怎么样。对于HardFault类问题SWD调试器几乎是必备的。我特别推荐在启动阶段就给项目配置好异常回溯功能——Cortex-M系列可以注册一个HardFault_Handler在异常发生时把CFSR、BFAR、MMFAR这些寄存器的值保存下来同时做一次栈回溯把函数调用链打印到串口。这套东西在量产阶段排查偶发问题能省大量时间。示波器用来验证时序上电时序、复位释放时序、时钟稳定时间、某些外设的使能信号是否按照预期先后产生。启动时对外设有严格的时序要求比如先稳定供电、再释放复位、再开始初始化这些时序用示波器一看便知。逻辑分析仪在排查通信类外设启动问题时很有用。比如设备启动后要跟外部芯片通过I2C/SPI做握手如果软件里初始化顺序不对可能会在总线协议层面违反时序要求。这时候逻辑分析仪可以把总线上的每一笔交易都记录下来对照数据手册确认哪个字节没对上。4. OTA升级工程化实战从方案选型到版本回滚的完整设计4.1 OTA升级的本质不是加个下载功能而是保证设备永远可救很多人理解OTA升级就是设备通过WiFi/4G下载固件然后写入Flash重启。这个理解太简单了。真正的OTA升级从设计层面要回答的问题比怎么写Flash多得多下载过程中断电了怎么办新固件启动失败怎么办存储空间不够怎么办大批量设备同时升级服务器扛得住吗升级失败导致设备变砖售后成本谁来承担所以我在专栏里反复强调一个观点OTA升级的本质不是加个下载功能而是保证设备永远可救。围绕这个目标工程上要做的事情包括但不限于分区规划、双备份机制、升级状态记录、断点续传、版本校验、失败回滚、灰度发布。4.2 Flash分区设计A/B分区方案与单分区方案的取舍在嵌入式设备上做OTA最核心的问题就是新固件放哪里、旧固件怎么处理、失败了怎么办。由此引出两种主流的Flash分区方案。单分区方案整个应用区域只有一个固件槽位。升级时新固件直接覆盖旧固件的空间。这种方案优点是Flash占用小缺点是升级过程中如果发生断电或写入错误设备直接变砖没有回退手段。所以单分区方案必须配合Bootloader的恢复机制——Bootloader检测到应用区校验失败就进入等待下载模式等待主机重新下发固件。这方案对Bootloader的设计要求很高。A/B双分区方案Flash里同时保留两份固件一个称为A槽一个称为B槽。Bootloader每次启动时根据升级标志决定从哪个槽启动。升级时新固件写入非当前运行的槽位写入完成后标记该槽位为待验证状态重启后Bootloader从新槽位启动应用运行正常则标记为已确认如果运行异常比如看门狗超时、启动失败则回滚到旧槽位。A/B方案在消费电子和车机领域几乎成了标配原因很简单它把升级失败变砖的概率压缩到极低。代价是Flash容量要求翻倍。在MCU资源受限的场景下需要在Flash容量和安全性之间做一个权衡。我个人的建议是如果Flash剩余空间充足比如有2MB固件只有500KB优先上A/B方案如果Flash吃紧至少也要保证Bootloader里有可靠的恢复接口串口、USB、网络等能让设备恢复到可下载状态。单分区不是不能用但必须有后手。下面是两种方案的核心对比对比维度单分区方案A/B双分区方案Flash占用低只需一份固件空间高需要两份固件空间升级失败恢复依赖Bootloader恢复下载自动回滚旧版本实现复杂度较低较高断电风险高写入中断可能变砖低最多启动一次未验证版本适用场景Flash极小的低成本MCU对可靠性和可维护性要求高的产品4.3 OTA升级的关键工程细节校验、断电保护、状态机确定了分区方案下面聊几个OTA升级必须处理的工程细节。校验是最基本的要求。固件包下载完成后必须做完整性校验——常见的是CRC32或者MD5/SHA256。我建议至少两层校验第一层固件包本身的校验下载完成后对整个包做哈希比对第二层写入Flash后的再校验写入完成后读回来重新计算哈希防写入过程出错。如果用的是带加密的固件签名方案那校验逻辑还要加上签名验证防止伪造固件。断电保护是OTA的生死线。以STM32为例写入内部Flash时如果中间断电可能会出现两种情况一是正在写入的扇区数据损坏二是擦除了一半导致整个扇区内容不可用。解决办法有三个层次第一使用双分区方案前文已说第二在写入前先对目标扇区做擦除备份比如把旧数据先搬到一个备份区域但这会占用额外Flash第三设计一个升级状态机每次写入都把当前升级进度记录在一个专门的状态存储区重启后Bootloader读取状态机知道前一次升级进行到第几步从而决定是继续、回滚还是重新下载。升级状态机设计空闲→下载中→校验→写入A→校验A→等待重启→标记A有效→运行新固件→等确认。状态机每一步都持久化到状态区。这个状态机的价值在于在升级的任何一步断电系统都能在重启后明确知道自己该干什么。没有状态机的OTA本质上是在赌运气好不会断电。固件包的管理也是工程化的重头。正式产品建议用固件包格式来封装版本信息、平台信息、硬件版本、CRC、签名等元数据。比如定义文件头Magic4字节 版本号4字节 硬件平台ID2字节 固件大小4字节 CRC324字节 签名可选 固件数据原始的BIN文件Bootloader在升级前先解析文件头检查平台ID是否匹配、版本号是否合法、CRC是否正确全部通过才允许写入。这一步能拦住很多误操作比如拿A型号的固件去刷B型号的设备。4.4 回滚机制与容错设计如何保证升级后设备还活着双分区方案天然支持回滚但回滚逻辑本身也是需要精心设计的。不可能出现任何异常都立刻回滚——有些情况是临时性故障比如升级完后第一次启动时外设还没稳定给新固件一个试运行窗口期是更合理的做法。我常用的一种做法是新固件启动后主动向一个日志区写新固件启动标记。应用在正常运行一段时间比如5分钟后如果一切正常则向状态区写确认OK。如果在窗口期内发生看门狗复位或者应用主动报告启动失败Bootloader判定新固件不可用回滚到旧槽位。窗口期的长度要根据产品场景设定传感器类设备可以很短30秒~1分钟复杂的工业设备可能需要更长的验证时间比如一次完整的自检流程。窗口期太短可能导致误判太长则可能让坏固件跑太久影响生产。这个需要权衡。另外要说一个常见的工程失误回滚之后没有限制反复升级的次数。如果新固件一直有问题设备就会陷入升级→启动失败→回滚→再次检测到新版本→再次升级→再次回滚的循环。正确的做法是在状态区记录连续升级失败的次数超过N次比如3次之后系统暂停自动升级进入仅手动恢复模式等待运维人员介入。4.5 一个低配版OTA的具体实现思路基于Ymodem或HTTP的入门方案讲完工程化整体思路给一个可以落地的入门方案。如果你的产品还在早期验证阶段不需要一步到位上A/B分区可以先实现一个最小可用OTA。思路Bootloader 应用层双区架构应用区32KBBootloader区16KB中间用一个标志扇区记录升级状态和信息。升级方式用Ymodem协议——因为实测显示它比Xmodem更稳而且支持文件名传输可以顺便传递版本信息——或者如果你设备有网络能力用HTTP下载固件包也行。大致流程是应用收到新固件包先把固件包放到外部Flash或文件系统的临时区应用校验固件包完整性CRC/哈希应用把升级标志写入标志扇区状态为准备升级系统软复位进入BootloaderBootloader读取标志扇区发现准备升级状态从临时区读取固件并写入应用区写入完成后再次校验通过则把标志改为已升级待确认跳转到应用应用确认运行正常后把标志改为OK这套方案的好处是即使升级失败由于Bootloader还在而且临时区的固件包还在Bootloader可以重新尝试写入。只有一种情况会变砖Bootloader自身被破坏。所以Design上要避免把Bootloader和应用放在同一个升级包里面Bootloader只通过烧录器更新。如果你第一次做OTA我强烈建议先在开发板上完整走一遍这个流程手动模拟各种异常写一半断电、固件包损坏、新固件启动即崩把这些场景全部验证通过再考虑上量产。5. 上篇课后思考题完整解析启动流程核心问题的拆解与延展5.1 思考题一为什么复位后必须先设置栈指针再跳转C语言入口函数这道题看起来简单但考察的是对C语言运行时环境的理解。CPU的栈是用来支持函数调用、局部变量、中断嵌套的没有合法的栈指针C语言代码根本无法执行。跳转到C函数的第一步通常就是压栈保存现场如果栈指针指向的是非法地址压栈操作直接触发总线错误或内存保护错误整个系统瞬间崩溃。更深一层栈初始化的正确性还关系到后续的所有中断处理。Cortex-M系列的异常处理会自动使用MSP主栈指针如果MSP在上电后没有被正确设置任何中断发生都可能导致HardFault。所以启动文件里第一条指令之前必须先把MSP设置好。手写链接脚本或移植启动文件时一个常见的错误就是栈的地址和大小定义错了——比如定义在了一个保留给外设用的地址段或者超出了RAM的实际大小。这种错误在调试器里看往往是莫名其妙的HardFault。5.2 思考题二在main函数最开头加一条打印语句为什么有时会触发HardFault这个问题问的其实是运行时环境有没有完全准备好很多工程师以为从复位向量跳转到main就万事大吉了其实main能被执行只说明栈指针和复位向量是好的不代表全部环境都已就绪。会出现HardFault的情况主要有几类打印功能依赖的外设还没初始化。比如printf重定向到串口USART1但在main最开始还未完成USART1的时钟和GPIO配置直接写串口寄存器可能访问到未使能的外设引发总线错误。.data段的全局变量还没拷贝完毕。如果printf的实现内部依赖某个全局变量比如缓冲区指针而这个全局变量还处于未初始化状态行为就是未定义。C运行库的初始化如堆的初始化还没执行而printf内部用了malloc或动态内存堆指针是空的。所以规范的做法是main函数里第一条有意义的代码应该是初始化最基础的环境——比如重定向串口所需的时钟和GPIO、系统时钟配置等。先保证最小输出能力可用再逐步扩展其他功能。这也是为什么很多工程模板的main函数总是有一个固定的初始化顺序系统时钟→调试串口→其他外设。这不是习惯问题而是有逻辑依据的。5.3 思考题三Bootloader跳转到应用前为什么要关中断、关外设、重置栈指针这是Bootloader设计中一个极其关键的细节。如果Bootloader不处理干净就直接跳转应用启动时大概率出问题。第一必须关中断。Bootloader运行期间可能已经打开了定时器中断、串口接收中断等。跳转到应用时应用的向量表地址和应用代码是新的但中断控制器的使能位还是Bootloader配的旧状态。此时如果有中断触发CPU会跳到应用的中断向量表去执行——但应用可能还没来得及初始化对应的中断源或者根本没有处理这个中断源的代码系统直接就崩了。正确做法是跳转前把NVIC里所有已使能的中断全部失能清除pending状态然后才执行跳转指令。第二必须关闭外设。Bootloader里打开的DMA、串口、SPI等外设如果不关闭它们可能在应用启动过程中继续产生传输请求、修改内存数据。特别是DMA——如果Bootloader有个正在进行的DMA传输跳到应用后DMA还会继续跑往内存里写数据可能覆盖应用正在使用的关键变量。跳转前的标准影式是停掉所有外设、停掉DMA、关掉中断、清理缓冲区。第三必须重置栈指针MSP。Bootloader自身的栈地址可能跟应用定义的栈地址不同尤其在Bootloader和应用是独立编译、独立链接的情况下。跳转时如果沿用Bootloader的栈指针应用代码一执行就可能栈溢出或者使用了非预期内存区域。规范做法是跳转前从应用向量表的首地址读取出初始栈指针值手动写到MSP然后再跳转。还有一个容易被忽略的点如果在FreeRTOS、RT-Thread这种RTOS环境下做跳转跳转前必须确保调度器已停止、所有任务已删除、SysTick已关闭。否则跳进应用后应用的调度器刚初始化旧任务的上下文还挂在中断里随时可能出诡异问题。5.4 思考题四RTOS的启动流程和裸机启动流程本质差异在哪里这道题更开放一些我给一个参考答案框架。裸机启动流程核心是从复位向量到main函数讲的是把C语言运行环境准备好、把硬件初始化好然后程序顺序执行。它的主线是线性的先准备环境再执行用户逻辑逻辑跑完就死循环或返回。RTOS启动流程核心是先建内核再开调度。在进入main之前或进入main之后系统要完成内核对象的创建——调度器、定时器、信号量、队列等——然后创建第一个任务通常是main线程或startup线程最后启动调度器。一旦调度器启动代码的主体就不再是用户程序顺序执行而是由调度器根据优先级和时间片决定哪个任务在哪个时刻执行。这两个流程的本质差异我觉得可以这样概括裸机启动的最终目标是跑用户代码RTOS启动的最终目标是启动调度器让用户代码以任务的形式并发运行。这个差异带来了一系列连锁反应裸机程序里全局变量随便用RTOS里要考虑多任务并发访问的互斥问题。裸机里中断处理函数可以做很多事只要改全局变量RTOS里中断处理函数要尽量短更多要做的是发消息给任务让任务去处理。裸机里主循环就是程序的全部RTOS里主循环只是一个优先级最低的任务随时可能被抢占。如果你从裸机转RTOS最先要扭转的不是API用法而是这个思维模型你已经不是在写一个程序而是在管理一组并发任务。代码质量评估标准随之变了——不再是逻辑是否正确而是任务间的协作是否安全、是否高效。6. 从专栏连载到工作实战我的几点经验总结写这个专栏的过程其实就是我把自己多年做嵌入式底层的经验重新梳理一遍的过程。很多知识平时用的时候觉得只是常识真正要写成系统性的文章时才发现很多细节从前只是得过且过的理解——比如U-Boot为什么要分两个阶段比如Reset_Handler里那一大段汇编到底每行在干嘛。第一个建议把啃启动代码当成必修课而不是选修课。无论你做裸机还是RTOS无论是MCU还是SoC花一个完整的时间段把厂家提供的启动文件、链接脚本一行一行读明白比刷十篇应用层框架文章都值。你以后遇到的80%的疑难杂症都能回溯到这个阶段。第二个建议每个项目都要有一个多级打印的启动日志体系。从开机到正常运行的每个关键阶段都设置不同级别的日志输出线上出问题后哪怕只拿回一段串口日志你也大概率能判断问题出在哪个模块。我见过太多项目量产之后的日志系统被精简得干干净净现场出问题时连一点排查线索都没有。省那几KB Flash往往得不偿失。第三个建议OTA不是功能是系统设计。别等产品量产出问题后才想要不要加个升级功能。在项目架构设计阶段就要规划好分区、Bootloader、升级协议、回滚策略。等设备铺了几千台再想改启动机制成本完全不是一个量级。说了这么多还是那句话嵌入式这个行当没有捷径但有很多前人踩过的坑可以避开。上面这些内容如果你能吸收一半并且在下一个项目里实际用上相信你也会感受到那种从只会点灯到能hold住整个系统的质变。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表