1. 先搞清楚 ISP、ICP、IAP 到底在争什么
做嵌入式这行,只要稍微往量产和售后方向走一步,就一定会在某一天被这三个词绕进去:ISP、ICP、IAP。论坛上、芯片手册里、甚至同一家厂商的不同文档里,这三个缩写的定义都各有各的说法,很多人做了几年项目,仍然是“能用就行、能烧进去就不深究”。可一旦碰上产线要提速、产品要远程升级、样机被锁死救不回来这类事,定义不清带来的代价就很直接——选错方案,轻则量产节拍卡在瓶颈上,重则产品装到现场才发现根本没有升级通道。
这篇内容聊的就是这件事。它面向三类人:正在选型的硬件工程师(到底要不要在板子上留调试口)、负责量产落地的工程人员(怎么把烧录时间压进节拍里)、写固件的软件工程师(升级流程怎么设计才不至于一键变砖)。我会把三种方式的硬件链路、协议层次、时间成本、失效模式全部拆开讲,并给出一套可以直接抄的分区方案和跳转代码,最后附上我自己踩过的坑。
先说一句前提:这三个词在国内从业者口中的定义,其实和官方文档里并不完全一致,下面所有讨论都基于“谁在执行烧写动作”这个判断依据,这也是我认为唯一靠谱的分类方法。
1.1 从一颗芯片上电的第一毫秒说起
任何烧录方式的讨论,都得回到芯片的存储结构和启动流程。以常见的 32 位 Cortex-M 内核 MCU 为例,上电复位后,内核会从固定地址取两个 32 位值:第一个是初始栈顶指针 MSP,第二个是复位向量(Reset Handler 地址)。这个固定地址通常是 0x00000000,但很多芯片内部做了地址重映射,把主 Flash 的起始地址、系统存储区(System Memory,出厂固化的 Bootloader)、甚至 SRAM 都映射到 0x00000000,具体映射哪一块,由几个BOOT 引脚的电平在上电复位瞬间采样决定。
这个机制就是理解三种烧录方式的钥匙:
- 映射到系统存储区,芯片执行的是厂商出厂就固化的那段程序,它不归你管,但它认识 UART、USB、CAN 这些通用接口,能跟电脑上的上位机软件对话,接收固件并写进主 Flash。这条路就是ISP。
- 映射到主 Flash,芯片执行的是你自己写的代码,此时如果调试口(SWD/JTAG)还开着,外部调试器就能直接接管内核,通过调试访问端口(DAP)读写 Flash 控制器。这条路就是ICP。
- 映射到主 Flash,但代码本身带有升级逻辑,程序自己从外部通道接收新固件、自己擦写自己的 Flash 区域,这条路就是IAP。
同样一颗芯片,三条路的物理入口完全不同,代价也完全不同。搞明白这一点,后面所有的选型其实都是顺推。
1.2 三个缩写的本质区别:谁动手、用什么口、在什么时候
我把这三者的核心差异整理成一张表,这张表建议直接截图存到项目文档里,比翻十几份手册都快。
| 维度 | ISP(在系统编程) | ICP(在电路编程) | IAP(在应用编程) |
|---|---|---|---|
| 执行烧写的“人” | 芯片内置 Bootloader | 外部调试器/编程器 | 用户自己的程序 |
| 典型物理接口 | UART、USB、CAN、I2C、SPI | SWD、JTAG、cJTAG、ICSP | 任意通信口(串口/总线/无线) |
| 芯片状态 | 复位后进 Boot 模式,用户程序不运行 | 内核被调试器接管,用户程序不运行 | 用户程序运行中,自己升级自己 |
| 需要预留资源 | 通信引脚 + BOOT 引脚 | 调试口 4~6 根线 | 通信口(本来就要有) |
| 烧写速度 | 慢(受串口波特率限制) | 快(受调试时钟和 Flash 编程时间限制) | 最慢(受现场链路带宽限制) |
| 能否在线调试 | 不能 | 能(断点、单步、看变量) | 不能 |
| 典型场景 | 样机救砖、无调试口的板子 | 研发调试、产线首次烧录 | 现场固件升级、远程维护 |
| 失效风险 | 中(引脚接触不良即失败) | 低(有回读校验) | 高(断电、校验缺失会变砖) |
这里必须专门提一句命名混乱的问题。有些厂商的文档把“用调试器通过 SWD 下载”直接叫 ISP,把产线上的脱机烧录器叫 ICP;另一批厂商则把串口 Bootloader 下载叫 ISP,把调试口下载叫 ICP。你不必去争论谁对谁错,只要抓住那个判据——烧写动作是芯片内置程序干的,还是外部工具干的——就不会被文档带偏。
提示:拿到一份新手册,先搜 “System Memory” 和 “Boot Configuration”,确认它有没有出厂 Bootloader、由哪几个引脚控制,这一条直接决定你能不能省掉调试口。
1.3 为什么这三种方式至今没有被统一
按道理讲,如果 ICP 又快又稳,为什么还要留着 ISP 和 IAP?如果 IAP 能远程升级,为什么产线还要用 ICP?答案藏在三个相互冲突的约束里:成本、可维护性、安全性。
产线的诉求是节拍和良率,ICP 一台编程器可以一拖四甚至一拖八,单颗烧写十几秒,良率 99.9% 以上,这是 ISP 做不到的。但 ICP 要求板子上留出调试口,还要加治具探针,对小尺寸模组类产品来说,那几个焊盘的面积和成本不能忽略。
研发的诉求是可观测性,必须能打断点、看寄存器、单步跟踪,只有 ICP 满足。
售后的诉求是零拆机,产品装在客户设备里,你不能扛着编程器上门,只能靠 IAP 从现场总线推固件下去。但 IAP 又带来一个致命问题:升级程序本身出错或者断电,产品就彻底失去响应,所以必须配双区备份或者最小可恢复 Bootloader。
这三者其实是互补关系,不是替代关系。成熟项目的做法通常是三条路全留:研发阶段用 ICP,产线用 ICP 一拖多或者离线烧录器,交付后走 IAP,ISP 作为最后一道救砖的后门。
2. 三种方式的核心原理与硬件链路拆解
这一章往深里挖,讲每种方式的协议层次、硬件连接要点,以及那些手册上不会明写、但踩了会疼很久的细节。
2.1 ISP:靠的是出厂固化的一小段代码
ISP 能成立,全靠芯片内部那块系统存储区。这块区域在出厂时就被写好了引导程序,用户无法修改,也擦不掉。它的职责很单一:上电时检测到 BOOT 引脚处于特定组合,就初始化一个通信外设(通常是 UART),进入等待命令的循环,接收上位机发来的擦除、写入、校验、跳转指令。
这里有个容易被忽略的细节:这段 Bootloader 使用的时钟源通常不是外部晶振,而是内部 RC 振荡器。原因很简单,厂商不知道你的板子上有没有焊晶振。内部 RC 的精度一般只有 ±1%~±2%,这就直接给 ISP 的波特率上限划了线。115200 波特率下,一个位宽 8.68 微秒,2% 的时钟偏差累积到第 10 位大约是 1.7 微秒,还在容错窗口内;一旦上到 460800 甚至 921600,就很容易出现丢包、校验失败、握手超时。实测下来,很多 8 位 MCU 的 ISP 稳定上限就在 115200,少数 32 位芯片能做到 921600,但那通常要求上位机软件做自适应波特率校准。
注意:如果 ISP 死活连不上,而线序、电平、BOOT 引脚都确认无误,先怀疑晶振——有些芯片的 Bootloader 在检测到外部晶振存在时会切过去用,而你的晶振起振时间偏长,握手就已经超时了。
协议层面,各家厂商的 ISP 协议都是私有的,但结构大同小异:握手包 → 擦除命令 → 分包写入 → 校验命令 → 跳转命令。分包大小一般是 64、128 或 256 字节,每包后面跟应答字节。这意味着实际的传输效率永远达不到理论值。举个具体例子:115200 波特率、8N1 格式,每个字节 10 个位,理论吞吐是 115200 / 10 = 11520 字节/秒。假设每包 128 字节数据,外加 6 字节包头包尾、1 字节应答、以及上位机等待应答的处理延迟,实际有效吞吐大概在 8~9.5 KB/s 之间。
那么烧一颗 512KB Flash 的芯片要多久?512 × 1024 / 9.5 ≈ 55000 毫秒,接近一分钟。这还没算擦除时间——整片擦除通常要 100~500 毫秒,扇区擦除按 4KB 一个扇区、每扇区 30 毫秒算,128 个扇区就是 3.84 秒。所以一次完整的 ISP 烧录,512KB 固件落在55~65 秒是正常区间。这个数字放到日产千台的产线上,就是不可接受的了。
ISP 的优势场景其实很明确:板子上没留调试口、调试口被读保护锁死、需要现场救砖。很多产品的调试口在做完 EMC 整改后会被物理去掉或者灌胶封死,这时候唯一能救回来的就是那几个 BOOT 引脚加一个串口。
2.2 ICP:调试器接管内核的完整链路
ICP 走的是完全不同的路径。调试器通过 SWD 的两根线(SWCLK、SWDIO)或者 JTAG 的四五根线,跟芯片内部的调试访问端口通信,进而通过总线访问 Flash 控制器。整个过程芯片不需要运行任何用户代码,甚至不需要 Flash 里有合法的程序——只要内核活着、调试模块没被关掉,就能烧。
这条链路上有几个参数值得算清楚。SWD 的时钟频率通常可以配到 1MHz~50MHz,但真正的瓶颈不在时钟,而在 Flash 编程本身。Flash 写入是物理过程,往浮栅里注电荷需要时间。典型参数是:页大小 1KB,页编程时间 10~20 毫秒;扇区擦除 4KB,耗时 25~40 毫秒。
按这个参数算一笔账,512KB 固件、页大小 1KB:
- 擦除:128 个 4KB 扇区 × 30 毫秒 = 3.84 秒
- 编程:512 页 × 12 毫秒 = 6.14 秒
- 校验(回读比对):按 200KB/s 算 ≈ 2.6 秒
- 连接握手 + 复位 + 校验 CRC ≈ 1~2 秒
合计约14~16 秒。这比 ISP 快了接近四倍,而且这个数字里绝大部分时间花在物理擦写上,再怎么优化时钟也压不下去。所以产线上真正提速的手段不是提时钟,而是一拖多——四工位并行,单板节拍直接降到 4 秒左右。
硬件连接上,SWD 最少只需要 4 根线:VCC、GND、SWDIO、SWCLK,加上 NRST(复位)会更稳,一共五根。这里有几个实操细节:
- 线长控制。SWD 时钟上到 4MHz 以上时,超过 15 厘米的排线就会开始出现误码。产线治具上的走线尽量短,必要时在 SWDIO、SWCLK 上各串一个 22~100 欧姆的电阻做阻抗匹配和限流。
- NRST 一定要接。不接复位线的时候,调试器只能靠“软件复位”或者“连接时保持内核停止”的方式接入,一旦芯片跑飞进了死循环、或者进了低功耗模式,就再也连不上了。
- 目标板独立供电。用调试器给目标板供电听起来方便,但板子上的电流一上来(比如无线模组发射瞬间),电压跌落会导致烧写中断,严重的还会把 Flash 写坏。产线上必须让目标板用自己的电源,调试器只接信号线。
ICP 还有一个 ISP 和 IAP 都没有的能力:在线调试。断点、单步、实时变量监控、指令跟踪,全靠这条路。所以研发阶段它是刚需,不可能被替代。
2.3 IAP:把烧录能力做进产品里
IAP 的本质是产品自己给自己做 ICP。你在 Flash 里放一段永远不会被覆盖的 Bootloader 代码,它负责接收新固件、校验、擦写应用区、跳转。用户看到的可能是一个手机 App、一个网页、一个上位机工具,底层跑的都是这套逻辑。
这套机制要成立,必须解决四个问题,缺一个就会在现场翻车:
第一,分区必须清晰。一块 Flash 至少切成三块:Bootloader 区、应用区(App)、参数区。如果要做回滚,应用区还要切成 A/B 双区。Bootloader 区一旦烧好就要写保护,绝不允许应用区代码碰它。
第二,跳转必须干净。从 Bootloader 跳到 App 之前,必须关中断、关 SysTick、反初始化所有外设,然后重设栈顶指针、重设向量表偏移寄存器 VTOR,最后跳到 App 的复位向量。任何一步漏了,表现都是“跳过去就死机”或者“中断进错地方”。
第三,校验必须可靠。只做长度校验等于没做。至少要 CRC32,更严格的做法是加数字签名,防止固件被篡改后刷进去。
第四,必须能掉电恢复。升级过程中断电是最常见的事故。如果没有保护,参数区里“升级中”这个标志写了一半,下次上电 Bootloader 就会陷入既不能跳 App 也不能升级的僵局。
关于速度,IAP 是所有方式里最慢的,因为它受现场链路带宽限制。举几个典型值:串口 115200 下大约 9KB/s,一个 512KB 固件要传将近一分钟;CAN 总线 500Kbps,考虑协议开销实际约 40KB/s,约 13 秒;低功耗无线链路每包只有几十字节还要等应答,可能得几十分钟。所以 IAP 一定要设计断点续传和分块校验,不能指望一口气传完。
3. 从裸片到量产:完整实操流程与关键代码
这一章给可以直接用的东西。我把流程分成研发、产线、现场三段,每段给关键配置。
3.1 研发阶段的 ICP 配置与工程设置
新建工程时,链接脚本里的 Flash 起始地址一定要按实际布局改。如果计划做 IAP,App 的起始地址必须往后挪,挪多少取决于 Bootloader 占多大。
/* App 工程链接脚本片段:Bootloader 占 32KB */ MEMORY { FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 480K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K } /* 关键:向量表必须跟着搬 */ _estack = ORIGIN(RAM) + LENGTH(RAM);对应的,App 代码里必须做向量表重定位。Cortex-M 的做法是写 VTOR 寄存器,但要注意必须在 main 函数之前完成,否则第一个中断进来就跳飞。稳妥做法是在SystemInit()里改:
/* App 启动时重定位向量表,偏移量与链接脚本保持一致 */ #define APP_VTOR_OFFSET 0x8000U void SystemInit(void) { /* 其他时钟配置省略 */ SCB->VTOR = FLASH_BASE + APP_VTOR_OFFSET; }Bootloader 那边负责跳转,这段代码我在多个项目里复用,加了栈顶合法性检查:
typedef void (*app_entry_t)(void); /* 返回 0 表示跳转失败,调用方可选择留在 Bootloader 等下次升级 */ int bootloader_jump_to_app(uint32_t app_base) { uint32_t sp = *(volatile uint32_t *)app_base; uint32_t pc = *(volatile uint32_t *)(app_base + 4U); /* 栈顶必须落在 SRAM 区间内,否则说明 App 区是空的或者被擦坏了 */ if ((sp < 0x20000000U) || (sp > 0x20020000U)) { return 0; } /* 复位向量必须是奇数(Thumb 状态位) */ if ((pc & 0x1U) == 0U) { return 0; } /* 关全局中断、关 SysTick、清所有 NVIC 使能位 */ __disable_irq(); SysTick->CTRL = 0U; SysTick->LOAD = 0U; SysTick->VAL = 0U; for (uint32_t i = 0U; i < 8U; i++) { NVIC->ICER[i] = 0xFFFFFFFFU; NVIC->ICPR[i] = 0xFFFFFFFFU; } /* 反初始化用到的外设,比如串口、定时器 */ bsp_deinit_all(); SCB->VTOR = app_base; __set_MSP(sp); ((app_entry_t)pc)(); return 0; /* 正常情况下不会走到这里 */ }这段代码里最容易被忽略的是bsp_deinit_all()。很多人跳转后一切正常,直到某天发现 App 里串口收到的第一个字节是乱码——原因就是 Bootloader 的串口还开着,中断标志没清,跳过去之后立刻触发了一次中断,而此时中断向量表刚切换过去,处理函数可能还是空的。
3.2 参数区设计:让升级过程扛得住断电
参数区不要存在一个固定地址上只写一份。Flash 擦写的最小单位是扇区(通常 1KB 或 4KB),写坏一次就是整块失效。我习惯的做法是参数区放两个扇区,各写一份,带序号轮转,写入时先擦旧的那份,序号加一,最后写一个魔数校验。上电时比较两份的序号和 CRC,取合法且序号大的那份。
参数结构大概长这样:
#define PARAM_MAGIC 0x5A5A1234U #define PARAM_VERSION 0x0002U typedef struct { uint32_t magic; uint32_t version; uint32_t seq; /* 每次写入递增 */ uint32_t app_size; /* 固件有效长度 */ uint32_t app_crc32; /* 固件 CRC32 */ uint32_t upgrade_flag; /* 0=正常启动 1=请求升级 2=已升级待确认 */ uint32_t retry_count; /* 启动失败重试计数 */ uint32_t reserved[8]; uint32_t crc32; /* 结构体自身校验,放最后 */ } param_t;retry_count这个字段非常关键。它解决的是“升级成功但新固件跑不起来”的场景——新固件启动后必须在一个超时窗口内(比如 30 秒)把upgrade_flag从 2 改成 0,表示“我活下来了”。如果 Bootloader 下次上电发现标志还是 2 且retry_count已经达到 3 次,就自动回滚到备份区。没有这个机制,一次错误的固件推送就能让整批设备变成砖头。
3.3 产线 ICP 一拖多实操与节拍测算
产线的核心指标是UPH(每小时产能)。以单颗 16 秒的 ICP 烧录时间算,单工位一小时理论 225 颗,实际算上取放板时间,能到 180 颗就很不错了。四工位并行可以做到 700 颗左右。
一拖多方案在实施时有几个硬性要求:每路必须有独立的电源和地,不能共用一根地线从同一个电源拉出来,否则某一路短路会拉垮全部;每路的 SWDIO/SWCLK 上串 33~100 欧姆电阻,防止一路的信号反射串到另一路;烧录软件要支持并行独立的握手,不能是轮询式的伪并行。
治具方面,量产用的探针建议选 2.54 间距的标准测试针,接触压力 100~150 克,探针寿命一般标称 10 万次以上。这里有个很实在的坑:探针氧化。用了一两个月之后,镀金层磨损露出底材,接触电阻从几十毫欧涨到几欧姆,症状是“有时候能烧、有时候报连接失败”,而且越来越频繁。解决办法是定期用无水乙醇加无尘布清理,或者直接定个寿命周期强制更换,别等出问题。
烧录完成后一定要做回读校验。不要相信编程器自己报的“成功”,让它把整个 Flash 读回来算 CRC 跟源文件比对,多花两秒,能挡掉绝大部分隐患。另外建议在烧录后写入唯一序列号,从数据库或离线文件按顺序取,写进参数区或者芯片的 OTP 区,这一步同时解决了售后追溯和防抄板两个问题。
3.4 IAP 升级通道的落地流程
完整的 IAP 流程分六步,每一步都要落到代码:
- 收到升级请求。通过串口命令、总线报文或者无线指令触发,Bootloader 或者 App 里的升级模块置位
upgrade_flag。 - 擦除目标区。注意不要擦 Bootloader 自己所在的扇区,也不要擦掉参数区。如果是 A/B 双区,擦的是备份区,不是当前运行区。
- 分包接收与写入。每包数据写完后立刻回 ACK,同时更新一个已接收长度到 RAM 中,并定期(比如每 16KB)同步到参数区的另一个副本,用于断点续传。
- 整体校验。所有数据收完后算 CRC32,跟固件头里带的值比对。
- 置位并重启。校验通过才允许修改
upgrade_flag,然后软复位。 - 启动确认。新固件正常运行超过预设时间后,清除升级标志,回滚计数归零。
第 3 步里,写入顺序有个讲究:先写数据,最后写固件头。固件头里带着长度和 CRC,如果先写头,中途断电时 Bootloader 会看到一个“合法但数据不全”的固件,校验失败后可能直接判定升级失败;而把头放最后,中途断电时头是空的,Bootloader 一眼就能判断这次传输没完成,直接进恢复模式等重传。
4. 选型决策:按场景对号入座,别凭感觉
讲完原理,选型其实是个列表查表的过程。我把常见的几种情况列出来,直接对号就行。
| 场景 | 首选方案 | 理由 | 需要提前准备 |
|---|---|---|---|
| 研发初期,频繁改代码 | ICP | 支持断点调试,秒级烧写 | 留 4~6 针调试口 |
| 小批量试产(<500 台) | ICP | 治具成本低,灵活 | 治具 + 离线烧录器 |
| 大批量量产(>10K) | ICP 一拖多 | 单板节拍可压到 4 秒内 | 多工位治具 + 序列号系统 |
| 板子空间受限,无调试口 | ISP | 只需通信口 + BOOT 引脚 | 板上留测试点 |
| 已出货产品的现场升级 | IAP | 免拆机,走现有通信链路 | 双区 + 回滚机制 |
| 调试口被读保护锁死 | ISP | 唯一能重写 Flash 的通道 | 保留 BOOT 引脚可操作 |
| 安全要求高的产品 | ICP + 读保护 | 烧完立即锁死调试口 | 量产流程固化 |
这里补几条表格装不下的判断逻辑。
第一,看产品生命周期成本,不只看单板成本。留一个 5 针的调试口,单板成本增加可能不到一毛钱,但它让你在售后阶段多了一条完整的恢复通道。反过来,如果产品是消费品、成本极度敏感、且出货量巨大,那就把调试口缩到 4 针甚至用半孔焊盘,靠 ISP 兜底。
第二,IAP 不是“有了就行”,而是“错了就完”。我见过太多项目,IAP 流程写得漂漂亮亮,唯独没做断电保护。一次产线断电、一次客户误操作,回来一堆砖。判断标准很简单:拔掉电源,在升级进行到一半时随机插入,重复二十次,如果每次上电都能重新开始升级,这套 IAP 才算合格。
第三,混合策略往往是最优解。我现在的默认方案是:Flash 里永远有一段最小 Bootloader(16KB 以内),它同时支持三种入口——检测到特定引脚电平进 ISP 兼容模式、收到特定报文进 IAP 升级流程、否则直接跳 App。这样一来,产线用 ICP 烧 Bootloader + App,售后用 IAP 推 App,救砖用 ISP 重刷 Bootloader,三条路互相兜底。
5. 常见问题与排查实录
这一节的每一条都是真实踩过的,按“症状—可能原因—验证方法”整理,方便直接查。
| 症状 | 高概率原因 | 快速验证方法 |
|---|---|---|
| SWD 连接失败,报“无法识别目标” | NRST 未接 / 芯片进低功耗 / 调试口被禁用 | 手动短接 NRST 到地再释放,同时点连接 |
| 连接时好时坏,偶发失败 | 排线过长 / 探针氧化 / 共地不良 | 换短线、清理探针、单独拉地线 |
| 烧录成功但程序不运行 | 链接脚本地址没改 / VTOR 未重定位 | 读回 0x08000000 处的前 8 字节看栈顶和向量 |
| 跳转到 App 后立刻 HardFault | 中断未关、外设未反初始化、栈顶非法 | 在跳转前加断点,检查 MSP 值 |
| ISP 上位机无应答 | BOOT 引脚电平不对 / 波特率偏差 / 串口线序反 | 示波器抓 TX 波形,看是否有回包 |
| ISP 传到一半中断 | 供电跌落 / 内部 RC 时钟温漂 | 换稳定电源,降低波特率到 57600 试 |
| IAP 升级后设备无响应 | 校验缺失导致坏固件被写入 / 无回滚机制 | 检查参数区标志位与 CRC 计算逻辑 |
| 升级过程中断电后设备变砖 | 参数区单副本写入 / 升级标志先于数据写入 | 检查参数区是否双备份、写入顺序 |
| 量产良率突然从 99% 掉到 85% | 治具探针磨损 / 烧录器电源老化 | 换一套治具对比,测烧录器输出电压 |
| 读保护开启后再也连不上 | 调试口被永久禁用 | 只能靠 ISP 全片擦除恢复 |
挑几个展开说。
“烧录成功但程序不运行”这条,八成是链接脚本和实际烧录地址对不上。有人把 Bootloader 和 App 编成了两个工程,App 忘了改 ORIGIN,编译出来还是从 0x08000000 开始,ICP 烧进去直接把 Bootloader 覆盖了。症状就是上电之后什么反应都没有。验证方法很土但很有效:用调试器读 0x08000000 处的 8 个字节,前 4 字节应该是合法的栈顶(落在 SRAM 范围内),后 4 字节应该是奇数(Thumb 位)。如果读到的是 0xFFFFFFFF,说明那块根本没烧进去。
“跳转到 App 后立刻 HardFault”这条更隐蔽。有个项目卡了我整整两天,最后发现是 Bootloader 里开了串口的 DMA 接收,跳转前没关 DMA,App 里一初始化串口就触发了残留的 DMA 传输完成中断,而此时中断向量表刚切过去,对应的处理函数地址是乱的。从那以后我养成了习惯:跳转前把所有外设的时钟全部关掉,用RCC->AHB1ENR = 0这类粗暴但有效的方式,反正跳过去之后 App 会重新初始化。
“IAP 升级后设备无响应”这条,最常见的原因是在 App 里擦除了自己正在运行的区域。有些实现为了省 Flash,只有一份 App 区,升级时先擦自己再写新的,一旦中途出问题就没救了。正确做法要么是 A/B 双区,要么是先把新固件完整收进外部存储或者 RAM(如果够大),校验通过后再一次性擦写。
再补一个排查技巧:用示波器看 SWD 波形比看日志有用一百倍。连接失败的时候,抓 SWCLK 和 SWDIO,如果 SWCLK 有波形但 SWDIO 一直是高电平,说明目标根本没应答,问题在硬件连接或者芯片状态;如果 SWDIO 有短促的低电平脉冲然后恢复,说明应答了但随后出错,问题在协议层或者时钟太快。
6. 几条用血换来的实操心得
写到这里,技术点基本覆盖完了,最后分享几条不成体系但很实用的经验。
第一条,Bootloader 的代码量要死守上限。我给自己定的规矩是不超过 16KB,最好是 8KB 以内。原因很实际:Bootloader 是唯一不参与升级的代码,一旦它有 bug,代价是全批次返厂。代码越少,bug 越少,审查越容易。所有花哨的功能(日志、多协议支持、加密)都要掂量着加。
第二条,调试口关不关,按产品定位决定,但关之前一定要验证 ISP 通道可用。有些团队为了防抄板,量产时直接开最高等级读保护,把调试口永久锁死。这本身没问题,问题是他们从来没验证过 ISP 能不能用,等第一批客户返修回来才发现,连厂商的编程器都救不回来。我的做法是:小批量阶段就把读保护打开,然后故意用 ISP 刷一遍,确认通道畅通,再放量。
第三条,序列号写入要放在烧录的最后一步,并且和校验绑在一起。先写序列号再烧固件,中间失败了序列号就浪费了;先烧固件最后写序列号,写失败还能重试。而且序列号最好写两份,一份在参数区,一份在芯片 OTP 或者备份扇区,防止参数区被擦。
第四条,产线的烧录工位要配离线烧录器做备份。在线编程器依赖电脑和 USB 连接,电脑一蓝屏、USB 线一松动,整条线就停了。离线烧录器脱机工作,把固件拷进去,按一下按钮就烧一颗,稳定性完全不是一个量级。成本也就几百块,非常值。
第五条,别在产线上用“只擦写必要扇区”这种优化。有些编程器支持增量烧写,只擦有变化的扇区,听起来快,实际风险很高——一旦上次烧录残留了脏数据,增量烧写不会清理,最后表现为“个别板子功能异常,重新全擦全写就好了”。产线一律全片擦除,宁可多花两秒。
第六条,IAP 的升级包要能降级。这条容易被忽略。新固件出了严重问题,客户现场需要退回旧版本,如果你的 Bootloader 校验里带了“版本号必须大于当前版本”的限制,那就退不回去了。版本号只做记录,不做方向限制,让决策权留在人手里。
第七条,所有跳转和擦写操作前,先存一份“事后能看懂”的日志。参数区里留几十个字节,记录最近一次启动的原因、跳转的目标地址、升级的起止时间和结果。出问题的时候把这几十个字节读出来,比抓一周的串口日志都管用。
这套东西说起来不复杂,但每一条背后基本都对应着一次现场事故或者一批返修。芯片烧录这件事,在项目顺利的时候是最不起眼的环节,可一旦出问题,它就是最要命的那个——因为它直接决定设备还能不能开机。