STM32H743-ARM例程38-UART-IAP
目录
- 实验平台
- IAP
- Xmodem 协议
- 协议细节
- 总结
- STM32CubeMX生成工程
- 实验代码
- 实验现象
实验平台
硬件:银杏科技GT7000双核心开发板-ARM-STM32H743XIH6,银杏科技iToolXE仿真器
软件:最新版本STM32CubeH7固件库,STM32CubeMX v6.10.0,开发板环境MDK v5.35,TCP&UDP测试工具,串口工具Extraputty
网盘资料包:链接: https://pan.baidu.com/s/1Y3nwaY4SMxfoUsdqPm2R3w?pwd=inba 提取码: inba
IAP
IAP 是 In Application Programming 的首字母缩写,IAP 是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。
在应用编程 IAP(In-Application Programming)是应用在 Flash 程序存储器的一种编程模式。 它可以在应用程序正常运行的情况下,通过调用特定的 IAP 程序对另外一段程序 Flash 空间进行读/写操作,甚至可以控制对某段、某页甚至某个字节的读/写操作,这为数据存储和固 件的现场升级带来了更大的灵活性。 通常在用户需要实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信管道(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:1.检查是否需要对第二部分代码进行更新;2. 如果不需要更新则转到 4;3.执行更新操作;4.跳转到第二部分代码执行。
第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一道烧入,以后需要程序更新是再通过第一 部分 IAP 代码更新。
我们将第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP 程序,他们存放在 STM32H743FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟 其后的就是 APP 程序,这样我们就是要实现 2 个程序:Bootloader 和 APP。
我们先来看看 STM32H7 正常的程序运行流程(为了方便说明 IAP 过程,我们先仅考虑 代码全部存放在内部 FLASH 的情况),如下图所示:

STM32H7 的内部闪存(FLASH)地址起始于 0X0800 0000, 一般情况下,程序文件就从此地址开始写入。此外 STM32H743 是基于 Cortex-M7 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动, 而这张“中断向量表”的起始地址是 0x08000004,当中断来临, STM32H743 的内部硬件机制亦会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
在上图中, STM32H743 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生了中断),此时 STM32H743 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④ 所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
当加入 IAP 程序之后,程序运行流程如下图所示:

在上图所示流程中, STM32H743 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,此部分同正常的程序运行流程图一样;在执行完 IAP 以后(即将新的 APP 代码写入 STM32H743 的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数, 如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32H743 的 FLASH,在不同位置上,共有两个中断向量表。
在 main 函数执行过程中,如果 CPU 得到一个中断请求, PC 指针仍然会强制跳转到地址 0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
1) 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
在本实验中,STM32 具有 IAP(在应用编程)功能,实现用户程序在运行的过程中对STM32片内 Flash 部分区域进行烧写来实现固件程序的更新升级,本实验有两部分代码(Bootloader(IAP) 程序和 APP 程序),第一部分代码不执行正常的功能操作,而是通过串口接收程序和数据,来执行对第二部分代码的更新,第二部分代码才是真正的功能代码。如果直接上电则执行第二部分代码,如果按下 ARM-KEY 上电,则执行第二部分代码的更新升级。
Xmodem 协议
本章实验我们通过UART串口,传输APP程序,采取Xmodem协议。
Xmodem是一种广泛使用的文件传输协议,设计用于通过串行通信(如RS-232)传输小型文件。协议最初由Ward Christensen在1977年提出,这是一个非常经典且简单的文件传输协议,即使在今天,在许多嵌入式系统、Bootloader 以及需要通过串口等不稳定链路传输文件的场景中依然被广泛使用。
-
传输模式: 半双工。通信双方轮流发送和接收,不能同时进行。
-
数据块: 将文件分割成固定大小的数据块进行传输。
-
纠错方式: 使用校验和或CRC进行错误检测,通过重传机制进行纠错。
-
核心思想: “发送-确认-重传”。接收方对每一个收到的数据块进行校验,如果正确则回复ACK,通知发送方发送下一个块;如果错误则回复NAK,请求发送方重传当前块。
协议细节
1.数据包格式
| 组成部分 | 大小(字节) | 说明 |
|---|---|---|
| 起始字节 | 1 | 固定为 SOH(Start Of Header),其 ASCII 值为 0x01。表示一个新的数据包开始。 |
| 包序号 | 1 | 从 1 开始计数,第一个包的序号是 1,第二个是 2,… 一直到 255 后回绕到 0。 |
| 包序号的补码 | 1 | 是包序号的按位取反(~包序号)。用于验证包序号本身在传输中是否出错。例如,包序号是 1 (0x01),则补码为 254 (0xFE)。 |
| 数据区 | 128 | 固定为 128 字节。如果文件最后一块不足 128 字节,用 Ctrl+Z (0x1A) 或 NULL (0x00) 填充。 |
| 校验和 | 1 | 对 128 字节的数据区 进行算术求和,然后取 256 的模(即只保留最低的一个字节)。 |
数据包结构:
| SOH (0x01) | 包序号 (1-255) | ~包序号 | 数据 (128 Bytes) | 校验和 (1 Byte) |
2. 通信流程

启动传输(握手)
-
接收方首先发送一个 NAK 字节(0x15),表示它已准备好,并请求发送方开始传输。接收方会持续等待 SOH 或 EOT。
-
在早期的实现中,接收方会每隔 10 秒发送一个 NAK,直到收到数据,这被称为 “NAK 风暴”。
数据传输
-
发送方收到 NAK 后,发送第一个数据包(序号为 1)。
-
接收方收到数据包后,进行如下检查:
-
第一个字节是否是 SOH?
-
包序号和它的补码是否匹配?(即 包序号 + 包序号补码 == 0xFF?)
-
计算数据区的校验和,是否与数据包末尾的校验和字节匹配?
-
-
如果所有检查都通过:接收方回复 ACK(0x06)。发送方收到 ACK 后,继续发送下一个数据包(序号加 1)。
-
如果任何一项检查失败:接收方回复 NAK(0x15)。发送方收到 NAK 后,重新发送上一次的同一个数据包。
结束传输
-
当发送方发送完文件的所有数据后,它会发送一个 EOT(End Of Transmission, 0x04)字节。
-
接收方收到第一个 EOT 后,应回复一个 NAK。这是一种确认机制,防止 EOT 在传输中丢失。
-
发送方收到这个 NAK 后,会再次发送 EOT。
-
接收方收到第二个 EOT 后,回复 ACK(0x06)进行最终确认。
-
至此,整个文件传输过程顺利完成。
总结
优点:**
简单易懂:协议逻辑和包结构非常简单,易于在资源受限的嵌入式环境中实现。
实现代码小:通常几百行 C 代码即可实现,对 MCU 的 Flash 和 RAM 消耗很小。
具备基本错误检测和恢复能力:通过校验和/CRC 和重传机制,保证了在不可靠信道(如串口)上传输数据的正确性。
缺点:
效率低下(停等协议):发送方每发送一个数据包,必须等待对方的 ACK/NAK 后才能进行下一步。这在高速或高延迟的信道上会造成严重的性能瓶颈。
数据块固定:标准的 128 字节块对于小文件浪费开销,对于大文件则效率不高。
弱校验:基础版的算术和校验和可靠性较差。
缺乏流控:没有流量控制机制。
“NAK 风暴”问题:如果启动握手阶段的 NAK 或后续的重传 NAK 丢失,通信双方会陷入僵局。
尽管有诸多缺点,XMODEM 的生命力依然顽强,主要应用于:
嵌入式系统 Bootloader: 当需要通过 UART 口向微控制器烧录固件时,XMODEM 是一个常见的选择。因为它实现简单,不占用太多芯片资源。
网络设备维护: 一些路由器、交换机的 Console 口,在其 Bootloader 或恢复模式下,也支持使用 XMODEM 来上传/下载固件。
终端软件功能: 像 Tera Term, SecureCRT, Minicom 等终端软件都内置了 XMODEM 及其变种的发送和接收功能,方便用户进行文件传输。
STM32CubeMX生成工程
我们参考前面章节STM32H743-结合CubeMX新建HAL库MDK工程,打开CubeMX软件,重复步骤不再展示。我们来看配置SPI、USART6配置如下图所示:
设置串口


SPI配置

实验代码
本章实验包含两个程序,APP和bootloader,APP程序可以参考STM32H743-ARM例程34-BootROM,把启动地址修改为0x8040000
#define FLASH_ADDRESS (uint32_t)0x8040000
bootloader程序:
1. 主函数
int main(void)
{/* USER CODE BEGIN 1 */uint16_t device_id=0; //Flash内部的IDuint8_t read_buf=0; //用于存放读取缓冲地址uint8_t write_buf=0; //用于存放写入缓冲地址int i = 0;/* Enable the CPU Cache */CPU_CACHE_Enable();/* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_SPI5_Init();MX_USART6_UART_Init();/* USER CODE BEGIN 2 */uart6.initialize(115200);uart6.printf("\x0c"); uart6.printf("\033[1;32;40m");uart6.printf("Hello,I am GT7000\r\n");LED_ON;if(ARM_KEY4_STATE == KEY_UP){ //按键松开状态直接跳向应用程序goto start;}while(1){ //按键按下,进入升级状态if(i++ == 5000000){//串口发送字符Cuart6.send_byte('C');i = 0;} if(uart6.receive_buffer[0] == SOH){break;}}/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */if(uart6.receive_ok_flag == 1){uart6.receive_ok_flag = 0;LED_OFF;xmodem.process();if(uart6.receive_buffer[0] == EOT){uart6.send_byte(ACK); //发送文件成功,灯亮LED_ON; while(1);} } }start:if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x24000000){ //跳转至用户程序JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);JumpToApplication = (pFunction) JumpAddress;//初始化用户程序的堆栈指针__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);//跳转至应用程序JumpToApplication();}else{ led_trade();}/* USER CODE END 3 */}
2.Xmodem协议相关函数
#include "crc16.h"
#include "xmodem.h"
#include "uart6.h"
#include "gpio.h"
#include "stmflash.h"
#include <stdio.h>
#include <math.h>
#include <string.h>//-----------------------define---------------------------////------------------ Function Prototype ------------------//static int process(void);
extern void led_trade(void);//----------------------variable--------------------------//
XMODEM_T xmodem = {.process = process
};
int packetno = 1;static int process(void)
{unsigned char xbuff[140]; /* 128 for XModem + 3 head chars + 2 crc + nul */ int i = 0;unsigned char * p;if(uart6.receive_buffer[0] == SOH){//接收到有效数据帧头xbuff[0]=uart6.receive_buffer[0];for(i=0;i<133;i++){//接收一帧数据xbuff[i+1]=uart6.receive_buffer[i+ 1];}if((xbuff[1]==(uint8_t)~xbuff[2])&&((packetno % 256) == xbuff[1])//包序号无误&&(crc16.check(&xbuff[3], 128) == (xbuff[131] << 8 | xbuff[132]))){//CRC校验无误if(packetno == 1){p = (unsigned char*)&xbuff[3]; iap_write_appbin(APPLICATION_ADDRESS,p ,128);packetno++;uart6.send_byte(ACK);return 0;}packetno++;p = (unsigned char*)&xbuff[3];iap_write_appbin(APPLICATION_ADDRESS + (packetno - 2) * 128,p ,128);uart6.send_byte(ACK);}else{//要求重发led_trade();}}return 0;
}
3.STM flash相关操作函数
#include "stmflash.h"
#include "uart6.h"
//读取指定地址的字(32位数据)
//faddr:读地址
//返回值:对应数据.
uint32_t STMFLASH_ReadWord(uint32_t faddr)
{return *(__IO uint32_t *)faddr;
}//获取某个地址所在的flash扇区,仅用于BANK1!!
//addr:flash地址
//返回值:0~11,即addr所在的扇区
uint16_t STMFLASH_GetFlashSector(uint32_t addr)
{if(addr >= FLASH_BANK2_ADDR) //BANK2{if(addr<ADDR_FLASH_SECTOR_1_BANK2)return FLASH_SECTOR_0;else if(addr<ADDR_FLASH_SECTOR_2_BANK2)return FLASH_SECTOR_1;else if(addr<ADDR_FLASH_SECTOR_3_BANK2)return FLASH_SECTOR_2;else if(addr<ADDR_FLASH_SECTOR_4_BANK2)return FLASH_SECTOR_3;else if(addr<ADDR_FLASH_SECTOR_5_BANK2)return FLASH_SECTOR_4;else if(addr<ADDR_FLASH_SECTOR_6_BANK2)return FLASH_SECTOR_5;else if(addr<ADDR_FLASH_SECTOR_7_BANK2)return FLASH_SECTOR_6;}else //BANK1{ if(addr<ADDR_FLASH_SECTOR_1_BANK1)return FLASH_SECTOR_0;else if(addr<ADDR_FLASH_SECTOR_2_BANK1)return FLASH_SECTOR_1;else if(addr<ADDR_FLASH_SECTOR_3_BANK1)return FLASH_SECTOR_2;else if(addr<ADDR_FLASH_SECTOR_4_BANK1)return FLASH_SECTOR_3;else if(addr<ADDR_FLASH_SECTOR_5_BANK1)return FLASH_SECTOR_4;else if(addr<ADDR_FLASH_SECTOR_6_BANK1)return FLASH_SECTOR_5;else if(addr<ADDR_FLASH_SECTOR_7_BANK1)return FLASH_SECTOR_6;}return FLASH_SECTOR_7; }//从指定地址开始写入指定长度的数据
//特别注意:因为STM32H7的扇区实在太大,没办法本地保存扇区数据,所以本函数
// 写地址如果非0XFF,那么会先擦除整个扇区且不保存扇区数据.所以
// 写非0XFF的地址,将导致整个扇区数据丢失.建议写之前确保扇区里
// 没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写.
//该函数对OTP区域也有效!可以用来写OTP区!
//OTP区域地址范围:0X1FF0F000~0X1FF0F41F
//WriteAddr:起始地址(此地址必须为4的倍数!!)
//pBuffer:数据指针
//NumToWrite:字(32位)数(就是要写入的32位数据的个数.)
void STMFLASH_Write(uint32_t WriteAddr,uint32_t *pBuffer,uint32_t NumToWrite)
{ FLASH_EraseInitTypeDef FlashEraseInit;HAL_StatusTypeDef FlashStatus=HAL_OK;uint32_t SectorError=0;uint32_t addrx=0;uint32_t endaddr=0; uint32_t bankFlag = 1;if(WriteAddr<STM32_FLASH_BASE||WriteAddr%4)return; //非法地址
// bankFlag = WriteAddr >= FLASH_BANK2_ADDR ? FLASH_BANK_2:FLASH_BANK_1;HAL_FLASH_Unlock(); //解锁 addrx=WriteAddr; //写入的起始地址endaddr=WriteAddr+NumToWrite*4; //写入的结束地址if(addrx<0X1FF00000){while(addrx<endaddr) //扫清一切障碍.(对非FFFFFFFF的地方,先擦除){if(STMFLASH_ReadWord(addrx)!=0XFFFFFFFF)//有非0XFFFFFFFF的地方,要擦除这个扇区{ FlashEraseInit.Banks=bankFlag; //操作BANK1或者2FlashEraseInit.TypeErase=FLASH_TYPEERASE_SECTORS; //擦除类型,扇区擦除 FlashEraseInit.Sector=STMFLASH_GetFlashSector(addrx); //要擦除的扇区FlashEraseInit.NbSectors=1; //一次只擦除一个扇区FlashEraseInit.VoltageRange=FLASH_VOLTAGE_RANGE_3; //电压范围,VCC=2.7~3.6V之间!!if(HAL_FLASHEx_Erase(&FlashEraseInit,&SectorError)!=HAL_OK) {uart6.printf("flash set operation err...\r\n");break;//发生错误了 }SCB_CleanInvalidateDCache(); //清除无效的D-Cache}else addrx+=4;FLASH_WaitForLastOperation(FLASH_WAITETIME,bankFlag); //等待上次操作完成}}FlashStatus=FLASH_WaitForLastOperation(FLASH_WAITETIME,bankFlag); //等待上次操作完成if(FlashStatus==HAL_OK){while(WriteAddr<endaddr)//写数据{if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD,WriteAddr,(uint64_t)pBuffer)!=HAL_OK)//写入数据{ uart6.printf("flash write operation err...\r\n");break; //写入异常}WriteAddr+=32;pBuffer+=8;}}HAL_FLASH_Lock(); //上锁
}//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToRead:字(32位)数
void STMFLASH_Read(uint32_t ReadAddr,uint32_t *pBuffer,uint32_t NumToRead)
{uint32_t i;for(i=0;i<NumToRead;i++){pBuffer[i]=STMFLASH_ReadWord(ReadAddr);//读取4个字节.ReadAddr+=4;//偏移4个字节. }
}#define FLASH_SECTOR 256*128 //256*4 就是1KB,*128就是128K
uint32_t iapbuf[FLASH_SECTOR]; //1K*128字节缓存
uint8_t binBuf[1024*200];
//appxaddr:应用程序的起始地址
//appbuf:应用程序CODE.
//appsize:应用程序大小(字节).
void iap_write_appbin(uint32_t appxaddr,uint8_t *appbuf,uint32_t appsize)
{uint32_t t;uint16_t i=0;uint32_t temp;uint32_t fwaddr=appxaddr;//当前写入的地址uint8_t *dfu=appbuf;for(t=0;t<appsize;t+=4){ temp=(uint32_t)dfu[3]<<24; temp|=(uint32_t)dfu[2]<<16; temp|=(uint32_t)dfu[1]<<8;temp|=(uint32_t)dfu[0]; dfu+=4;//偏移4个字节iapbuf[i++]=temp; if(i==FLASH_SECTOR){i=0; STMFLASH_Write(fwaddr,iapbuf,FLASH_SECTOR);fwaddr+=FLASH_SECTOR*4;//偏移2048 512*4=2048}} if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.
}
实验现象
打开EXputty之后,按住ARM-KEY K4按键,并按下ARM-RST复位按键,进入以下界面:GT7000不断输出’C’字符,是在等待串口发送bin文件。

点击Files Transfer中的Xmodem向GT7000发送bin文件(\hex_to_bin\app.bin),如下图所示:

发送完成后按下ARM-RST按键重启板子,就可以看到 GT7000跳转到APP程序,ARM-LED 灯闪烁。
