拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

芯片烧录全解析:ICP、ISP、IAP三种方式与实战避坑

芯片烧录全解析:ICP、ISP、IAP三种方式与实战避坑

芯片烧录这个词,很多刚入行的朋友第一反应是“把程序写进芯片就完事”,但真正干过几轮硬件调试后,你会发现烧录这件事背后有一整套门道:ISP、ICP、IAP三种方式分别解决什么问题、什么场景该用哪一种、为什么明明代码没问题却烧不进去,这些问题如果不提前搞清楚,很容易在样板阶段就卡住进度。

这篇文章我不打算讲得太教科书化,就用做项目时的真实视角,把芯片烧录的本质、三种模式的原理和区别、实际踩过的坑,以及IAP升级里那些容易被忽略的细节一次说清。不管是刚接触单片机的新手,还是被现场升级问题折磨过的工程师,读完应该都能对“怎么把固件弄进芯片”这件事建立起完整的判断框架。

1. 先从“烧录”这件事说起

1.1 烧录的本质:把编译好的二进制放进Flash

很多人第一次用开发板,点一下编译、再点一下下载,程序就跑了,感觉整个过程特别简单。但“烧录”这个词背后的关键动作,并不是“传输”,而是“写入存储器”。

微控制器内部用来保存代码的,是Flash存储器。我们在Keil、IAR、GCC这些工具里写完C代码,经过编译、链接之后,会生成两种最常见的文件格式:HEX和BIN。HEX文件是带有地址信息的文本格式,每一行都告诉你这段数据该写到Flash的哪个地址;BIN文件则是纯二进制数据流,一个字节接一个字节地对应Flash中的存储内容。烧录器把Flash当作一张可以擦写的“草稿纸”,先把对应的扇区擦除,再把程序数据按地址写入,最后做一次校验,确保写入的内容和原始固件完全一致。

以STM32F103为例,它的内部Flash起始地址是0x08000000,编译出来的程序就是从0x08000000开始存放的。烧录器会按芯片手册的时序,把二进制内容从这个地址开始逐个写入,这个过程严格来说叫Flash编程。擦除、写入、校验,这三个动作缺一不可,任何一步失败,程序都可能跑飞。

1.2 为什么叫“烧录”而不是“复制”

“烧录”这个词有点历史感。早期有一类可编程存储器叫EPROM,里面的数据是靠紫外线照射擦除的,芯片上有一个石英窗口,每次修改代码都得用专门的紫外线灯“晒”上十几分钟才能擦干净。更早还有熔丝型PROM,编程时是真的会把芯片内部的熔丝“烧断”,用物理烧断的方式记录数据。所以“烧”这个字,是从硬件层面真实发生过的事情演变来的。

现代单片机虽然早就不用这种暴力方式了,但术语保留了下来。现在说的烧录,本质上就是擦除+写入+校验。理解了这个历史背景,你就知道为什么芯片擦写是有次数限制的,比如NOR Flash通常标称10万次擦写寿命,虽然日常工作根本用不完,但在做产品升级逻辑设计时,还是要尽量避免频繁整片擦写。

2. 核心概念拆解:ICP、ISP、IAP三种模式到底差在哪

2.1 ICP:用烧录器直接操作芯片,开发调试和量产的主力

ICP的全称是In-Circuit Programming,直译是“在电路编程”,也就是把烧录器通过调试接口直接连到目标芯片上,由外部工具主导擦写Flash。这是最常见、最直接的一种烧录方式,开发阶段几乎天天都在用。

常见接口有两类:一类是JTAG,四线制(TMS、TCK、TDI、TDO),速度快但占用的引脚多;另一类是SWD,两线制(SWDIO、SWCLK),在ARM芯片上非常主流,只需要两根信号线加电源和地。ST-Link、J-Link、DAP-Link这些调试器,就是通过SWD接口实现程序下载和在线调试的。

SWD连接说起来很简单,就是四根线:

调试器端芯片端作用
SWDIOPA13 / SWDIO双向数据线
SWCLKPA14 / SWCLK时钟线
GNDGND共地基准
3V3VDD供电或参考电平

实际项目中容易出现一个很有意思的坑:有些工程师为了省事只接SWDIO、SWCLK和GND三根线,不接目标板的供电,靠调试器给板子供电,结果大电流器件一启动就把电压拉垮,烧录反复失败。我的建议是能接电源就接电源,让烧录器只做通信,不承担供电任务,这样排查问题会简单很多。

ICP还有一个独特优势是“不依赖芯片里是否已有代码”。哪怕芯片是全新的、内部Flash是空的,只要物理连接没问题,就能烧录。开发阶段改代码、量产前写序列号、读取芯片Flash内容做备份,基本都是ICP干的活。

2.2 ISP:借芯片自带Bootloader完成串口下载,成本低但有限制

ISP的全称是In-System Programming,直译是“在系统编程”。它不再需要外部烧录器,而是利用芯片出厂时固化在ROM里的一段引导程序(Bootloader),通过串口、USB、SPI、CAN等通信接口,把新的固件接收进来并写入Flash。

用STM32举一个很典型的例子。芯片内部有一个“系统存储器”(System Memory),里面固化了一段ISP Bootloader,它的启动条件是特定组合的BOOT引脚电平。比如STM32F1系列,把BOOT0拉高、BOOT1拉低,复位后芯片就会从系统存储器启动,而不是从主Flash启动。这时用USB转TTL工具连上串口1,配合ST官方工具或第三方软件,就能通过串口把固件下发到芯片Flash里。

为什么要设计ISP这种方式?两个直接好处。第一,生产阶段不需要购买昂贵的烧录器,一条USB转串口线就能批量下载程序;第二,芯片已经焊到板子上也能烧录,不用像早期那样先烧好芯片再贴片。很多8位单片机,比如STC系列,至今依然靠ISP作为主要的程序下载方式,原因就是成本足够低、使用足够便捷。

但ISP也有明显的限制。它依赖芯片出厂自带的Bootloader,这部分代码是芯片厂商固化好的,你改不了里面的逻辑。通信协议固定了,可用的外设引脚也固定了,比如STM32F1的ISP固件通常就绑定在USART1上,如果你的产品设计时把USART1的引脚复用了别的功能,那ISP这条路就走不通。此外,ISP通常不能对Bootloader自身所在的存储区编程,也就是说它只能更新用户Flash区域,没办法像IAP那样灵活地规划整个Flash布局。

2.3 IAP:应用程序运行中更新自己,OTA升级的底层原理

IAP的全称是In-Application Programming,直译是“在应用编程”。理解IAP最好的方式,是把它跟ISP放在一起对比。

ISP是芯片里出厂时就已经有了一段Bootloader,开机先跑Bootloader,再根据外部信号决定要不要接收固件;IAP则是你自己写一段Bootloader放在Flash的最前面,上电先执行你的Bootloader,由你的代码决定接下来是正常跳转到应用程序,还是接收新固件完成升级。ISP的引导程序和通信协议是芯片厂商定的,IAP完全是由开发者自己设计和控制的。

可以说,IAP给了你对Flash布局的最高控制权。常见的做法是把Flash分成两个区域:Bootloader区放在最低地址,用户应用程序区放在Bootloader之后。Bootloader负责检查升级标志、接收固件数据包、写入应用程序区;应用程序收到升级指令后,把自己的升级标志位设置好,然后跳转到Bootloader执行,Bootloader再完成最后的数据搬运和跳转回调。

你手机上收到系统更新通知,点击升级,系统重启后进入“正在安装更新”界面,这个过程的嵌入式版本就是IAP。IoT领域常说的OTA,本质上就是通过网络传输固件,再通过IAP机制完成Flash更新。跑在设备上的代码,最终要利用IAP这个通道把自己替换成新版本。

三种方式放到一张表里看更直观:

维度ICPISPIAP
编程发起方外部烧录器/调试器芯片自带Bootloader用户自定义Bootloader/应用程序
额外硬件需要ST-Link/J-Link等调试器需要串口/USB转接工具通过现有通信接口即可
是否依赖芯片内部代码不依赖,空片也能烧依赖出厂Bootloader依赖自己写的Bootloader
能否升级Bootloader本身可以(通过烧录器)通常不能可以(但需特殊设计)
典型场景开发调试、量产烧录低成本小批量生产产品在线升级、OTA

3. 从实际应用出发,怎么选才不会后悔

3.1 按开发阶段和量产阶段来匹配

选ISP、ICP还是IAP,没有绝对的“哪个更好”,只有“哪个更合适当前阶段”。

个人开发或小批量样板阶段,首选ICP。原因很现实:你需要频繁修改代码,SWD接口不但能下载程序,还能打断点、看变量、单步调试,调试器配合IDE能大幅缩短“改代码—验证效果”的循环周期。如果你在这个阶段用ISP,意味着每次改代码都要手动切换BOOT引脚、进引导模式、再用串口下载,折腾几次你就想摔板子了。

试产或成本敏感的小批量生产阶段,ISP值得考虑。不需要买几十个调试器,一条量产下载线接上,换板子、点下载,简单培训就能上手。但要注意流水线效率,比如每次下载前需要人工做BOOT引脚切换,或者需要复位后快速进入引导程序,这些都会影响节拍。如果量比较大,更专业的做法是使用“离线烧录器”,先把固件存入烧录器内部,接上芯片后按一下按钮完成烧录,不用连电脑、不用装软件,产线工人操作门槛极低。

产品已经上市或需要远程维护的阶段,IAP就必须提前规划了。你不可能要求用户拆开设备插上烧录器,也不能指望现场工程师都带着SWD调试器。IAP配合无线模块、4G模块或以太网,就能实现远程发版本,这就是OTA。设计IAP方案时,最关键的是在产品方案初期就把Bootloader写进架构,不要等到产品已经量产了才想起要升级,那时Flash分区、中断向量、通信协议全都定死了,改起来代价极高。

3.2 同一个芯片怎么在三种方式之间切换

很多单片机其实同时支持三种方式。拿STM32F103举例,它既能通过SWD用ICP烧录,也能通过串口1用ISP烧录,用户还可以自己实现IAP。

这里有一个很重要的BOOT引脚逻辑,新手经常在这上面栽跟头。STM32F1系在复位瞬间会根据BOOT0和BOOT1的电平决定从哪里启动:

BOOT0BOOT1启动区域实际用途
0任意主Flash正常运行用户程序
10系统存储器进入ISP Bootloader
11SRAM调试用,掉电即丢

做开发板时通常会在BOOT0上接一个拨码开关或跳线,就是这个原因。想用ISP下载,就先把BOOT0拨到1,复位一下,让芯片进入厂商Bootloader;下载完毕再把BOOT0拨回0,再次复位,程序才会从主Flash启动。如果你不拨回来直接运行,芯片会一直停在ISP引导程序里,看似“程序烧录成功但跑不起来”。

很多新手的困惑来自这里:明明烧录成功,程序为什么不运行?答案往往是BOOT引脚状态不对。所以遇到“程序不运行”的第一反应应该是:先确认芯片是从主Flash启动的,再看代码逻辑。

3.3 量产烧录时的文件和配置细节

量产阶段还有一个容易被忽视的点:烧录文件格式和烧录选项。开发阶段用IDE下载,通常是直接用调试器把可执行文件烧进去,参数都是IDE配好的。但量产时如果用离线烧录器,就需要自己导出烧录文件,并确认芯片型号、地址范围、加密选项等参数。

HEX和BIN在量产中的差异很明显。HEX带地址信息,烧录器可以根据地址自动分布数据,适合分段烧录;BIN是纯数据,必须指定起始地址,只要地址配置错了,程序就会跑到错误位置。我见过工厂把BIN文件的起始地址配在0x08000000,但实际上代码是从0x08008000开始的,结果每一台机器都不启动,最后才发现是地址参数的问题。

量产时还应该考虑“读保护”的配置。给芯片打开读保护(RDP级别1)后,外部调试器就不能随意读取Flash内容,这能在一定程度上保护固件不被抄板。但要注意,开启读保护后,再次用ICP烧录时需要先解除保护,而解除保护通常伴随着Flash全片擦除。也就是说,你不能在不解保护的情况下直接增量烧录,第一次烧录就要把保护和程序一并完成。

4. 实操避坑:烧录失败排查和那些没人告诉你的小细节

4.1 下载失败时的标准排查顺序

“找不到设备”“连接超时”“下载失败”应该是嵌入式工程师最高频的报错三兄弟。我总结出来一套排查顺序,按这个顺序走,大多数问题都能定位。

第一步,查供电。芯片有没有复位循环?电源纹波大不大?如果目标板上有电机、继电器这类大负载设备,烧录时一定先把它们断开。SWD烧录时若出现电压跌落,调试器很容易断开连接,这种问题换再好的调试器也没用。

第二步,查接线。SWDIO、SWCLK有没有接反,GND有没有共地,排线有没有松动。特别要注意杜邦线连接时接触不良的问题,看起来插上了,实际上氧化层导致信号时断时续。

第三步,查电平占用。芯片的SWD引脚如果被程序设计成普通GPIO,而且当前正被强制拉低或拉高,调试器依然可能无法建立连接。这时需要用到“连接时复位”功能,在IDE或烧录工具中勾选Under Reset模式,让调试器在复位期间抓住芯片,再抢占SWD接口。

第四步,查Flash保护。如果芯片之前开过读保护,标准的连接和读取会被拒绝。解决方案一般是执行全片擦除或解除保护,但这会清空Flash数据,所以文件备份要提前做好。

最后一步,降速率。很多奇怪的连接失败,其实是SWCLK速率太高引起的。把SWD时钟从默认的4MHz或8MHz降到1MHz,经常就能稳定连接了。这个操作在调试器超长排线或者芯片与调试器电平不匹配时尤其见效。

4.2 ISP模式进不去的几个常见原因

用ISP方式下载时,最常见的失败现象是“上位机软件一直提示等待超时”。

先检查BOOT0电平。很多板子上的BOOT0跳线帽接触不好,或者引脚被其他外围电路拉低,导致芯片实际没有进入系统存储器。建议用万用表量一下BOOT0引脚的实际电压,不要只看跳线帽位置。

再检查串口接线。ISP下载用的串口波特率通常较高,比如115200或57600,如果USB转TTL模块质量太差,或者线材太长,数据就容易丢包。另外TX和RX必须交叉连接,芯片的RX接USB转TTL模块的TX,芯片的TX接模块的RX,接反了就完全不通。

还有一个细节是“断电上电复位”。ISP握手过程一般要求芯片在上电复位后才能进入引导程序,所以很多工具都提示你“先按复位按钮”或“先重新上电”。如果你只是用接线方式连接但没有复位芯片,芯片可能还运行着旧的应用程序,自然不听串口指令。

4.3 关于“去弹窗”这类工具的提醒

网络上搜芯片烧录相关内容时,经常会看到“某系列ISP软件去弹窗”“去弹窗版下载工具”之类的词。这类工具确实能省掉一些等待时间和推广弹窗,但我的态度一直是:开发和生产工具尽量用正版官方渠道。厂商在ISP软件里加入启动等待或推送,虽然烦人,但至少行为可预期。

如果真想绕开厂商ISP软件的弹窗等待,更稳妥的做法是使用官方提供的命令行工具,或者直接通过串口用底层协议与Bootloader通信。比如ST就提供了STM32CubeProgrammer的命令行模式,脚本化执行烧录,既干净又适合流水线集成。为省几秒钟的等待去用来路不明的修改版软件,万一工具里夹带了额外数据或者错误配置,烧坏一批芯片的代价远远大于省下的时间。

4.4 烧录器的选择建议

个人建议开发阶段至少准备两个调试器。一个固定的ST-Link或DAP-Link连主力开发板,一个便宜的备用调试器应对突发状况。J-Link这类高端调试器功能强大,但早期很多兼容版固件升级会出问题,反而影响效率。

做脱机量产烧录时,可以关注支持多芯片型号的烧录器,比如一些主打“一拖八”或“一拖十六”的离线烧录器,可以同时烧录多块板子,产线效率提升明显。不过要注意烧录器对芯片序列号的写入支持,有些产品需要在固件中写入唯一序列号,普通的“只烧程序”工具就搞不定,得选支持序列号生成和写入的型号。

5. 深入IAP:中断向量、升级协议和变量复位的那些事

5.1 先搞清楚Flash分区和中断向量重映射

我接触过不少做IAP升级半途而废的项目,原因惊人地一致:按照网上的教程把程序分成了Boot和App两个部分,烧录都正常,跳转也能跳过去,但一进中断就死机。

问题基本都出在中断向量表。ARM Cortex-M系列的芯片上电后,会从固定地址(一般是0x00000000或0x08000000)读取向量表,这里存放着栈顶指针和各个中断服务函数的入口地址。如果你的App放在0x08008000,但是中断向量表还是指向0x08000000,那么一旦发生中断,CPU就会去读Boot区开头的中断向量,找不到对应的App中断服务函数,程序立刻跑飞。

解决办法是在App代码启动的最早阶段,把向量表重新定位到App所在的地址。STM32F1/F2/F3系列常用这样的方式:

#define APP_ADDR 0x08008000U void app_main(void) { SCB->VTOR = APP_ADDR; // 后续初始化外设和中断 }

F7/H7系列也是用SCB->VTOR,但H7还涉及Flash接口和地址别名的问题,配置时如果发现跳转后仍然异常,需要确认芯片是否有“地址映射重定向寄存器”需要同步设置。GD32虽然号称兼容STM32,但个别型号在中断向量重映射时的行为有差异,踩坑概率比想象中高,升级前一定要对着具体型号的手册核对。

5.2 Bootloader跳转App的完整步骤和注意点

跳转这件事看似简单,实际上有几个步骤必须按顺序做。一段最常见的跳转代码大概是这样的:

typedef void (*app_entry_t)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_sp = *(volatile uint32_t *)app_addr; uint32_t app_pc = *(volatile uint32_t *)(app_addr + 4); app_entry_t entry = (app_entry_t)app_pc; __disable_irq(); // 跳转前关闭全局中断 SCB->VTOR = app_addr; // 设置新中断向量表 __set_MSP(app_sp); // 切换到App的栈指针 entry(); // 跳转到App的Reset_Handler }

跳转前为什么要先关中断?因为Bootloader运行时可能已经打开了某个外设中断,如果带着中断跳过去,App的启动代码还没来得及配置这个外设,中断一来就是未定义状态。跳转后App的SystemInit和main会重新配置时钟和中断,所以Boot侧关掉中断后,不需要再手动开启,交由App接管即可。

这个过程中还容易犯一个错:在Bootloader里直接用函数指针跳转,跳转之后又用局部变量判断“是否跳转成功”。实际上跳转后Bootloader的代码就不再执行了,栈空间也会被App重新初始化,任何在Boot里定义的普通全局变量都会在这个切换过程中失效。

5.3 “IAP Boot里面定义的变量复位后会怎样”

这个问题在相关搜索里出现频率很高,其实问的是:“我在Bootloader里设置一个标志位,重启后让App知道要不要执行升级,这个标志位还在吗?”

答案分两种情况。如果是正常的软件复位、引脚复位或看门狗复位,MCU的启动代码会把RAM清零,你在Bootloader里设置的普通全局变量不会保留。更准确地说,启动代码会执行初始化操作,把.bss段清零、把.data段从Flash复制到RAM,所以你之前在RAM里存的值早就被覆盖了。如果你指望复位后还能读到这个值,必须“绕过”常规RAM的初始化机制。

一个常用的做法是把变量放在“不初始化”段:

__attribute__((section(".noinit"))) volatile uint8_t upgrade_flag = 0;

同时需要在链接脚本里把.noinit段保留下来,不让启动代码清零。这种做法本质上就是在RAM里划出一块“自留地”,复位后内容保持不变。

比这更稳妥的设计是把握手信息放在备份寄存器中。备份寄存器在芯片掉电或复位时不一定会丢失,因为由VBAT域供电,代码可以通过RTC或备份寄存器的接口来读写。这类方案在低功耗设备和需要断电重启升级的场景下更可靠,因为有些设备的IAP流程会走到“断电后重新上电”,彻底刷新RAM,只有备份域数据能撑过这个阶段。

5.4 大容量芯片和外部Flash的IAP特殊操作

不同芯片做IAP时的差异很大。比如STM32H750VBT6,它的内部Flash在部分量产型号上容量偏紧张,很多方案会把应用程序放到外部QSPI Flash中运行或存放。这时IAP的流程就变得更复杂:Bootloader要先初始化QSPI控制器,从外部Flash读取固件,然后写入内部Flash,或者直接在外部Flash中执行代码。

直接外部Flash执行代码涉及“映射地址”的概念,需要把外部存储器的数据映射到芯片的地址空间,中断向量表也要指向这个映射区。此时SCB->VTOR的设置值就不再是内部Flash地址,而是外部映射基地址。这个问题如果在设计阶段没想清楚,后期排查会非常痛苦,因为现象千奇百怪:有时能跑起来、有时一触发中断就挂、有时上电顺序不对就完全没反应。

我的建议是,凡是涉及外部Flash执行代码的项目,至少提前用官方评估板验证方案,再动PCB设计。外部Flash的读取速率、等待周期、内存映射页大小、烧录算法,每一项都可能影响系统稳定性,把问题留在硬件定型前去解,效率最高。

5.5 升级过程断电了怎么办

IAP在设计时必须把“断电”当成正常流程来考虑,而不是异常情况。因为产品在客户端升级时,用户很可能随时拔电、锁屏、断网。

最简单的容错方案是“双分区”,也叫A/B升级。Flash里同时保留两个应用程序分区,一个运行、一个待写入。升级写入时只影响待升级分区,即使写入一半断电,另一个分区还是完好可用的。下次启动时Bootloader检查新分区不完整,就自动回退到旧分区,用户几乎无感。

如果芯片Flash容量不允许双分区,也可以用“备份+恢复”策略:先把旧固件备份到外部Flash或足量的剩余扇区,再写入新固件。但这种方式对Flash剩余空间要求不低,而且备份和恢复都要消耗时间,现场体验差一些。

除此之外,升级协议本身也要做好“断点续传”意识。固件分包编号、每包CRC校验、总固件长度和版本号,都要在应用层设计好。即便传输中断,重新升级时也不需要从头开始,或者至少能快速确认目标分区是否完整,避免在错误的分区上花时间。

6. 最后再分享一点个人经验

做了这么多年嵌入式,我最深的体会是,烧录问题看起来千奇百怪,但绝大多数都逃不过“连接不稳定、电平不匹配、Flash保护状态不明、中断向量配置错误、启动条件不满足”这几类。遇到现象诡异的问题,先冷静下来按供电、接线、电平、保护、速率这个顺序排查,比反复拔插调试器有用得多。

另外,每一次涉及IAP的固件发布,我都会强制要求做一个“回滚验证”:先升级到新版本,再回滚到旧版本,确认Bootloader在两个方向上都稳定。这个动作看起来简单,但能在出货前拦下大量现场故障。烧录这件事,单个环节都不复杂,难的是把每个环节的细节都照顾到位。

一块板子能稳定地烧录一百次,并且每次烧录后程序都能按预期跑起来,你的工程化能力就过关了。

返回列表