1. Hex与Bin格式的本质区别刚接触嵌入式开发时我也曾被各种文件格式搞得晕头转向。直到有一次为了给STM32做OTA升级不得不深入研究Hex和Bin的区别才发现这其实是嵌入式工程师的必修课。Hex文件就像是带着详细快递单的包裹。它采用ASCII文本格式每行以冒号开头包含数据长度、内存地址、记录类型、数据内容和校验值。这种结构让它可以描述不连续的内存区域还能携带调试信息。我常用的IAR编译链生成的Hex文件打开后能看到这样的内容:1000000000400020210000083501000839010008DF :100010003D010008410100084501000800000000E1而Bin文件则是纯粹的二进制裸数据。它没有地址信息也没有校验机制就像把包裹里的物品直接倒出来堆在一起。Bootloader最爱的就是这种简单粗暴的格式比如ST官方烧录工具就只认Bin文件。这两种格式的典型差异可以用快递来类比Hex 快递单(地址) 包装盒(校验) 商品(数据)Bin 直接堆放的裸商品2. 手把手实现Hex解析器2.1 解析Hex文件头写解析器时我踩过的第一个坑就是没处理好文件头。标准的Intel Hex格式每行以冒号开始这个字符就像田径比赛的起跑线没找到它就会全盘错乱。这是我的验证函数bool is_valid_hex(FILE* fp) { char marker; while ((marker fgetc(fp)) ! EOF) { if (marker :) { rewind(fp); return true; } } return false; }2.2 解码数据记录Hex文件最核心的是00类型记录它携带实际要烧录的数据。解析时要注意大端序问题——地址高位在前低位在后。有次我搞反了顺序导致程序跑飞调试了整整两天。typedef struct { uint8_t length; uint16_t address; uint8_t type; uint8_t data[256]; uint8_t checksum; } HexRecord; void parse_data_record(FILE* fp, HexRecord* rec) { char hex[3] {0}; // 读取长度 fread(hex, 2, 1, fp); rec-length strtol(hex, NULL, 16); // 读取地址 fread(hex, 2, 1, fp); rec-address strtol(hex, NULL, 16) 8; fread(hex, 2, 1, fp); rec-address | strtol(hex, NULL, 16); // 读取类型 fread(hex, 2, 1, fp); rec-type strtol(hex, NULL, 16); // 读取数据 for(int i0; irec-length; i){ fread(hex, 2, 1, fp); rec-data[i] strtol(hex, NULL, 16); } // 校验和 fread(hex, 2, 1, fp); rec-checksum strtol(hex, NULL, 16); }3. 内存地址映射的玄机3.1 处理扩展线性地址第一次遇到0x04类型记录时我完全懵了。原来Hex文件用这种记录实现32位地址扩展。比如看到这样的记录:020000040800F2表示后续数据的高16位地址是0x0800要拼接到普通记录的地址前。uint32_t base_address 0; void handle_extended_linear(HexRecord* rec) { if(rec-type 0x04 rec-length 2) { base_address (rec-data[0] 24) | (rec-data[1] 16); } }3.2 生成连续Bin文件Hex可能描述碎片化内存但Bin需要连续存储。我的做法是用malloc申请足够大的缓冲区初始填充0xFF擦除状态。有个项目因为没初始化缓冲区导致未使用的区域随机值被当成代码执行。#define FLASH_SIZE (512 * 1024) // 假设最大512KB uint8_t* prepare_bin_buffer() { uint8_t* buf malloc(FLASH_SIZE); if(buf) { memset(buf, 0xFF, FLASH_SIZE); } return buf; }4. 完整转换工具的实现4.1 主程序架构经过多次迭代我的工具形成了这样的处理流程参数检查输入Hex路径/输出Bin路径文件验证Hex格式校验预扫描确定最大地址实际转换写出Bin文件int main(int argc, char** argv) { if(argc 3) { printf(Usage: %s input.hex output.bin\n, argv[0]); return 1; } FILE* hex fopen(argv[1], r); if(!hex || !is_valid_hex(hex)) { printf(Invalid HEX file\n); return 2; } uint8_t* bin prepare_bin_buffer(); HexRecord rec; while(!feof(hex)) { if(fgetc(hex) :) { parse_data_record(hex, rec); if(rec.type 0x04) { handle_extended_linear(rec); } else if(rec.type 0x00) { uint32_t full_addr base_address rec.address; memcpy(bin full_addr, rec.data, rec.length); } } } FILE* out fopen(argv[2], wb); fwrite(bin, FLASH_SIZE, 1, out); fclose(hex); fclose(out); free(bin); return 0; }4.2 实用功能扩展在实际项目中我还添加了几个贴心功能自动填充空白区域用0xFF填充地址间隙避免随机值分段输出支持生成多个Bin文件对应不同内存区域校验和计算生成Bin的同时输出CRC32校验值void calculate_crc(const uint8_t* data, size_t len) { uint32_t crc 0xFFFFFFFF; for(size_t i0; ilen; i) { crc ^ data[i]; for(int j0; j8; j) { crc (crc 1) ^ (0xEDB88320 -(crc 1)); } } printf(CRC32: 0x%08X\n, ~crc); }5. 常见问题与调试技巧5.1 地址越界处理有次客户反映转换后的程序异常最后发现是Hex文件包含超出芯片Flash范围的地址。现在我的工具会先扫描最大地址uint32_t max_address 0; void check_address_range(uint32_t addr, uint32_t len) { uint32_t end addr len - 1; if(end FLASH_SIZE) { printf(Error: Address 0x%08X exceeds flash size\n, end); exit(3); } if(end max_address) { max_address end; } }5.2 校验和验证Hex每行都有校验和但新手常会忽略验证。我的经验是校验失败就立即报错避免写入错误数据bool verify_checksum(const HexRecord* rec) { uint8_t sum rec-length; sum (rec-address 8) 0xFF; sum rec-address 0xFF; sum rec-type; for(int i0; irec-length; i) { sum rec-data[i]; } return ((sum rec-checksum) 0xFF) 0; }6. 工程化改进建议6.1 使用内存映射文件对于大容量芯片比如1MB Flash直接分配内存可能不够高效。可以用mmapLinux或CreateFileMappingWindows实现文件映射#ifdef _WIN32 HANDLE hFile CreateFile(argv[2], GENERIC_READ|GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); HANDLE hMap CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, FLASH_SIZE, NULL); uint8_t* bin MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, FLASH_SIZE); #else int fd open(argv[2], O_RDWR|O_CREAT, 0666); ftruncate(fd, FLASH_SIZE); uint8_t* bin mmap(NULL, FLASH_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); #endif6.2 集成到构建系统在Makefile中添加自动转换规则编译后直接生成可烧录的Bin文件%.bin: %.hex ./hex2bin $ $ flash: firmware.bin st-flash write $ 0x08000000这个工具虽然只有几百行代码但在我们公司的多个产品线中每天都要运行上百次。最让我自豪的是有次紧急更新时它帮我们节省了至少两小时的调试时间。
嵌入式开发实战:从Hex到Bin的格式转换工具实现
发布时间:2026/6/11 10:45:12
1. Hex与Bin格式的本质区别刚接触嵌入式开发时我也曾被各种文件格式搞得晕头转向。直到有一次为了给STM32做OTA升级不得不深入研究Hex和Bin的区别才发现这其实是嵌入式工程师的必修课。Hex文件就像是带着详细快递单的包裹。它采用ASCII文本格式每行以冒号开头包含数据长度、内存地址、记录类型、数据内容和校验值。这种结构让它可以描述不连续的内存区域还能携带调试信息。我常用的IAR编译链生成的Hex文件打开后能看到这样的内容:1000000000400020210000083501000839010008DF :100010003D010008410100084501000800000000E1而Bin文件则是纯粹的二进制裸数据。它没有地址信息也没有校验机制就像把包裹里的物品直接倒出来堆在一起。Bootloader最爱的就是这种简单粗暴的格式比如ST官方烧录工具就只认Bin文件。这两种格式的典型差异可以用快递来类比Hex 快递单(地址) 包装盒(校验) 商品(数据)Bin 直接堆放的裸商品2. 手把手实现Hex解析器2.1 解析Hex文件头写解析器时我踩过的第一个坑就是没处理好文件头。标准的Intel Hex格式每行以冒号开始这个字符就像田径比赛的起跑线没找到它就会全盘错乱。这是我的验证函数bool is_valid_hex(FILE* fp) { char marker; while ((marker fgetc(fp)) ! EOF) { if (marker :) { rewind(fp); return true; } } return false; }2.2 解码数据记录Hex文件最核心的是00类型记录它携带实际要烧录的数据。解析时要注意大端序问题——地址高位在前低位在后。有次我搞反了顺序导致程序跑飞调试了整整两天。typedef struct { uint8_t length; uint16_t address; uint8_t type; uint8_t data[256]; uint8_t checksum; } HexRecord; void parse_data_record(FILE* fp, HexRecord* rec) { char hex[3] {0}; // 读取长度 fread(hex, 2, 1, fp); rec-length strtol(hex, NULL, 16); // 读取地址 fread(hex, 2, 1, fp); rec-address strtol(hex, NULL, 16) 8; fread(hex, 2, 1, fp); rec-address | strtol(hex, NULL, 16); // 读取类型 fread(hex, 2, 1, fp); rec-type strtol(hex, NULL, 16); // 读取数据 for(int i0; irec-length; i){ fread(hex, 2, 1, fp); rec-data[i] strtol(hex, NULL, 16); } // 校验和 fread(hex, 2, 1, fp); rec-checksum strtol(hex, NULL, 16); }3. 内存地址映射的玄机3.1 处理扩展线性地址第一次遇到0x04类型记录时我完全懵了。原来Hex文件用这种记录实现32位地址扩展。比如看到这样的记录:020000040800F2表示后续数据的高16位地址是0x0800要拼接到普通记录的地址前。uint32_t base_address 0; void handle_extended_linear(HexRecord* rec) { if(rec-type 0x04 rec-length 2) { base_address (rec-data[0] 24) | (rec-data[1] 16); } }3.2 生成连续Bin文件Hex可能描述碎片化内存但Bin需要连续存储。我的做法是用malloc申请足够大的缓冲区初始填充0xFF擦除状态。有个项目因为没初始化缓冲区导致未使用的区域随机值被当成代码执行。#define FLASH_SIZE (512 * 1024) // 假设最大512KB uint8_t* prepare_bin_buffer() { uint8_t* buf malloc(FLASH_SIZE); if(buf) { memset(buf, 0xFF, FLASH_SIZE); } return buf; }4. 完整转换工具的实现4.1 主程序架构经过多次迭代我的工具形成了这样的处理流程参数检查输入Hex路径/输出Bin路径文件验证Hex格式校验预扫描确定最大地址实际转换写出Bin文件int main(int argc, char** argv) { if(argc 3) { printf(Usage: %s input.hex output.bin\n, argv[0]); return 1; } FILE* hex fopen(argv[1], r); if(!hex || !is_valid_hex(hex)) { printf(Invalid HEX file\n); return 2; } uint8_t* bin prepare_bin_buffer(); HexRecord rec; while(!feof(hex)) { if(fgetc(hex) :) { parse_data_record(hex, rec); if(rec.type 0x04) { handle_extended_linear(rec); } else if(rec.type 0x00) { uint32_t full_addr base_address rec.address; memcpy(bin full_addr, rec.data, rec.length); } } } FILE* out fopen(argv[2], wb); fwrite(bin, FLASH_SIZE, 1, out); fclose(hex); fclose(out); free(bin); return 0; }4.2 实用功能扩展在实际项目中我还添加了几个贴心功能自动填充空白区域用0xFF填充地址间隙避免随机值分段输出支持生成多个Bin文件对应不同内存区域校验和计算生成Bin的同时输出CRC32校验值void calculate_crc(const uint8_t* data, size_t len) { uint32_t crc 0xFFFFFFFF; for(size_t i0; ilen; i) { crc ^ data[i]; for(int j0; j8; j) { crc (crc 1) ^ (0xEDB88320 -(crc 1)); } } printf(CRC32: 0x%08X\n, ~crc); }5. 常见问题与调试技巧5.1 地址越界处理有次客户反映转换后的程序异常最后发现是Hex文件包含超出芯片Flash范围的地址。现在我的工具会先扫描最大地址uint32_t max_address 0; void check_address_range(uint32_t addr, uint32_t len) { uint32_t end addr len - 1; if(end FLASH_SIZE) { printf(Error: Address 0x%08X exceeds flash size\n, end); exit(3); } if(end max_address) { max_address end; } }5.2 校验和验证Hex每行都有校验和但新手常会忽略验证。我的经验是校验失败就立即报错避免写入错误数据bool verify_checksum(const HexRecord* rec) { uint8_t sum rec-length; sum (rec-address 8) 0xFF; sum rec-address 0xFF; sum rec-type; for(int i0; irec-length; i) { sum rec-data[i]; } return ((sum rec-checksum) 0xFF) 0; }6. 工程化改进建议6.1 使用内存映射文件对于大容量芯片比如1MB Flash直接分配内存可能不够高效。可以用mmapLinux或CreateFileMappingWindows实现文件映射#ifdef _WIN32 HANDLE hFile CreateFile(argv[2], GENERIC_READ|GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); HANDLE hMap CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, FLASH_SIZE, NULL); uint8_t* bin MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, FLASH_SIZE); #else int fd open(argv[2], O_RDWR|O_CREAT, 0666); ftruncate(fd, FLASH_SIZE); uint8_t* bin mmap(NULL, FLASH_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); #endif6.2 集成到构建系统在Makefile中添加自动转换规则编译后直接生成可烧录的Bin文件%.bin: %.hex ./hex2bin $ $ flash: firmware.bin st-flash write $ 0x08000000这个工具虽然只有几百行代码但在我们公司的多个产品线中每天都要运行上百次。最让我自豪的是有次紧急更新时它帮我们节省了至少两小时的调试时间。