3. 串口YMODEM协议固件传输

串口YMODEM协议固件传输 学习卡片

任务目标

本案例实现Bootloader的核心存储功能:将接收到的固件数据写入STM32内部Flash的指定应用区域,并完成扇区擦除与编程操作。通过串口接收固定格式的固件数据帧,写入Flash后回读校验,验证存储正确性。

硬件与依赖

  • 硬件:STM32F103系列(本文以STM32F103C8T6为例,Flash 64KB,页大小1KB)
  • 依赖:STM32标准外设库(或HAL库,本文使用标准库寄存器操作以保持最小依赖)
  • 串口:USART1,波特率115200,用于接收固件数据

Flash分区规划

  • Bootloader区:0x08000000 ~ 0x08003FFF(16KB,存放本Bootloader代码)
  • 应用区起始地址:APP_START_ADDR = 0x08004000(用户固件写入此区域)
  • 应用区最大容量:48KB(0x08004000 ~ 0x0800BFFF)

关键完整代码

#include "stm32f10x.h"
#include <string.h>

#define APP_START_ADDR      0x08004000  // 应用区起始地址
#define APP_MAX_SIZE        (48 * 1024) // 应用区最大48KB
#define FLASH_PAGE_SIZE     1024        // STM32F103页大小1KB

// 串口接收缓冲区(简单示例:一次接收一帧完整数据)
#define FRAME_MAX_SIZE      2048
uint8_t rx_buffer[FRAME_MAX_SIZE];
uint32_t rx_len = 0;

// 擦除应用区指定地址的页(addr必须页对齐)
void Flash_ErasePage(uint32_t addr) {
    FLASH_Unlock();
    while (FLASH_ErasePage(addr) != FLASH_COMPLETE);
    FLASH_Lock();
}

// 编程:将data写入addr,len为字节数(addr建议半字对齐)
void Flash_ProgramData(uint32_t addr, uint8_t *data, uint32_t len) {
    FLASH_Unlock();
    for (uint32_t i = 0; i < len; i += 2) {
        uint16_t half_word = data[i] | (data[i+1] << 8);
        while (FLASH_ProgramHalfWord(addr + i, half_word) != FLASH_COMPLETE);
    }
    FLASH_Lock();
}

// 从Flash读取数据到缓冲区
void Flash_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) {
    for (uint32_t i = 0; i < len; i++) {
        buf[i] = *(volatile uint8_t *)(addr + i);
    }
}

// 将接收到的固件写入Flash(核心函数)
// 参数:firmware_data - 固件数据指针;len - 固件长度
// 返回:0成功,1长度超限,2擦除失败,3校验失败
int StoreFirmware(uint8_t *firmware_data, uint32_t len) {
    if (len == 0 || len > APP_MAX_SIZE) return 1;
    
    // 1. 擦除应用区所有页(从APP_START_ADDR开始,覆盖len所需页数)
    uint32_t pages_needed = (len + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE;
    for (uint32_t i = 0; i < pages_needed; i++) {
        Flash_ErasePage(APP_START_ADDR + i * FLASH_PAGE_SIZE);
    }
    
    // 2. 编程写入
    Flash_ProgramData(APP_START_ADDR, firmware_data, len);
    
    // 3. 回读校验
    uint8_t verify_buf[FRAME_MAX_SIZE];
    Flash_ReadData(APP_START_ADDR, verify_buf, len);
    if (memcmp(firmware_data, verify_buf, len) != 0) return 3;
    
    return 0;
}

// 串口接收中断(示意:接收一帧数据后调用StoreFirmware)
void USART1_IRQHandler(void) {
    if (USART_GetITStatus(USART1, USART_IT_RXNE)) {
        uint8_t ch = USART_ReceiveData(USART1);
        if (rx_len < FRAME_MAX_SIZE) {
            rx_buffer[rx_len++] = ch;
        }
    }
}

// 主函数示例:接收完一帧后调用存储
int main(void) {
    // 系统初始化(时钟、GPIO、USART1等,此处省略具体配置)
    // USART1_Init(115200);
    
    while (1) {
        // 假设通过串口接收完一帧完整固件,rx_len为实际长度
        // 实际项目中需定义帧协议(如帧头+长度+数据+校验)
        if (rx_len > 0) {
            int result = StoreFirmware(rx_buffer, rx_len);
            if (result == 0) {
                // 存储成功,可跳转到APP_START_ADDR执行(本案例不涉及跳转)
                // 发送成功应答
            } else {
                // 发送错误码
            }
            rx_len = 0; // 清空接收计数
        }
    }
}

编译与运行

  1. 使用Keil MDK或IAR创建工程,芯片选择STM32F103C8。
  2. 将上述代码加入工程,注意将Bootloader链接地址设置为0x08000000(默认即可)。
  3. 编译生成HEX文件,通过ST-Link烧录到开发板。

验证方法

  1. 功能验证:在main函数中构造测试数据(如uint8_t test_data[] = {0x01, 0x02, 0x03, 0x04, ...}),直接调用StoreFirmware(test_data, sizeof(test_data)),观察返回值是否为0。
  2. 数据完整性验证:调用Flash_ReadData(APP_START_ADDR, read_buf, len),对比read_buf与原始数据是否一致。若一致,说明擦除和编程操作正确。
  3. 边界测试:尝试写入超过APP_MAX_SIZE的数据,应返回错误码1。
  4. 通过条件:StoreFirmware返回0,且回读数据与写入数据完全一致,即判定Flash存储功能正常。

注意事项

  • 擦除操作前必须调用FLASH_Unlock(),操作完成后调用FLASH_Lock()。
  • Flash编程最小单位为半字(16位),因此数据长度必须为偶数;若长度为奇数,需在帧协议中做填充处理。
  • 擦除操作会清除整页数据,因此应用区起始地址必须页对齐(1KB边界)。
  • 本案例未包含串口帧协议解析和Boot跳转功能,仅聚焦Flash擦写核心操作;实际Bootloader需在此基础上增加帧头/长度/校验字段。