关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞
文 | 无际(微信:2777492857)
全文约4976字,阅读大约需要 10 分钟
在2016年,我在做一个网关项目时,需要实现远程固件升级功能,也就是大家常听到的OTA。
当时,我对此一窍不通,Bootloader、IAP、OTA都是啥玩意?全部都是我的知识盲区。
公司也没人教,我只能硬着头皮去学习,记得之前对接的是云智易,整个项目前前后后搞了一年,才勉强做稳定,其实还有一些BUG,APP获取子设备信息不稳定,平台的协议没设计好,链路交互不够精简。
这个项目其中有个功能就是OTA,当时我啥也不懂,厚着脸皮,这里问问,那里搜搜,又从他们平台技术支持的嫖一点,在他爱搭不理的状态下,慢慢磨出来的经验。
一、Bootloader到底是个啥玩意儿?
简单来说,Bootloader就是单片机上电或复位后,在你的主应用程序(也就是你辛辛苦苦写的业务逻辑代码)运行之前,先跑起来的一小段特殊程序。
你可以把它想象成电脑开机时的BIOS/UEFI。电脑按下电源键,不是直接就进Windows或MacOS了对吧?得先有个东西检查下硬件,做点初始化,然后再去加载操作系统。
单片机的Bootloader干的也是类似的事情,只不过规模小得多,目标也更专一:初始化必要的硬件,然后决定是启动主应用程序,还是进入某种特殊模式(比如,准备接收新的固件)。
它通常被烧录在单片机内部或者外部Flash存储器的一个特定区域,这块区域往往还带有写保护,防止被应用程序不小心破坏掉。毕竟,如果连Bootloader都挂了,那设备很可能就真的“启动不能”了,到时候返厂维修,那成本可就上去了。
二、为啥我们需要Bootloader?直接跑应用它不香吗?
直接跑应用当然行,很多简单的项目就是这么干的。代码编译、烧录、运行,一条龙。但问题来了,如果你的产品已经卖出去了,部署到了天南海北,这时候发现一个bug,或者想加个新功能,咋办?
总不能让用户把设备拆开,拿出J-Link/ST-Link,吭哧吭哧连上电脑,重新烧录吧?这用户体验也太差了,而且很多场景下根本不现实。
这时候,Bootloader的价值就体现出来了。它最重要的使命之一,就是实现固件的远程更新,也就是我们常说的IAP(In-Application Programming,在应用编程)或者更时髦的OTA(Over-The-Air,空中升级)。
有了Bootloader,我们就可以通过预留的通信接口(比如串口UART, USB, CAN, 以太网, 甚至无线方式如蓝牙, Wi-Fi, LoRa, NB-IoT等),给设备发送新的应用程序固件。Bootloader负责接收这些固件数据,把它写入到应用程序的存储区域,然后重新启动,加载新的程序。
总结一下Bootloader的核心优势:
1.固件更新能力:无需物理接触和专用烧录工具,即可更新设备功能或修复bug,极大降低维护成本。
2.灵活性:产品可以先发布,后续通过软件升级迭代。
3.可靠性(如果设计得好):可以设计成即使应用程序损坏,也能通过Bootloader进行恢复。
所以你看,对于需要持续维护和升级的产品来说,Bootloader不是可有可无的点缀,而是实实在在的刚需。
三、Bootloader的工作原理
咱们来深入一点,看看Bootloader内部是怎么运作的。
1. 启动流程:
?单片机上电或复位。
?CPU根据内部设定,从一个固定的地址(通常是Flash的起始地址,也就是复位向量表所在位置)取出第一条指令。
?如果Bootloader被放置在这个起始地址,那么CPU就开始执行Bootloader的代码。
2. 硬件初始化:
?Bootloader不需要像主应用程序那样初始化所有的外设。它只需要初始化最基本的硬件,满足它的核心功能需求即可。
?这通常包括:
?时钟系统:让CPU和外设跑起来。
?GPIO:可能需要用来检测某个引脚状态,判断是否进入升级模式,或者控制LED指示状态。
?通信接口:用于接收新固件的接口,比如UART, SPI, I2C, CAN, USB等。
3. 决策逻辑:跑应用还是搞升级?
这是Bootloader的一个关键环节。初始化完成后,它需要决定接下来干什么。常见的判断方式有:
?检测特定GPIO引脚:比如,上电时长按某个按钮,Bootloader检测到这个引脚是低电平,就进入升级模式。
?检查共享内存区域的标志位:应用程序可以在某个不易失存储(如Flash的特定扇区)写入一个标志,请求下次启动时进入升级模式。Bootloader启动时读取这个标志。
?通信接口等待:启动后,在一定时间内(比如1-2秒)监听某个通信接口,如果收到了特定的“进入升级”指令,就进入升级模式;否则超时后就去启动应用程序。
?检查应用程序有效性:计算应用程序区域的校验和(Checksum/CRC),如果校验和错误或者程序区域为空,可能强制进入升级模式。
这个决策过程,有点像Bootloader每天都要面对的灵魂拷问:“今天,我是该跳转到应用,还是留下来等等更新?”
4. 跳转到应用程序:
如果决定运行主应用程序,Bootloader需要做几件重要的事情(以ARM Cortex-M为例):
?设置主堆栈指针 (MSP):应用程序有自己的堆栈,Bootloader需要从应用程序的向量表(通常位于应用程序Flash区域的起始处)的第一个字读取MSP的初始值,并设置到CPU的MSP寄存器。
?获取复位处理函数地址:应用程序的入口点不是main函数,而是复位处理函数(Reset_Handler)。这个函数的地址通常存储在应用程序向量表的第二个字。
?执行跳转:获取到地址后,通过函数指针或者汇编指令,直接跳转到应用程序的Reset_Handler执行。
注意:如果应用程序使用了中断,还需要考虑中断向量表的重定向问题。通常通过修改NVIC的VTOR(Vector Table Offset Register)寄存器,将中断向量表的基地址指向应用程序的向量表。这一步可以在Bootloader跳转前完成,也可以由应用程序自己初始化时完成。
5. 进入升级模式 (IAP过程):
如果决定进入升级模式,Bootloader会:
?初始化通信接口:准备接收数据。
?遵循预定义的通信协议:与上位机(发送固件的工具或服务器)进行交互。协议可能很简单(比如,握手->发数据块->应答->结束),也可能比较复杂(带加密、分包、重传机制)。
?接收固件数据:通常按块(Packet/Frame)接收。
?数据校验:每接收一块数据,都要进行校验(如CRC校验),确保数据在传输过程中没有损坏。
?擦除Flash:在写入新数据之前,必须先擦除应用程序占用的Flash扇区。Flash存储器的特性是写之前必须先擦除,而且通常是按扇区(Sector)或块(Block)擦除的。
?写入Flash:将校验通过的数据块写入到Flash的对应位置。
?状态反馈:向上位机报告接收和写入的状态(成功、失败、重传请求等)。
?完成与验证:全部数据接收并写入完成后,可能会进行一次整体的固件校验。然后向上位机发送完成信号。
?复位:升级完成后,通常会触发一次软件复位,让CPU重新从头开始执行。这次,Bootloader检查发现无需升级(或者升级标志已被清除),就会跳转到刚刚写入的新应用程序。
四、Bootloader, IAP, OTA,别再傻傻分不清
这三个词经常一起出现,关系紧密但含义不同:
?Bootloader:是那段程序本身,它提供了加载应用程序和执行固件更新的基础能力。
?IAP (In-Application Programming):是一种技术或过程,指的是在设备运行状态下(通常是在Bootloader的引导下,或者有时甚至是应用程序自己调用特定代码)对自身的程序存储器进行擦写,以达到更新固件的目的。
?OTA (Over-The-Air):是一种固件交付方式,特指通过无线通信(Wi-Fi, Bluetooth, 蜂窝网络等)将新的固件包发送到设备。设备接收到OTA包后,通常会利用其IAP能力来完成实际的烧录更新。
所以,可以说:OTA是实现远程固件更新的一种高级方式,它依赖于设备的IAP能力,而IAP能力的实现往往离不开一个健壮的Bootloader。
五、动手写一个简单的Bootloader?
想自己动手写一个Bootloader?完全可以!虽然细节不少,但核心思路是清晰的。你需要关注以下几个方面:
1. 内存规划
你需要精确规划Flash的内存布局。哪部分给Bootloader,哪部分给应用程序,可能还需要一块用于Bootloader和应用程序通信的共享区域(比如存放升级标志)。
这通常通过修改编译器的链接脚本来实现。你需要告诉链接器,Bootloader的代码和数据放在哪里,应用程序的代码和数据从哪个地址开始放。
例如,我们可以在Keil里面设置APP程序从哪个地址开始放。
eqgetu1i0qc64018888020.png
通过上图设置,告诉编译器,我们的应用程序代码是从0x800C800这个地址开始,而Bootloader的,则是从0x8000000开始。
qw41ggghpmz64018888120.png
2. 通信协议设计
拿我们无际单片机项目6举例,这个项目实现了OTA升级,设备通过WiFi和4G连接我们开发的云平台。
1i2imh2g1nk64018888220.png
WiFi和4G之间通过MQTT,而单片机到WiFi和4G模块之前通过串口通讯。
uehku5zhwev64018888320.jpg
这样,就需要设计一个串口协议。
12p0ozyrk0w64018888421.png
2zkkgvlbdd264018888521.png
1m053wlcut364018888621.png
例如,定义几个命令:
?查询服务器是否有新的固件版本(0x21,0x22)
?MCU申请获取OTA升级固件内容(0x24,0x25)
3. 核心逻辑实现
Bootloader的main函数:
?初始化时钟、GPIO、通信接口。
?检查是否需要进入升级模式(CheckIfUpdateRequest())。
?如果需要升级,调用 EnterUpdateMode()。
?如果不需要升级,调用 JumpToApplication()。
跳转函数的实现(C语言示例,ARM Cortex-M):
#define APP_ADDRESS (uint32_t)0x800C800 // 假设应用程序起始地址typedef void (*pFunction)(void); // 定义函数指针类型void JumpToApplication(void){ uint32_t jumpAddress; pFunction applicationEntry; // 检查应用程序起始地址是否有效 (至少包含向量表) if (((*(volatile uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) { // 1. 获取应用程序的堆栈顶地址 (向量表第一个字) uint32_t appStack = *(volatile uint32_t*)APP_ADDRESS; // 2. 设置主堆栈指针 MSP __set_MSP(appStack); // 3. 获取应用程序的复位处理函数地址 (向量表第二个字) jumpAddress = *(volatile uint32_t*)(APP_ADDRESS + 4); applicationEntry = (pFunction)jumpAddress; // 4. 跳转到应用程序的复位处理函数 applicationEntry(); } else { // 应用程序无效,可以在这里处理,比如闪烁LED报错 while(1) { } }}
升级模式的实现 (EnterUpdateMode()):
?循环等待接收命令。
?根据命令处理:擦除Flash、接收数据、校验数据、写入Flash。
?使用芯片厂商提供的Flash驱动库来执行擦写操作。注意处理好擦写的时序和错误。
应用程序的注意事项:
?应用程序的链接脚本也要相应修改,确保它被链接到正确的地址(APP_ADDRESS)。
?如果使用了中断,应用程序需要在初始化时设置NVIC的VTOR寄存器指向自己的向量表。
znl4bc3ox5s64018888721.png
六、从IAP走向OTA
实现了基本的IAP Bootloader,除了IAP的基础,OTA还需要考虑:
1. 无线通信栈
需要在Bootloader(或者一个专门的、可更新的下载器应用)中集成无线通信协议栈(如Wi-Fi, BLE)。这对Bootloader的大小和复杂度提出了更高的要求。有时会采用两阶段Bootloader,第一阶段极简,负责加载第二阶段(带通信能力)或应用程序。
2. 安全性
这是OTA的重中之重!
?固件签名与验签:防止恶意固件被刷入。服务器用私钥对固件签名,设备用公钥验证签名,确认固件来源可靠且未被篡改。
?传输加密:防止固件在传输过程中被窃听或篡改,可以使用TLS/DTLS等安全传输协议。
?安全存储:密钥的安全存储也是个挑战。
3. 健壮性
?断点续传:无线环境不稳定,传输中断是常事。需要机制能从中断的地方继续传输。
?电源管理:升级过程中断电怎么办?需要保证设备不会变砖。常用的方法是“双备份区(Dual-Bank)”或者使用日志/状态记录来确保更新的原子性(要么完全成功,要么回滚到旧版本)。
?版本管理与回滚:如果新版本有问题,需要有机制能回滚到上一个稳定版本。
4. 后端支持
需要有服务器来存储固件、管理设备、推送更新任务,比如我们无际单片机的项目,通过自研的云平台,配合上自己设计的协议,就能打造一套完整的物联网OTA系统。
七、Bootloader开发的“坑”
开发Bootloader时,要特别小心,因为一旦出错,后果可能很严重。几个常见的“坑”和建议:
?大小限制:Bootloader占用的空间越小越好,给应用程序留出更多空间。代码要精简,避免引入不必要的库。
?稳定性压倒一切:Bootloader是设备的“最后防线”,必须极其稳定。充分测试各种边界条件和异常情况(如通信错误、Flash擦写失败)。
?别忘了看门狗(Watchdog):在Bootloader执行擦写Flash等耗时操作时,一定要记得“喂狗”,防止看门狗超时导致复位,让设备陷入无限重启的循环。
?日志与调试:Bootloader调试起来比应用程序困难,因为它运行在系统最早期。可以利用一个UART输出最关键的日志信息,或者通过调试接口(JTAG/SWD)单步跟踪。但要小心,连接调试器可能会影响某些硬件的正常初始化。
?保护机制:Flash的写保护功能一定要用起来,保护Bootloader自身不被意外修改。
?版本意识:Bootloader和应用程序都需要有明确的版本号,方便管理和问题追溯。升级时可能需要进行版本兼容性检查。
好了,关于Bootloader的原理、应用到OTA的演进,我们就聊到这里。希望这篇有点啰嗦但力求实在的文章,能帮你揭开Bootloader的神秘面纱。它虽然只是一段“开场前的小程序”,但却肩负着让设备具备“进化”能力的关键使命。从简单的串口IAP到复杂的安全OTA,Bootloader技术的发展也见证了嵌入式系统功能的不断强大。
掌握Bootloader,不仅能让你在项目开发中更加游刃有余,也是衡量一个单片机工程师能力的重要标尺之一。
end
arpz2b32q2y64018888821.jpg
下面是更多无际原创的个人成长经历、行业经验、技术干货。
1.电子工程师是怎样的成长之路?10年5000字总结
2.如何快速看懂别人的代码和思维
3.单片机开发项目全局变量太多怎么管理?
4.C语言开发单片机为什么大多数都采用全局变量的形式?
5.单片机怎么实现模块化编程?实用程度让人发指!
6.c语言回调函数的使用及实际作用详解
7.手把手教你c语言队列实现代码,通俗易懂超详细!
8.c语言指针用法详解,通俗易懂超详细! |