尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

GD32 IAP Bootloader串口Ymodem固件升级方案详解

发布时间:2026/9/27 1:01:38

资讯中心
01
ARTICLE

GD32 IAP Bootloader串口Ymodem固件升级方案详解

GD32 IAP Bootloader串口Ymodem固件升级方案详解
手头有个项目要在GD32上做远程固件升级需求一句话就能说清楚产品已经装在现场不能拆壳、不能连仿真器只能靠一个串口把新固件灌进去。我最后选的方案是“GD32 IAP bootloader 串口 Ymodem协议”这段时间把整个流程从头到尾跑通了也踩了不少坑。这篇文章把我自己的实现过程、代码结构、协议细节和调试心得都整理出来给做GD32 bootloader开发的同行一个可以直接参考的范本。IAPIn-Application Programming不稀奇但真正落地时涉及的问题很多Flash分区怎么划、bootloader和APP各自怎么改链接脚本、Ymodem协议的状态机怎么写、跳转APP时中断向量表怎么处理、烧写过程中哪些中断要关、哪些坑会导致程序直接HardFault……这些全是实践经验堆出来的东西。下面按我实际开发的顺序把整个工程拆开讲。1. 为什么要做IAP从一次现场拆壳说起1.1 现场升级的痛点拆壳、仿真器与物理接触我最早做单片机固件升级用的都是最原始的方式拿着J-Link或者ST-Link到现场打开设备外壳找到板子上的SWD接口然后连上电脑烧录。听起来没毛病但真正到了现场就发现处处难受。工业设备往往安装在机柜深处、户外箱体里甚至高空支架上拆壳本身可能就是个大工程。而且有些设备出厂时会打胶密封一拆就破坏防护等级后续防水防尘全没了。更麻烦的是现场工程师未必有仿真器也不一定用得明白Keil或调试软件。哪怕我远程把烧录文件发过去对方也可能因为接触不良、接线顺序问题折腾半天。所以我当时定了一个硬性需求升级方式必须简单到现场工程师只会用一根USB转串口线和电脑就能完成不需要拆壳不需要J-Link最好在设备上留一个外接调试串口即可。这就是IAP方案最典型的应用场景——通过MCU自身运行的bootloader程序接收并写入新固件。固件升级从“物理接触”变成了“逻辑操作”设备本身变成了烧录器。1.2 ICP、ISP与IAP三个概念别再搞混在做GD32升级方案前一定要先分清三个概念ICP、ISP和IAP。我见过不少同事把这三个词混着用结果方案设计从一开始就歪了。ICPIn-Circuit Programming就是通过仿真器比如J-Link、ST-Link、CMSIS-DAP直接连接SWD/JTAG口操作Flash。开发调试阶段最常用但需要物理连接仿真器不适合现场升级。ISPIn-System Programming一般指利用芯片出厂时固化的系统bootloader通过串口、USB、CAN等通信接口下载程序。GD32出厂时也有一段系统bootloader可以用官方工具通过串口烧录。但它的问题是这段固化程序是芯片出厂时的固定版本更新流程不可定制比如无法做固件加密、版本校验、A/B备份等逻辑而且每次升级都要重启进系统bootloader流程死板。IAPIn-Application Programming则是用户自己在Flash里写一段bootloader程序上电先跑bootloader根据需要接收数据并写入APP区写完再跳转启动APP。整个流程完全由自己控制灵活性最高。表格对比如下方式物理接口是否需要仿真器升级流程可控性适用场景ICPSWD/JTAG需要不可控直接用工具擦写开发调试、产线烧录ISP串口/USB等不需要使用芯片出厂固化bootloader流程固定出厂烧录、简单现场升级IAP串口/USB/网口/无线等不需要完全自主可控现场升级、OTA升级、批量维护我需要的是“完全可控”所以IAP是唯一选择。GD32内部Flash可以自写自擦这是IAP能成立的前提硬件上不需要做任何额外改动。1.3 方案选型为什么锁定串口Ymodem确定IAP大方向后传输通道和协议也需要定下来。传输通道可选串口、USB、以太网、CAN、无线等。产品本身没有网络和USB需求使用串口是最简单且最通用的选择。另一个重要原因是串口对外接线简单很多设备本身就预留了调试串口硬件成本几乎为零。USB转TTL模块遍地都是现场工程师手里基本都有。协议上我在Xmodem和Ymodem之间犹豫了一下。Xmodem是最经典的串口文件传输协议按128字节或1024字节分块传输带CRC校验简单可靠。但Xmodem的块里只有纯数据没有文件名、文件大小这些元信息。接收端无法事先知道固件到底多大只能盲收直到发完这在实际工程里很被动。Ymodem在Xmodem基础上增加了块0的握手包发送端会先发一个包含文件名和文件大小的128字节帧接收端解析后就知道整个固件长度能提前判断固件是否过大、空间是否不足。而且Ymodem本身的差错控制机制很完善收发双方通过ACK、NAK、CAN、EOT等控制字符确认每一帧的状态发现CRC错误或帧序号异常就要求重发。这对串口这种容易受干扰的物理链路来说非常重要。选Ymodem还有一层原因大多数串口调试工具和终端软件都原生支持比如SecureCRT、XCOM等现场工程师上手几乎没有学习成本。2. 内存与启动流程bootloader的地基2.1 Flash分区给bootloader和APP各划一块地很多第一次写IAP的人最容易犯的错误就是不理解为什么APP不能从0x08000000直接运行。这要从Cortex-M3的启动机制说起但做工程前重要的问题是先规划Flash分区。我的示例以GD32F103CBT6为例它有128KB的Flash。如果使用更常见的GD32F103C8T664KB Flash只需把分区按比例缩小。分区原则是bootloader区必须足够跑完接收、校验、擦写、跳转这套升级逻辑同时APP区要尽量大。我自己的推荐分区如下区域起始地址大小说明Bootloader0x0800000032KB中断向量表、串口驱动、Ymodem协议、Flash驱动、跳转函数APP0x0800800096KB应用程序区参数区可选0x0801F8002KB存放升级标志、版本号、CRC校验值等为什么Bootloader要32KB这么大如果只是“串口YmodemFlash擦写”实际上16KB甚至8KB都够用。但我要在bootloader里做一些升级标志判断、版本回显、APP有效性校验而且调试过程可能再加日志输出多预留点空间不至于后期挤到优化代码。APP区起始地址0x08008000注意这个地址必须向中断向量表对齐。GD32F103是Cortex-M3内核其向量表要求按0x100或者更大的边界对齐无论怎样起始地址至少要保持128字节对齐32KB对齐自然没问题。对于C8T6这种64KB Flash的芯片建议将Bootloader压缩到16KB0x08000000~0x08003FFFAPP从0x08004000开始APP区48KB。如果产品功能不多48KB完全足够。2.2 上电后CPU做了什么SP、PC、VTORCortex-M3的启动流程对IAP跳转理解极其关键。芯片上电复位后CPU并不是直接执行main函数而是先做两件事从Flash地址0x08000000读取初始栈顶指针MSP的值从Flash地址0x08000004读取复位向量Reset_Handler的地址然后跳转执行。这两项数据就存放在固件的最前面也就是中断向量表的头两个位置。工程编译出的bin文件前8个字节固定是这两项。所以bootloader本质上是贴着地址0x08000000摆放的一段独立固件它自己也有一套完整的中断向量表有自己的启动文件startup_gd32f10x_hd.s需要编译成独立工程。APP则不同。APP烧录地址被偏移到0x08008000但CPU本来不知道这个事。bootloader升级完APP后如果想要跳转执行APP必须手动完成“读APP向量表的SP和Reset地址然后设置新的栈顶指针和PC指针”这个过程。同时还有一个很关键的动作要把中断向量表偏移量寄存器VTOR指到APP的起始地址否则APP跑起来后一旦发生串口中断、定时器中断CPU会去按0x08000000处的中断向量表找中断服务函数也就是说会跑到bootloader的向量表里大概率直接死机或者误入莫名其妙的函数。GD32F103的VTOR寄存器位于SCB基地址偏移0x30处可以直接用SCB-VTOR APP_BASE_ADDR来设置。部分GD32官方固件库提供nvic_vector_table_set这类封装函数本质也一样只是入口参数不同。我在实际项目中明确不依赖封装直接操作寄存器更清楚。2.3 GD32 Flash控制器擦写到底要过几道关GD32的Flash控制器叫FMC不同型号页大小有差异。GD32F103系列的Flash页大小是1KB也就是说擦除的最小单位是1KB。这个尺寸和Ymodem协议的大块1024字节刚好对应一块擦一页设计起来很顺手。GD32F30x等系列可能是2KB页需要按2KB处理分区。写Flash的正确流程是固定的解锁Flashfmc_unlock清除错误标志fmc_flag_clear擦除目标页fmc_erase_page等待Busy位清零按32位字写入数据fmc_word_program写完再次等待Busy位清零上锁Flashfmc_lock。有一个很重要的问题Flash擦除和编程操作期间如果CPU还从Flash里取指执行会怎样实际上控制器内部有处理但如果你在擦写Flash期间触发了中断而中断服务函数也在Flash里那中断入口的取指就会和Flash操作冲突轻则等待总线导致中断响应延迟严重时直接导致Flash操作失败或者程序跑飞。所以我在bootloader的擦写函数里做了这个处理进入擦写前关闭全局中断擦写完成后再打开。实际调试证明这能明显降低偶发死机概率。另外每次操作前必须调用fmc_flag_clear清楚上次可能残留的错误标志尤其是写保护错误和编程错误。如果不清理下次操作可能直接失败。很多人升级偶尔失败重启一下又能用就是这个标志位没有清干净。3. Ymodem协议串口升级的核心基础3.1 帧格式SOH/STX、块号、CRCYmodem协议本质上是在Xmodem协议基础上扩展出来的把传输过程分成两阶段。第一阶段用于传输文件信息块0第二阶段用于传输文件正文。所有数据都按“帧”组织帧有两种大小128字节帧和1024字节帧。帧结构如下字段字节数取值/说明帧头1SOH0x01表示128字节帧STX0x02表示1024字节帧块号1从0x00开始递增到0xFF后回绕慢速传递块号反码10xFF减去块号用于校验块号是否正确数据128或1024实际有效数据最后一块用0x1A或0x00填充CRC高字节1CRC16校验值的高字节CRC低字节1CRC16校验值的低字节块0是128字节帧数据内容格式一般为文件名字符串以0x00结尾文件大小十进制ASCII字符串以0x00结尾时间戳等信息以0x00结尾。但不同Ymodem发送工具对块0的处理并不完全一致有些工具只在文件名后补一个0x00不发送文件大小。所以bootloader里解析块0时要按“能取到文件大小就取取不到就盲收”的方式处理不能因为块0格式不完美就直接报错退出。收到块0后接收端一般直接ACK不需要对文件名做过多判断。3.2 一次完整的Ymodem传输流程完整流程可以分成握手、文件信息、文件数据、结束四步。第一步接收端bootloader先发送字符C0x43表示“我准备好了用CRC校验方式收发请发送文件”。第二步发送端收到C后发送块0128字节里面带着文件名和大小。接收端校验CRC无误后回复ACK。第三步接收端再次发送C开始正文传输。发送端按1024字节分块发送数据帧。接收端每收到一帧校验帧头、块号、块号反码、CRC全部正确就ACK有问题就回复NAK发送端会重发当前帧。这个ACK机制是Ymodem可靠性的核心串口丢字节不怕重传能补回来。第四步正文发完后发送端发EOT0x04接收端回复ACK。然后接收端再发C发送端有两种可能的收尾方式一是再发一个EOT二是直接发送一个空块块号0数据全0。我的状态机两种都处理既兼容SecureCRT也兼容其他工具。我用表格把整个交互过程整理如下步骤接收端Bootloader发送端PC端软件1发送C等待2等待块0发送文件信息块03校验后发送ACK收到ACK4发送C开始发送数据帧5每帧校验并ACK发送下一帧6等待EOT发送EOT7发送ACK再发送C发送EOT或空包8结束等待结束要特别注意接收端在整个传输过程中要严格控制状态不能把下一个文件的数据误认为当前文件的数据。如果实现的是多文件连续传输空包之后还要支持继续发C等待下一个文件我只做单固件升级所以收到空包就直接判定传输结束。3.3 CRC16计算别在这里翻车Ymodem的CRC算法是标准的CRC-16/CCITT生成多项式是0x1021初始值0x0000。它的计算方式和普通CRC32不太一样是逐字节、逐位处理的具体说就是每个字节先左移8位进入CRC寄存器然后按位移位异或。协议里的CRC是16位值发送时高字节在前低字节在后。很多初次写Ymodem的人代码逻辑没问题但发送接收双方CRC字节序搞反结果每一帧都报CRC错误然后就怀疑通信有问题。这里先记死一条CRC字节序是高字节在前。下面是适合放在bootloader里的标准CRC16实现uint16_t ymodem_crc16(const uint8_t *data, uint32_t length) { uint16_t crc 0x0000; uint8_t i; while (length--) { crc ^ (uint16_t)(*data) 8; for (i 0; i 8; i) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }这个函数对128字节和1024字节帧都适用直接传入帧数据区和数据长度即可。CRC校验范围是从块号到数据区结束不包含SOH/STX帧头也不包含CRC本身。4. Bootloader代码实现从串口到Flash4.1 串口接收中断环形缓冲区Ymodem传输的数据量按100KB固件算拆成大约100个1024字节块。如果使用轮询方式在接收循环里while(USART_GetFlagStatus(...) RESET)等待每个字节理论上也能工作但效率很低而且如果擦写Flash耗时较长容易丢字节。所以我采用中断接收环形缓冲区方案。串口每收到一个字节就进中断把数据压进环形缓冲区主循环的Ymodem状态机从缓冲区里取数据解析。这样发送端连续快速发数据时MCU即使在处理其他逻辑也能通过中断把字节存住不会丢。初始化串口的代码如下我以USART0为例PA9是TXPA10是RX波特率1152008位数据位、1位停止位、无校验#include gd32f10x.h #define UART_RING_SIZE 2048 static volatile uint8_t uart_ring[UART_RING_SIZE]; static volatile uint16_t uart_head 0; static volatile uint16_t uart_tail 0; static void uart_ring_push(uint8_t byte) { uint16_t next (uart_head 1) % UART_RING_SIZE; if (next ! uart_tail) { uart_ring[uart_head] byte; uart_head next; } } static int uart_ring_pop(uint8_t *out) { if (uart_head uart_tail) return 0; *out uart_ring[uart_tail]; uart_tail (uart_tail 1) % UART_RING_SIZE; return 1; } void uart_init(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_USART0); gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9); gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_10); usart_deinit(USART0); usart_baudrate_set(USART0, 115200U); usart_word_length_set(USART0, USART_WL_8BIT); usart_stop_bit_set(USART0, USART_STB_1BIT); usart_parity_config(USART0, USART_PM_NONE); usart_receive_config(USART0, USART_RECEIVE_ENABLE); usart_transmit_config(USART0, USART_TRANSMIT_ENABLE); usart_interrupt_enable(USART0, USART_INT_RBNE); nvic_irq_enable(USART0_IRQn, 2, 0); usart_enable(USART0); } void USART0_IRQHandler(void) { if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE) ! RESET) { uart_ring_push((uint8_t)usart_data_receive(USART0)); } }注意GD32各系列固件库的中断标志宏命名可能略有差异比如有的库用USART_INT_FLAG_RBNE有的用USART_FLAG_RBNE。写的时候以你当前工程使用的固件库头文件为准但如果你的IDE已经自动生成中断服务框架多半不会出错。环形缓冲区大小我设置为2048字节足够缓冲两帧1024字节数据实际测试中即使Flash擦写过程中来数据也不会溢出。4.2 Flash擦写封装Flash擦写封装是bootloader里最容易踩坑的模块。我提供一个抽象层把解锁、擦除、编程、上锁封装成两个函数一个是整页擦除一个是按任意长度写入。这里为方便理解我假设数据写入按4字节对齐写之前调用擦除函数。#define APP_BASE_ADDR 0x08008000UL #define APP_MAX_SIZE 0x00018000UL // 96KB #define FLASH_PAGE_SIZE 1024UL void flash_erase_page(uint32_t page_addr) { fmc_unlock(); fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR); fmc_erase_page(page_addr); while (fmc_flag_get(FMC_FLAG_BUSY) ! RESET) { } fmc_lock(); } void flash_write_words(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i 0; uint32_t word; fmc_unlock(); while (i 4 len) { word (uint32_t)buf[i] | ((uint32_t)buf[i 1] 8) | ((uint32_t)buf[i 2] 16) | ((uint32_t)buf[i 3] 24); fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR); fmc_word_program(addr i, word); while (fmc_flag_get(FMC_FLAG_BUSY) ! RESET) { } i 4; } fmc_lock(); }使用这个封装时要注意几个点擦除地址必须按页对齐。如果是1KB页地址必须能被1024整除。写入地址要落在已擦除的页范围内。Ymodem的1024字节块正好和1KB页对应所以我在主流程中收到1024字节数据后先擦除目标地址所在页再写入整块数据。写Flash期间我虽然已经关闭了全局中断但fmc_word_program执行期间程序仍从Flash取指这在Cortex-M3上是允许的。不过如果你执行更高级的Flash操作想从RAM里执行代码那是另一套玩法IAP用不到。如果遇到GD32库函数名称或FMC标志宏与实际不完全一致只要按同样的流程调整函数名即可。核心操作顺序不能乱解锁、清标志、擦除、等待、编程、等待、上锁。4.3 Ymodem状态机主循环Ymodem接收状态的实现是整个bootloader最核心的代码。我写了一个比较精简但功能完整的版本主要分为几个函数从环形缓冲区读取一个字节带超时接收完整一帧解析帧头和校验主循环里根据状态推进。我先写一个带超时的读字节函数static int uart_read_byte_timeout(uint8_t *byte, uint32_t timeout_ms) { uint32_t start systick_get(); while (systick_get() - start timeout_ms) { if (uart_ring_pop(byte)) return 1; } return 0; }这里使用了systick提供毫秒计数在bootloader早期初始化时必须开启SysTick定时器。如果没有现成的systick接口用延迟函数配合状态轮询方式也可以只是超时判断要自己换算。接收一帧的函数如下。它先从串口读帧头根据帧头类型决定后续数据长度然后依次读块号、反码、数据、CRC最后校验。#define PKT_INVALID (-1) #define PKT_TIMEOUT (-2) #define PKT_CANCEL (-3) static int ymodem_recv_frame(uint8_t *data_buf, uint32_t timeout_ms) { uint8_t header; uint8_t blk, blk_neg; uint8_t crc_hi, crc_lo; uint16_t crc_calc; uint32_t len, i; if (!uart_read_byte_timeout(header, timeout_ms)) return PKT_TIMEOUT; if (header 0x04) /* EOT */ return 0x04; if (header 0x18) /* CAN */ return 0x18; if (header 0x01) len 128; else if (header 0x02) len 1024; else return PKT_INVALID; if (!uart_read_byte_timeout(blk, timeout_ms)) return PKT_TIMEOUT; if (!uart_read_byte_timeout(blk_neg, timeout_ms)) return PKT_TIMEOUT; if ((uint8_t)(blk blk_neg) ! 0xFF) return PKT_INVALID; for (i 0; i len; i) { if (!uart_read_byte_timeout(data_buf[i], timeout_ms)) return PKT_TIMEOUT; } if (!uart_read_byte_timeout(crc_hi, timeout_ms)) return PKT_TIMEOUT; if (!uart_read_byte_timeout(crc_lo, timeout_ms)) return PKT_TIMEOUT; crc_calc ymodem_crc16(data_buf, len); if (((uint8_t)(crc_calc 8) ! crc_hi) || ((uint8_t)(crc_calc 0xFF) ! crc_lo)) return PKT_INVALID; return (int)len; }注意这个函数里我假设块0也是128字节帧发送端会按标准发。有些工具发送1024字节帧时最后一个不足1024字节的块会用128字节帧SOH发送这样更省流量。所以接收端不能假设所有数据帧都是1024字节每次要根据frame header判断。主状态机循环代码如下。它做的事情是先处理块0文件信息然后循环收正文数据帧写Flash收EOT收尾。int ymodem_iap_run(void) { uint8_t frame_buf[1024]; uint32_t app_size 0; uint32_t current_addr APP_BASE_ADDR; int ret; int is_first 1; uart_write_byte(0x43); /* C */ ret ymodem_recv_frame(frame_buf, 5000); if (ret 0) return -1; /* 块0文件名/大小只校验不写Flash */ uart_write_byte(0x06); /* ACK */ uart_write_byte(0x43); /* C */ while (1) { ret ymodem_recv_frame(frame_buf, 5000); if (ret 0x18) /* CAN */ return -2; if (ret 0x04) /* EOT */ { uart_write_byte(0x06); /* ACK */ /* 等待第二个EOT或空包 */ ret ymodem_recv_frame(frame_buf, 5000); if (ret 0x04 || ret 0) { uart_write_byte(0x06); } return (int)app_size; } if (ret 0) return -3; if (is_first) { is_first 0; } if (current_addr ret APP_BASE_ADDR APP_MAX_SIZE) return -4; /* 擦除本页写入数据 */ flash_erase_page(current_addr ~(FLASH_PAGE_SIZE - 1)); flash_write_words(current_addr, frame_buf, (uint32_t)ret); current_addr (uint32_t)ret; app_size (uint32_t)ret; uart_write_byte(0x06); /* ACK */ } }这段代码有几个细节需要说明第一次uart_write_byte(0x43)之后发送端可能不会立刻响应所以ymodem_recv_frame的超时时间我给了5000ms留足人工点击发送固件的时间。块0没有写入Flash只是解析文件名和大小。如果愿意这里还可以把文件名和大小提取出来判断固件是否适配。每收到一帧正文current_addr会直接累加。对于1024字节帧正好提升1KB对于最后一个不足1024的128字节帧同样按实际长度累加。但要提醒的是如果最后一帧用了128字节帧且正好是某一页的最后部分注意是否需要对页边界对齐处理。通常APP设计时将整个固件大小补齐到页边界编译实际上不会有太大问题。如果收到CAN字符表示发送端主动取消传输我直接返回错误码。连续收到两个CAN在Ymodem里才是标准取消但很多工具只发一个我为了兼容性保留单个处理。主循环里还应该对ACK和NAK做区分处理。当前代码在接收帧失败时会直接返回错误更完善的实现可以在收到CRC校验失败帧时回复NAK并等待重传同一块数据。实际Ymodem工具在发送端也会处理重传所以你即使不实现NAK重传脚本工具也可能重发。但既然做协议我建议还是补上。补法很简单把ret为PKT_INVALID的情况单独处理发送0x15NAK然后继续循环等待同一帧。具体实现可参考下面这段逻辑if (ret PKT_INVALID) { uart_write_byte(0x15); /* NAK要求重发 */ continue; }4.4 跳转到APP那一步很多人写错APP写完以后bootloader的最后一个关键动作是跳转执行APP。跳转本身不难真正容易写错的是两个点一是没有校验栈顶指针有效性跳到了环地址直接HardFault二是没有先设置VTOR导致APP中断全部错乱。我使用下面的跳转函数typedef void (*app_reset_handler)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_sp; uint32_t app_reset; app_reset_handler jump; app_sp *(volatile uint32_t *)app_addr; app_reset *(volatile uint32_t *)(app_addr 4); /* 栈顶指针应指向RAM区域GD32F103 RAM为0x20000000起始 */ if ((app_sp 0x2FFE0000) ! 0x20000000) { return; } /* 关闭全局中断清理外设状态 */ __disable_irq(); SCB-VTOR app_addr; __set_MSP(app_sp); jump (app_reset_handler)app_reset; jump(); }这里栈顶指针校验用的是粗粒度掩码。GD32F103CBT6的RAM是20KB地址范围是0x20000000~0x20004FFF如果换成更大RAM的芯片这个掩码要调整。更通用的做法是直接在工程头文件里定义一个RAM起始和RAM大小宏然后判断app_sp RAM_BASE app_sp RAM_BASE RAM_SIZE。跳转之前关全局中断是为了避免跳转那一刻有中断请求插入导致执行流被拉回旧中断向量表。APP启动后会在自己的SystemInit函数里重新配置时钟和中断。如果你的APP初始化特别快没有及时重新设置VTOR可能出现跳转成功后PC跑飞。所以我在APP工程的启动代码里也做了向量表偏移设置下一节会说到。4.5 APP工程的修改点很多人写完bootloader编译下载后却发现APP不运行这一步往往是APP工程没有修改正确。APP工程必须做两处调整第一处是链接脚本/Keil工程设置。打开Keil的Options for Target - Target页把IROM1的起始地址改成APP在Flash中的实际地址大小改成APP区大小。比如APP起始0x08008000大小0x1800096KB。这一步决定了APP编译出的中断向量表位置、代码地址和所有绝对地址引用。第二处是中断向量表偏移设置。GD32F103的启动文件会执行SystemInit然后在跳转main前初始化向量表。但SystemInit默认把向量表指到0x08000000。APP工程里通常在SystemInit函数末尾或者在main函数开始时执行SCB-VTOR 0x08008000;如果你的项目使用Keil还有一种方式是在system_gd32f10x.c里找到类似#define VECT_TAB_OFFSET 0x0的定义改成0x8000。这样SystemInit会在早期完成向量表重映射。我从实测经验讲最好两处都设置因为不同库版本的SystemInit对VTOR的处理时机不同。设置早了问题不大设置晚了可能中断频繁的代码直接HardFault。APP工程编译生成的bin文件默认从0x08008000开始前8字节就是自己的SP和Reset向量。bootloader跳转时读取地址0x08008000和0x08008004拿到的是APP自己的向量表内容这正好对得上。5. 完整升级流程与实测记录5.1 编译Bootloader并首次烧录Bootloader本身是独立工程需要单独编译、单独下载。首次下载我用J-Link通过SWD接口直接烧到GD32起始地址0x08000000。烧录后可以先给串口接上USB转TTL模块上电应该能看到bootloader打印提示比如“Bootloader v1.0, wait Ymodem…”这样的输出。我实际代码里在串口初始化后加了一行版本打印方便确认bootloader已经活过来。我建议在bootloader里加一个简单的升级触发方式默认上电后如果检测到升级标志比如某个GPIO电平或者某个备份寄存器标志就进入升级模式否则如果APP区已经有有效固件直接跳转APP。也可以做成“上电后延时500ms如果收到0x43也就是串口工具主动发送Ymodem的握手信号就进入升级否则超时后跳APP”。第二种方式现场工程师操作简单只要在上电瞬间点一下“发送文件”就不需要额外按按钮。实测下来两者结合最好用保留一个升级触发引脚可以紧急恢复同时也可以通过串口唤醒进入升级。5.2 生成APP.binMDK中的fromelfKeil MDK编译APP工程后默认生成的是axf和hex文件。Ymodem传输工具通常需要bin文件所以要在MDK中配置一个fromelf命令自动从编译结果中生成bin文件。在MDK的Options for Target - User标签页After Build/Rebuild里勾选Run #1然后填写命令行fromelf --bin --output.\Objects\APP.bin .\Objects\APP.axf前提是fromelf命令在Keil的ARM编译器目录里可以被系统PATH找到。如果提示找不到命令可以在前面加上完整路径例如C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output.\Objects\APP.bin .\Objects\APP.axf如果使用AC6编译器路径会变成C:\Keil_v5\ARM\ARMCLANG\bin\fromelf.exe。不同版本路径略有差异找到实际路径即可。编译完成后会生成一个从0x08008000开始排布的APP.bin。这个bin文件就是通过Ymodem传输的固件文件。5.3 用终端工具完成一次Ymodem升级实测流程如下用USB转TTL模块连接GD32的USART0TXD接板子的RXDRXD接板子的TXDGND必须共地。打开串口工具选择对应的COM口波特率设置为115200数据位8、停止位1、无校验、无流控。给设备上电bootloader启动后串口窗口能看到提示信息。在工具的传输菜单里选择“发送Ymodem”或者“Ymodem”选择APP.bin文件进行发送。观察发送进度。传输过程中bootloader每接收一帧串口工具应能看到ACK信号或进度条滚动。发送完毕后APP自动启动。如果APP里有打印串口窗口会出现APP的启动日志。这里重点强调一项设置串口工具必须关闭硬件流控。很多USB转TTL模块没有RTS/CTS引线如果工具开启了流控Ymodem握手会卡住因为双方没有物理引脚进行流控信号交换。我实测时用SecureCRT发送大约96KB的固件115200波特率下传输加擦写总共花了十几秒中间没有出现错误重传。如果换成XCOM这类国产串口工具需要在发送文件时选择Ymodem大多数工具都支持。5.4 如何判断升级是否真正成功升级是否成功不能光看串口工具上的“传输完成”。我做了三层验证第一层bootloader在升级完成后把接收到的固件总长度和预期值从块0解析的文件大小做比较。如果两者不等说明传输过程数据不完整。第二层bootloader可对整个APP区做一次CRC校验把计算结果和固件末尾预存的校验值比对。但这要求固件生成时把校验值放进bin里这属于额外设计。如果不想做这么复杂至少可以在升级完成后读取APP起始地址处的app_sp判断是否落在合法RAM区域。第三层APP启动后打印自己的版本号。我在APP的main函数开头加了一句串口打印比如APP Version 2.0 started。升级完成后看到这行输出就可以确认跳转成功且APP运行正常。这个验证最直观我在现场就靠这个判断是否升级成功。每次升级前bootloader还会把APP区前4个字节即APP的初始栈顶指针读出来检查如果栈顶指针明显不合法就认为APP区没有有效程序。这个检查能防止“空APP跳转”导致死机。5.5 升级失败后的恢复升级过程中如果网络抖动、串口线松动、或者突然断电APP区可能写到一半处于“半完整”状态。这时bootloader不能直接跳转到这个半成品APP否则运行必然出问题。我的处理策略是升级完成后先读取APP起始地址的栈顶指针判断它是否合法。同时记录一个“APP有效标志”放在参数区。只有在升级完成后将标志置为有效上电才跳转。如果升级中断、标志未置位bootloader会继续等待下一次Ymodem升级或者等待用户重新发送固件。这样设计能保证一个关键体验bootloader永远不会因为升级失败而变成砖。即使APP写坏了bootloader依然能启动可以通过串口再次升级修复。这也是我不断向客户强调的——绝对不要让bootloader自己毁掉自己。6. 常见问题与避坑实录6.1 典型问题速查我把开发调试过程中遇到的高频问题整理成了一张表方便排查现象可能原因解决办法串口发送Ymodem后无响应波特率不匹配、TXD/RXD接反、没有共地检查接线用串口助手自发自收测试确认线路正常升级传输过程中反复报CRC错误波特率过高、串口干扰、发送工具开了流控降到9600或115200关闭流控更换短线或加磁环传输结束APP不运行跳转时栈顶指针无效或者向量表偏移未设置检查APP工程IROM地址和VTOR设置确认APP bin从正确地址生成APP运行时串口中断触发HardFault跳转前没有设置VTOR或者APP内中断向量表偏移没生效确保跳转前设置SCB-VTORAPP启动系统初始化时再次设置Flash擦写偶尔失败上次操作标志位未清除或者Flash被读保护每次操作前fmc_flag_clear检查选项字节读保护升级到一半卡死擦写Flash时中断未关闭中断服务函数在Flash里执行擦写期间关全局中断或者把擦写函数放到RAM执行发送端提示“文件名过长”或“文件头错误”块0格式不标准接收端不强行解析文件名只跳过块0或换标准Ymodem工具串口工具进度条走完了固件大小对不上最后一帧不足1024字节时填充和帧类型处理错误检查接收端是否支持128字节帧收尾检查块号是否回绕6.2 我的几条底线建议做这个项目我踩过几个印象很深的坑总结成建议。第一Flash擦写期间务必关中断。这个我在2.3节提过但值得再强调一次。GD32的Flash控制器在编程擦除时对Flash正常读操作会产生等待而中断服务函数一般都存放在Flash里。也就是中断确实能进但取指会被卡住。如果中断处理逻辑比较复杂极容易超时或者状态错乱。我的bootloader里主动把SysTick也停了擦写完成后重新启动。升级过程本来就不是实时系统关那几十毫秒中断完全没影响。第二不要迷信“官方例程”。GD32官方例程一般只演示Flash怎么写、串口怎么收发不一定直接给你完整可用的IAP。很多例程默认烧写在0x08000000直接拿来做APP会跟bootloader冲突。一定要把APP工程、bootloader工程、链接地址全部分开核对。第三Ymodem工具的选择要固定。我开发时用SecureCRT现场测试时用XCOM两个工具的Ymodem实现细节有差异。比如某个工具在发送完正文后直接发空包另一个会再发一个EOT。这两种我的代码都能兼容但如果你只按一种工具调试交付给客户用到另一种工具就会遇到“传输完了不退出”的情况。验收阶段最好至少用两款不同的终端软件做兼容性验证。第四升级时要保证供电稳定。串口升级过程中Flash擦写是电流小高峰如果设备用劣质USB转TTL模块供电电压跌落可能导致MCU复位。我现场遇到过几次传输到一半设备重启后来改成外部独立供电才稳定。第五给bootloader保留一个简单的串口命令交互而不是一上来就只有Ymodem被动接收。比如我做了个“输入v回车返回版本号”的功能这样现场排查时能快速确认设备当前bootloader版本和升级状态再决定是否升级。6.3 关于可靠性还想多说两句IAP项目做到最后你发现技术难度不是最大的问题最大的问题是如何保证“永远有救”。所以我额外做了一个A/B分区备份方案。原理很简单把Flash划分成两个APP区正常运行区A和备用区B。当新固件升级到A区时如果校验失败bootloader自动回滚到B区运行只有新固件完整且成功运行后才把B区也更新。计划后续把这篇内容单独整理一下这次先讲串口Ymodem版本是因为A/B备份涉及的分区逻辑和启动策略更复杂如果你是第一次接触IAP先把这篇文章中的基础版跑通再说。我自己的体会是IAP bootloader不是那种“照着抄一个就能完事”的模块它跟你的产品形态、升级频率、现场维护能力都强相关。串口Ymodem这套方案胜在通用、务实、调试可见在没有网络的产品上是性价比非常高的选择。写完bootloader后你再回头看那些拆壳烧录的日子会觉得整个开发流程终于像个正经产品了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。