> 技术文档 > STM32固件升级设计——串口IAP升级(基于YMODEM协议)_stm32 iap

STM32固件升级设计——串口IAP升级(基于YMODEM协议)_stm32 iap

目录

一、功能描述 

1、BootLoader部分:

2、APP部分:

二、BootLoader程序制作

1、分区定义

2、 主函数

3、YMODEM协议的实现

4、程序跳转

三、APP程序制作

四、工程配置(默认KEIL5)

五、运行测试 

结束语


概述

        IAP(In Application Programming)即在应用中编程,允许在应用程序运行时更新或切换固件。STM32通过修改MSP(主堆栈指针)和PC(程序计数器)实现从不同地址启动,包括Flash或RAM地址。默认情况下,嵌入式程序以连续二进制形式烧录到STM32的可寻址Flash区域。若Flash容量足够存储多个完整程序,每个程序独立且完整,上电后可通过修改MSP值选择不同程序入口,从而实现多固件切换或升级。

         BootLoader(引导加载程序)是嵌入式系统或计算机启动时运行的一段小型程序,负责初始化硬件、加载操作系统内核并将其控制权移交。它是系统从关机状态到操作系统完全运行之间的桥梁。

        所以,固件升级的基本思路是将stm32的flash划分为若干个区域,其中包括BootLoader区域和APP区等,将各自的程序写到对应的flash区域里,本文将采用YMODEM协议来接收固件,只重实践不讲原理。

一、功能描述 

        使用STM32的串口总线和Ymodem协议实现串口IAP升级程序。将FLASH分为3个部分。
分区介绍:
        本文使用stm32f103vet6,flash是512k,sector是1k,BootLoader整个代码编译下来有11K左右,所以使用0x08000000~0x00002FFF,SETTING主要存放升级标志位,使用0x08003000~0x00003FFF,剩下的FLASH将分为两个部分都用来存放代码,APP使用0x08004000~0x0807C000。(我这里已经极致压缩FLASH了,如果大家要移植的话,肯定要修改各个区域大小的)

区域 起始地址 区域大小 功能 BOOT 0x08000000 0x00003000(12k) 存放BootLoader程序 SETTING 0x08003000 0x00001000(4k) 存放升级标志位/其它掉电不丢失标志位 APP 0x08004000 0x0007C000(496k) 存放产品主程序

tips: 选择以上分区方式的好处是在上电瞬间可以触发升级,在进入主程序运行期间也可以触发升级,这样就算升级了错误的程序,只要复位了再次进行升级就行,就不会导致变砖了。如果选择在主程序执行接收升级文件步骤,那如果程序有误,就很大概率会变砖,当然也有办法,那就是加校验或者将bin文件加密,这部分就不做了。

1、BootLoader部分:
  • 运行程序时首先从SETTING区域读取升级标志位,如果需要升级就一直用串口发送字符C(Ymodem协议的一部分),否则就直接跳转到APP。
  • 上电长按KEY1并复位,直接设置升级标志位进入升级,一直发送字符C,用xshell来发送升级bin文件。先将APP区域的程序全部擦除掉,然后xshell会按照Ymodem协议发送128或1024字节给MCU,MCU分别写入到APP区域内后复位即可实现升级程序,具体请查看本文源码。
2、APP部分:

        该部分需要在程序起始设置中断向量跳转指针,为了符合需求我用串口接收到‘1’就会设置程序更新标志位,复位后进入BootLoader升级。(大家也可以设置一个串口命令或者其它触发方式,只要能写更新标志位和复位就行)

二、BootLoader程序制作

        主要包含了YMODEM协议的驱动代码。

1、分区定义
#define FLASH_SECTOR_SIZE 1024 //MCU sector size#define FLASH_SECTOR_NUM 512 // 512k#define FLASH_START_ADDR ((uint32_t)0x08000000)#define FLASH_END_ADDR ((uint32_t)(0x08000000 + FLASH_SECTOR_NUM * FLASH_SECTOR_SIZE))#define BOOT_SECTOR_ADDR 0x08000000 // BOOT sector start address#define BOOT_SECTOR_SIZE 0x3000#define SETTING_SECTOR_ADDR 0x08003000 // APP设置的boot升级标志位#define SETTING_SECTOR_SIZE 0x1000#define APP_SECTOR_ADDR 0x08004000 // APP sector start address #define APP_SECTOR_SIZE 0x7C000 // APP sector size #define APP_ERASE_SECTORS (APP_SECTOR_SIZE / FLASH_SECTOR_SIZE) //507904/1024=496#define SETTING_BOOT_STATE 0x08003000#define START_PROGRAM_STATE 2#define UPDATE_PROGRAM_STATE 3#define UPDATE_SUCCESS_STATE 4typedef enum {NONE = 0, BUSY,START_PROGRAM, //进入APP主程序或者有更新就执行更新UPDATE_PROGRAM, //进入更新UPDATE_SUCCESS //更新成功写标志位}process_status; //更新状态
2、 主函数

        这部分包含了升级的所有状态,该框架可以说对比上一篇文章是完全不变的,在UPDATE_PROGRAM需要一直发送字符C来请求数据,具体看代码。

static void iap_process(void){process_status process; process = get_boot_state();switch (process) {case NONE:break;case START_PROGRAM:printf(\"start app...\\r\\n\");delay_ms(50);if ((((*(vu32*)(APP_SECTOR_ADDR+4))&0xFF000000)==0x08000000)&&(!iap_load_app(APP_SECTOR_ADDR))) {printf(\"no program\\r\\n\");delay_ms(1000);}printf(\"start app failed\\r\\n\");break;case UPDATE_PROGRAM:ymodem_c();LED1_TOGGLE; delay_ms(1000);break;case UPDATE_SUCCESS:write_setting_boot_state(START_PROGRAM_STATE);NVIC_SystemReset();break;default:break;}}int main(void){LED_GPIO_Config();ymodem_init();Key_GPIO_Config();printf(\"jin ru boot\\r\\n\");set_boot_state(read_setting_boot_state());if(Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == 1){set_boot_state(UPDATE_PROGRAM);}while(1){iap_process();}}
3、YMODEM协议的实现

        接收固件按帧来接收的,默认使用1024个字节每帧接收,起始帧和结束帧(YMODEM_SOH)都不传输数据,数据帧(YMODEM_STX)以1024个字节接收固件,所以只需要接收YMODEM_STX,至于串口收发、队列和定时器的配置就不说了,只可以意会不可言传,反正根据YMODEM协议发完一帧需要MCU给应答才会发第二帧。

推荐以下两篇文章学习YMODEM协议

Ymodem协议详解-CSDN博客

Ymodem文件传输协议_ymodem协议-CSDN博客

#define YMODEM_SOH0x01 // start of 128#define YMODEM_STX0x02 // start of 1024#define YMODEM_EOT0x04 // end of transmission#define YMODEM_ACK0x06 // positive acknowledge#define YMODEM_NAK0x15 // negative acknowledge#define YMODEM_CA0x18 // cancel终止#define YMODEM_C0x43 // control character \'C\'#define YMODEM_END 0x4F // control character \'O\'关闭传输void ymodem_ack(void) { uint8_t buf; buf = YMODEM_ACK; Usart_Send_Data(&buf, 1);}void ymodem_nack(void) { uint8_t buf; buf = YMODEM_NAK; Usart_Send_Data(&buf, 1);}void ymodem_c(void) { uint8_t buf; buf = YMODEM_C; Usart_Send_Data(&buf, 1);}void ymodem_end(void){uint8_t buf; buf = YMODEM_END; Usart_Send_Data(&buf, 1);}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, APP_ERASE_SECTORS); ymodem_ack(); ymodem_c(); ymodem.status++; } break; case 1: if (type == YMODEM_SOH || type == YMODEM_STX) { if (type == YMODEM_SOH)//起始帧、结束帧这里可以不操作flash {  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_end(); ymodem.status = 0; ymodem.process = UPDATE_SUCCESS; } } p->len = 0;}
4、程序跳转

        跳转这部分网上也很多,基本没什么区别,关于中断可能要注意一下,可能跳转之前,某些外设中断是开启的,跳转之后,中断产生了,但是APP代码中没有处理对应该中断的中断处理函数,所以就可能会直接死机。

tips:本文的开发环境只有64k ram,所以需要特别注意这里,虽然bootloader的ram不太可能会超。

if (((*(__IO uint32_t*)appxaddr) & 0x2FFF0000 ) == 0x20000000) 

详细参考以下文章:

关于STM32单片机IAP升级中if(((*(__IO uint32_t*)ulAddr_App) & 0x2FFE0000) == 0x20000000)语句的理解-CSDN博客

uint8_t iap_load_app(uint32_t appxaddr){uint8_t i; uint32_t jump_addr; if (((*(__IO uint32_t*)appxaddr) & 0x2FFF0000 ) == 0x20000000) { jump_addr = *(__IO uint32_t*) (appxaddr + 4); jump2app = (iapfun)jump_addr; /* 关闭所有中断,清除所有中断挂起标志 */ for (i = 0; i ICER[i]=0xFFFFFFFF;NVIC->ICPR[i]=0xFFFFFFFF;} __set_MSP(*(__IO uint32_t*)appxaddr); jump2app(); return 1; } return 0;}

三、APP程序制作

        这部分需要设置一下flash的偏移量,为了满足需求,还需要在串口中断函数添加触发更新标志位。

NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000);
// 串口中断服务函数void USART1_IRQHandler(void){uint8_t ucTemp;if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET){ucTemp = USART_ReceiveData(DEBUG_USARTx);USART_SendData(DEBUG_USARTx,ucTemp); if(ucTemp==\'1\'){write_setting_boot_state(UPDATE_PROGRAM_STATE);NVIC_SystemReset();}} }

四、工程配置(默认KEIL5)

BootLoader部分:0x08000000~0x08002FFF

APP部分:0x08004000~0x0807C000

五、运行测试 

        用串口助手执行升级操作,我用Xshell 8来操作(用其它支持YMODEM协议的串口助手也可以)。

1、配置Xshell 8。点击文件新建->点击连接->修改名称(随便改)和协议(选SERIAL)->点击串口->修改常规设置确认(图不截了,不会自己百度了解一下)

2、直接跳转APP。根据下图看到首先进来boot,然后直接跳转APP了。

​3、进入更新。按下KEY1按键后复位板子会重新进入boot,然后进入更新,MCU会一直发送字符C请求数据,根据下图发送bin文件给MCU接收。

可以看到已经更新成功,根据需求,大家也可以在主程序发送字符1给单片机也可以实现该功能。

tips: 测试完有个瑕疵,每次更新程序都需要把APP的全部扇区都擦除掉,那样很费时间,当然也有解决的办法,就是YMODEM协议可以抓取数据包的长度,根据数据包的长度去擦除固定的扇区,但是需要琢磨一下,我懒得弄,在我看来这个瑕疵是可以接受的。

结束语

        以上基于YMODEM协议的串口IAP升级功能已实现,这只是其中的一种升级方式,后面大家看到的也希望可以得到大家的指点或者互动,感谢各位亦菲彦祖们了。下面给出了完整工程,也包含了另一种分区(BOOT+SETTING+APP+DOWNLOAD)的代码。

完整代码下载地址:

基于YMODEM协议的串口IAP升级固件资源-CSDN下载