文章目录
- 一. 关于IAP升级
- 二. IAP升级的分类
- 二. IAP升级原理
- 2.1 正常启动流程
- 2.2 IAP启动流程
- 三. Ymodem协议
- 3.1 传输过程
- 3.2 帧命令
- 3.3 起始帧
- 3.4 数据帧
- 3.5 结束帧
- 四. IAP代码实现
- 4.1 Boot 程序
- 4.2 App 程序
- 4.3 展示效果
- 五. Demo源码
- 六. Qt 上位机
一. 关于IAP升级
IAP
,即In-Application Programming
,指的是在单片机中写入用户自己的Bootloader程序,使用微控制器支持的任一种通信接口(如I/O口、USB、CAN、UART、I2C、SPI等)下载新程序到存储器中。简单来说,就是当开发者代码出现Bug,或者需要添加新功能时,可以利用事先预留的通讯接口,对代码进行升级和维护。
IAP升级的优点是:非常灵活,可以通过各种外设给芯片进行升级,如USART、SPI、以太网、以及SD卡等,协议完全可以由自己定义,只要上位机和下位机一致既可;缺点是:IAP中的Bootloader会占用一部分MCU的flash资源。
二. IAP升级的分类
IAP可以有多种分区方式,各个分区的功能和分区位置如下:
分区名 | 起始地址 | 分区位置 | 功能 |
---|---|---|---|
Boot | 0x8000000 | 片内Flash | 升级固件下载、本地应用程序更新 |
App | 自定义 | 片内Flash | 升级固件下载、应用程序执行 |
Download(可选) | 自定义 | 片内Flash、片外SPI Flash | 存储待升级固件 |
Factory (可选) | 自定义 | 片内Flash、片外SPI Flash | 存储出厂固件 |
忽略Factory分区,根据是否有Download分区
以及下载固件的区域不同(Boot/App)
可以大致分为以下三种IAP升级方式:
固件下载区域 | Download分区 | 优势 | 局限 | |
---|---|---|---|---|
方式一 | Boot下载固件 | 无 Download | 节省存储空间 | 1. 固件下载功能不能被升级 2. 断网或中断需要手动复位 |
方式二 | 有 Download | 断网或中断不会死机 | 1. 固件下载功能不能被升级 2. 断网或中断需要手动复位 | |
方式三 | App下载固件 | 1. 断网或中断不会死机 2. 固件下载功能可以升级 | 需要较大内存 |
本文主要针对方式一进行进一步讲解,即只有App与Boot分区,且在Boot分区对固件进行下载。这是最简单的IAP升级方式。当板子上电时,首先进入Boot分区,判断固件是否需要更新,如果是则对固件进行下载,同时覆盖掉在App的原有固件,更新完毕后则跳转至App运行应用程序。
二. IAP升级原理
2.1 正常启动流程
在了解IAP之前,我们需要事先了解STM32单片机从上电到运行的过程正常启动流程。
1. 将 0x08000000 位置存放的堆栈栈顶地址
存放到 SP 中(MSP)。
2. 将 0x08000004 位置存放的复位中断向量
装入 PC 程序计数器。
3. 开始执行复位中断服务程序 Reset_Handler()
。复位中断服务程序会调用SystemInit()
来对时钟和其他外设进行初始化,然后跳转到 C 库中__main 函数
。
4. 由 C 库中的__main 函数完成用户程序的初始化工作,最后由__main() 调用用户写的main()
,开始执行用户的应用程序App。
2.2 IAP启动流程
要想实现远程升级,我们需要把Boot程序放在Flash的起始地址。板子上电时,依然从0x08000004处取出复位中断向量地址,执行复位中断函数后跳转到Boot的main。
在Boot的main函数判断升级条件,如果满足升级条件则用接收的新程序覆盖旧的用户程序,否则强制跳转到0x08000004+N+M处(即用户程序中断向量表的复位中断地址处)执行复位中断程序,最后跳转到用户main函数中,运行用户应用程序App。
需要注意的是:在执行应用程序的时候,需要先设置中断向量表的偏移。否则App程序中的中断还是会去Bootloader的中断向量表中获取中断服务函数,这里可能会导致运行死机的情况。
当我们设置中断向量表偏移量为N+M,在Boot程序中的main函数的执行时,如果CPU得到一个中断请求,PC指针会被强制跳转到0x08000004+N+M处的中断向量表中得到相应的中断函数地址,再跳转到相应新的中断服务函数,执行结束后返回到main函数中来。
三. Ymodem协议
如果下载固件用的是串口通信,局限于MCU的RAM有限,我们很多时候不可能用串口接收完整个固件进缓冲接收区,因此我们需要使用串口分包接收,然后写入Flash。文件传输协议有很多种,本文主要讲述使用频率最多的Ymodem协议。
YModem 协议是由 XModem 协议演变而来的,每包数据可以达到 1024 字节,是一个非常高效的文件传输协议。我们平常所说的 Ymodem 协议是指的 Ymodem-1K,除此还有 Ymodem-g(没有 CRC 校验,不常用)。YModem-1K 协议用 1024 字节数据帧传输取代了XModem的 128 字节数据帧传输,发送的数据会使用 CRC 校验,保证数据传输的正确性。它每传输一个信息块时,就会等待接收端返回 ACK 信号,接收到响应信号后,才会继续传输下一个信息块,从而保证能够接收到全部数据。
3.1 传输过程
3.2 帧命令
Ymodem中的命令均为一个字节,当一方接收到另一方的命令时,会立即执行相应的操作:
命令 | 命令码 | 发送端 | 备注 |
---|---|---|---|
EOT | 0x04 | 发送方 | 文件传输结束命令 |
ACK | 0x06 | 接收方 | 接收正确应答命令 |
NAK | 0x15 | 接收方 | 重传当前数据包请求命令 |
CA | 0x18 | 发送方/接收方 | 连续发送两次为终止命令 |
C | 0x43 | 接收方 | ‘C’ == 0x43, 文件传输请求命令 |
ABORT1 | 0x41 | 发送方 | ‘A’ == 0x41,用户终止命令 |
ABORT2 | 0x61 | 发送方 | ‘a’ == 0x61,用户终止命令 |
3.3 起始帧
Ymodem起始帧并不直接传输文件内容,而是先将文件名和文件大小以字符串的形式置于起始帧中传输。起始帧是以SOH133字节长度传输的,格式如下:
名称 | 结构 | 长度(byte) | 备注 | |
---|---|---|---|---|
帧头 | SOH | 1 | 起始帧固定为128字节帧 | |
帧序号 | FN | 1 | 起始帧的帧序号固定为 0x00 | |
帧序号反码 | XFN | 1 | 起始帧的帧序号反码固定为 0xFF | |
数据段 | FILENAME | 128 | 要传输的文件的名字(字符串) | |
数据段 | FILESIZE | 要传输的文件的大小(字符串),单位为字节 | ||
数据段 | NULL | 若 FILENAME 和 FILESIZE加起来不满 128 字节,则以 0x00 填充剩余字节 | ||
校验 | CRC | 2 | CRC 校验 |
3.4 数据帧
Ymodem数据帧传输,在数据段填充有效数据:
名称 | 结构 | 长度(byte) | 备注 | |
---|---|---|---|---|
帧头 | SOH/STX | 1 | 数据帧可以是 128 字节帧或 1024 字节帧; | |
帧序号 | FN | 1 | 数据帧帧序号为 0~255; | |
帧序号反码 | XFN | 1 | 帧序号的反码,数值为0xFF-FN; | |
数据段 | DATA | 128 | 要传输的数据; | |
数据段 | NULL | 若 DATA 不满 128或1024 字节,则以 0x1A 填充剩余字节; | ||
校验 | CRC | 2 | 数据部分的 CRC 校验; |
3.5 结束帧
Ymodem的结束帧采用SOH 133字节长度帧传输,该帧不携带数据(空包),即数据区、校验都以0x00填充:
名称 | 结构 | 长度(byte) | 备注 |
---|---|---|---|
帧头 | SOH | 1 | 结束帧为128字节帧 |
帧序号 | FN | 1 | 结束帧的帧序号固定为 0x00 |
帧序号反码 | XFN | 1 | 结束帧的帧序号反码固定为 0xFF |
数据段 | DATA | 128 | 结束帧的数据部分不存放任何信息,全部用 0x00 填充 |
校验 | CRC | 2 | 数据部分的 CRC 校验,结束帧固定以0x00 |
四. IAP代码实现
开发平台: STM32F103C8T6
开发环境: Keil-5.31 / CubeMX-6.8.0
烧录工具: SecureCRT 9.5 (下载链接) / XShell 8 (下载链接)/自制上位机(下载链接)
ST官方
针对不同型号的芯片都有相应的IAP升级的Demo
,需要的可以自行下载。使用的都是Ymodem协议传输固件。都是是基于标准库
的,其实不管用的是什么芯片和库函数,对于IAP升级的原理都是相通的。
STM32F0xx | STM32F10xx | STM32L1xx | STM32F2xx | STM32F3xx | STM32F4 |
---|---|---|---|---|---|
Demo | Demo | Demo | Demo | Demo | Demo |
板子用的是TB买的STM32F103C8T6
最小系统板,加了CH340C实现TTL信号转USB信号,可以实现MCU与PC的串口通信,所以就直接以它作为基础使用HAL库
写了一个IAP升级的程序,包含Boot和App。已经把写好的Demo打包上传至CSDN(包括Boot程序、App程序、板子资料),需要的可以自己下载(下载链接)
STM32F103C8T6一共有64KB的Flash,我们需要根据自身情况给Boot与App分配合适的空间,这里给Boot分配12KB,App分配54KB。烧录时只需要把对应的程序烧录在MCU对应的Flash地址上即可。
分区 | Flash地址 | 空间大小 | 实际占用 | 功能 |
---|---|---|---|---|
Boot | 0x8000000 - 0x8003000 | 12KB | 10.55KB | 下载固件、上传固件 |
App | 0x8003000 - 0x8010000 | 54KB | 5.13KB | 用户的应用程序 |
具体设置为Keil ->Options for Target ->Target -> Read/Only Memory Areas-> IROM1
4.1 Boot 程序
Boot程序的实现可以分为三个部分,分别是:串口终端的菜单列表、Ymodem协议、Flash读写。
终端菜单列表 menu.c:
/* Includes ------------------------------------------------------------------*/
#include "menu.h"/* Private variables ---------------------------------------------------------*/
pFunction Jump_To_Application;
uint32_t JumpAddress;
uint8_t tab_1024[1024] ={0};
uint32_t FlashProtection = 0;extern uint8_t FileName[FILE_NAME_LENGTH];
extern UART_HandleTypeDef huart1;/* Private functions ---------------------------------------------------------*//*** @brief 串口下载固件* @param None* @retval None*/
void SerialDownload(void)
{uint8_t Number[10] = {0};int32_t Size = 0;printf("Waiting for the file to be sent ... (press 'a' to abort)\n\r");/* Ymodem接受文件并写入flash中 */Size = Ymodem_Receive(&tab_1024[0]);if (Size > 0){HAL_Delay(200);printf("\nProgramming Completed Successfully!\n\r-------------------\r\n Name: ");printf("%s",FileName);Int2Str(Number, Size);printf("\n\r Size: ");printf("%s",Number);printf(" Bytes\r\n");printf("-------------------\n");}else if (Size == -1){printf("\n\rThe image size is higher than the allowed space memory!\n\r");}else if (Size == -2){printf("\n\rVerification failed!\n\r");}else if (Size == -3){printf("\r\nAborted by user.\n\r");}else{printf("\n\rFailed to receive the file!\n\r");}
}/*** @brief 串口上传固件* @param None* @retval None*/
void SerialUpload(void)
{uint8_t status = 0 ; printf("\n\n\rSelect Receive File\n\r");if (GetKey() == CRC16){/* Ymodem发送现有的App固件至上位机 */status = Ymodem_Transmit((uint8_t*)APPLICATION_ADDRESS, (const uint8_t*)"UploadedFlashImage.bin", USER_FLASH_SIZE);if (status != 0) {printf("\n\rError Occurred while Transmitting File\n\r");}else{printf("\n\rFile uploaded successfully \n\r");}}
}void Main_Menu ()
{uint8_t key = 0;uint8_t writeprotect = 0;printf("\r\n ======================================================================");printf("\r\n| CSDN: https://lindoglog.blog.csdn.net |");printf("\r\n| |");printf("\r\n| Title: STM32F1xx In-Application Programming Demo |");printf("\r\n| |");printf("\r\n| Author: DongGua~ |");printf("\r\n ======================================================================");printf("\r\n");/* Test if any sector of Flash memory where user application will be loaded is write protected */if (FLASH_GetWriteProtectionStatus() != 0) {FlashProtection = 1;}else{FlashProtection = 0;}while(1){printf("\r\n ============================= Main Menu ==============================\r\n");printf(" Press <1> Download Image To the STM32F1xx Internal Flash - - - - - 1\r\r\n");printf(" Press <2> Upload Image From the STM32F1xx Internal Flash - - - - - 2\r\n");printf(" press <3> Execute The New Program- - - - - - - - - - - - - - - - - 3\r\n");if(FlashProtection != 0){printf("\r\n Disable the write protection - - - - - - - - - - - - - - - 4\r\n");}printf(" =======================================================================\r\n");key = GetKey();if (key == 0x31){/* 下载固件至Flash中 */SerialDownload();}else if (key == 0x32){/* 从Flash中上传固件 */SerialUpload();}else if (key == 0x33) {JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);Jump_To_Application = (pFunction) JumpAddress;__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);/* 跳转至App程序中 */Jump_To_Application();}else if ((key == 0x34) && (FlashProtection == 1)){/* Disable the write protection */writeprotect = FLASH_DisableWriteProtection();switch (writeprotect){case 0:{printf("Write Protection disabled...\r\n");printf("...and a System Reset will be generated to reload the new option bytes\r\n");/* Launch loading new option bytes */HAL_FLASH_OB_Launch();break;}case 1:{printf("Error: Flash write unprotection failed...\r\n");break;}case 2:{printf("Flash memory not write protected\r\n");break;}default:{}}}else{if (FlashProtection == 0){printf("Invalid Number ! ==> The number should be either 1, 2 or 3\r");}else{printf("Invalid Number ! ==> The number should be either 1, 2, 3 or 4\r");}}}
}
Ymodem协议的实现 ymodem.c:
需要注意的是,CubeMX默认生成的代码里,MCU的最小栈为0x400,即1024个字节,但是在Ymodem传输过程中,我们的缓冲数组大小就为1029(1024+2+3) 个字节,为了保证代码运行过程有足够的栈空间,可以设置为0x800,即2048字节,具体位置为CubeMX ->Project Manager-> Linker Settings ->Minimum Stack Size
/* Includes ------------------------------------------------------------------*/
#include "ymodem.h"
#include "flash.h"
#include "common.h"
#include <string.h>/* Private variables ---------------------------------------------------------*/
uint8_t FileName[FILE_NAME_LENGTH];
extern uint8_t Data_Size;
extern uint8_t Data_input[128];
extern UART_HandleTypeDef huart1;/* Private functions ---------------------------------------------------------*//*** @brief 发送一个字节* @param c: 发送的自己* @retval */
static uint32_t Send_Byte (uint8_t c)
{HAL_UART_Transmit(&huart1,&c,1,0x01);return 0;
}/*** @brief 为输入字节更新CRC16* @param CRC输入值* @param 输入字节* @retval */
uint16_t UpdateCRC16(uint16_t crcIn, uint8_t byte)
{uint32_t crc = crcIn;uint32_t in = byte|0x100;do{crc <<= 1;in <<= 1;if(in&0x100){++crc;}if(crc&0x10000){crc ^= 0x1021;}} while(!(in&0x10000));return (crc&0xffffu);
}/*** @brief 为Ymodem包计算校验(CRC16)* @param 数组* @param 长度* @retval CRC值*/
uint16_t Cal_CRC16(const uint8_t* data, uint32_t size)
{uint32_t crc = 0;const uint8_t* dataEnd = data+size;while(data<dataEnd){crc = UpdateCRC16(crc,*data++);}crc = UpdateCRC16(crc,0);crc = UpdateCRC16(crc,0);return (crc&0xffffu);
}/*** @brief 计算数据的校验和(Checksum)* @param 数组* @param 长度* @retval 累加值低8位*/
uint8_t CalChecksum(const uint8_t* data, uint32_t size)
{uint32_t sum = 0;const uint8_t* dataEnd = data+size;while(data < dataEnd){sum += *data++;}return (sum&0xffu);
}/**
* @brief 从发送方接受一个字节
* @param c: 接受字节
* @param timeout: 超时时间
* @retval 0: 成功
* -1: 超时
*/
static int32_t Receive_Byte (uint8_t *c, uint32_t timeout)
{if (HAL_UART_Receive(&huart1,c,1,timeout)!=HAL_OK){return -1;}return 0;
}/*** @brief 接受发送方一个帧的数据* @param 数组* @param 长度* @param 超时时间* @retval 0: 成功接收并校验了数据包* -1: 接收失败或发生错误* 1: 接收到中断传输的命令*/
static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
{uint16_t packet_size;uint16_t computedcrc; uint8_t c;*length = 0;if (Receive_Byte(&c, timeout) != 0){return -1; }switch (c){case SOH:packet_size = PACKET_SIZE; break;case STX:packet_size = PACKET_1K_SIZE;break;case EOT:return 0;case CA: if ((Receive_Byte(&c, timeout) == 0) && (c == CA)){*length = -1;return 0;}else{return -1;}case ABORT1:case ABORT2: return 1;default:return -1;}*data = c;uint16_t i;for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++){if (Receive_Byte(data + i, timeout) != 0){return -1;}}if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff)){return -1;}computedcrc = Cal_CRC16(&data[PACKET_HEADER], (uint32_t)packet_size);if (computedcrc != (uint16_t)((data[packet_size+3]<<8) | data[packet_size+4])){return -1;}*length = packet_size;return 0;
}/*** @brief 使用Ymodem协议接受一个文件* @param buf: 第一个字节的地址* @retval 返回文件大小*/
int32_t Ymodem_Receive (uint8_t *buf)
{uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD];//一帧数据[1024+3+2]uint8_t file_size[FILE_SIZE_LENGTH]; //文件大小(字符串)uint8_t *file_ptr; //起始帧的数组指针uint8_t *buf_ptr; //文件数据段的数组指针int32_t packet_length; //数据段长度(1024/128)int32_t session_done; //传输异常结束标志位int32_t packets_received;//帧的序号(0~255)int32_t errors; //传输的错误int32_t session_begin; //通信开始标志位int32_t file_done = 0; //传输正常结束标志位int32_t size = 0; //文件大小(int)uint32_t flashdestination;//Flash写入的地址uint32_t ramsource; //写入Flash的数组地址flashdestination = APPLICATION_ADDRESS;for (session_done = 0, errors = 0, session_begin = 0; ;){for (packets_received = 0, buf_ptr = buf; ;){switch (Receive_Packet(packet_data, &packet_length, ACK_TIMEOUT)){case 0:errors = 0;switch (packet_length){case - 1:Send_Byte(ACK);return 0;case 0:if(file_done==0){Send_Byte(NAK);file_done = 1;}else{Send_Byte(ACK);Send_Byte(CRC16);break;}default:if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff)){Send_Byte(NAK);}else{if (packets_received == 0){if (packet_data[PACKET_HEADER] != 0){int32_t i;for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);){FileName[i++] = *file_ptr++;}FileName[i++] = '\0';for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < (FILE_SIZE_LENGTH - 1));){file_size[i++] = *file_ptr++;}file_size[i++] = '\0';Str2Int(file_size, &size);if (size > (USER_FLASH_SIZE + 1)){Send_Byte(CA);Send_Byte(CA);return -1;}FLASH_Erase(APPLICATION_ADDRESS);Send_Byte(ACK);Send_Byte(CRC16);}else{Send_Byte(ACK);file_done = 1;session_done = 1;
// Send_Byte(XSHELL_OVER); //XShell独有的结束符号break;}}else{ memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);ramsource = (uint32_t)buf;if (FLASH_Write(&flashdestination, (uint32_t*) ramsource, (uint16_t) packet_length/4) == 0){Send_Byte(ACK);}else {Send_Byte(CA);Send_Byte(CA);return -2;}}packets_received ++;session_begin = 1; }}break;case 1:Send_Byte(CA);Send_Byte(CA);return -3;default:if (session_begin > 0){errors ++;}if (errors > MAX_ERRORS){Send_Byte(CA);Send_Byte(CA);return 0;}Send_Byte(CRC16);break;}if (file_done != 0){break;}}if (session_done != 0){break;}}return (int32_t)size;
}/*** @brief 准备起始帧* @param 超时时间* @retval None*/
void Ymodem_PrepareIntialPacket(uint8_t *data, const uint8_t* fileName, uint32_t *length)
{uint16_t i, j;uint8_t file_ptr[10];data[0] = SOH;data[1] = 0x00;data[2] = 0xff;for (i = 0; (fileName[i] != '\0') && (i < FILE_NAME_LENGTH);i++){data[i + PACKET_HEADER] = fileName[i];}data[i + PACKET_HEADER] = 0x00;Int2Str (file_ptr, *length);for (j =0, i = i + PACKET_HEADER + 1; file_ptr[j] != '\0' ; ){data[i++] = file_ptr[j++];}for (j = i; j < PACKET_SIZE + PACKET_HEADER; j++){data[j] = 0;}
}/*** @brief 准备数据帧* @param timeout* @retval None*/
void Ymodem_PreparePacket(uint8_t *SourceBuf, uint8_t *data, uint8_t pktNo, uint32_t sizeBlk)
{uint16_t i, size, packetSize;uint8_t* file_ptr;packetSize = sizeBlk >= PACKET_1K_SIZE ? PACKET_1K_SIZE : PACKET_SIZE;size = sizeBlk < packetSize ? sizeBlk :packetSize;if (packetSize == PACKET_1K_SIZE){data[0] = STX;}else{data[0] = SOH;}data[1] = pktNo;data[2] = (~pktNo);file_ptr = SourceBuf;for (i = PACKET_HEADER; i < size + PACKET_HEADER;i++){data[i] = *file_ptr++;}if ( size <= packetSize){for (i = size + PACKET_HEADER; i < packetSize + PACKET_HEADER; i++){data[i] = 0x1A; }}
}/**
* @brief 使用Ymodem协议发送一帧数据* @param 数据* @param 长度* @retval None*/
void Ymodem_SendPacket(uint8_t *data, uint16_t length)
{uint16_t i;i = 0;while (i < length){Send_Byte(data[i]);i++;}
}/*** @brief 通过Ymodem协议发送一个文件* @param buf: 第一个字节的地址* @retval 文件大小*/
uint8_t Ymodem_Transmit (uint8_t *buf, const uint8_t* sendFileName, uint32_t sizeFile)
{uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD];uint8_t FileName[FILE_NAME_LENGTH];uint8_t *buf_ptr, tempCheckSum ;uint16_t tempCRC, blkNumber;uint8_t receivedC[2], CRC16_F = 0, i;uint32_t errors = 0, ackReceived = 0, size = 0, pktSize;for (i = 0; i < (FILE_NAME_LENGTH - 1); i++){FileName[i] = sendFileName[i];}CRC16_F = 1;Ymodem_PrepareIntialPacket(&packet_data[0], FileName, &sizeFile);do {Ymodem_SendPacket(packet_data, PACKET_SIZE + PACKET_HEADER);if (CRC16_F){tempCRC = Cal_CRC16(&packet_data[3], PACKET_SIZE);Send_Byte(tempCRC >> 8);Send_Byte(tempCRC & 0xFF);}else{tempCheckSum = CalChecksum (&packet_data[3], PACKET_SIZE);Send_Byte(tempCheckSum);}/* 等待Ack和指令'C' */if (Receive_Byte(&receivedC[0], ACK_TIMEOUT) == 0 && receivedC[0] == ACK) {if(Receive_Byte(&receivedC[1], CRC16_TIMEOUT) == 0){if (receivedC[1]==CRC16){ ackReceived = 1;}}}else{errors++;}}while (!ackReceived && (errors < 0x0A));if (errors >= 0x0A){return errors;}buf_ptr = buf;size = sizeFile;blkNumber = 0x01;while (size){Ymodem_PreparePacket(buf_ptr, &packet_data[0], blkNumber, size);ackReceived = 0;receivedC[0]= 0;receivedC[1]= 0;errors = 0;do{if (size >= PACKET_1K_SIZE){pktSize = PACKET_1K_SIZE;}else{pktSize = PACKET_SIZE;}Ymodem_SendPacket(packet_data, pktSize + PACKET_HEADER);if (CRC16_F){tempCRC = Cal_CRC16(&packet_data[3], pktSize);Send_Byte(tempCRC >> 8);Send_Byte(tempCRC & 0xFF);}else{tempCheckSum = CalChecksum (&packet_data[3], pktSize);Send_Byte(tempCheckSum);}/* 等待Ack */if (Receive_Byte(&receivedC[0], ACK_TIMEOUT) == 0) { if (receivedC[0] == ACK){ackReceived = 1; if (size > pktSize){buf_ptr += pktSize; size -= pktSize;if (blkNumber == (USER_FLASH_SIZE/1024)){return 0xFF; }else{blkNumber++;}}else{buf_ptr += pktSize;size = 0;}}}else{errors++;}}while(!ackReceived && (errors < 0x0A));if (errors >= 0x0A){return errors;}}ackReceived = 0;receivedC[0] = 0x00;receivedC[1] = 0x00;errors = 0;do { Send_Byte(EOT); if (Receive_Byte(&receivedC[0], ACK_TIMEOUT) == 0) {if(receivedC[0] == ACK && (Receive_Byte(&receivedC[1], CRC16_TIMEOUT) == 0)){if (receivedC[1]==CRC16){ ackReceived = 1;}}}else{errors++;} }while (!ackReceived && (errors < 0x0A));if (errors >= 0x0A){return errors;}/* 准备结束帧 */ackReceived = 0;receivedC[0] = 0x00;receivedC[1] = 0x00;errors = 0;packet_data[0] = SOH;packet_data[1] = 0;packet_data [2] = 0xFF;for (i = PACKET_HEADER; i < (PACKET_SIZE + PACKET_HEADER); i++){packet_data [i] = 0x00;}do {Ymodem_SendPacket(packet_data, PACKET_SIZE + PACKET_HEADER);tempCRC = Cal_CRC16(&packet_data[3], PACKET_SIZE);Send_Byte(tempCRC >> 8);Send_Byte(tempCRC & 0xFF);/* 等待Ack */if ((Receive_Byte(&receivedC[0], ACK_TIMEOUT) == 0) && receivedC[0] == ACK){ackReceived = 1; }else{errors++;} }while (!ackReceived && (errors < 0x0A));if (errors >= 0x0A){return errors;}/* 文件传输成功 */return 0;
}
Flash读写 Flash.c:
/* Includes ------------------------------------------------------------------*/
#include "flash.h"/* Private functions ---------------------------------------------------------*//*** @brief 解锁Flash写权限* @param None* @retval None*/
void FLASH_Init(void)
{ HAL_FLASH_Unlock();
}/*** @brief 擦除用户App程序的闪存区域* @param StartSector: Flash的起始区域* @retval 0: 成功* 1: 失败*/
uint32_t FLASH_Erase(uint32_t StartSector)
{FLASH_EraseInitTypeDef myFlash;myFlash.PageAddress = StartSector;myFlash.NbPages = (uint32_t)(0x8010000-StartSector)/0x400;myFlash.TypeErase = FLASH_TYPEERASE_PAGES;uint32_t PageError = 0;if (HAL_FLASHEx_Erase(&myFlash,&PageError) != HAL_OK){return (1);}return (0);
}/*** @brief 在flash中写入一组数据(数据是32位对齐的)* @note 写入数据缓冲区后,检查flash内容。* @param FlashAddress: 写入Flash的起始地址* @param Data: 数据缓冲区上的指针* @param DataLength: 数组长度(单位为32位字)* @retval 0: 数据写入Flash成功* 1: 向闪存写入数据时发生错误* 2: 写入数据与预期数据不一致*/
uint32_t FLASH_Write(__IO uint32_t* FlashAddress, uint32_t* Data ,uint16_t DataLength)
{uint32_t i = 0;for (i = 0; (i < DataLength) && (*FlashAddress <= (USER_FLASH_END_ADDRESS-4)); i++){if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,*FlashAddress,*(uint32_t*)(Data+i))==HAL_OK){if (*(uint32_t*)*FlashAddress != *(uint32_t*)(Data+i)){ return(2);}*FlashAddress += 4;}else{return (1);}}return (0);
}/*** @brief 禁用用户所需页面的写保护* @param None* @retval 0: 成功取消写保护* 1: Flash取消写保护失败* 2: Flash不受写保护*/
uint32_t FLASH_DisableWriteProtection(void)
{uint32_t UserMemoryMask = 0, WRPR = 0;HAL_StatusTypeDef status = HAL_BUSY;FLASH_OBProgramInitTypeDef myOBInit;HAL_FLASHEx_OBGetConfig(&myOBInit);WRPR = myOBInit.WRPPage;if ((~WRPR & FLASH_PROTECTED_PAGES) != 0x00){HAL_FLASH_OB_Unlock(); status = HAL_FLASHEx_OBErase();UserMemoryMask = FLASH_PROTECTED_PAGES | WRPR;if (UserMemoryMask != 0xFFFFFFFF){myOBInit.WRPPage = (uint32_t)~UserMemoryMask;status = HAL_FLASHEx_OBProgram(&myOBInit);}if (status == HAL_OK){return (0);}else{return (1);}}else{return(2);}
}/*** @brief 返回FLASH的写保护状态* @param None* @retval 如果扇区是写保护的,则返回值为1* 如果扇区不受写保护,则返回值为0*/
uint32_t FLASH_GetWriteProtectionStatus(void)
{ FLASH_OBProgramInitTypeDef myOBInit;HAL_FLASHEx_OBGetConfig(&myOBInit);return(~myOBInit.WRPPage & FLASH_PROTECTED_PAGES);
}
4.2 App 程序
App里的程序主要是用户根据自己需求来写,这里为了展示,就打印出固件的版本信息。需要注意的是,App的程序需要在main中设置中断向量表的偏移
,App的实际地址为0x8003000 - 0x8010000,偏移值为0x3000。
main.c:
....................SCB->VTOR = FLASH_BASE | 0x3000;//设置中断向量表的偏移,偏移值为0x3000printf("\r\n");printf("\r\n ======================================================================");printf("\r\n| The program successfully entered the App |");printf("\r\n ======================================================================");printf("\r\n");/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */printf(" Upgrade firmware successfully \r\n");printf(" The firmware version is V2.0.0 \r\n");HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);HAL_Delay(4000);}......................
IAP固件升级需要使用App的bin文件,还需要再Option->User->After Build/Rebuild里添加指令fromelf --bin !L --output XXXX.bin,这样编译后就会自动生成所需要的bin文件。
4.3 展示效果
软件使用的是SecureCRT 9.5
和XShell 8
。
固件升级:
过程: 板子上电时,按住PA0按键的同时,按下Reset按键,电路复位后会进入Boot程序,在SecureCRT中可以看到进入Menu菜单列表,键盘输入1进入下载模式,选择Ymodem升级的Bin文件,MCU就会自己下载程序,并且在Flash中自动覆盖掉原有的固件,程序支持Ymodem(1024字节),也兼容Ymodem(128字节)。下载完成后,键盘输入3,Boot程序自动跳转至App程序。
注意: 实际使用过程中,发现Ch340C接SecureCRT或者XShell的时候,板子没办法正常启动,接ST-Link也无法进入DeBug,检查发现是CH340C的RST和DTR引脚分别接了芯片的RESET和BOOT0引脚,在SecureCRT/XShell启动时,导致STM32频繁复位以及从系统存储器启动而非用户的Flash。最后的解决办法是把CH340C的RST和DTR引脚用烙铁敲掉,就可以恢复正常串口通信,但是原厂的一键烧录功能就没了。
SecureCRT 9.5:
固件读取:
同样方式进入Menu列表,键盘输入2进入固件读取模式,此时打开Ymodem接受,就可以读出STM32 App分区的固件。固件是从0x8003000地址一直读至0x8010000的,所以虽然程序只有5KB,但是读出来的Bin文件有52KB。固件可以重新烧录至MCU中,全部功能都测试过,没有任何问题。
SecureCRT 9.5:
XShell 8:
注意: 如果使用XShell的ymodem协议传输数据时,会发现明明看到数据已经传输完毕但没有显示传输完成,然后进度一直卡在100%,这是因为XShell有自己特殊的结束字符O (0x4F),加上即可实现“传输完毕”。
五. Demo源码
已经把写好的Demo打包上传至CSDN(包括Boot程序、App程序、板子资料),需要的可以自己下载(下载链接)。
六. Qt 上位机
自己还基于QT写了个上位机可以用于MCU固件的烧录和下载,配合上面编写的Boot程序和App程序使用。
固件升级:
固件读取:
QT的代码也已经打包上传至CSDN(包括Boot程序、App程序、QT上位机),需要的可以自己下载(下载链接)。