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上位机),需要的可以自己下载(下载链接)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/462186.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Hello World 】

【Hello World 】! C语言实现C实现Java实现Python实现 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 几乎每一个程序员都是从Hello World!开始自己的程序人生&#xff0c;作为一个初学编程的小朋友&#xff0c;也需要先编程来输出Hello Wo…

LabVIEW程序员赚钱不仅限于上班

LabVIEW程序员拥有多种途径来实现财富增值&#xff0c;而不仅仅局限于传统的全职工作。以下是一些他们可以利用自身技能和专业知识实现更高财务收益的方法&#xff1a; 1. 专注领域的自由职业与合同工作 制造、科研、医疗技术等行业都需要LabVIEW的专业知识。通过自由职业&…

vue3项目中el-tooltip实现内容溢出时再显示,并设置tip的最大宽度

html代码 <el-tooltip :disabled"!textIsOverflow" placement"top"><template #content><div class"tooltip-div">tootip的内容</div></template><div class"textOverflow" ref"textRef"…

文案语音图片视频管理分析系统-视频矩阵

文案语音图片视频管理分析系统-视频矩阵 1.产品介绍 产品介绍方案 产品名称&#xff1a; 智驭视频矩阵深度分析系统&#xff08;SmartVMatrix&#xff09; 主要功能&#xff1a; 深度学习驱动的视频内容分析多源视频整合与智能分类高效视频检索与编辑实时视频监控与异常预警…

HTML 基础标签——分组标签 <div>、<span> 和基础语义容器

文章目录 1. `<div>` 标签特点用途示例2. `<span>` 标签特点用途示例3. `<fieldset>` 标签特点用途示例4. `<section>` 标签特点用途示例5. `<article>` 标签特点用途示例总结HTML中的分组(容器)标签用于结构化内容,将页面元素组织成逻辑区域…

安防被动红外和主动红外

被动红外探测器是依靠被动的吸收热能动物活动时身体散发出的红外热能进行报警的&#xff0c;也称热释红外探头&#xff0c;其探测器本身是不会发射红外线的。 被动红外探测器中有2个关键性元件&#xff0c;一个是菲涅尔透镜&#xff0c;另一个是热释电传感器。**自然界中任何高…

Windows下将网盘挂载到本地使用(Docker+AList+RaiDrive)

文章目录 安装安装Docker安装Alist安装RaiDrive 安装 安装Docker Windows下安装Docker网上有很多教程&#xff0c;也可以参考我写的博客链接 3.1章节 安装Alist 官网 “切换中文”并找到“使用指南” ”安装“–>"使用Docker” 打开cmd执行如下命令启动容器 do…

C语言 | Leetcode C语言题解之第519题随机翻转矩阵

题目&#xff1a; 题解&#xff1a; typedef struct {unsigned long long val;UT_hash_handle hh; } Hash;typedef struct {Hash *hash;int n_rows;int n_cols; } Solution, SL;Solution* solutionCreate(int n_rows, int n_cols) {SL *obj malloc(sizeof(SL));obj->hash …

C++之多态(上)

C之多态 多态的概念 多态(polymorphism)的概念&#xff1a;通俗来说&#xff0c;就是多种形态。多态分为编译时多态(静态多态)和运⾏时多 态(动态多态)&#xff0c;这⾥我们重点讲运⾏时多态&#xff0c;编译时多态(静态多态)和运⾏时多态(动态多态)。编译时 多态(静态多态)主…

EtherCAT转ModbusTCP相关技术

EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关https://item.taobao.com/item.htm?ftt&id822721028899 MS-GW15 概述 MS-GW15 是 EtherCAT 和 Modbus TCP 协议转换网关&#xff0c;为用户提供一种 PLC 扩展的集成解决方案&#xff0c;可以轻松容易将 Modbu…

kafka相关面试题

文章目录 什么是消息中间件&#xff1f;kafka 是什么&#xff1f;有什么作用&#xff1f;kafka 的架构是怎么样的&#xff1f;Kafka Replicas是怎么管理的&#xff1f;如何确定当前能读到哪一条消息&#xff1f;生产者发送消息有哪些模式&#xff1f;发送消息的分区策略有哪些&…

.NET Core WebApi第7讲:项目的发布与部署

一、理解 前端跟后端拿数据&#xff0c;然后在前端页面中展示&#xff0c;就是我们要完成的事情。 把前端跟后端开发好之后&#xff0c;我们需要落地部署&#xff0c;这个时候就需要一个服务器。 服务器就是一台电脑&#xff0c;只要windows里面有一个叫IIS的管理器。 二、项目…

JAVA题目笔记(十一)多态+带有抽象类/接口的JavaBean类

一、多态定义方法并调用子类特有方法 public class Animal {private String colour;private int age;public Animal(){}public Animal(int age,String colour){this.ageage;this.colourcolour;}public String getColour() {return colour;}public void setColour(String colour…

【Visual Studio】解决 CC++ 控制台程序 printf 函数输出中文和换行符显示异常

问题描述 C&C 控制台程序 printf 函数输出中文和换行符 \n 显示异常。 #include <stdio.h>int main() {int choice;printf("菜单:\n");printf("1. 选项一\n");printf("2. 选项二\n");printf("3. 选项三\n");printf("…

从线性代数到unity mvp矩阵

坐标变换&#xff1a;矩阵是一种线性空间变换的描述&#xff08;矩阵的列向量&#xff0c;是坐标变换后的基向量&#xff09;。 如: 如上图,即向量(-1,2)在经过由基底x轴:(1, -2) ,y轴:(3, 0)组成的矩阵变换后得到向量(5,2) 实际上就是-1倍的x轴:(1, -2)加上2倍的y轴:(3,…

【ShuQiHere】 如何理解渐进符号及其应用:大 O、大 Ω 和大 Θ

List item &#x1f4d8; 【ShuQiHere】 &#x1f680; 在算法复杂度分析中&#xff0c;渐进符号&#xff08;Asymptotic Notation&#xff09;是必不可少的工具&#xff0c;帮助我们估计算法的时间和空间需求&#xff0c;特别是当输入规模非常大时。这篇文章将为大家详细介绍…

Docker篇(安装容器)

目录 一、安装mysql容器 1. 拉取mysql镜像 2. 创建并运行容器 二、安装Tomcat容器 1. 拉取镜像 2. 创建并运行容器 三、安装Nginx容器 1. 拉取镜像 2. 创建并运行容器 四、安装Redis容器 1. 拉取镜像 2. 创建并运行容器 五、安装RabbitMQ 1. 拉取镜像 2. 创建并运…

Python酷库之旅-第三方库Pandas(187)

目录 一、用法精讲 866、pandas.Index.T属性 866-1、语法 866-2、参数 866-3、功能 866-4、返回值 866-5、说明 866-6、用法 866-6-1、数据准备 866-6-2、代码示例 866-6-3、结果输出 867、pandas.Index.memory_usage方法 867-1、语法 867-2、参数 867-3、功能 …

PostgreSQL 到 PostgreSQL 数据迁移同步

简述 PostgreSQL 是一个历史悠久且广泛使用的数据库&#xff0c;不仅具备标准的关系型数据库能力&#xff0c;还具有相当不错的复杂 SQL 执行能力。用户常常会将 PostgreSQL 应用于在线事务型业务&#xff0c;以及部分数据分析工作&#xff0c;所以 PostgreSQL 到 PostgreSQL …

JDK的下载

目录 JDK官网 Windows Ubantu 1.安装JDK 2.确定JDK版本 卸载OpenJDK Centos 1.下载JDK 2.安装JDK 3.验证JDK JDK官网 官网网址&#xff1a;Java Downloads | Oracle Windows 双击运⾏exe⽂件, 选择安装⽬录, 直⾄安装完成 Ubuntu 1.安装JDK 更新软件包 sudo apt u…