系列文章目录
1.OTA是什么?
2.解析什么是ymode协议
3.Bootloader引导程序设计
文章目录
前言
随着物联网的快速发展,越来越多的设备需要进行固件更新和升级,以提供新功能、修复漏洞或改善性能。而传统的手动升级方式往往不仅耗时耗力,还存在一定的风险。为了解决这些问题,提供一种安全、高效、灵活的远程升级解决方案变得至关重要。至此,对于一名嵌入式工程师来说学习OTA技术是有必要的。
一、OTA是什么?
OTA全称“Over-The-Air”,即空中下载技术,早期被广泛应用在手机行业中,终结了手机软件升级需要连接电脑、下载软件、再安装更新的繁复操作。近年来,随着汽车网联技术不断发展,汽车OTA也成为了行业热词。
通过OTA技术对汽车进行远程升级,不仅可以持续为车辆改善终端功能和服务,让车主拥有更便捷、更智能的用车体验,而且还可以被用于快速修复漏洞,帮助实施汽车召回。正在逐渐成为企业解决软、硬件系统问题的重要措施。业内公认的汽车OTA最早出现是在2012年,特斯拉推出的ModesS首次采用OTA技术,更新范围涉及人机交互、自动驾驶、动力电池系统等模块,当时特斯拉可以通过OTA完成钥匙卡漏洞、提升续航里程、提高最高速度、提升乘坐舒适度等,让车的功能迭代更加灵活和便捷。后来,OTA技术开始被丰田、福特、大众、宝马等传统车企所尝试。期间,国内的蔚来、理想、小鹏、上汽、比亚迪等也陆续推出了可以实现部分功能或整车OTA的车型。
具体详细的内容请自行问度娘。过于细节的内容我在此不再赘述。
二、ymodem是什么?
首先要想学习OTA更新的技术内容,我们首先来了解一个技术协议------ymodem
2.1 ymodem协议简介
ymodem协议是一个文件传输协议,由ChuckForsberg于上世纪90年代开发完成,通常用于资源受限的设备。
xmodem、ymodem和zmodem协议是最常用的三种通信协议。ymodem协议是由xmodem协议演变而来的,是一种发送并等待的协议,即发送方发送一个数据包以后,都要等待接收方的确认。如果是ACK信号,则可以发送新的包。如果是NAK信号,则重发或者错误退出。ymodem-1k用1024字节信息块传输取代标准的128字节传输,每包数据可以达到1024字节,是一个非常高效的文件传输协议,所用到的符号如下。
#define MODEM_SOH 0x01 //数据块起始字符
#define MODEM_STX 0x02 //1028字节开始
#define MODEM_EOT 0x04 //文件传输结束
#define MODEM_ACK 0x06 //确认应答
#define MODEM_NAK 0x15 //出现错误
#define MODEM_CAN 0x18 //取消传输
#define MODEM_C 0x43 //大写字母
2.2 ymodem帧格式
2.2.1 ymodem协议的传输过程
如下图所示:
开启是由接收方开启传输,它发一个大写字母C(0x43)开启传输。然后进入等待SOH(0x01)状态,如果没有回应,就会超时退出。发送方开始时处于等待过程中,等待C。收到C以后,发送(SOH)数据包开始信号,发送序号(00),补码(FF),“文件名”,“\0”“文件大小”“除去序号外,补满128字节”,16位CRC校验两个字节,高字节在前,低字节在后。进入等待(ACK)状态。
内容示例:SOH 00 FF Foo.bin NUL[123] CRC CRC
接收方收到以后,CRC校验满足,则发送ACK。发送方接收到ACK,又进入等待“文件传输开启”信号,即重进入等待“C”的状态。发送接收到“C”以后,发送第一个数据包,(SOH) + (01序号) + (FE补码) + (128位数据) +(CRC校验),或者(STX) + (01序号) + (FE补码) + (1024位数据) + (CRC校验),不满128或者1024,用0x00补齐,等待接收方“ACK”。
内容示例:STX 01 FE data[1024] CRC CRC
文件发送完以后,发送方发出一个“EOT”信号,接收方也以“ACK”回应。然后接收方会再次发出“C”开启另一次传输,若接着发送方会发出一个“全0数据包”,接收方“ACK”以后,本次通信正式结束。
2.2.2 起始帧的数据格式ymodem的起始帧并不直接传输文件的数据,而是将文件名与文件的大小放在数据帧中传输,它的帧长=3字节数据首部+128字节数据+2字节CRC16校验码=133字节。
它的数据结构如下:
SOH 00 FF foo.c 3232 NUL[118] CRCH CRCL
- SOH:表示本帧数据块大小为128字节
- 00: 表示数据帧序号,初始是0,依次向下递增,FF是帧序号的取反
- foo.c:是要传输的文件名,是ASCII字符串(以空字符结尾)
- 3232:表示文件的大小,是ASCII字符串(以空字符结尾)
- NUL[118]:剩余部分用空字符填充
- CRCH/L: 表示16位CRC校验码的高8位与低8位
2.3 数据帧的数据格式
ymodem的数据帧的数据块大小可以是128字节或者1024字节。
// 128字节的数据块
SOH 01 FE data[128] CRCH CRCL
// 1024字节的数据块
STX 01 FE data[1024] CRCH CRCL
一般会使用1024字节的数据块进行传输,这样可以加快传输速度,如果最后文件数据不足1024字节,则将其拆分为128字节的数据块进行传输,如果拆分后有不足128字节的数据依然按照128字节的数据块进行传输,但是剩余空间全部用0x1A填充,以表示文件结束。
2.4 结束帧数据结构
当文件传输结束时,除了发送EOT传输结束指令外,还需要发送一个结束
帧。ymodem的结束帧与起始帧的数据格式相同,数据块大小为128字节,但是
结束帧的数据块要全用空字符填充。
SOH 3A C5 NUL[128] CRCH CRCL
2.5 crc16函数
CRC即循环冗余校验码:是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。循环冗余检查(CRC)是一种数据传输检错功能,对数据进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。
/**
* @bieaf CRC-16 校验
*
* @param addr 开始地址
* @param num 长度
* @param num CRC
* @return crc 返回CRC的值
*/
#define POLY 0x1021
uint16_t crc16(uint8_t *addr, int32_t num, uint16_t crc)
{
int32_t i;
for (; num > 0; num--) /* Step through bytes in memory */
{
crc = crc ^ (*addr++ << 8); /* Fetch byte from memory, XOR into CRC top byte*/
for (i = 0; i < 8; i++) /* Prepare to rotate 8 bits */
{
if (crc & 0x8000) /* b15 is set... */
crc = (crc << 1) ^ POLY; /* rotate and XOR with polynomic */
else /* b15 is clear... */
crc <<= 1; /* just rotate */
} /* Loop for 8 bits */
crc &= 0xFFFF; /* Ensure CRC remains 16-bit value */
} /* Loop until num=0 */
return(crc); /* Return updated CRC */
}
2.6 ymodem传输大小选择
在SecureCRT中,ymodem默认为128字节数据包大小,如下图。当然亦可选择为1024字节(Xmodem-1k/Ymodem-1k)
3.流程代码
#include "stm32f4xx.h"
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
#include "flash.h"
#include "key.h"
#include "ymodem.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/**
* @bieaf CRC-16 校验
*
* @param addr 开始地址
* @param num 长度
* @param num CRC
* @return crc 返回CRC的值
*/
#define POLY 0x1021
uint16_t crc16(uint8_t *addr, int32_t num, uint16_t crc)
{
int32_t i;
for (; num > 0; num--) /* Step through bytes in memory */
{
crc = crc ^ (*addr++ << 8); /* Fetch byte from memory, XOR into CRC top byte*/
for (i = 0; i < 8; i++) /* Prepare to rotate 8 bits */
{
if (crc & 0x8000) /* b15 is set... */
crc = (crc << 1) ^ POLY; /* rotate and XOR with polynomic */
else /* b15 is clear... */
crc <<= 1; /* just rotate */
} /* Loop for 8 bits */
crc &= 0xFFFF; /* Ensure CRC remains 16-bit value */
} /* Loop until num=0 */
return(crc); /* Return updated CRC */
}
/* 设置升级的步骤 */
static enum UPDATE_STATE update_state = TO_START;
void ymodem_set_state(enum UPDATE_STATE state)
{
update_state = state;
}
/* 查询升级的步骤 */
uint8_t ymodem_get_state(void)
{
return update_state;
}
/* 发送指令 */
void ymodem_send_cmd(uint8_t command)
{
USART_SendData(USART1,command);
//等待数据发送成功
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
USART_ClearFlag(USART1,USART_FLAG_TXE);
delay_ms(10);
}
/* 标记升级完成 */
void update_set_down(void)
{
uint32_t update_flag = 0xAAAAAAAA; // 对应bootloader的启动步骤
flash_program((APPLICATION_2_ADDR + APPLICATION_2_SIZE - 4), &update_flag,1 );
}
/**
* @bieaf ymodem下载
*
* @param none
* @return none
*/
void ymodem_download(void)
{
uint16_t crc = 0;
static
uint8_t data_state = 0;
if(ymodem_get_state()==TO_START)
{
ymodem_send_cmd(CCC);
delay_ms(1000);
}
/* 串口1接收完一个数据包 */
if(g_usart1_rx_end)
{
/* 清空接收完成标志位、接收计数值 */
g_usart1_rx_end=0;
g_usart1_rx_cnt=0;
switch(g_usart1_rx_buf[0])
{
case SOH://数据包开始
{
crc = 0;
/* 计算crc16 */
crc = crc16((uint8_t *)&g_usart1_rx_buf[3], 128, crc);
if(crc != (g_usart1_rx_buf[131]<<8|g_usart1_rx_buf[132]))
return;
if((ymodem_get_state()==TO_START)&&(g_usart1_rx_buf[1] == 0x00)&&(g_usart1_rx_buf[2] == (uint8_t)(~g_usart1_rx_buf[1])))// 开始
{
ymodem_set_state(TO_RECEIVE_DATA);
/* 若ymodem_send_cmd执行在sector_erase之前,则导致串口数据丢包,因为擦除会关闭所有中断 */
/* 擦除应用程序2的扇区 */
sector_erase(APPLICATION_2_SECTOR);
data_state = 0x01;
ymodem_send_cmd(ACK);
ymodem_send_cmd(CCC);
}
else if((ymodem_get_state()==TO_RECEIVE_END)&&(g_usart1_rx_buf[1] == 0x00)&&(g_usart1_rx_buf[2] == (uint8_t)(~g_usart1_rx_buf[1])))// 结束
{
update_set_down();
ymodem_set_state(TO_START);
ymodem_send_cmd(ACK);
/* 嘀一声示,表示下载完成 */
beep_on();delay_ms(80);beep_off();
/* 复位 */
NVIC_SystemReset();
}
else if((ymodem_get_state()==TO_RECEIVE_DATA)&&(g_usart1_rx_buf[1] == data_state)&&(g_usart1_rx_buf[2] == (uint8_t)(~g_usart1_rx_buf[1])))// 接收数据
{
/* 烧录程序 */
flash_program((APPLICATION_2_ADDR + (data_state-1) * 128), (uint32_t *)(&g_usart1_rx_buf[3]), 32);
data_state++;
ymodem_send_cmd(ACK);
}
}break;
case EOT://数据包传输结束
{
if(ymodem_get_state()==TO_RECEIVE_DATA)
{
ymodem_set_state(TO_RECEIVE_EOT2);
ymodem_send_cmd(NACK);
}
else if(ymodem_get_state()==TO_RECEIVE_EOT2)
{
ymodem_set_state(TO_RECEIVE_END);
ymodem_send_cmd(ACK);
ymodem_send_cmd(CCC);
}
}break;
default:break;
}
}
}
三、Bootloader引导程序设计
1.编写代码
/**
* @bieaf 进行程序的覆盖
* @detail 1.擦除目的地址
* 2.源地址的代码拷贝到目的地址
* 3.擦除源地址
*
* @param 搬运的源地址
* @param 搬运的目的地址
* @return 搬运的程序大小
*/
void move_code(uint32_t dest_addr, uint32_t src_addr,uint32_t word_size)
{
uint32_t temp[256];
uint32_t i;
/*1.擦除目的地址*/
printf("> start erase application 1 sector......\r\n");
//擦除
sector_erase(APPLICATION_1_SECTOR);
printf("> erase application 1 success......\r\n");
/*2.开始拷贝*/
printf("> start copy......\r\n");
for(i = 0; i <word_size/1024; i++)
{
flash_read((src_addr + i*1024), temp, 256);
flash_program((dest_addr + i*1024), temp, 256);
}
printf("> copy finish......\r\n");
/*3.擦除源地址*/
printf("> start erase application 2 sector......\r\n");
//擦除
sector_erase(APPLICATION_2_SECTOR);
printf("> erase application 2 success......\r\n");
}
2.程序跳转
2.1程序跳转前需要用汇编进行重新设置MSP(main stack pointer)。
/* 采用汇编设置栈的值 */
__asm void MSR_MSP (uint32_t ulAddr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}
函数体中的__asm表示这是一段内嵌汇编代码。MSR MSP, r0将r0寄存器中的值设置为主栈指针(MSP)的值,即将栈地址设置为ulAddr。BX r14则是一个分支指令,将控制权返回到调用该函数的地址。
通过使用这段汇编代码,可以在ARM Cortex-M系列处理器上设置主栈值该处使用的url网络请求的数据。
2.2程序跳转需要使用函数指针,预先提前定义。
typedef void(*jump_func)(void);
2.3.执行程序覆盖
void iap_execute_app (uint32_t app_addr)
{
jump_func jump_to_app;
//printf("* ( __IO uint32_t * ) app_addr =%08X ,app_addr=%08X\r\n",* ( __IO uint32_t * ) app_addr,app_addr );
//if ( ( ( * ( __IO uint32_t * ) app_addr ) & 0x2FFE0000 ) == 0x200006B0 ) //检查栈顶地址是否合法.
//{
//printf("stack is legal\r\n");
jump_to_app = (jump_func) * ( __IO uint32_t *)(app_addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP( * ( __IO uint32_t * ) app_addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump_to_app(); //跳转到APP.
//}
//printf("stack is illegal\r\n");
}
栈顶:栈顶=IRAM起始地址+RW-data大小+ZI-data大小,当程序成功编译后,
输出显示以下信息:
若IRAM起始地址为0x20000000,则栈顶地址
= 0x20000000+464 + 72 =0x200001D0。
3 应用程序
3.1 工程配置
在【Target】标签页,重新配置IROM的起始地址和大小,目前该应用代码存储在扇区5,则起始地址为0x08020000,大小为128KB,即0x20000,如下图。
3.2 在【Linker】标签页,R/O Base设置为扇区5的起始地址0x08020000。
3.3 在【User】标签页中,在“After Build/Rebuild”中勾选口Run #1,并增加“fromelf -
-bin --output .\Objects\demo.bin .\Objects*.axf”,生成bin文件,可用于ymodem下
载。
详细代码过程可以上本人githup上自行借鉴:https://github.com/201697suxi/OTA