欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 能源 > STM32实现IAP串口升级含源码(HAL库)

STM32实现IAP串口升级含源码(HAL库)

2025/8/16 6:04:18 来源:https://blog.csdn.net/weixin_44793491/article/details/142635754  浏览:    关键词:STM32实现IAP串口升级含源码(HAL库)

文章目录

  • 一. 关于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可以有多种分区方式,各个分区的功能和分区位置如下:

分区名起始地址分区位置功能
Boot0x8000000片内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中的命令均为一个字节,当一方接收到另一方的命令时,会立即执行相应的操作:

命令命令码发送端备注
EOT0x04发送方文件传输结束命令
ACK0x06接收方接收正确应答命令
NAK0x15接收方重传当前数据包请求命令
CA0x18发送方/接收方连续发送两次为终止命令
C0x43接收方‘C’ == 0x43, 文件传输请求命令
ABORT10x41发送方‘A’ == 0x41,用户终止命令
ABORT20x61发送方‘a’ == 0x61,用户终止命令

3.3 起始帧

Ymodem起始帧并不直接传输文件内容,而是先将文件名和文件大小以字符串的形式置于起始帧中传输。起始帧是以SOH133字节长度传输的,格式如下:

名称结构长度(byte)备注
帧头SOH1起始帧固定为128字节帧
帧序号FN1起始帧的帧序号固定为 0x00
帧序号反码XFN1起始帧的帧序号反码固定为 0xFF
数据段FILENAME128要传输的文件的名字(字符串)
数据段FILESIZE要传输的文件的大小(字符串),单位为字节
数据段NULL若 FILENAME 和 FILESIZE加起来不满 128 字节,则以 0x00 填充剩余字节
校验CRC2CRC 校验

3.4 数据帧

Ymodem数据帧传输,在数据段填充有效数据:

名称结构长度(byte)备注
帧头SOH/STX1数据帧可以是 128 字节帧或 1024 字节帧;
帧序号FN1数据帧帧序号为 0~255;
帧序号反码XFN1帧序号的反码,数值为0xFF-FN;
数据段DATA128要传输的数据;
数据段NULL若 DATA 不满 128或1024 字节,则以 0x1A 填充剩余字节;
校验CRC2数据部分的 CRC 校验;

3.5 结束帧

Ymodem的结束帧采用SOH 133字节长度帧传输,该帧不携带数据(空包),即数据区、校验都以0x00填充:

名称结构长度(byte)备注
帧头SOH1结束帧为128字节帧
帧序号FN1结束帧的帧序号固定为 0x00
帧序号反码XFN1结束帧的帧序号反码固定为 0xFF
数据段DATA128结束帧的数据部分不存放任何信息,全部用 0x00 填充
校验CRC2数据部分的 CRC 校验,结束帧固定以0x00

四. IAP代码实现

开发平台: STM32F103C8T6

开发环境: Keil-5.31 / CubeMX-6.8.0

烧录工具: SecureCRT 9.5 (下载链接) / XShell 8 (下载链接)/自制上位机(下载链接)

ST官方针对不同型号的芯片都有相应的IAP升级的Demo,需要的可以自行下载。使用的都是Ymodem协议传输固件。都是是基于标准库的,其实不管用的是什么芯片和库函数,对于IAP升级的原理都是相通的。

STM32F0xxSTM32F10xxSTM32L1xxSTM32F2xxSTM32F3xxSTM32F4
DemoDemoDemoDemoDemoDemo

板子用的是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地址空间大小实际占用功能
Boot0x8000000 - 0x800300012KB10.55KB下载固件、上传固件
App0x8003000 - 0x801000054KB5.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.5XShell 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上位机),需要的可以自己下载(下载链接)。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词