> 技术文档 > STM32/GD32/STC8H单片机IAP与OTA升级技术解析_iap升级和ota升级

STM32/GD32/STC8H单片机IAP与OTA升级技术解析_iap升级和ota升级


致谢与前言

在完成本技术方案的过程中,我特别感谢以下资源给予的启发和帮助:

  • 超子说物联网的B站教程提供了基础框架思路

  • 博客园文章《基于STM32的OTA升级设计》

本文章阐述了基础概念的实现,涵盖STM32、GD32和STC8H三种主流单片机平台。

一、基础概念解析

1.1 IAP与OTA的核心区别

特性 IAP OTA 传输方式 有线(串口/USB等) 无线(WiFi/蓝牙等) 实现复杂度 较低 较高 依赖硬件 无需额外模块 需要通信模块 升级距离 本地 远程 典型应用 工厂烧录 现场升级

1.2 三大关键技术点

  1. Flash分区管理:合理划分Bootloader、应用程序和下载缓存区

  2. 程序跳转机制:ARM的中断向量表偏移 vs 51的中断向量跳转

  3. 数据传输协议:XMODEM用于串口IAP,HTTP/MQTT用于OTA

二、Flash分区规划实战

2.1 GD32F303ZET6(512KB Flash)分区方案

#define GD32_FLASH_SADDR 0x08000000#define GD32_PAGE_SIZE 2048#define GD32_PAGE_NUM 256#define GD32_B_PAGE_NUM 20#define GD32_A_PAGE_NUM 30#define GD32_C_PAGE_NUM (GD32_PAGE_NUM - GD32_B_PAGE_NUM - GD32_A_PAGE_NUM) #define GD32_A_START_PAGE GD32_B_PAGE_NUM#define GD32_C_START_PAGE (GD32_A_START_PAGE + GD32_A_PAGE_NUM)#define GD32_A_START_ADDR (GD32_FLASH_SADDR + GD32_A_START_PAGE * GD32_PAGE_SIZE) #define GD32_C_START_ADDR (GD32_FLASH_SADDR + GD32_C_START_PAGE * GD32_PAGE_SIZE) #define FLASH_END_ADDRESS0x0807FFFF // 512K

关键点

  • 中断向量表偏移设置:nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0xA000)

  • 预留10%空间应对未来需求

  • 边界地址必须与页大小对齐

2.2 STC8H8K64U(64KB Flash)分区方案

#define STC_FLASH_SADDR 0x0000#define STC_PAGE_SIZE 512#define STC_PAGE_NUM 119#define STC_B_PAGE_NUM 8#define STC_A_PAGE_NUM 60#define STC_C_PAGE_NUM (STC_PAGE_NUM - STC_B_PAGE_NUM - STC_A_PAGE_NUM) #define STC_A_START_PAGE STC_B_PAGE_NUM#define STC_C_START_PAGE (STC_A_START_PAGE + STC_A_PAGE_NUM)#define STC_A_START_ADDR (STC_FLASH_SADDR + STC_A_START_PAGE * STC_PAGE_SIZE) #define STC_C_START_ADDR (STC_FLASH_SADDR + STC_C_START_PAGE * STC_PAGE_SIZE) #define STC_END_ADDRESS0xEFFD // Լ60K

特殊处理

  • Bootloader需禁用中断

  • 使用IAP_CONTR寄存器控制重启

  • 实际可用空间约60KB(0x0000-0xEFFD)

三、Hex文件合并与处理

这里简短介绍:

  • HEX程序合并的部分(BOOT区的HEX文件删除最后一行+程序运行区的HEX文件) = 程序合并HEX文件然后烧录到单片机
  • 然后IAP部分接收程序是接收新程序的BIN文件,bin文件的提取可以在网上查阅资料,然后新固件的BIN文件要删除程序最前面的0XFF部分

四、程序跳转关键实现

4.1 GD32程序跳转代码

void BootLoader_Clear(void){ usart_deinit(USART0); usart_deinit(USART1); gpio_deinit(GPIOA); gpio_deinit(GPIOB);}__asm void MSR_SP(uint32_t addr){ MSR MSP,r0 BX r14}/* 最终执行这条函数可跳转程序区 LOAD_A(GD32_A_START_ADDR); */void LOAD_A(uint32_t addr) // { uint32_t sp = *(uint32_t *)addr; uint32_t pc = *(uint32_t *)(addr + 4); u0_printf(\"MSP: 0x%08X, Reset_Handler: 0x%08X,GD32_A_START_ADDR:0x%08X\\r\\n\", sp, pc,GD32_A_START_ADDR); u0_printf(\"MSP: 0x%08X, Reset_Handler: 0x%08X,GD32_C_START_ADDR:0x%08X\\r\\n\", sp, pc,GD32_C_START_ADDR); if( (*(uint32_t *)addr>=0x20000000) && (*(uint32_t *)addr<=(0x20000000 + 0x4FFF))) { MSR_SP(*(uint32_t *)addr); load_A = (load_a)*(uint32_t *)(addr + 4); // PC指针偏移4 BootLoader_Clear(); // 清除 NVIC 中断挂起和使能位 load_A(); } else { u0_printf(\"进入分区失败,无程序\\r\\n\"); }}

4.2 STC8H程序跳转实现

Bootloader启动代码修改

LDR_SIZE EQU 1000HMAPISR MACRO ADDR CSEG AT ADDR LJMP LDR_SIZE + $ ENDM MAPISR 0003H MAPISR 000BH MAPISR 0013H MAPISR 001BH MAPISR 0023H MAPISR 002BH MAPISR 0033H MAPISR 003BH MAPISR 0043H MAPISR 004BH MAPISR 0053H MAPISR 005BH MAPISR 0063H MAPISR 006BH MAPISR 0073H MAPISR 007BH MAPISR 0083H MAPISR 008BH MAPISR 0093H MAPISR 009BH MAPISR 00A3H MAPISR 00ABH MAPISR 00B3H MAPISR 00BBH MAPISR 00C3H MAPISR 00CBH MAPISR 00D3H MAPISR 00DBH MAPISR 00E3H MAPISR 00EBH MAPISR 00F3H MAPISR 00FBH MAPISR 0103H MAPISR 010BH MAPISR 0113H MAPISR 011BH MAPISR 0123H MAPISR 012BH MAPISR 0133H MAPISR 013BH MAPISR 0143H MAPISR 014BH MAPISR 0153H MAPISR 015BH MAPISR 0163H MAPISR 016BH MAPISR 0173H MAPISR 017BH MAPISR 0183H MAPISR 018BH MAPISR 0193H MAPISR 019BH MAPISR 01A3H MAPISR 01ABH MAPISR 01B3H MAPISR 01BBH MAPISR 01C3H MAPISR 01CBH MAPISR 01D3H MAPISR 01DBH MAPISR 01E3H MAPISR 01EBH MAPISR 01F3H MAPISR 01FBH END

应用程序跳转代码

#define LDR_SIZE 0x1000 // APP程序的起始地址typedef bit BOOL;typedef unsigned char BYTE;typedef unsigned int WORD;typedef unsigned long DWORD;void JumpToApp(void) { // 检查应用程序有效性 if ((*(uint8_t code *)(LDR_SIZE) == 0x02) && (*(uint16_t code *)(LDR_SIZE + 1) >= LDR_SIZE + 3)) { EA = 0; // 关闭中断 IAP_CONTR = 0x20; // 软复位并跳转到LDR_SIZE }}

五、IAP升级完整实现

5.1 串口双缓冲区设计

typedef struct { uint8_t *start; uint8_t *end;} UCB_URxBuffptr;typedef struct { uint32_t URxCounter; UCB_URxBuffptr URxDataPtr[10]; UCB_URxBuffptr *URxDataIN; UCB_URxBuffptr *URxDataOUT; UCB_URxBuffptr *URxDataEND;} UCB_CB;UCB_CB U0CB;uint8_t U0_RxBuff[2048];void USART0_IRQHandler(void) { if(usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE)) { uint32_t remain = dma_transfer_number_get(DMA0, DMA_CH4); uint32_t recv_len = 256 - remain; // 更新缓冲区指针 U0CB.URxDataIN->end = U0_RxBuff + recv_len; U0CB.URxDataIN++; U0CB.URxCounter++; // DMA重新配置 dma_channel_disable(DMA0, DMA_CH4); DMA0_CH4CNT = 256; DMA0_CH4MADDR = (uint32_t)U0_RxBuff; dma_channel_enable(DMA0, DMA_CH4); }}

5.2 XMODEM协议处理

unsigned char CdataBuff[2048];unsigned char OverdataBuff[2048];unsigned char WQdataBuff[2048];unsigned char wq_count = 0;uint16_t i = 0;void TaskHandler(void){ if(U0CB.URxDataOUT != U0CB.URxDataIN) { BootLoader_Event(U0CB.URxDataOUT->start, U0CB.URxDataOUT->end - U0CB.URxDataOUT->start + 1); U0CB.URxDataOUT++; if(U0CB.URxDataOUT == U0CB.URxDataEND) { U0CB.URxDataOUT = &U0CB.URxDataPtr[0]; } } /*--------------------------------------------------*/ /*  Xmodem协议发送C  */ /*--------------------------------------------------*/ if (BootStaFlag & IAP_XMODEMC_FLAG) //如果IAP_XMODEMC_FLAG标志位置位,表明需要发送C { if (UpDataA.XmodemTimer >= 100) //计算间隔时间,到时进入if { u0_printf(\"C\");  //发送C UpDataA.XmodemTimer = 0; //清除计算间隔时间的变量 } UpDataA.XmodemTimer++; //计算间隔时间的变量++ } else if(BootStaFlag & UPDATA_WQ_TO_A_FLAG) { BootStaFlag &= ~UPDATA_WQ_TO_A_FLAG; if(OTA_Info.WQ_Page_Size%4==0) { for(i = 0;i < OTA_Info.WQ_Page_Size/GD32_PAGE_SIZE;i++) { ReadNorflashData2(i*GD32_PAGE_SIZE,WQdataBuff,GD32_PAGE_SIZE); Internal_FLASH_PageWrite(GD32_A_START_ADDR + i*GD32_PAGE_SIZE,(uint32_t *)WQdataBuff, GD32_PAGE_SIZE); } if(OTA_Info.WQ_Page_Size%GD32_PAGE_SIZE!=0) { ReadNorflashData2(i*GD32_PAGE_SIZE,WQdataBuff,OTA_Info.WQ_Page_Size%GD32_PAGE_SIZE); Internal_FLASH_PageWrite(GD32_A_START_ADDR + i*GD32_PAGE_SIZE,(uint32_t *)WQdataBuff, OTA_Info.WQ_Page_Size%GD32_PAGE_SIZE); }// OTA_Info.WQ_Page_Size = UpDataA.XmodemNB * 128; OTA_Info.OTA_flag = 0x00;// WriteEepromData(0, (uint8_t *)&OTA_Info.WQ_Page_Size,2); WriteEepromData(2, (uint8_t *)&OTA_Info.OTA_flag,1); u0_printf(\"操作完成\\r\\n\"); u0_printf(\"进入A区\\r\\n\"); DelayNms(100); LOAD_A(GD32_A_START_ADDR); } else { u0_printf(\"长度错误\\r\\n\"); //串口0输出信息 } BootLoader_Info(); } else if(BootStaFlag & UPDATA_B_TO_A_FLAG) { BootStaFlag &= ~UPDATA_B_TO_A_FLAG; u0_printf(\"C区写了%d页\\r\\n\",OTA_Info.C_Page_Count); u0_printf(\"C区最后剩余写了%d个字节\\r\\n\",OTA_Info.C_Over_Size); u0_printf(\"开始读取\\n\"); for(uint16_t i = 0;i  0) { if (!FlashRead(GD32_C_START_ADDR + OTA_Info.C_Page_Count*GD32_PAGE_SIZE,OverdataBuff, OTA_Info.C_Over_Size)) { u0_printf(\"Flash读数据故障,请排查!\\n\"); return; } Internal_FLASH_PageWrite(GD32_A_START_ADDR + OTA_Info.C_Page_Count*GD32_PAGE_SIZE,(uint32_t *)OverdataBuff, OTA_Info.C_Over_Size); u0_printf(\"操作完成\\r\\n\"); } BootLoader_Info(); } }

六、OTA升级与TUYA集成

OTA升级是要使用的是TUYA wifi模组内部的升级方案,详细可参考文章上面的博客园文章:这里只贴关键代码部分

extern long xdata DfuFlag;unsigned char mcu_firm_update_handle(const unsigned char value[],unsigned long position,unsigned short length){// #error \"请自行完成MCU固件升级代码,完成后请删除该行\" unsigned int pageNum; if(length == 0) { //固件数据发送完成OTA_Info.OTA_flag = 0xAA;IapErase(0x8600);IapProgram(0x8600,OTA_Info.OTA_flag);IapProgram(0x8601,OTA_Info.WQ_Page_Size);delay_ms(100);DfuFlag = DFU_TAG; //当需要执行用户ISP代码时,将强制执行标志赋值到DFU标志变量中IAP_CONTR = 0x20;  //然后执行软复位 }else { //固件数据处理  OTA_Info.WQ_Page_Size++; pageNum = position / 256; write_flash(STC_C_START_ADDR + pageNum*256,(unsigned char *)value,length); } return SUCCESS;}

七、bootloader完整代码

7.1GD32部分:

定义boot结构体:
#ifndef __MAIN_H__#define __MAIN_H__#include \"stdint.h\"#include \"stdio.h\"#include \"string.h\"#define GD32_FLASH_SADDR 0x08000000#define GD32_PAGE_SIZE 2048#define GD32_PAGE_NUM 256#define GD32_B_PAGE_NUM 20#define GD32_A_PAGE_NUM 30#define GD32_C_PAGE_NUM (GD32_PAGE_NUM - GD32_B_PAGE_NUM - GD32_A_PAGE_NUM) #define GD32_A_START_PAGE GD32_B_PAGE_NUM#define GD32_C_START_PAGE (GD32_A_START_PAGE + GD32_A_PAGE_NUM)#define GD32_A_START_ADDR (GD32_FLASH_SADDR + GD32_A_START_PAGE * GD32_PAGE_SIZE) #define GD32_C_START_ADDR (GD32_FLASH_SADDR + GD32_C_START_PAGE * GD32_PAGE_SIZE) #define FLASH_END_ADDRESS0x0807FFFF // 512K/*********************************************************************************************/#define IAP_B_TO_WQ_FLAG 0x00000080 //从写入固件到外部flash#define UPDATA_WQ_TO_A_FLAG 0x00000040 //从外部flash写入到A区#define UPDATA_B_TO_A_FLAG 0x00000001 //置位表明从B区回来需要更新A了#define IAP_XMODEMC_FLAG 0x00000002 //置位表明Xmdoem协议第一阶段发送大写C #define IAP_XMODEMD_A_FLAG 0x00000004 //置位表明Xmdoem协议第二阶段处理A区数据包 #define IAP_XMODEMD_C_FLAG 0x00000008 //置位表明Xmdoem协议第二阶段处理C区数据包#define IAP_B_TO_A_FLAG 0x00000010 // B区跳转至A区#define IAP_B_TO_C_FLAG 0x00000020 // B区跳转至C区#define OTA_SET_FLAG 0xAAtypedef struct{ uint8_t OTA_flag; // eeprom:0 1Byte 标志性的变量,等于OTA_SET_FLAG定义的值时,表明需要OTA更新A区 uint8_t C_Page_Count; // eeprom:1 1Byte 记录了写入了几次2048到C区 uint16_t C_Over_Size; // eeprom:2-3 2Byte (范围1-2048)记录最后写入C区的剩余字节数 uint16_t WQ_Page_Size; // 写入了外部flash的大小}OTA_InfOCB;typedef struct{ uint8_t CdataBuff[GD32_PAGE_SIZE]; uint8_t Updatabuff[GD32_PAGE_SIZE]; //更新A区时,缓存区 uint32_t XmodemTimer;  //用于记录Xmdoem协议第一阶段发送大写C 的时间间隔 uint32_t XmodemNB; //用于记录Xmdoem协议接收到多少数据包了 uint32_t XmodemCRC; //用于保存Xmdoem协议包计算的CRC16校验值} UpDataA_CB;#define OTA_INFOCB_SIZE sizeof(OTA_InfOCB)extern OTA_InfOCB OTA_Info;extern UpDataA_CB UpDataA;extern uint32_t BootStaFlag; //外部变量声明#endif#ifndef __BOOT_H__#define __BOOT_H__#include \"stdint.h\"#include \"stdio.h\"#include \"string.h\"#include \"stdbool.h\"#define Internal_FLASH_PageWrite GD32_WriteFlash//内部Flash写入函数#define Internal_ErasePage GD32_EraseFlash //内部Flash擦除函数void ReadNorflashData2(uint32_t readAddr, uint8_t *pBuffer, uint16_t len);void ProgramNorflashPage2(uint32_t addr, uint8_t *pBuffer, uint32_t len);void WriteNorflashData(uint32_t writeAddr, uint8_t *pBuffer, uint32_t len);void EraseNorflashSector64K(uint32_t eraseAddr);void GD32_EraseFlash(uint16_t start,uint16_t num);void GD32_WriteFlash(uint32_t saddr,uint32_t *wdata,uint32_t wnum);bool FlashRead(uint32_t readAddr, uint8_t *pBuffer, uint32_t numToRead);typedef void (*load_a)(void);void BootLoader_Info(void);uint16_t Xmodem_CRC16(uint8_t *data, uint16_t datalen);void BootLoader_Event(uint8_t *data, uint16_t datalen);void BootLoader_Brance(void);__asm void MSR_SP(uint32_t addr);void LOAD_A(uint32_t addr);void BootLoader_Clear(void);#endif
uart配置:
#ifndef __UART_H__#define __UART_H__#include \"string.h\"#include \"stdint.h\"#include \"stdarg.h\"#include \"stdio.h\"#define U0_TX_SIZE 2048#define U0_RX_SIZE 2048#define U0_RX_MAX 256#define NUM 10typedef struct{ unsigned char *start; unsigned char *end;}UCB_URxBuffptr;typedef struct{ unsigned int URxCounter; UCB_URxBuffptr URxDataPtr[NUM]; UCB_URxBuffptr *URxDataIN; UCB_URxBuffptr *URxDataOUT; UCB_URxBuffptr *URxDataEND;}UCB_CB ;extern UCB_CB U0CB;extern unsigned char U0_RxBuff[U0_RX_SIZE];void Usart0_Init(uint32_t bandrate);void DMA_Init(void);void U0Rx_PtrInit(void);void u0_printf(char *format,...);#endif#include \"gd32f30x.h\"#include \"uart.h\"unsigned char U0_RxBuff[U0_RX_SIZE];unsigned char U0_TxBuff[U0_TX_SIZE];UCB_CB U0CB; void Usart0_Init(uint32_t bandrate){ rcu_periph_clock_enable(RCU_USART0); rcu_periph_clock_enable(RCU_GPIOA); gpio_init(GPIOA,GPIO_MODE_AF_PP,GPIO_OSPEED_50MHZ,GPIO_PIN_9); gpio_init(GPIOA,GPIO_MODE_IN_FLOATING,GPIO_OSPEED_50MHZ,GPIO_PIN_10); usart_deinit(USART0); usart_baudrate_set(USART0,bandrate); usart_parity_config(USART0,USART_PM_NONE); usart_word_length_set(USART0,USART_WL_8BIT); usart_stop_bit_set(USART0,USART_STB_1BIT); usart_transmit_config(USART0,USART_TRANSMIT_ENABLE); usart_receive_config(USART0,USART_RECEIVE_ENABLE); usart_dma_receive_config(USART0,USART_RECEIVE_DMA_ENABLE); nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); nvic_irq_enable(USART0_IRQn,0,0); usart_interrupt_enable(USART0,USART_INT_IDLE); U0Rx_PtrInit(); DMA_Init(); usart_enable(USART0);}void DMA_Init(void){ dma_parameter_struct dma_init_struct; rcu_periph_clock_enable(RCU_DMA0); dma_deinit(DMA0,DMA_CH4); dma_init_struct.periph_addr = USART0 + 4; dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; dma_init_struct.memory_addr = (uint32_t)U0_RxBuff; dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT; dma_init_struct.number = U0_RX_MAX + 1; dma_init_struct.priority = DMA_PRIORITY_HIGH; dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; dma_init(DMA0,DMA_CH4,&dma_init_struct); dma_circulation_disable(DMA0,DMA_CH4); dma_channel_enable(DMA0,DMA_CH4);}void U0Rx_PtrInit(void){ U0CB.URxDataIN = &U0CB.URxDataPtr[0]; U0CB.URxDataOUT = &U0CB.URxDataPtr[0]; U0CB.URxDataEND = &U0CB.URxDataPtr[NUM - 1]; U0CB.URxDataIN->start = U0_RxBuff; U0CB.URxCounter = 0;}void u0_printf(char *format,...){ uint16_t i; va_list listdata; va_start(listdata,format); vsprintf((char *)U0_TxBuff,format,listdata); va_end(listdata); for(i=0;i<strlen((const char*)U0_TxBuff);i++) { while(usart_flag_get(USART0,USART_FLAG_TBE)!=1); usart_data_transmit(USART0,U0_TxBuff[i]); } while(usart_flag_get(USART0,USART_FLAG_TC)!=1);}
main函数:
OTA_InfOCB OTA_Info;UpDataA_CB UpDataA;uint32_t BootStaFlag; //记录全局状态标志位int main(void){DrvInit();AppInit(); u0_printf(\"This is tuya_ota_bootloader!\\r\\n\"); ReadOTAInfo(); DelayNms(10); BootLoader_Brance();while (1){ TaskHandler();}}void ReadOTAInfo(void){ ReadEepromData(0,(uint8_t *)&OTA_Info.WQ_Page_Size,2); ReadEepromData(2, (uint8_t *)&OTA_Info.OTA_flag,1); u0_printf(\"OTA_Info.WQ_Page_Size:%d\\r\\n\",OTA_Info.WQ_Page_Size); u0_printf(\"OTA_Info.OTA_flag:0x%02X\\r\\n\",OTA_Info.OTA_flag);}void BootLoader_Brance(void){ if (BootLoader_Enter(50) == 0) { if(OTA_Info.OTA_flag == OTA_SET_FLAG) { u0_printf(\"OTA更新\\r\\n\"); BootStaFlag = UPDATA_WQ_TO_A_FLAG; DelayNms(10); u0_printf(\"开始擦除A区\\r\\n\"); //串口0输出信息 Internal_ErasePage(GD32_A_START_PAGE, GD32_A_PAGE_NUM); //擦除A分区占用的扇区u0_printf(\"擦除A区结束\\r\\n\"); } else { u0_printf(\"跳转A分区\\r\\n\");  } } u0_printf(\"进入BootLoader命令行\\r\\n\");  //串口0输出信息 BootLoader_Info(); //串口输出命令行信息}void BootLoader_Event(uint8_t *data, uint16_t datalen){ int i; //temp用于版本号sscanf判断格式 i用于for循环 /*--------------------------------------------------*/ /* 没有任何事件,判断顶层命令  */ /*--------------------------------------------------*/ if (BootStaFlag == 0) //如果BootStaFlag等于0,没有任何事件,进入if,判断是哪个命令 { if ((datalen == 1) && (data[0] == \'1\'))  //如果数据长度1字节 且 是字符 1 { u0_printf(\"开始擦除A区\\r\\n\"); //串口0输出信息 Internal_ErasePage(GD32_A_START_PAGE, GD32_A_PAGE_NUM); //擦除A分区占用的扇区u0_printf(\"擦除A区结束\\r\\n\"); } else if ((datalen == 1) && (data[0] == \'2\')) //如果数据长度1字节 且 是字符 2 { u0_printf(\"开始擦除C区\\r\\n\"); //串口0输出信息 Internal_ErasePage(GD32_C_START_PAGE, GD32_C_PAGE_NUM); //擦除A分区占用的扇区u0_printf(\"擦除C区结束\\r\\n\"); } else if ((datalen == 1) && (data[0] == \'3\')) //如果数据长度1字节 且 是字符 3 { u0_printf(\"开始从C区获取数据,写入A区\\r\\n\"); BootStaFlag = UPDATA_B_TO_A_FLAG; } else if ((datalen == 1) && (data[0] == \'4\')) //如果数据长度1字节 且 是字符 3 { u0_printf(\"开始从外部flash获取数据,写入A区\\r\\n\"); BootStaFlag = UPDATA_WQ_TO_A_FLAG; } else if ((datalen == 1) && (data[0] == \'5\')) //如果数据长度1字节 且 是字符 4 { u0_printf(\"开始从串口IAP下载A区程序\\r\\n\"); BootStaFlag = (IAP_XMODEMC_FLAG | IAP_XMODEMD_A_FLAG); UpDataA.XmodemTimer = 0; UpDataA.XmodemNB = 0; } else if ((datalen == 1) && (data[0] == \'6\')) //如果数据长度1字节 且 是字符 5 { u0_printf(\"开始从串口IAP下载C区程序\\r\\n\"); BootStaFlag = (IAP_XMODEMC_FLAG | IAP_XMODEMD_C_FLAG); UpDataA.XmodemTimer = 0; UpDataA.XmodemNB = 0; OTA_Info.C_Page_Count = 0; } else if ((datalen == 1) && (data[0] == \'7\')) //如果数据长度1字节 且 是字符 5 { u0_printf(\"开始从串口IAP下载外部flash程序\\r\\n\"); BootStaFlag = (IAP_XMODEMC_FLAG | IAP_B_TO_WQ_FLAG); UpDataA.XmodemTimer = 0; UpDataA.XmodemNB = 0; DelayNms(100); EraseNorflashSector64K(0); OTA_Info.WQ_Page_Size = 0; } else if ((datalen == 1) && (data[0] == \'8\')) //如果数据长度1字节 且 是字符 5 { u0_printf(\"进入A区\\r\\n\"); DelayNms(100); LOAD_A(GD32_A_START_ADDR); } else if ((datalen == 1) && (data[0] == \'9\')) //如果数据长度1字节 且 是字符 5 { u0_printf(\"进入C区\\r\\n\"); DelayNms(100); LOAD_A(GD32_C_START_ADDR); } else if ((datalen == 1) && (data[0] == \'A\')) //如果数据长度1字节 且 是字符 5 { u0_printf(\"执行重启\\r\\n\"); DelayNms(100); //延时 NVIC_SystemReset(); //重启 } } /*--------------------------------------------------*/ /* 发生Xmodem事件  */ /*--------------------------------------------------*/ else if(BootStaFlag & IAP_XMODEMD_A_FLAG || BootStaFlag & IAP_XMODEMD_C_FLAG ||\\ BootStaFlag & IAP_B_TO_WQ_FLAG) { if ((datalen == 133) && (data[0] == 0x01)) //判断 Xmodem协议一包总长133字节 且 第一个字节帧头是0x01(SOH) { BootStaFlag &= ~ IAP_XMODEMC_FLAG;  //已经收到数据包了,所以清除 IAP_XMODEMC_FLAG,不再发送大写C UpDataA.XmodemCRC = Xmodem_CRC16(&data[3], 128);//计算本次接收的数据包数据的CRC if (UpDataA.XmodemCRC == (data[131] * 256 + data[132]) ) //计算的CRC 和 接收的CRC 比较,一样说明正确,进入if { UpDataA.XmodemNB++; //已接收的数据包数量+1 /*将本次接收的数据,暂存到UpDataA.Updatabuff缓存区,缓存区的大小与内部FLASH页大小一致 128的意思是一包数据的大小是128B, UpDataA.XmodemNB - 1是为了从数组[0]开始,(STM32_PAGE_SIZE / 128)是为了计算多少包数据可以凑满一页,%取余是为了memcpy数组正确赋值*/ memcpy(&UpDataA.Updatabuff[((UpDataA.XmodemNB - 1) % (GD32_PAGE_SIZE / 128)) * 128], &data[3], 128); if ((UpDataA.XmodemNB % (GD32_PAGE_SIZE / 128)) == 0) //对C8T6而言,如果已接收数据包数量是8的整数倍,说明都满1扇区1024字节,进入if { //如果页大小是2KB的芯片,收到16个数据包才进行写入 2048/128=16  if (BootStaFlag & IAP_XMODEMD_A_FLAG) //判断如果是命令5启动Xmodem的话,也就是向外部Flash下载程序  { //地址偏移-->((UpDataA.XmodemNB / (STM32_PAGE_SIZE / 128)) - 1)*STM32_PAGE_SIZE Internal_FLASH_PageWrite(GD32_A_START_ADDR + ((UpDataA.XmodemNB / (GD32_PAGE_SIZE / 128)) - 1)*GD32_PAGE_SIZE,(uint32_t *)UpDataA.Updatabuff, GD32_PAGE_SIZE);  }  else if(BootStaFlag & IAP_XMODEMD_C_FLAG) //判断如果不是命令5启动Xmodem的话,那就是串口IAP启动的,进入else  { //写入到单片机A区相应的扇区//地址偏移-->((UpDataA.XmodemNB / (STM32_PAGE_SIZE / 128)) - 1)*STM32_PAGE_SIZE Internal_FLASH_PageWrite(GD32_C_START_ADDR + ((UpDataA.XmodemNB / (GD32_PAGE_SIZE / 128)) - 1)*GD32_PAGE_SIZE,(uint32_t *)UpDataA.Updatabuff, GD32_PAGE_SIZE); OTA_Info.C_Page_Count++; }  else if(BootStaFlag & IAP_B_TO_WQ_FLAG)  { for (i = 0; i < 8; i++) //W25Q64每次写入256字节,对C8T6而言,1扇区1024字节,需要循环4次写 (STM32_PAGE_SIZE/256) { ProgramNorflashPage2((UpDataA.XmodemNB / (GD32_PAGE_SIZE / 128) - 1) * (GD32_PAGE_SIZE / 256) + i,&UpDataA.Updatabuff[i * 256],256); }  }  } u0_printf(\"\\x06\"); //正确,返回ACK给CRT软件 } else//如果CRC校验错误,进入else { u0_printf(\"\\x15\"); //返回NCK给CRT软件 } }//当收到的数据不足一页的时候就进行最后数据的写入 if ((datalen == 1) && (data[0] == 0x04)) //如果收到1个字节数据 且 是0x04(NOT),进入if,说明收到EOT,表明数据已经发生完毕 { u0_printf(\"\\x06\");  //返回ACK给CRT软件 if ((UpDataA.XmodemNB % (GD32_PAGE_SIZE / 128)) != 0) //对C8T6而言,判断是否还有不满1扇区1024字节的数据,如果有进入if,把剩余的小尾巴写入 { if (BootStaFlag & IAP_XMODEMD_A_FLAG) //判断如果是命令5启动Xmodem的话,也就是向外部Flash下载程序 {  Internal_FLASH_PageWrite(GD32_A_START_ADDR + ((UpDataA.XmodemNB / (GD32_PAGE_SIZE / 128)))*GD32_PAGE_SIZE,(uint32_t *)UpDataA.Updatabuff, (UpDataA.XmodemNB % (GD32_PAGE_SIZE / 128)) * 128); } else if(BootStaFlag & IAP_XMODEMD_C_FLAG) //判断如果不是命令5启动Xmodem的话,那就是串口IAP启动的,进入else {//写入到单片机A区相应的扇区  Internal_FLASH_PageWrite(GD32_C_START_ADDR + ((UpDataA.XmodemNB / (GD32_PAGE_SIZE / 128)))*GD32_PAGE_SIZE,(uint32_t *)UpDataA.Updatabuff, (UpDataA.XmodemNB % (GD32_PAGE_SIZE / 128)) * 128);  OTA_Info.C_Over_Size = ((UpDataA.XmodemNB % (GD32_PAGE_SIZE / 128)) * 128);  } else if(BootStaFlag & IAP_B_TO_WQ_FLAG) {  for (i = 0; i =0x20000000) && (*(uint32_t *)addr<=(0x20000000 + 0x4FFF))) { MSR_SP(*(uint32_t *)addr); load_A = (load_a)*(uint32_t *)(addr + 4); // PC指针偏移4 BootLoader_Clear(); // 清除 NVIC 中断挂起和使能位 load_A(); } else { u0_printf(\"进入分区失败,无程序\\r\\n\"); }}void BootLoader_Clear(void){ usart_deinit(USART0); usart_deinit(USART1); gpio_deinit(GPIOA); gpio_deinit(GPIOB);}/** * @brief Xmodem_CRC16校验 * @param data:数据指针* @param datalen:数据长度 * @retval 校验后的数据 */uint16_t Xmodem_CRC16(uint8_t *data, uint16_t datalen){ uint8_t i;  //用于for循环 uint16_t Crcinit = 0x0000; //Xmdoem CRC校验的初始值,必须是0x0000 uint16_t Crcipoly = 0x1021;//Xmdoem CRC校验的多项式,必须是0x1021 while (datalen--) //根据datalen大小,有多少字节循环多少次 { Crcinit = (*data << 8) ^ Crcinit; //先将带校验的字节,挪到高8位 for (i = 0; i < 8; i++)//每个字节8个二进制位,循环8次 { if (Crcinit & 0x8000)  //判断BIT15是1还是0,是1的话,进入if Crcinit = (Crcinit << 1) ^ Crcipoly; //是1的话,先左移,再异或多项式 else  //判断BIT15是1还是0,是0的话,进入else Crcinit = (Crcinit <start, U0CB.URxDataOUT->end - U0CB.URxDataOUT->start + 1); U0CB.URxDataOUT++; if(U0CB.URxDataOUT == U0CB.URxDataEND) { U0CB.URxDataOUT = &U0CB.URxDataPtr[0]; } } /*--------------------------------------------------*/ /*  Xmodem协议发送C  */ /*--------------------------------------------------*/ if (BootStaFlag & IAP_XMODEMC_FLAG) //如果IAP_XMODEMC_FLAG标志位置位,表明需要发送C { if (UpDataA.XmodemTimer >= 100) //计算间隔时间,到时进入if { u0_printf(\"C\");  //发送C UpDataA.XmodemTimer = 0; //清除计算间隔时间的变量 } UpDataA.XmodemTimer++; //计算间隔时间的变量++ } else if(BootStaFlag & UPDATA_WQ_TO_A_FLAG) { BootStaFlag &= ~UPDATA_WQ_TO_A_FLAG; if(OTA_Info.WQ_Page_Size%4==0) { for(i = 0;i < OTA_Info.WQ_Page_Size/GD32_PAGE_SIZE;i++) { ReadNorflashData2(i*GD32_PAGE_SIZE,WQdataBuff,GD32_PAGE_SIZE); Internal_FLASH_PageWrite(GD32_A_START_ADDR + i*GD32_PAGE_SIZE,(uint32_t *)WQdataBuff, GD32_PAGE_SIZE); } if(OTA_Info.WQ_Page_Size%GD32_PAGE_SIZE!=0) { ReadNorflashData2(i*GD32_PAGE_SIZE,WQdataBuff,OTA_Info.WQ_Page_Size%GD32_PAGE_SIZE); Internal_FLASH_PageWrite(GD32_A_START_ADDR + i*GD32_PAGE_SIZE,(uint32_t *)WQdataBuff, OTA_Info.WQ_Page_Size%GD32_PAGE_SIZE); }// OTA_Info.WQ_Page_Size = UpDataA.XmodemNB * 128; OTA_Info.OTA_flag = 0x00;// WriteEepromData(0, (uint8_t *)&OTA_Info.WQ_Page_Size,2); WriteEepromData(2, (uint8_t *)&OTA_Info.OTA_flag,1); u0_printf(\"操作完成\\r\\n\"); u0_printf(\"进入A区\\r\\n\"); DelayNms(100); LOAD_A(GD32_A_START_ADDR); } else { u0_printf(\"长度错误\\r\\n\"); //串口0输出信息 } BootLoader_Info(); } else if(BootStaFlag & UPDATA_B_TO_A_FLAG) { BootStaFlag &= ~UPDATA_B_TO_A_FLAG; u0_printf(\"C区写了%d页\\r\\n\",OTA_Info.C_Page_Count); u0_printf(\"C区最后剩余写了%d个字节\\r\\n\",OTA_Info.C_Over_Size); u0_printf(\"开始读取\\n\"); for(uint16_t i = 0;i  0) { if (!FlashRead(GD32_C_START_ADDR + OTA_Info.C_Page_Count*GD32_PAGE_SIZE,OverdataBuff, OTA_Info.C_Over_Size)) { u0_printf(\"Flash读数据故障,请排查!\\n\"); return; } Internal_FLASH_PageWrite(GD32_A_START_ADDR + OTA_Info.C_Page_Count*GD32_PAGE_SIZE,(uint32_t *)OverdataBuff, OTA_Info.C_Over_Size); u0_printf(\"操作完成\\r\\n\"); } BootLoader_Info(); } }

7.2STC8H部分

内存分布:
#ifndef __IAP_H__#define __IAP_H__#define STC_FLASH_SADDR 0x0000#define STC_PAGE_SIZE 512#define STC_PAGE_NUM 119#define STC_B_PAGE_NUM 8#define STC_A_PAGE_NUM 60#define STC_C_PAGE_NUM (STC_PAGE_NUM - STC_B_PAGE_NUM - STC_A_PAGE_NUM) #define STC_A_START_PAGE STC_B_PAGE_NUM#define STC_C_START_PAGE (STC_A_START_PAGE + STC_A_PAGE_NUM)#define STC_A_START_ADDR (STC_FLASH_SADDR + STC_A_START_PAGE * STC_PAGE_SIZE) #define STC_C_START_ADDR (STC_FLASH_SADDR + STC_C_START_PAGE * STC_PAGE_SIZE) #define STC_END_ADDRESS0xEFFD // Լ60K#endif

操作flash函数:

#include \"main.h\"#define WT_12M 0x80void IapIdle_boot(void){ IAP_CONTR = 0; //??IAP?? IAP_CMD = 0; //??????? IAP_TRIG = 0; //??????? IAP_ADDRH = 0x80; //???????IAP?? IAP_ADDRL = 0;}char IapRead_boot(int addr){ char dat; IAP_CONTR = WT_12M; //??IAP IAP_TPS=12; IAP_CMD = 1; //??IAP??? IAP_ADDRL = addr; //??IAP??? IAP_ADDRH = addr >> 8;//??IAP??? IAP_TRIG = 0x5a; //?????(0x5a) IAP_TRIG = 0xa5; //?????(0xa5) _nop_(); dat = IAP_DATA; //?IAP?? IapIdle_boot(); //??IAP?? return dat;}void IapProgram_boot(int addr, char dat){ IAP_CONTR = WT_12M; //??IAP IAP_TPS=12; IAP_CMD = 2; //??IAP??? IAP_ADDRL = addr; //??IAP??? IAP_ADDRH = addr >> 8;//??IAP??? IAP_DATA = dat; //?IAP?? IAP_TRIG = 0x5a; //?????(0x5a) IAP_TRIG = 0xa5; //?????(0xa5) _nop_(); IapIdle_boot(); //??IAP??}void IapErase_boot(int addr){ IAP_CONTR = WT_12M; //??IAP IAP_TPS=12; IAP_CMD = 3; //??IAP???? IAP_ADDRL = addr; //??IAP??? IAP_ADDRH = addr >> 8;//??IAP??? IAP_TRIG = 0x5a; //?????(0x5a) IAP_TRIG = 0xa5; //?????(0xa5) _nop_();  // IapIdle_boot(); //??IAP??}void erase_flash_boot(unsigned int addr,unsigned char dst_page_num){ unsigned int i; // 循环擦除每一页 for (i = 0; i < dst_page_num; i++) { IapErase_boot(addr); // 擦除当前页 Delay_ms_boot(10); // 给 Flash 恢复时间(建议 5~10ms) addr += STC_PAGE_SIZE; // 移动到下一页 }}void write_flash_boot(unsigned int addr,const char *data_t, unsigned int len){ unsigned int i; for (i = 0; i < len; i++) { IapProgram_boot(addr + i, data_t[i]); }}void read_flash(unsigned int addr, char *buffer, unsigned int len){ unsigned int i; for (i = 0; i < len; i++) { buffer[i] = IapRead_boot(addr + i);// SendString_boot(\"addr is:\");// SendHex(addr + i);// SendString_boot(\"data:\");//SendHex(buffer[i]);//SendString_boot(\"\\r\\n\"); }}void EEPROM_Trig(void){F0 = EA; //保存全局中断EA = 0; //禁止中断, 避免触发命令无效IAP_TRIG = 0x5A;IAP_TRIG = 0xA5;  //先送5AH,再送A5H到IAP触发寄存器,每次都需要如此//送完A5H后,IAP命令立即被触发启动//CPU等待IAP完成后,才会继续执行程序。_nop_();_nop_();EA = F0; //恢复全局中断}voidDisableEEPROM(void){IAP_CONTR = 0;//禁止IAP操作IAP_CMD = 0;//去除IAP命令IAP_TRIG = 0;//防止IAP命令误触发IAP_ADDRH = 0xff;//清0地址高字节IAP_ADDRL = 0xff;//清0地址低字节,指向非EEPROM区,防止误操作}void EEPROM_read_n(unsigned int EE_address,unsigned char *DataAddress,unsigned int number){IAP_ENABLE(); //设置等待时间,允许IAP操作,送一次就够IAP_READ(); //送字节读命令,命令不需改变时,不需重新送命令do{IAP_ADDRH = EE_address / 256; //送地址高字节(地址需要改变时才需重新送地址)IAP_ADDRL = EE_address % 256; //送地址低字节EEPROM_Trig();//触发EEPROM操作*DataAddress = IAP_DATA; //读出的数据送往EE_address++;DataAddress++;}while(--number);DisableEEPROM();}void EEPROM_write_n(unsigned int EE_address,unsigned char *DataAddress,unsigned int number){IAP_ENABLE(); //设置等待时间,允许IAP操作,送一次就够IAP_WRITE(); //宏调用, 送字节写命令do{IAP_ADDRH = EE_address / 256; //送地址高字节(地址需要改变时才需重新送地址)IAP_ADDRL = EE_address % 256; //送地址低字节IAP_DATA = *DataAddress; //送数据到IAP_DATA,只有数据改变时才需重新送EEPROM_Trig();  //触发EEPROM操作EE_address++;  //下一个地址DataAddress++;  //下一个数据}while(--number);  //直到结束DisableEEPROM();}
main函数:
#include \"main.h\"unsigned int i = 0;unsigned char jump_flag = 0;unsigned char clear_flag = 0;unsigned char updata_flag = 0;unsigned char e_temp = 0;unsigned char ota_flag = 0;unsigned int page_size = 0;#define Max_Length 256 //读写EEPROM缓冲长度unsigned char xdata ota_tmp[Max_Length]; //EEPROM操作缓冲 30 unsigned char xdata read_tmp[Max_Length];int main(){All_IO_Config();Timer_Config();Delay_ms_boot(500);SendString_boot(\"This is IAP test demo 1 \\r\\n\");e_temp=IapRead_boot(0x8600);if(e_temp != 0xff){SendString_boot(\"read eeprom now \\r\\n\");ota_flag = IapRead_boot(0x8600);page_size = IapRead_boot(0x8601)*256;}else{SendString_boot(\"eeprom is empty \\r\\n\");}if(ota_flag == 0xAA){SendString_boot(\"ota_flag is 0xAA \\r\\n\");sendbyte_float(page_size);SendString_boot(\"\\r\\n\");updata_flag = 1;}else{SendString_boot(\"ota_flag is not 0xAA \\r\\n\");updata_flag = 0;SendString_boot(\"go to the app 2 demo \\r\\n\");jump_flag = 1;}sendbyte_float(page_size);Delay_ms_boot(50);while(1){if(jump_flag){jump_flag = 0;if ((*(BYTE code *)(LDR_SIZE) == 0x02) &&(*(WORD code *)(LDR_SIZE + 1) >= LDR_SIZE + 3)){((void (code *)())(LDR_SIZE))();}else{SendString_boot(\"go to the app error \\r\\n\");}}if(updata_flag){updata_flag = 0;SendString_boot(\"up data now \\r\\n\");erase_flash_boot(STC_A_START_ADDR,STC_A_PAGE_NUM-2);SendString_boot(\"clear success!!\\r\\n\");for(i = 0;i < page_size/256 ;i++){read_flash(STC_C_START_ADDR + i*256,(unsigned char*)ota_tmp,256);write_flash_boot(STC_A_START_ADDR + i*256,(unsigned char*)ota_tmp,256);}IapErase_boot(0x8600);SendString_boot(\"write A firm success\\r\\n\");jump_flag = 1;}}}void Delay_ms_boot(unsigned int xms){unsigned int Delay_i,Delay_j;for(Delay_i = 0;Delay_i < 980;Delay_i++)for(Delay_j = 0;Delay_j < xms;Delay_j++);}

八、知识点总结

8.1 IAP升级流程

  1. 设备进入Bootloader模式

  2. 主机发送XMODEM启动字符\'C\'

  3. 设备响应ACK(0x06)

  4. 主机发送128字节数据包+CRC

  5. 设备校验并写入Flash,响应ACK

  6. 重复步骤4-5直到传输完成

  7. 主机发送EOT(0x04)

  8. 设备验证固件完整性

  9. 更新标志位并重启

8.2 OTA升级流程

  1. 云平台推送新固件通知

  2. WiFi模组通过串口通知MCU

  3. MCU初始化OTA并擦除下载区

  4. 分片接收固件并写入Flash

  5. 下载完成后校验固件

  6. 设置升级标志位

  7. 重启进入Bootloader

  8. Bootloader复制固件到运行区

  9. 清除标志位并跳转新固件

九、知识点细节太多了,一时半会说不完,以后有空会更新更详细的讲解,还望包涵