ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

C2000双镜像Hex合并实战:从bootloader.out到Uniflash可烧录文件

C2000双镜像Hex合并实战:从bootloader.out到Uniflash可烧录文件

简介:这份资源面向使用TI C2000系列C28x内核芯片的嵌入式开发者,尤其是需要实现在线升级(LFU)与量产烧录的工程师。它提供一套Python脚本工具,解决bootloader.out与user_app.out的Hex转换及合规合并问题,最终生成可直接用于Uniflash烧录的merged.hex文件。压缩包共23个文件,约1.51MB,包含4个hex、4个py脚本、2个pyc缓存、2个exe工具及out、map等中间文件,其中hex2000.exe负责ELF转Intel Hex,BinMerge.py与OutconvertHex.py完成合并与校验,map文件便于地址核对。脚本适配CCS12.x,自动生成CRC32校验值,严格遵循Intel Hex规范,Bootloader在前、UserApp在后,仅保留一个结束行,并过滤空行与注释,失败时打印明确错误而不中断流程。目前已有104人学习,适合需要快速搭建C2000在线升级与烧录流程的开发者参考,可省去手动拼接Hex的繁琐与风险。

1. C2000 双镜像 Hex 合并:从 bootloader.out 到 Uniflash 可烧录文件

手上有两个.out文件——bootloader.out和user-app.out,一个负责引导,一个跑业务逻辑,但 Uniflash 只认一个烧录文件。这个场景在 C2000 系列 C28x 内核芯片的量产阶段非常常见:Bootloader 占住 Flash 起始扇区,User App 从固定偏移地址开始存放,两者必须合并成一个地址连续的 Hex 文件才能一次性烧录。问题在于,TI 的 hex2000 工具分别转换两个.out文件后,得到的两个 Hex 文件在地址空间上可能有重叠、可能有间隙,直接拼接会导致烧录后程序跑飞。这篇内容面向正在用 C2000、C28x 做 Bootloader + App 双镜像方案的嵌入式工程师,把 Hex 转换、地址对齐、合规合并这条链路拆开讲清楚,每一步都能照着复现。

2. 先搞清楚 C28x 的 Flash 地址布局与 Hex 格式约束

2.1 C28x 的 Flash 扇区划分与 Bootloader 驻留区

C2000 系列 C28x 内核芯片的 Flash 地址空间从0x080000开始(不同型号有差异,比如 F28004x 从0x080000起,F2837x 从0x080000起但扇区大小不同)。Bootloader 通常占用起始的几个扇区,User App 从后续扇区开始。以 F280049 为例,Flash 扇区划分如下:

扇区起始地址大小典型用途
Sector A0x0800004K x 16Bootloader 入口
Sector B0x0820004K x 16Bootloader 主体
Sector C0x0840004K x 16Bootloader 参数区
Sector D0x0860004K x 16User App 起始
………User App 延续

这个布局决定了合并时的核心约束:Bootloader 的 Hex 地址范围不能侵入 User App 的起始地址,User App 的 Hex 也不能覆盖 Bootloader 的扇区。如果两个.out文件的链接命令文件(.cmd)没有严格划分地址范围,转换出来的 Hex 就会在地址空间上打架。

常见做法是在两个工程的.cmd文件里显式指定BEGIN和END地址。Bootloader 工程限制在0x080000~0x083FFF,User App 工程从0x086000开始。这样 hex2000 转换出来的 Hex 文件天然不重叠,合并时只需要按地址排序拼接即可。

2.2 TI Hex 格式与 Intel Hex 的关键差异

TI 的 hex2000 工具默认输出的是 TI-TXT 格式,但 Uniflash 烧录 C2000 时通常需要 Intel Hex 格式(.hex)或直接使用.out文件。这里有个容易翻车的点:hex2000 加-i参数输出 Intel Hex,但 C28x 的地址是 16 位字寻址,而 Intel Hex 是字节寻址,地址需要乘以 2 或者按字地址处理。

具体来说,C28x 的 Flash 一个地址存 16 位数据,hex2000 在转换时会根据-memwidth和-romwidth参数决定地址映射方式。如果参数设错,生成的 Hex 文件烧录后地址会整体偏移,程序直接跑飞。

我一般会这样设置 hex2000 的参数:

hex2000 -i -memwidth 16 -romwidth 16 -order MS -boot -e 0x080000 -o bootloader.hex bootloader.out

参数说明:

  • -i:输出 Intel Hex 格式
  • -memwidth 16:内存宽度 16 位,匹配 C28x 的字寻址
  • -romwidth 16:ROM 宽度 16 位
  • -order MS:大端序,C28x 默认
  • -boot:生成引导表格式,如果 Bootloader 需要被 ROM 引导加载则加上
  • -e 0x080000:指定入口地址
  • -o:输出文件名

对于 User App,入口地址不同,且通常不需要-boot:

hex2000 -i -memwidth 16 -romwidth 16 -order MS -e 0x086000 -o user-app.hex user-app.out

转换完成后,用文本编辑器打开两个 Hex 文件,检查扩展线性地址记录(Type 04)和扩展段地址记录(Type 02)是否与预期一致。如果看到地址从0x080000直接跳到0x086000,说明链接器已经正确分配了地址空间。

2.3 合并前的地址冲突检测方法

在合并之前,必须确认两个 Hex 文件的地址范围没有重叠。最直接的方法是用 Python 脚本解析两个 Intel Hex 文件,提取所有数据记录的地址范围,然后做区间重叠检测。

import re def parse_hex_addresses(hex_file): """解析 Intel Hex 文件,返回 (起始地址, 结束地址) 列表""" ranges = [] with open(hex_file, 'r') as f: for line in f: line = line.strip() if not line.startswith(':'): continue byte_count = int(line[1:3], 16) addr = int(line[3:7], 16) rec_type = int(line[7:9], 16) if rec_type == 0x00 and byte_count > 0: ranges.append((addr, addr + byte_count - 1)) return ranges def check_overlap(ranges_a, ranges_b): """检测两组地址范围是否有重叠""" for a_start, a_end in ranges_a: for b_start, b_end in ranges_b: if a_start <= b_end and b_start <= a_end: print(f"冲突: [{hex(a_start)}, {hex(a_end)}] 与 [{hex(b_start)}, {hex(b_end)}]") return True return False boot_ranges = parse_hex_addresses('bootloader.hex') app_ranges = parse_hex_addresses('user-app.hex') if not check_overlap(boot_ranges, app_ranges): print("地址无重叠,可以安全合并")

这段脚本的逻辑是:逐行读取 Hex 文件,只处理 Type 00(数据记录),提取每条记录的起始地址和结束地址。然后对两组地址范围做两两比较,判断是否存在交集。如果输出“地址无重叠”,说明两个镜像在地址空间上是干净的,可以进入合并步骤。如果有冲突,需要回到.cmd文件调整链接地址。

3. 用 Python 脚本实现 Hex 文件的合规合并

3.1 Intel Hex 记录类型与合并策略

Intel Hex 文件由多条记录组成,每条记录以冒号开头,格式为:LLAAAATT[DD...]CC。其中LL是数据字节数,AAAA是 16 位地址,TT是记录类型,DD是数据,CC是校验和。合并两个 Hex 文件时,需要处理以下几种记录类型:

  • Type 00:数据记录,包含实际固件数据
  • Type 01:文件结束记录,每个文件末尾有一条
  • Type 04:扩展线性地址记录,设置高 16 位地址
  • Type 02:扩展段地址记录,C28x 场景下较少用
  • Type 03/05:起始地址记录,通常不需要合并

合并策略是:解析两个文件的所有数据记录,按地址排序,重新生成一个完整的 Hex 文件。Type 04 记录需要根据地址变化重新插入,Type 01 只在最终文件末尾保留一条。

3.2 合并脚本的完整实现与逐行说明

import struct def parse_intel_hex(filename): """解析 Intel Hex 文件,返回数据记录列表 [(address, data_bytes), ...]""" records = [] upper = 0 # 扩展线性地址的高16位 with open(filename, 'r') as f: for line in f: line = line.strip() if not line or not line.startswith(':'): continue raw = bytes.fromhex(line[1:]) byte_count = raw[0] addr = (raw[1] << 8) | raw[2] rec_type = raw[3] data = raw[4:4+byte_count] if rec_type == 0x04: # 扩展线性地址记录,更新高16位 upper = (data[0] << 8) | data[1] elif rec_type == 0x00: # 数据记录,计算完整32位地址 full_addr = (upper << 16) | addr records.append((full_addr, data)) return records def merge_hex(records_a, records_b, output_file): """合并两组数据记录,按地址排序后生成新的 Hex 文件""" all_records = records_a + records_b # 按起始地址排序 all_records.sort(key=lambda x: x[0]) # 检查重叠 for i in range(len(all_records) - 1): end_prev = all_records[i][0] + len(all_records[i][1]) if end_prev > all_records[i+1][0]: raise ValueError(f"地址重叠: {hex(end_prev)} > {hex(all_records[i+1][0])}") # 生成合并后的 Hex with open(output_file, 'w') as f: current_upper = None for addr, data in all_records: upper = (addr >> 16) & 0xFFFF lower = addr & 0xFFFF if upper != current_upper: # 插入扩展线性地址记录 rec = bytes([0x02, 0x00, 0x00, 0x04, (upper >> 8) & 0xFF, upper & 0xFF]) checksum = (-sum(rec)) & 0xFF f.write(f":{rec.hex().upper()}{checksum:02X}\n") current_upper = upper # 写入数据记录 rec = bytes([len(data), (lower >> 8) & 0xFF, lower & 0xFF, 0x00]) + data checksum = (-sum(rec)) & 0xFF f.write(f":{rec.hex().upper()}{checksum:02X}\n") # 文件结束记录 f.write(":00000001FF\n") # 执行合并 boot_records = parse_intel_hex('bootloader.hex') app_records = parse_intel_hex('user-app.hex') merge_hex(boot_records, app_records, 'merged_firmware.hex') print("合并完成,输出 merged_firmware.hex")

这段脚本的核心逻辑分三步:解析、排序、重建。解析阶段处理 Type 04 记录来还原完整的 32 位地址,因为 C28x 的 Flash 地址超过 16 位范围,必须依赖扩展线性地址记录。排序阶段按地址从小到大排列所有数据记录,同时检查相邻记录之间是否有地址重叠——这是防止合并后固件损坏的关键检查点。重建阶段在地址跨越 64K 边界时自动插入新的 Type 04 记录,确保地址映射正确。

参数方面,upper变量跟踪当前的高 16 位地址,lower是低 16 位偏移。校验和的计算方式是取记录所有字节之和的二进制补码低 8 位,这是 Intel Hex 标准规定的。

3.3 合并后的验证:地址连续性检查与校验和验证

合并完成后,不能直接拿去烧录,必须先验证。验证分两步:地址连续性检查和校验和验证。

地址连续性检查用之前的parse_hex_addresses函数解析合并后的文件,确认 Bootloader 结束地址和 User App 起始地址之间没有意外的间隙或重叠。如果 Bootloader 结束于0x083FFF,User App 起始于0x086000,中间0x084000~0x085FFF是空的,这是正常的——Uniflash 烧录时会自动填充未使用的区域。

校验和验证可以用srec_cat工具或者自己写脚本。Intel Hex 每条记录的最后一个字节是校验和,计算方式是:将记录中除校验和之外的所有字节求和,取低 8 位,再用 0x100 减去这个值。如果计算结果与文件中的校验和一致,说明记录完整。

def verify_hex_checksum(filename): """验证 Intel Hex 文件每条记录的校验和""" with open(filename, 'r') as f: for line_num, line in enumerate(f, 1): line = line.strip() if not line.startswith(':'): continue raw = bytes.fromhex(line[1:]) checksum = raw[-1] calculated = (-sum(raw[:-1])) & 0xFF if checksum != calculated: print(f"第 {line_num} 行校验和错误: 期望 {calculated:02X}, 实际 {checksum:02X}") return False print("所有记录校验和正确") return True verify_hex_checksum('merged_firmware.hex')

这个验证步骤看起来简单,但能拦住大部分因为手动编辑或脚本 bug 导致的文件损坏。我遇到过因为 Python 脚本里upper变量没有正确重置,导致合并后的 Hex 在 64K 边界处地址跳变,烧录后 Bootloader 能跑但跳转到 App 就死机。后来加了校验和验证,这类问题在烧录前就能发现。

4. 避坑与排查:Hex 合并烧录中的 5 个血泪教训

4.1 现象:烧录后 Bootloader 正常但 App 不运行

原因:两个.out文件的链接地址有重叠,合并脚本虽然检测到了重叠但被忽略,或者合并时排序错误导致 App 的起始地址被 Bootloader 的数据覆盖。

解决:回到两个工程的.cmd文件,用MEMORY指令显式划分地址范围。Bootloader 的FLASH区域设为ORIGIN = 0x080000, LENGTH = 0x004000,User App 的FLASH区域设为ORIGIN = 0x086000, LENGTH = 0x020000。重新编译后再次转换和合并,用 3.3 节的地址连续性检查确认无重叠。

4.2 现象:Uniflash 提示“Hex 文件格式错误”或“地址超出范围”

原因:hex2000 转换时-memwidth和-romwidth参数设置错误,导致生成的 Hex 地址是字节地址而非字地址,地址值翻倍后超出了芯片的 Flash 范围。

解决:确认 hex2000 命令中-memwidth 16 -romwidth 16两个参数都正确设置。如果用的是 C2000 的较新型号(如 F2838x),还需要检查-order参数是否与芯片端序匹配。用文本编辑器打开 Hex 文件,看第一条数据记录的地址是否在0x080000附近,如果看到0x100000这样的地址,说明地址被错误地按字节计算了。

4.3 现象:合并后的 Hex 文件烧录成功但程序跑飞

原因:合并脚本在处理 Type 04 扩展线性地址记录时,没有在地址跨越 64K 边界时正确插入新的记录,导致高 16 位地址没有更新。

解决:在合并脚本中,每次写入数据记录前检查当前地址的高 16 位是否与current_upper一致,不一致就插入 Type 04 记录。这个逻辑在 3.2 节的脚本里已经实现,关键是要确保current_upper在每次地址变化时都更新。可以用一个简单的测试用例验证:手动构造两个地址跨越0xFFFF边界的 Hex 文件,跑一遍合并脚本,检查输出文件中是否有正确的 Type 04 记录。

4.4 现象:Bootloader 跳转到 App 后,中断向量表指向错误

原因:C28x 的中断向量表通常位于 Flash 的固定偏移处,如果 User App 的链接命令文件没有正确设置向量表地址,或者合并时向量表所在扇区被 Bootloader 占用,跳转后中断会进入错误的处理函数。

解决:在 User App 的.cmd文件中,用SECTION指令将.intvecs段定位到 App 区域的起始地址。例如PIE_VECT : > FLASH_APP, PAGE = 0,其中FLASH_APP的起始地址是0x086000。合并后检查 Hex 文件中0x086000附近的数据是否与 User App 的向量表内容一致。

4.5 现象:Uniflash 烧录速度极慢或卡死

原因:合并后的 Hex 文件包含大量地址不连续的小数据块,Uniflash 在解析时频繁切换地址,导致烧录效率下降。

解决:在合并脚本中增加数据块合并逻辑,将地址连续的数据记录合并成更大的块。具体做法是在排序后,遍历记录列表,如果当前记录的结束地址等于下一条记录的起始地址,就将两条记录的数据拼接。这样可以减少 Hex 文件中的记录条数,提升 Uniflash 的解析和烧录速度。对于 C2000 的 Flash 烧录,通常建议将连续数据合并到不超过 256 字节一条记录,避免单条记录过长导致校验和计算溢出。

5. 进阶技巧:用批处理脚本串联转换、合并与验证

5.1 一键完成从 .out 到可烧录 Hex 的完整流程

在实际项目中,每次修改代码后都要重新执行 hex2000 转换、Python 合并、校验和验证这一整套流程,手动操作容易漏步骤。我一般会写一个批处理脚本(Windows 下用.bat,Linux 下用.sh)把整个链路串起来。

#!/bin/bash # build_merged_hex.sh - C2000 双镜像 Hex 合并一键脚本 set -e # 任何一步失败立即退出 BOOT_OUT="bootloader.out" APP_OUT="user-app.out" BOOT_HEX="bootloader.hex" APP_HEX="user-app.hex" MERGED_HEX="merged_firmware.hex" echo "[1/4] 转换 Bootloader..." hex2000 -i -memwidth 16 -romwidth 16 -order MS -boot -e 0x080000 -o $BOOT_HEX $BOOT_OUT echo "[2/4] 转换 User App..." hex2000 -i -memwidth 16 -romwidth 16 -order MS -e 0x086000 -o $APP_HEX $APP_OUT echo "[3/4] 合并 Hex 文件..." python merge_hex.py $BOOT_HEX $APP_HEX $MERGED_HEX echo "[4/4] 验证合并结果..." python verify_hex.py $MERGED_HEX echo "完成: $MERGED_HEX 已生成并通过验证"

这个脚本的关键在于set -e,它确保任何一步出错(比如 hex2000 转换失败、Python 脚本抛出异常)都会立即停止,不会带着错误继续往下走。merge_hex.py和verify_hex.py分别对应第 3 章的合并脚本和校验脚本。

5.2 用 Uniflash 命令行模式做烧录前的最终确认

Uniflash 提供了命令行接口,可以在不打开 GUI 的情况下执行烧录操作。在批量生产场景下,这个功能非常实用。命令行模式的基本用法是:

dslite.bat -c user_files/configs/ccxml/F280049.ccxml -l user_files/settings/generated.ufsettings -f merged_firmware.hex

参数说明:

  • -c:指定目标配置文件(.ccxml),包含芯片型号和调试器设置
  • -l:指定 Uniflash 的设置文件(.ufsettings),包含 Flash 烧录算法和地址范围
  • -f:指定要烧录的 Hex 文件

在执行烧录之前,建议先用-r参数读取芯片当前的 Flash 内容,与合并后的 Hex 做比对,确认地址映射一致。这个步骤在首次烧录新板子时尤其重要,能避免因为芯片型号不匹配导致的烧录失败。

5.3 我踩过的坑与固化习惯

早期做 C2000 双镜像方案时,我习惯直接用 hex2000 分别转换两个.out,然后用文本编辑器手动拼接两个 Hex 文件。结果有一次因为两个文件的 Type 04 记录冲突,合并后的文件在0x080000处的高位地址被覆盖,烧录后 Bootloader 的前 64K 数据全部错位。板子上的电机控制程序直接失控,幸好是在测试台上,没造成更大损失。

从那以后,我固化了三个习惯:第一,永远用脚本做合并,不手动编辑 Hex 文件;第二,合并后必须跑校验和验证和地址连续性检查;第三,在 Uniflash 烧录前,先用命令行模式读取芯片 Flash 做一次比对。这三个习惯帮我拦住了至少五次潜在的烧录事故。

另外,C2000 的 Flash 烧录对电源稳定性比较敏感,如果烧录过程中电压波动,可能导致部分扇区写入失败。建议在烧录时用稳压电源供电,避免用 USB 直接供电。这个细节在实验室里容易被忽略,但在产线上是必须控制的变量。

希望帮到你。

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

RELATED READING

延伸阅读

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