工作经验:Bootloader - 文本格式转HEX文件
参考内容
- Python计算CRC32函数zlib.crc32()定义:Python Documentation contents - zlib
- HEX文件格式解析:优快云笔记 - 工作经验总结:Hex文件解析(作者:不吃鱼的猫丿)
声明
本文章中所有数据均进行了模糊处理,且对创作参考进行标注,如有侵权请联系我。
需求
将某种自定义的XML文件中的数据提取出来,并生成HEX文件,以供控制器Bootloader烧写。XML文件格式见 输入形式 章节,也可以简单理解为是一种HEX数据文本。
基础知识
1 Hex文件简介
- HEX文件,全称Intel HEX,是一种由Intel公司规定的标准文件格式,通常用于传输将被存储于ROM或EEPROM中的程序和数据。;
- 车载通信领域,Hex文件主要用于ECU Bootloader烧写升级;
- HEX文件每行以冒号开头,内容全部为16进制码,2个ASCII码字符表示1个Hex字节;
- HEX文件不同于Bin文件,文件中会包含地址地址信息。
如下图,分别是通过文本文档和Hexview软件打开HEX文件的示例:
2 HEX文件格式
2.1 基本结构
HEX文件的每一行代表一个记录(Record),每个记录都以冒号(:)开始,后面跟着一系列的十六进制数,最后以回车符(CR)和换行符(LF)结束。
2.2 Record格式
RECORD MARK ‘:’ | RECLEN | OFFSET | RECTYPE | DATA OR INFO | CHKSUM |
---|---|---|---|---|---|
1 Byte | 1 Byte | 2 Bytes | 1 Byte | N Bytes | 1 Byte |
- 起始字符(RECORD MARK):每行的开始是一个冒号(:)。
- 记录长度(RECLEN):表示该行 [DATA OR INFO] 字段的字节数,常见10hex、20hex等。
- 偏移地址(OFFSET):表示该行 [DATA OR INFO] 起始处的偏移地址。
- 记录类型(RECTYPE):表示该行Record的类型,可分为数据、地址和结束标志等,常见00hex、01hex、04hex,具体参考本部分2.3节。
- 数据或地址信息(DATA OR INFO):数据或者信息, [RECLEN] 定义长度,[RECTYPE] 定义类型。
- 校验和(CHKSUM):用于错误检测,计算RECLEN、OFFSET、RECTYPE、DATA OR INFO字段的十六进制累加和得S,CHKSUM=(0x100-S)&0xFF。
2.3 Record类型和说明
HEX文件中定义了几种不同的记录类型:
- 00:数据记录(Data Record),用于存储实际的数据。
- 01:文件结束记录(End of File Record),表示文件的结束,该类型Record不含数据字段。
- 02:扩展段地址记录(Extended Segment Address Record),用于指定段地址。
- 03:开始段地址记录(Start Segment Address Record),用于指定开始段地址。
- 04:扩展线性地址记录(Extended Linear Address Record),用于指定线性地址。
- 05:开始线性地址记录(Start Linear Address Record),用于指定程序的起始线性地址。
1 RECTYPE=04 扩展线性地址记录
此记录通常出现在HEX文件中某段数据的首行,声明扩展线性地址,用于表征数据的起始地址。在新的RECTYPE=04记录出现前,后续的数据类型记录均按照 扩展地址<<16 + 地址偏移 的方式确认数据地址。
- 起始字符(RECORD MARK):冒号(:)
- 记录长度(RECLEN):02hex,表示 [DATA OR INFO] 长度为2个字节;
- 偏移地址(OFFSET):0000hex,表示偏移地址;
- 记录类型(RECTYPE):04hex,表示该记录类型为扩展地址记录;
- 数据或地址信息(DATA OR INFO):0001hex,表示扩展地址。RECTYPE=04时,该字段为地址信息。扩展线性地址= 0x0001 << 16 + 0x0000 = 0x00010000,此为后续数据的起始地址;
- 校验和(CHKSUM):计算累加和得S,S = 02 + 00 + 00 + 04 + 00 + 01 = 07hex,CHKSUM=(0x100-S)&0xFF=F9hex
2 RECTYPE=00 数据记录
数据记录是HEX文件中最常见的记录类型,主要用于承载数据。
- 起始字符(RECORD MARK):冒号(:)
- 记录长度(RECLEN):0Ahex,表示 [DATA OR INFO] 长度为10个字节;
- 偏移地址(OFFSET):A000hex,表示偏移地址。此段数据的起始地址为基于扩展地址/开始地址+偏移地址。如,以上述扩展线性地址为例,此数据记录的起始地址为 0x00010000 + 0xA000 = 0x0001A000;
- 记录类型(RECTYPE):00hex,表示该记录类型为数据记录;
- 数据或地址信息(DATA OR INFO):RECTYPE=00时,该字段为数据信息。扩展线性地址= 0x0001 << 16 + 0x0000 = 0x00010000,此为后续数据的起始地址;
- 校验和(CHKSUM):计算累加和得S,S = 0A + A0 + 00 + 00 + 11 + 22 + 33 + 44 + 55 + 66 + 77 + 88 + 99 + 00 = 3A7hex,CHKSUM=(0x100-S)&0xFF=59hex
3 RECTYPE=01 文件结束记录
文件结束字段不包含数据段,是HEX文件的最后一行,其形式固定为":00000001FF"
- 起始字符(RECORD MARK):冒号(:)
- 记录长度(RECLEN):00hex,表示 [DATA OR INFO] 长度为0个字节,文件结束记录不包含 [DATA OR INFO] 字段;
- 偏移地址(OFFSET):0000hex,表示偏移地址;
- 记录类型(RECTYPE):01hex,表示该记录类型为文件结束记录;
- 数据或地址信息(DATA OR INFO):RECTYPE=01时,文件结束记录不包含 [DATA OR INFO] 字段;
- 校验和(CHKSUM):计算累加和得S= 01hex,CHKSUM=(0x100-S)&0xFF=FFhex
此处仅介绍本项目中使用的3中类型Record,其余内容请参考原作者:
优快云笔记 - 工作经验总结:Hex文件解析(作者:不吃鱼的猫丿)
输入形式
项目的输入文件是一种XML(eXtensible Markup Language)语言,通过</>标签定义数据。本文章中会隐去其他</>标签数据的数据,仅展示用于Bootloader烧写的数据,且烧写数据为自定义虚假数据,形式如下:
<Data>
<DataBlock StartAddr="106496" BlockSize="10">11223344556677889900</DataBlock>
<DataBlock StartAddr="16777216" BlockSize="239616">11223344556677889900AABBCCDDEEFF11223344556677889900AABBCCDDEEFF…</DataBlock>
</Data>
- </DataBlock>中为HEX数据,两个ASCII字符为1个字节。
- StartAddr(int)是此段数据的起始位置。
- BlockSize(int)是此段数据的长度。
代码实现
# XML文件 </DataBlock>中
import xml.etree.ElementTree as ET
def calculate_checksum(hex_data):
if len(hex_data) % 2 != 0: # 确保输入数据的长度是偶数
raise ValueError("XML Data Error")
values = [int(hex_data[i:i+2], 16) for i in range(0, len(hex_data), 2)] # 转换为字节形式
S = sum(values) # 计算累加和
CHKSUM = (0x100 - S) & 0xFF # 计算校验和
return CHKSUM
def create_hex_record(length, offset, data, record_type=0x00):
record = f":{length:02X}{offset:04X}{record_type:02X}{data}" # 此时recordCHKSUM校验和
checksum = calculate_checksum(record[1:]) # 计算校验和
return f"{record}{checksum:02X}\n" # 返回Record
def parse_xml_and_create_hex(xmlFilePath, hexFilePath):
tree = ET.parse(xmlFilePath)
root = tree.getroot()
with open(hexFilePath, 'w') as hex_file: # 读取并解析XML文件
for dataBlock in root.findall('.//DataBlock'): # 遍历</DataBlock>
startAddr = int(dataBlock.get('StartAddr'), 0) # 获取当前Block的起始地址
blockSize = int(dataBlock.get('BlockSize'), 0) # 获取当前Block的数据长度
data = dataBlock.text[:blockSize * 2] # 按照数据长度 截断数据(此处一致性由输入文件保证)
extended_address = startAddr
current_extended_address = startAddr >> 16 # 当起始地址(startAddr)长度超过2字节时,前2个字节使用扩展地址表示
if current_extended_address != extended_address: # 当RECTYPE=04提供的地址已经用完,即数据长度已大于0xFFFF,则重新定义扩展线性地址记录
extended_address = current_extended_address
ex_linear_addr_str = f'{extended_address:04X}' # 构造扩展线性地址记录
firstRecord = create_hex_record(2, 0, ex_linear_addr_str, 0x04)
hex_file.write(firstRecord)
max_length = 32 # 暂时设定为每行32字节,RECLEN=0x20
for i in range(0, len(data), max_length * 2): # 构造数据类型记录
recordData = data[i:i + max_length * 2] # [DATA OR INFO]字段
recordLen = len(recordData) // 2 # [RECLEN]字段
offset = (startAddr + i // 2) # 当前偏移量
if offset >= (extended_address << 16) + 0x10000: # 当数据量超出0xFFFF时,需要更新扩展线性地址,即扩展地址+1
extended_address += 1
ex_linear_addr_str = f'{extended_address:04X}'
firstRecord = create_hex_record(2, 0, ex_linear_addr_str, 0x04)
hex_file.write(firstRecord) # 重新写入扩展线性地址记录
offset = (startAddr + i // 2) & 0xFFFF # 重新计算偏移量
recordOffset = offset & 0xFFFF # [OFFSET]字段
record = create_hex_record(recordLen, recordOffset, recordData) # 构造Record
hex_file.write(record)
hex_file.write(":00000001FF\n") # 文件结束记录
# 定义文件路径
xmlFilePath = r"D:\testForXml.txt"
hexFilePath = r"D:\testForHex.hex"
# 根据XML生成HEX文件
parse_xml_and_create_hex(xmlFilePath, hexFilePath)
运行成果和经验分享
生成的HEX文件,先通过文本文档打开,确认以下关键信息是否正确:
- 确认文件首行记录,扩展线性地址经过计算后是否与StartAddr相等;
- 确认前几行数据记录的长度、偏移量、数据负载是否正确一致;
- 确认</DataBlock>切换时,扩展线性地址是否正确;
- 确认当BlockSize大于0xFFFF时,是否重新生成了扩展线性地址记录;
- 确认StartAddr小于2字节,或处于2字节至4字节之间时,是否鲁棒。
- …
使用HexView软件打开HEX文件,如能正常打开,则无格式错误;若打开为空白,则有格式错误。本人调试过程中遇到过如下几个代码错误:
- 记录的Checksum计算错误;
- 由于数据长度超出了0xFFFF,数据的地址出现重叠;
- 若想排除数据错误,使用zlib.crc32()函数计算HEX数据的CRC32即可。