STM32G070 串口 OTA 升级
由于没有外挂Flash,内存又比较小,所以在此使用内置Flash作为缓存,
G070CBT6整体flash位128K,为flash规划分区,分区表如下。
从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端时此处发送ACK或CC+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