STM32G070 串口 OTA 升级

由于没有外挂Flash,内存又比较小,所以在此使用内置Flash作为缓存,

G070CBT6整体flash位128K,为flash规划分区,分区表如下。


如何提升emmc速度_嵌入式硬件

从STM32G070寄存器手册可以看到,内部flash是2K对齐总共有64个页,同时写操作flash时要注意地址为4字节对齐。

功能

地址

占用空间

bootloader

0x8000000 - 0x8005000

20K

config

0x8005000 - 0x8005800

2K

app

0x8005800 - 0x8012000

50K

ota

0x8012000 - 0x801E800

50K

未使用

0x801E800 - 0x801FFFF

5K

串口更新协议

YModem协议格式

协议头

简写

命令码

说明

SOH

0x01

128字节数据长度,整帧长度133字节

STX

0x02

1024字节数据长度,整帧长度1029字节

EOT

0x04

文件传输结束

ACK

0x06

正确接收应答

NAK

0x15

重传当前数据包

CAN

0x18

连续发送5条此命令取消传输

CC

0x43

字符C

握手信息

握手是时首先Client端向Server端发送字符“C”(ASCII码“43”)

Server端接收后发送起始帧,格式如下

帧头

包号

包号反码

文件名称

文件大小

填充区

校验高位

校验低位

SOH

0x00

0xff

File name+0x00

File size+0x00

NULL(0x00)

CRC-H

CRC-L

数据包

Client端接收起始帧并解码获得文件名称与文件大小后向Server端发送ACK

Server端接收ACK后回复第一帧数据,此时第一帧数据包号为1,数据格式如下

帧头

包号

包号反码

有效数据

校验高位

校验低位

SOH

PN

XPN

128字节DATA

CRC-H

CRC-L

如果使用STX模式发送,数据则为1024字节。

Client端正确接收后回复ACK,Server端回复第二帧数据。

之后数据包以此类推。

此处有些疑惑,在使用Xshell作为Server端测试STM32的Client端时此处发送ACKCC+ACK均可接收正确回复

包号由于是一字节,所以范围为0-255,若数据包总数超过255则包号到达255后归零重新开始计数。

Client户端接收数据包后CRC校验出错,可向Server端恢复NAK,Server端接收NAK后会重发此次数据包。

在Server端发送至最后一帧数据,若此时数据不足128字节或1024字节,则用0x1A补齐剩余字节数,使数据对齐。

此时Client端在接收完成最后一帧数据后若继续发送ACK,Server端则恢复EOT(文件发送结束)。

结束帧

结束帧为133字节长度,格式如下

帧头

包号

包号反码

数据区

校验高位

校验低位

SOH

0x00

0xff

NULL(0x00)

CRC-H

CRC-L

此处疑惑,在使用Xshell作为Server测试时,结束帧可以跳过,在Client接收到EOT之后直接连发6个CAN,Xshell也会默认Client文件接收完成。而使用Xshell作为Client时则Client接收EOT帧之后会发送一次NAK再次恢复EOT之后Client会回复一次CC+ACK,此时Server向Client发送结束帧后Client判断接收完成,向Server发送六个CAN

Client接收文件发送结束后连续发送6个CAN至Server表示接收完成,断开接收,Server接收后判断发送完成。

CRC校验

Ymodem协议所用的CRC公式与Xmodem一致,为0x1021,C语言写法如下

data为数据(除去帧头,包号、包号反码、帧尾高低校验位),len为长度

static uint16_t Ymodem_CRC16(const uint8_t* data, uint16_t len) {
    uint16_t crc = 0;

    for (uint16_t i = 0; i < len; i++) {
        crc ^= (uint16_t)data[i] << 8;
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x8000) {
                crc = (crc << 1) ^ 0x1021;
            } else {
                crc <<= 1;
            }
        }
    }
    return crc;
}

取高低位

crc_buf//crc计算结果
(uint8_t)(crc_buf>>8)//CRC_H
(uint8_t)crc_buf//CRC_L
Ymodem接收部分代码

只写了128字节模式,1024字节模式可以自己补充,这里懒得写了🤣

//YMODEM协议处理
void Ymodem(const uint8_t *data) {
    uint8_t data_buf[1030];
    memcpy(data_buf,data,sizeof(data_buf));
    switch(YMODEM_FLAG) {
    case 0: { //处理第0帧数据
        uint8_t *p = &data_buf[3];
        uint8_t num = 0;
        if((data_buf[0] == SOH) && (data_buf[1] == 0)) {
            //CRC校验
            uint16_t crc_buf = Ymodem_CRC16(p,128);
            debug_print("%02X\t%02X\t%04X\n",p[128],p[129],crc_buf);
            if((p[128] != (uint8_t)(crc_buf>>8)) && (p[129] != (uint8_t)crc_buf)) {
                debug_print("CRC ERROR\n");
                HAL_Delay(5);
                Ymodem_Send_Data(NAK);//校验失败重传当前包
                goto ERROR;
            }
            for(uint16_t i = 0; i<128-3; i++) { //获取文件名
                if(data_buf[i] == 0) { //文件名结束,跳出循环
                    break;
                }
                if(i<sizeof(file_name)/sizeof(file_name[0])) { //超出缓存长度进行截断处理
                    file_name[i] = p[i];
                }
                num = i;
            }
            num ++;
            for(uint16_t i = 0; i<128-num; i++) { //获取文件大小
                if(p[num+i] == 0) { //文件大小结束
                    break;
                }
                file_len[i] = p[num+i];
            }
            YMODEM_FLAG = 1;
            Ymodem_Send_Data(ACK);//处理完成发送应答信号
            Ymodem_Send_Data(CC);
            break;
        } else {
            Ymodem_Send_Data(CC);
        }
        break;
    }
    case 1: { //处理数据帧
        if(data_buf[0] == SOH) { //128字节数据类型
            uint8_t *p = &data_buf[3];
            uint8_t data[130] = {0};
            //CRC校验
            uint16_t crc_buf = Ymodem_CRC16(p,128);
            debug_print("%02X\t%02X\t%04X\n",p[128],p[129],crc_buf);
            if((p[128] != (uint8_t)(crc_buf>>8)) && (p[129] != (uint8_t)crc_buf)) {
                debug_print("CRC ERROR\n");
                HAL_Delay(10);
                Ymodem_Send_Data(NAK);//校验失败重传当前包
                goto ERROR;
            }
            for(uint8_t i = 0; i<128; i++) {
                data[i] = p[i];
            }
						//写flash
            Write_Flash_Data(addr,data,128);
						
            HAL_Delay(1);
            Ymodem_Send_Data(ACK);//处理完成发送应答信号
            break;
        } else if(data_buf[0] == STX) { //1024字节数据类型
//            uint8_t *p = &data_buf[3];
//            //CRC校验
//            uint16_t crc_buf = Ymodem_CRC16(p,1024);
//            debug_print("%02X\t%02X\t%04X\n",p[1023],p[1024],crc_buf);
//            if((p[1023] != (uint8_t)(crc_buf>>8)) && (p[1024] != (uint8_t)crc_buf)) {
//                debug_print("CRC ERROR\n");
//                HAL_Delay(5);
//                Ymodem_Send_Data(NAK);//校验失败重传当前包
//                goto ERROR;
//            }
//            //数据处理

//            HAL_Delay(5);
//            Ymodem_Send_Data(ACK);//处理完成发送应答信号
            break;
        } else if(data_buf[0] == CAN) { //主机取消发送
            debug_print("CANCELL\n");
            addr = FLASH_ADDR;
            YMODEM_FLAG = 0;
            break;
        } else if((data_buf[0] == EOT)) {//回复文件接收结束
            for(uint8_t i = 0; i<=5; i++) {
                HAL_Delay(5);
                Ymodem_Send_Data(CAN);
            }
            addr = FLASH_ADDR;
            Write_Config(UPDATE_FLAG);
            printf("[REST START]\r\n");
            HAL_NVIC_SystemReset();
            YMODEM_FLAG = 0;
            break;
        }
        Ymodem_Send_Data(NAK);//校验失败重传当前包
        break;
    }
    default: {

        break;
    }

    }
ERROR:
    return;
}

写Flash

协议接收后每接收一次数据直接写入flash分区进行缓存。全部接收完成后在flash的config分区写入升级标志(此处应该要进行完整性校验的,偷懒没写),之后进行软重启,接下来的搬运分区覆盖掉原app分区交给bootloader进行完成。此篇只写了app内程序,bootloader就很简单了,挖个坑放下篇文章再写🫠

/*
 * @Author: Memory 1619005172@qq.com
 * @Date: 2023-06-06 15:03:55
 * @LastEditors: Memory 1619005172@qq.com
 * @LastEditTime: 2023-06-06 15:05:23
 * @FilePath:
 * @Description:
 */
#include "update.h"
#define TGA "UPDATE"


uint8_t file_name[200] = {0};
uint8_t file_len[50] = {0};

uint32_t addr = FLASH_ADDR;


static FLASH_SATAE_T Erase_Flash(uint32_t addr,uint8_t num);
static uint32_t GetPage(uint32_t Addr);
static uint16_t Ymodem_CRC16(const uint8_t* data, uint16_t len);

FLASH_SATAE_T Update_Init(void) {
    HAL_FLASH_Unlock();//解锁flash
    if(Erase_Flash(FLASH_ADDR,ERASE_LEN/2) != FLASH_OK) { //擦除flash
        goto ERROR;
    }
    HAL_FLASH_Lock();
    return FLASH_OK;
ERROR:
    HAL_FLASH_Lock();
    return FLASH_ERROR;
}

FLASH_SATAE_T Write_Flash_DoubleWord(uint32_t addr,uint64_t data) {
    HAL_FLASH_Unlock();
    if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,addr,data) != HAL_OK) {
        debug_print("WRITE FLASH ERROR\n");
        goto ERROR;
    }
    HAL_FLASH_Lock();
    return FLASH_OK;
ERROR:
    HAL_FLASH_Lock();
    return FLASH_ERROR;
}

FLASH_SATAE_T Write_Config(uint64_t data) {
    HAL_FLASH_Unlock();//解锁flash
    if(Erase_Flash(CONFIG_ADDR,1) != FLASH_OK) { //擦除flash
        goto ERROR;
    }
    if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,CONFIG_ADDR,data) != HAL_OK) {
        debug_print("WRITE FLASH ERROR\n");
        goto ERROR;
    }
    debug_print("SUCCESS\n");
ERROR:
    HAL_FLASH_Lock();//上锁
    return FLASH_ERROR;
}

static FLASH_SATAE_T Erase_Flash(uint32_t addr,uint8_t num) {
    uint32_t SectorError=0;

    UPDATE_FLASH.TypeErase = FLASH_TYPEERASE_PAGES;
    UPDATE_FLASH.Page = GetPage(addr);
    UPDATE_FLASH.NbPages = num;
    if(HAL_FLASHEx_Erase(&UPDATE_FLASH,&SectorError) != HAL_OK) {
        debug_print("ERASE ERROR %x\n",SectorError);
        goto ERROR;
    }
    debug_print("ERASE SUCCESS %x\n",SectorError);
    return FLASH_OK;
ERROR:
    return FLASH_ERROR;
}

static uint32_t GetPage(uint32_t Addr)
{
    uint32_t page = 0;
    page = (Addr-FLASH_BASE) / FLASH_PAGE_SIZE;
    return page;
}

FLASH_SATAE_T Write_Flash_Data(uint32_t address, uint8_t *buf, uint32_t length)
{
    uint32_t i = 0;
    uint32_t start_address = address;
    uint64_t data = 0;
    uint32_t data1 = 0, data2 = 0;
    //长度为0时直接返回
    if(length == 0)
    {
        goto ERROR;
    }
    //长度非8字节倍数时则补齐新的8字节
    if(length%8 != 0)
    {
        length = (length/8)*8 + 8;
    }

    for (i=0; i<(length/8); i++)
    {
        //小端格式存放
        data1 = buf[i*8 + 0];
        data1 |= buf[i*8 + 1] << 8;
        data1 |= buf[i*8 + 2] << 16;
        data1 |= buf[i*8 + 3] << 24;
        data2 = buf[i*8 + 4];
        data2 |= buf[i*8 + 5] << 8;
        data2 |= buf[i*8 + 6] << 16;
        data2 |= buf[i*8 + 7] << 24;
        data = (uint64_t)data2*256*256*256*256 + (uint64_t)data1;

        Write_Flash_DoubleWord(start_address, data);
        start_address += 8;
    }
    addr = start_address;
    return FLASH_OK;
ERROR:
    return FLASH_ERROR;
}

update.h

#ifndef __UPDATE_H__
#define __UPDATE_H__

#include "comm.h"

#define JUMP_ADDR 		0x8005800		//跳转地址
#define CONFIG_ADDR		0X8005000		//升级信息存储分区
#define FLASH_ADDR		0x8012000		//Flash读取地址
#define ERASE_LEN			50					//擦除长度
#define UPDATE_FLAG   0x123456		//升级标志

#define YMODEM_UART huart1


enum {
    SOH = 0x01,
    STX = 0x02,
    EOT = 0x04,//文件传输结束
    ACK = 0x06,//正确接收
    NAK = 0x15,//重传当前包
    CAN = 0x18,//取消传输命令
    CC = 0x43,//字符C
};

FLASH_SATAE_T Update_Init(void);
FLASH_SATAE_T Write_Flash(void);
void Ymodem(const uint8_t *data);

END