
做嵌入式开发的老哥们估计都有过这种经历编译完固件拿到一个.hex或者.s19文件然后就得想办法处理它——有的要合并两个固件有的要裁剪出某个地址段有的要补上空白区域还有的要往固定偏移塞一个 CRC 校验值。以前我都是拿 Python 手撸脚本Hex 文件简单还好说一旦遇到 S19 这种带地址长度分级的格式再赶上换行符、校验和的问题脚本改到天亮是常有的事。后来换了 Srecord 这套命令行工具才算是把这块彻底理顺了。Srecord 是个专门处理 SREC、Intel HEX、二进制等格式文件的工具集核心命令就三个——srec_cat、srec_info、srec_cmp但组合起来能干的活非常多从裁剪、拼接、填充到生成 CRC、转格式、比对差异全都能用一条命令解决。这篇东西不打算照着官方手册念我按自己从零上手到写进构建脚本的过程来写把最常用的操作、容易踩的坑、还有命令背后为什么要这么写都一并讲清楚。无论你用的是 STM32、NXP 还是瑞萨的平台只要你的编译产物需要被做“二次加工”这篇文章就能帮上忙。1. 为什么嵌入式项目需要 Srecord 这类文件处理工具1.1 从编译产物到烧录镜像中间缺一环先说个扎心的事实Keil、IAR 或者 GCC 工具链生成的.hex、.s19文件绝大多数情况下不能直接拿去烧录。不是格式坏了而是它跟你产品量产时的需求之间往往隔着一堆“加工步骤”。举几个我实际遇到过的场景多区域合并Bootloader 和 App 是分开编译的产出两个独立 hex量产时要么用烧录软件去拼接、要么先手动合并成一个镜像。用 Srecord 一句话就完成还不会出现地址重叠的问题。空白区域填充Flash 中未被代码覆盖的区域如果随机值读回来的数据既不美观、也影响校验。量产固件一般会统一填成0xFF或者0x00这活儿让 Srecord 干最顺手。附加校验信息很多 Bootloader 会要求 App 末尾或者头部某固定地址放一个 CRC 值用来做完整性校验。编译阶段拿不到这个值只能在拿到链接产物后追加然后重新生成 hex。格式转换调试器、烧录器、产测工具支持的格式不一样有的要.bin、有的要.hex、有的偏要.s19不可能每次让同事重编一遍工程。这一类需求本质上是“构建后处理”。Srecord 就是专门干这个的它不参与编译、不参与链接只做输出文件的加工。理解了这层定位你就能明白为什么我建议把它放到每个嵌入式项目的构建流程里而不是等到要发版了再临时找个工具手动点来点去。1.2 为什么选 Srecord 而不是写脚本有人可能会说这种活用 Python 写脚本也就几十行为什么非要引入一个额外工具我这里说句公道话脚本当然能做但做不“全”。SREC 格式远比很多人想得复杂它分为 S0/S1/S2/S3 等多条记录地址长度不一样数据的校验和算法虽然简单但不同工具对格式的容忍度不同。你自己写脚本处理自己工程里的文件没问题一旦要跟别的部门、别的厂商、别的烧录器对接各种兼容性问题会消耗掉你大量的时间。Srecord 是一个有十多年历史的开源工具从 SourceForge 时代活到了现在。它内置了对 SREC、Intel HEX、Motorola S-record、二进制、Verilog、Tektronix 等格式的读写支持地址范围计算、重叠检测、CRC 生成都是现成的。而且它的命令行参数设计非常克制没有花哨交互纯文本天然适合集成到 Makefile、CMake 和 CI 脚本里。另外用命令处理还有一个隐藏好处可审计、可复现。今天你手动点了三下鼠标完成拼接明天同事问你镜像里 CRC 是怎么算的你可能要想半天但如果是 Makefile 里写的一条srec_cat命令任何人 checkout 代码之后一条make就能重新生成完全一致的固件。2. 环境准备与一条命令入门2.1 安装与基础救援Srecord 的安装非常简单Linux 各发行版基本都有现成包macOS 用 Homebrew 也能直接装。Windows 下建议在 MSYS2 或 WSL 里用原生 Windows 版本也有但我个人觉得维护价值不高。几个常见平台的安装方式我列一下# Debian / Ubuntu sudo apt-get install srecord # CentOS / RHEL sudo yum install srecord # macOS brew install srecord # MSYS2 (Windows) pacman -S mingw-w64-x86_64-srecord装完之后验证一下三个核心命令是否可用srec_info --version srec_cat --version srec_cmp --version看到版本号就说明环境没问题。2.2 第一次处理查看文件信息拿到一个 hex 文件第一件事永远是“看看里面有什么”。我一般直接用srec_info这个命令srec_info firmware.hex输出大概是这样的Motorola S-Record format: header: MY_PROJECT data: 0x08000000 - 0x0801FFFC address: 0x08000000 - 0x0801FFFC execution start address: 0x08000185 data: 0x08020000 - 0x0802FFFF address: 0x08020000 - 0x0802FFFF execution start address: 0x08020000这几行信息能告诉你很多东西文件格式是 S-recordS19/S28/S37还是 Intel HEX。地址范围数据分布在哪些地址区间有没有空洞。执行起始地址有些链接器会把入口地址写进文件这信息可以用于反汇编或调试器加载。我拿到任何固件都会先跑一遍srec_info确认地址范围和分段情况对不对再决定后续怎么做。这就像拆快递先看清单省的后面操作时“货不对板”。2.3 第一个常用操作格式转换以前需要把 hex 转 bin我第一时间想到的是objcopy。但 objcopy 的默认行为是直接把数据连续铺开如果 S-record 文件里有地址洞它会帮你全部补零这在某些场景下非常危险——你原本只写了 32KB 代码但地址跨度是 128KB转出来的 bin 文件就有 128KB里面塞满了0x00烧录或者分析时完全被误导。用 Srecord 转换格式可以先用-crop裁剪出实际范围再转srec_cat firmware.hex -crop 0x08000000 0x08020000 -o firmware.bin -binary这样转出来的.bin就是连续的、只包含实际数据区间的镜像。如果不加-crop直接转Srecord 也会忠实地把空洞填成空白默认 0xFF 或 0x00但关键是你得心里有数。更多关于-crop和填充的细节下一节展开。3. 核心命令解析srec_cat 里最容易搞混的三类操作3.1 裁剪、填充与拼装的基本逻辑Srecord 整套工具的设计哲学是把“输入”和“输出”变成一条流水线。srec_cat这个名字看着像个“拼接命令”但它实际上是那个最全能的“处理器”——它是根据你给的参数把输入文件的内容按顺序处理后再输出到目标文件。我个人把它拆成三类核心操作来记这样就不会乱操作类型命令参数作用常见场景裁剪-crop 起始地址 结束地址只保留指定地址区间内的数据从整个 Flash 镜像中提取 Bootloader 段填充-fill 填充值 起始地址 结束地址把指定地址区间内的空洞填上Flash 空白区统一填0xFF偏移-offset 偏移值把数据整体移动地址把 App 从加载地址挪到运行时地址这三个操作经常组合使用顺序不同结果也可能完全不同。下面用一个实战例子把所有操作串起来。假设我手里有一个app.hex它的真实代码分布在0x08010000 - 0x0801F000现在要做三件事把范围裁剪成0x08010000 - 0x0801F000排除掉头部的一些配置信息把这段范围里未被占用的空隙全部填充为0xFF把整段数据搬移到0x08020000开始的地址上。对应的命令是srec_cat app.hex \ -crop 0x08010000 0x0801F000 \ -fill 0xFF 0x08010000 0x0801F000 \ -offset 0x00010000 \ -o app_relocated.s19这条命令执行完就会生成一个地址范围在0x08020000 - 0x0802F000的连续 S19 文件。注意-offset的正负号正值往后挪负值往前挪。还有一点要特别提醒偏移操作是在裁剪和填充之后才执行的所以填充范围必须按照原始地址来写而不是偏移后的地址。3.2 地址区间是半开区间一个会坑到所有人的细节Srecord 的地址参数默认是“起始地址包含结束地址不包含”也就是数学上的半开区间[start, end)。这个细节坑过很多人包括我自己。举个例子srec_cat app.hex -crop 0x08000000 0x08010000 -o first_64k.hex这个命令保存的数据范围是0x08000000到0x0800FFFF共 0x1000064KB字节但0x08010000这个地址本身的数据是不会被包含进来的。为什么要用半开区间因为这样设计两个相邻的区间可以无缝拼接[A, B)和[B, C)中间没有重叠也没有遗漏。这在做内存分区时非常自然。如果你习惯用闭区间[start, end]去思考就会犯一个经典错误想把0x08000000到0x08010000前 64KB 1 个字节都裁出来结果写成了-crop 0x08000000 0x08010000最后发现数据少了一块。要真想保留0x08010000这个地址必须写成0x08010001。3.3 hex 文件合并时的重叠检测-cat系列命令是专门用来合并多个文件的Srecord 对重叠区域的态度非常严格。如果两个输入文件的地址区间有重叠srec_cat默认会报错拒绝输出这是为了保护你不被静默地覆盖数据。我实际工作中经常遇到的一个场景是Bootloader 和 App 分区明明是相邻的但由于链接脚本里定义的 Flash 区域有冗余两个 hex 文件在边界处多出了几个地址的重叠。这种情况想合并不能直接-cat而要在合并前先用-crop把重叠部分裁掉srec_cat boot.hex -crop 0x08000000 0x08010000 \ app.hex -crop 0x08010000 0x08040000 \ -o combined.hex这样两个文件的输出区间就是无缝接合的不会触发重叠检测。另外如果合并时同一地址真的发生了数据冲突Srecord 会提示类似input line ... is out of order或data conflict的错误。看到这类信息不要慌先检查链接脚本里 Flash 分区是否真的重叠了这通常意味着内存布局有问题。4. 进阶实战CRC 校验、BCS 校验和与执行记录4.1 在固件末尾自动填充 CRC32嵌入式产品做 OTA 升级时Bootloader 拿到新固件第一件事就是做完整性校验最常见的就是 CRC32。Srecord 内置了 CRC 生成功能一条命令就能在指定地址写入 CRC 值。拿 STM32 举例假设 App 固件有效数据范围是0x08008000到0x0801BFFC最后 4 个字节0x0801BFFC到0x0801C000我想放 CRC32。命令如下srec_cat app.hex \ -fill 0xFF 0x08008000 0x0801C000 \ -crop 0x08008000 0x0801BFFC \ -crc32-b-e 0x0801BFFC \ -o app_with_crc.hex这条命令到底做了什么我拆开解释首先把整个0x08008000 - 0x0801C000范围内的空白全部填为0xFF这样计算 CRC 时就不会因为随机空洞导致结果不稳定。然后裁剪掉最后 4 个字节0x0801BFFC - 0x0801C000这 4 个字节是留给 CRC 值本身的。-crc32-b-e 0x0801BFFC的意思是计算当前数据段的 CRC32并把这个值以大端字节序写到地址0x0801BFFC处。-crc32-b-e里的-b表示 bit 反转reflected algorithm-e表示最终结果再做一次 XOR对应的是 zlib/ST 标准里常用的 CRC32 算法。如果你的 Bootloader 用的是CRC32/MPEG-2之类不带反转的算法就得用-crc32-b-e对应的另一套参数-crc32-c之类具体查一下 Srecord 的手册即可。不同平台 CRC 算法可能不同务必和 Bootloader 的校验代码保持一致否则会出现“明明算对了但校验不过”的诡异现象。4.2 生成执行起始记录方便调试器加载很多调试器支持通过 S-record 文件里的执行起始记录execution start address来直接定位PC。但并不是所有编译工具链都会在输出文件里带上这个记录这时候 Srecord 可以手动帮你加上。假设入口地址是0x08000185命令如下srec_cat app.hex -execution-start-address 0x08000185 -o app_start.hex加上这个记录之后用某些调试器直接加载 hex 文件时它就会自动跳转到入口地址调试起来会很方便。如果生成的 S19 文件里没有执行起始记录不妨试试这个命令。4.3 文件比对srec_cmp 的一百种用法发布固件之前很多人都会做一遍“重新编译两次确认文件一致”的操作。用cmp或diff直接比较 hex 文本往往会因为文件头注释、记录切分方式不同而误报差异——明明数据一样文本却不一样。这时候用srec_cmp就非常省心它比较的是数据内容而非文本格式。srec_cmp build_old.hex build_new.hex如果两份文件的地址和数据完全一致命令返回 0 退出码静默结束如果有差异会输出差异地址和值。这个命令在 CI 里很有用比如“确认某次代码改动只影响了预期地址段”就可以用-crop裁出关心区域再比较防止无关区域扰乱了 diff。5. 常见问题与排查技巧实录5.1 输出文件为空或“no data”这种情况十有八九是裁剪范围写错了。如果你-crop 0x08010000 0x08020000但输入文件的数据范围是0x08000000 - 0x0800FFFF那结果就是空文件。Srecord 处理完裁剪后如果没有数据它会直接不生成输出文件或者生成一个空文件——取决于具体参数。排查方法很简单先用srec_info看输入文件的数据范围再检查自己的-crop范围注意半开区间。5.2 填充值到底填 0xFF 还是 0x00这问题没有标准答案取决于芯片的 Flash 空白状态。绝大多数 Nor Flash 擦除后是0xFF所以填充0xFF是最常见的但某些芯片或分区策略会要求留0x00比如部分 Bootloader 用0x00作为“无效”标记。还有一类场景是 OTA 差分包处理填充值必须和压缩算法假定的“空洞值”一致否则校验全部失败。所以填充之前先看一下芯片参考手册里 Flash 的擦除值是什么。5.3 校验和报错S-record checksum error这个错误通常在读取一个“不标准”的 S19 文件时出现。常见原因有文件被某些 Windows 编辑器打开过把换行符改掉了导致解析异常文件本身就是其他工具生成的有 BUG 输出使用了错误的输入格式类型。处理方式很简单先用srec_info看一下文件能否正常解析如果它自己都报错那说明问题出在源头而不是 Srecord 本身。5.4 大文件转二进制时崩溃或内存溢出嵌入式固件一般很小但如果你想处理的是外部 Flash 镜像像是 16MB 的串行 Flash 全量 bin那么直接把 S-record 转成 binary 时二进制文件可能非常大。Srecord 的处理方式是直接代表整个地址空间中间的空洞全会被填掉。这种场景我一般建议先-crop裁剪成多个小文件再分别处理避免一次性生成超大镜像。6. 把 Srecord 集成进构建系统Makefile、CMake 与 CI6.1 Makefile 的典型写法我最早把 Srecord 用起来就是在 Makefile 里加了一个 target。每次编译完自动生成带 CRC 的烧录文件# 变量定义 OBJ_HEX : build/app.hex CRC_HEX : build/app_crc.hex FLASH_START : 0x08000000 FLASH_END : 0x08020000 CRC_ADDR : 0x0801FFFC # 默认编译目标 all: $(CRC_HEX) $(CRC_HEX): $(OBJ_HEX) srec_cat $ \ -fill 0xFF $(FLASH_START) $(FLASH_END) \ -crop $(FLASH_START) $(CRC_ADDR) \ -crc32-b-e $(CRC_ADDR) \ -o $ # 清理 clean: rm -rf build这个写法有个好处只要app.hex变了app_crc.hex就会重新生成不会出现“改了代码忘了更新 CRC”的尴尬。6.2 CMake 的 add_custom_command用 CMake 的嵌入式项目也很多用add_custom_command实现同样的功能add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/app_crc.hex COMMAND srec_cat ${CMAKE_BINARY_DIR}/app.hex -fill 0xFF 0x08000000 0x08020000 -crop 0x08000000 0x0801FFFC -crc32-b-e 0x0801FFFC -o ${CMAKE_BINARY_DIR}/app_crc.hex DEPENDS ${CMAKE_BINARY_DIR}/app.hex COMMENT Generating CRC32-protected firmware image )这里注意DEPENDS一定要写对否则增量编译时可能不会重新生成处理后的文件。6.3 CI 流水线中的固件校验现在的嵌入式项目基本都有 CI。我习惯在 CI 里加一步“构建后处理”和一步“文件比对校验”编译生成原始 hex用 Srecord 生成带 CRC 的烧录包用srec_cmp把当前产物和上次发布版本做对比确认没有意外差异再用srec_info检查地址范围、执行入口是否符合预期。这一套下来发版前的很多低级错误都被自动化拦截了。以前手工拿文件比对一忙起来就容易漏掉细小的地址偏移问题现在全都交给命令处理省心太多。6.4 配合其他工具链的组合用法Srecord 不只跟自家命令搭配它跟objcopy、hexdump、checksum这些工具也能组合。比如我想快速确认某个 bin 文件的 CRC32 是否正确可以先把 bin 转成 srecsrec_cat firmware.bin -binary -offset 0x08000000 -o firmware.s19然后继续用srec_cat计算 CRCsrec_cat firmware.s19 -crc32-b-e 0x0801FFFC -o /dev/null很多烧录器、上位机读的其实都是这种派生文件先转换再计算能确保两边拿到的是同一个东西。7. 特殊领域扩展从“格式处理”到“镜像合规”7.1 OTA 差分包生成现在很多产品都支持 OTA而 OTA 差分包往往要求“只包含变化区域”。Srecord 虽然没有直接生成差分包的命令但你可以用srec_cat分别裁剪出新旧版本的 bin再用支持差分压缩的工具去处理。关键是裁剪范围必须准确这就要用到srec_info先定位数据边界。7.2 安全启动与签名区域的预留很多带安全启动的芯片会在固件的固定偏移处预留一定字节用于签名或者消息认证码。编译链接脚本里一般会预留位置但实际签名和 MAC 计算是在编译后做的这时 Srecord 的-fill就特别有用先把预留区填充好计算完签名再把签名填回去。由于 Srecord 能精确控制字节范围整个过程可以写成脚本完整复现。7.3 多镜像合并烧录的自动化量产时如果一片 Flash 里要烧 Bootloader、App、字库、配置文件等多个镜像各自的地址区段不同手动用烧录器逐个操作容易出错。用 Srecord 把它们合并成一个 S19 文件产线只要烧一个文件即可。地址边界、填充策略提前定好产线效率和可靠性都提高不少。8. 我对 Srecord 的一点使用体会Srecord 这工具看着不起眼但确实是我从写脚本处理 hex 文件的时代“解放”出来的关键。它最大的价值不是某个单一功能而是把嵌入式文件处理这件事做成了可复用、可脚本化、可审计的流程。有了它你的构建流程里不需要再依赖某个“老同事写的 Python 脚本”也不需要担心换一个人就处理出不同的结果。最后补充一个小技巧Srecord 的手册man srec_cat非常详细但读起来不够“场景化”。我建议你按“我要做什么”去查参数而不是从头到尾通读。遇到一个需求先想清楚是对数据范围做裁剪、填充、偏移还是计算校验、转换格式再去手册里找对应参数这样学习效率最高。用熟了之后你会发现原来困扰半天的文件加工问题往往就是一条命令的事。