1. 项目概述:为什么AB分区OTA在STM32F103上不是“锦上添花”,而是“生死线”
你手头有一块跑着温控算法的STM32F103C8T6最小系统板,固件已经稳定运行了八个月。某天客户突然要求加一个远程参数校准功能,你改完代码、编译通过、用J-Link烧录测试——一切OK。第二天一早,产线反馈:50台设备全部变砖,串口无响应,J-Link连不上,SWD引脚测不到时钟。你拆开其中一台,发现Flash里前4KB全是0xFF,Bootloader跳转地址被擦成了0x00000000。这不是运气差,这是单一分区OTA的必然宿命。
AB分区OTA,对STM32F103这类资源极度受限的Cortex-M3芯片而言,从来就不是什么高大上的“在线升级黑科技”,它是一套用空间换时间、用冗余保生存的硬核容错机制。它的核心逻辑非常朴素:永远保留一份已知可用的完整固件(A区),所有升级操作都在另一份空白区域(B区)进行;只有当B区写入完成、校验无误、且能成功跳转执行后,才把启动指针从A切到B。哪怕升级中途断电、通信中断、甚至Flash写入一半被拉闸,下一次上电,芯片依然会从A区冷启动,设备照常工作——用户根本感知不到后台发生过一场“生死劫”。
这背后牵扯的全是硬骨头:STM32F103的Flash擦除必须按页(1KB/页),而标准库v3.50里没有现成的AB分区管理API;它的SRAM只有20KB,放不下双份固件,更容不下一个完整的压缩解包器;它的Bootloader必须自己重写,不能依赖ST官方提供的UART Bootloader(那个只支持串口下载,不支持应用层触发的OTA流程);最要命的是,一旦Bootloader跳转逻辑出错,或者APP固件入口地址没对齐到4字节边界,就会直接卡死在HardFault_Handler,连调试信息都吐不出来。网上那些“5分钟搞定STM32 OTA”的教程,90%都省略了Flash页擦除时序、向量表偏移重映射、IAP校验失败后的回滚策略这些真正决定成败的细节。本教程不讲虚的,只带你从零开始,一行一行敲出能在真实产线环境扛住断电、通信抖动、Flash老化等恶劣条件的AB分区OTA实现。适合所有正在用STM32F103做工业传感器、智能电表、楼宇控制器的工程师,也适合被“error: flash download failed - target dll has been cancelled”折磨到怀疑人生的嵌入式新手——这个错误,90%是因为你在未关闭全局中断的情况下擦除了包含中断向量表的Flash页。
2. 整体架构设计与关键取舍:为什么放弃“优雅”,选择“粗暴可靠”
2.1 分区规划:不是数学题,是物理约束下的妥协
STM32F103C8T6的Flash总容量为64KB,但实际能用于APP的远少于这个数。我们先画一张真实的内存地图:
| 地址区间 | 大小 | 用途 | 关键约束 |
|---|---|---|---|
| 0x08000000–0x08003FFF | 16KB | Bootloader区 | 必须包含复位向量、中断向量表、IAP擦写函数、跳转逻辑;需预留至少2KB应对未来功能扩展 |
| 0x08004000–0x0800BFFF | 32KB | A区(主程序) | 存放当前运行的APP固件;起始地址必须是Flash页边界(0x08004000是第16页起始) |
| 0x0800C000–0x08013FFF | 32KB | B区(升级区) | 存放待升级的新固件;大小必须≥A区,否则无法容纳新版本 |
你可能会问:为什么A/B区各32KB?64KB Flash减去16KB Bootloader,只剩48KB,平分就是24KB。但这里有个致命陷阱:STM32F103的Flash擦除是以页(Page)为单位,每页1KB。如果你把A区设为24KB(0x08004000–0x0801BFFF),那它跨越了24个页。而OTA升级时,B区需要先整片擦除再写入。如果B区也按24KB规划,它就必须从0x0801C000开始,但0x0801C000之后只剩12KB空间(到0x08027FFF),根本不够放一个24KB的固件。更糟的是,0x0801C000不是Flash页边界(第28页起始是0x0801C000?错,第28页是0x0801C000?查RM0008手册Table 11:第28页起始地址是0x0801C000?不,是0x0801C000?翻手册确认:第0页0x08000000,第1页0x08000400…第28页是0x0800B000?不对,重新计算:每页1KB=0x400字节,第n页起始=0x08000000 + n×0x400。第16页=0x08000000+16×0x400=0x08001000?错了!0x08000000是第0页,0x08000400是第1页,所以第16页是0x08000000+16×0x400=0x08001000?但0x08001000是第16页?查手册Table 11明确写着:STM32F103xx medium-density devices have pages of 1 Kbyte, starting from address 0x08000000 (page 0), 0x08000400 (page 1), ..., up to 0x0800FC00 (page 63)。所以第16页起始是0x08000000+16×0x400=0x08001000?0x08000000+0x1000=0x08001000,对。但我们的Bootloader要占16KB,即16页(0x0000–0x3FFF),所以Bootloader结束于0x08003FFF,下一页(第16页)起始是0x08004000。这才是A区的起点。因此,A区从0x08004000开始,占32页(32KB),到0x0800BFFF结束。B区紧接着,从0x0800C000(第28页起始)开始,占32页,到0x08013FFF结束。这样,A/B区都是整页对齐,擦除时只需调用FLASH_ErasePage(0x08004000)和FLASH_ErasePage(0x0800C000),不会跨页误擦。这个规划不是拍脑袋,是手册白纸黑字的物理约束倒逼出来的唯一解。
提示:很多初学者栽在分区地址不对齐上。例如把A区设为0x08004100,结果擦除时
FLASH_ErasePage(0x08004100)会自动向下取整到0x08004000,把Bootloader末尾1KB也擦掉了。务必用地址&0xFFFFF000(对齐到4KB)或查手册确认页边界。
2.2 Bootloader与APP的职责切割:谁该干脏活,谁该享清福
一个健壮的AB分区系统,本质是两个独立程序的精密协作。它们之间必须有清晰的“楚河汉界”,否则就是灾难的开始。
Bootloader的铁律:
- 绝不主动修改自身代码:Bootloader的Flash区域(0x08000000–0x08003FFF)在运行时必须是只读的。任何升级操作,只能擦写A区或B区,Bootloader自身代码和数据必须固化。
- 只做三件事:a) 上电后检查B区有效性(CRC32校验+魔数验证);b) 若有效,则跳转至B区;c) 若无效,则跳转至A区。其余所有事(接收升级包、解析、写Flash、校验)均由APP发起并控制。
- 向量表重映射是生命线:当APP在B区运行时,其中断向量表在0x0800C000,但CM3内核默认从0x08000000取向量。必须在APP启动时执行
SCB->VTOR = FLASH_BASE | 0x0000C000;,将向量表基址重映射到B区首地址。否则,任何中断(如SysTick、USART)都会跳转到Bootloader的中断服务程序,导致不可预测行为。
APP的契约义务:
- 提供标准化的IAP接口:在APP的
main()之前,必须定义一个全局函数指针数组,暴露IAP_WriteFlash,IAP_ReadFlash,IAP_GetAppStatus等函数。Bootloader通过这个接口与APP通信,而不是直接调用APP内部函数。 - 承担全部升级逻辑:APP负责通过UART/USB/以太网接收升级包(通常为bin文件),将其解包、校验、写入B区指定地址,并在写入完成后设置一个标志位(如在B区末尾写入特定魔数0xDEADBEEF)。
- 安全退出机制:APP在完成B区写入并校验无误后,不能直接跳转。必须先设置一个“待重启标志”(例如在备份SRAM或Flash特定位置写入0xAA55),然后调用
NVIC_SystemReset()软复位。复位后,Bootloader读取该标志,确认应从B区启动。
- 提供标准化的IAP接口:在APP的
这种设计看似繁琐,实则是为了隔离风险。如果Bootloader自己去实现UART接收和Flash写入,一旦UART驱动有bug导致死循环,整个系统就彻底锁死,连J-Link都无法连接。而让APP来干,即使APP升级逻辑崩溃,Bootloader依然能保证从A区启动,设备可恢复。
2.3 为什么不用HAL库,坚持用标准库v3.50
网上大量教程推荐用STM32CubeMX生成HAL库工程来做OTA。我试过,也踩过坑。HAL库的HAL_FLASHEx_Erase()函数封装了页擦除逻辑,看起来很美。但问题在于:HAL库的Flash驱动默认启用了FLASH_FLAG_EOP(操作完成)和FLASH_FLAG_WRPRTERR(写保护错误)中断。在OTA升级过程中,如果恰好有SysTick中断或其他外设中断抢占了Flash擦除操作,而你的中断服务程序里又调用了HAL_Delay()(它依赖SysTick),就会陷入死锁——因为SysTick被挂起,HAL_Delay()永远等不到超时。标准库v3.50的FLASH_ErasePage()是纯轮询实现,不依赖任何中断,只要在擦除前关闭全局中断(__disable_irq()),擦除后开启(__enable_irq()),就能100%避免此类竞态。虽然代码多写几行,但换来的是在产线7×24小时运行的绝对可靠。对于资源紧张的F103,少一个中断服务程序,就少几百字节RAM占用,这笔账,必须算清楚。
3. 核心细节解析与实操要点:从向量表重映射到CRC32校验的每一处陷阱
3.1 向量表重映射:不是配个寄存器就完事
向量表重映射(Vector Table Relocation)是AB分区能跑起来的第一道门槛。很多教程只告诉你一句SCB->VTOR = 0x0800C000;,然后就没了。但实际中,这句话放在哪里,决定了你是成功还是HardFault。
错误做法:在APP的
main()函数第一行就写SCB->VTOR = 0x0800C000;。后果:此时C库的初始化(如.data段复制、.bss段清零)尚未完成,SCB结构体可能还未被正确映射,访问SCB->VTOR会触发UsageFault。正确做法:在APP的启动文件(startup_stm32f10x_md.s)中,在
Reset_Handler标签之后、跳转到main之前,插入重映射代码。具体步骤:- 打开
startup_stm32f10x_md.s,找到Reset_Handler标号。 - 在
LDR R0, =SystemInit指令之后、LDR R0, =main指令之前,插入:LDR R0, =0x0800C000 ; B区向量表起始地址 MOV R1, #0x20000000 ; SCB->VTOR寄存器地址 (0xE000ED08) STR R0, [R1, #8] ; 将R0值写入VTOR (偏移8字节) - 确保
SystemInit()函数中没有调用任何会修改SCB->VTOR的代码(标准库v3.50的SystemInit不会动它)。
- 打开
为什么必须在这里做?因为Reset_Handler是CPU复位后执行的第一段代码,此时所有硬件寄存器都处于初始状态,SCB基地址0xE000ED00是确定的,VTOR寄存器(偏移0x08)可以安全访问。等C库初始化完成,栈指针、全局变量都就绪了,再跳进main,就不会有地址访问异常。
注意:
SCB->VTOR的值不是直接写入地址,而是写入FLASH_BASE | offset。offset必须是256字节的整数倍(即低8位为0)。B区向量表必须从0x0800C000开始,因为0x0800C000 & 0xFFFFFF00 = 0x0800C000,满足对齐要求。如果B区从0x0800C100开始,VTOR写入0x0800C100会导致内核从错误地址取向量,必死无疑。
3.2 Flash页擦除的时序与保护:别让“擦得干净”变成“擦得绝望”
STM32F103的Flash擦除不是瞬间完成的,它需要时间。根据手册,擦除一页(1KB)典型时间为20ms,最大可达40ms。如果你在擦除后立刻尝试写入,或者在擦除过程中响应了中断,就会触发FLASH_SR_BSY(忙)标志,导致后续操作失败。
实操要点:
- 擦除前,必须关闭所有可能触发Flash操作的中断:不仅仅是SysTick,还包括任何使用
HAL_Delay()、osDelay()(如果你用FreeRTOS)的定时器。最稳妥的做法是擦除全程关闭全局中断:__disable_irq(); FLASH_ErasePage(0x0800C000); __enable_irq();。 - 擦除后,必须轮询等待完成:标准库
FLASH_ErasePage()函数内部已经包含了轮询FLASH_SR_BSY的逻辑,但它返回的是FLASH_Status。你必须检查返回值:FLASH_Status status = FLASH_ErasePage(0x0800C000); if(status != FLASH_COMPLETE) { // 擦除失败!可能是写保护未解除,或电压不稳 while(1); // 进入死循环,便于调试 } - 写保护解除是前提:F103的Flash默认是写保护的。在擦除或写入前,必须调用
FLASH_Unlock();操作完成后,必须调用FLASH_Lock()上锁。FLASH_Unlock()需要向KEYR寄存器写入两个特定密钥(0x45670123, 0xCDEF89AB),顺序不能错,否则锁死。我见过太多人因为复制粘贴时漏掉一个0,导致Flash再也无法写入,只能用J-Link的“Unlock Flash”功能救急。
- 擦除前,必须关闭所有可能触发Flash操作的中断:不仅仅是SysTick,还包括任何使用
一个血泪教训:某次调试,我把
FLASH_Unlock()放在了main()里,然后在while(1)循环里反复擦写B区做测试。结果第一次擦写成功,第二次就报FLASH_ERROR_WRPRT。原因?FLASH_Lock()只锁了一次,但FLASH_Unlock()需要每次操作前都调用。正确的模式是:FLASH_Unlock(); FLASH_ErasePage(0x0800C000); // ... 写入数据 ... FLASH_Lock(); // 每次操作后立即上锁
3.3 CRC32校验:为什么不用简单的累加和
OTA升级的核心信任链,始于对B区固件完整性的校验。很多人图省事,用一个16位累加和(sum += data[i])来校验。这在实验室环境下可能没问题,但在产线,一个电源纹波、一次EMI干扰,就足以让累加和“碰巧”通过。CRC32是工业级标准,它能检测出所有单比特错误、所有双比特错误、所有奇数个比特错误,以及长度≤32比特的突发错误。
实现要点:
- 校验范围必须精确:不是校验整个B区(0x0800C000–0x08013FFF),而是校验B区中APP固件的实际长度。假设你的APP.bin文件大小为28672字节(0x7000),那么校验范围就是0x0800C000–0x08012FFF(0x0800C000 + 0x7000 - 1)。多校验一个字节,CRC值就不同。
- 校验值存储位置:将计算出的CRC32值(4字节)写入B区的固定偏移,例如B区末尾的4个字节(0x08013FFC–0x08013FFF)。Bootloader启动时,先读取这4个字节作为期望值,再对B区固件主体(0x0800C000–0x08012FFF)重新计算CRC32,两者比对。
- 字节序陷阱:STM32是小端机(Little-Endian)。CRC32计算函数返回的uint32_t值,其最低字节(LSB)存储在最低地址。因此,当你把
crc_value写入Flash时,应该用:
如果你直接用uint8_t crc_bytes[4]; crc_bytes[0] = (crc_value >> 0) & 0xFF; // LSB crc_bytes[1] = (crc_value >> 8) & 0xFF; crc_bytes[2] = (crc_value >> 16) & 0xFF; crc_bytes[3] = (crc_value >> 24) & 0xFF; // MSB // 然后依次写入0x08013FFC, 0x08013FFD, 0x08013FFE, 0x08013FFF*(uint32_t*)0x08013FFC = crc_value;,在小端机上,这等价于上面的操作,是安全的。但显式拆解字节,逻辑更清晰,也方便移植到大端平台。
性能考量:对32KB数据做CRC32,用软件查表法(256项表)约需15ms(72MHz主频)。这在OTA流程中是可以接受的。不要为了省几毫秒而用不安全的校验方式。
4. 实操过程与核心环节实现:从Keil工程配置到J-Link烧录的全流程
4.1 Keil MDK工程配置:三个关键分散加载文件(Scatter File)
Keil的分散加载文件(.sct)是控制代码和数据在Flash/RAM中布局的灵魂。一个AB分区工程,必须有三个不同的.sct文件,分别对应Bootloader、A区APP、B区APP。
Bootloader.sct:
LR_IROM1 0x08000000 0x00004000 { ; load region size_region ER_IROM1 0x08000000 0x00004000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; 20KB SRAM .ANY (+RW +ZI) } }这个文件告诉链接器:Bootloader代码从0x08000000开始,最大占16KB(0x4000),RAM从0x20000000开始。
APP_A.sct(用于编译A区固件):
LR_IROM1 0x08004000 0x00008000 { ; A区:32KB ER_IROM1 0x08004000 0x00008000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (+RW +ZI) } }注意
ER_IROM1的起始地址是0x08004000,大小0x8000(32KB)。APP_B.sct(用于编译B区固件):
LR_IROM1 0x0800C000 0x00008000 { ; B区:32KB ER_IROM1 0x0800C000 0x00008000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (+RW +ZI) } }起始地址变为0x0800C000。
配置步骤:
- 在Keil中,右键点击工程 → “Options for Target...” → “Linker”选项卡。
- 勾选“Use Memory Layout from Target Dialog”(取消勾选),然后在“Scatter File”框中,输入对应.sct文件的路径,例如
.\scatter\APP_B.sct。 - 切换到“C/C++”选项卡,在“Define”框中添加宏定义,例如
APP_IN_B_REGION。这个宏会在APP代码中用于条件编译,比如:#ifdef APP_IN_B_REGION SCB->VTOR = 0x0800C000; // B区向量表 #else SCB->VTOR = 0x08004000; // A区向量表 #endif
提示:编译B区APP时,必须确保其
main()函数入口地址(即Reset_Handler地址)被正确放置在0x0800C000。Keil的链接器会自动将*.o (RESET, +First)放在输出段的最前面,所以只要.sct文件配置正确,就万无一失。
4.2 J-Link烧录:如何避免“error: flash download failed - target dll has been cancelled”
这个错误是STM32开发者最熟悉的“噩梦”。它出现的原因五花八门,但AB分区场景下,90%与以下三点有关:
Flash区域重叠或越界:你在Keil中配置的.sct文件起始地址是0x0800C000,但J-Link Commander或Keil的Flash Download设置里,目标地址却填成了0x08000000。J-Link试图往Bootloader区域写入APP代码,自然失败。解决方法:在Keil中,“Options for Target...” → “Utilities” → “Settings” → “Flash Download” → 点击“Add”添加你自己的Flash算法(如STM32F10x_128.FLM),然后在“Edit”中,确认“Start Address”与.sct文件中的
ER_IROM1起始地址完全一致。Flash算法版本过旧:你用的是老版本J-Link驱动(V6.x),而新版本(V7.x)修复了F103在高主频下擦除失败的Bug。解决方法:去SEGGER官网下载最新版J-Link Software and Documentation Pack,安装后重启Keil。同时,在Keil的“Utilities”设置里,点击“Flash Download”旁边的“Settings”,在“Flash”选项卡中,勾选“Verify after programming”,这能帮你第一时间发现写入错误。
硬件连接不稳定:SWDIO/SWCLK线过长(>15cm)、未加100Ω串联电阻、或目标板供电不足(<3.0V),都会导致J-Link通信超时,最终报此错。解决方法:用万用表测量VDD引脚电压,确保在3.3V±5%;在SWDIO和SWCLK线上各串一个100Ω电阻;缩短排线长度。
烧录顺序(至关重要):
- 首先,用J-Link烧录
Bootloader.hex(由Bootloader.sct生成)到0x08000000。 - 然后,烧录
APP_A.hex(由APP_A.sct生成)到0x08004000。 - 最后,烧录
APP_B.hex(由APP_B.sct生成)到0x0800C000。注意:
APP_B.hex是空的占位文件,里面只有填充的0xFF。它只是为B区预留出32KB空间,防止后续OTA升级时写入失败。真正的B区内容,由APP在运行时通过IAP写入。
4.3 OTA升级协议设计:一个极简但可靠的自定义协议
OTA不是把一个bin文件扔过去就完事。你需要一个轻量级协议,来协调“发端”(上位机/云平台)和“收端”(STM32 APP)之间的握手、分包、校验、确认。
我们采用一个3帧协议,总开销仅12字节,专为低速UART(115200bps)优化:
| 字段 | 长度 | 说明 |
|---|---|---|
| SOF | 1字节 | 起始符,固定为0xAA |
| CMD | 1字节 | 命令码:0x01=请求升级,0x02=发送数据块,0x03=升级完成 |
| LEN | 2字节 | 数据块长度(小端),最大64KB |
| DATA | LEN字节 | 实际bin数据 |
| CRC | 2字节 | 整个帧(SOF到DATA)的CRC16-CCITT |
| EOF | 1字节 | 结束符,固定为0x55 |
APP端处理逻辑:
// 伪代码 while(receive_frame(&frame)) { switch(frame.cmd) { case 0x01: // 请求升级 erase_B_region(); // 擦除B区 send_ack(0x01, SUCCESS); break; case 0x02: // 发送数据块 write_to_B_region(frame.data, frame.len, current_offset); current_offset += frame.len; send_ack(0x02, SUCCESS); break; case 0x03: // 升级完成 calculate_crc32_on_B_region(); write_crc32_to_B_region_end(crc32); set_reboot_flag(); NVIC_SystemReset(); break; } }这个协议没有复杂的滑动窗口、没有重传机制,因为它假设上位机是可信的(如本地PC工具)。如果网络环境差,应在上位机层面实现TCP重传或MQTT QoS1。把复杂性留在上位机,让MCU保持简单,是嵌入式开发的黄金法则。
5. 常见问题与排查技巧实录:那些让你凌晨三点还在抓头发的Bug
5.1 问题速查表:症状、原因、解决方案
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 上电后LED不亮,J-Link能连上但无法擦除Flash | Bootloader的Flash区域被意外擦除或写坏 | 用J-Link Commander执行unlock命令,然后重新烧录Bootloader.hex |
| APP能正常运行,但OTA升级后,复位进入Bootloader,却跳转到A区而非B区 | B区末尾的CRC32校验值未写入,或写入地址错误(如写到了0x08013FF0而非0x08013FFC) | 用J-Link Commander的mem32 0x08013FFC 1命令读取最后4字节,确认是否为预期CRC值;检查APP中写CRC的代码,确保地址计算正确 |
| B区写入完成后,APP跳转到B区,但立即进入HardFault_Handler | B区的向量表重映射未生效,或B区首地址(0x0800C000)处的数据不是有效的向量表(即前4字节不是栈顶地址) | 用mem32 0x0800C000 2命令读取B区开头8字节,确认第一个字(栈顶地址)在0x20000000–0x20004FFF范围内(F103的SRAM范围);检查APP的.sct文件,确保*.o (RESET, +First)确实放在了0x0800C000 |
| 串口接收升级包时,偶尔丢包或数据错乱 | UART中断优先级设置过低,被其他高优先级中断(如TIM)抢占,导致接收缓冲区溢出 | 在NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)后,将USARTx_IRQn的优先级设为最高(如NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;) |
error: flash download failed - "cortex-m4 | 你正在用针对Cortex-M4的J-Link驱动(如STM32F4xx.FLM)去烧录Cortex-M3的F103 | 在Keil的“Flash Download”设置中,删除所有M4算法,只添加STM32F10x_128.FLM |
5.2 独家避坑技巧:来自产线的真实经验
技巧1:用“影子Flash”做升级预演
在正式OTA前,先在RAM中模拟一次完整的B区写入和CRC校验。方法:分配一块32KB的RAM(如uint8_t b_region_shadow[32*1024];),把接收到的bin数据先memcpy进去,然后对这块RAM计算CRC32。如果RAM校验通过,再执行真实的Flash写入。这能提前捕获bin文件损坏、传输错误等问题,避免把错误固件写进Flash。技巧2:给Bootloader加一个“强制回滚”按键
在Bootloader中,增加一个GPIO检测逻辑:如果上电时某个按键(如BOOT0)被按下,则无视B区状态,强制从A区启动。这在B区固件有严重Bug,导致设备无法联网、无法触发OTA回滚时,是唯一的救命稻草。代码只需几行:RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) { jump_to_app(0x08004000); // 强制跳A区 }**技巧3:监控Flash