尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F030C8T6串口Ymodem IAP升级实战:从分区到跳转

STM32F030C8T6串口Ymodem IAP升级实战:从分区到跳转 简介面向STM32F030C8T6开发者的IAP升级代码包解决通过串口1采用Ymodem协议实现固件在线更新的问题。项目基于ARM Cortex-M0内核芯片完整演示从初始化、帧接收、CRC校验到闪存读写与状态管理的全过程适合嵌入式开发者学习或直接移植。代码包共141个文件以h头文件、c源文件为主辅以uvprojx工程文件、axf/hex等编译产物及htm说明文档等压缩包整体约1.76MB目录结构清晰便于查找。已有1019人学习下载。除标准外设库驱动外资源还包含可复用的IAP框架与Ymodem接收处理逻辑稍作调整即可适配同系列其他型号。通过阅读源码与工程配置可掌握bootloader与App分区设计、串口中断处理、CRC校验重传机制以及Flash擦写时序等关键技能是构建稳定串口升级方案的良好参考。1. 串口 Ymodem IAP 升级的第一步是定布局把 Ymodem 跑在 stm32f030c8t6 的串口1上做 IAP传输本身只占三成工作量帧格式、CRC16、ACK/NAK 握手加起来不过百行代码。七成工作量在工程边界上——F030 是 Cortex-M0没有 VTOR中断向量表不能像 F1/F4 那样直接改地址偏移它的 Flash 低 32KB 是 1KB 页、高 32KB 是 2KB 页擦除函数少写一个分支固件就写烂了。这里按一条完整 Bootloader 的落地顺序讲先定存储布局和向量表方案再实现 Ymodem 接收、Flash 写入、跳转与回滚最后给出一组串口工具的实测参数和排错表。适合正在做量产 IAP 升级、或者打算把旧工程从 Xmodem 换成 Ymodem 的嵌入式工程师。2. STM32F030C8T6 的 IAP 存储布局与中断向量重定向2.1 Bootloader 和 App 的分区怎么划IAP 和 Bootloader 的关系很容易被一句话带过但分区才是整个升级方案的地基。MCU 上电先跑 Bootloader它检查一个升级标志需要升级就通过串口1收 Ymodem 固件不需要就直接跳转到 App。App 平时只管业务逻辑只有收到远程指令或用户触发时才把升级标志写进 Flash然后复位进入 Bootloader。F030C8T6 的资源是 64KB Flash、8KB SRAM没有内置 USB用串口1做升级通道是成本最低的路。Flash 分区我按“Bootloader App 备份区 参数页”四段划分带回滚能力起始地址结束地址大小用途0x080000000x08001FFF8KBBootloaderYmodem接收、Flash驱动、跳转逻辑0x080020000x08007FFF24KBApp业务固件0x080080000x0800DFFF24KB备份区新固件先落这里校验后再拷贝到App区0x0800E0000x0800FFFF8KB参数页升级标志、固件长度、CRC选 0x08002000 做 App 起点是为了让 App 区完全落在前 32KB 的 1KB 页区间Ymodem 的 1024 字节包正好对应一页边收边写时不需要处理页大小切换。备份区从 0x08008000 开始落在 2KB 页区间擦写时按 2KB 页处理。如果产品不需要回滚可以把 App 区扩到 48KB但擦除逻辑要额外兼容 0x08008000 之后的 2KB 页代价不大风险却多了一种。2.2 F030 没有 VTOR向量表不能直接改偏移这是 F030 做 IAP 最容易被坑的点。Cortex-M0 内核没有 VTOR 寄存器中断向量表固定从 0x00000000 读取Flash 在 0x00000000 的别名就是 0x08000000。也就是说不管 App 编译在哪个地址CPU 响应中断时都会去 0x08000000 找向量表——那正是 Bootloader 的中断服务函数地址App 的所有中断都会跑飞。F103 上常见的SCB-VTOR APP_ADDR在 F030 上根本不存在编译都过不去。常见做法是把 0x00000000 重映射到 SRAM先把 App 向量表原样复制到 0x20000000再设置 SYSCFG_CFGR1 的 MEM_MODE 为 0b10让地址 0x00000000 命中 SRAM 起始处。这样中断发生时CPU 从 SRAM 读向量表向量值直接指向 App 的中断函数因为向量表里存的是绝对地址不需要改动任何一项。向量表搬多少F030 只有 32 个外部中断加上系统异常一共不超过 40 个向量固定搬运 0x100 字节64 个向量最省心也不怕后面换型号向量变多。2.2.1 向量表搬移的标准实现#define APP_ADDR 0x08002000UL #define VECTOR_TABLE_LEN 0x100UL static void vector_table_relocate(uint32_t app_addr) { uint32_t *src (uint32_t *)app_addr; uint32_t *dst (uint32_t *)0x20000000UL; for (uint32_t i 0; i VECTOR_TABLE_LEN / 4; i) { dst[i] src[i]; } __DSB(); __ISB(); /* MEM_MODE 0b10将0x00000000映射到SRAM */ SYSCFG-CFGR1 (SYSCFG-CFGR1 ~SYSCFG_CFGR1_MEM_MODE_Msk) | (0x2UL SYSCFG_CFGR1_MEM_MODE_Pos); }代码里先复制再改映射顺序不能反。复制数据源是 Flash 里的 App 向量表目标地址是 SRAM 起始处__DSB和__ISB保证复制完成后缓存和流水线状态一致再切换映射。SYSCFG 的时钟在 F030 上挂在 APB2使用前需要RCC-APB2ENR | RCC_APB2ENR_SYSCFGEN否则写 CFGR1 无效。提示Bootloader 自身如果开启了中断必须在改 MEM_MODE 之前关闭所有中断使能否则映射切换后下一个中断会从 SRAM 取根本不存在的向量直接 HardFault。2.3 App 工程只需要改链接地址App 侧几乎不用改 C 代码只需要把链接脚本的 Flash 起始地址改成 0x08002000长度改为 24KB。启动文件里的向量表会自然放在 0x08002000SystemInit不需要写 VTOR 相关的代码。启动文件里的第 1 个向量是初始 MSP第 2 个是 Reset_Handler跳转时读这两个值就够了。一个容易忽略的依赖Bootloader 和 App 里 UART1 的波特率分频必须一致。F030 不像 F103 那样 APB1/APB2 时钟频率可能不同F030 的 USART1 和 USART2 都挂在同一个 PCLK 上所以只要两边都用 48MHz 主频配 115200参数就是同一套。如果 App 里把系统时钟改成了 HSI 直出 8MHz跳转后串口波特率会偏得离谱。3. 用串口1实现Ymodem接收帧格式、CRC与状态机3.1 Ymodem 数据帧和控制字符Ymodem 是 Xmodem 的扩展核心差异是开始传输前先发一个 0 号文件信息包包含文件名和文件大小数据包支持 128 字节和 1024 字节两种长度校验统一用 CRC16-CCITT而不是 Xmodem 早期的校验和。对固件升级来说1024 字节包能把有效吞吐拉满128 字节包则用于文件信息包。数据包帧格式字段长度说明SOH 或 STX10x01 表示后面数据区 128 字节0x02 表示 1024 字节包序号1从 0x00 开始每个包 1溢出后回绕包序号取反10xFF - 包序号用于校验序号完整性数据区128 / 1024负载CRC 高字节1CRC16 结果的高 8 位CRC 低字节1CRC16 结果的低 8 位控制字符对应的含义ACK 是 0x06NAK 是 0x15EOT 是 0x04CAN 是 0x18接收方请求 CRC 模式时发送 ASCII 字符C0x43。在串口调试助手观察原始数据流如果看不到这些字符说明对方根本不是 Ymodem。3.2 双向握手的完整时序Ymodem 是半双工握手协议接收方STM32主动发起发送方PC 工具被动响应。完整时序分四个阶段接收方发C发送方回 0 号包数据区是文件名\0文件大小\0某些工具还会追加 mtime接收方对 0 号包回 ACK再发C发送方开始发 1 号、2 号……数据包每个数据包接收方校验 CRC 和序号正确回 ACK错误回 NAK 等待重发数据发完后发送方发 EOT接收方第一次回 NAK发送方再发 EOT接收方回 ACK然后发C发送方发一个 128 字节的空 0 号包作为结束包接收方回 ACK传输结束。注意第 4 步第一次 EOT 必须回 NAK这是 Ymodem 标准行为目的是防止 EOT 在传输途中损坏。有些 PC 工具对此要求不严格第二次 EOT 后都按 ACK 处理但 Bootloader 端按标准写最稳妥。BC4556static uint16_t ymodem_crc16(const uint8_t *buf, uint16_t len) { uint16_t crc 0x0000; while (len--) { crc ^ (uint16_t)(*buf) 8; for (uint8_t i 0; i 8; i) { if (crc 0x8000) { crc (crc 1) ^ 0x1021; } else { crc 1; } } } return crc; }多项式 0x1021、初值 0x0000、不反转输入输出这是 Xmodem/Ymodem 系约定和 Modbus CRC16、CRC32 都不同。计算范围只覆盖数据区不包含帧头、序号、序号取反和 CRC 自身。128 字节包算 128 字节1024 字节包算 1024 字节这个参数在ymodem_crc16里体现为传入的len。3.3 串口1接收为什么必须用中断加 FIFOBootloader 接收固件时会穿插 Flash 擦写单页擦除在 F030 上一个字节一个字节等 BSY 清零需要毫秒级时间。如果主循环里每收一字节就运行一次while (USART1-ISR USART_ISR_RXNE)查询式读串口擦页期间串口来的字节全部丢失Ymodem 包一断就 CRC 失败。我一般会用串口1接收中断加 1KB 环形缓冲区。F030 的 USART1 中断服务函数里把USART1-RDR读走放入 FIFO主循环只管从 FIFO 取字节喂 Ymodem 状态机。600 字节的包即使主循环卡 2ms 也不丢数据。#define FIFO_SIZE 1024 static uint8_t uart_fifo[FIFO_SIZE]; static volatile uint16_t fifo_r; static volatile uint16_t fifo_w; void USART1_IRQHandler(void) { if (USART1-ISR USART_ISR_RXNE) { uint8_t c USART1-RDR; uint16_t next (fifo_w 1) % FIFO_SIZE; if (next ! fifo_r) { uart_fifo[fifo_w] c; fifo_w next; } } }F030 的 USART 寄存器名和 F103 不一样状态寄存器叫ISR数据寄存器读RDR、写TDR不是SR和DR从 F1 转过来的人最容易在中断里写错寄存器名。初始化时 PA9 配 AF1 做 TX、PA10 配 AF1 做 RX波特率 115200、8N1、关闭流控。RCC-AHBENR | RCC_AHBENR_GPIOAEN; RCC-APB2ENR | RCC_APB2ENR_USART1EN; GPIOA-MODER ~(0x3UL 18); GPIOA-MODER | (0x2UL 18); /* PA9 AF */ GPIOA-MODER ~(0x3UL 20); GPIOA-MODER | (0x2UL 20); /* PA10 AF */ GPIOA-AFR[1] ~(0xFUL 4); GPIOA-AFR[1] | (0x1UL 4); /* PA9 AF1 */ GPIOA-AFR[1] ~(0xFUL 8); GPIOA-AFR[1] | (0x1UL 8); /* PA10 AF1 */ USART1-BRR 48000000UL / 115200UL; USART1-CR1 USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE; NVIC_EnableIRQ(USART1_IRQn);BRR直接写 416实际波特率和 115200 的误差在 0.2% 以内串口通信没问题。如果要参考 STM32CubeMX 生成的代码它会算带小数位的 BRR但对 IAP 场景没有本质区别。3.4 Ymodem 接收状态机的核心实现状态机按“找包头 → 收序号 → 校序号取反 → 收数据 → 收 CRC”五步推进。用static变量保存中间状态每收到一个字节就推进一次。这样主循环可以在收包间隙做别的事比如查升级标志、喂看门狗。#define SOH 0x01 #define STX 0x02 #define ACK 0x06 #define NAK 0x15 #define EOT 0x04 #define CAN 0x18 #define C 0x43 typedef enum { YM_NONE 0, YM_OK, YM_BAD, YM_EOT, YM_CAN } ym_ret_t; static uint8_t pkt_buf[1024]; static uint16_t pkt_len; static uint16_t pkt_idx; static uint8_t rx_seq; static uint8_t pkt_hdr; ym_ret_t ymodem_poll(void) { static uint8_t st 0; static uint8_t crc_hi; uint8_t c; if (!uart_fifo_get(c)) { return YM_NONE; } switch (st) { case 0: if (c SOH || c STX) { pkt_hdr (c STX) ? 1024 : 128; pkt_len pkt_hdr; pkt_idx 0; st 1; } else if (c EOT) { return YM_EOT; } else if (c CAN) { return YM_CAN; } break; case 1: rx_seq c; st 2; break; case 2: if ((uint8_t)~c ! rx_seq) { st 0; return YM_BAD; } st 3; break; case 3: pkt_buf[pkt_idx] c; if (pkt_idx pkt_len) { st 4; } break; case 4: crc_hi c; st 5; break; case 5: { uint16_t crc (uint16_t)((crc_hi 8) | c); st 0; if (crc ! ymodem_crc16(pkt_buf, pkt_len)) { return YM_BAD; } return YM_OK; } default: st 0; break; } return YM_NONE; }每次ymodem_poll返回YM_OK时pkt_buf里是一包完整数据pkt_len是这包的有效长度。外层主循环根据当前所处的握手阶段决定怎么处理收到 0 号包就解析文件名和大小收到数据包就写 Flash 并发 ACK收到YM_BAD就回 NAK收到YM_EOT就进入 EOT 处理子状态。注意 Ymodem 的包序号从 0x00 开始0 号包是文件信息包1 号包才是第一个数据包。状态机里没做序号连续检测实际工程建议在返回YM_OK后由上层比对rx_seq和期望序号不一致也回 NAK。4. F030C8T6 Flash 擦写页大小差异与边收边写4.1 F030C8T6 的 Flash 页布局F030C8T6 的 64KB Flash 分为两种页大小这是 F0 系列里最容易踩的坑因为很多人直接拿 F103 的经验以为每页都是 1KB地址范围页大小页数0x08000000 ~ 0x08007FFF1KB32 页0x08008000 ~ 0x0800FFFF2KB16 页前面布局里 App 区从 0x08002000 到 0x08007FFF全部是 1KB 页所以 App 区可以按 1KB 页擦写。备份区从 0x08008000 开始是 2KB 页复制固件到备份区时必须按 2KB 页擦除。如果只用 STM32CubeMX 生成的擦除接口要注意它默认的页大小宏不一定适配高地址区间最好自己写一个按地址判断页大小的函数。4.2 Flash 解锁、擦除、编程的标准代码F030 的 Flash 控制器操作和 F1 类似但寄存器细节不同。擦除和编程前必须先往 KEYR 写解锁键值操作完成或出错时 CR 寄存器会自动重新上锁。每次擦除和写入半字后都要等待 BSY 清零。static void flash_unlock(void) { FLASH-KEYR 0x45670123UL; FLASH-KEYR 0xCDEF89D9UL; } static void flash_erase_page(uint32_t addr) { while (FLASH-SR FLASH_SR_BSY); FLASH-CR | FLASH_CR_PER; FLASH-AR addr; FLASH-CR | FLASH_CR_STRT; while (FLASH-SR FLASH_SR_BSY); if (FLASH-SR FLASH_SR_WRPRTERR) { /* 页写保护未解除需要处理 RDP 或 WRPR */ } FLASH-CR ~FLASH_CR_PER; } static void flash_program_halfword(uint32_t addr, uint16_t data) { while (FLASH-SR FLASH_SR_BSY); FLASH-CR | FLASH_CR_PG; *(volatile uint16_t *)addr data; while (FLASH-SR FLASH_SR_BSY); FLASH-CR ~FLASH_CR_PG; }写 Flash 必须按半字 16 位写不能按字节写否则会触发编程错误。flash_erase_page传入的地址必须是页起始地址App 区按 1KB 对齐备份区按 2KB 对齐。擦除和编程期间要关闭中断或者至少保证中断服务函数不访问 Flash否则擦除过程中取指失败会导致 HardFault。4.3 边收边写Ymodem 数据包怎么落进 FlashYmodem 的 1024 字节数据包正好等于 App 区的一页 1KB所以最简单可靠的策略是把收到的数据包攒到 RAM 页缓冲里凑满一页就擦除一个页、写入一个页不用等整个固件收完再写。这既节约 RAM也能在传输中途断电时定位到具体写到哪一页。static uint8_t page_buf[2048]; static uint16_t page_fill; static uint32_t app_write_addr APP_ADDR; static void page_flush(void) { uint32_t page_size (app_write_addr 0x08008000UL) ? 1024UL : 2048UL; uint16_t halfwords; if (page_fill ! page_size) { return; } flash_erase_page(app_write_addr); halfwords page_size / 2; for (uint16_t i 0; i halfwords; i) { uint16_t w (uint16_t)(page_buf[i * 2] | (page_buf[i * 2 1] 8)); flash_program_halfword(app_write_addr i * 2, w); } app_write_addr page_size; page_fill 0; }page_buf按 2KB 分配是因为备份区在 0x08008000 之后是 2KB 页如果只支持 App 区 1KB 页可以省 1KB RAM。page_fill记录当前页缓冲已填充的字节数每次收到 Ymodem 数据包就把数据追加进page_buf当page_fill等于当前地址对应的页大小时调用page_flush。如果发送方用的是 128 字节小包一页 1KB 需要 8 个包才能凑满期间 RAM 缓冲一直持有未落盘的数据。如果传输中断最后不足一页的数据要单独处理收到 EOT 后如果page_fill大于 0擦除当前页并把剩余数据补齐写入。这个“半页写”是掉电后固件损坏风险最高的地方所以才会需要备份区方案。4.4 收完最后一包的校验固件全部接收完毕后建议从 Flash 读回整段 App 区做一次 CRC 校验再决定是否跳转。F030 自带 CRC 外设可以把 Flash 内容按字喂给 CRC 模块比软件逐字节算快得多。校验通过后把升级标志改成“App 有效”再执行跳转。如果升级标志写在参数页要注意参数页不能被 App 区误擦。最简单的方式是把参数页规划在 0x0800F800 开始的最后一页 2KB独立于 App 和备份区并且给参数页单独定义一个结构体包含 magic、固件长度、固件 CRC、状态字。5. 从Bootloader跳转到App向量表、升级标志与回滚5.1 升级标志与启动流程Bootloader 每次上电都先读参数页的升级标志根据标志决定直接跳 App、进入升级模式、还是尝试从备份区恢复。标志取值需要避开 0xFFFFFFFFFlash 擦除后的默认值否则新片第一次上电会误判进入升级模式。标志值含义Bootloader 动作0xFFFFFFFF无升级请求校验 App 区有效后直接跳转0xA5A5A5A5备份区有新固件App 区未更新从备份区拷贝到 App 区成功后置回 0xFFFFFFFF0x5A5A5A5A升级进行中进入 Ymodem 接收模式0x00000000升级中断或校验失败等待重新升级参数页的写入要先把所在页擦除再写入整个结构体。擦除 0x0800F800 这一页 2KB 会清掉同一页里其他数据所以升级标志、固件长度、CRC 要放在同一个结构体内一起管理不能分散存放。5.2 跳转 App 的完整代码跳转是 Bootloader 里风险最高的动作顺序错了任何一步都会死机。标准顺序是关闭所有中断、清中断 pending、搬移向量表到 SRAM、设置 MEM_MODE 映射、设置 MSP、调用 Reset_Handler。typedef void (*fnc_ptr)(void); void jump_to_app(uint32_t app_addr) { uint32_t sp *(volatile uint32_t *)app_addr; uint32_t rv *(volatile uint32_t *)(app_addr 4); fnc_ptr reset_handler (fnc_ptr)rv; __disable_irq(); NVIC-ICER[0] 0xFFFFFFFFUL; NVIC-ICPR[0] 0xFFFFFFFFUL; vector_table_relocate(app_addr); __set_MSP(sp); reset_handler(); while (1); }sp是 App 的初始栈顶指针rv是 Reset_Handler 地址。设置 MSP 之前Bootloader 自身栈还在使用所以要先执行完所有现场清理再切换。NVIC-ICER[0]一次性清掉所有中断使能ICPR[0]清掉挂起确保跳转瞬间没有残留中断请求。跳转后 App 的 Reset_Handler 会重新初始化时钟、外设、栈和 .data 段Bootloader 的开局工作到此结束。之所以跳 Reset_Handler 而不是直接跳main是为了让 App 走上完整的启动流程避免外设初始化和 C 运行时环境缺失。5.3 升级失败与回滚策略没有备份区的 IAP 是单向的新固件覆盖旧固件一旦传输中断、校验失败、写入时掉电设备就变砖了。对量产设备来说Ymodem 升级最常见的问题恰恰是现场人员误操作导致传输中断。备份区方案把新固件先放到 0x08008000全部校验通过后再拷贝到 App 区旧固件一直保留到最后一刻。uint8_t copy_backup_to_app(uint32_t app_len) { uint32_t addr; for (addr 0; addr app_len; addr 2) { uint8_t lo *(volatile uint8_t *)(BACKUP_ADDR addr); uint8_t hi *(volatile uint8_t *)(BACKUP_ADDR addr 1); flash_program_halfword(APP_ADDR addr, (uint16_t)(lo | (hi 8))); } return 1; }这段代码按半字从备份区读、往 App 区写不需要额外的 RAM 缓冲。拷贝前要把目标页逐个擦除拷贝完成后读回 App 区再算一次 CRC和参数页里保存的固件 CRC 比对。拷贝过程中断电App 区可能不完整但备份区还是完整的下次上电 Bootloader 检测到 App 区 CRC 不一致就会重新执行拷贝相当于把升级续上了。如果连备份区拷贝都想省可以把新固件直接写入 App 区通过 Ymodem 每包 CRC 保证传输正确。但那样升级失败就只能重新烧录适合开发板和个人项目不建议出现在产品里。6. 串口Ymodem升级的实测参数与排错技巧6.1 串口工具的推荐参数PC 端串口工具的 Ymodem 入口不同但底层行为一致关键是这几项参数不能错波特率 115200、数据位 8、停止位 1、无校验、无流控。流控选项常常是卡死的元凶如果 RTS/CTS 被误开STM32 没接那两根线发送方会一直等待 CTS 信号整个握手停在第一字节。Tera Term 在 File - Transfer - YMODEM - Send 里选固件 bin 文件SecureCRT 在 Transfer - YMODEM 菜单里发两个工具我都验证过和上面的状态机兼容。发送前把固件用 objcopy 转成 bin不要直接发 hex因为 Bootloader 不解析 Intel HEX 的地址记录。6.2 常见故障与排查表现象原因处理一直收不到文件信息包PC 工具选成了 Xmodem或流控未关闭确认 YMODEM 菜单关闭 RTS/CTS 再重试每包都回 NAK重试后放弃CRC16 多项式或字节序算错用上位机发一串已知数据对拍 CRC 结果传一半卡住不动Flash 擦写期间串口查询丢字节改成中断 FIFO 接收主循环喂状态机跳转后立刻 HardFault向量表未复制或 MEM_MODE 未设置断点停在vector_table_relocate后检查 0x20000000 内容串口能收发但打印乱码App 和 Bootloader 的时钟频率不一致两边统一用 48MHz 配 115200擦写 0x08008XXX 报写保护错2KB 页误按 1KB 擦擦除地址 0x08008000 时按 2048 字节对齐6.3 一个验证 Ymodem 帧的调试技巧开发阶段可以把收到的每一个原始包通过另一路串口打印出来检查帧头、序号和 CRC。比如打开一个调试宏在ymodem_poll返回YM_OK后输出rx_seq、pkt_len和 CRC 值debug_printf(seq%d len%d crc0x%04X\n, rx_seq, pkt_len, crc_value);如果只有一个串口就先别在正式升级时开这个宏否则打印会拖慢主循环导致超时。更实用的做法是抓 0 号包的文件名和文件大小Ymodem 工具发出第一个 128 字节包里面是文件名和十进制文件大小对照实际固件大小能直接判断文件有没有选错。把pkt_buf前 32 字节按 ASCII 打印出来看到led_fw.bin 24576这样的内容说明握手已经通了剩下的就是 Flash 写入和跳转的验证。本文还有配套的精品资源点击获取
返回列表