DSP串口在线升级:Flash双区乒乓与BootLoader避坑指南
发布时间:2026/11/2 9:21:54 锦皓数字建站

简介面向嵌入式DSP开发与工业控制领域工程师的技术文档围绕TMS320F2811程序在线升级展开针对传统依赖JTAG仿真器烧写、需开壳操作、后期维护困难等痛点给出基于串口的BootLoader升级方案。文中从Flash地址空间重新划分入手说明128K×16存储区10个Sector的区段用途规划、双CCS工程辅助升级程序与待升级程序的软件模块划分并详细讨论引导程序流程、A/B有效区标志检测、擦除异常时的回滚机制以及上位机与MCU之间的串口通信协议设计配有系统工作原理框图、地址划分表和主程序流程图可直接对照移植到同类C2000系列芯片项目。压缩包共1个文件为239KB的PDF文档篇幅紧凑便于打印或随设备归档查阅。已有126人学习适合从事DSP固件开发、希望为产品补充远程升级能力的工程师参考。1. 机壳封死之后DSP 在线升级为什么必须走串口这条慢路DSP 在线升级这件事说到底是给已经出货的板子留一条不拆壳的维修通道。设备装在控制柜里机壳封死前面板只有一个 DB9为了改三行逻辑就得拆盖、接仿真器、重烧、再装回去——这种活干过一次就不想干第二次。TMS320F2811 这类老型号 DSP 的常规烧写路径是 JTAG产线调试没问题到了现场维护就是灾难。这里要复现的方案核心是把 128K×16 的片内 Flash 切成「引导区 两个程序区 一个标志区」让芯片上电后自己去读标志、跳到当前有效区需要升级时通过串口把待升级的 Intel HEX 数据一帧一帧打进另一个空闲区写完翻转标志、重启生效。适合手里有 F2811 存量板、又必须做远程或现场维护的人也适合拿它当 BootLoader 入门样本的嵌入式新手——代码量不大链路完整坑还特别典型。2. Flash 十区重排A/B 双区乒乓与一个 2 字宽的有效标志2.1 原厂实际扇区布局和它的代价F2811 的片内 Flash 是 128K×16 位按扇区Sector划分成 10 块从 A 到 J。它的特点是扇区大小不均匀A、B、I、J 各 8K×16C 到 H 各 16K×16。擦除的最小单位就是扇区不能按字擦这个物理约束决定了后面所有分区方案的形状。区段地址范围容量A0x3F6000 – 0x3F7FF68K×16B0x3F4000 – 0x3F5FFF8K×16C0x3F0000 – 0x3F3FFF16K×16D0x3EC000 – 0x3EFFFF16K×16E0x3E8000 – 0x3EBFFF16K×16F0x3E4000 – 0x3E7FFF16K×16G0x3E0000 – 0x3E3FFF16K×16H0x3DC000 – 0x3DFFFF16K×16I0x3DA000 – 0x3DBFFF8K×16J0x3D8000 – 0x3D9FFF8K×16如果直接把整个应用塞进连续的扇区串口升级就会出现一个无解的死局擦除是破坏性的擦到一半掉电程序区就是半新半旧板子既跑不起来也没法再进升级流程。JTAG 之所以让人放心是因为仿真器可以在芯片「已经废了」的状态下重新灌程序串口升级没有这条退路所以必须用空间换安全——同一时刻Flash 里至少有一份完整可运行的镜像。这也是很多人第一次做 IAP 翻车的地方只切了一个程序区升级过程中一断电就只能返厂。双区乒乓不是过度设计是这种芯片上唯一能自洽的结构。2.2 重排方案J 段放标志D–E 与 G–H 各放一套程序按功能的划分方式如下A 段和 J 段是「不动产」其余全部可换。区段用途说明ABootloader 程序入口复位后第一条跳转永不被擦除BBoot_API 函数库擦写 Flash 的底层函数封装成 .lib 供两套程序共用Cc_int00 函数入口运行库启动代码的落点D–E辅助升级程序工程 A负责串口收发、解析 HEX、驱动擦写G–H待升级程序工程 B真正跑业务的用户程序J有效区标志只占头 2 个 16 位字其余保留F / I预留留作参数区或后续扩展这个分配里最关键的一点Bootloader 只占 A 段 8K它不参与任何一次升级也不允许被上位机的擦除命令碰到。它的职责被压到极小——上电后初始化时钟和串口读 J 段标志校验目标区首地址是否落在合法范围、校验字是否成立然后跳过去。所有复杂业务都丢给 D–E 段的辅助升级程序。为什么辅助升级程序和待升级程序要分两套因为升级动作本身也需要代码来执行。如果只保留一套用户程序那么升级到一半它自己被擦掉就没有任何代码能继续处理串口了。把「执行升级的那段代码」单独放在 D–E 段升级 G–H 的时候它自己在跑逻辑才闭环。2.3 有效标志的读写与自动回滚J 段的前两个 16 位字用来存标志。第一个字存魔数第二个字存区码两个都对得上才算有效只判断区码的话Flash 擦除后默认值 0xFFFF 会被误判成合法值这是最阴的一个坑。/* 有效区标志定位在 J 段起始链接脚本里必须与 .cmd 的 BOOT_SECTION 一致 */ #pragma DATA_SECTION(ActiveSecFile, BOOT_SECTION) volatile Uint16 ActiveSecFile[2] {0xAAAA, 0x0001}; #define SEC_A 0x0001 /* 当前运行 D-E 段辅助升级程序 */ #define SEC_B 0x0002 /* 当前运行 G-H 段用户程序 */ Uint16 Get_Active_Section(void) { Uint16 magic ActiveSecFile[0]; Uint16 flag ActiveSecFile[1]; /* 魔数不对或区码越界一律按 A 区兜底绝不让板子停在非法状态 */ if (magic ! 0xAAAA || (flag ! SEC_A flag ! SEC_B)) { return SEC_A; } return flag; } void Set_Active_Section(Uint16 sec) { /* 先擦 J 段整个 8K再按「魔数 区码」的顺序写回顺序不能反 */ Flash_Erase((Uint16 *)0x3D8000, 0x2000); ActiveSecFile[0] 0xAAAA; ActiveSecFile[1] sec; Flash_Program((Uint16 *)0x3D8000, (Uint16 *)ActiveSecFile, 2); }Get_Active_Section的返回值直接决定跳转目标所以它必须做三重校验魔数、区码取值范围、目标区入口地址是否可执行。参数sec只接受SEC_A或SEC_B两个值别在调用侧拼字符串或者传 0/1一旦有人传了 0写进去的标志就成了非法值下次上电要走兜底分支。Set_Active_Section里擦除长度写的是0x2000也就是 J 段的完整 8K不是 2 个字。原因很直接Flash 只能按扇区擦写之前必须先擦哪怕你只改 2 个字。这一步如果写成「只擦 2 个字」函数会返回错误但很多人不检查返回值最后的表现是标志没更新、重启还是老程序——这是我在现场遇到频率最高的一类「升级成功但没生效」。回滚逻辑藏在上电流程里。Bootloader 读标志假设读到 A 区就去校验 D–E 段的首地址和校验字如果 D–E 段因为上次擦除中途掉电而损坏Bootloader 会自动改跳到 G–H 段由那边的程序重新把辅助升级程序写回 D–E。同理如果标志指向 B 而 G–H 段已损坏就退回 A 区等待重新下发。这套互救机制的正确性完全依赖「两套程序不会同时被擦」这个前提所以擦除命令里必须先判断目标扇区编号命中当前运行区就直接拒绝并返回错误码不能交给上位机去保证。3. 把段钉死在地址上.cmd 定位脚本与 hex2000 转换链路3.1 BOOT_SECTION / B_SEC_ADDR / B_SEC_VER 三段定位C 语言里声明一个变量它的地址是链接器分配的跟你想放的扇区没有任何关系。要让它精确落在 J 段头部只能靠.cmd链接命令文件里的MEMORY和SECTIONS两段配合。MEMORY { PAGE 0 : /* J 段头部两个 16 位字存有效区标志两个工程的 .cmd 必须写成同一地址 */ BOOT_SECTION : origin 0x3D8000, length 0x000002 /* B 区入口记录目标区首地址 校验字升级完成后由上位机回读确认 */ B_SEC_ADDR : origin 0x3E7FF0, length 0x000002 /* B 区版本号3 个 16 位字里只用最低一个 */ B_SEC_VER : origin 0x3E7FF2, length 0x000001 /* G~H 段主代码区长度要避开上面两个小段否则链接直接报溢出 */ FLASHH : origin 0x3E0000, length 0x003FF0 } SECTIONS { .cinit : FLASHH PAGE 0 .pinit : FLASHH PAGE 0 .text : FLASHH PAGE 0 .const : FLASHH PAGE 0 .econst : FLASHH PAGE 0 .switch : FLASHH PAGE 0 ActiveSecFile : BOOT_SECTION PAGE 0 BSecAddr : B_SEC_ADDR PAGE 0 BSecVer : B_SEC_VER PAGE 0 }origin就是绝对地址length是这段地址允许占用的字数。FLASHH的length写0x003FF0而不是0x004000就是为了给B_SEC_ADDR和B_SEC_VER让出末尾 16 个字。这个长度差一旦写错链接器会提示段重叠但也有人直接改小B_SEC_ADDR的地址把冲突「绕过去」结果就是版本号段落进了代码区运行期被代码覆盖回读出来的版本号永远是乱的。ActiveSecFile必须定义成volatile否则在开了优化的情况下链接器认为这个变量只写不读可能整个段被裁掉map文件里根本找不到它写标志那一行就变成了往一片野地址写数据。3.2 两个工程必须对齐的三处工程 A辅助升级程序烧在 D–E和工程 B用户程序烧在 G–H是两个独立的 CCS 工程各自有一份.cmd。它们之间有三处必须严格对齐错一处就升不上去第一处是BOOT_SECTION。两个工程里这段的origin和length必须完全一样因为标志是共享的。有人图省事在工程 B 里把这段改到自己的区里结果就是升级完成后标志写进了 G 段Bootloader 在 J 段读到的还是老值。第二处是B_SEC_ADDR和B_SEC_VER的含义。这两个段只对工程 B 有意义工程 A 的.cmd里可以删掉但删之前要确认链接器没有别的段占用那块地址。第三处是c_int00的入口位置。段 C 里放的是运行库启动代码Bootloader 跳过去的时候用的就是它的首地址所以这个地址在两次编译之间不能变。稳妥的做法是在.cmd里把c_int00所在的段固定到0x3F0000起始然后写死一个常量在 Boot 里而不是每次从map文件里手工抄。3.3 hex2000 的四个宽度参数与批处理封装CCS 编译出来的是.out文件一种带符号和段信息的 ELF/COFF 格式MCU 端的解析器没必要去啃这种格式。转成 Intel HEX 是行业惯例格式简单、每行自带地址和校验和串口传输出错也容易定位到具体行。工具用随 CCS 一起装的hex2000.exe参数写在一个命令文件里first.out -i -memwidth 16 -romwidth 16 -datawidth 16 -o dsp_update_B.hex四个参数逐个说。-i指定输出 Intel HEX 格式不加这个参数默认输出的是 TI-Tagged 格式行首带q、b之类的标记MCU 端按 Intel 格式解析会全部失败。-memwidth 16告诉工具目标存储器的寻址宽度是 16 位这直接决定 HEX 行里的地址是按字递增还是按字节递增——F2811 是 16 位字寻址这里写成 8 会让地址整体翻倍烧进去的程序从第二条指令开始就是错的。-romwidth 16和-datawidth 16是输出 ROM 的位宽和数据位宽三个 16 必须一致任何一个是 8都会触发工具内部的字节拆分生成一份看起来正常、烧进去跑飞的 HEX。外层用批处理封装把转换和错误判断放在一起避免有人双击完看都不看就去烧echo off REM 与 hex2000.exe、program.cmd、first.out 放在同一目录下执行 hex2000 program.cmd if errorlevel 1 ( echo [ERROR] hex conversion failed, check program.cmd pause exit /b 1 ) echo [OK] dsp_update_B.hex generated pauseerrorlevel判断不能省。hex2000在输入文件缺失、段溢出、参数非法的时候都会返回非零但它不会弹窗只是命令行一闪而过。少了这个判断最后拿到的是一个上次编译留下的旧 HEX烧进去当然「升级成功」只是升的还是老版本。3.4 产物核对HEX 首行和 map 文件转换完不要急着烧做两个核对。第一打开 HEX 文件看第一行数据记录的地址字段。工程 B 的代码起始地址应该是0x3E0000转换成 Intel HEX 的记录地址就应该是3E00Intel HEX 的地址字段是字节地址对于 16 位字寻址的芯片工具会自动处理扩展地址记录。如果首行地址是3D80或者0000说明.cmd里的段分配没生效链接器把代码放回了默认位置。第二翻map文件确认ActiveSecFile的地址是0x3D8000、BSecVer是0x3E7FF2。map文件里还有每个段的长度看一眼.text段是不是超过了FLASHH的0x003FF0——超了的话链接器会报警但报的是「段溢出」不会告诉你是哪个变量太大得回去查。4. 一帧一擦一写命令字、CRC 与 115200 下的超时账4.1 帧格式设计串口上跑的是二进制数据没有任何边界标记靠超时切帧在 115200 下极不可靠。所以每一帧都必须自带帧头、长度、校验和帧尾四个要素缺一不可。字段长度说明帧头2 字节固定0xAA 0x55双字节头比单字节抗干扰命令字1 字节见下表数据长度2 字节小端只算数据域不含帧头帧尾数据域N 字节N 上限 128配合 140 字节的整帧长度CRC162 字节小端校验范围为命令字到数据域末尾帧尾1 字节固定0x0D数据域上限压到 128 字节不是随便定的。F2811 的 Flash 一次写入 16 位但串口接收缓冲区通常只开到 128 或 256 字节帧太长会在擦写 Flash 关中断的那几百毫秒里溢出。128 字节对应 64 个 16 位字正好是一个可以接受的重传粒度。命令字只保留六个够用且好排查命令字名称数据域响应0x01ENTER_UPDATE无0x81 当前有效区1 字节0x02ERASE_REGION目标区标志1 字节0x82 结果0x03WRITE_BLOCK目标地址4 字节 数据0x83 结果0x04CHECK_CRC整区 CRC324 字节0x84 结果0x05READ_VERSION无0x85 版本号3 字节0x06JUMP_NEW目标区标志1 字节无直接重启4.2 五步命令序列与时序一次完整的升级是严格串行的五步上位机每发一帧都要等应答不能连续猛发。第一步ENTER_UPDATE。MCU 收到后把当前有效区码回给上位机上位机据此决定这次往哪个区写——如果当前跑 A 区就往 G–H 写跑 B 区就往 D–E 写。这一步是整个流程的安全阀上位机绝对不能自己猜目标区。第二步ERASE_REGION。MCU 收到后先做一次范围检查确认目标区不是当前运行区也不是 A 段 Bootloader检查通过才真正擦除。擦一个 16K×16 的扇区在 F2811 上大约需要几百毫秒上位机的等待超时不能按普通命令的 200ms 设建议单独给擦除命令 35 秒。第三步WRITE_BLOCK。从 HEX 里解析出来的数据按 128 字节切片每片配上目标地址。MCU 收到后先写 Flash 缓冲区再触发写入写完后回一个结果字节。这一步最忌讳上位机「发完就完事」必须等应答再发下一帧否则 MCU 的接收缓冲区在擦写期间会溢出表现为随机丢块、最后 CRC 对不上。第四步CHECK_CRC。上位机把整份 HEX 数据的 CRC32 发过去MCU 把刚写入的区域读回来算一遍比对一致才回成功。这一步是唯一能证明「数据真的落到 Flash 里」的环节跳过它就只能靠重启后程序跑不跑来验证太晚。第五步JUMP_NEW。MCU 更新 J 段标志然后触发软复位或者直接跳到新程序入口。跳转前要关掉全局中断、关看门狗、把用到的外设恢复到复位状态否则新程序的c_int00会在一个被污染的环境里跑起来。4.3 上位机侧的 HEX 解析与组帧上位机用 Python 写最省事pyserial加一个 CRC 函数就够了。关键是把 Intel HEX 的扩展地址记录处理好很多解析器只认类型 00 记录遇到类型 04 就跳过结果地址超过 64K 的部分全部错位。import serial import time FRAME_HEAD b\xAA\x55 FRAME_TAIL b\x0D def crc16(buf): 与 MCU 端一致的 CRC16/MODBUS初值 0xFFFF多项式 0xA001 crc 0xFFFF for b in buf: crc ^ b for _ in range(8): crc (crc 1) ^ 0xA001 if crc 1 else crc 1 return crc 0xFFFF def build_frame(cmd, payloadb): 命令字 长度 数据 参与校验帧头帧尾不参与 body bytes([cmd]) len(payload).to_bytes(2, little) payload return FRAME_HEAD body crc16(body).to_bytes(2, little) FRAME_TAIL def parse_hex(path): 返回 [(线性地址, 数据bytes)]类型 04 记录负责切换高 16 位基址 records, base [], 0 with open(path, r, encodingascii) as f: for line in f: line line.strip() if not line.startswith(:): continue raw bytes.fromhex(line[1:]) n raw[0] addr (raw[1] 8) | raw[2] rtype raw[3] data raw[4:4 n] if rtype 0x00: # 数据记录 records.append((base addr, data)) elif rtype 0x04: # 扩展线性地址 base ((data[0] 8) | data[1]) 16 elif rtype 0x01: # 文件结束 break return recordscrc16的初值和多项式必须跟 MCU 端逐位一致两边各自实现、各算各的是校验失败的头号原因。parse_hex里把类型 04 的扩展地址左移 16 位累加到基址上这一步对应 F2811 的 0x3E0000 这类地址漏掉就会把整份数据当成从 0x0000 开始。下发循环的骨架如下重点是每帧之后必须读应答def send_and_wait(ser, cmd, payloadb, timeout0.2): ser.write(build_frame(cmd, payload)) ser.timeout timeout resp ser.read(8) # 应答帧定长 8 字节 if len(resp) 8 or resp[3] ! 0x80 | cmd: # 应答命令字 请求 | 0x80 raise IOError(frame timeout or bad ack) return resp[4]timeout默认 0.2 秒适用于握手和写块擦除命令要单独传 5.0。应答帧定长 8 字节是刻意设计的——变长应答需要额外的长度字段解析在现场调试时远不如定长好处理。5. 避坑排查六个把板子刷成砖的现场记录5.1 提示升级成功重启还是老程序现象上位机五步全部返回成功断电重启后程序行为没有任何变化读版本号还是旧值。原因标志位写进去了但 Bootloader 读的不是同一个地址。常见两种情况一是工程 B 的.cmd里BOOT_SECTION的origin跟工程 A 不一致二是Set_Active_Section里擦除长度只写了 2 个字Flash 擦除返回错误但代码没检查返回值后面的写入实际没生效。解决在两个工程的map文件里搜ActiveSecFile确认地址都是0x3D8000擦除函数的返回值必须判断失败就回错误码给上位机别让流程往下走。5.2 HEX 转换成功烧进去从第二条指令就跑飞现象CRC 校验能过说明数据确实写进去了但程序一跑就进非法中断。原因hex2000的-memwidth写成了 8。地址按字节递增生成MCU 端按字地址去取指令每条指令都偏移了一倍前几条还能碰巧执行后面全乱。解决三个宽度参数统一写 16转换完打开 HEX 看第一条数据记录的地址是不是3E00对不上就回去改命令文件别靠烧板子验证。5.3 擦除中途断电板子彻底不启动现象升级到一半设备掉电再上电串口无任何输出仿真器也连不上。原因擦除范围没有做保护把当前运行区或者 A 段 Bootloader 擦掉了。Bootloader 一旦被擦芯片上电后没有任何代码可执行只能返厂用 JTAG 救。解决ERASE_REGION的处理函数里第一步就做地址白名单判断目标起始地址只能是0x3E0000或0x3EC000且必须不等于当前有效区。Bootloader 所在 A 段0x3F6000起永远不在白名单里。这个判断放在 MCU 端不放在上位机因为上位机是可以被改的。5.4 大数据量升级必然丢块现象小文件小于 4K升级稳定一超过 16K 就随机出现 CRC 失败重试几次有时能过。原因上位机连续发送不等应答MCU 在擦写 Flash 期间关了中断串口接收 FIFO 溢出。擦一个扇区几百毫秒这期间发过来的帧全丢。解决每一帧等应答擦除命令单独给 5 秒超时MCU 端的串口接收用中断加环形缓冲不要用查询方式帧数据域不超过 128 字节。5.5 版本号回读永远是 0xFFFF现象升级流程全部成功READ_VERSION返回的三个字节全是0xFF。原因BSecVer变量没有被任何代码引用开了优化后被链接器裁掉段根本没有分配地址或者FLASHH的length写得太大把0x3E7FF2覆盖进了代码区写版本号的时候写到了代码上。解决BSecVer声明为volatile并在代码里至少有一次读取操作FLASHH的length卡到0x003FF0转换完查map确认版本号段的地址。5.6 跳转后新程序不进 main现象JUMP_NEW之后串口彻底没反应但重新上电又能正常进入新程序。原因跳转前没有关中断和看门狗。老程序里开着定时器中断跳过去之后新程序的向量表还没建立中断打到旧的入口地址直接跑飞或者看门狗在跳转后溢出复位正好落在新程序初始化的中间状态。解决跳转函数里先DINT关全局中断再喂一次狗并关闭看门狗然后清掉所有外设使能位最后用函数指针跳到c_int00的首地址。跳转前把新程序的入口地址从BSecAddr读出来别用硬编码常量。6. 升级到底成没成三层验证与一条回滚后悔药判断一次升级是否真的成功不能只看上位机弹的那个「成功」提示我一般会走三层验证任何一层过不了就当失败处理。第一层是数据层CHECK_CRC返回一致。这一层只证明写进 Flash 的数据跟 HEX 文件一致不证明程序能跑。第二层是版本层READ_VERSION读回的版本号跟编译时写进BSecVer的值一致。版本号建议用「主版本 次版本 编译日期低字节」三个字别用递增整数因为递增整数在多次回滚之后会分不清哪个是哪个。第三层是行为层也是最容易被跳过的一层断电重启观察设备能不能在正常时间内进入工作状态串口有没有按预期输出心跳。验证层手段通过标准失败时的第一动作数据层CHECK_CRC整区 CRC32 一致查 HEX 解析和分块边界版本层READ_VERSION与编译期写入值一致查 BSecVer 段的 map 地址行为层断电重启正常启动并输出心跳查跳转前的关中断和看门狗回滚这条后悔药要提前埋好不能等出事再想。做法是让 Bootloader 支持一个「强制回退」入口上电时如果检测到某个特定 GPIO 被拉低或者串口在 2 秒内收到连续三个0x55就无条件把标志翻到另一个区。这个入口不需要上位机软件配合现场只要短接一个测试点就能救回来。D–E 段的辅助升级程序在写 G–H 段之前也要先把 G–H 段的入口地址和校验字备份到 F 段这样即使 G–H 被擦了一半Bootloader 也能判断出「B 区不完整」并自动退回 A 区。还有一个容易被忽略的细节升级完成后第一次重启建议在辅助升级程序里做一次「自检延时」——延时 3 秒再做别的动作同时等上位机发一个确认帧。如果新程序有问题这 3 秒内还能收到回退命令。这个窗口不长但足够覆盖大部分「升完就发现不对」的场景。从那以后我每次做这类串口升级都会在动板子之前先把「强退入口」和「版本号回读」这两条路走一遍确认它们在没有上位机软件的情况下也能工作才敢往 Flash 里写第一帧。血泪经验是升级流程本身很少出问题出问题的永远是出问题之后你还有没有第二条路。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。