资讯详情

资讯详情

基于STM32的离线下载器:串口ISP与SPI Flash固件烧录实战

做过批量烧录的朋友应该都有体会贴片回来几十块板子搬着电脑蹲在产线旁边一块接一块地按下载按钮偶尔还要拔下调试器看日志。遇到客户现场升级更头疼一个功能要改固件人都到现场了总不能背一台电脑和一堆线。所以我花了一段时间用手头最常见的 STM32F103C8T6 做主控做了一个完全脱离电脑的离线下载器固件事先灌进外挂 SPI Flash现场接上目标板按一下按键自动完成擦除、编程、校验。整个过程不依赖电脑由一颗主控芯片和几个小器件完成全部工作。这个项目的核心就是三件事把目标固件安全地存下来、通过一个可靠的下载协议送进目标芯片、用清晰的反馈告诉操作者结果。这个方案特别适合三类朋友一是做小批量产或经常跑客户现场的技术人员二是想深入理解 STM32 启动机制和串口 ISP 协议、SWD 协议的学生和开发者三是有一定嵌入式基础、想做点实用小工具的人。即使你之前没写过大项目只要会 STM32 点灯、用过串口跟着下面的思路走一圈也能把这个下载器做出来。我会把整个链路拆开来讲为什么选这个方案、协议底层的原理、硬件怎么连线、固件怎么写、PC 端工具怎么做最后给你一个能直接抄的完整实操记录。1. 项目概述这玩意儿到底解决什么问题1.1 在线下载解决不了的三个场景很多人觉得下载固件这种事用调试器连电脑不就完了吗这个想法在日常生活中没问题但到了真实干活的环境就完全不一样。我遇到过三个非常有代表性的场景。第一个是小批量产。贴片厂把板子焊好寄回来一共 30 块每块都要烧录一遍程序。如果全部从电脑下载就算每次只要 30 秒你也得坐在那里盯一个多小时中间还可能遇到某块板接触不良要反复插拔调试线。更麻烦的是不同的板子可能要留不同版本的程序你要在工程里来回切换 target特别容易出错。第二个场景是现场升级。设备已经装到客户那边工程师到现场发现程序有 bug需要升级固件。这时候背着电脑、调试器、各种线材跑过去先要装驱动、打开 IDE、确认调试器型号再折腾半天才能下载。大部分售后问题其实不需要调试只需要把新固件刷进去用离线下载器过去按一下按钮就能解决。第三个场景是产线分工。批量生产时负责烧录的工人不一定懂单片机。他们需要的是操作最简单、反馈最明确的工具接上线按一个键指示灯变绿就是成功变红就是失败。你不能要求每个人都学会用 IDE。离线下载器的意义就是把固件分发、擦除、编程、校验这几件事全部封装在设备内部让操作者只面对一个按键和几个灯。这个项目的本质就是把下载这件事从 PC 和调试器身上解耦。目标固件存在你手头一个 SPI Flash 芯片里下载器的主控是一个自由实现的下载主机它按约定的协议与目标芯片的出厂 Bootloader 或片上调试接口通信完成固件写入。1.2 方案选型串口 ISP 还是 SWD做离线下载器首先要想清楚用哪种物理接口和协议去写目标芯片。STM32 系列最常用的两条路是串口 ISP 和 SWD。串口 ISP 依赖的是 STM32 出厂时固化在系统存储器里的 Bootloader。你把目标的 BOOT0 引脚置为高电平再复位它就从系统存储区启动运行一段厂商写好的串口引导程序。这段程序提供一套公开的协议外部设备可以通过串口对它发命令完成读、擦、写、校验等操作。这套方式的优点是协议稳定、底层逻辑透明而且不需要目标芯片的调试接口工作只要芯片内置 Bootloader 没有被覆盖就能用。缺点是速度一般写入速度取决于波特率实测在 115200 下面大概每秒不到 10KB而且需要控制目标芯片的 BOOT0 和复位引脚接线会多两根。SWD 是 ARM Cortex-M 内核在芯片上的调试接口两线制SWDIO 和 SWCLK。它最大的优势是速度快而且除了下载还能读内存、控制内核执行、单步调试。缺点是对主控来说协议实现要复杂得多你不仅要处理 SWD 物理层时序还要理解 DAP 抽象层访问 AP、访问系统总线、操作内核调试寄存器最后还要往目标 RAM 里填一段下载算法才能烧 Flash。相当于在一个小系统里再做一个迷你调试器。做第一版时我的原则是先跑通再优化。串口 ISP 方案逻辑简单、出错容易查用万能表就能确认每一步信号是否正常。SWD 方案留作后续升级方向硬件接口先在板子上预留好。所以接下来的主线按串口 ISP 来讲SWD 的原理单独拆一部分方便你理解两者的差异。1.3 整机架构与工作流程整个下载器的硬件非常紧凑。主控是一颗 STM32F103C8T6工作频率 72MHz它负责两件事一是驱动外挂的 W25Q64 SPI Flash存放目标固件和相关配置二是按串口 ISP 协议与目标板通信。外设上我留了两路串口一路连接 PC 烧录口用于把固件灌进 SPI Flash另一路连接目标芯片的串口 ISP 接口。按键用来触发下载三颗 LED 分别指示运行中、成功、失败另外留了目标 BOOT0 和 RST 的控制引脚。整套系统的工作流程分成两阶段。第一阶段叫灌装在电脑上把工程编译出来的 bin 文件通过一个 Python 脚本和 USB 转串口模块写入下载器的 SPI Flash同时记录文件大小、CRC 校验值等信息。第二阶段叫离线下载拿下载器接上目标板按下按键主控先从 SPI Flash 读出固件然后对目标芯片做同步握手、整片擦除、按块写入、回读校验最后把目标复位到运行状态。整个过程从按下按键到绿灯亮起大约 10 到 20 秒取决于固件大小。2. 核心原理想抄作业先搞懂协议2.1 启动模式与出厂 Bootloader 的工作机制STM32F103 的启动方式由 BOOT0 和 BOOT1 两个引脚决定芯片上电后会根据这两个引脚的电平选择从哪里取复位向量。BOOT0BOOT1启动区域对应地址低任意主 Flash0x08000000高低系统存储器0x1FFFF000高高内嵌 SRAM0x20000000正常情况下程序运行在主 Flash对应的就是 BOOT0 为低。如果你想进入出厂 Bootloader就把 BOOT0 拉高、BOOT1 保持低然后复位芯片。芯片会从 0x1FFFF000 开始执行这段代码是芯片出厂前固化好的 ROM用户没法擦除或修改。对 STM32F103 来说这片出厂 Bootloader 支持的通信接口里有串口默认主要使用 USART1 的 PA9 和 PA10这就是串口 ISP 方案的物理基础。出厂 Bootloader 的工作逻辑并不复杂。它上电后会初始化时钟和串口然后静静等待第一个同步字节 0x7F。如果收到合法同步字节就返回一个确认字节 0x79之后进入命令循环。每条命令有固定的格式Bootloader 会校验命令码、地址、长度和数据然后执行对应操作并回 ACK 或 NACK。启动模式的切换意味着下载器必须有能力控制目标芯片的 BOOT0 和复位引脚不能在接线时靠人去拨开关。这也是整套硬件设计里必须重点考虑的环节后面我会专门说明。2.2 串口 ISP 协议的关键帧格式与校验规则串口 ISP 协议在芯片原厂的 AN3155 文档里有详细定义这里只把工程中真正用得上的部分讲透。通信双方是主机下载器和从机目标芯片的 Bootloader所有数据经过串口传输常见配置是 8 位数据位、无校验、1 位停止位波特率通常选 115200。握手时主机发送单字节 0x7F目标芯片的 Bootloader 如果状态正常会回复 0x79也就是 ACK。如果收到 0x1F那是 NACK说明命令或者参数没通过校验。如果收到 0x76表示芯片正忙需要等一下再重试。每个命令的发送规则是先发命令码再发命令码的取反字节然后等待 ACK。比如向目标芯片发送写内存命令就是先发 0x31再发 0xCE然后等目标回 0x79。取反校验相当于一个简单但有效的保护能避免串口噪声把命令码冲错。针对 F103 最常用的几个命令如下命令功能关键参数0x00Get 版本和命令表无0x01读取芯片 ID无0x11读内存起始地址 长度0x21跳转执行目标地址0x31写内存起始地址 长度 数据0x43擦除页号或全片0x63 / 0x73写保护/读保护选项参考原厂手册写内存命令是最核心的。完整流程分三段第一段是命令码 0x31 加取反 0xCE等待 ACK第二段发送 4 字节大端序地址再加上前面 4 个字节的异或校验值等待 ACK第三段发送一个字节的长度值实际长度减 1等待 ACK最后连续发送这么多字节的数据等待 ACK。F103 的 Bootloader 一次最多接收 256 字节所以长固件要分块每块 256 字节逐块写。擦除命令 0x43 在 F103 上支持全片擦除参数是页号 0xFF。擦除完成后芯片内部 Flash 变为全 0xFF之后才能写。有些新系列芯片用 0x44 扩展擦除命令页面粒度是 2KB参数格式略有差别但基本思路一样写代码时只要针对目标系列分支判断即可。2.3 SWD 协议与调试域访问如果你想在这个项目上做进阶了解 SWD 是绕不开的。SWD 只有两根信号线SWDIO 是双向数据线SWCLK 是时钟线由主机驱动。每次传输以主机发送 8 位请求包开始包含起始位、传输方向位、访问域选择、寄存器地址位、奇偶校验位和停止位。目标芯片收到请求后会回 3 位 ACK 确认然后主机或目标根据方向继续传输 32 位数据。整个 SWD 访问模型分为两层DP 层和 AP 层。DP 是调试端口负责管理整个调试链路比如 IDCODE、CTRL/STAT 寄存器AP 是访问端口负责实际访问芯片内部的总线内存最常用的是 MEM-AP。主机写目标内存的流程是先通过 DP 的 SELECT 寄存器选中某个 AP再向 AP 的 TAR 寄存器写入目标地址然后读写 DRW 寄存器完成数据访问。光能读写系统总线还不够Cortex-M3 内核默认正在运行用户程序如果不把它停下来Flash 操作会冲突。所以要操作 DHCSR 寄存器设置 C_DEBUGEN 和 C_HALT让内核停止再用 DEMCR 寄存器打开跟踪控制然后把一段叫做 Flash Loader 的小程序下载到目标芯片的 RAM 里让内核执行这段小程序来擦写 Flash。整个过程绕了一大圈但换来的是速度快和通用性高这也是很多商业离线烧录器系统的核心原理。作为入门验证我建议你先别急着做完整 SWD 下载第一步只写一个读 IDCODE的功能。如果能正确读回芯片 ID说明 SWD 物理层时序跑通了之后再扩展内存访问和下载算法会顺利得多。2.4 为什么我把串口 ISP 作为首选落地方案做工具类项目有个朴素的规律先保证 80% 场景能用再考虑剩下的 20%。串口 ISP 方案在工程上最大的优点是可以分段验证。握手不行就查接线命令没响应就量电平每个环节都用最直接的方法定位不需要逻辑分析仪也能干活。SWD 一旦卡了时序问题比如 SWCLK 频率太高、数据建立时间不够排错难度会成倍上升。从产线场景看串口 ISP 的写入速度其实也够用。一块 20KB 的固件按 115200 波特率换算大约 2KB/s写完全片加校验大概 15 秒左右。产线烧录讲究的是稳定和一键化这比单纯追求速度更贴合实际需求。当然如果你的固件动辄几百 KB或者需要同时烧很多颗芯片那就值得把 SWD 模式做出来速度优势会非常明显。3. 硬件设计与材料清单3.1 器件选型一颗 C8T6 打底外挂 SPI Flash整套板子的器件几乎没有冷门型号基本都是手边常备的。主控选 STM32F103C8T6 是因为它便宜、资料多、引脚够用64KB 内部 Flash 用来放下载器本身的固件完全够20KB SRAM 跑协议栈和缓冲区也不紧张。外挂 Flash 选 W25Q64容量 8MB可以放很多个目标固件如果量小用 W25Q16、W25Q32 也行驱动代码基本通用。器件型号数量作用主控STM32F103C8T61下载器主机SPI FlashW25Q641存储目标固件按键轻触开关2烧录触发、模式切换LED红/绿/黄3状态指示三极管NPN 或 NMOS1目标复位控制稳压AMS1117-3.31供电晶振8MHz 无源1主时钟USB 转串口CH340 模块1PC 灌装排针2.54mm若干目标接口用面包板搭一个原型是可以的但建议至少打到洞洞板或画一块 PCB因为目标线上有 BOOT0、RST、串口走线太乱容易接触不良调试时会怀疑人生。3.2 完整接线定义与原理说明主控引脚分配要提前规划好。SPI Flash 挂在 SPI1 上PA5 做 SCK、PA6 做 MISO、PA7 做 MOSI片选我用 PA4 软件控制。目标串口用 USART1PA9 作为下载器发送端接目标板 PA10PA10 作为接收端接目标板 PA9。PC 灌装串口用 USART2PA2 和 PA3 接 CH340 模块。目标 BOOT0 控制用 PC5目标复位控制用 PC6。按键接 PB1 和 PB2三颗 LED 接 PB12、PB13、PB14。这里有两个电气细节要特别说。BOOT0 控制引脚不能简单用一个电平去推因为不同目标板 BOOT0 的下拉电阻不一样。有的最小系统板自带 10k 下拉有的没有。我用一个 NPN 三极管做控制集电极接目标 BOOT0发射极接地基极通过 4.7k 电阻接主控 IO。当主控输出高电平时三极管导通BOOT0 被拉到低输出低电平时三极管截止BOOT0 由目标板的上拉状态决定。配合在目标 BOOT0 引脚上留一个 10k 上拉电阻到目标 VCC这样在主控不驱动时 BOOT0 自然保持高方便进入系统存储器启动。使用时三极管导通即 BOOT0 为低运行目标程序三极管截止即 BOOT0 为高进入 ISP。逻辑上正好是反的所以我固件里把对应的引脚电平定义写清楚避免在项目后期晕头转向。另一个是目标复位。复位脚 NRST 是低有效目标板一般已经有上拉和复位电路。下载器要做的只是把 NRST 短暂拉低我用一颗开漏输出的 NPN 三极管集电极接目标 NRST发射极接地主控 IO 高电平输出时拉低复位低电平时释放复位。不会干扰目标板自己的复位按钮和外部看门狗复位电路。目标接口我定义成一个 6Pin 排针1 脚 VCC、2 脚 GND、3 脚 RX、4 脚 TX、5 脚 BOOT0、6 脚 RST。注意串口交叉关系下载器的 RX 要接目标的 TX下载器的 TX 要接目标的 RX。3.3 PCB Layout 的几点叮嘱如果你决定打板布局上注意几个点就能少踩坑。SPI Flash 尽量靠近主控放置MISO、MOSI、SCK 三根线不要绕太远SPI 频率如果工作在 18MHz 以下问题不大但信号线尽量短是原则。电源部分3.3V 入口处放一个 10uF 的钽电容加 100nF 陶瓷电容主控处再放一个 100nF 去耦电容。晶振周围不要走高速信号线负载电容按 8MHz 晶振手册要求一般用 20pF 到 33pF 的经验值具体的要看晶振规格。目标接口那排排针放在板子边缘全部引脚标注丝印尤其是 BOOT0 和 RST。别小看这块丝印实际操作时省了很多查线的时间。三颗 LED 放在板子正面容易看到的位置烧录键和模式键不要挨得太近防止误按。板子可以做成双面板尺寸控制在 5cm x 3cm 左右这样拿在手里像一个小 U 盘去现场也方便。4. 下位机固件实现状态机与底层驱动4.1 固件分区与存储格式下载器的代码是跑在主控内部 Flash 里的这块程序用在线调试器直接烧进去就行平时不会再动。真正负责存放用户固件的是外挂 W25Q648MB 空间如果简单地从第 0 地址存 bin后面想扩展多固件管理会非常麻烦。我在 W25Q64 里做了分区前面的几个 4KB 扇区存放目录信息后面的空间按 4KB 对齐存放固件数据。目录项的结构定义如下#pragma pack(push, 1) typedef struct { uint32_t magic; // 固定为 0x424C4631即 BLF1 uint16_t file_index; // 槽位编号从 0 开始 uint16_t reserved; // 保留 uint32_t bin_size; // bin 文件实际大小 uint32_t entry_addr; // 下载后运行的入口地址一般填 0x08000000 uint32_t crc32; // bin 数据的 CRC32 char name[16]; // 固件名称 } FileHeader_t; #pragma pack(pop)这个头部固定占 32 字节写入目录区时剩余空间按 0xFF 填充留足一个扇区的余量。固件数据从 W25Q64 的 0x1000 地址开始存放一块 bin 至少要占一个扇区多余部分填充 0xFF。选择扇区对齐是为了以后擦除和重写方便避免新旧固件交叠导致脏数据。目录信息是什么时候写入的由 PC 端灌装脚本负责脚本先把 bin 数据和头部组织好加上 CRC然后通过串口通知下载器写入 SPI Flash。下载器在灌装阶段只负责搬运不具备主动生成 bin 的能力这个划分让代码职责非常清晰。4.2 驱动 SPI Flash 的正确姿势W25Q64 的驱动本质是围绕几个 SPI 命令写读写函数。初始化时先把 SPI1 配成主机模式、8 位数据帧、SPI Mode 0CPOL0、CPHA0速率先设到 18MHz跑稳定了再往上提。读 Flash 没有任何风险发 0x03 命令后跟 24 位地址然后连续读字节。写就麻烦一点必须先发 0x06 写使能命令再发 0x02 页编程命令加地址加数据写完要轮询状态寄存器的 WIP 位直到写入结束。擦除也有讲究0x20 是 4KB 扇区擦除0xD8 是 64KB 块擦除全片擦除用 0xC7。写入之前对应区域必须已经是 0xFF否则写进去的数据和 Flash 原有内容做与操作会产生脏数据。下面是我常用的扇区擦除和页写函数void W25Q_EraseSector(uint32_t sector_addr) { W25Q_CS_LOW(); SPI_SendByte(0x06); // 写使能 W25Q_CS_HIGH(); W25Q_CS_LOW(); SPI_SendByte(0x20); // 扇区擦除 SPI_SendByte((sector_addr 16) 0xFF); SPI_SendByte((sector_addr 8) 0xFF); SPI_SendByte(sector_addr 0xFF); W25Q_CS_HIGH(); W25Q_WaitBusy(); // 等 WIP 清零 } void W25Q_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { W25Q_CS_LOW(); SPI_SendByte(0x06); // 写使能 W25Q_CS_HIGH(); W25Q_CS_LOW(); SPI_SendByte(0x02); // 页编程 SPI_SendByte((addr 16) 0xFF); SPI_SendByte((addr 8) 0xFF); SPI_SendByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI_SendByte(buf[i]); } W25Q_CS_HIGH(); W25Q_WaitBusy(); }写入边界要小心页编程最多 256 字节而且不能跨页写入。如果你的缓冲数据跨了两页边界拆成两次写。我在第一版因为懒得处理跨页结果固件稍微大一点就在某个地址反复出错排查了好久才意识到是页边界问题。4.3 实现 ISP 协议栈的细节ISP 协议栈是下载器固件的核心。我设计的结构分两层底层是串口收发和超时管理上层是协议命令函数。串口接收必须带超时不能死等。目标芯片可能没上电、没进 Bootloader、接线断了任何情况都不能让主控卡死在等待 ACK 上。我用一个简单的超时函数每收到一个字节刷新计时器超过 200ms 没有新数据就返回超时。所有底层函数都返回成功、失败或超时上层根据返回值处理异常。协议函数的关键在于严格按帧格式组装和校验。以写内存命令为例我前面已经拆过帧对应代码长这样uint8_t ISP_WriteMemory(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd 0x31; uint8_t check; USART_SendByte(cmd); USART_SendByte(cmd ^ 0xFF); if (ISP_WaitAck() ! 0x79) return 0; uint8_t addr_frame[5]; addr_frame[0] (addr 24) 0xFF; // 大端序 addr_frame[1] (addr 16) 0xFF; addr_frame[2] (addr 8) 0xFF; addr_frame[3] addr 0xFF; addr_frame[4] addr_frame[0] ^ addr_frame[1] ^ addr_frame[2] ^ addr_frame[3]; for (uint8_t i 0; i 5; i) USART_SendByte(addr_frame[i]); if (ISP_WaitAck() ! 0x79) return 0; USART_SendByte((uint8_t)(len - 1)); if (ISP_WaitAck() ! 0x79) return 0; for (uint32_t i 0; i len; i) USART_SendByte(buf[i]); if (ISP_WaitAck() ! 0x79) return 0; return 1; }实际项目里还要处理一种情况串口发送完最后一字节后目标芯片的 Bootloader 需要一点时间收数据和写 Flash。所以每包写完要加 10ms 到 20ms 的延时再发下一包否则容易出现 NACK。这个延时不能太久太久整片烧录时间拉长也不要太短根据实测 15ms 比较合适。4.4 主循环状态机与按键处理下载器固件整体用一个状态机驱动状态迁移非常直观IDLE → PREPARE → SYNC → ERASE → WRITE → VERIFY → DONE任何一步出问题迁移到 ERROR红灯常亮。按下按键后系统从 IDLE 进入 PREPARE读 SPI Flash 目录检查固件是否存在、CRC 是否完整。SYNC 阶段控制 BOOT0 拉高并复位目标然后发送握手字节等待 ACK。ERASE 阶段发送全片擦除命令WRITE 阶段逐块写固件VERIFY 阶段回读全部数据并和源数据对比最后 DONE 阶段把 BOOT0 拉低、复位目标、让用户程序运行。按键处理必须做消抖。20ms 延时消抖是最稳妥的不要用单纯的中断触发因为按键按下时会有很长的机械抖动处理不好会导致一次按键重复触发多次烧录。我实际使用的是下降沿检测加 20ms 再确认的方式确认有效后才置标志位主循环扫描到这个标志位再进入状态机。LED 反馈尽量明确烧录过程中黄灯闪烁成功绿灯常亮 3 秒然后熄灭失败红灯常亮并在按任意键后清除。产线工人不需要理解代码只需要记住绿能红停这个反馈设计非常重要。5. PC 端灌装工具与文件格式5.1 灌装方式怎么选离线下载器本身不带键盘屏幕目标固件怎么从电脑进入 SPI Flash是必须解决的问题。我试过两种方式各有适用场景。第一种是把 SPI Flash 焊下来用专门的编程器烧录这种方式适合打样阶段因为下载器还没组装好Flash 可以先烧好再焊上去。缺点很明显量产或频繁修改固件时不能每次都拆芯片。第二种是让下载器进入PC 灌装模式。我在固件里预留了一个特殊逻辑按住模式键再上电下载器进入灌装模式此时主控把 PC 串口收到的数据块直接转发给 SPI Flash并返回 ACK。电脑端用一个 Python 脚本通过 USB 转串口模块连接下载器读取本地 bin 文件计算 CRC组织成命令帧发送。这是最实用的方式因为下载器已经做出来了不需要额外工具固件更新也只动脚本。灌装模式的通信协议我自定义得很简单上位机发 0xA5 作为帧头接着 4 字节地址、4 字节长度、CRC32 和实际数据。下载器每收完一帧写入对应的 Flash 地址再回一个 0x79 表示成功。整个灌装过程大概几秒钟因为 W25Q64 的写入速度和串口相比富余很多。5.2 文件头与 CRC 设计文件头的重要性容易被低估。如果 SPI Flash 里只存裸 bin下载器上电后根本不知道固件有多大、从哪个地址开始、数据有没有损坏。我把 32 字节的 FileHeader_t 写在 W25Q64 的固定地址下载器每次上电先读这个结构体检查 magic 是否完全匹配再检查 bin_size 和 crc32 是否一致。CRC32 用常见的多项式算法即可很多语言都有现成库Python 里直接 zlib.crc32 就行。除了 CRC地址信息也很关键。STM32 的程序入口一般是 0x08000000这是它的主 Flash 起始地址向量表从这开始。当你下载 bin 文件时地址就是 0x08000000不用做偏移。但如果以后想给带有自定义 Bootloader 的芯片烧录应用固件入口地址就可能是 0x08008000 之类的偏置地址需要在上位机脚本里留一个入口地址参数不要写死。5.3 一个可用的 Python 灌装脚本下面是一个精简但能用的脚本读取 bin计算 CRC组装文件头和数据包通过串口写到下载器。import serial import struct import zlib import os, time BIN_PATH app.bin PORT COM3 BAUD 115200 HEADER_ADDR 0x000000 DATA_ADDR 0x001000 def main(): bin_data open(BIN_PATH, rb).read() if len(bin_data) 8 * 1024 * 1024: raise Exception(bin too large) crc zlib.crc32(bin_data) 0xFFFFFFFF header struct.pack(IHHII16s, 0x424C4631, 0, 0, len(bin_data), 0x08000000, crc, bdemo_app) ser serial.Serial(PORT, BAUD, timeout1) time.sleep(0.2) send_frame(ser, HEADER_ADDR, header) padded bin_data b\xFF * ((-len(bin_data)) % 4096) send_frame(ser, DATA_ADDR, padded) ser.close() print(灌装完成, bin size %d % len(bin_data)) def send_frame(ser, addr, data): while True: ser.write(b\xA5) ser.write(struct.pack(I, addr)) ser.write(struct.pack(I, len(data))) ser.write(struct.pack(I, zlib.crc32(data) 0xFFFFFFFF)) ser.write(data) ack ser.read(1) if ack b\x79: break print(重试当前块...)每次灌装前建议先全片擦除 W25Q64避免旧数据残留。脚本里可以加一句先写 0xC7 全片擦除命令的逻辑下载器在灌装模式下收到 0xB2 命令就执行全片擦除。这样每次灌装都是从干净状态开始杜绝了地址残留导致的脏数据。6. 从零到第一次离线烧录成功的完整实操6.1 准备目标演示固件为了验证下载器我写了一个非常简单的目标程序上电后初始化 GPIO控制板载 LED 周期性闪烁同时在 USART1 上通过串口打印一行信息。编译工具就用常规的 Keil MDK芯片选 STM32F103C8Tx勾选生成 HEX 文件同时打开生成 bin 文件选项。bin 是纯二进制适合下载器使用HEX 自带地址信息下载器用不上。编译完成后看输出栏bin 文件只有几 KB因为用到的外设特别少。如果你自己做的目标程序比较大也没关系脚本会把 bin 按 4KB 对齐填充后写入 Flash。这里要确认一件事bin 的第一字节对应的是初始栈指针偏移 4 字节是复位向量。下载器不需要关心这些它只需要把数据写到 0x08000000 即可。6.2 灌装流程实测灌装之前先把下载器固件烧好。我用一枚在线调试器把编译好的下载器固件烧进主控然后断开调试器把 CH340 模块接到主控的 USART2 上再给下载器上电。按住模式键再上电进入灌装模式设备管理器里应该能识别出 CH340 的串口号。运行 Python 脚本注意把 BIN_PATH 改成自己编译出来的 bin 文件PORT 改成实际串口号。脚本执行过程中下载器上的黄灯会保持常亮或者闪烁表示在写 Flash。所有数据写完后脚本打印灌装完成下载器黄灯熄灭。此时断开 CH340下载器进入待机状态SPI Flash 里已经有了完整的文件头和 bin 数据。灌装这一关最容易出问题的是串口没识别、波特率不对、地址算错。如果脚本卡在重试当前块超过几次优先检查下载器是否真的进入了灌装模式以及 CH340 是否接对了 PA2 和 PA3。6.3 离线烧录的完整步骤记录目标板准备一块 STM32F103 最小系统板板载 LED 和串口都有方便观察。先把目标板接到一个 3.3V 电源上确认它能正常工作。然后关闭目标板电源把下载器的 6Pin 排线接到目标板对应的引脚上。接线顺序有讲究先接 GND再接 VCC最后接信号线。这样能尽量降低插拔瞬间对芯片的伤害。之后给下载器上电黄灯亮起表示系统就绪。按下烧录键整个过程的时序大概是主控把目标 BOOT0 拉高拉低目标 RST 约 100ms 再释放然后开始发同步字节 0x7F。能看到目标板上的电源指示灯先是短暂灭一下然后恢复。接下来黄灯开始快闪代表正在擦除和写入。等黄灯显眼地慢闪两下最终变为绿灯常亮说明烧录完成。目标板的 LED 开始闪烁同时串口打印出 Hello from app。第一次成功的时候很有成就感但别急着欢呼我建议做一次完整性验证把目标板断电拔掉排线重新接上一块没烧过的同型号目标板再按一次按键确认同样能烧录成功。这样才能排除一次性的偶然因素。7. 常见问题与排查技巧实录7.1 握手失败先查电平再查接线离线烧录最常见的故障是按下按键后黄灯转红灯失败在握手阶段。握手失败意味着下载器发了 0x7F但目标芯片没有回复 ACK 0x79。排查这件事有一套固定顺序。第一确认目标芯片确实进入了 ISP 模式。用万用表量 BOOT0 引脚电压在下载器发出复位信号后BOOT0 应该接近高电平。如果一直是低说明 BOOT0 控制电路没工作可能三极管接反或者主控引脚配置错误。第二确认串口交叉正确。下载器的 TX 必须接目标的 RX下载器的 RX 必须接目标的 TX习惯性按照同名相连的方式接线就会握手失败。第三确认共地。GND 没连或者接触不良串口信号会飘忽不定表现为时好时坏。第四确认目标芯片供电正常。很多最大系统板要用 3.3V 供电下载器如果只给了 5V目标板上的 LDO 确实能出 3.3V但 BOOT0 逻辑电平和串口电平会处于边缘状态。调整完后把目标板断电再上电重新按一次烧录键。不要在一个状态下反复按很多次每次都停留在错误状态只会浪费时间。7.2 擦写失败与写保护选项字节如果握手成功但卡在擦除或写入阶段问题往往不在协议而在目标芯片的 Flash 保护状态。STM32 的 Flash 有读保护等级和写保护页如果目标芯片之前被设置了读保护出厂 Bootloader 会拒绝擦除和写入操作。这种情况在二手拆机芯片和长期测试板上非常常见。解决办法有两个方向一是用在线调试器连上目标芯片把选项字节里的读保护调低全片擦除一次二是在 ISP 协议里发读保护命令 0x73设置保护等级后再解除保护也就是先置保护再清保护。实际操作中第二种方式会让芯片Flash先被强制擦除一遍所以确认它能接受数据丢失后再执行。另外还要检查目标芯片的供电是否稳定。写入时 Flash 需要短时间大电流如果电源线太细或供电模块带载能力不足会在写入瞬间电压跌落导致擦写失败。给目标板供电的导线尽量短而粗或者直接使用下载器输出的 3.3V。7.3 校验失败数据完整性是个系统工程校验失败比握手失败更难查因为前面擦除和写入都完成了回读时却发现数据对不上。遇到这种情况我先查 SPI Flash 侧再查通信链路。SPI Flash 侧的问题是固件可能没灌装完整。如果灌装过程中 PC 串口有丢包或者脚本没做 CRC 校验就写入SPI Flash 里的数据就是错的。解决办法是把 PC 灌装脚本的重试机制完善好并在灌装完成后立即读回数据做一次比对。通信链路侧的问题是下载器和目标之间信号质量差。串口线太长、杜邦线松动、目标板串口引脚被其他外设占用都会导致写入时某个字节损坏。我遇到最隐蔽的一次是目标板上的串口引脚接了上拉电阻虽然平时不出问题但在高速写入时导致边沿变缓偶发字节错误。解决方法是把波特率从 115200 降到 57600或者 38400牺牲一点速度换稳定性。如果是严重的信号兼容问题还可以考虑在下载器和目标串口之间加逻辑电平转换芯片或光耦隔离。但对大多数 3.3V 目标板来说直接连接就够用关键是保证接线短、接触好。7.4 规范化操作与扩展方向做工具类项目养成规范操作的习惯比排故本身更重要。我总结了几条实际工作中会用到的操作守则连接目标板时先接地线再接电源线最后接信号线拔线时反过来先拔信号线再拔电源线和地线。烧录过程中不要带电插拔排线尤其是 BOOT0 和 RST瞬时电平异常可能会损坏目标芯片。下载器长期不使用时把 SPI Flash 的片选引脚拉高避免误写入。这个项目做完之后扩展空间很足。你可以加一个 OLED 屏显示当前固件版本、烧录次数、剩余空间让产线工人在操作时更有底可以加几颗按键做成多固件选择器一台设备管理多个产品线的固件可以增加 SWD 模式把高级下载算法做进去把写入速度从十几秒提到两三秒还可以加一个 USB 接口直接用 USB 虚拟串口替代 CH340让灌装过程更集成。我后来把屏幕和双固件选择加进去了实际体验是产线上的人几乎不需要培训看一眼指示灯和屏幕就会用。回头再看这个项目最值得回味的不是最终成品多精致而是整个实现过程中把 Bootloader 机制、Flash 编程时序、串口协议、状态机设计这些零散知识点串成了完整闭环。我踩过很多坑其中印象最深的几个已经写在上面了。希望这份从原理到实操的记录能帮你少走几步弯路更痛快地做出属于自己的离线下载器。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →