ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

smartflash programmer 2 烧录 CC2538 实战排错

smartflash programmer 2 烧录 CC2538 实战排错

简介:面向使用CC2538芯片进行ARM内核无线开发、样机调试与小型量产的工程师,这款SmartFlash Programmer 2提供可烧录CC2538的下载工具,弥补常见Programmer 1仅支持CC2530的不足,适合固件烧写、设备验证等场景。压缩包约50.44MB,共2个文件,以可执行程序为主,承担设备识别、固件加载与烧录操作,另附HTML说明页,便于查看版本信息和使用指引,解压后即可按需调用。已有205人浏览学习,可作为CC2538开发环境中的实用补充。读者能借此完成从CC2530到CC2538的烧录工具切换,减少工具不兼容带来的反复摸索,并借助版本说明快速确认软件信息,提升固件下载与调试效率。

1. 30 片模块烧到第 7 片报错:先搞清楚 smartflash programmer 2 到底管哪一段

一批 CC2538 模块上测试架,前 6 片用 smartflash programmer 2 烧得干干净净,第 7 片开始连续报读不到器件 ID。换探针、换 USB 口、重装驱动折腾一下午,最后发现是测试架上那根共地线虚焊。这类现场每天都在发生,问题往往不在工具本身,而在于没人把「连接层、镜像层、参数层」这三段分开看。

smartflash programmer 2 是一套跑在 Windows 上的 Flash 烧录上位机,面向 CC2538 这类带 802.15.4 射频的 Cortex-M3 SoC。它干的事很具体:通过调试探针把 hex/bin 镜像写进片内 Flash,顺带完成整片擦除、写后校验、读回导出、CCA 区配置和批量序列号烧写。做 802.15.4 模块固件、跑小批量产线、或者实验室里要给几十块板子灌同一个镜像的人,用它比手动敲命令行省事得多。但它对硬件链路和镜像地址的要求很硬,链路虚一点、基址错一位,症状都是同一句「连不上」。

2. 连上再说:CC2538 调试口的接线、供电与驱动三件事

2.1 cJTAG 两线还是 JTAG 四线:先看板上留了几个焊盘

CC2538 的调试口支持两种模式:完整 JTAG 用 TCKC、TMSC、TDI、TDO 四根信号线,cJTAG 两线只用 TCKC 和 TMSC。模块类产品为了省面积,板上通常只引出两线加复位和地,所以你打开工具选接口时,选错模式的表现就是「探针识别到了,目标器件一直 Unknown」。

信号两线 cJTAG四线 JTAG接线注意
TCKC必须必须走线尽量短,别和射频走线并排
TMSC必须必须同上,加 10k 上拉更稳
TDI / TDO不用必须只做四线时才接
nRESET强烈建议强烈建议不接时工具无法主动复位目标
GND必须必须最容易出问题的一根
VDD 检测建议建议用于探针判断目标电平

血泪经验是 nRESET 那根线。很多人图省事只接 TCKC、TMSC、GND 三根,读 ID 偶尔能成,一旦芯片跑进低功耗或者卡在死循环里,工具就再也拉不回来。接上复位之后,同样的板子连不上的概率会明显下降。

2.2 供电:别让目标板从 USB 口偷电

CC2538 的工作电压是 2.0V 到 3.6V,烧录瞬间的电流峰值比静态大不少。我见过最典型的翻车是把模块直接插在带载能力弱的 USB 转串口板上,静态能跑,一按烧录电压就掉到 2.0V 以下,工具报校验失败。校验失败比连不上更坑,因为它看起来像是镜像问题。

我的习惯是:烧录时目标板用独立稳压源,电压设 3.3V,限流先给 100mA,看着不对劲再往上加。探针的地和目标板的地必须共地,而且共地点要靠近芯片,不要一个在板头一个在板尾。另外探针本身不负责给目标供电,靠探针那点电流驱动整块板加射频模块,掉压是必然的。

2.3 驱动与设备识别:设备管理器里该看到什么

装完上位机之后,插上探针,设备管理器里应该出现调试口对应的条目。XDS 系列的探针一般会枚举出两个串口加一个调试接口,如果只看到一个未知设备,说明驱动没装全,或者被系统当成别的设备抢了。先在设备管理器里确认没有黄色感叹号,再打开工具。

还有一个常被忽略的点:调试口是独占资源。如果另一个 IDE 或者终端软件还挂着同一个探针,smartflash programmer 2 打开时会显示探针存在但连目标失败。遇到这种先关掉所有可能占用调试口的程序,实在不确定就拔插一次探针。

2.4 第一次连接:先读,别急着写

新板子上手,第一件事不是烧镜像,而是读芯片信息。操作顺序我固定成四步:

  1. 工具里选器件系列为 CC2538,接口选 cJTAG(两线板)或 JTAG(四线板)。
  2. 调试时钟先降到 1MHz 以下,等连接稳定了再往上提。
  3. 点读取芯片 ID / Flash 大小,确认返回的容量和你手上的型号对得上(512KB 和 256KB 器件的地址空间不一样)。
  4. 再点一次整片读回或者读 CCA 区,把这颗芯片出厂状态备份成一份 bin 存好。

第 4 步是后悔药。CCA 区里的锁位一旦被写过,后面所有工具都可能连不上这颗芯片,而原始 CCA 备份是唯一能让你在出问题后对照分析的东西。这个动作只花十几秒,但能救回来的板子不止一块。

3. 镜像怎么摆:HEX/BIN 地址、分区与合并脚本

3.1 512KB Flash 的分区现实

CC2538 的片内 Flash 从 0x00200000 起映射,512KB 器件一直铺到 0x0027FFFF,页大小 2KB。实际项目里没人把整片当一个区用,通常是这么切的:

区域典型位置大小说明
引导/升级程序0x00200000 起16–32KB上电先跑,负责跳转应用或串口升级
应用镜像引导之后或 0x00200000视协议栈而定主固件,链接脚本里的 ROM 起始要与之对齐
NV / 参数区应用之后1–3 页网络参数、校准值,量产时通常要保留
CCA 区最后一页的高地址段2KB 内的一段锁位、IEEE 地址、引导使能等

具体偏移每家的链接脚本都不一样,唯一不能错的是:应用镜像的链接起始地址,必须和烧录时填的地址一致,也必须和引导程序的跳转目标一致。这三处只要有一处不对,烧录全程绿灯,上电就是不跑。

3.2 HEX 和 BIN 的差别,就在地址从哪来

Intel HEX 每条记录自带地址,工具解析完就知道每段数据该落在哪,所以你只给一个文件它也能烧对。BIN 是裸数据流,第一个字节对应哪个地址全靠你在工具里填的起始地址,填错了就是整片平移。

判断标准很简单:如果镜像里有引导程序和应用两段不连续的内容,用 HEX;如果只是单独一个应用且链接脚本固定从 0x00200000 起,BIN 也行,但起始地址那栏一定要手动确认,别用工具记住的上一次的值。我在产线见过一次批量事故,操作员换了台电脑,工具里起始地址还是上一款产品的,烧出来 200 片全部不启动。

3.3 合并镜像:一个脚本把引导和应用拼起来

量产时更省事的做法是把引导和应用合并成一份 HEX,烧录时只选一个文件,减少人为选错的可能。下面这个脚本用 intelhex 库做合并,顺带检查区间重叠和容量越界。

#!/usr/bin/env python3 # 合并 bootloader 与应用镜像,检查重叠与容量,导出整片 hex 和 bin import sys from intelhex import IntelHex FLASH_BASE = 0x00200000 # CC2538 Flash 起始地址 FLASH_SIZE = 512 * 1024 # 512KB 器件;256KB 器件改成 256 * 1024 def load(path): ih = IntelHex() ih.loadhex(path) return ih def main(boot_path, app_path, out_hex, out_bin): boot, app = load(boot_path), load(app_path) b_lo, b_hi = boot.minaddr(), boot.maxaddr() + 1 a_lo, a_hi = app.minaddr(), app.maxaddr() + 1 print(f"bootloader: 0x{b_lo:08X} - 0x{b_hi:08X}") print(f"app : 0x{a_lo:08X} - 0x{a_hi:08X}") # 区间相交就退出,别让后写入的数据静默覆盖前面的 if a_lo < b_hi and b_lo < a_hi: sys.exit("地址区间重叠,先回去改链接脚本的 ROM 起始偏移") merged = IntelHex() for addr, val in {**boot.todict(), **app.todict()}.items(): merged[addr] = val hi = merged.maxaddr() + 1 if hi > FLASH_BASE + FLASH_SIZE: sys.exit(f"镜像结束于 0x{hi:08X},超出 {FLASH_SIZE // 1024}KB 容量") # 未覆盖的地址按 0xFF 填充,和擦除后的 Flash 状态一致 merged.padding = 0xFF merged.write_hex_file(out_hex) merged.tobinfile(out_bin, start=FLASH_BASE, size=hi - FLASH_BASE) print(f"merged : 0x{FLASH_BASE:08X} - 0x{hi:08X} ({hi - FLASH_BASE} bytes)") if __name__ == "__main__": main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])

逻辑说明:先分别算出两个文件的地址区间,重叠判断必须在合并之前做——直接写字典推导会把重叠部分静默覆盖,烧进去照样「成功」,但运行到某个功能就崩,排查成本极高。padding = 0xFF是为了让导出的 bin 里空洞和空白 Flash 状态一致,避免校验时把空洞当成脏数据。参数方面,FLASH_BASE和FLASH_SIZE按你手上的具体型号改,256KB 器件忘了改这里,脚本会放行一段实际不存在的地址,烧录时才报错。

3.4 序列号烧写:模板加脚本,别在工具里手填

只要产品要组网,IEEE 地址就必须一片一个。工具界面里确实能配递增序列号,但它在批量场景下不好审计——烧到第几片、下一个号是多少、有没有跳号,全靠人盯。我一般自己生成 N 份镜像,文件名带序号,烧录脚本按文件名跑,日志天然可查。

#!/usr/bin/env python3 # 基于模板 hex 批量生成镜像,把 8 字节 IEEE 地址写进 CCA 区 import sys from intelhex import IntelHex CCA_MAC_ADDR = 0x0027FFCC # 示意偏移,务必按芯片参考手册的 CCA 章节核对 BASE_MAC = 0x00124B0000000001 COUNT = 10 def mac_bytes(value, n=8, order='big'): return list(value.to_bytes(n, order)) def main(tpl, prefix): for i in range(COUNT): ih = IntelHex(tpl) mac = BASE_MAC + i for off, b in enumerate(mac_bytes(mac)): ih[CCA_MAC_ADDR + off] = b out = f"{prefix}_{i:04d}.hex" ih.write_hex_file(out) print(f"{out} mac={mac:016X}") if __name__ == "__main__": main(sys.argv[1], sys.argv[2])

参数说明:CCA_MAC_ADDR这个偏移必须对着手册的 CCA 章节确认,不同容量器件落在不同地址;order是字节序,写反了的现象非常有迷惑性——读出来 MAC 看着是一串合法的十六进制,但设备入网时的地址和标签对不上,查半天以为是协议栈问题。BASE_MAC建议从你申请的地址段起算,别随手填一串。

4. 擦写参数:Mass Erase、校验与 CCA 锁位怎么配

4.1 擦除策略:整片擦还是按页擦

工具里通常给三种选择:整片擦除、按页/扇区擦除、只写不擦。选哪个取决于你要保留什么。

整片擦除最干净,把 CCA 区也一起清了,适合新片首次烧录或者现场救砖。按页擦除适合保留 NV 参数和 CCA 的升级场景,但前提是你清楚应用占哪几页,页边界算错一页,参数区就被抹了。只写不擦看着最快,实际最危险:Flash 只能把 1 写成 0,没法把 0 写回 1,旧数据残留会让新镜像的某些字节「写不进去」,而工具的校验是按写入数据比对的,有时候照样通过。

我的固定做法是:产线首烧一律整片擦除;返修板或者保留参数的场景才用按页擦,且擦之前先把 NV 区读出来备份。

4.2 校验:写后 verify 到底比的是什么

工具里的 verify 是拿你提供的镜像和 Flash 里的实际内容逐字节比对,听起来很可靠,但有两个盲区。

第一个盲区是没被镜像覆盖的地址。你的镜像只覆盖 0x00200000 到 0x00210000,后面的地址校验时根本不看,残留数据不影响校验结果。如果你的应用会去读某个参数区,而那片区里是上一版固件的垃圾,问题就留到运行期了。

第二个盲区是 CCA 区。有些流程是「先合并镜像再整片擦」,CCA 里的锁位和地址被清成默认值,校验照样通过,但设备行为已经变了。所以校验通过不等于状态正确,关键区域要单独读回确认。

4.3 CCA 与锁位:勾选之前先想好怎么救

CCA 区里除了 IEEE 地址,还有 Flash 锁位和调试口相关的配置。锁位一旦写进去,调试探针就读不了 Flash 内容,这对量产防抄板有用。问题是:如果同时把「禁止整片擦除」也写了,这颗芯片就再也救不回来——工具连不上,擦不掉,改不了,只能换片。

所以我的规矩是:任何一次烧录锁位之前,先完整读回一次 CCA 区存成文件;锁位和禁止擦除这两个选项,绝不一起勾。如果产品确实需要防读,先小批量试三片,确认烧录流程、序列号写入、升级路径都没问题,再放开批量。

4.4 命令行批量:把一次成功配置固化成脚本

GUI 适合调通,批量必须走命令行。工具安装目录里通常带一个命令行可执行文件,把 GUI 里那次成功烧录的参数照着写一遍,然后用脚本循环。下面这个封装做了三件事:逐片调用、失败不中断、日志带时间戳。

#!/usr/bin/env bash # 批量烧录封装:逐片写入递增 MAC 的镜像,失败跳过并记录 set -uo pipefail PROG="./srfprog.exe" # 命令行可执行文件,实际名字以安装目录为准 DEV="CC2538" IMG_DIR="./images" LOG="./burn_$(date +%Y%m%d_%H%M%S).log" for img in "$IMG_DIR"/*.hex; do sn=$(basename "$img" .hex) printf '[%s] start %s\n' "$(date +%H:%M:%S)" "$sn" | tee -a "$LOG" # -t 目标类型 -d 器件 -c 连接方式 -e 擦除 -m 整片擦 -f 文件 -v 校验 if "$PROG" -t soc -d "$DEV" -c JTAG -e -m -f "$img" -v >>"$LOG" 2>&1; then printf '[%s] OK %s\n' "$(date +%H:%M:%S)" "$sn" | tee -a "$LOG" else printf '[%s] FAIL %s\n' "$(date +%H:%M:%S)" "$sn" | tee -a "$LOG" fi done

逻辑说明:失败不exit是量产脚本的关键,一片连不上不应该让整批停在那里,日志里标 FAIL 之后人工回头补。参数名每个版本可能有出入,第一次用先在命令行敲帮助参数,把实际支持的选项打出来对照,别照抄别人的博客。日志带时间戳和文件名,出了问题能直接定位到具体哪一片、哪一份镜像。

5. 排查现场:五个把板子烧到连不上的记录

5.1 连接与硬件类

现象:探针能识别,点连接一直报读不到器件 ID。原因八成是共地不良或者复位悬空,尤其手工焊的测试架,地线走的是排针而不是焊盘。解决:万用表量探针地和芯片地之间的通断,阻值应该接近零;把 nRESET 补上;调试时钟先降到 500kHz 试一次。

现象:整片擦除之后,这颗芯片换任何工具都连不上。原因是之前烧录时把锁位和禁止擦除一起写了,或者擦除过程中掉电导致 CCA 区写坏。解决:只能换片。预防手段是每次写锁位前先读回 CCA 备份,并且锁位与禁擦选项不同时勾选。

现象:同一批板子,同一镜像,有的能烧有的不能,换台电脑又能烧。原因是 USB 口供电不足或线材太长,烧录瞬间掉压。解决:换主机后面的 USB 口,用带供电的 Hub,目标板改独立供电;调试线缩短到 15cm 以内再试。

5.2 镜像与运行类

现象:烧录全程绿灯,校验也过,上电设备不启动。原因通常是 bin 的起始地址填错,或者应用镜像的链接起始地址和引导程序的跳转目标不一致。解决:把读回的 bin 和原始镜像做一次逐字节比对,看第一段有效数据落在哪个地址;同时确认中断向量表的偏移配置。

现象:批量烧完,网络里出现大量地址冲突。原因是模板镜像里 IEEE 地址没改,或者生成脚本的字节序写反导致编号重复。解决:生成完先抽查三份镜像,把地址字段读出来核对;烧录日志里把每片的 MAC 打出来,出问题直接查日志定位。

6. 读回验证:烧完不算完,三分钟把差异揪出来

工具显示 OK 之后,真正可靠的动作是读回一次。这一步在产线上被跳过最多,因为它多花十几秒,而故障往往在下游才暴露。我的做法是每批抽三片做完整读回,用下面的脚本和原始镜像比对,CCA 区单独放行。

#!/usr/bin/env python3 # 读回比对:把工具读回的 bin 与原始 hex 逐字节对比,CCA 区跳过 import sys from intelhex import IntelHex BASE = 0x00200000 CCA_LO, CCA_HI = 0x0027FFC0, 0x0027FFFF # 示意区间,按手册 CCA 范围调整 def main(golden_hex, readback_bin): golden = IntelHex(golden_hex) addrs = set(golden.addresses()) with open(readback_bin, "rb") as f: data = f.read() bad = [] for i, got in enumerate(data): addr = BASE + i if CCA_LO <= addr <= CCA_HI: continue # 序列号在这里,必然与模板不同 exp = golden[addr] if addr in addrs else 0xFF if exp != got: bad.append((addr, exp, got)) print(f"diff bytes = {len(bad)}") for addr, exp, got in bad[:20]: print(f"0x{addr:08X} expect 0x{exp:02X} got 0x{got:02X}") sys.exit(1 if bad else 0) if __name__ == "__main__": main(sys.argv[1], sys.argv[2])

几个参数值得说清楚。BASE必须和读回时工具里的起始地址一致,不一致的话所有差异都会对不上,你会以为整片都烧错了。CCA_LO/CCA_HI是放行区间,因为每片的序列号写在这里,和模板必然不同,不放行的话每片都会报差异。比对结果里如果差异集中在某一段连续地址,通常是那一段没擦干净;如果差异是散落的单字节,多半是链接脚本里那段本来就没数据,被当成了 0x00。

另一个我必做的检查是看差异位置是否落在向量表上。向量表前几个字是栈顶和复位入口,如果这两个字和镜像不一致,芯片上电第一件事就跳飞,症状是「烧录成功但完全无反应」,很容易被误判成硬件坏。

从那以后我每次上产线前,都会先拿三片样片走一遍「整片擦 — 写 — 校验 — 读回比对」,三片全过才放开批量,不管工期多紧都不跳这一步。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进