ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103C8T6串口IAP升级:Bootloader分区与跳转避坑指南

STM32F103C8T6串口IAP升级:Bootloader分区与跳转避坑指南

简介:面向STM32F103C8T6开发者的串口IAP升级固件源码包,完整解决单片机通过串口实现在线固件升级与跳转执行的需求,适合需要掌握Bootloader原理或正在调试在线升级方案的嵌入式工程师,也适合为仪器仪表、物联网节点等设备增加远程维护能力的项目参考。资源共430个文件、2.23MB,以86个C源文件与86个头文件构成主要逻辑,另有汇编启动文件、链接脚本以及uvproj、ewp等工程配置,覆盖从源码修改、编译链接到生成hex/bin固件的完整开发链条;汇编入口与链接脚本便于分析启动过程和内存布局,说明文档则可帮助快速理解代码结构。包内基础工程直接搭建了IAP升级环境,按典型流程展示Flash分区规划、串口通信协议与固件跳转处理,且保留较多可配置参数,方便根据实际硬件或协议需求进行裁剪移植,省去从零构建引导程序的繁琐环节。该资源已有5148人学习下载,对初学STM32在线升级或需要快速复现Bootloader方案的中高级开发者,是一份实用价值较高的参考源码包。

1. 串口IAP升级,STM32F103C8T6最反直觉的一次开发

在 STM32F103C8T6 的项目里做串口 IAP 固件升级,最反直觉的一点是:决定升级过程能不能可靠完成的,不是那几行“接收串口数据、写 Flash”的代码,而是 Bootloader 里两个看起来不起眼的瞬间——上电后等多久再跳 App,以及“升级请求”标志被放在哪个地址。这两处没想清楚,设备在产线上烧好第一版时一切正常,发到现场要通过串口升级时,十次里有三次变砖。

这个源码包做的就是把 C8T6 的 64KB Flash 拆成 16KB Bootloader 和 48KB App,用串口协议把新固件分包写入 App 区,校验通过后跳转执行。适合刚接触 IAP 的人照着完整流程手搓一遍,也适合正在做量产设备、被现场升级稳定性反复折磨的开发者对照排查。下面按我从拿到源码到跑通、再到改出自己一版的顺序来讲。

2. 整体设计与 Flash 分区:Bootloader 和 App 怎么在 64KB Flash 里共存

拿到 C8T6 的第一件事,是把“Flash 有 64KB”这个数字忘掉。实际应用可用的只有 48KB,因为前面 16KB 要划给 Bootloader。很多人的 App 加了协议栈、日志和状态机之后塞不进 48KB,十有八九是在选型时没做这个心理预期。IAP 不是把两个程序随便丢进 Flash,而是先画好地盘再写代码。

2.1 双区架构与内存规划:16KB Bootloader + 48KB App

源码包里的分区方案很直接,一张表就能说清:

区域起始地址大小存放内容
Bootloader 区0x0800000016KBIAP 主程序、串口驱动、Flash 擦写驱动
App 区0x0800400048KB业务固件、App 中断向量表
参数页0x08003C001KB升级请求标志、版本号记录

为什么要留 16KB 给 Bootloader,而不是省到 8KB?我用标准外设库编译过这个工程,串口加 Flash 驱动加上协议解析,编译出来大约 6~9KB,留 16KB 是给自己留后路。后面要加 CRC 校验之外的加密、签名,代码量会明显膨胀;如果把 Bootloader 压缩到 4KB,纯寄存器操作确实能塞下,但可读性和可维护性都会变差。

这里有个绕不开的硬件事实:Cortex-M3 上电后从 Flash 起始地址取两个字——第一个字是初始主堆栈指针 MSP,第二个字是复位向量。Bootloader 的向量表必须在 0x08000000,因为这是硬件固定的。而 App 的向量表本来也应在 0x08000000,可那个位置被 Bootloader 占着,App 只能把向量表挪到 0x08004000。麻烦在于 CPU 并不知道这个偏移,必须通过 SCB->VTOR 告诉它。STM32F103 的 VTOR 在 0xE000ED08,F1 全系列都有这个寄存器,这点比某些 Cortex-M0 单片机要省事。

2.2 启动流程与跳转条件:升级标志位、串口窗口与跳转判定

整个启动流程如果用文字描述,是这样的链路:

上电复位 → Bootloader 初始化时钟和串口 → 读取参数页的升级请求标志 → 若标志有效,清标志并直接进入升级等待模式 → 若标志无效,等待约 300ms 的串口命令窗口 → 窗口内收到握手帧,进入升级模式 → 窗口结束仍未收到命令,跳转到 App。

为什么是“等约 300ms”而不是无限等?现场升级要求上电盲操作,设备一上电 Bootloader 先开一个短暂的升级窗口,上位机只要能在这个窗口里发出握手帧,就能抢占。窗口太短,现场人员来不及;窗口太长,设备每次开机都要慢几秒。300ms 是我在速度和成功率之间试出来的经验值,源码包里也留了宏定义,可以自己调。

升级请求标志放在参数页,而不是放在 App 区尾部,原因很实际:App 运行时不能擦写自己正在执行的 Flash 页。假如把标志写在 App 区同一页,程序一边从 Flash 取指令一边擦写那一页,轻则写失败,重则 HardFault。所以 App 请求升级时,是把标志写给 Bootloader 区里独立的一页。核心代码是这样的:

#define PARA_FLAG_ADDR 0x08003C00 /* Bootloader区最后一页,1KB */ #define FLAG_MAGIC 0x5A5AA5A5 void app_request_upgrade(void) { uint32_t flags = FLAG_MAGIC; FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); FLASH_ErasePage(PARA_FLAG_ADDR); /* 先整页擦除 */ FLASH_ProgramWord(PARA_FLAG_ADDR, flags); /* 写入魔数 */ FLASH_Lock(); NVIC_SystemReset(); /* 软复位进Bootloader */ }

这段代码关键在两点:擦除后写入是标准流程,不能省掉 ClearFlag;写完魔数后立刻软复位,让 Bootloader 来决定接下来是升级还是正常跳转,App 自己不做升级流程的分发。现场人员只要触发 App 里的这个函数,设备重启后就会自动停在升级等待状态。

Bootloader 启动时的判断同样简单:

if (*(volatile uint32_t *)PARA_FLAG_ADDR == FLAG_MAGIC) { FLASH_ErasePage(PARA_FLAG_ADDR); /* 清掉请求标志,防止下次重复误入升级 */ iap_state = IAP_STATE_UPGRADE; /* 进入升级等待模式 */ }

清标志必须在跳转升级逻辑之前完成,否则升级过程中途断电,再上电会又一次进入升级模式,反而把正常 App 挡住。这个顺序我一开始写反过,结果现场出现“设备永远停在升级等待,App 起不来”的翻车,原因就是标志没被清掉。

3. 串口升级协议与 Bootloader 核心代码拆解:一帧一帧把固件搬进 Flash

Bootloader 的本质是一个串口透传写入器:上位机发一帧,Bootloader 解析一帧、校验一帧、写一次 Flash。协议设计得不好,升级过程就会变成一场对时钟和运气的赌博。这章我把协议帧、状态机、Flash 操作和跳转逻辑逐一拆开讲。

3.1 串口协议帧设计:帧头、命令字、长度与 CRC16 校验

源码包里的协议帧格式很简单,但足够防错:

字节序字段长度说明
0帧头2 字节固定 0xAA 0x55
2命令字1 字节0x01 升级开始,0x02 数据块,0x03 完成校验,0x04 跳转 App
3数据长度2 字节小端序,payload 字节数,不含帧头和 CRC
5Payload变长数据块命令里是 offset(4 字节) + 固件数据(最多 256 字节)
末尾CRC162 字节CRC16-MODBUS,覆盖数据长度和 Payload

命令字是升级流程的骨架。0x01 升级开始命令里,上位机告诉 Bootloader 固件总长度,Bootloader 收到后擦除整个 App 区并回复 ACK;0x02 数据块命令承担真正的搬砖工作,每帧最多带 256 字节数据;0x03 完成校验命令用于回读 Flash 算 CRC;0x04 才是真正触发跳转的那一下。

串口接收我建议用状态机而不是直接等一个“完整帧中断”,因为 STM32F103 的串口一次中断只停在一个字节上,数据是碎片化到达的。状态机在串口中断里逐字节喂,最稳妥:

typedef enum { ST_WAIT_HEAD1 = 0, ST_WAIT_HEAD2, ST_WAIT_CMD, ST_WAIT_LEN, ST_WAIT_PAYLOAD, ST_WAIT_CRC } RX_STATE; static RX_STATE rx_state = ST_WAIT_HEAD1; static uint16_t payload_len = 0; static uint16_t payload_idx = 0; static uint16_t crc_idx = 0; void IAP_RxHandler(uint8_t byte) { switch (rx_state) { case ST_WAIT_HEAD1: if (byte == 0xAA) rx_state = ST_WAIT_HEAD2; break; case ST_WAIT_HEAD2: rx_state = (byte == 0x55) ? ST_WAIT_CMD : ST_WAIT_HEAD1; break; case ST_WAIT_CMD: rx_frame.cmd = byte; payload_len = 0; payload_idx = 0; crc_idx = 0; rx_state = ST_WAIT_LEN; break; case ST_WAIT_LEN: if (payload_idx == 0) { payload_len = byte; /* 长度低字节先到 */ payload_idx = 1; } else { payload_len |= (uint16_t)byte << 8; /* 高字节拼出完整长度 */ payload_idx = 0; rx_state = ST_WAIT_PAYLOAD; } break; case ST_WAIT_PAYLOAD: rx_frame.payload[payload_idx++] = byte; if (payload_idx >= payload_len) { crc_idx = 0; rx_state = ST_WAIT_CRC; } break; case ST_WAIT_CRC: if (crc_idx == 0) { rx_frame.crc = byte; /* CRC 低字节 */ crc_idx = 1; } else { rx_frame.crc |= (uint16_t)byte << 8; IAP_OnFrameComplete(&rx_frame); /* 一帧收齐 */ rx_state = ST_WAIT_HEAD1; /* 回到帧头状态 */ } break; default: rx_state = ST_WAIT_HEAD1; break; } }

状态机的几个参数值得说明:payload_len 是两字节小端,所以要先收低字节再收高字节;payload_idx 在这里实际是长度收齐后的数据计数,实现时也可以单独用一个计数变量,逻辑更清楚;任何一帧中间字节出错,状态机都会回到 ST_WAIT_HEAD1,下一帧能自动恢复,不用重启设备。

帧收齐后进入分发性处理,先把 CRC 校验做掉,再按命令字分发:

void IAP_OnFrameComplete(IAP_Frame *frame) { uint16_t calc_crc = CRC16_Modbus((uint8_t *)&frame->cmd, 3 + frame->len); if (calc_crc != frame->crc) { IAP_SendAck(AK_CRC_ERR); return; } switch (frame->cmd) { case CMD_UPGRADE_START: IAP_OnStart(frame); break; case CMD_UPGRADE_DATA: IAP_OnData(frame); break; case CMD_UPGRADE_FINISH: IAP_OnFinish(frame); break; case CMD_UPGRADE_JUMP: IAP_JumpToApp(); break; default: IAP_SendAck(AK_CMD_ERR); break; } }

CRC 的覆盖范围是命令字 + 长度 + Payload,帧头不参与计算。这是有意为之,因为帧头本身有 AA 55 做同步,而 CRC 要保护的是真正影响数据的字段。源码里用的是 CRC16-MODBUS,初值 0xFFFF 多项式 0xA001,实现只有几行:

uint16_t CRC16_Modbus(uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; }

上位机计算 CRC 时必须用同样的多项式,否则联调时会出现“每一帧都回 CRC_ERR”的现象。这种问题排查起来特别像玄学,其实是两端 CRC 不一致。

3.2 Flash 擦写与存储逻辑:先整片擦除、再半字写入、最后回读校验

STM32F103 的 Flash 有一个特性:写入以半字(16 位)为单位,擦除以页为单位(每页 1KB)。写入前目标页必须处于已擦除状态,否则写入结果不确定。这直接决定了升级策略:收到第一帧命令时不能边收边写,而是先把整个 App 区擦干净。

#define APP_BASE_ADDR 0x08004000 #define FLASH_PAGE_SIZE 1024 #define APP_TOTAL_SIZE 0xC000 /* 48KB */ static uint32_t write_offset = 0; static uint8_t flash_started = 0; void IAP_OnStart(IAP_Frame *frame) { uint32_t total_len = *(uint32_t *)&frame->payload[0]; if (total_len > APP_TOTAL_SIZE) { IAP_SendAck(AK_LEN_ERR); return; } FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); /* 整片擦除App区,防止旧数据和残留页干扰 */ for (uint32_t addr = APP_BASE_ADDR; addr < APP_BASE_ADDR + APP_TOTAL_SIZE; addr += FLASH_PAGE_SIZE) { FLASH_ErasePage(addr); } write_offset = 0; flash_started = 1; IAP_SendAck(AK_OK); }

整片擦除 48KB 共 48 页,按 HSI 8MHz 或 PLL 倍频后的 CPU 时钟算,大约要几百毫秒。所以上位机发完升级开始命令后不能立刻发数据,必须等 Bootloader 回 ACK。这个等待关系在协议设计里要写清楚,否则上位机发得快,数据帧会因为 Flash 还没擦完而被忽略。

数据块写入是每帧动作,核心是把 payload 里的偏移量和数据取出来,逐个半字写入:

void IAP_OnData(IAP_Frame *frame) { uint32_t offset; uint8_t *data; uint16_t data_len; uint32_t target_addr; if (!flash_started) { IAP_SendAck(AK_NOT_STARTED); /* 没收到升级开始命令,直接拒绝 */ return; } offset = *(uint32_t *)&frame->payload[0]; /* 前4字节是目标偏移 */ data = &frame->payload[4]; data_len = frame->len - 4; target_addr = APP_BASE_ADDR + offset; /* 越界检查,防住上位机发疯 */ if ((target_addr + data_len) > (APP_BASE_ADDR + APP_TOTAL_SIZE)) { IAP_SendAck(AK_ADDR_ERR); return; } /* 半字写入,data_len 确保是偶数 */ for (uint16_t i = 0; i < data_len; i += 2) { uint16_t half_word = data[i] | (data[i + 1] << 8); FLASH_ProgramHalfWord(target_addr + i, half_word); } write_offset = offset + data_len; IAP_SendAck(AK_OK); }

这里有两个必须守住的约定:offset 必须按 2 字节对齐,data_len 必须是偶数。上位机在发送数据帧之前要把 bin 文件按 2 字节补齐,最后不足 2 字节补 0xFF。如果上位机发来奇数长度的数据,Bootloader 应该直接回错误而不是硬写,否则最后一次写入会踩到未知地址。写完后回 ACK 表示这一帧落盘,上位机再发下一帧。

升级完成的校验,比“写入成功”更可靠的是“整片回读再算 CRC”。FLASH_ProgramHalfWord 返回状态位在某些操作异常时不代表数据真写对了,回读是最后防线:

void IAP_OnFinish(IAP_Frame *frame) { uint32_t expect_crc = *(uint32_t *)&frame->payload[0]; uint32_t real_crc = CRC32_Flash(APP_BASE_ADDR, app_total_len); if (real_crc == expect_crc) { IAP_SendAck(AK_CRC_OK); } else { IAP_SendAck(AK_CRC_ERR); /* 不跳转,保留现场等上位机重传 */ } }

CRC32_Flash 是源码包里提供的函数,遍历 App 区已写入的字节计算 CRC32。比较失败时不擦除、不跳转,等上位机重新发起升级。这比直接跳进一个损坏的 App 要安全得多。

3.3 跳转到 App 的代码实现:栈顶检查、VTOR 切换与中断屏蔽

最后一步跳转,是 IAP 里最容易写翻车的地方。跳转的本质不是“跳”,而是“把 CPU 的执行环境完整换掉”:换中断向量表、换栈指针、换 PC 指针。

typedef void (*IAP_JumpFunc)(void); void IAP_JumpToApp(void) { uint32_t app_sp; uint32_t app_pc; IAP_JumpFunc jump_func; app_sp = *(volatile uint32_t *)APP_BASE_ADDR; app_pc = *(volatile uint32_t *)(APP_BASE_ADDR + 4); /* 栈顶值必须在RAM地址范围内,否则App区是空白或损坏 */ if ((app_sp & 0x2FFE0000) != 0x20000000) { return; } if ((app_pc & 0xFFFFF000) == 0) { return; } jump_func = (IAP_JumpFunc)app_pc; /* 跳转前彻底关中断,防止VTOR切换瞬间中断乱入 */ __set_PRIMASK(1); SCB->VTOR = APP_BASE_ADDR; /* 让App找到自己的向量表 */ __set_MSP(app_sp); /* 换成App的初始栈指针 */ jump_func(); /* 跳到App复位向量 */ /* 如果走到这里,说明App没起来 */ __set_PRIMASK(0); }

这个函数里有几处细节决定生死。app_sp 是 App 向量表第一个字,也就是 App 初始主堆栈指针;跳转前不换栈指针,Bootloader 的栈调用链会继续压栈,App 的 main 函数跑起来后栈空间被污染。__set_PRIMASK(1)在切换 VTOR 前关闭全部可屏蔽中断,避免串口中断在向量表切换的瞬间来一下,CPU 拿着新向量表去找旧中断入口,直接进 HardFault。跳转前的两次检查能拦掉“App 区是空白 Flash”的情况,空白 Flash 读出来是 0xFFFFFFFF,通不过 RAM 地址范围判断。

4. 编译配置与烧录避坑:让 Bootloader 和 App 各就各位

代码写完只是第一步,真正让这个工程跑起来的是两个 Keil 工程的配置。这章讲清楚怎么建两个工程、怎么生成 bin、怎么用串口助手手搓一次升级,最后给出我踩过的三个坑。

4.1 两套 Keil 工程的 IROM 配置与 bin 文件生成

源码包分了两个 Keil 工程,一个 Bootloader,一个 App。各自的关键配置用一张表说清:

配置项Bootloader 工程App 工程
IROM1 起始地址0x080000000x08004000
IROM1 大小0x40000xC000
是否设置 VTOR不需要设置偏移 0x4000
烧录方式ST-Link 写入通过 Bootloader 串口升级

App 工程里设置向量表偏移有两种方式:一种是在 main 函数里手动执行SCB->VTOR = 0x08004000;,另一种是在 system_stm32f10x.c 里宏定义VECT_TAB_OFFSET 0x4000。我推荐用后一种,因为 SystemInit 在 startup 文件里比 main 更早执行,向量表在 C 环境初始化之前就切好了,更稳。如果是手动方式,注意别放在 main 的后面,否则启动早期来的中断照样会跑飞。

生成 bin 文件是升级的前提,Keil 的 After Build 命令行里加一句:

fromelf --bin -o "$L@L.bin" "#L"

$L@L会自动展开成不带扩展名的工程输出路径,#L是链接后的映像文件。生成后把这颗 bin 交给上位机程序,就能用于串口升级。注意工程路径不要带空格,否则 fromelf 会把路径拆成多个参数,这是我实际遇到过的低级问题。

4.2 用 Python 脚本模拟上位机,手搓一遍完整升级流程

在没有现成上位机程序时,用 Python 串口脚本就能联调起来。核心是构造协议帧:

import serial import struct def crc16_modbus(data: bytes) -> int: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if crc & 0x0001: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc def make_frame(cmd: int, payload: bytes) -> bytes: body = bytes([cmd]) + struct.pack('<H', len(payload)) + payload crc = crc16_modbus(body) return b'\xaa\x55' + body + struct.pack('<H', crc)

联调时 BOOT0 保持低电平,先用 ST-Link 烧 Bootloader,然后只接 USB 转串口 TTL,打开串口后按这个顺序跑:发送升级开始帧(payload 里是固件总长度),等待 Bootloader 回 ACK;分段读取 bin 文件,每段 256 字节,前三字节是因为对齐要处理,发数据帧并逐帧确认 ACK;发完成校验帧;收到 CRC_OK 后发跳转帧。

第一次联调建议用 9600 波特率,虽然慢,但容错高。波特率越高,现场线材和干扰对帧错误率的影响越大。源码包里默认配置是 115200,如果发现 CRC 错误率高,先把波特率降下来再排查。

4.3 避坑记录:变砖、Flash 写入 HardFault、App 跑飞的三类排查经验

这一节记录的是我实际踩过的三个坑,每条都按现象、原因、解决展开。

第一个坑是“升级中途断电再上电,设备变砖”。现象是升级到一半,现场工人误拔电源,再上电后设备没有任何反应,App 起不来,Bootloader 也没响应。原因是写 Flash 只擦除了一部分页,App 区里既有新数据又有残留的旧数据,程序还尝试直接跳转。解决方法是跳转前做严格的栈顶检查和 App 有效魔数检查,魔数在 App 工程编译时固定在向量表后第 4 个字节位置,Bootloader 跳转前读这个位置,不是约定的值就绝不跳转,并且等待上位机重新升级。

第二个坑是“FLASH_ProgramHalfWord 写入时进 HardFault”。现象是写入执行到一半卡死,调试器定位到 HardFault_Handler。原因有两个:目标页没擦除干净,或者写地址不是半字对齐。我第一次写 IAP 时边收边写,收到哪页写哪页,没先整片擦除,结果在旧数据页上写入直接触发错误。解决方法是升级开始命令里整片擦除,并在 IAP_OnData 里校验 offset 是否为 2 的倍数、data_len 是否为偶数,两端约定数据补齐策略。

第三个坑是“跳转后 App 跑飞”。现象是升级流程全部返回成功,设备串口就是不工作,调试器显示停在了 HardFault_Handler。原因是 App 向量表偏移没设。跳转成功后,App 的 main 还没跑起来,一个 SysTick 或串口中断到达,CPU 从 0x08000000 读中断向量,读到的是 Bootloader 的中断处理函数地址,入口对不上,直接 HardFault。解决方式就是在 App 工程里设置 VECT_TAB_OFFSET 为 0x4000,并在跳转函数里先__set_PRIMASK(1)再切 VTOR。这类问题表现很像玄学,但根因基本都是向量表偏移。

5. 二次开发思路:给串口 IAP 加上固件加密、版本回滚和生产验证

跑通基础升级之后,要考虑的是怎么让这套东西更接近产品级。Flash 只有 64KB,很多雕花功能做不了,但有几件务实的事值得做。

5.1 固件加密与签名:从裸数据帧到加密帧

裸 bin 文件被上位机直接抓走就能逆向出固件,所以量产产品通常会做一层加密。我在二次开发时采用的思路是:上位机把 bin 用 AES-128-CBC 加密,密钥硬编码在 Bootloader 里,升级时先传加密数据,Bootloader 收到一帧后先解密、再写入 Flash。解密用软件实现,STM32F103 的主频跑 AES 会慢一点,但好在数据量不大,48KB 的固件多花几百毫秒可以接受。

要注意的是,密钥放 Flash 里只能防外行,不能防专业抄板。真要防篡改,得上签名校验:上位机对固件做 SHA256 摘要,再用私钥签个名,Bootloader 里放公钥验签。验签通过才擦写。这个项目里我把签名设计成可选的,Bootloader 编译宏打开才启用,节省了不必要的时间开销。

5.2 版本回滚、断点续传与 BKP 记录:Flash 小的务实方案

C8T6 的 64KB 放不下 A/B 双系统的大回滚方案,但可以实现小成本回滚:升级前把当前运行中的 App 关键版本号记录在备份寄存器里。STM32F103 的 BKP 寄存器在复位后不会丢失,App 启动时读 BKP 里的版本号,和当前固件版本比对,如果发现版本回退或者写失败,就主动请求重写。

断点续传比回滚更实用。因为串口升级最怕断,断了要从头传。Bootloader 里记录已经写入的偏移量到 BKP,再次上电时上位机可以直接从断点续传,省掉整包重传的时间。这个功能实现不复杂,但要改协议,在升级开始命令里带上“上次写入偏移”的查询帧。

5.3 出厂验证的习惯,比功能本身更值钱

做完这些扩展,有个动作是每次都必须做的:把整个验证脚本固化成出厂测试用例。上电不跳 App 的空白 Flash 测试、升级到一半断电再上电的断电测试、下载完成后断电重启的验收测试,这三条我每做一个 IAP 工程都会强制走一遍。源码包能帮你跑通流程,但这三次测试能帮你确定它在现场不会翻车。从那以后我每次做升级功能,都坚持把这三步写进测试清单,一次都没漏过,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进