学习依据的源文链接:STM32 OTA应用开发——通过USB实现OTA升级
前言
在《STM32 OTA Bootloader部分 demo流程学习》一文中,梳理了OTA过程在Bootloader中的逻辑流程,这篇将梳理APP部分在OTA过程的处理;
需要注意的是,这份demo里面,Bootloader部分主要是进行APP跳转(无需升级)和Download分区程序覆盖写入APP分区(有升级需求)的功能,而将新APP传输到Download分区这部分工作放到了APP部分,所以主要是对这部分进行梳理;
APP的OTA功能工作原理
APP运行起来后,需要连接USB,等待上位机发送升级命令,如果收到对应的升级命令,则进入下载模式;demo当中采用USB连接PC,实际实现可能会使用UART替代USB;
考虑到适用性,USB传输固件的方式采用Ymodem协议,可以适配很多上位机的终端程序(Xshell,secureCRT等等),当然,也可以自定义协议;
如上图是APP部分处理OTA过程的流程图,后面重点分析demo是如何通过Ymodem协议传输固件的,以及Ymodem过程的状态机;
YModem协议简介
Ymodem协议是一种数据传输协议,可以用于在计算机之间传输文件。它是Xmodem协议的改进版,支持传输更大的文件和更高的传输速度。Ymodem协议将文件分成128字节的块,每个块都有一个序号和校验号,以确保数据的完整性。传输过程中,接收方会发送一个ACK信号来确认每个块是否已经正确接收。如果出现错误,发送方会重新发送该块。Ymodem协议还支持批处理传输,可以将多个文件打包成一个压缩文件进行传输。
这样看这个协议有些抽象,举个实际通信的例子,例如MCU通过UART与PC上位机终端对接,MCU接收数据,PC发送数据,两方的动作:
- MCU向PC发送一个C字符,请求文件传输。
- PC收到启动命令后,通过Xshell等终端工具集成的Ymodem协议向MCU发送数据包。
- PC将发送的文件分为若干个带有SOH或STX的数据包,分次向MCU发送。
- MCU每收到一个SOH或STX的数据包,需要向PC端返回ACK。
- PC端要收到ACK后才会发送下一个数据包,直到发送完最后一个数据块,收到MCU的ACK后,向MCU发送EOT表示发送结束。
- MCU收到EOT后需要返回一个ACK。
需要注意的是,C字符,ACK,EOT等信息,在Ymodem协议当中都是规定的一个转义字符,具有特定含义,以下列举协议当中的转义字符:
- SOH(Start of Header):表示一个数据块的开始,其值为0x01。
- STX(Start of Text):表示一个数据块的开始,其值为0x02,与SOH的区别在于STX支持1024字节的数据块,而SOH只支持128字节。
- EOT(End of Transmission):表示文件传输结束,其值为0x04。
- ACK(Acknowledgement):表示接收方已经成功接收到一个数据块,其值为0x06。
- NAK(Negative Acknowledgement):表示接收方接收数据出现错误,需要重新发送数据,其值为0x15。
- CAN(Cancel):表示终止文件传输,其值为0x18。
- CRC(Cyclic Redundancy Check):表示数据块的校验和,其值为两个字节的校验和值。
上述的字符值都是与ASCII码对应的;
源码解析
main流程
demo当中的main.c除开初始化系统时钟,USB设备,打印APP信息这些常规操作以外,主要是执行了ymodem_init()和循环执行ymodem_handle();
其中,ymodem_init()主要是初始化了定时器和一个接收队列(ymodem.c line 216),定时器和接收队列在后面的接收数据流程中会用到;
ymodem_handle()当中,针对三个状态做处理:
START_PROGRAM:没有收到升级命令,继续执行原本程序
UPDATE_PROGRAM:收到升级命令,通过USB向上位机发送一个字符C,表示可以进行Ymodem传输;
UPDATE_SUCCESS:升级完成后,重置Setting区域的标志,然后重启MCU,后面又会从Bootloader启动,在Bootloader中检测到Setting区域的标志变化,再执行新旧固件的擦除搬运(Bootloader这部分的处理流程见《STM32 OTA Bootloader部分 demo流程学习》源码解析中的第5点);
接收数据流程
- 首先是usb_rxdata_handle()接口和TIM3_IRQHandler()接口,分别是USB中断接收信息和TIM3定时器中断执行函数;前者是用来接收来自PC机的信息和固件数据,接收到之后,会使能TIM3的定时,到达定时后产生UPDATE定时中断,跳转到后者函数再执行之后的数据处理操作;
- 在TIM3_IRQHandler()接口当中,检查接收队列当中是否有信息(ymodem.c line 268),如果有,就代表上位机发送了消息过来,之后再将接收队列(rx_queue)中的内容出队到接收缓存(recvBuf.data)当中
- 再根据接收到的数据长度,进入到ymodem_recv()接口当中进行处理;
注:之所以通过队列---定时器这种方式来接收上位机的数据,是为了给接收数据提供一层缓存,可以保证接收数据的顺序性和可靠性;
YModem数据处理流程
因为YModem协议规定,发送来的数据包第一个byte是特殊转义字符SOH或者STX,所以在ymodem_recv()接口中对接收到的第一个byte进行判断就能知道数据发送的步骤;
- 在demo中,原作者设定为,字符‘1’作为升级命令,可以看到在ymodem_recv()接口中对字符‘1’的处理(ymodem.c line 128)
- 接收到升级命令字符‘1’后,将状态置为UPDATE_PROGRAM,这样就可以跳转到main流程中的UPDATE_PROGRAM状态处理过程,向上位机每隔一秒发送一个字符C,表示可以进行Ymodem传输;
- PC上位机收到字符C后就可以向MCU发送固件数据包了,这个时候ymodem.status仍是初始状态0,还是进入case 0的流程,代表第一个数据包的处理,收到SOH后确定是Ymodem的数据包,擦除0x08012000地址为起始的Download分区数据,然后向上位机发送ACK和字符C,ymodem.status自增
- 进入case 1的流程,接收固件数据包的数据块,并且将SOH或STX的数据块写入Download分区,一旦收到EOT,表示固件已经传输完成后,MCU向PC返回一个NACK要求重发,ymodem.status自增;
- 进入case 2的流程,确认EOT后,返回ACK和C;
- 根据返回的C收到PC的最后一个SOH,返回ACK,标志着新固件包的接收完毕,ymodem_recv状态机复位;将状态置为UPDATE_SUCCESS,跳转到main流程中的UPDATE_SUCCESS状态处理过程;
注:demo这里在收到EOT之后返回ACK的同时,又发送了一个C,启动了下一次传输,收到的SOH属于下一次传输的头数据包,这里的处理流程存疑;
附:
ymodem.c
#include "ymodem.h"
#include "flash.h"
#include "SysTick.h"
#include "hw_config.h"
ymodem_t ymodem = {START_PROGRAM, 0, 0, APP_SECTOR_ADDR, 0, {0}};
download_buf_t recvBuf;
seq_queue_t rx_queue;
// 初始化queue_initiate(Q)
void queue_initiate(seq_queue_t *Q)
{
Q->rear = 0;
Q->front = 0;
Q->count = 0;
}
// 非空否queue_not_empty(Q)
//判断循环队列Q非空否,非空则返回1,否则返回0
int queue_not_empty(seq_queue_t *Q)
{
if(Q->count != 0)
return 1;
else
return 0;
}
// 入队列queue_append(Q, x)
//把数据元素值x插入顺序循环队列Q的队尾,成功返回1,失败返回0
int queue_append(seq_queue_t *Q, uint8_t x)
{
if(Q->count > 0 && Q->rear == Q->front)
{
printf("queue is full ! \n");
return 0;
}
else
{
Q->queue[Q->rear] = x;
Q->rear = (Q->rear + 1) % MAX_QUEUE_SIZE;
Q->count ++;
return 1;
}
}
// 出队列 queue_delete(Q, d)
//删除顺序循环队列Q的队头元素并赋值给d,成功则返回1,失败返回0
int queue_delete(seq_queue_t *Q, uint8_t *d)
{
if(Q->count == 0)
{
// printf("queue is empty! \n");
return 0;
}
else
{ *d = Q->queue[Q->front];
Q->front = (Q->front + 1) % MAX_QUEUE_SIZE;
Q->count--;
return 1;
}
}
// 取队头数据元素 queue_get(Q, d)
int queue_get(seq_queue_t Q, uint8_t *d)
{
if(Q.count == 0)
{
// printf("queue is empty! \n");
return 0;
}
else
{
*d = Q.queue[Q.front];
return 1;
}
}
void ymodem_ack(void)
{
usb_printf("%c\r", YMODEM_ACK);
}
void ymodem_nack(void)
{
usb_printf("%c\r", YMODEM_NAK);
}
void ymodem_c(void)
{
usb_printf("%c\r", YMODEM_C);
}
void set_ymodem_status(process_status process)
{
ymodem.process = process;
}
process_status get_ymodem_status(void)
{
process_status process = ymodem.process;
return process;
}
void ymodem_start(ymodem_callback cb)
{
if (ymodem.status == 0)
{
ymodem.cb = cb;
}
}
void ymodem_recv(download_buf_t *p)
{
uint8_t type = p->data[0];
switch (ymodem.status)
{
case 0:
if (type == YMODEM_SOH)
{
ymodem.process = BUSY;
ymodem.addr = APP_SECTOR_ADDR;
mcu_flash_erase(ymodem.addr, ERASE_SECTORS);
ymodem_ack();
ymodem_c();
ymodem.status++;
}
else if (type == '1')
{
printf("enter update mode\r\n");
ymodem.process = UPDATE_PROGRAM;
}
break;
case 1:
if (type == YMODEM_SOH || type == YMODEM_STX)
{
if (type == YMODEM_SOH)
{
mcu_flash_write(ymodem.addr, &p->data[3], 128);
ymodem.addr += 128;
}
else
{
mcu_flash_write(ymodem.addr, &p->data[3], 1024);
ymodem.addr += 1024;
}
ymodem_ack();
}
else if (type == YMODEM_EOT)
{
ymodem_nack();
ymodem.status++;
}
else
{
ymodem.status = 0;
}
break;
case 2:
if (type == YMODEM_EOT)
{
ymodem_ack();
ymodem_c();
ymodem.status++;
}
break;
case 3:
if (type == YMODEM_SOH)
{
ymodem_ack();
ymodem.status = 0;
ymodem.process = UPDATE_SUCCESS;
}
}
p->len = 0;
}
void system_reboot(void)
{
__set_FAULTMASK(1);//关闭总中断
NVIC_SystemReset();//请求单片机重启
}
void ymodem_handle(void)
{
uint8_t boot_state;
process_status process;
process = get_ymodem_status();
switch (process)
{
case START_PROGRAM:
break;
case UPDATE_PROGRAM:
usb_printf("C\r\n");
delay_ms(1000);
break;
case UPDATE_SUCCESS:
boot_state = UPDATE_PROGRAM_STATE;
mcu_flash_erase(SETTING_BOOT_STATE, 1);
mcu_flash_write(SETTING_BOOT_STATE, &boot_state, 1);
printf("firmware download success\r\n");
// mcu_flash_read(SETTING_BOOT_STATE, &boot_state, 1);
// printf("boot_state:%d\n", boot_state);
// PrintTip();
printf("system reboot...\r\n");
delay_ms(2000);
system_reboot();
break;
default:
break;
}
}
void ymodem_init(void)
{
timer_init();
queue_initiate(&rx_queue);
}
void usb_rxdata_handle(uint8_t *buf, uint16_t len)
{
uint16_t i;
for(i = 0; i < len; i++)
{
queue_append(&rx_queue, buf[i]);
// printf("%02X ", buf[t]);
}
TIM3->CNT = 0;
TIM_Cmd(TIM3, ENABLE);
}
void timer_init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
//定时器TIM3初始化
TIM_TimeBaseStructure.TIM_Period = 999; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 (9999+1)*1us=10ms
TIM_TimeBaseStructure.TIM_Prescaler = 71; //设置用来作为TIMx时钟频率除数的预分频值 72M/(71+1)=1MHz 1us
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_ITConfig(TIM3,TIM_IT_Update, ENABLE); //使能指定的TIM3中断,允许更新中断
//中断优先级NVIC设置
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //从优先级1级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIMx
}
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
int result = 1;
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
TIM_Cmd(TIM3, DISABLE);
result = queue_not_empty(&rx_queue);
if(result == 1)
{
uint8_t i = 0;
recvBuf.len = 0;
do
{
result = queue_delete(&rx_queue, &recvBuf.data[i]);
if(result == 1)
{
recvBuf.len ++;
i ++;
}
}
while(result);
for (i = 0; i < recvBuf.len; i++)
{
ymodem_recv(&recvBuf);
}
}
}
}
ymodem.h
#ifndef __YMODEM_H
#define __YMODEM_H
#include "stm32f10x_conf.h"
#include "flash.h"
#include "usart.h"
#define YMODEM_SOH 0x01
#define YMODEM_STX 0x02
#define YMODEM_EOT 0x04
#define YMODEM_ACK 0x06
#define YMODEM_NAK 0x15
#define YMODEM_CA 0x18
#define YMODEM_C 0x43
#define FLASH_SECTOR_SIZE 1024
#define FLASH_SECTOR_NUM 128 // 128K
#define FLASH_START_ADDR ((uint32_t)0x08000000)
#define FLASH_END_ADDR ((uint32_t)(0x08000000 + FLASH_SECTOR_NUM * FLASH_SECTOR_SIZE))
#define APP_SECTOR_ADDR 0x08012000
#define ERASE_SECTORS ((FLASH_END_ADDR - APP_SECTOR_ADDR) / FLASH_SECTOR_SIZE) // 128k - 16 - 56 = 56k
#define MAX_QUEUE_SIZE 1200
#define SETTING_BOOT_STATE 0x08003000
#define UPDATE_PROGRAM_STATE 2
#define UPDATE_SUCCESS_STATE 3
typedef enum {
NONE,
BUSY,
START_PROGRAM,
UPDATE_PROGRAM,
UPDATE_SUCCESS
} process_status;
typedef void (*ymodem_callback)(process_status);
typedef struct {
process_status process;
uint8_t status;
uint8_t id;
uint32_t addr;
uint32_t filesize;
char filename[32];
ymodem_callback cb;
} ymodem_t;
//顺序循环队列的结构体定义如下:
typedef struct
{
uint8_t queue[MAX_QUEUE_SIZE];
int rear; //队尾指针
int front; //队头指针
int count; //计数器
} seq_queue_t;
typedef void (*jump_callback)(void);
typedef struct
{
uint8_t data[1200];
uint16_t len;
} download_buf_t;
extern seq_queue_t rx_queue;
extern download_buf_t down_buf;
void queue_initiate(seq_queue_t *Q);
int queue_not_empty(seq_queue_t *Q);
int queue_delete(seq_queue_t *Q, uint8_t *d);
void set_ymodem_status(process_status process);
process_status get_ymodem_status(void);
void ymodem_start(ymodem_callback cb);
void ymodem_recv(download_buf_t *p);
void ymodem_init(void);
void ymodem_handle(void);
void timer_init(void);
#endif