前言:ota升级是物联网开发项目中经常会用到的功能,大部分都是为了后期迭代版本,服务器对硬件设备进行远程更新使用。之前开源了ota里面bootloader的设置,本期开源ota升级固件部分代码和升级思路,(这里项目芯片为stm32f103系列)供各位大佬进行参考。
ota远程升级流程:
本次OTA升级基于物联网OTA功能,可以支持统一推送和定向推送。
升级信息会保存在后台服务器,包含:
-
- 主机ID
- 当前主机版本
- 当前设备版本
- 升级包主机版本
- 升级包设备版本
- 是否存在更新
- 升级模式
小程序可以通过升级信息获取当前版本状态,是否更新,升级模式和升级包版本。主机每次上电、推送升级、升级完成都会更新该信息。
基本升级流程为:
物联网平台推送升级包------->后台服务器------->主机------->固件设备端(开发板端)
固件设备升级:
接下来是固件设备升级的具体流程和细节供参考:(需根据项目需求进行删改)
- 主机获取版本,版本较旧,发起升级,主机就会给设备端发送对应升级信息(这里需要和主机以及服务器约束对应的升级信息格式),一旦设备端收到约定好的升级消息,立马进入复位重启,写flash标志为升级中。
- 设备重启进入bootloader,判断如果标志为升级中,则等待接收数据,如果是升级完成,写入重启次数,如果重启次数大于3,则修改跳转标志为老程序,并跳转老程序,清除标志和重启次数。
- 接收数据,写入flash,完成后校验数据是否完整。不完整则清除标志,清除重启次数,跳转老程序。完整则写flash标志为升级完成,写跳转标志为新程序。跳转新程序,重启次数加1。
- 新程序启动成功,清除标志,清除重启次数。
- 主机读取新的版本号,升级完成。
OTA远程升级实现代码:
bootloader部分:(不知道如何编写bootloader,APP1,APP2请移步OTA远程升级之bootloader无法跳转解决方法:-CSDN博客,写的非常详细)
以下是OTA升级设备侧的bootloader主程序,可根据实际项目开发需求进行删改:
#define Application1Address FLASH_APP1_START_ADDR //APP1起始地址
#define Application2Address FLASH_APP2_START_ADDR //APP2起始地址
#define APP1_Flag 0xaa550001 //APP1标志位
#define APP2_Flag 0xaa550002 //APP2标志位
#define TIMEOUT (50 * 60)//60s //超时时间
typedef void (*pFunction)(void); //函数指针
pFunction Jump_To_Application;
uint32_t JumpAddress;
uint32_t m_ticks = 0; //定时器计数
unsigned char buffer[256]; //缓冲区,用来装主机发来的数据包,这里数据包每次发64字节数据
unsigned char firmware_data[FLASH_SECTOR_SIZE]; //缓冲区,用于装从APP2拷贝到APP1的数据,每次一个扇区大小进行拷贝
extern TIM_HandleTypeDef htim1; //外部调用定时器1
void Jump2App(void) //跳转函数,用于执行bootloader到APP1和APP2之间进行跳转
{
/* Test if user code is programmed starting from address "ApplicationAddress" */
if (((*(__IO uint32_t*)Application1Address) & 0x2FFE0000 ) == 0x20000000)
{
/* Jump to user application */
JumpAddress = *(__IO uint32_t*) (Application1Address + 4);
Jump_To_Application = (pFunction) JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) Application1Address);
Jump_To_Application();
}
}
int fputc(int ch, FILE *f) //重定向函数,这里串口1(huart1)用于打印调试,串口2用于收发数据
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 20);
return ch;
}
void LoadApp2Firmware(void) //拷贝APP2到APP1
{
int i = 0, j, k, m;
printf("Copy App2 to App1 ...\n\r");
for(i = 0; i < 96; i ++)
{
flash_clear(Application1Address + i * FLASH_SECTOR_SIZE, 1); //清除APP1 flash的数据(每次清除一个扇区(2048字节))
flash_read_firmware(Application2Address + i * FLASH_SECTOR_SIZE, \ //把数据从flash读出来,每次读个一个扇区(2048字节)
firmware_data, FLASH_SECTOR_SIZE);
flash_write_firmware(Application1Address + i * FLASH_SECTOR_SIZE, \
firmware_data, FLASH_SECTOR_SIZE); //每次写入一个扇区的数据到APP1
}
printf("load app2 data to app1 sucess!\n\r");
}
void clear_remove_devs(void) //清除所有配置的外设,避免跳转不成功
{
__HAL_RCC_TIM1_CLK_DISABLE();
HAL_NVIC_DisableIRQ(TIM1_UP_TIM10_IRQn);
HAL_TIM_Base_DeInit(&htim1);
HAL_TIM_Base_DeInit(&htim2);
HAL_UART_DeInit(&huart1);
HAL_UART_DeInit(&huart2);
__HAL_RCC_GPIOD_CLK_DISABLE();
__HAL_RCC_GPIOA_CLK_DISABLE();
}
unsigned char secure_GetChecksum(unsigned char *ptr, int len) //用来每次从主机发来的数据包是否和接收到的一致(可参考crc校验)
{
int i;
unsigned char checkSum = 0;
for (i = 0; i < len; i ++)
checkSum ^= ptr[i];
checkSum = ~checkSum;
return checkSum;
}
以上代码部分是bootloader主函数会用到的函数,这里依次分析对应函数的作用。外设配置部分不做展示,根据自己项目需求进行配置然后调用就行。
void Jump2App(void):执行跳转函数,这里我们默认跳转到APP1执行;
int fputc(int ch, FILE *f):重定向函数,支持printf;
void LoadApp2Firmware(void):拷贝APP2的数据到APP1,每次一个扇区大小的数据进行拷贝,这里我们是默认最大拷贝数据为96个扇区,可根据实际需要拷贝的数据进行修改;
void clear_remove_devs(void):清除配置外设,根据项目实际进行配置的外设清除即可;
unsigned char secure_GetChecksum(unsigned char *ptr, int len):校验数据包函数;
flash下载部分代码:
可根据项目需求进行删改
static uint32_t Address = 0;
static char flash_write_flag = 0;
uint32_t initalData = 0x55aa55aa;
void flash_clear(unsigned int start, int blocks)
{
FLASH_EraseInitTypeDef f;
uint32_t PageError = 0;
HAL_FLASH_Unlock();
f.TypeErase = FLASH_TYPEERASE_PAGES;
f.PageAddress = start;
f.NbPages = blocks;
HAL_FLASHEx_Erase(&f, &PageError);
HAL_FLASH_Lock();
}
int flash_write(unsigned int start, unsigned char *p_data, int size)
{
int i;
FLASH_EraseInitTypeDef f;
uint32_t PageError = 0;
uint32_t start_addr = start;
uint32_t readData, *writeData = (uint32_t *)p_data;
flash_clear(start, 1);
HAL_FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, start_addr, initalData);
readData = *(__IO uint32_t*)(start_addr);
if(readData != initalData)
{
printf("[FLASH] Write Flash Error !! 1\n\r");
HAL_FLASH_Lock();
return -1;
}
start_addr += 4;
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, start_addr, size);
readData = *(__IO uint32_t*)(start_addr);
if(readData != size)
{
printf("[FLASH] Write Flash Error !! 2\n\r");
HAL_FLASH_Lock();
return -1;
}
start_addr += 4;
for(i = 0; i < size / 4; i ++)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, start_addr, *writeData);
readData = *(__IO uint32_t*)(start_addr);
if(readData != *writeData)
{
printf("[FLASH] Write Flash Error !! 3 %x %x\n\r", readData, *writeData);
HAL_FLASH_Lock();
return -1;
}
start_addr += 4;
writeData ++;
}
HAL_FLASH_Lock();
printf("[FLASH] Write Flash OK !! \n\r");
return 0;
}
int flash_read(unsigned int start, uint32_t *p_data, int *size)
{
int i;
uint32_t start_addr = start;
uint32_t cur_addr = start;
uint32_t *readData = (uint32_t *)p_data;
if(*(__IO uint32_t*)(start_addr) != initalData)
{
printf("[FLASH] No Valid Data in Flash !! \n\r");
return -1;
}
start_addr += 4;
*size = *(__IO uint32_t*)(start_addr);
cur_addr = start_addr + 4;
for(i = 0; i < *size / 4; i ++)
{
*readData = *(__IO uint32_t*)(cur_addr);
cur_addr += 4;
readData ++;
}
printf("[FLASH] Read Flash OK !! \n\r");
return 0;
}
//小端转大端
int little2big(int le) {
return (le & 0xff) << 24
| (le & 0xff00) << 8
| (le & 0xff0000) >> 8
| (le >> 24) & 0xff;
}
//大端转小端
int big2little(int be)
{
return ((be >> 24) &0xff )
| ((be >> 8) & 0xFF00)
| ((be << 8) & 0xFF0000)
| ((be << 24));
}
int flash_write_firmware(unsigned int start, unsigned char *p_data, int size)
{
int i;
FLASH_EraseInitTypeDef f;
uint32_t PageError = 0, tmpdata;
uint32_t start_addr = start;
uint32_t readData, *writeData = (uint32_t *)p_data;
HAL_FLASH_Unlock();
for(i = 0; i < size / 4; i ++)
{
tmpdata = little2big(*writeData);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, start_addr, tmpdata);
readData = *(__IO uint32_t*)(start_addr);
if(readData != tmpdata)
{
printf("[FLASH] Write Flash Error !! 3 %x %x\n\r", readData, tmpdata);
HAL_FLASH_Lock();
return -1;
}
start_addr += 4;
writeData ++;
}
HAL_FLASH_Lock();
// printf("[FLASH] Write Flash OK !! \n\r");
return 0;
}
int flash_read_firmware(unsigned int start, unsigned char *pdata, int size)
{
unsigned int count = 0;
unsigned int read_data;
uint32_t start_addr = start;
while (start_addr < start + size)
{
read_data = *(__IO uint32_t*)(start_addr);
*((unsigned char *)pdata + count + 3) = (read_data >> 24) & 0xff;
*((unsigned char *)pdata + count + 2) = (read_data >> 16) & 0xff;
*((unsigned char *)pdata + count + 1) = (read_data >> 8) & 0xff;
*((unsigned char *)pdata + count + 0) = read_data & 0xff;
start_addr = start_addr + 4;
count = count + 4;
}
return 0;
}
main.c代码:
int main(void)
{
unsigned int app_active_flag = 0, size, isUpdate = 0;
/* 用到的外设初始化 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_USART2_UART_Init();
MX_IWDG_Init();
MX_TIM2_Init();
printf("Bootloader is Started !!\n\r");
if(flash_read(FLASH_UPDATE_FLAG_START_ADDR, (unsigned char *)&isUpdate, &size) < 0) //如果读取flash数据失败,把更新标志置0
isUpdate = 0;
if(isUpdate == 0) //不需要更新
{
if(flash_read(FLASH_APP_SEL_START_ADDR, (unsigned char *)&app_active_flag, &size) < 0)
app_active_flag == APP1_Flag; //如果读取校验标志位,将校验标志位赋值为APP1标志位
printf("app_active_flag %x \n\r", app_active_flag);
if(app_active_flag == APP1_Flag) //判断校验标志如果为APP1,直接清除外设,跳转到APP1(不需要更新)
{
printf("Jump to App 1 !!\n\r");
clear_remove_devs();
Jump2App();
}
else if(app_active_flag == APP2_Flag) //如果标志为APP2,则说明有更新,将APP2数据拷贝到APP1,然后跳转到APP1
{
printf("Load App 2 !!\n\r");
LoadApp2Firmware();
app_active_flag = APP1_Flag;
flash_write(FLASH_APP_SEL_START_ADDR, &app_active_flag, 4);
HAL_Delay(1000);
clear_remove_devs();
Jump2App();
}
else //如果两种都不是,说明判断出现错误,直接跳转到APP1,继续正常运行上一版本的程序
{
printf("App1 Flag and App2 Flag are not valid !!\n\r");
printf("Try to Jump to App 1 !!\n\r");
clear_remove_devs();
Jump2App();
}
while(1)
{
;
}
}
knx_uart_reinit(); //接收数据串口初始化
int i, len, first = 0, writeResult, timeout = TIMEOUT;
unsigned int totalSize;
unsigned char crc8=0;
uint32_t app2_start_addr = FLASH_APP2_START_ADDR; //将App2开始地址赋值给变量
flash_clear(FLASH_APP2_START_ADDR, 96);//擦除APP2, 一共从App2开始地址擦除96个扇区大小,准备接收升级数据
printf("Watiing for receive APP2 Data !\n\r");
while (1)
{
if(knx_receive_frame_service(buffer, &len) > 0) //接收数据,buffer和len用来装数据和一条数据的长度
{
if(len > 1)
{
if(buffer[0]==0x3c && buffer[1]==0xe8 && buffer[7]==0x00 && buffer[8]==0x08 && buffer[9]==0x01) //判断是否是需要写入的文件数据
{
for(i = 0; i < len; i ++)
printf("%02x ", buffer[i]);
printf("\n\r");
timeout = TIMEOUT;
if(first == 0) //第一条数据包
{
totalSize = *(unsigned int *)&buffer[11]; //将发送所有数据长度赋值,方便后续计算升级数据大小
printf("total size is %d\n\r", totalSize);
crc8 = secure_GetChecksum(&buffer[8], len - 10); //校验每条数据包
printf("crc8: %02x --buffer[len-2]:%02x \r\n",crc8,buffer[len-2]);
if(buffer[len-2]==crc8) //如果校验数据匹配
{
printf("crc8: %02x --buffer[len-2]:%02x \r\n",crc8,buffer[len-2]);
writeResult = flash_write_firmware(app2_start_addr, &buffer[15], len - 17); //接收一条数据则写入一条数据到APP2
if (writeResult != 0) //写入失败,跳转到错误处理部分
goto Err;
printf("writen %d bytes ...\n\r", len - 17);
app2_start_addr += len - 17; //写多少个字节数据,就要从App2开始位置偏移多少个地址
first = 1;
}
else //如果校验数据不匹配,直接将isUpdate写入
//FLASH_UPDATE_FLAG_START_ADDR,跳转到APP1继续正常运行上一版本的程序
{
printf("data packet check error !!!\r\n");
printf(" try jump to app1!!! \r\n");
isUpdate = 0;
flash_write(FLASH_UPDATE_FLAG_START_ADDR, &isUpdate, 4);
clear_remove_devs();
Jump2App();
}
}
else //继续写入
{
crc8 = secure_GetChecksum(&buffer[8], len - 10);
if(buffer[len-2]==crc8)
{
printf("crc8: %02x --buffer[len-2]:%02x \r\n",crc8,buffer[len-2]);
writeResult = flash_write_firmware(app2_start_addr, &buffer[11], len - 13);
if (writeResult != 0)
goto Err;
printf("writen %d bytes ... total writen %d bytes\n\r", len - 13, app2_start_addr - FLASH_APP2_START_ADDR);
app2_start_addr += len - 13;
if(app2_start_addr - FLASH_APP2_START_ADDR >= totalSize) //最后的数据
{
printf("last packet OK ... finished ! total writen %d bytes\n\r", app2_start_addr - FLASH_APP2_START_ADDR);
isUpdate = 0;
flash_write(FLASH_UPDATE_FLAG_START_ADDR, &isUpdate, 4);
app_active_flag = APP2_Flag;
flash_write(FLASH_APP_SEL_START_ADDR, &app_active_flag, 4);
HAL_Delay(200);
NVIC_SystemReset(); //全部写入成功,直接系统复位进入bootloader
}
}
else
{
printf("data packet check error !!!\r\n");
printf(" try jump to app1!!! \r\n");
isUpdate = 0;
flash_write(FLASH_UPDATE_FLAG_START_ADDR, &isUpdate, 4);
clear_remove_devs();
Jump2App();
}
}
}
}
}
if(timeout > 0)
timeout --;
if(timeout == 0)
{
printf("OTA Timeout !!\n\r");
break;
}
HAL_Delay(20);
}
Err:
printf("OTA Failed !!\n\r");
isUpdate = 0;
flash_write(FLASH_UPDATE_FLAG_START_ADDR, &isUpdate, 4);
HAL_Delay(200);
NVIC_SystemReset();
/* USER CODE END 3 */
}
这里分析main.c代码逻辑,可根据项目需求修改细节部分:
1.首先进入bootloader程序,会去读取isUpdate地址flash的数据,这里isUpdate会在APP1收到升级信息的时候去修改,为0代表不需要升级,不为0代表需要升级,则会执行下载数据到App2。
2.如果设备不需要更新,则会读取APP校验标志,读取失败,则会将标志写为APP1。读取成功,判断标志为APP1则跳转APP1,如果标志为APP2,则会将App2数据拷贝到APP1,拷贝完成后跳转APP1.
3.接收升级数据部分:
3.1 首先会去判断是不是我们需要的下载的数据包,如果是,则会去接收。
3.2 如果是第一条数据包,将会把存放在第一条数据包的总数据长度取出来,这样子就方便在升级的时候判断是否升级完毕。
3.3 然后开始准备下载数据,下载前先去校验数据是否一致,如果一致就开始下载,然后下载到最后一条包时,则将APP校验标志置为APP2,然后系统复位,就会重新进入bootloader开头。
3.4 如果不一致说明传输错误,说明后面的数据也可能会传输错误,所以就直接跳转到APP1继续执行上个版本的程序,然后将isUpdate标志写到flash。
OTA远程升级可能遇到的问题:
这里用来总结升级过程中遇到的问题,可以根据芯片实际情况进行参考:
1.升级数据包发送太快:
升级数据发送太快,也可能会导致丢包问题和接收数据对不上问题,同时也可能导致写入flash失败的问题,所以在遇到时,建议看看发送数据方是不是发送数据太快,建议可以使用定时器固定时间发送和接收。
2.收到的数据包顺序不对:
如果直接发送数据包,及其有可能收到的数据包顺序不对,这里的数据就是错误的,这里可以使用队列的方式进行发送,同时下载到flash的时候,需要搞清楚数据是大端序存储还是小端序存储的。
3.堆栈溢出:
这个问题搞了我好久,在定义接收缓冲区时我把接收数据包的缓冲区放在了下载函数内部,导致溢出,程序一直复位,后来定在了全局就解决了这个问题,这里建议所有的缓冲区全部定义在全局。
4.莫名复位问题:
这里需要考虑几个方面,看看是不是内存越界或者溢出,还有种可能看门狗没有喂狗,导致的复位,这里提供几点供参考。
5.丢包:
丢包问题,在传输数据中经常会遇到,这里可以在传输的数据中定义专门的标识位和计数位来进行判断是否丢包。
6.写入flash问题:
需要根据开发芯片进行编写,这里所有的标志位都需要写入flash,才能保证标志位数据不会丢失。
OTA升级需要优化的点:
1.拷贝APP2的数据到APP1是固定的(因为考虑到后续的程序也不可能超过180多kb)
2.在逻辑判断时还有可优化的部分
3.在开发文档时是设计下载更新数据到APP2,然后更新完毕才跳到APP2,这样如果升级
或者跳转错误,还能回退版本,但在实现时还是存在一些问题,后续可以根据此思路进
行修改
最后因为在实际开发时遇到了很多的问题,所以以上所有部分都是尽量写到大家清晰易懂,同时大家在阅读时,建议根据实际项目需求进行参考。最后希望各位大佬编译成功,有不足的地方望提出,可更深一步进行优化,后续会输出APP1和APP2部分,如果对各位帮助希望各位大佬一键三连~