简介:这份资源为 SmartFlash Programmer 2 烧录工具,面向使用 TI CC2538 系列无线芯片的嵌入式开发者与硬件调试人员。网上常见的是只能烧录 CC2530 的 SmartFlash Programmer 版本,而这里提供的是可支持 ARM 内核 CC2538 的程序,恰好补上了从 8051 内核方案过渡到 ARM 架构时的工具缺口,适合已有一定 ZigBee 或 802.15.4 开发经验、需要给自研板卡量产烧写或调试固件的工程师。压缩包内共 2 个文件,分别为可安装执行的 exe 主程序与一份 html 格式的版本清单说明,整体约 50.44MB,体积不大但安装包与版本信息齐全,下载后可直接部署到 Windows 环境使用,清单文件还能帮助确认软件版本与组件构成。该资源已有 205 人学习下载,属于小众但垂直度较高的工具类资料。对于正在用 CC2538 做无线模块开发、苦于找不到可烧 ARM 内核芯片的官方或非官方编程器的读者,这份资源能直接解决工具链缺失的问题,省去四处翻找适配版本的时间。
1. 一块 CC2538 板子烧不进去,问题往往不在代码
上周帮人看一块自制的 CC2538 节点,同一份固件在别的板子上跑得好好的,换到新板子上,SmartFlash Programmer 2 连得上、能读到芯片 ID,一点 Program 就报错退出。查了两个小时,最后发现是镜像里带了 CCA 区的数据,把出厂写好的 IEEE 地址覆盖成了全 F。这类问题在 802.15.4 项目里出现频率高得离谱,因为 CC2538 的 flash 尾部有一段不属于用户代码、但随时可能被顺手擦掉的区域。下面按接线、镜像与地址、擦除策略、批量化、排障、回滚的顺序,把 SmartFlash Programmer 2 在 CC2538 上的落地过程拆一遍,重点放在参数怎么填、失败时看哪里。
2. 连接链路:先让工具认到芯片,再谈烧录
烧录失败九成不是固件的问题,而是链路这一层没打通。SmartFlash Programmer 2 本身只是个前端,它把“读芯片 ID、擦除、写 flash、回读校验”这些动作翻译成调试探针能听懂的时序,真正跟 CC2538 说话的是探针。所以第一步永远是把连接状态做实:工具里能看到芯片型号、flash 容量、当前 flash 内容,才进入下一步。跳过这一步直接点 Program,失败了连报错都读不懂。
2.1 三条链路怎么选:调试口、串口 Bootloader、脱机烧录器
CC2538 支持 cJTAG(两线)和标准 JTAG(四线/五线),出厂 ROM 里还带一个串口 Bootloader,另外还有第三方脱机烧录器。三条链路的能力边界差别很大,选错了会在后面反复吃亏。
| 链路 | 依赖条件 | 速度 | 能做的事 | 典型失效点 |
|---|---|---|---|---|
| cJTAG / JTAG | 调试探针、驱动、排线 | 快 | 读写全片、读芯片 ID、单步调试 | 探针固件版本、排线接触、目标板供电 |
| ROM 串口 Bootloader | CCA 区 Bootloader 使能位置 1、UART 引脚引出 | 慢 | 只能写 flash,不能调试 | CCA 被擦、引脚接错、波特率协商失败 |
| 脱机 / 量产烧录器 | 独立设备、镜像格式匹配 | 最快 | 批量、脱机 | 镜像起始地址、文件格式 |
开发阶段用调试口,因为要能停下来看寄存器。小批量试产用串口 Bootloader,省得每块板子插探针。量产才会考虑脱机烧录器。多数人翻车是这么来的:开发时一直用调试口,压根不知道 CCA 区被擦过,等到产线想用串口升级,发现 Bootloader 使能位早就没了,板子只能拆回来重新上探针。
提示:如果项目规划里有“现场通过串口升级固件”这一条,从第一天起就要把 CCA 区当成不可再生资源对待,后面第 3 章会展开。
2.2 驱动与端口:设备管理器里该出现什么
Windows 下插上探针,设备管理器里应该多出一个调试器设备和一个串口设备;如果只多出一个未知设备,或者带黄色感叹号,先解决驱动,别急着打开烧录工具。Linux 下更直接,插拔前后对比内核日志,能立刻看出枚举是否成功。
# 插拔调试探针前后各跑一次,对比多出来的设备 lsusb # 看内核有没有认到、有没有报枚举失败 sudo dmesg -T | tail -n 40 # 串口侧:按稳定路径看,不要记 /dev/ttyUSB0 这种会漂的编号 ls -l /dev/serial/by-id/lsusb的输出对比是最省事的判断方式:插上前记一次,插上后再看一次,如果两次完全一样,说明探针根本没被系统识别,问题在 USB 线、供电或者探针本身。dmesg里如果出现反复的枚举-断开循环,多半是 USB 线材质量或者目标板供电不足,换一根短而粗的线比改任何参数都有效。/dev/serial/by-id/下的名字虽然长,但它是跟着设备序列号走的,插哪个口都不变,脚本里用这个路径比用ttyUSB0稳得多。
目标板供电这一项单独拎出来说。CC2538 在擦除瞬间的电流会比平时高一截,如果板子只靠探针那点供电,很容易在擦除到一半时掉电复位,表现就是“有时候成功有时候失败”。我一般会要求目标板独立供电,探针只负责信号,这条经验值回好几次返工。
2.3 第一次连接只做一件事:读,不写
新建工程、选好芯片型号之后,第一件事是让工具把芯片信息读出来,而不是直接烧。SmartFlash Programmer 2 在连接成功后能显示芯片型号、flash 容量、当前 flash 里已有的内容,有的版本还能直接读出 IEEE 地址。看到这些信息,说明链路这一层彻底通了。
紧接着做第二件事:把整片 flash 读出来存成文件。这一步很多新手会跳过,觉得“反正我要烧新固件”。但 CC2538 出厂时 flash 尾部的 CCA 区里带着这块板子独有的信息,包括 IEEE 地址、flash 保护位配置、Bootloader 使能位。读一份存下来,成本是几十秒,收益是这块板子以后无论被烧成什么样,都有一条回到出厂状态的路。
# 把工具回读出来的整片 dump 做一次体检,顺便把 CCA 区单独存档 DUMP = "cc2538_full_dump.bin" FLASH_BASE = 0x00200000 FLASH_SIZE = 512 * 1024 # 按实际型号改,256KB 的型号填 256*1024 data = open(DUMP, "rb").read() print(f"dump 长度: {len(data)} 字节") assert len(data) == FLASH_SIZE, "长度对不上,多半是起始地址或容量选错了" # CCA 区固定是 flash 最后一个 2KB 页 cca = data[-2048:] open("cca_backup.bin", "wb").write(cca) print(f"CCA 区已单独存档,长度 {len(cca)} 字节") # 找出整片里非 0xFF 的区域,确认出厂信息落在哪一段 first_used = next(i for i, b in enumerate(data) if b != 0xFF) print(f"首个非空字节偏移: +0x{first_used:X} (绝对地址 0x{FLASH_BASE + first_used:08X})")这段脚本做三件事。第一,校验回读长度,长度不对说明起始地址或芯片容量选错了,后面所有分析都是错的。第二,把最后 2KB 单独切出来存盘,这就是这块板子的“身份证备份”,哪天 MAC 被覆盖了,从这里恢复。第三,扫描首个非空字节,能大致看出出厂信息写在了 flash 的哪一段,如果发现首个非空字节出现在接近尾部的位置,说明前面是空的,属于正常出厂状态;如果整片都是 0xFF,那这块板子可能是全新的,也可能 CCA 已经被擦干净了,需要进一步确认。
FLASH_SIZE这个参数一定要按手里的具体型号填。CC2538 有 256KB 和 512KB 两种容量,工具里如果选错了型号,回读长度和实际对不上,写的时候也会出问题。
3. 镜像与地址:hex、bin 和 CCA 区的边界
链路通了之后,第二个高频翻车点就是镜像本身。同样一份代码,编译出来烧进去不跑,八成是文件格式、起始地址或者链接脚本三者对不上。SmartFlash Programmer 2 对 hex 和 bin 的处理逻辑不一样,搞清楚这一点能省掉大量猜测。
3.1 Intel HEX 自带地址,裸 bin 全靠你填
Intel HEX 文件里每一行都带着这条数据应该落到哪个地址,工具读进去就能自己算,用户几乎不用操心。裸 bin 是纯粹的字节流,第一个字节对应哪个地址,完全取决于你在工具里填的起始地址。填错一个数,整个镜像平移,芯片照样“烧录成功”,但复位向量指向一片空白,表现就是烧完不启动。
更隐蔽的一种情况是链接脚本用了地址别名。有些工程把 flash 的链接起始写成 0x00200000,有些写成 0x0(芯片内部会把 0x0 映射到 flash 起始的别名区),两种写法编译出来的 bin 内容一样,但如果你的烧录起始地址填的是 0x0,工具会从绝对地址 0x0 开始写,而这个地址在 CC2538 上并不等于 flash 物理起始。判断方法很简单:看 bin 的前 8 个字节,Cortex-M 的向量表第一个字是初始栈指针,第二个字是复位向量,这两个值应该落在 RAM 和 flash 的合法范围内,不合法就说明基址对不上。
# 不依赖任何第三方库,手工解析 Intel HEX,算出镜像的真实覆盖范围 def parse_ihex(path): base, ext_base = 0, 0 lo, hi = None, None with open(path) as f: for line in f: line = line.strip() if not line.startswith(":"): continue raw = bytes.fromhex(line[1:]) count, addr, rtype = raw[0], (raw[1] << 8) | raw[2], raw[3] if rtype == 0x04: # 扩展线性地址 ext_base = ((raw[4] << 8) | raw[5]) << 16 elif rtype == 0x00: # 数据记录 start = ext_base + addr lo = start if lo is None else min(lo, start) hi = start + count - 1 if hi is None else max(hi, start + count - 1) return lo, hi FLASH_BASE, FLASH_END = 0x00200000, 0x0027FFFF CCA_START = 0x0027F800 # 最后一个 2KB 页,CCA 就在这里面 lo, hi = parse_ihex("app.hex") print(f"镜像覆盖 0x{lo:08X} - 0x{hi:08X},共 {hi - lo + 1} 字节") assert lo >= FLASH_BASE and hi <= FLASH_END, "镜像超出 flash 范围,检查链接脚本" if hi >= CCA_START: print("警告:镜像压到了 CCA 区,确认是否真的要覆盖出厂参数")解析逻辑不复杂,重点在最后两行判断。assert拦住的是链接脚本写错的情况,比如把 RAM 段误编进了 flash 镜像。if拦住的是更危险的情况:镜像尾部压到了 CCA 区。这时候烧进去,MAC 地址、Bootloader 使能位、flash 保护位会被一起改写,而且工具不会给你任何提示,因为从它的角度看,你只是写了一段合法的 flash 数据。
3.2 起始地址到底填什么
CC2538 的 flash 物理起始是 0x00200000,Cortex-M3 复位后从向量表取初始栈指针和复位向量,而向量表就放在 flash 起始。所以裸 bin 的烧录起始地址填 0x00200000,前提是链接脚本里 flash 的 ORIGIN 也是 0x00200000。如果链接脚本写的是 0x0,那你需要确认编译器是按哪种映射生成的——最稳妥的办法是别用别名,统一成 0x00200000,省得在烧录环节反复试。
带 Bootloader 的工程又是另一套算法。Bootloader 占 flash 开头一段,应用程序往后偏移,偏移量必须在三个地方保持一致:Bootloader 里跳转的目标地址、应用程序链接脚本里的 ORIGIN、烧录时填的起始地址。这三者只要有一个对不上,现象就是 Bootloader 跑起来了但跳过去就死。我见过最常见的错法是链接脚本改了、烧录时还在按老地址填,bin 被写到 Bootloader 身上,两块代码叠在一起。
3.3 擦除策略:整片擦除会带走什么
SmartFlash Programmer 2 里跟擦除相关的选项,本质上是三种语义:擦除后编程、只编程不擦除、单独整片擦除。选哪个,取决于你要不要保留 CCA 区。
| 操作 | 擦掉什么 | 什么时候用 | 风险 |
|---|---|---|---|
| 擦除后编程 | 镜像覆盖的扇区 | 日常迭代 | 镜像压到 CCA 时会顺带擦掉出厂信息 |
| 只编程 | 不擦,直接写 | 补丁式更新、写 MAC | 旧数据残留,flash 只能 1 写 0,写不干净 |
| 整片擦除 | 全部 flash 包括 CCA | 换固件架构、清异常状态 | MAC、Bootloader 使能位、保护位全部丢失 |
第二行值得展开。CC2538 的 flash 写入只能把 1 变成 0,不能把 0 变回 1,所以“只编程不擦除”只适用于目标区域当前是全 0xFF 的情况。如果那块区域之前写过东西,你写进去的结果是旧数据和新数据的按位与,校验多半过不了,运气好报错,运气不好写进去一个半残的镜像,上电行为诡异。
整片擦除是最容易被滥用的操作。新手遇到烧不进去就勾整片擦除,一次两次确实“解决”了问题,但每擦一次,CCA 区就被洗一次。等到某天发现串口升级进不去了,或者 MAC 地址变成全 F 导致两个节点互相打架,才回头找原因,这时候出厂信息已经没了,只能靠之前备份的 dump 恢复。
注意:如果你的板子是外购的成品模块,CCA 区里的 IEEE 地址是供应商写好的,整片擦除等于把这块板子的网络身份抹掉。恢复需要重新写入合法且唯一的地址,不是随便填一个就行。
3.4 校验不是走形式,回读比对才作数
工具里的“编程后校验”选项默认是勾上的,但它的校验逻辑是拿刚写进去的缓冲区跟 flash 比,能发现写入失败,发现不了“你烧错了镜像”。真正有用的校验是回读一份 dump,跟源文件逐字节比。
# 烧录完成后回读,跟源镜像做逐字节比对 src = open("app.bin", "rb").read() back = open("readback.bin", "rb").read() if len(src) != len(back): print(f"长度不一致:源 {len(src)},回读 {len(back)}") print("大概率是回读时起始地址或长度填错了,先修这个再比对内容") diff = [i for i, (a, b) in enumerate(zip(src, back)) if a != b] print(f"差异字节数: {len(diff)} / {len(src)}") if diff: i = diff[0] print(f"首个差异在偏移 +0x{i:X}:源=0x{src[i]:02X},回读=0x{back[i]:02X}") print("如果差异集中在尾部,检查是不是压到了 CCA 区")比对结果里差异的位置本身就是线索。差异集中在开头,通常是起始地址偏了;均匀散布,通常是写入时序或供电问题;集中在尾部,就要怀疑 CCA 区被动过。这三种现象对应完全不同的处理方向,比单纯看一个“校验失败”的红叉有用得多。
4. 批量化:把烧录、校验、贴标串成一条流水
单块板子调试靠手点没问题,超过十块就必须脚本化,不然重复劳动里一定混进人为错误——同一块板子烧两遍、MAC 忘了写、烧完没记录序列号,这些都是批量阶段的血泪经验。这一章的做法是把“镜像准备”和“逐块烧录”拆成两段,中间用一份带序列号的清单衔接。
4.1 镜像预处理:先合并,再烧
带 Bootloader 的工程,如果每次烧录都要分两次操作(先烧 Bootloader 再烧 App),出错概率翻倍。常见做法是在烧录前把两段合并成一份整片镜像,一次写完。
# 把 Bootloader 和 App 拼成一份整片镜像,偏移量必须和链接脚本一致 python3 - <<'PY' BOOT = open("bootloader.bin", "rb").read() APP = open("app.bin", "rb").read() APP_OFFSET = 0x8000 # App 起始偏移,必须等于链接脚本里的 ORIGIN - FLASH_BASE FILL = 0xFF # 未使用区域填 0xFF,和擦除后的 flash 状态一致 img = bytearray(b"\xFF" * (APP_OFFSET + len(APP))) img[:len(BOOT)] = BOOT img[APP_OFFSET:APP_OFFSET + len(APP)] = APP open("factory_full.bin", "wb").write(img) print(f"整片镜像 {len(img)} 字节,Bootloader {len(BOOT)},App {len(APP)}") print(f"App 落点: 0x{0x00200000 + APP_OFFSET:08X}") PYAPP_OFFSET是这段脚本里唯一需要动脑的参数,它必须等于“App 链接地址减去 flash 基址”。如果 App 链接在 0x00208000,那偏移就是 0x8000。填小了,App 会盖住 Bootloader 的尾部;填大了,中间留出一段空洞,虽然不影响运行,但会浪费 flash 空间,并且在某些按块校验的流程里造成困惑。填充字节用 0xFF 而不是 0x00,是因为擦除后的 flash 就是 0xFF,保持一致能让后续的差异比对更干净。
合并完不要急着烧,先拿 3.1 节的解析脚本过一遍,确认覆盖范围没压到 CCA 区。这一步花十秒,能避免一批板子集体丢 MAC。
4.2 循环烧录:把命令固定成脚本
批量烧录的脚本不需要多聪明,需要的是可重复、失败即停、每块都留下记录。命令行入口各家环境不一样,有的是工具自带的 CLI,有的是团队自己封的一层,下面的骨架把可变部分抽成变量,替换成你环境里实际的调用即可。
#!/usr/bin/env bash set -euo pipefail PROG="./flash_cli" # 换成实际命令行烧录入口 IMG="factory_full.bin" ADDR="0x00200000" LOG="burn_$(date +%Y%m%d_%H%M%S).log" # 按稳定路径枚举串口,避免 ttyUSB0/ttyUSB1 顺序漂移 for dev in /dev/serial/by-id/*; do echo "==> 开始烧录 $dev" | tee -a "$LOG" # --erase-chip 只在不保留 CCA 的场景使用,正常批量应改成按扇区擦除 "$PROG" --port "$dev" --chip cc2538 \ --addr "$ADDR" --verify "$IMG" 2>&1 | tee -a "$LOG" # 每块烧完立刻回读,落盘留证 "$PROG" --port "$dev" --read --addr "$ADDR" --size 0x80000 \ --out "readback_$(basename "$dev").bin" 2>&1 | tee -a "$LOG" echo "==> $dev 完成" | tee -a "$LOG" done三个参数决定这条脚本能不能用。set -euo pipefail保证任何一步失败就整体停下,避免“前一块失败、后一块继续烧”的连锁错误。--addr统一成 0x00200000,不要在这里搞花样。回读那一步的--size要跟芯片容量一致,512KB 就是 0x80000,填错了回读文件长度不对,后面的比对全是无用功。
脚本跑完,每块板子都对应一个readback_*.bin。这些文件不要删,它们是这批板子的出厂快照,返修时能直接对比出哪块被动过。
4.3 MAC 地址:批量烧录里最容易出事的环节
IEEE 地址必须唯一,这是 802.15.4 网络能正常组网的前提。批量时最常见的错误做法是把 MAC 写进整片镜像里,然后一炉烧下去——结果几十块板子地址一模一样,上电后互相踢,现象是网络时通时断,排查起来极其玄学。
正确做法是整片镜像里不含 MAC,烧完基础固件之后,用工具的独立入口逐块写入,或者通过串口 Bootloader 下发。写入之前先用脚本把地址分配表生成好,做到“一块板子一个号,号不对不烧”。
# 按板子序列号生成唯一的 IEEE 地址清单 import csv OUI = [0x02, 0x00, 0x00] # 本地管理地址段,正式产品换成自己申请的段 rows = [] with open("boards.csv") as f: for i, r in enumerate(csv.DictReader(f)): # 低三字节用序号填充,高位在前 mac = OUI + [(i >> 16) & 0xFF, (i >> 8) & 0xFF, i & 0xFF] rows.append([r["sn"], "".join(f"{b:02X}" for b in mac)]) with open("mac_plan.csv", "w", newline="") as f: w = csv.writer(f) w.writerow(["sn", "ieee_addr"]) w.writerows(rows) print(f"生成 {len(rows)} 条地址分配记录") print("首条:", rows[0] if rows else "无")地址表生成之后要做一次去重检查,尤其是分批生产、表文件被手工改过的情况下。OUI那三个字节只是占位,实际项目要用自己申请的地址段,用本地管理位随便填会导致跟真实设备地址冲突。地址写入完成后,用工具回读一次 CCA 区,确认写入的值和表里一致,这一步不能省——写入成功和写入正确是两回事。
5. 避坑:五个把板子烧成砖的现场
下面五条都是实际遇到过的,按“现象、原因、解决”的顺序写。有些看着像低级错误,但在赶进度的时候一个都躲不掉。
5.1 现象:连上了,但 Program 按钮一直是灰的
现象很明确:工具状态栏显示已连接,芯片型号也读出来了,但烧录按钮点不动,或者点了没反应。
原因通常有三个。一是没有选中要烧录的镜像文件,有些版本的工具在没加载文件时不激活编程按钮。二是芯片处于保护状态,flash 保护位被置起来了,工具读得到 ID 但拒绝写入。三是目标板电压偏低,探针能建立连接但擦除动作被拒绝。
解决顺序:先确认文件已加载,再检查芯片保护状态,最后量一下目标板供电。保护位这一项容易被忽略,尤其是拿到的板子是别人烧过的,前一个使用者可能出于“防抄”目的开了保护。解除保护一般需要整片擦除,这时候 CCA 区就保不住了,所以操作前先回读备份。
5.2 现象:烧录成功,拔掉探针重新上电就不启动
这是最典型的“假成功”。烧录过程一切正常,校验也通过,但断电重启后芯片没有任何反应,串口不出数据,射频也不发。
原因基本锁定在启动路径上:向量表没落在 0x00200000,或者 Bootloader 跳转地址和 App 链接地址不一致。烧录工具只负责把字节写到指定位置,它不知道你的代码期望从哪里开始执行。
排查方法:回读 flash 的前 8 个字节,看初始栈指针和复位向量是否落在合法范围。栈指针应该指向 RAM 区,复位向量应该指向 flash 内的奇数地址(Thumb 模式最低位为 1)。如果复位向量是 0xFFFFFFFF,说明那个位置压根没写进去,回头检查起始地址和镜像偏移。
5.3 现象:串口升级再也进不去了
板子之前一直能用串口 Bootloader 升级,某次烧录之后,不管怎么发同步字节都没回应,只能重新上探针。
原因是 CCA 区里的 Bootloader 使能位被擦掉了。整片擦除或者镜像压到 CCA 区,都会导致这个位丢失。芯片上电时读不到使能标志,就不会进入 ROM Bootloader,串口自然没反应。
解决要靠备份:用之前存的cca_backup.bin把最后 2KB 写回去,然后重新上电验证。如果没备份,就得查芯片手册找到使能位的具体位置手工恢复,费时且容易出错。这也是为什么第 2 章要坚持“第一次连接先读一份整片 dump”。
5.4 现象:同一份 hex,有的板子正常,有的异常
一批板子里挑不出规律,同样的镜像、同样的工具版本,大部分能跑,少数几块行为不对,有的射频发不出去,有的跑一段时间死机。
原因往往不在烧录,而在硬件差异被固件放大了。常见的是 flash 容量型号混用(256KB 和 512KB 的板子混在一批里),或者外部晶振批次差异导致射频校准参数不匹配。还有一种情况是烧录时擦除不彻底,旧固件残留在未被覆盖的扇区,两块代码在 flash 里共存,运行时跳到了旧的中断向量。
解决:先把异常板子的整片 flash 回读出来,跟正常板子做二进制比对,差异位置会直接指向问题区域。如果差异出现在参数区,查硬件批次;如果差异出现在代码区,说明擦除策略有问题,改成整片擦除后重烧验证。
5.5 现象:校验通过,功能就是不对
回读比对字节完全一致,但设备行为跟预期不符,比如版本号显示的还是老的,或者某个新加的功能完全没生效。
原因通常是烧错了镜像。批量目录里堆着好几个版本的 bin,文件名相似,脚本里写死的是相对路径,工作目录一变就烧了旧文件。另一种可能是构建产物没有重新生成,编译器判定无改动直接复用了上次的 bin。
解决:在烧录脚本里加一步打印镜像的哈希值和构建时间,跟版本管理系统里的记录对一遍。这一步看着多余,但在多版本并行的项目里能拦下大量“明明烧了却没生效”的扯皮。
6. 进阶:给每块板子留一份可回滚的出厂镜像
做到第 5 章,基本的烧录流程已经稳了。真正把一个项目从“能跑”推到“可维护”的,是回滚能力。我的习惯是每块板子在第一次烧录完成后,立刻做一份完整快照,包括整片 flash、CCA 区单独副本、以及一份说明文件。听起来很重,实际上一块板子多花二十秒,返修时能省掉半天。
具体做法分三步。第一步,烧录并校验通过后,立刻回读整片 flash 存成sn_factory.bin。第二步,从整片里切出最后 2KB 存成sn_cca.bin,这就是这块板子的身份凭证。第三步,写一个同名的.txt,记下镜像哈希、烧录时间、工具版本、目标板硬件版本。这三样东西放在同一个目录里,命名统一用序列号做前缀,找起来不用翻记录。
# 烧录完成后一次性归档,目录按序列号组织 SN="A0001" mkdir -p "archive/$SN" ./flash_cli --read --chip cc2538 --addr 0x00200000 --size 0x80000 \ --out "archive/$SN/${SN}_factory.bin" # 切出 CCA 区,恢复时只写这 2KB,不动用户代码 python3 - "archive/$SN/${SN}_factory.bin" <<'PY' import sys data = open(sys.argv[1], "rb").read() assert len(data) == 512 * 1024, "整片长度不对,先修回读参数" open(sys.argv[1].replace("_factory.bin", "_cca.bin"), "wb").write(data[-2048:]) print("CCA 已切出") PY # 记录镜像指纹,方便日后追版本 sha256sum "factory_full.bin" >> "archive/$SN/${SN}_meta.txt" date -Iseconds >> "archive/$SN/${SN}_meta.txt"归档策略上有个取舍值得说清楚。整片镜像体积大,几百块板子就是几百兆,但恢复时最省事,一步写回。CCA 单独副本体积小,可以全部塞进版本库,恢复时只补最后 2KB,用户代码不受影响。我的做法是两者都留:CCA 副本进版本库,整片镜像放共享盘按批次归档,超过半年的批次只留 CCA 和元数据,整片清掉。
恢复的时候,顺序很重要。先写整片镜像回到已知状态,再把 CCA 单独写回去,最后上电验证 MAC 和串口升级是否正常。不要图省事只写 CCA,因为你不确定当前的用户代码是不是干净的;也不要只写整片,那样会把 CCA 一起覆盖成归档时的状态,如果归档镜像本身就带着别人的 MAC,等于把问题复制了一遍。
有一个判断值得提前想清楚:什么时候该整片擦、什么时候不该。换固件架构、flash 布局调整、怀疑有残留数据干扰,这几种情况整片擦,擦完立刻从归档恢复 CCA。日常迭代、打补丁、只改应用层,按扇区擦除,绕开最后 2KB。把这条规则写进团队的烧录流程文档,比事后靠人记忆靠谱。
从那以后我每次拿到一块新板子,第一件事都是回读整片存档,第二件事才是烧固件。这个习惯救过好几次——有批板子出厂时 CCA 里的地址段写错了,靠存档比对才发现是供应商的问题,不是我们固件的事。希望帮到你。
本文还有配套的精品资源,点击获取