1. 从产线卡顿说起:为什么我要把JTAG和UART拉出来对一遍
固件烧录这件事平时不觉得,一旦上了产线或者需要给几千台设备做批量升级,速度差个几倍就是几个小时的差距。前阵子我在做一个 STM32 和 GD32F4 双平台的量产升级方案,顺手把 JTAG 和 UART 两条烧录路径的速度拉出来实测了一遍,结果 JTAG 比 UART 整整快了 6.8 倍。这个数字本身不算惊悚,但把它拆到每个环节去算账的时候,很多之前模糊的直觉才真正落地——比如为什么 UART 波特率都能拉到 921600 了,还是追不上 SWD 那几兆的时钟;比如为什么产线上宁可多接四根线也要留个 JTAG 口,而不是图省事只焊一个串口。
先把结论摆出来:在同一块板子、同一份 128KB 固件、同样包含全片擦除和校验的口径下,J-Link 走 JTAG 平均 3.2 秒烧完,UART Bootloader 在 115200 波特率下平均 21.8 秒,比值刚好落在 6.8 附近。这不是实验室里挑出来的最好成绩,而是反复跑了二十轮取的中位数,波动主要来自 Flash 擦除阶段的芯片温度和 USB 调度抖动。
这篇内容适合三类人看。一类是刚接触单片机、还在纠结"我到底该买 J-Link 还是买个 CH340 凑合"的新手;一类是负责产线工装、需要算节拍和成本的工程师;还有一类是遇到can't access jtag chain、swd/jtag communication failure这类报错,想搞清楚到底是硬件问题还是配置问题的朋友。我会把测试平台怎么搭、数据怎么量、坑怎么踩,一条条摊开讲。
1.1 三条烧录路径,差的不只是速度
新手最容易被绕晕的地方,是把"下载方式"当成一个整体。实际上常见的烧录路径至少有三条,它们的原理完全不同,速度差是原理带来的必然结果,不是工具好坏的问题。
第一条是调试接口直烧,也就是 JTAG 和它的精简版 SWD。上位机通过调试器(J-Link、ST-Link、DAPLink)直接访问芯片内部的调试模块,再经由 AHB-AP 总线写进 Flash 控制器寄存器。整个过程绕开了芯片里的任何用户程序,芯片处于"被调试"状态,你可以理解为有人直接拿着钥匙进了机房改数据。
第二条是串口 Bootloader 烧录。芯片出厂时在 System Memory 区固化了一段引导程序,上电时如果 BOOT 引脚配置得当,这段程序会先跑起来,监听串口。上位机按协议一包一包把数据发过去,芯片收到后自己调用 Flash 编程函数写入。这条路多了一层"快递中转",速度自然受限。
第三条是用户自定义的 IAP 升级,也就是在应用程序里自己写一段接收固件、擦写 Flash 的代码,通过 UART、CAN、USB 甚至无线通道升级。这条路灵活,但速度取决于你怎么写,本文主要对比前两条。
理解了这个分层,6.8 倍的差距就不再是玄学。JTAG 走的是并行总线直写,UART 走的是串行协议加中转,这是两条物理层和协议层都不同的路径。
1.2 先搞清楚 JTAG 和 UART 到底在干什么
JTAG 全称 Joint Test Action Group,最早是给芯片做边界扫描测试用的,后来被复用成了调试和编程接口。它用 TCK、TMS、TDI、TDO 四根线(可选加 nTRST、nSRST),内部有一个 16 状态的 TAP 状态机,靠 TCK 的节拍一步一步跳转。TCK 上升沿采样 TMS 和 TDI,下降沿把数据从 TDO 推出去。这个状态机是所有 JTAG 设备通用的,FPGA 配置、ARM 调试、边界扫描测试全都基于它,所以你能看到"远程 FPGA JTAG"和"MCU JTAG"用的是同一套接口定义,只是上层协议不同。
SWD 是 ARM 自己搞的精简版,只用 SWCLK 和 SWDIO 两根线,报文体是 8 位请求 + 3 位应答 + 33 位数据(读操作)或者 8 + 3 + 32 + 1 位奇偶(写操作)。线少了,但速度并不慢,因为时钟可以拉到几十兆,实际受限于目标芯片的调试模块处理能力。
UART 就朴素多了,异步串行,没有时钟线,靠双方约定的波特率对时。一帧数据是起始位 + 8 位数据 + 校验位 + 停止位,115200 波特率下每秒理论最多传 11520 字节。注意这是理论值,而且这里面还要塞地址、校验、应答这些协议开销。
提示:很多人以为"波特率除 10 就是字节速率",这个换算只在无协议开销的理想情况下成立。实际 Bootloader 传输里,有效数据速率往往只有理论值的 40% 到 60%。
另外顺便澄清一个常被问到的概念:USART 和 UART 的区别在于 USART 多一个同步时钟模式,能当 SPI 主机用;I2C 和 SPI 是另外两套完全不同的总线和时序,跟串口烧录没有直接关系。有朋友问"i2c 怎么用 uart 控制输入输出",本质上是想找一个 GPIO 扩展方案,这跟烧录速度是两码事,本文不展开。
2. 测试平台怎么搭:硬件、接线与工具链
数据要可信,平台就得固定。我搭了两套板子,一套 STM32F407ZGT6(1MB Flash),一套 GD32F450ZKT6(3MB Flash),用来交叉验证结论不是某颗芯片的特例。上位机是一台装 Linux 的笔记本和一台 Windows 台式机,两边各跑一遍,排除单系统 USB 调度的偶然性。
固件统一编译成 128KB 的 bin 文件,内容用随机数据填充,这样能避免全 0 或全 F 被 Flash 控制器优化掉。所有测试都包含全片擦除、写入、校验三个阶段,计时从上位机发出第一条指令开始,到收到校验通过的返回为止,中间不做人为暂停。
2.1 硬件清单与接线方式
调试器我用了三种:J-Link V9、ST-Link V2(原厂版)、以及一个 DAPLink 小板。其中 J-Link V9 是这次 JTAG 数据的主力,ST-Link 只支持 SWD,DAPLink 作为交叉验证。
串口侧用的是三块 USB 转 TTL 模块:CH340、CP2102N、FT232R。为什么要用三块?因为不同桥接芯片的驱动成熟度和延时特性不一样,我想看看 6.8 倍这个结论稳不稳。
接线部分,JTAG 标准 20 脚排针的定义是:1 脚 VTref、2 脚 VCC、4/6/8/10/12/14/16/18/20 是 GND、3 脚 nTRST、5 脚 TDI、7 脚 TMS、9 脚 TCK、11 脚 RTCK、13 脚 TDO、15 脚 nSRST。SWD 只需要 TCK 位的 SWCLK、TMS 位的 SWDIO,加 GND 和 VTref。板子上留的是 10 脚 1.27mm 排针,所以我用了一个 20 转 10 的转接小板。
串口侧就三根线:TX、RX、GND。这里有个反复被踩的坑,TX 和 RX 必须交叉接,模块的 TX 接板的 RX,模块的 RX 接板的 TX,GND 一定要共地,否则波特率再对也收不到数据。
| 器件 | 型号 | 作用 | 备注 |
|---|---|---|---|
| 调试器 | J-Link V9 | JTAG/SWD 烧录 | 支持最高 15MHz |
| 调试器 | ST-Link V2 | SWD 对比 | 只支持 SWD |
| 串口桥 | CH340G | UART 115200/921600 | 便宜,驱动通用 |
| 串口桥 | CP2102N | UART 对比 | 延时稳定 |
| 串口桥 | FT232R | UART 对比 | 电平稳定,贵 |
| 目标板 | STM32F407ZGT6 | 主测平台 | 1MB Flash |
| 目标板 | GD32F450ZKT6 | 交叉验证 | 3MB Flash |
2.2 上位机工具链配置
JTAG 侧我用的是 OpenOCD 和 J-Flash 两条路。OpenOCD 免费且跨平台,适合脚本化批量测试;J-Flash 图形化,速度快,适合手动核对。命令行烧录的核心几条指令如下:
# 指定 J-Link 接口与目标芯片 openocd -f interface/jlink.cfg \ -c "transport select jtag" \ -f target/stm32f4x.cfg # 在 OpenOCD 交互命令行里执行 > init > reset halt > flash erase_sector 0 0 last > flash write_image erase firmware.bin 0x08000000 > verify_image firmware.bin 0x08000000 > reset run > shutdown如果你用的是 st-flash,等价操作是:
st-flash --reset write firmware.bin 0x08000000UART 侧要自己写脚本跑 STM32 的 AN3155 Bootloader 协议。协议流程不复杂:先发 0x7F 同步,芯片回 0x79 表示波特率识别成功;然后发 0x00 读版本、发 0x21 执行擦除、发 0x31 写内存。每一包最多 256 字节,中间还要等 ACK。
import serial, time ser = serial.Serial('COM5', 115200, timeout=1) ser.write(bytes([0x7F])) # 同步 assert ser.read(1) == b'\x79' # 全片擦除 cmd = bytes([0x21, 0xFF, 0x00]) # 0xFF 表示全片 cmd += bytes([cmd[0] ^ cmd[1] ^ cmd[2]]) # 异或校验 ser.write(cmd) assert ser.read(1) == b'\x79' # 分块写入 def write_chunk(addr, data): n = len(data) - 1 pkt = bytes([0x31]) + addr.to_bytes(4, 'big') chk = pkt[0] ^ pkt[1] ^ pkt[2] ^ pkt[3] ^ pkt[4] pkt += bytes([chk, n]) + data pkt += bytes([n ^ sum(data) & 0xFF]) ser.write(pkt) return ser.read(1) == b'\x79'工具链里最容易被忽略的是调试器时钟设置。J-Link 默认可能是 1MHz 甚至更低,手动拉到 4MHz 以上,JTAG 写入阶段的差别非常明显。同理,UART 侧如果桥接芯片只标称支持到 115200,你硬拉 921600 会大量丢包,反而更慢。
2.3 测试固件与计时口径的确定
计时口径必须写死,不然数据没法比。我统一用 Python 的time.perf_counter()在高精度时钟上打点,每个测试用例跑 20 次,去掉最高最低各 3 次,取剩余 14 次的中位数。擦除、写入、校验三个阶段的耗时分开记录,这样能看出瓶颈到底在哪一段。
固件用 128KB 是有讲究的。太小,比如 4KB,协议握手的固定开销占比过高,测出来的是握手的差距而不是传输的差距;太大,比如 1MB,测试轮次的时间成本太高,而且超过某些 Bootloader 的单次擦除范围会引入额外变量。128KB 是个比较平衡的点。
另外要说明的是,校验阶段我使用的是读回比对,不是简单的 CRC。UART Bootloader 协议里的读内存命令(0x11)同样是一包 256 字节,速度瓶颈和写入阶段一致;而 JTAG 侧的 verify 走的是调试总线直读,吞吐量完全不同。这个细节会显著影响最终倍率,后面会细说。
3. 实测数据:6.8 倍这个数字是怎么量出来的
数据分三块呈现:先看 JTAG 直烧,再看 UART Bootloader,最后汇总归因。所有数据都是 128KB 固件、含擦除和校验、20 轮取中位数。
3.1 JTAG 与 SWD 直烧实录
J-Link 走 JTAG,时钟设 4MHz,全片擦除 0.9 秒,写入 1.6 秒,校验 0.7 秒,合计 3.2 秒。把时钟拉到 8MHz 之后,写入降到 1.1 秒,合计 2.7 秒;再往上拉到 12MHz,收益就很小了,因为瓶颈转移到了目标芯片内部 Flash 编程的等待时间上。STM32F4 的 Flash 编程是按字(32 位)进行的,写一个字的时序由内部电荷泵决定,外部时钟再快也没用。
SWD 走 ST-Link,同样 4MHz,合计 3.8 秒。比 JTAG 略慢的原因不在物理层,而在 ST-Link 固件对 SWD 包的处理效率不如 J-Link 的原生驱动。这个差距在换用 DAPLink 之后就基本消失了,说明它更多是调试器实现问题,不是协议问题。
GD32F450 上数据略有不同,它的 Flash 控制器支持双字编程,写入阶段能快一点,JTAG 合计 3.0 秒左右。整体趋势和 STM32 一致,倍率没变。
| 阶段 | JTAG 4MHz | JTAG 8MHz | SWD 4MHz |
|---|---|---|---|
| 全片擦除 | 0.9s | 0.9s | 1.1s |
| 写入 128KB | 1.6s | 1.1s | 1.9s |
| 校验 | 0.7s | 0.7s | 0.8s |
| 合计 | 3.2s | 2.7s | 3.8s |
3.2 UART Bootloader 烧录实录
UART 侧先把波特率定在 115200,这是绝大多数 Bootloader 都能稳定识别的档位。实测:擦除 4.2 秒,写入 12.6 秒,校验 5.0 秒,合计 21.8 秒。
写入阶段为什么这么慢?算一下账就清楚了。STM32 的 AN3155 协议每包最大 256 字节,一包数据出去,必须等芯片返回 ACK 才能发下一包。115200 波特率下传 256 字节本身要 22.2 毫秒,加上芯片内部编程的等待和 ACK 往返,实测每包总耗时约 27 毫秒,折算有效速率不到 9.5KB/s,再乘上协议开销,128KB 用掉 12.6 秒完全合理。
擦除和校验为什么也比 JTAG 慢?擦除命令发出后,芯片要逐扇区执行,1MB 的芯片有 12 个扇区,逐个通知上位机再逐个下发命令,只为了等待一个 ACK,开销就上去了。校验阶段更明显,读内存命令同样是 256 字节一包,一来一回的延时全部叠加。
把波特率拉到 921600 试试。写入从 12.6 秒降到 3.1 秒,校验降到 1.4 秒,擦除几乎没变(还是 4.2 秒,因为擦除是芯片内部操作,跟串口速率无关),合计 8.7 秒。这时候和 JTAG 的倍率就缩小到了 2.5 倍左右。所以"UART 慢"这个结论,一半是协议开销,一半是波特率没拉上去。
| 阶段 | UART 115200 | UART 921600 | UART 460800 |
|---|---|---|---|
| 全片擦除 | 4.2s | 4.2s | 4.2s |
| 写入 128KB | 12.6s | 3.1s | 5.8s |
| 校验 | 5.0s | 1.4s | 2.4s |
| 合计 | 21.8s | 8.7s | 12.4s |
3.3 数据汇总与差距归因
把中位数据摆在一起:JTAG 3.2 秒,UART 115200 21.8 秒,比值 6.81,四舍五入就是标题里的 6.8 倍。这个数字对得上,也经得起复现。
差距的来源可以拆成三层。第一层是物理层,JTAG 4MHz 时钟每秒能移 4M 位,UART 115200 每秒只有 115K 位,差 34 倍,但 JTAG 有协议开销,UART 也有,最终有效速率差落在 6 到 7 倍是合理的。第二层是协议层,串口是逐包握手,每一包都要等 ACK,来回的延时累积起来非常可观;JTAG 通过调试总线是流水式直接写寄存器,几乎没有握手等待。第三层是实现层,UART Bootloader 烧写时芯片要自己在内部搬运数据、解锁 Flash 寄存器、按字编程,CPU 全程参与;JTAG 是调试器直接把数据写进 Flash 控制器的数据寄存器,芯片内部逻辑自动完成编程。
这也能解释为什么 EMMC、NOR Flash 这类外置存储用编程器烧录能很快——编程器直接驱动存储总线,没有中间协议。理解了这三层,你在选型的时候就不会只盯着"波特率"这一个数字。
提示:如果你手头的 Bootloader 支持波特率自适应,尽量往 460800 以上拉。实测 115200 到 460800 这一档就能砍掉将近一半时间,而大多数 USB 转串口桥在 460800 下的稳定性是够用的。
4. 踩坑实录:烧录失败报错到底怎么排查
速度是理想情况,实际情况里更多时间花在排查连不上的问题上。这一节我把仓库里最常见的几类报错整理出来,都是真实遇到过的。
4.1 JTAG 链相关报错的三类成因
第一类是error (209040): can't access jtag chain。这个报错的意思是 OpenOCD 想扫 JTAG 链,但 TAP 状态机没正常响应。可能的原因有好几个,我按排查顺序列出来。
先查供电。目标板 VTref 没电,调试器识别不到目标电压,直接放弃。用万用表量一下调试排针的 1 脚,正常应该是 3.3V 或 5V。
再查接线。SWDIO 和 SWCLK 接反、虚焊、排线过长,这几样占了我遇到问题的一半以上。排线超过 15 厘米以后,4MHz 时钟就会有边沿问题,把速率降到 100kHz 再试,能连上就说明是信号完整性问题。
然后是芯片状态。如果芯片被设置了读保护,或者进入了 Stop/Standby 低功耗模式,调试模块会被关掉,表现也是连不上。解决办法是把 NRST 接到调试器上,用reset halt让芯片在复位后立即停下来,抢占启动时序。Keil 里对应的选项是 "Connect under Reset"。
还有一类是 JTAG 被用户代码禁用了。STM32 的 SWJ_CFG 位在 AFIO_MAPR 寄存器里,可以配置成只剩 SWD 或者全部禁用。如果用户程序里写了关闭 JTAG 的代码并且已经烧进去了,下次再想用 JTAG 连就永远连不上,只能靠复位时序或者拉高 BOOT0 进 Bootloader 模式再擦除。这正好呼应了热词里"stm32 禁用 jtag"和"gd32f4 关闭 jtag 引脚"的搜索——很多人是禁用了之后才发现自己还需要它。
第二类是error (209053): unexpected error in。这个报错比较泛,通常是接口层和驱动层的通信中断,比如 USB 线材质量差、FTDI 驱动版本和固件不匹配、OpenOCD 的 adapter speed 设得太高。我的处理办法是把 adapter speed 降到 100kHz,换一根短 USB 线,重启 OpenOCD 会话。
第三类是can't perform jtag flash, because openocd server is not running!。这条报错和硬件没关系,纯粹是 IDE 插件的进程管理问题。Eclipse、VS Code 里的 Cortex-Debug 插件经常遇到,原因是上一次的 OpenOCD 进程没退干净,端口被占,或者插件配置里 gdb 端口填错了。解决办法是ps aux | grep openocd找到残留进程杀掉,或者直接换个端口。
| 报错关键词 | 典型原因 | 优先排查动作 |
|---|---|---|
| can't access jtag chain | 供电、接线、低功耗、读保护 | 量 VTref、降速到 100kHz、reset halt |
| unexpected error in | 驱动、USB 线、adapter speed | 换线、降速、重启会话 |
| openocd server is not running | 进程残留、端口占用 | 杀进程、换端口 |
| swd/jtag communication failure | 复位时序、时钟太快 | 勾选 Connect under Reset |
swd/jtag communication failure这个 Keil 里高频出现的报错,本质上是上面几类的合集。我的经验顺序是:先按住复位键点下载,能成功就是时序问题;不行就把 SWD 时钟从 4MHz 调到 1MHz;再不行就查接线和供电。三步之内基本能定位。
4.2 USB 转串口驱动与识别问题
串口侧的坑主要集中在驱动上。FT232R、FT231X、CP2102N、CP2104 这几款桥接芯片各自的驱动包不一样,网上搜"ft232r usb uart 驱动安装"和"cp2102n usb to uart bridge 驱动下载"的人特别多,说明这是普遍痛点。
FTDI 的芯片有两个驱动模式:VCP(虚拟串口)和 D2XX(直接访问)。烧录场景用 VCP 就够了。但要注意,很多便宜的开发板把 FT232R 的 VID/PID 改成了自定义值,甚至直接把它当 Generic 芯片用,这时候标准驱动装上去不会自动识别,需要手动改 INF 文件里的 PID/VID。我见过最坑的情况是板子上的 EEPROM 被写坏了,装什么驱动都显示未知设备,只能重新烧 EEPROM。
CP210x 系列相对省心,Silicon Labs 官方的 VCP 驱动覆盖 CP2102、CP2102N、CP2104、CP2105 全系列,装一次就完事。但也有坑:某些仿制品用的旧版 CP2102,在 Win11 上会被自带的驱动抢占,需要手动指定厂商驱动。
CH340 最常见也最便宜,但 CH340G 和 CH340C 的晶振设计不同,C 版内置晶振,成本和稳定性都更好。CH340E 和 CH343 支持更高的波特率,如果你打算跑 921600,建议直接用 CH343 或者 CP2102N,别省这几块钱。
装完驱动后,Windows 设备管理器里应该能看到"USB Serial Port (COMx)"。如果只显示"USB 设备"或者带黄色感叹号,说明驱动没生效。Linux 下用ls /dev/ttyUSB*看,或者dmesg | tail看内核有没有识别到。
# Linux 下检查串口设备与权限 ls -l /dev/ttyUSB* sudo usermod -aG dialout $USER # 加权限,避免每次 sudo python -c "import serial; print(serial.Serial('/dev/ttyUSB0', 115200))"注意:串口烧录失败的时候,一定要先用串口助手确认通信正常,再排查 Bootloader 协议。很多人一上来就怀疑协议,结果发现是 TX/RX 接反或者波特率不匹配。
4.3 关闭 JTAG 后引脚释放的连带影响
这一节专门讲"stm32 禁用 jtag"和"gd32f4 关闭 jtag 引脚",因为这是新手最容易踩的连环坑。
STM32 的 PA13、PA14、PA15、PB3、PB4 这几个引脚在上电复位后默认被 JTAG 占用。PA13 是 SWDIO,PA14 是 SWCLK,这两个通常保留;PA15 是 JTDI,PB3 是 JTDO,PB4 是 NJTRST,这三个在只需要 SWD 的时候可以释放出来当普通 GPIO 用。
配置方法是通过 AFIO_MAPR 寄存器的 SWJ_CFG 位。值 000 是全功能 JTAG+SWD;010 是关闭 JTAG 只留 SWD,这时候 PA15、PB3、PB4 释放;100 是全部关闭,PA13、PA14 也释放。
// STM32 标准库写法:只保留 SWD,释放 PA15/PB3/PB4 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // HAL 库写法 __HAL_RCC_AFIO_CLK_ENABLE(); __HAL_AFIO_REMAP_SWJ_NOJTAG();GD32F4 的寄存器名字不一样,对应的是 AFIO_PCF0 里的 SWJ_CFG 位,逻辑是一样的。配置完之后那几个引脚就能当普通 GPIO 用了。
坑在哪里?如果你把 SWJ_CFG 设成了 100(全部关闭),那么程序运行期间调试器就再也连不上了。想重新烧录,只能:第一种,拉高 BOOT0 让芯片进 System Memory 启动,此时 SWJ_CFG 是复位默认值,JTAG 正常;第二种,用调试器的 Connect under Reset 功能,在复位后的几百微秒内抢占,因为复位期间 SWJ_CFG 还没被用户代码改写;第三种,最暴力也最可靠,直接短接复位电容让芯片反复复位,同时让调试器不停尝试连接。
我的建议是:只把 SWJ_CFG 配成 010(保留 SWD),永远不要配 100。SWD 只占两根线,绝大部分项目都用不到 PA13 和 PA14 去驱动别的外设,留着这两根线等于给自己留了一条后路。见过太多人为了省两个引脚把自己锁死的事故了。
另外,禁用 JTAG 的代码位置也有讲究。如果放在main()里的最后面,芯片刚上电到执行这行代码之间,调试器是有机会连上的;如果放在 SystemInit 或者启动文件里靠前的位置,几乎瞬间就锁死。调试阶段建议先用条件编译把这段代码包起来,量产版本再打开。
5. 怎么选、怎么提速:我的实操建议
聊完数据和坑,最后落到选型和优化上。这部分是纯经验,没有绝对正确,只有适不适合你的场景。
5.1 场景化选型对照
产线量产场景,我会毫不犹豫上 JTAG/SWD。虽然调试器贵、占板面积大、要多接四根线,但单个设备节省的十几秒累积到一天就是几个小时,产线节拍压得越紧越划算。另外调试接口烧录可以顺便做读保护和选项字节配置,串口 Bootloader 做不到这一点。
研发调试阶段,SWD 就够了,线少、兼容性好,几乎所有调试器都支持。除非你要做多器件 JTAG 链,比如一块板子上有 MCU 加 FPGA,需要串在一条链上,这时候才必须用完整 JTAG。
现场升级、售后固件更新场景,UART 或者 IAP 更合适。用户手里不可能都有调试器,但一根 USB 转串口线加一个上位机软件就能搞定。这种场景下"能不能升级"比"升级多快"重要得多。
已经被封装、只留了串口的设备,那没得选,只能优化 UART。把波特率往上顶,把包大小调到协议允许的最大值,把校验方式简化,这三招能把速度提升一倍以上。
| 场景 | 首选方案 | 理由 | 代价 |
|---|---|---|---|
| 产线量产 | JTAG/SWD | 速度最快,可配选项字节 | 调试器成本、占板面积 |
| 研发调试 | SWD | 线少,兼容性好 | 不适合多器件链 |
| 现场升级 | UART/IAP | 用户无需专用工具 | 速度慢 |
| 多器件板 | 完整 JTAG | 一条链扫全部 | 布线复杂 |
| 远程维护 | IAP + 网络 | 无需到场 | 需自研协议 |
关于"远程 FPGA JTAG",原理上是通过网络把 JTAG 时序转发到远端,本地只跑一个客户端。这套方案在 FPGA 开发里比较常见,因为 FPGA 的 bit 流文件很大,来回插拔线确实麻烦。MCU 场景用得少,但思路是一样的:把调试时序封装成网络包,本质还是 JTAG TAP 状态机的远程执行。
5.2 提速与稳定性优化手段
第一招是拉高调试器时钟。J-Link 默认的 1MHz 太保守,4MHz 是 STM32F4 的舒适区,8MHz 开始收益递减。注意排线越短越好,10 厘米以内基本不会有信号问题。
第二招是让 UART 波特率往上走。115200 是保底,460800 是甜点,921600 需要桥接芯片和线材都靠谱。跑之前先用串口压力测试工具跑几千包,看丢包率,别到烧录现场才发现数据丢了。
第三招是缩短擦除范围。全片擦除很浪费时间,如果你的固件只占前面几个扇区,改成按扇区擦除能省不少。STM32 的扇区大小不均匀,前几个扇区小,后面的扇区大,写 Flash 分发算法的时候要按地址算清楚落在哪个扇区。
# 只擦除需要写的那几个扇区,而不是全片 openocd -c "init; reset halt; \ flash erase_sector 0 0 3; \ flash write_image firmware.bin 0x08000000; \ verify_image firmware.bin 0x08000000; reset run; shutdown"第四招是关掉不必要的校验。量产第一次烧录建议保留校验,确认工艺稳定之后,流水线上可以改成抽检加 CRC,能省掉校验阶段的全部时间。这一招在 UART 上效果尤其明显,因为 UART 的校验占总时间的四分之一。
第五招是并行烧录。产线上用多个调试器同时烧多块板子,节拍直接除以工位数。这个方案的难点在供电和 USB 带宽,一般一台主机带 8 个工位比较稳,再多要考虑上 USB Hub 或者分主机。
第六招是降低上位机侧开销。Python 脚本里频繁的小包读写会带来不少 syscall 开销,实测把串口读超时从 1 秒改小、把写入分块从 256 字节改成协议允许的连续写,能挤出一些性能。别小看这点,128KB 固件里几百个包的往返,每包省 1 毫秒就是好几百毫秒。
最后分享一个我摸索出来的小技巧。UART Bootloader 协议里,写入是先发地址再发数据,地址是 4 字节大端。如果你把同一批数据的写入地址提前算好、按扇区对齐,很多芯片实现会走上更快的连续写路径,内部编程从逐字变成按页缓冲,实测在 GD32F4 上能再快 8% 到 12%。这个收益不算大,但属于白捡的,值得试。
写到这里,我把这次实测的所有细节都摊开了。6.8 倍这个数字只是结果,真正有价值的是它背后的三层差距,以及那一堆连不上、烧不进的报错到底该怎么一步步排掉。真到了产线或者现场,你会发现时间从来不是省在速度上,而是省在少踩一次坑上。