
1. 一条HEX记录里到底塞了哪些字段前阵子帮人看一批固件包两个HEX文件单独烧都正常合并成一个之后设备起不来。CRC对得上烧录器也不报错最后发现问题出在合并工具把两段不连续的地址当成连续数据拼进了bin——HEX文件本身没错错在没人认真读过它里面的地址记录。这类事故在嵌入式圈子里不算少见根子往往是大家对Intel HEX 文件格式只停留在哦就是那个冒号开头的文本文件的认知层面。Intel HEX 是一种用纯ASCII文本描述二进制数据和加载地址的文件格式最早由Intel为其早期微处理器开发工具定义后来因为足够简单、足够自描述被大量编译器、烧录器、仿真器沿用至今。它解决的问题很朴素二进制bin文件只有字节流没有这段数据该放到哪个地址的信息而HEX把地址、数据、校验一起写进了每一行任何一个解析器拿到文件就能独立还原出完整的存储器映像。这篇内容适合三类人看一是刚接触单片机、第一次打开HEX文件看到满屏冒号一脸懵的新手二是需要写bootloader、OTA升级、固件合并工具的开发者三是被烧录成功但跑不起来折腾过、想搞明白底层逻辑的老手。下面我会从一行的字节构成讲到地址空间拼接再给一份能直接跑的解析器最后把几类高频异常的排查链路摊开讲。1.1 冒号之后的六个字段每一行HEX记录的骨架是固定的用BNF写出来大概长这样:LLAAAATT[DD...]CC拆开看字段长度含义:1字符记录起始标志每一行必须以它开头LL1字节本行有效数据字节数取值 0x00~0xFFAAAA2字节16位偏移地址大端序高字节在前TT1字节记录类型00/01/02/03/04/05 六种DD...LL字节实际数据长度由LL决定CC1字节校验和注意每一个字段都是两个十六进制字符表示一个字节的文本形式所以一行最短的合法记录是:00000001FF共11个字符最长理论上能到1 2 4 2 510 2 521个字符LL0xFF时但实际工程里几乎见不到。拿一条经典样本来手算一遍:10010000214601360121470136007EFE09D2190140LL 0x10本行16个数据字节AAAA 0x0100偏移地址TT 0x00数据记录数据是21 46 01 36 01 21 47 01 36 00 7E FE 09 D2 19 01CC 0x40这行数据会被加载到当前基址 0x0100 的位置。至于当前基址是什么取决于它前面出现过哪条扩展地址记录——这是后面第3节要展开的重点。1.2 校验和的两种算法与一个高频笔误校验和是整个格式里最容易写错、也最容易救你一命的部分。标准定义是把LL、AAAA两字节、TT、DD...全部按字节累加取低8位再求二进制补码。用公式表达checksum (0x100 - (sum_of_all_bytes 0xFF)) 0xFF等价写法是checksum (~sum 1) 0xFF再等价一点就是checksum (-sum) 0xFF。三种写法结果一致你可以挑自己顺手的。反过来验证时把所有字节含校验和自己加起来结果的低8位必须是0(sum(LL, AA_hi, AA_lo, TT, DD..., CC)) 0xFF 0我们拿上面那条记录验一下0x10 0x01 0x00 0x00 0x11数据部分累加得到低字节0xAF0x11 0xAF 0xC0补码0x100 - 0xC0 0x40和文件里的CC完全对上。再看两个短样本你可以自己动手验一遍:020000040800F2 :00000001FF第一个是扩展线性地址记录0x02 0x00 0x00 0x04 0x08 0x00 0x0E0x100 - 0x0E 0xF2。第二个是结束记录0x00 0x00 0x00 0x01 0x010x100 - 0x01 0xFF。最容易踩的坑是把补码校验写成了简单累加和。这两种算法在累加结果为0时恰好一致所以单元测试偶尔能蒙混过关一旦跑真实数据立刻露馅。我见过一个自研的固件校验脚本作者图省事写成了sum 0xFF结果只有某几个特定固件能过校验排查了半天才发现是这个低级错误。另一个高频问题是文本层面被污染。HEX是纯文本这意味着任何文本编辑器都可能动它Windows记事本另存可能给你加上BOM头或者在末尾补一个空行跨平台传输时CRLF和LF混用从聊天窗口复制粘贴时行尾多一个空格。校验和本身没错但解析器按行读取时把空格也算进字符序列直接报错。我的习惯是解析前先做一次清洗去掉\r、去掉首尾空白、跳过空行、遇到不以冒号开头的行立刻抛异常而不是静默忽略——静默忽略会让真正的错误藏得更深。1.3 为什么记录长度很少超过32字节LL字段允许到255但你在实际文件里几乎看不到超过32字节的记录绝大多数工具默认输出16字节一行少数用32。原因有三层。第一层是历史惯性。早期开发工具的缓冲区小16字节刚好是很多ROM编程器的写入粒度一行一条记录正好对应一次写操作调试时肉眼定位也方便。第二层是错误隔离。文本格式难免损坏一行越长损坏一次丢掉的数据越多。16字节一条记录坏一行只损失16字节而且能精确定位到具体的地址段255字节一条记录同样的损坏面积大一个数量级。第三层是体积权衡。每行的固定开销是1(冒号) 2(LL) 4(AAAA) 2(TT) 2(CC) 2(换行) 13字节。16字节数据13 32 45 字节膨胀率约 2.8 倍32字节数据13 64 77 字节膨胀率约 2.4 倍255字节数据13 510 523 字节膨胀率约 2.05 倍从16加到32体积能省下约14%从32加到255只再省不到15%但可读性和容错性全线下降。所以32基本是工程上的甜点值这也是为什么很多工具提供-record-length之类的参数让你在16和32之间选。如果你的固件要给产线烧录别一味追体积。产线工具链对超长记录的支持参差不齐我见过某款烧录器解析32字节记录正常、64字节直接截断的情况。交付前拿真实烧录器跑一遍比在PC上验证十遍都管用。2. 六种记录类型各自的分工记录类型字段只有1字节但6个取值撑起了整个格式的寻址能力。很多人只认识00和01遇到02、04就跳过不看结果就是前面说的那种地址错位事故。类型名称数据长度作用00Data RecordLL携带实际固件数据01End Of File Record0文件结束标志02Extended Segment Address2段基址值×1603Start Segment Address4段式入口地址 CS:IP04Extended Linear Address2线性基址值1605Start Linear Address4线性入口地址 EIP这张表建议直接存进笔记。下面逐类拆解使用场景。2.1 00与01数据搬运与文件收尾00类型是绝对主力一个固件文件里95%以上的行都是它。规则很直白把DD...这LL个字节依次写入基址 AAAA开始的位置写完后地址指针自然递增。有个细节值得强调一条00记录内部的地址是连续的但两条相邻的00记录之间不一定连续。有些生成工具为了优化体积会跳过全0xFF的区域不写于是在两条看起来紧挨着的记录之间留下空洞。解析的时候必须按各自的AAAA独立计算目标地址绝不能接着上一行末尾往下写。这个错误我在三份不同的自研解析代码里都见过是名副其实的高频雷区。01类型是文件结束标志标准要求LL 0x00、AAAA 0x0000整行固定为:00000001FF。它有两个作用一是告诉解析器后面没有了二是提供一次额外的完整性暗示——因为校验和固定是0xFF如果这行都读不对说明文件在传输过程中大概率被破坏了。实际解析时读到01应该立即停止并且忽略它之后的所有内容。有些文件在01之后还有空行甚至注释行严格解析器会报错宽容解析器会忽略。我倾向于报错——既然格式规定01之后无内容那多出来的内容就说明生成链路有问题早暴露比晚暴露好。当然如果你的下游客户就是会手动追加几行说明文字那就在配置里开个宽容开关但一定要在日志里打出来。还要注意一种情况某些合并工具会把两个HEX拼在一起中间保留前一个文件的01记录。这种文件在部分烧录器上只有第一段被写入第二段被静默丢弃。判断方法很简单——用文本工具搜一下:00000001出现了几次超过一次就要警惕。2.2 02与04两套扩展地址方案的由来16位的AAAA字段最多只能寻址64KB这在1970年代够了放到今天就完全不够看。于是标准先后引入了两套扩展方案。02 扩展段地址ESA是早期方案。它的数据是2字节含义是段基址除以16实际基址 值 × 16。因为值是16位最大0xFFFF × 16 0xFFFF0加上16位偏移寻址上限约1MB。这套方案明显带着x86实模式的烙印——段地址左移4位加偏移正是当年16位处理器的寻址方式。04 扩展线性地址ELA是后来补的方案。数据同样是2字节但含义是高16位地址实际基址 值 16。16位值左移16位得到32位基址配合16位偏移寻址上限4GB。举个例子要把数据写到0x0800C000:020000040800F2 :10C00000...后续数据记录AAAA C0000x0800 16 0x08000000加上偏移0xC000正好是0x0800C000。同一段地址如果用02表示需要基址0x08000000 / 16 0x00800000这已经超出16位范围了——02方案根本无法表达这种地址。这就是为什么现代工具链几乎清一色用0432位地址空间是刚需02方案在地址超过1MB时直接失效。两套方案混用会怎样标准没有明确规定优先级不同解析器行为不一致。有的按最后出现的扩展记录生效有的干脆只认04、忽略02。我在自己的解析器里选择遇到02就切到段模式、遇到04就切到线性模式并记录一条警告因为混用本身就是生成工具的bug信号。2.3 03与05入口地址记录该不该信03和05都是起始地址记录描述程序从哪里开始执行。03是段式的CS:IP四字节05是线性的EIP四字节。理论上烧录器读到这条记录就知道该把PC设到哪里。实际上大量烧录器和调试器直接忽略这两条记录理由很实际现代MCU的复位向量是硬件固定的程序从哪个地址开始跑由芯片设计决定不是由HEX文件说了算。你写个05记录说从0x08001234开始芯片复位后照样从向量表取SP和PC该跳哪跳哪。那这两条记录还有什么用主要是给仿真器和某些带引导加载能力的工具用以及在裸机环境下做零散的代码下载时提供入口提示。另外它们也承担一定的文件合法性作用——如果解析器发现05记录里的地址不在任何已加载的数据区间内通常意味着文件拼接出了问题。我的处理策略是解析出来存着打印日志但不参与任何决策。如果哪天真的遇到依赖入口地址的烧录流程再从日志里取这段信息比现场抓瞎强。3. 地址空间这条暗线拼接规则与边界陷阱第2节讲完记录类型其实只讲了半张地图。真正决定数据落在哪里的是基址 偏移这个拼接过程以及它背后那条64KB的隐形分界线。这一节是全文最值得反复看的部分。3.1 从16位偏移到32位绝对地址解析器的核心状态其实只有一个变量base。规则如下base 0 # 初始基址 # 遇到 04 记录 base int.from_bytes(payload, big) 16 # 遇到 02 记录 base int.from_bytes(payload, big) * 16 # 遇到 00 记录 abs_addr base int(AAAA, 16)就这三行构成了整个HEX格式的寻址模型。base是一个持续生效的状态不是每行都重算——这也是为什么单独看某一行数据记录你根本不知道它落在哪个地址必须从头顺序解析。这里有个容易忽视的点abs_addr的计算结果是字节地址不是字地址。绝大多数架构上这没问题但如果你在按字寻址的架构上工作比如某些16位或32位DSP工具链可能会在生成HEX之前就做一次地址换算。判断方式是对照链接脚本里的段起始地址和应用手册里的存储映射两边对不上就是这里出了问题。我吃过一次这个亏一个项目的HEX解析出来地址全部减半查了两小时才发现是工具脚本里多写了一次除以2。还有一点base 不会自动回绕。如果文件里出现04记录把 base 设成0x0001 16随后的数据记录偏移是0xF000那数据就落在0x0001F000。但如果一条数据记录偏移0xFFF8、长度为16字节末尾会越过0xFFFF进入下一个64KB块——这时绝对地址确实连续可有些解析器在内部用16位变量存偏移会悄悄回绕到base段的开头。这是典型的整数溢出型事故症状是固件末尾几十字节写错位置。3.2 连续记录不等于连续地址我在第2节提过一次这里展开说因为它值得单独占一节。一段典型的多段固件长这样:020000040800F2 :10000000... → 0x08000000 ~ 0x0800000F :10001000... → 0x08000010 ~ 0x0800001F :020000040801F1 :10000000... → 0x08010000 ~ 0x0801000F第一段在0x0800_xxxx第三行的04记录把基址切到0x0801_0000第四行数据直接跳到了0x0801_0000。中间0x08000020到0x0800FFFF这一大段地址是空的——可能是因为那段是未使用的Flash区域工具选择不输出。如果你写解析器时图省事用当前写入指针顺序追加那第四行的数据就会紧贴在第三行后面落到0x08000020。烧录后芯片从0x08010000取数据的地方全是0xFF程序自然跑不起来而烧录器一句话都不会说。正确的做法只有一个每条00记录都独立计算绝对地址用字典或稀疏数组按地址存储。mem {} # key: 绝对地址, value: 字节 for i, b in enumerate(payload): mem[abs_addr i] b用字典的代价是内存占用高每个字节一个dict entryPython里大约几十字节开销但换来的是绝对正确的地址语义。如果固件很大可以按页存储以256字节或1KB为粒度做dict[int, bytearray]既省内存又保留了空洞信息。我在处理2MB级别的固件时用的就是页式方案峰值内存控制在几十MB完全可以接受。3.3 一条跨界记录引发的烧录错位这是我在一个真实项目里踩过的坑症状很有代表性。那次的情况是固件大小刚好是 64KB 16 字节。生成HEX之后前64KB的数据用0x0800作基址最后16字节的地址是0x08010000。理论上工具应该在这之前插入一条:020000040801F1但那个版本的生成脚本有个判断分支写错了只在起始地址变化超过64KB时才插入新记录导致最后16字节的数据记录仍然带着0x0800的基址和0xFFF0的偏移。绝对地址算下来是0x0800_0000 0xFFF0 0x0800FFF0正好是最后16字节应该在的位置——数据本身没错。问题出在写Flash的时候烧录器把0x0800FFF0到0x0800FFFF这16字节写进去了但芯片的Flash页边界是0x08010000这16字节落在了最后一页的尾部而程序期望它们在下一页的开头。结果就是那句经典的烧录成功校验通过开机无反应。排查链路是这样的先确认烧录器日志里的写入地址范围发现末端是0x0800FFFF比预期少了16字节。用二进制工具把HEX转成bin对比bin大小发现是0x10010字节比预期的0x10010一致——bin没问题。但bin是从0x08000000开始铺的说明转换工具把0x08010000处的内容也当成连续数据放在同一个bin里了。回头看HEX才发现最后一条数据记录的地址字段是FFF0而不是0000。定位到生成脚本的插入条件判断改成任何跨越64KB边界的记录前都插入新的04记录。修复只有一行代码排查花了大半天。教训是生成HEX之后必须做一次地址连续性自检把所有00记录的绝对地址区间列出来检查是否存在区间跨64KB边界的情况有就报警。一个简单的自检思路遍历时记录上一段区间一旦发现上一段起始 长度落入了不同的64KB页而中间没有出现新的04/02记录就判定为可疑。这个检查不到20行代码能挡住一整类事故。4. 手写一个HEX解析器从能读到读对网上能搜到的HEX解析代码一大把但多数只能处理工具正常生成的、格式规整的文件遇到稍微脏一点的数据就崩。这一节我给一份相对完整的实现重点讲清楚每一处防御性检查为什么加。4.1 解析骨架与状态机import sys def parse_hex(path, strictTrue): base 0 pages {} # 页号 - bytearray(256) eof_seen False start_addr None line_count 0 with open(path, rb) as f: # 二进制模式避免换行转换 for lineno, raw in enumerate(f, 1): line raw.decode(ascii, errorsreplace).strip() if not line: if strict: print(f[warn] 第{lineno}行: 空行, filesys.stderr) continue if not line.startswith(:): raise ValueError(f第{lineno}行: 不以冒号开头 - {line[:20]}) if eof_seen: raise ValueError(f第{lineno}行: 结束记录之后还有内容) body line[1:] if len(body) % 2 ! 0: raise ValueError(f第{lineno}行: 字符数为奇数) try: rec bytes.fromhex(body) except ValueError: raise ValueError(f第{lineno}行: 含非十六进制字符) if len(rec) 5: raise ValueError(f第{lineno}行: 记录长度不足5字节) count rec[0] if len(rec) ! count 5: raise ValueError( f第{lineno}行: 声明{count}字节数据实际{len(rec)-5}字节) offset (rec[1] 8) | rec[2] rtype rec[3] payload rec[4:4count] csum rec[-1] if (sum(rec[:-1]) csum) 0xFF: raise ValueError(f第{lineno}行: 校验和错误) line_count 1 if rtype 0x00: for i, b in enumerate(payload): addr base offset i pg, off divmod(addr, 256) pages.setdefault(pg, bytearray(b\xff * 256))[off] b elif rtype 0x01: eof_seen True elif rtype 0x02: base int.from_bytes(payload, big) * 16 elif rtype 0x04: base int.from_bytes(payload, big) 16 elif rtype 0x03: start_addr (segment, int.from_bytes(payload, big)) elif rtype 0x05: start_addr (linear, int.from_bytes(payload, big)) else: raise ValueError(f第{lineno}行: 未知记录类型 {rtype:02X}) if not eof_seen: raise ValueError(缺少文件结束记录文件可能被截断) return pages, start_addr, line_count逐段说明几处有意为之的设计用rb而不是r打开。文本模式在不同平台会做换行转换\r\n变\n虽然对strip()之后的解析没影响但会掩盖文件行尾不一致这个潜在问题。二进制模式读原始字节出问题时日志更真实。校验和放在长度检查之后。顺序很重要如果长度字段本身就是坏的那后面的校验和验证必然失败先报长度错误能更快指向根因。每行独立计算绝对地址。这就是3.2节说的原则addr base offset i里三者都是当前的不做任何跨行状态传递。页式存储。pages用divmod拆成页号和页内偏移缺页时自动填充0xFF——这个默认值很关键Flash擦除后的状态就是0xFF用别的值填充会在后续对比时产生假差异。EOF缺失要报错。有些工具生成的HEX没有01记录严格来说不合规。如果你的上游就是这样可以降级为警告但一定要打日志。文件被截断和文件正常结束靠的就是这条记录区分。4.2 合并、裁剪与地址空洞处理解析出来之后最常见的三个后续操作是合并多个HEX、按地址区间裁剪、把空洞填成固定值。合并的核心是冲突检测。两份固件在0x08004000处都有数据怎么办我的规则是默认报冲突除非显式指定优先级。生产环境的合并工具如果静默覆盖等于把事故埋进了固件里。实现上页式存储里用0xFF表示未写过合并时检查目标位置是不是0xFF不是就记录下来。裁剪要区分两种语义按字节区间裁剪[start, end)和按页对齐裁剪。前者精确但可能破坏Flash页语义后者安全但会多写一些0xFF。烧录场景我建议按页对齐因为很多芯片的写入操作要求整页。空洞填充要谨慎。把空洞填0xFF是安全的因为等价于擦除状态。填0x00就得问清楚你是真的要写一串0进去还是想表达这段没有数据我见过把空洞填0的固件烧录后芯片在那段区域无法再次擦写排查了很久才想到是这里。把页式结构写回bin文件的代码很短def pages_to_bin(pages, out_path): if not pages: with open(out_path, wb): pass return lo, hi min(pages), max(pages) with open(out_path, wb) as f: for pg in range(lo, hi 1): f.write(pages.get(pg, b\xff * 256))注意lo和hi之间可能存在大片空洞页这些页会被写成全0xFF。如果固件的地址跨度是4GB比如某些高端芯片的分区布局这段代码会生成一个4GB的bin——所以在实际使用前一定要加区间限制参数。4.3 用现成工具做交叉验证自己写的解析器无论如何都会带主观偏差最好的验证方式是用成熟工具跑一遍比对输出。工具典型用法特点objcopyobjcopy -I ihex -O binary in.hex out.binbinutils自带覆盖面广srec_catsrec_cat in.hex -intel -o out.bin -binary支持几十种格式互转能做填充和偏移烧录器自带转换各厂商GUI里一般有导出结果最贴近实际烧录行为交叉验证的比对点有三个bin文件大小。这是最简单的断言两边大小不一致说明有地址区间没对上。MD5。大小对上了内容也可能不同跑个MD5一步到位。地址区间列表。如果需要更精细的定位把两边识别出的有数据的地址段列出来对比能精确到哪一段差了。我习惯在合并脚本里内置一次objcopy转换并比对MD5不一致就直接失败退出。代价是每次多花几百毫秒收益是彻底杜绝对合并后固件错位这类问题的侥幸心理。5. HEX与BIN的取舍格式选型的现实考量前面四节都在讲HEX内部机制这一节跳出来看既然bin更小更直接为什么还要用HEX什么时候必须用HEX什么时候用bin更好5.1 自描述与哑数据的本质差别bin文件是纯粹的字节序列它连起点在哪都不知道。你要烧一个bin进芯片必须同时告诉烧录器起始地址——这个信息以参数形式存在外部一旦参数写错或者用错文件烧进去就是废的而且烧录器无从发现。HEX把地址内嵌进文件任何解析器独立读取都能还原完整映像。这个差别在单文件场景下不明显一旦涉及多段固件就暴露了现代MCU的存储映射里Flash、EEPROM、OTP、配置区通常分布在不连续的地址上。用bin的话你得准备三个文件加三个地址参数用HEX的话一个文件搞定每段数据自带地址。这个自描述属性在远程升级场景里价值更大。设备端收到的固件包如果只有字节流任何一环没有把地址传对就是变砖HEX自带地址设备端解析器照单全收容错边界清晰得多。5.2 体积膨胀与传输代价前面算过16字节记录下HEX约为原始数据的2.8倍。对一个256KB的固件来说bin是256KBHEX约700KB。在带外传输比如通过串口、蓝牙、蜂窝网络时这个差距是实打实的流量和时间。我在一个低带宽场景里做过实测128KB固件HEX约350KB9600波特率下串口传输要接近5分钟而bin只要不到2.5分钟。后来改成设备侧直接在传输层做压缩HEX的字符集只有16种符号压缩率很高再在设备端解压后解析总耗时才压下来。所以选型逻辑很简单本地烧录、产线烧录用HEX自描述省事体积无所谓。带宽紧张、体积敏感传输bin但必须把地址和校验一起打包别裸传。需要保留多段地址信息、又不想要文本开销考虑SREC或者自定义的二进制带址格式。顺带提一下SREC。它是Motorola系的同类格式每行以S开头记录类型是S0~S9语义和HEX高度对位。它的一个实际优势是记录类型分得更细S1/S2/S3分别对应16/24/32位地址不需要额外维护基址状态。如果你的工具链两边都支持SREC在解析器实现上会简单一些。缺点也很明显生态里HEX的兼容性更好很多老烧录器只认HEX。5.3 什么情况下必须锁死HEX有几种场景我会明确要求全链路只允许HEX不设bin通道产线多工位、多机型共用一套工具。工位多意味着操作员多参数传递一定出错。HEX把地址封在文件里操作员只需要选文件不用填地址出错面小一个数量级。固件分区间升级。比如只升级应用区、保留引导区和参数区。HEX里天然只有应用区的数据记录直接烧就行用bin的话得先读出来、偏移、擦除、写回每一步都可能出错。审计与追溯要求高的场景。HEX是文本任何一次修改都会在版本对比里留下清晰的diff。bin是二进制git diff出来一堆乱码你根本没法快速判断这次提交到底改了哪几个字节。反过来禁止使用HEX的场景我也列一下芯片容量极小比如8KB FlashHEX的文本开销在存储空间上不划算。传输链路有严格帧长限制文本行需要额外分帧处理。工具链对HEX的支持有已知bug见过某转换工具把04记录的基址算错16倍那就老老实实用bin。6. 高频异常的排查链路前面讲的都是应该怎样这一节讲出问题怎么查。我把这些年遇到的案例归成三类每一类给一条可以照着走的排查路径。6.1 校验和报错但看不出错在哪症状解析器报第N行校验和错误你打开那一行左看右看字符数对格式也对。排查顺序建议是第一步隔离变量。把报错行单独存成一个只含该行和EOF记录的小文件再解析一次。如果还报错说明问题在行内容本身如果不报错了说明是上下文比如前面缺了04记录导致基址异常——虽然校验和计算不依赖基址但如果你解析器里做了跨行的额外验证就可能被牵连。第二步手算校验和。把那一行的字节序列手写出来累加和文件里的CC对比。这一步能区分文件真的坏了和解析器算错了。第三步检查不可见字符。用十六进制查看器打开报错行的末尾看是不是多了0x20空格、0x09Tab或者行尾只有一个0x0D。这类问题在跨系统传文件时特别常见Windows记事本保存的文件如果行尾只有\r多数解析器按\n切行会把整个文件当成一行。第四步问生成链路。如果文件是自动生成的去查生成脚本里用的校验算法。我遇到过一次生成方用的是简单累加和、解析方用的是补码两边各自正确接口对不上。还有一种隐蔽情况文件被中间某个环节规范化过。某些版本管理工具或文本编辑器会自动转换编码、去掉行尾空白、在文件末尾补换行——这些操作对HEX都是破坏性的。判断方法是把原始文件和当前文件做二进制diff看差异是不是集中在行尾。6.2 烧录器说成功程序就是不跑这是嵌入式里最经典的一类问题HEX格式相关的原因通常集中在三个点上。地址基址完全没有生效。有些烧录器在配置文件里要求手动指定HEX基址如果设成了0而文件里的04记录写的是0x0800数据就会全部落到地址0附近。表现是烧录成功、读回来验证也通过因为读的是同一段错地址但芯片从正确地址取指令全是0xFF。判断方法烧录后用调试器直接读芯片存储看固件内容是否在预期的地址上。这一步能五分钟内定性。64KB边界处的数据错位。就是第3.3节讲的那类问题。判断方法是把HEX转成bin检查bin的首尾地址与链接脚本里的段定义是否一致。多个01记录导致后半段被丢弃。合并过的HEX里出现多个EOF记录某些烧录器读到第一个就停。判断方法是直接搜文件里的:00000001出现次数大于1就有嫌疑。修法是合并工具正确去掉中间的EOF只在末尾保留一个。入口地址记录误导。极少数烧录器会读取05记录并据此设置PC如果05记录里的地址是链接时生成的老地址、和实际烧录地址不一致就会出现烧录成功但PC跑飞。判断方法是搜索:04000005开头的行看里面的地址是否合理。我把这几条的判断顺序排成一张表现象优先怀疑快速验证读回内容全0xFF基址未生效调试器读芯片存储只有前部分内容正确多个EOF记录搜:00000001次数末尾一小段地址错位64KB边界未切记录bin尾部地址比对复位后PC异常05记录地址不对搜:040000056.3 合并多文件后的重叠与覆盖合并是HEX使用中最容易出事的一环因为大多数合并工具默认行为是后写覆盖先写一声不吭。我给自己定的合并流程是四步第一步先单文件解析输出各自的地址区间列表。这一步是基线没有基线就无法判断合并是否引入了问题。第二步做区间交集检测。把两个文件的地址区间做二维比较找出所有重叠段。重叠不一定是错误比如两个文件都包含同一份配置区数据且内容一致但必须被看见。第三步对重叠段做内容比对。内容一致就跳过不一致就中止并报出具体地址。这一步能挡住99%的静默覆盖事故。第四步合并后做整体自检。检查项包括总记录数是否等于两个文件之和、EOF记录是否只有一个、地址区间是否等于两文件的并集、bin的MD5是否和预期的拼接结果一致。代码层面判断重叠只需要在页式存储写入时多一次检查def write_byte(pages, addr, value, origin): pg, off divmod(addr, 256) page pages.setdefault(pg, bytearray(b\xff * 256)) old page[off] if old ! 0xFF and old ! value: raise ValueError( f地址 0x{addr:08X} 冲突: 已有 0x{old:02X} f{origin} 想写入 0x{value:02X}) page[off] value注意这里的判断条件是old ! 0xFF也就是把擦除态当作未写。这个约定和Flash的物理特性一致同时也让先合并再烧和分两次烧的结果保持一致。有一类覆盖是合法的bootloader和应用程序共用同一份中断向量表两个文件都在0x0000处写了向量内容完全一样。这种情况用上面的检查会通过因为值相同不用特殊处理。但如果内容不同那必须人工介入——向量表都不一样说明两个文件根本不配套。最后补充一个我自己的习惯合并后的HEX一定重新走一遍完整解析而不是在内存里改完直接输出。重新解析能发现输出环节引入的格式问题比如写出了奇数个十六进制字符、忘了加换行、校验和算错。多花的那点时间比在产线上抓瞎强得多。