ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

8051 Bootloader串口升级实战:IAP、Flash分区与中断向量处理

8051 Bootloader串口升级实战:IAP、Flash分区与中断向量处理 简介这是一份面向8051嵌入式开发者的C51 Bootloader实现资源基于Keil C51环境覆盖了 Bootloader 从启动初始化、自举检测、数据传输到程序加载与跳转执行的完整工程框架。资源共18个文件压缩包大小约17KB主要包含A51汇编启动代码、C语言核心源码、Keil工程备份与配置文件以及简要说明文档其中汇编文件负责复位后寄存器、堆栈及中断向量的初始化C源码实现外部存储读取、数据校验、应用区写入与跳转逻辑工程文件则帮助还原编译与调试环境。已有1530人学习适合需要为8位单片机增加固件在线升级能力的嵌入式工程师。从中可以掌握Bootloader各环节的设计思路包括UART等接口的数据接收、校验和验证、程序烧写与入口跳转同时借助Keil工程快速迁移到自身项目中减少底层调试工作量是一份可直接参考的 Bootloader 工程模板。 做嵌入式开发这些年Bootloader一直是老生常谈但真要在C51也就是8051上从零实现一套能搜到的完整资料确实不多。最近一个项目要求已出货的设备不拆机、直接通过串口做固件升级主控用的是STC15系列增强型8051开发环境是Keil C51我硬着头皮把这套Bootloader方案从内存划分、IAP驱动、串口协议到中断向量处理全部做了一遍。整个过程最深的感受是8051做Bootloader比ARM平台麻烦不少因为它没有VTOR寄存器帮你映射中断向量Flash擦写也完全依赖芯片的IAP机制。这篇博文把这个方案的关键设计思路和踩坑记录整理出来给同样在8051上做在线升级的朋友做个参考。1. 先搞懂C51 Bootloader的适用边界1.1 传统8051做不了在线自更新很多刚开始接触这个问题的朋友第一反应是“Bootloader不是所有单片机都能做吗”其实不是。经典8051比如AT89C51、89C52这类老芯片片内Flash本身不具备自擦写能力更不存在引导区跳转的概念。就算你写了Bootloader放进去Boot程序也无法在运行时把App区域擦掉再重新写入因为代码空间和可修改的存储器空间在芯片物理设计上就不支持这个动作。真正能玩转Bootloader的是带IAPIn-Application Programming能力的增强型8051。STC15系列、STC8系列、新唐N76E003、Silicon Labs的C8051F系列都属于这一类。它们内部多了一套IAP控制器通过几个特殊功能寄存器SFR发起对Flash的读、写、擦除操作。IAP控制器的本质是“CPU授权另一个硬件模块去改写Flash”所以即便是当前正在执行的代码也能通过这个机制重写另一块Flash区域。这就是C51 Bootloader能够成立的基础。顺便区分一个概念ISP和IAP不是一回事。ISP是芯片出厂时固化的引导程序用户串口下载器直接操作你平时用STC-ISP软件烧程序就是走这条链路。IAP则是应用程序运行期间自行擦写Flash的能力Bootloader要利用的正是IAP而不是依赖出厂ISP。很多资料把这两者混着讲实际开发时一定要分清楚Bootloader要自己控制Flash擦写不能用厂家的ISP监控程序去代劳。1.2 Bootloader要解决的现实问题Bootloader真正要解决的问题很朴素产品已经发到客户现场固件有Bug要修、功能要升级总不能要求客户把设备拆回来用编程器刷。通过串口、CAN等接口把新固件送进设备指定区域再由设备自身完成Flash更新这就是Bootloader的价值。用在我这个项目里就是设备通过RS-485总线组网运维人员在电脑上发一个升级指令整条总线上所有节点就能逐个完成固件升级单台拆装时间从半小时降到了两三分钟。与此同时Bootloader还承担了另一个隐藏职责升级过程中的容错。如果升级中途断电或者数据传错设备不能直接变砖。所以Bootloader在设计上要考虑分区备份、校验失败回退这些兜底策略。这些不是锦上添花而是产品化必须要过的门槛。2. Flash分区与Keil工程配置这是地基2.1 Boot放在低端还是高端直接影响中断处理复杂度Flash空间划分是整个Bootloader方案的地基。8051的代码空间寻址范围通常是64KB但具体芯片的Flash大小不同比如STC15W408AS是32KB Flash新唐N76E003是18KB Flash。一般Bootloader占4KBApp占用剩余空间。关键问题是Boot放在低端还是高端方案一Boot放在低端也就是0x0000开始的区域App偏移到0x1000之后。这是Bootloader最早占领复位向量的天然选择芯片上电后CPU必然从0x0000取指Boot天然先运行。但麻烦在于8051的中断向量表固定在0x0003、0x000B、0x0013这些地址Boot占用了低端就等于把中断向量表也接管了。App从0x1000启动后其中断向量表在0x1003、0x100B等位置而CPU触发中断时只会去0x0003等固定地址找向量。这就必须做中断向量转发后面章节详细说。方案二Boot放在高端比如0xF000-0xFFFFApp从0x0000开始正常编译。这个方案避开了中断向量转发问题因为App的中断向量表就在低端固定位置和裸机开发完全一致。缺点是每次芯片复位先执行的是App而不是Boot所以App的启动代码必须主动检查一个升级标志判断是否需要跳转到Boot。如果App本身有严重Bug复位后根本跑不到检查标志的代码升级入口就彻底失效了。我最终采用了方案一。原因有两个一是升级入口必须100%可控不管App当前状态如何只要把升级命令通过串口发出来设备复位后Boot能立刻接管二是中断转发通过修改启动汇编其实不难实现做好一次后面所有项目都能复用。2.2 让Keil C51按你的地址布局编译确定Boot占低端4KB、App从0x1000开始之后Keil C51的工程配置要分成两套。Boot工程本身不需要特殊设置它天生从0x0000开始编译当作一个普通裸机程序写就行。App工程则需要做两处关键修改。第一处是Target选项里的代码偏移设置。在Keil uVision中打开App工程的Options for Target进入Target页面在Off-chip Code memory区域把Eprom的Start设置为0x1000Size设置为剩余Flash大小比如32KB芯片就是0x7000。保存后重新编译生成的HEX文件地址就会从0x1000开始中断向量表也随之落在0x1003、0x100B这些位置。第二处是启动文件的配合。App工程的STARTUP.A51默认会在复位后跳转到C51运行库的初始化入口这个流程不需要改因为App被跳转到时会从这里正常完成所有初始化包括寄存器清零、堆栈准备、C变量初始化最后才进入main函数。这里有个细节要注意Boot跳转App时不能直接跳转到App的main函数地址而要跳转到App复位向量在0x1000处的那条指令。因为App的启动代码需要先完成片上外设初始化和C运行环境准备直接进main会跳过这些前置步骤跑起来必然出问题。2.3 App侧代码的耦合改动App工程从0x1000偏移编译后代码本身通常不需要改但有一个问题要想清楚App如何知道该不该进入Boot由于Boot在低端先运行它可以在上电后主动检查是否收到升级命令如果没有收到就直接跳转AppApp并不感知Boot的存在。这是一种“被动升级”模式。另一种是“主动升级”模式App在main开头写一段软复位逻辑把标志写入一片RAM然后软复位进BootBoot检查到标志进入升级模式。两种模式可以结合无操作超时自动跳App有升级标志则等待帧数据。我个人推荐被动模式为主因为对App代码的侵入最小App完全不用关心Boot的存在升级流程全部由Boot和上位机配合完成。App这边唯一要做的是在编译时把代码偏移到0x1000。如果你的App还要跑IAP功能比如自己修改EEPROM备份区那就额外注意不要把地址写进Boot区域这个问题在联调时最容易踩。3. 串口升级协议设计把固件安全送进Flash3.1 帧结构设计的原则有了Flash分区基础接下来是通信协议。串口升级协议是整个方案的血管所有固件数据都靠它运输。设计协议时不能拍脑袋随便定帧格式要针对8051的资源特点做取舍。我用的帧结构是帧头命令地址数据长度数据块校验。帧头固定0xA5为什么用0xA5而不是0x55或者0xAA因为0xA5的二进制是10100101相邻位始终翻转对连续相同电平的周期性干扰不敏感用来做帧同步比较可靠。命令字段区分握手、擦除、写Flash、读回校验、跳转App这五类操作。地址是16位的Flash绝对地址。数据块长度我限制在一帧最多64字节这个值不是随便定的它受两个条件约束串口接收缓冲区的大小和Flash单次写入页的大小。8051片内RAM有限如果不做扩展RAMBoot的接收缓冲区用64字节很合理和Flash页大小STC15系列常见512字节一页但写操作按字节触发配合也方便。校验字段我用的是累加和一字节。可能有人问为什么不用CRC16累加和太弱了。在8051这种资源紧张的芯片上算CRC16每帧数据都要循环几百次Boot程序本身还承担着Flash擦写的职责CPU能省则省。如果走的是RS-485总线数据速率9600bps这种低速场景累加和误判率已经足够低。真要在噪杂工业环境用我会升级到CRC16但前提是上位机和Boot都扛得住计算开销。3.2 应答机制与超时重传串口升级是半双工还是全双工取决于你的物理层。我用的是RS-485半双工所以协议必须是一问一答的严格主从模式。上位机发送一帧Boot处理完回一帧ACK/NACK上位机再发下一帧。没有ACK的帧上位机等300ms超时后重发连续重发3次仍失败整个升级流程终止并上报错误。这个300ms超时不是随便来的。Boot收到帧后要执行Flash写操作写单个字节需要几十微秒到十几毫秒不等不同芯片差异很大擦除一个扇区则需要几十毫秒。取300ms既保证Boot有足够时间完成擦写又不会让上位机等待过久。在实际调的时候我建议把超时设成可配置参数方便现场调整。ACK和NACK的帧内容也要设计得有意义。NACK最好带上错误码比如0x01表示校验错误、0x02表示地址越界、0x03表示Flash操作失败。这样上位机在调试时能直接给出具体错误提示而不是干瞪眼。我在项目里用1字节错误码收到NACK的上位机直接弹窗显示错误原因排查效率高很多。3.3 擦除、写入与校验的时序配合协议设计里最容易被忽略的是擦除、写入、校验三步之间的时序关系。Flash这玩意儿有个物理特性写操作只能在已擦除的单元上进行所以每往一个扇区写数据之前必须先擦除整个扇区。我设计的升级流程是上位机先发送擦除扇区命令指定扇区地址Boot执行扇区擦除并返回ACK然后上位机逐帧发送写数据命令每帧64字节Boot按地址写入所有数据写完后上位机发送读回校验命令Boot把刚写入的区域读出来返回给上位机比对。为什么要单独做读回校验因为写完数据直接跳转App太冒险了。Flash写入偶尔会出现个别字节未能正确写入的情况电压不稳、干扰、擦除不彻底系统性的校验能把这些概率性错误拦在跳转之前。校验通过后上位机再发跳转App命令Boot收到后把升级标志写入一个固定RAM地址做软复位新的固件就开始运行。每次升级结束后Boot还会把CRC校验码保存在Flash的末尾区域App启动时可以自行校验固件完整性发现不对就回退到Boot这个兜底措施值得做。4. 中断向量转发与App跳转Bootloader的咽喉要道4.1 8051没有VTOR中断怎么重定向这是C51 Bootloader和STM32方案分水岭的地方。STM32有VTOR寄存器可以把中断向量表整体搬到任意地址Boot跳App之前设置一下VTORApp的中断逻辑完全不用改。8051可没这个寄存器中断向量表硬件固定在低地址Boot占了低地址后那个区域就是Boot的向量表App的中断处理函数根本接不到中断。解决办法是中断向量转发。思路很简单中断来了CPU还是去0x0003、0x000B等地址取指这些地址在Boot区域。Boot在这些位置放一条LJMP指令直接跳到App向量表对应的位置。比如外部中断0CPU进入0x0003这里的指令是LJMP 0x10030x1003处是App编译器自动生成的向量表条目里面放着LJMP到App外部中断0的中断服务函数地址。这样中断经过两级LJMP跳转最终进入App的ISR开销只有4个机器周期对绝大多数中断系统来说完全可接受。具体实现不用在C代码里做直接改Boot工程的STARTUP.A51汇编文件最干净。在中断向量地址段写入类似这样的代码CSEG AT 0x0003 LJMP 0x1003 ; 外部中断0 CSEG AT 0x000B LJMP 0x100B ; 定时器0 CSEG AT 0x0013 LJMP 0x1013 ; 外部中断1 CSEG AT 0x001B LJMP 0x101B ; 定时器1 CSEG AT 0x0023 LJMP 0x1023 ; 串口中断这样处理之后Boot自身就不要再声明任何C51的interrupt中断函数了因为启动文件已经把这些向量地址占满。Boot需要接收串口数据我用轮询方式处理串口标志位中断完全交给App。这里我要强调一下Boot的串口接收不要用中断就老老实实轮询RI标志。因为Boot在擦写Flash期间要关全局中断中断方式很容易出现数据丢失或状态错乱轮询加超时机制反而最稳定。4.2 跳转App时的现场清理中断转发搞定之后跳转App的代码看似只有几行但现场清理不彻底App跑起来就是各种诡异问题。完整跳转流程是这样的先关闭全局中断EA0然后关闭所有外设中断使能位并把中断标志位清零再重置栈指针SP最后通过函数指针跳转到0x1000地址。关中断这一步没问题但很多人会漏掉中断标志位清理。看一种常见场景串口正在接收数据收到最后一个字节时触发了串口中断此时EA1CPU响应中断进入中断服务程序。如果Boot关中断太晚或者中断服务程序没有完整处理挂起事件中断标志位可能还保持着。跳转App之后App第一件事就是初始化外设、使能中断此时那个残留的中断标志位立刻触发一次假中断App在调用栈还没建立完整的情况下进入ISR跑飞概率极高。重置SP也同样关键。Boot运行时栈可能已经用了不少深度直接带着这个SP跳进AppApp的局部变量和函数调用栈会撞上Boot残留的栈数据轻则行为异常重则直接跑飞。跳转时把SP设置成复位值0x07让App从零开始建立自己的栈空间。void jump_to_app(void) { EA 0; IE 0x00; IP 0x00; TCON 0x0F; // 清定时器和外部中断标志 SCON 0x3F; // 清串口标志 SP 0x07; // 重置栈指针 ((void (code *)(void))0x1000)(); }跳转目标0x1000是App的复位向量那里放着一条LJMP指令指向App的启动代码。前面我说过一定要跳到复位向量而不是直接跳到App的main函数因为C51运行库的初始化工作要从启动代码走一遍。4.3 IAP操作的时序与中断配合接下来是IAP操作本体的实现。以STC15系列为例IAP通过一组SFR控制包括地址寄存器、数据寄存器、命令寄存器、触发寄存器。操作顺序是关闭全局中断、使能IAP控制器、写入目标地址、写入命令字、执行触发序列、等待操作完成、关闭IAP控制器。最关键的时序约束是触发序列。STC15要求向IAP_TRIG寄存器依次写0x5A、0xA5两个值这个序列不能被打断。如果中间来了个中断第二个值没有被及时写入这次IAP操作就失效了。所以IAP操作的临界区保护必须做最简单的做法就是全程EA0等Flash操作完全结束再恢复。因为Flash擦写时间最长可能到几十毫秒这期间中断全部丢失所以在做协议设计时一个扇区擦除加多帧写入期间不要依赖中断收发数据。sfr IAP_DATA 0xC2; sfr IAP_ADDRH 0xC3; sfr IAP_ADDRL 0xC4; sfr IAP_CMD 0xC5; sfr IAP_TRIG 0xC6; sfr IAP_CONTR 0xC7; void IAP_WriteByte(unsigned int addr, unsigned char dat) { EA 0; IAP_CONTR 0x80; IAP_CMD 0x02; IAP_ADDRH addr 8; IAP_ADDRL addr 0xFF; IAP_DATA dat; IAP_TRIG 0x5A; IAP_TRIG 0xA5; // 等待操作完成 EA 1; IAP_CONTR 0x00; }写到这里Bootloader的核心技术链路其实就通了Boot上电检查升级标志没有就跳App有就进入升级流程通过串口协议把固件逐帧写入Flash写完后校验通过软复位跳回App。这个主干跑通整个方案就成了一大半。5. 实测中的坑与排查思路5.1 跳转后App跑飞先查SP和中断残留我第一次把Boot和App联调时遇到最典型的问题就是跳转后App跑飞。现象是Boot正常跳转指令一执行App过几分钟就死机有时候甚至启动就直接异常。排查思路走了不少弯路最后锁定在两个点上SP重置和中断标志清理。SP问题的原因是Boot跳转时没有把栈指针恢复成App期望的初始值。App编译时Keil C51会生成自己的启动代码其中可能包含对栈区的初始化逻辑但这里的初始化只会重置部分变量不会把硬件SP自动归零。如果Boot运行时SP已经漂移到0x30甚至更高App的局部变量和跨函数调用的压栈就会从很深的地址开始和Boot留下的残渣混在一起。中断残留问题更隐蔽。有一次我在Boot里做了串口轮询接收上位机发送完最后一个数据帧后串口的REN还在使能状态接收到的字节可能让RI标志置位。跳转App后App初始化串口并打开中断RI置位导致立刻进入串口中断服务程序此时App主流程还没走到预期位置自然就乱套了。这两个问题排查时建议用仿真器在跳转指令前后单步看SP寄存器和IE、SCON等SFR的值一眼就能看出问题。5.2 擦写Flash时看门狗复位第二个大坑是看门狗。设备原固件开启了看门狗Boot跳转后App继续使用看门狗本来没问题但Boot在擦写Flash期间IAP操作占用了几十毫秒如果Boot没有接管看门狗并定期喂狗设备会在擦写中途被看门狗强制复位升级永远完不成。处理方案有两类。一类是Boot不做任何外设初始化保持芯片上电默认状态这时候看门狗还没被开启天然没有这个问题。但如果Boot需要跑完升级流程再跳AppApp启动后统一喂狗则Boot期间没开启看门狗就是安全的。第二类是Boot主动接管看门狗在擦写Flash期间定时喂狗从Boot跳App时把看门狗状态完整交还。我推荐第一类方案前提是Boot程序里禁止开启看门狗App在升级完成后自行开启。实际测试时我会把擦写一个扇区的时间用示波器量出来然后和看门狗超时时间对比。STC15系列擦除一个扇区典型需要15-20ms如果系统看门狗超时是1秒那Boot擦写不会触发复位如果看门狗是几十毫秒的超时就必须考虑喂狗策略了。5.3 升级断电导致设备变砖的兜底方案最后一个要紧的问题是断电保护。如果上位机正在升级写到一半设备断电了Boot区还完好App区已经擦掉一半或只写了一半设备上电后会进入一种“半固件”状态。如果Boot跳转逻辑看到升级标志就去跳App跳过去也跑不起来设备就变砖了。我的兜底策略是双区备份和启动有效性校验。方案在Flash里划分三个区域Boot区、App运行区、App备份区。上位机升级时先把新固件完整写入备份区写入完成后做CRC校验校验通过后把分区交换标记写入Flash的末尾区域然后复位App启动时先检查这个标记如果标记有效则从备份区引导运行同时把运行区和备份区的指针交换。升级中断电备份区的数据不完整CRC校验失败设备仍然从运行区启动旧的固件继续工作这就是完整的回退机制。考虑到8051的Flash空间有限双区方案可能让可用程序空间减少一半不是每个项目都扛得住。折中做法是Boot区独立保留App区只有一个但App启动时先自校验固件CRC校验失败就在原地等待串口升级指令不跳转也不复位配合LED灯提示“固件异常”。这个方案在小Flash芯片上更现实只要Boot不坏设备就永远有救回来的通道。我在实际操作中的体会是Bootloader的坑往往不在Boot本身而在于对运行环境的假设不够谨慎。SP、中断标志、看门狗状态这三个东西在裸机上平时谁也不在意一旦涉及Boot和App两个程序的交接任何一个没有做好移交都会变成难查的暗病。如果你正在做8051的Bootloader建议先把这三点写在代码评审清单里等这几个关卡都守住剩下的串口协议和IAP细节都是按部就班的事。本文还有配套的精品资源点击获取
返回列表