目录

1.  前提

2.  OTA 升级的 "控制大脑"

3.  如何进入串口命令行

4.  串口交互命令行实现

5.  Xmodem 协议

6.  CRC-16代码实现

7.  Xmodem协议实现串口IAP功能

7.1  发送大写字母C

7.2  bin文件生成

7.3  通过IAP实现A区更新

7.4  设置OTA版本号

7.5  查询OTA版本号

7.6  向外部Flash下载程序

7.7  使用外部Flash内程序


1.  前提

        前面我们已经实现了无OTA事件B区跳转到A区的操作:

【OTA】手把手写BootLoader程序·无OTA事件B区跳转到A区-CSDN博客

        下面我们来继续实现发生OTA事件以后,更新应用程序去A区程序。再次之前我们先搞清楚几个问题。

2.  OTA 升级的 "控制大脑"

        前面我们了解到 AT24C02 是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息。这里我们想了问什么不使用W25Q64或者内部Flash来进行存放呢?

        那是因为EEPROM 的擦写寿命是100 万次,而 W25Q64 SPI Flash 是 10 万次,STM32 内部 Flash 仅 1 万次。OTA 升级需要频繁写入标志位,用 EEPROM 可以避免 Flash 过早损坏。

        除此之外,EEPROM 的字节写是原子操作,即使写过程中掉电,也只会损坏正在写的那个字节,不会像 Flash 那样需要先擦除整个扇区(擦除过程中掉电会导致整个扇区数据丢失)。

        我们通过 AT24C02 存放整个 OTA 系统的核心数据结构,存储了 Bootloader 判断是否需要升级的所有关键信息:

/*** OTA信息控制块结构体 ***/
typedef struct{
	uint32_t OTA_flag;// OTA升级标志位,匹配OTA_SET_FLAG时触发升级
}OTA_InfoCB;

#define OTA_INFOCB_SIZE sizeof(OTA_InfoCB)//OTA信息控制块结构体的大小宏

        要想实现OTA的升级,我们需要知道新的程序固件的大小,这样我们就需要有个数据去记录新固件的总字节长度,让 Bootloader 知道确切的固件长度才能准确地从 W25Q64 拷贝到 STM32 内部 Flash,避免多拷贝或拷贝不足导致固件损坏。

typedef struct{
    uint32_t OTA_flag;    // OTA升级标志位,匹配OTA_SET_FLAG时触发升级
    uint32_t Firelen[11]; // 0号成员对应OTA的大小
}OTA_InfoCB;
#define OTA_INFOCB_SIZE sizeof(OTA_InfoCB)

        这里为了方便后续拓展直接创成数组的形式,第0位用来存放 OTA 的大小,至于为什么是11。

        我们知道M24C02 的页大小是 16 字节,一次最多可以连续写入 16 个字节,且页写比单字节写快 10 倍以上,同时减少了 I2C 总线的操作次数,提高了可靠性。而如果我们创成11,那么此时结构体大小:4 字节 (OTA_flag) + 4×11 字节 (Firelen) = 48 字节,48 ÷ 16 = 3 次页写操作,正好写完整个结构体。

        然后来到 I2C_EEPROM.c 文件更改 EE_WriteOTAInfo() 函数,批量写入整个 OTA  控制块:

void EE_WriteOTAInfo(void)
{
	uint8_t i;
	uint8_t *wptr;

	wptr = (uint8_t *)&OTA_Info;
	for( i = 0; i < OTA_INFOCB_SIZE/16; i++)
	{
		EE_WriteBytes(wptr + i*16, i*16, 16);
		EE_Delay(0x0FFFFF);
	}
}

        一次写 16 字节(M24C02 页大小),循环 3 次写完 48 字节结构体,写入地址从 0 开始,每次加16,数据指针每次偏移 16 字节。

        来到主函数验证一下:

int main(void)
{
	LED_GPIO_Config();//LED引脚初始化
	USARTx_DMA_Init();//串口初始化
	I2C_Gpio_Config();//IIC初始化
	SPI_FLASH_Init();//SPI初始化

//	EE_Test();

	uint8_t i = 0;
	OTA_Info.OTA_flag = 0xFDFEFF01;

	for(i = 0; i<11; i++)
	{
		OTA_Info.Firelen[i] = i;
	}

	EE_WriteOTAInfo();
	EE_ReadOTAInfo();

  printf("读取OTA信息成功,标志位: 0x%08X\r\n", OTA_Info.OTA_flag);
	for(i = 0; i<11; i++)
	{
		 printf("%x\r\n", OTA_Info.Firelen[i]);
	}

	BootLoader_Branch();

	while(1)
	{
		
	}
}

        可以看到由于我们此时OTA升级的标志位不同,因此未检测到标志位,不进行更新直接跳转A分区,执行A分区程序:

        并且也成功写入了 OTA 结构体数据 0~a 共11个数据,我们将标志位改为一样:

        可以发现执行OTA更新,只是这里我们还没有写更新后的操作,所以只打印了一条数据,下面我们来进行后续操作。

后续没完成一小部分代码,就会将当前完成功能代码贴出来,方便验证。

验证代码:

基于STM32实现BootLoader程序B区跳转到A区资源-CSDN下载

        之后我们想要实现通过串口去实现OTA事件的执行。

3.  如何进入串口命令行

        首先我们先设计一下命令行的菜单,也就是我们之后想要完成什么样的功能:

void BootLoader_Info(void)
{
	printf("\r\n");
	printf("[1]擦除A区\r\n");
	printf("[2]串口IAP下载A区程序\r\n");
	printf("[3]设置OTA版本号\r\n");
	printf("[4]查询TA版本号\r\n");
	printf("[5]向外部Flash下载程序\r\n");
	printf("[6]使用外部Flash内程序\r\n");
	printf("[7]重启\r\n");
}

        现在简单设计这七个功能,后续有需要可以继续添加:

  • 擦除 A 区:擦除内部 Flash 中 A 区的所有内容
  • 串口 IAP 下载 A 区程序:通过串口接收二进制固件文件,写入内部 Flash 的 A 区
  • 设置 OTA 版本号:将 OTA 版本号写入 AT24C02 等非易失性存储
  • 查询 TA 版本号:读取并显示当前 A 区应用程序的版本号
  • 向外部 Flash 下载程序:通过串口接收固件文件,写入外部 W25Q64 Flash
  • 使用外部 Flash 内程序:将外部 Flash 中的固件复制到内部 Flash 的 A 区,然后跳转运行
  • 重启:系统软复位,重新进入 BootLoader

        这里我们要思考既然想要完成对应的功能,那么就需要外部输入,这里不能再用简单的串口,需要Xmdoem(这个下面会讲),要想实现该功能,我们还需要一个软件 SecureCRTPortable 进行辅助:

OTA事件Xmodem辅助工具资源-CSDN下载


拓展:工具使用

        找到快速连接点击进去:

        找到串口位置,点击:

        根据自己的串口配置,设置相关数据:

        点击连接:


        那么我们何时进入命令行呢?又如何进入命令行呢?我们可以通过串口从缓冲区接收数据,如果在规定时间内接收到该数据,则进入命令行:

uint8_t BootLoader_Enter(uint8_t timeout)
{
	uint16_t len;//用于接收从环形缓冲区获取的数据长度
	printf("%dms内输入小写字母 w,进入BootLoader命令行\r\n",timeout*100);

	while(timeout--)
	{
		Delay_ms(100);

		UCB_URxBuffptr *rx_buf = USARTx_DMA_GetRxData(&len);// 从环形缓冲区获取数据

		if(rx_buf != NULL && len >= 1)
		{
			if(rx_buf->start[0] == 'w')
			{

				USARTx_DMA_ReleaseRxBuf(rx_buf);// 释放缓冲区
				return 1;//进入命令行
			}
			USARTx_DMA_ReleaseRxBuf(rx_buf);// 释放缓冲区
		}
	}
	return 0;
}

        这里等待 timeout*100 毫秒的时间,如果在时间段内我们输入w,则进入命令行,如果没有则进入OTA事件检测:

void BootLoader_Branch(void)
{
	if(BootLoader_Enter(20) == 0)
	{
		if(OTA_Info.OTA_flag == OTA_SET_FLAG)
		{
			printf("检测到OTA标志,执行OTA更新\r\n");
		}
		else
		{
			printf("未检测到OTA标志,跳转A分区\r\n");
			LOAD_A(0x08008000);
		}
	}
	else
	{
		printf("进入BootLoader命令行\r\n");
		BootLoader_Info();
	}
}

        主函数没什么变动:

#include "stm32f10x.h"           
#include "Delay.h"

#include "main.h"
#include "Bsp_LED_Gpio.h"
#include "Usartx_DMA.h"
#include "I2C_gpio.h"
#include "SPI_gpio.h"
#include "Internal_Flash.h"

#include "I2C_EEPROM.h"
#include "W25Qx_Flash.h"
#include "BootLoader.h"

OTA_InfoCB OTA_Info;
UpDataA_CB UpDataA;

int main(void)
{
	LED_GPIO_Config();//LED引脚初始化
	USARTx_DMA_Init();//串口初始化
	I2C_Gpio_Config();//IIC初始化
	SPI_FLASH_Init();//SPI初始化

	EE_ReadOTAInfo();
	BootLoader_Branch();

	while(1)
	{
		
	}
}

        我们下载看一下,可以看到如果我们在规定时间内输入w,正常进入命令行:

        如果我们不输入,则会在时间结束后,进入OTA检测部分:

代码验证:

基于STM32实现BootLoader程序串口命令行跳转资源-CSDN下载

4.  串口交互命令行实现

        这里我们先完成1和7的命令,这两个命令比较简单,容易查看效果,我们先来查看一下实验现象,首先对于1擦除A区的命令,主要调用我们之前声明的擦除内部Flash函数 InternalFLASH_ErasePages_Num(),功能实现如下:

        先判断命令长度与命令字是否都为1,然后调用函数擦除A区:

	if((datalen == 1) && (data[0] == '1'))
	{
		printf("\r\n开始擦除A分区,请勿断电...\r\n");

		FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);

		if(status == FLASH_COMPLETE)
		{
				printf("A分区擦除成功!\r\n");
		}
		else
		{
				printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
		}
	}

        对于重启命令也只需要调用 NVIC_SystemReset() 函数:

	else if((datalen == 1) && (data[0] == '7'))
	{
		printf("重启!\r\n");
		Delay_ms(100);
		NVIC_SystemReset();
	}

        封装到一个函数当中:

void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
	if((datalen == 1) && (data[0] == '1'))
	{
		printf("\r\n开始擦除A分区,请勿断电...\r\n");

		FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);

		if(status == FLASH_COMPLETE)
		{
				printf("A分区擦除成功!\r\n");
		}
		else
		{
				printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
		}
	}
	else if((datalen == 1) && (data[0] == '7'))
	{
		printf("重启!\r\n");
		Delay_ms(100);
		NVIC_SystemReset();
	}
}

        不过这里我们需要考虑一点,那就是如果我们使用完擦除A区,那么A区代码将清空,为了方便查看我们是否正常跳转到A区,我们对加载并跳转 A 区函数 LOAD_A() 进行一个简单的修改,如果跳转失败打印失败:

void LOAD_A(uint32_t addr)
{
	uint32_t app_stack_addr = *(uint32_t *)addr; // 读取A区应用的栈顶地址

	if((app_stack_addr >= BootLoader_SRAM_BASE)&&(app_stack_addr <= BootLoader_SRAM_END))
	{
		MSR_SP(app_stack_addr);                   // 设置MSP栈指针
		Load_A = (load_a)*(uint32_t *)(addr + 4); // 读取A区复位函数地址
		BootLoader_Clear();                       // 复位所有外设、关闭中断
		Load_A();																	// 跳转到A区应用
	}
	else
	{
		printf("A分区跳转失败!!!\r\n");
	}
}

        并且如果跳转是失败后直接进入命令行进行输入其他命令:

void BootLoader_Branch(void)
{
	if(BootLoader_Enter(20) == 0)
	{
		if(OTA_Info.OTA_flag == OTA_SET_FLAG)
		{
			printf("检测到OTA标志,执行OTA更新\r\n");
		}
		else
		{
			printf("未检测到OTA标志,跳转A分区\r\n");
			LOAD_A(0x08010000);
		}
	}
	printf("进入BootLoader命令行\r\n");
	BootLoader_Info();
}

     然后我们回到主函数,对数据进行接收:

#include "stm32f10x.h"           
#include "Delay.h"

#include "main.h"
#include "Bsp_LED_Gpio.h"
#include "Usartx_DMA.h"
#include "I2C_gpio.h"
#include "SPI_gpio.h"
#include "Internal_Flash.h"

#include "I2C_EEPROM.h"
#include "W25Qx_Flash.h"
#include "BootLoader.h"

OTA_InfoCB OTA_Info;
UpDataA_CB UpDataA;

int main(void)
{
	LED_GPIO_Config();//LED引脚初始化
	USARTx_DMA_Init();//串口初始化
	I2C_Gpio_Config();//IIC初始化
	SPI_FLASH_Init();//SPI初始化

	EE_ReadOTAInfo();
	BootLoader_Branch();

	uint16_t rx_len = 0;
	UCB_URxBuffptr *rx_buf = NULL;

	while(1)
	{
		rx_buf = USARTx_DMA_GetRxData(&rx_len);
		if (rx_buf != NULL && rx_len > 0)
		{
			BootLoader_Event(rx_buf->start,rx_len);
			USARTx_DMA_ReleaseRxBuf(rx_buf);
		}
	}
}

           下载测试一下,首先是我们不进行任何操作,可以发现其未检测到升级标志,直接跳转到A分区,执行串口打印代码:

注意A分区需要导入代码,导入过程可以查看:

【OTA】手把手写BootLoader程序·无OTA事件B区跳转到A区-CSDN博客

        然后我们复位,输入w,可以发现进入命令行:

        然后输入1,可以发现开始擦除A分区:

        此时A分区已经擦除完成,此时我们使用命令7进行重启,不进行操作,会发现不能再跳转到A分区,因为此时已经被擦除了:

验证代码:

基于STM32实现BootLoader程序串口命令行功能实现资源-CSDN下载

5.  Xmodem 协议

        Xmodem 是一种历史悠久、结构简洁的异步串行文件传输协议,由沃德・克里斯滕森(Ward Christensen)于 1977 年为 CP/M 操作系统设计,最初用于早期 BBS(电子公告系统)环境下的调制解调器通信。

        Xmodem 采用主从模式停等 ARQ(自动重传请求)机制,将待传文件按固定长度分块为 "帧",每帧独立封装、编号、校验,并依赖接收方显式反馈(ACK/NACK)与发送方超时重传实现端到端的差错控制与流量控制。

        标准 Xmodem 帧结构(128 字节数据块):

字节位置 字段名称 长度 (字节) 说明
Byte1 起始标志 (SOH) 1 ASCII 码 0x01,标识一个新数据块开始
Byte2 包序号 1 从 1 开始递增,超过 255 后循环
Byte3 包序号反码 1 255 - 包序号,用于验证包头完整性
Byte4~Byte131 数据区 128 实际传输的文件数据,不足 128 字节用 CTRLZ (0x1A) 填充
Byte132 校验字段 1 或 2 8 位校验和或 16 位 CRC 校验值

注意整个帧长度是:起始标志+包序号+包序号反码+数据区+校验字段(我们这里使用的是16位的)= 1+1+1+128+2 = 133字节

不过CRC的校验只校验了数据区的数据。

        为提高大文件传输效率,Xmodem-1K 将数据块大小扩展至 1024 字节,使用 STX (0x02) 作为起始标志:

字节位置 字段名称 长度 (字节) 说明
Byte1 起始标志 (STX) 1 ASCII 码 0x02,标识 1K 数据块开始
Byte2~Byte3 包序号 + 反码 2 与标准 Xmodem 相同
Byte4~Byte1027 数据区 1024 1K 字节数据
Byte1028~Byte1029 校验字段 2 通常使用 16 位 CRC 校验

        Xmodem 协议使用以下 ASCII 控制字符进行通信:

  • SOH(0x01):128 字节数据块开始
  • STX(0x02):1024 字节数据块开始
  • EOT(0x04):传输结束
  • ACK(0x06):确认接收正确
  • NAK(0x15):请求重传
  • CAN(0x18):取消传输
  • CTRLZ(0x1A):数据填充字符

        传输时序:

步骤 发送方 (Sender) 传输方向 接收方 (Receiver) 详细说明
1 等待 <--- 发送 'C'(0x43) 接收方主动发起 CRC 模式握手请求
2 无响应 3 秒超时 发送方未及时响应,接收方触发超时机制
3 等待 <--- 再次发送 'C' 接收方重发 CRC 握手请求
4 发送 SOH 0x01 0xFE Data CRC16 ---> 数据包校验正确 发送方响应握手,发送第 1 个数据包包序号 0x01,反码 0xFE (255-1=254)
5 等待 <--- 发送 ACK (0x06) 接收方确认第 1 包正确
6 发送 SOH 0x02 0xFD Data CRC16 ---> 传输过程中线路干扰 发送第 2 个数据包时发生传输错误
7 等待 <--- 发送 NAK (0x15) 接收方校验失败,请求重传第 2 包
8 重发 SOH 0x02 0xFD Data CRC16 ---> 数据包校验正确 发送方重传第 2 个数据包
9 等待 <--- 发送 ACK 接收方确认第 2 包正确
10 发送 SOH 0x03 0xFC Data CRC16 ---> 数据包校验正确 发送第 3 个数据包
11 等待 <--- 发送 ACK 接收方确认第 3 包正确
12 发送 EOT (0x04) ---> 收到 EOT 发送方所有数据传输完毕,发送结束标志
13 未收到 ACK <--- 发送 ACK 接收方确认收到 EOT,但 ACK 在传输中损坏
14 超时后重发 EOT ---> 再次收到 EOT 发送方超时未收到 ACK,重发 EOT
15 收到 ACK <--- 发送 ACK 接收方再次确认 EOT,传输完成

        Xmodem 协议最独特的设计:传输永远由接收方启动

        与其他串行文件传输协议的对比:

协议 数据块大小 校验方式 多文件支持 传输效率 实现复杂度
Xmodem 128/1024 字节 校验和 / CRC16 不支持 极低
Ymodem 1024 字节 CRC16 支持
Zmodem 可变 CRC32 支持

6.  CRC-16代码实现

        对于CRC详细介绍可以参考:

CRC(循环冗余校验)·CRC校验原理及步骤解析入门教程(C语言)_crc校验 c语言-CSDN博客

        这里我们需要使用一个CRC校验的工具,上面文章里面也有,如果对CRC不熟悉的建议先看一下CRC章节:

CRC(循环冗余校验)在线计算_ip33.com

        找到里面的CRC-16/XMODEM协议:

        我们可以知道该模型的多项式和初始值:

	uint16_t Crcinit = 0x0000;//初始值
	uint16_t Crcipoly = 0x1021;//多项式

        CRC不分不再做过多的描述,上面链接有详细的讲解,带入即可:

uint32_t Xmodem_CRC16(uint8_t *data, uint16_t datalen)
{
	uint8_t i;
	uint16_t Crcinit = 0x0000;//初始值
	uint16_t Crcipoly = 0x1021;//多项式

	while(datalen--)//处理每个数据字节
	{
		Crcinit = (*data << 8) ^ Crcinit;//将当前字节与CRC寄存器高8位异或

		for(i = 0; i<8; i++)//处理当前字节的8个比特位
		{
			if(Crcinit & 0x8000)//检查CRC寄存器的最高位
			{
				Crcinit = (Crcinit << 1) ^ Crcipoly;//如果最高位为1,左移1位后异或多项式
			}
			else
			{
				Crcinit = (Crcinit << 1);//如果最高位为0,直接左移1位
			}
		}
		data++;//// 处理下一个字节
	}
	return Crcinit;
}

        当然除了上述直接运算的方法,还有一个更快的查表法,原因是因为对于 8 位数据来说,所有可能的输入只有 256 种,我们可以预先计算出每个字节对应的 CRC 更新值,存储在一个表中,实际计算时,直接查表得到结果,不需要进行 8 次循环,查表法的核心公式:

crc = (crc << 8) ^ table[(crc >> 8) ^ *data];

        对于需要频繁计算 CRC 或处理大文件的场景,强烈推荐使用查表法,速度可以提升 8 倍以上,代码如下:

/**
 * @brief  Xmodem CRC16查表法预计算表
 * @note   可以在编译时静态生成,也可以在初始化时动态生成
 */
static const uint16_t xmodem_crc16_table[256] = {
    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
    0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
    0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
    0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
    0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
    0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
    0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
    0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
    0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
    0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
    0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
    0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
    0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
    0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
    0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
    0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
    0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
    0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
    0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
    0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
    0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
    0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
    0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
    0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
    0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
    0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
    0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
    0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
    0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
    0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
    0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
    0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
};

/**
 * @brief  查表法计算Xmodem协议的CRC16校验值
 * @param  data: 指向待计算数据的指针
 * @param  datalen: 待计算数据的长度(字节)
 * @retval 16位CRC校验值
 */
uint16_t Xmodem_CRC16_Fast(const uint8_t *data, uint16_t datalen)
{
    uint16_t crc = 0x0000;
    
    while (datalen--)
    {
        crc = (crc << 8) ^ xmodem_crc16_table[(crc >> 8) ^ *data];
        data++;
    }
    
    return crc;
}

        来到主函数我们两种方式都验证一下,创建一个数组:

uint8_t example[5] = {0x01,0x02,0x03,0x04,0x05};

        直接将计算结果打印出来:

printf("%x\r\n",Xmodem_CRC16(example,5));
printf("%x\r\n",Xmodem_CRC16_Fast(example,5));

        此时主函数:

#include "stm32f10x.h"
#include "Delay.h"

#include "main.h"
#include "Bsp_LED_Gpio.h"
#include "Usartx_DMA.h"
#include "I2C_gpio.h"
#include "SPI_gpio.h"
#include "Internal_Flash.h"

#include "I2C_EEPROM.h"
#include "W25Qx_Flash.h"
#include "BootLoader.h"
#include "Xmodem_CRC16.h"

OTA_InfoCB OTA_Info;
UpDataA_CB UpDataA;

uint8_t example[5] = {0x01,0x02,0x03,0x04,0x05};

int main(void)
{
	LED_GPIO_Config();//LED引脚初始化
	USARTx_DMA_Init();//串口初始化
	I2C_Gpio_Config();//IIC初始化
	SPI_FLASH_Init();//SPI初始化

	printf("%x\r\n",Xmodem_CRC16(example,5));
	printf("%x\r\n",Xmodem_CRC16_Fast(example,5));
	while(1);//不需要下面的代码直接卡死在这里

	EE_ReadOTAInfo();
	BootLoader_Branch();

	uint16_t rx_len = 0;
	UCB_URxBuffptr *rx_buf = NULL;

	while(1)
	{
		rx_buf = USARTx_DMA_GetRxData(&rx_len);
		if (rx_buf != NULL && rx_len > 0)
		{
			BootLoader_Event(rx_buf->start,rx_len);
			USARTx_DMA_ReleaseRxBuf(rx_buf);
		}
	}
}

        此时可以看到,两种的计算结果都是8208:

        我们使用CRC校验工具看一下,可以发现计算结果也是8208,可以多试几组验证是否正确:

验证代码:

基于STM32实现BootLoader程序串口命令XmodemCRC16验证资源-CSDN下载

7.  Xmodem协议实现串口IAP功能

        步骤实现主要基于我们介绍Xmodem协议时的这张表格:

步骤 发送方 (Sender) 传输方向 接收方 (Receiver) 详细说明
1 等待 <--- 发送 'C'(0x43) 接收方主动发起 CRC 模式握手请求
2 无响应 3 秒超时 发送方未及时响应,接收方触发超时机制
3 等待 <--- 再次发送 'C' 接收方重发 CRC 握手请求
4 发送 SOH 0x01 0xFE Data CRC16 ---> 数据包校验正确 发送方响应握手,发送第 1 个数据包包序号 0x01,反码 0xFE (255-1=254)
5 等待 <--- 发送 ACK (0x06) 接收方确认第 1 包正确
6 发送 SOH 0x02 0xFD Data CRC16 ---> 传输过程中线路干扰 发送第 2 个数据包时发生传输错误
7 等待 <--- 发送 NAK (0x15) 接收方校验失败,请求重传第 2 包
8 重发 SOH 0x02 0xFD Data CRC16 ---> 数据包校验正确 发送方重传第 2 个数据包
9 等待 <--- 发送 ACK 接收方确认第 2 包正确
10 发送 SOH 0x03 0xFC Data CRC16 ---> 数据包校验正确 发送第 3 个数据包
11 等待 <--- 发送 ACK 接收方确认第 3 包正确
12 发送 EOT (0x04) ---> 收到 EOT 发送方所有数据传输完毕,发送结束标志
13 未收到 ACK <--- 发送 ACK 接收方确认收到 EOT,但 ACK 在传输中损坏
14 超时后重发 EOT ---> 再次收到 EOT 发送方超时未收到 ACK,重发 EOT
15 收到 ACK <--- 发送 ACK 接收方再次确认 EOT,传输完成

7.1  发送大写字母C

        首先我们先声明一个记录全局状态标志位,方便调用:

uint32_t BootStaFlag;	//记录全局状态标志位

        然后创建一个结构体用于A区更新:

typedef struct{
	uint8_t UpDatabuff[FLASH_PAGE_SIZE];//更新A区时用于保存从W25Q64读取的数据
	uint32_t W25Q64_BlockNB;			//用于记录从哪个W25Q64的块中读取的数据
	uint32_t XmodemTimer;     			//超时检测
}UpDataA_CB;

UpDataA_CB UpDataA;		//A区更新用到的结构体

        然后更改A区功能实现的代码,也就是上面4小节实现的功能,对其加入串口命令行2的功能:

void BootLoader_Info(void)
{
	printf("\r\n");
	printf("[1]擦除A区\r\n");
	printf("[2]串口IAP下载A区程序\r\n");
	printf("[3]设置OTA版本号\r\n");
	printf("[4]查询TA版本号\r\n");
	printf("[5]向外部Flash下载程序\r\n");
	printf("[6]使用外部Flash内程序\r\n");
	printf("[7]重启\r\n");
}

        下载前需要先擦除,然后置于接收状态,将超时检测函数置0:

		else if((datalen == 1) && (data[0] == '2'))//串口IAP下载A区程序
		{
			printf("通过Xmodem协议,串口IAP下载A区程序,请使用bin格式文件\r\n");

			FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);//写入之前先擦除

			if(status != FLASH_COMPLETE)//判断是否擦除成功
			{
				printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
				return; // 擦除失败,不启动Xmodem
			}

			printf("A分区擦除成功!\r\n");

			BootStaFlag |= IAP_XMODEMC_FLAG;//设置BootStaFlag的IAP_XMODEMC_FLAG位,通知主循环进入 Xmodem 接收状态
			UpDataA.XmodemTimer = 0;		//初始化UpDataA.XmodemTimer为 0,用于 Xmodem 协议的超时控制
		}

        此时的函数:

void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
	if(BootStaFlag == 0)//只有在空闲状态下才接受新命令
	{
		if((datalen == 1) && (data[0] == '1'))//擦除A区
		{
			printf("\r\n开始擦除A分区,请勿断电...\r\n");

			FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);

			if(status == FLASH_COMPLETE)
			{
					printf("A分区擦除成功!\r\n");
			}
			else
			{
					printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
			}
		}
		else if((datalen == 1) && (data[0] == '2'))//串口IAP下载A区程序
		{
			printf("通过Xmodem协议,串口IAP下载A区程序,请使用bin格式文件\r\n");

			FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);//写入之前先擦除

			if(status != FLASH_COMPLETE)//判断是否擦除成功
			{
				printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
				return; // 擦除失败,不启动Xmodem
			}

			printf("A分区擦除成功!\r\n");

			BootStaFlag |= IAP_XMODEMC_FLAG;//设置BootStaFlag的IAP_XMODEMC_FLAG位,通知主循环进入 Xmodem 接收状态
			UpDataA.XmodemTimer = 0;				//初始化UpDataA.XmodemTimer为 0,用于 Xmodem 协议的超时控制
		}
		else if((datalen == 1) && (data[0] == '7'))//重启
		{
			printf("重启!\r\n");
			Delay_ms(100);
			NVIC_SystemReset();
		}
	}
}

        然后主函数判断,每隔1s发送一次C:

		if(BootStaFlag & IAP_XMODEMC_FLAG)
		{
			if(UpDataA.XmodemTimer >= 100)
			{
				printf("C");
				UpDataA.XmodemTimer = 0;
			}
			else
			{
				UpDataA.XmodemTimer++;
			}
		}

        此时的主函数:

#include "stm32f10x.h"
#include "Delay.h"

#include "main.h"
#include "Bsp_LED_Gpio.h"
#include "Usartx_DMA.h"
#include "I2C_gpio.h"
#include "SPI_gpio.h"
#include "Internal_Flash.h"

#include "I2C_EEPROM.h"
#include "W25Qx_Flash.h"
#include "BootLoader.h"
#include "Xmodem_CRC16.h"

OTA_InfoCB OTA_Info;  //保存在24C02内的OTA信息相关的结构体
UpDataA_CB UpDataA;		//A区更新用到的结构体
uint32_t BootStaFlag;	//记录全局状态标志位

int main(void)
{
	LED_GPIO_Config();//LED引脚初始化
	USARTx_DMA_Init();//串口初始化
	I2C_Gpio_Config();//IIC初始化
	SPI_FLASH_Init();//SPI初始化

	EE_ReadOTAInfo();
	BootLoader_Branch();

	uint16_t rx_len = 0;
	UCB_URxBuffptr *rx_buf = NULL;

	while(1)
	{
		Delay_ms(10);

		rx_buf = USARTx_DMA_GetRxData(&rx_len);
		if (rx_buf != NULL && rx_len > 0)
		{
			BootLoader_Event(rx_buf->start,rx_len);
			USARTx_DMA_ReleaseRxBuf(rx_buf);
		}

		if(BootStaFlag & IAP_XMODEMC_FLAG)
		{
			if(UpDataA.XmodemTimer >= 100)
			{
				printf("C");
				UpDataA.XmodemTimer = 0;
			}
			else
			{
				UpDataA.XmodemTimer++;
			}
		}
	}
}

        下载验证一下,功能正常:

验证代码:

基于STM32实现BootLoader程序串口命令Xmodem超时检测资源-CSDN下载

7.2  bin文件生成

        找到图示位置:

        输入如下指令:

D:\kile5\ARM\ARMCC\bin\fromelf.exe --bin -o .\Objects\Project.bin .\Objects\Project.axf

        对于该指令的作用:将 Keil 编译生成的带调试信息的.axf 文件,剥离所有调试符号、段头信息和元数据,提取出纯粹的可执行代码和数据,按照链接脚本 (.sct) 指定的 Flash 地址拼接成连续的二进制流,生成可直接写入单片机 Flash 的.bin文件。

  • D:\kile5\ARM\ARMCC\bin\fromelf.exe

        ARM 官方镜像处理工具的绝对路径,对应 Keil5 安装的标准位置,随 ARMCC 编译器一起安装,这个需要改成自己的安装路径:

  • --bin:核心转换指令:指定输出格式为纯二进制文件 (.bin),这是单片机 Flash 烧录、OTA 升级最常用的格式,文件体积最小。
  • -o :output(输出)
  • .\Objects\ :当前工程根目录下的 Objects文件夹,这个需要改成自己的命名:

  • Project.bin :最终生成的烧录文件名,编译完后,可在Objects文件找到,下面是我编译完后的:

  • .\Objects\Project.axf:输入源文件,Keil 编译成功后生成的ARM 可执行调试文件,包含完整的程序代码、数据、调试信息、符号表和内存映射信息

7.3  通过IAP实现A区更新

        根据流程,我们发送完大写字母C后,开始收发数据包的操作:

步骤 发送方 (Sender) 传输方向 接收方 (Receiver) 详细说明
1 等待 <--- 发送 'C'(0x43) 接收方主动发起 CRC 模式握手请求
2 无响应 3 秒超时 发送方未及时响应,接收方触发超时机制
3 等待 <--- 再次发送 'C' 接收方重发 CRC 握手请求
4 发送 SOH 0x01 0xFE Data CRC16 ---> 数据包校验正确 发送方响应握手,发送第 1 个数据包包序号 0x01,反码 0xFE (255-1=254)
5 等待 <--- 发送 ACK (0x06) 接收方确认第 1 包正确
6 发送 SOH 0x02 0xFD Data CRC16 ---> 传输过程中线路干扰 发送第 2 个数据包时发生传输错误
7 等待 <--- 发送 NAK (0x15) 接收方校验失败,请求重传第 2 包
8 重发 SOH 0x02 0xFD Data CRC16 ---> 数据包校验正确 发送方重传第 2 个数据包
9 等待 <--- 发送 ACK 接收方确认第 2 包正确
10 发送 SOH 0x03 0xFC Data CRC16 ---> 数据包校验正确 发送第 3 个数据包
11 等待 <--- 发送 ACK 接收方确认第 3 包正确
12 发送 EOT (0x04) ---> 收到 EOT 发送方所有数据传输完毕,发送结束标志
13 未收到 ACK <--- 发送 ACK 接收方确认收到 EOT,但 ACK 在传输中损坏
14 超时后重发 EOT ---> 再次收到 EOT 发送方超时未收到 ACK,重发 EOT
15 收到 ACK <--- 发送 ACK 接收方再次确认 EOT,传输完成

        在声明一个Xmodem数据处理标志位,表示系统正在处理 Xmodem 协议数据:

#define IAP_XMODEMD_FLAG  0x00000004

        同时随A区更新用到的结构体,添加两个参数,一个用于数据包计数,一个用来CRC校验:

typedef struct{
	uint8_t UpDatabuff[FLASH_PAGE_SIZE];//更新A区时用于保存从W25Q64读取的数据
	uint32_t W25Q64_BlockNB;			//用于记录从哪个W25Q64的块中读取的数据
	uint32_t XmodemTimer;     			//超时检测
	uint32_t XmodemNB;					//数据包计数
	uint32_t XmodemCRC;					//CRC校验
}UpDataA_CB;

        然后来到上面编写的功能2的代码,将此时的模式更改为控制状态加数据接收状态,同时将数据包计数器清零:

		else if((datalen == 1) && (data[0] == '2'))//串口IAP下载A区程序
		{
			printf("通过Xmodem协议,串口IAP下载A区程序,请使用bin格式文件\r\n");

			FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);//写入之前先擦除

			if(status != FLASH_COMPLETE)//判断是否擦除成功
			{
				printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
				return; // 擦除失败,不启动Xmodem
			}

			printf("A分区擦除成功!\r\n");

			BootStaFlag |= (IAP_XMODEMC_FLAG | IAP_XMODEMD_FLAG);//进入Xmodem下载模式,IAP_XMODEMC_FLAG = 控制状态,IAP_XMODEMD_FLAG = 数据接收状态
			UpDataA.XmodemTimer = 0;//初始化UpDataA.XmodemTimer为 0,用于 Xmodem 协议的超时控制

			UpDataA.XmodemNB = 0;//初始化帧号计数器(收到多少包)
		}

        然后判断当前是否处于 Xmodem 数据接收状态:

	if(BootStaFlag&IAP_XMODEMD_FLAG)
    {
    }

        如果处于接收状态,判断是否接收到第一个数据包:

4 发送 SOH 0x01 0xFE Data CRC16 ---> 数据包校验正确 发送方响应握手,发送第 1 个数据包包序号 0x01,反码 0xFE (255-1=254)
if((datalen == 133) && (data[0] == 0x01))
{
}

        接收到第一个数据包开始,数据传输,此时可以先关闭控制模式,专心收数据:

BootStaFlag &=~ IAP_XMODEMC_FLAG;

        通过CRC判断接收的数据是否准确:

			UpDataA.XmodemCRC = Xmodem_CRC16_Fast(&data[3],128);// 计算这 128 字节数据的 CRC16 校验值

			if(UpDataA.XmodemCRC == (data[131]*256+data[132]))//对比计算的CRC == 发送方发来的CRC
            {
            }

        没接收到一帧数据,数据包计数加一,同时计算偏移,因为我们使用的是ZET6一页Flash的大小为2048字节,而一包数据(也就是Xmodem一帧的数据包)是128字节,那么16包刚好一页,不过这里为了方便更改,我们直接使用前面定义好的标志位 FLASH_PAGE_SIZE/128 = 2048/128 = 16,把 128 字节复制到页缓存 UpDatabuff,送 \x06(ACK) 告诉上位机:这包收好了,发下一包:

UpDataA.XmodemNB++;

uint16_t buf_offset = ((UpDataA.XmodemNB - 1) % (FLASH_PAGE_SIZE/128)) * 128;// 计算当前数据应该放在缓存的哪个位置
memcpy(&UpDataA.UpDatabuff[buf_offset], &data[3], 128); // 把收到的 128 字节复制到缓存数组

printf("\x06");	// 回复 ACK(0x06)告诉串口助手上一帧接收成功


        每收满 16 包(1 页),一次性写入内部 Flash A 区:

// 每收到16帧(2KB)写入一次Flash
if((UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) == 0)
{
		uint32_t flash_addr = FLASH_A_START_ADDR + ((UpDataA.XmodemNB / (FLASH_PAGE_SIZE/128)) - 1) * FLASH_PAGE_SIZE;// 计算要写入的 Flash 地址
		InternalFLASH_WriteWords(flash_addr, (uint32_t *)UpDataA.UpDatabuff, FLASH_PAGE_SIZE / 4);// 写入整页 Flash
}

        判断是否接收到0x04,表示所有数据都接收完:

发送 EOT (0x04) ---> 收到 EOT 发送方所有数据传输完毕,发送结束标志
if((datalen == 1) && (data[0] == 0x04))
{
    printf("\x06");	// 回复 ACK 确认结束

    BootStaFlag &=~ IAP_XMODEMD_FLAG;//清除接收状态标志位,退出 Xmodem 模式

    Delay_ms(100);
    NVIC_SystemReset();// 延时后重启,运行新下载的程序
}

        不过这里要考虑一个问题上面我们写入数据都是整页写入,如果最后接收完数据,还有一部分不够整夜要怎么办呢?我们可以多加一层判断,当数据接收完成,判断最后剩下的数据还够不够2048,如果够那么不需要进行别的操作,如果不够则需要将剩下的数据写入:

if((UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) != 0)// 如果最后剩下的数据不够 2048 字节
{
		uint32_t flash_addr = FLASH_A_START_ADDR + (UpDataA.XmodemNB / (FLASH_PAGE_SIZE/128)) * FLASH_PAGE_SIZE;// 计算写入地址
		uint16_t bytes_to_write = (UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) * 128;// 计算剩余有效字节数
		uint16_t words_to_write = (bytes_to_write + 3) / 4;// 转换成 32位字 数量(向上取整)
		
		InternalFLASH_WriteWords(flash_addr, (uint32_t *)UpDataA.UpDatabuff, words_to_write);// 把最后不足一页的数据写入 Flash
}

        此时的 BootLoader_Event() 函数:

void BootLoader_Event(uint8_t *data, uint16_t datalen)
{
	if(BootStaFlag == 0)//只有在空闲状态下才接受新命令
	{
		if((datalen == 1) && (data[0] == '1'))//擦除A区
		{
			printf("\r\n开始擦除A分区,请勿断电...\r\n");

			FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);//擦除 A 分区所有页

			if(status == FLASH_COMPLETE)
			{
					printf("A分区擦除成功!\r\n");
			}
			else
			{
					printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
			}
		}
		else if((datalen == 1) && (data[0] == '2'))//串口IAP下载A区程序
		{
			printf("通过Xmodem协议,串口IAP下载A区程序,请使用bin格式文件\r\n");

			FLASH_Status status = InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES,FLASH_A_PAGES_NUM);//写入之前先擦除

			if(status != FLASH_COMPLETE)//判断是否擦除成功
			{
				printf("A分区擦除失败!错误码: 0x%02X\r\n", status);
				return; // 擦除失败,不启动Xmodem
			}

			printf("A分区擦除成功!\r\n");

			BootStaFlag |= (IAP_XMODEMC_FLAG | IAP_XMODEMD_FLAG);//进入Xmodem下载模式,IAP_XMODEMC_FLAG = 控制状态,IAP_XMODEMD_FLAG = 数据接收状态
			UpDataA.XmodemTimer = 0;//初始化UpDataA.XmodemTimer为 0,用于 Xmodem 协议的超时控制

			UpDataA.XmodemNB = 0;//初始化帧号计数器(收到多少包)
		}
		else if((datalen == 1) && (data[0] == '7'))//重启
		{
			printf("重启!\r\n");
			Delay_ms(100);
			NVIC_SystemReset();
		}
	}

	if(BootStaFlag&IAP_XMODEMD_FLAG)// 如果当前处于 Xmodem 数据接收状态
	{
		if((datalen == 133) && (data[0] == 0x01))// Xmodem 标准:一包数据 = 133字节,开头是 0x01
		{
			BootStaFlag &=~ IAP_XMODEMC_FLAG;// 关闭控制等待状态,专心处理数据

			UpDataA.XmodemCRC = Xmodem_CRC16_Fast(&data[3],128);// 计算这 128 字节数据的 CRC16 校验值

			if(UpDataA.XmodemCRC == (data[131]*256+data[132]))//对比计算的CRC == 发送方发来的CRC
			{
				UpDataA.XmodemNB++;

				uint16_t buf_offset = ((UpDataA.XmodemNB - 1) % (FLASH_PAGE_SIZE/128)) * 128;// 计算当前数据应该放在缓存的哪个位置
				memcpy(&UpDataA.UpDatabuff[buf_offset], &data[3], 128); // 把收到的 128 字节复制到缓存数组

				// 每收到16帧(2KB)写入一次Flash
				if((UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) == 0)
				{
						uint32_t flash_addr = FLASH_A_START_ADDR + ((UpDataA.XmodemNB / (FLASH_PAGE_SIZE/128)) - 1) * FLASH_PAGE_SIZE;// 计算要写入的 Flash 地址
						InternalFLASH_WriteWords(flash_addr, (uint32_t *)UpDataA.UpDatabuff, FLASH_PAGE_SIZE / 4);// 写入整页 Flash
				}
				printf("\x06");	// 回复 ACK(0x06)告诉串口助手上一帧接收成功
			}
			else
			{
				printf("\x15");//校验错误,回复 NAK(0x15)要求重发
			}
		}
		if((datalen == 1) && (data[0] == 0x04))// 发送方发完所有数据后,会发送 0x04 表示结束
		{
			printf("\x06");	// 回复 ACK 确认结束

			if((UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) != 0)// 如果最后剩下的数据不够 2048 字节
			{
					uint32_t flash_addr = FLASH_A_START_ADDR + (UpDataA.XmodemNB / (FLASH_PAGE_SIZE/128)) * FLASH_PAGE_SIZE;// 计算写入地址
					uint16_t bytes_to_write = (UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) * 128;// 计算剩余有效字节数
					uint16_t words_to_write = (bytes_to_write + 3) / 4;// 转换成 32位字 数量(向上取整)
					
					InternalFLASH_WriteWords(flash_addr, (uint32_t *)UpDataA.UpDatabuff, words_to_write);// 把最后不足一页的数据写入 Flash
			}
			BootStaFlag &=~ IAP_XMODEMD_FLAG;//清除接收状态标志位,退出 Xmodem 模式

			Delay_ms(100);
			NVIC_SystemReset();// 延时后重启,运行新下载的程序
		}
	}
}

        下载测试一下,我们首先先验证1和7的命令,将A区数据擦除:

        然后输入命令2:可以发现开始发送大写C:

        找到图示位置:

        找到我们刚刚生成号的bin文件,发送:

        可以发现bin文件开始下载到A区,并且下载完重启,发现A分区已经更新完毕:

完整工程:

基于STM32实现BootLoader程序串口命令Xmodem实现A区更新资源-CSDN下载

7.4  设置OTA版本号

        下面我们在继续完成命令3的功能:设置OTA版本号。首先在保存在24C02内的OTA信息相关的结构体中创建一个数组,用于记录版本号:

typedef struct{
	uint32_t OTA_flag;		// OTA升级标志位,匹配OTA_SET_FLAG时触发升级
	uint32_t Firelen[11];	//0号成员对应OTA的大小
	uint8_t OTA_ver[32];	//用于记录版本号
}OTA_InfoCB;

        创建一个设置版本号标志位:

#define SET_VERSION_FLAG  0x00000008

        增加命令3的置位:

		else if((datalen == 1) && (data[0] == '3'))//设置OTA版本号
		{
			printf("设置版本号\r\n");
			BootStaFlag |= SET_VERSION_FLAG;
		}

        当置位后判断版本号长度是否正确,然后判断格式是否正确:

    if(BootStaFlag&SET_VERSION_FLAG)
	{
		if(datalen == 26)
		{
			if(sscanf((char *)data,"VER-%d.%d.%d-%d/%d/%d-%d:%d",&temp,&temp,&temp,&temp,&temp,&temp,&temp,&temp) == 8)
			{
				memset(OTA_Info.OTA_ver,0,32);
				memcpy(OTA_Info.OTA_ver,data,26);
				EE_WriteOTAInfo();
				printf("版本号正确\r\n");
				BootStaFlag &=~ SET_VERSION_FLAG;

				BootLoader_Info();
			}
			else
			{
				printf("版本号格式错误!!!\r\n");
			}
		}
		else
		{
			printf("版本号长度错误!!!\r\n");
		}
	}

        这里的长度是因为我们版本号准备设置成这样:

VER-1.0.0-2026/05/13-12:00

        想要设计别的样式,只需要更改长度和sscanf里面的内容即可。

        下载测试一下:

        这里需要注意一点里面的版本号输入,只需要在外面Ctrl+C,然后来到应用鼠标右键即可,不要Ctrl+V,否则显示长度错误:

7.5  查询OTA版本号

        这个实现就比较简单了,只需要调用读取函数即可:

		else if((datalen == 1) && (data[0] == '4'))//查询OTA版本号
		{
			printf("查询版本号\r\n");
			EE_ReadOTAInfo();//从24C02读取数据
			printf("版本号:%s\r\n",OTA_Info.OTA_ver);

			BootLoader_Info();//返回命令行
		}

        验证一下:

完整工程:

基于STM32实现BootLoader程序串口IAP实现OTA版本号的读写资源-CSDN下载

7.6  向外部Flash下载程序

        创建两个标志位:

#define CMD_5_FLAG  			0x00000010
#define CMD5_XMODEM_FLAG        0x00000020

        如果输入命令字5置位:

		else if((datalen == 1) && (data[0] == '5'))//向外部Flash下载程序
		{
			printf("向外部Flash下载程序,输入需要使用的块编号(1~9)\r\n");
			BootStaFlag |= CMD_5_FLAG;
		}

        如果置位,则判断是否是1~9(这里我们创建九个块),如果是置下载标志位CMD5_XMODEM_FLAG,然后将对应OTA块的编号写入,简单来时就是可以在1~9的编号分别存储不同的代码,需要哪个后面可以下载那个:

	else if(BootStaFlag&CMD_5_FLAG)
	{
		if(datalen == 1)
		{
			if((data[0] >= 0x31)&&(data[0] <= 0x39))//ASCII码1~9的取值范围
			{
				UpDataA.W25Q64_BlockNB = data[0] - 0x30;
				BootStaFlag |= (IAP_XMODEMC_FLAG | IAP_XMODEMD_FLAG | CMD5_XMODEM_FLAG);

				UpDataA.XmodemTimer = 0;//初始化UpDataA.XmodemTimer为 0,用于 Xmodem 协议的超时控制
				UpDataA.XmodemNB = 0;//初始化帧号计数器(收到多少包)

				OTA_Info.Firelen[UpDataA.W25Q64_BlockNB] = 0;
				SPI_FLASH_SectorErase(UpDataA.W25Q64_BlockNB);

				printf("通过Xmodem协议,向外部Flash第%d个块下载程序,请使用bin格式文件\r\n",UpDataA.W25Q64_BlockNB);
				BootStaFlag &=~ CMD_5_FLAG;
			}
			else
			{
				printf("编号错误!!!\r\n");				
			}
		}
		else
		{
			printf("数据长度错误!!!\r\n");			
		}
	}

        然后来到我们上面编写好的下载函数部分,主要更改的就是接收到完整帧结构后的下载代码,我们之前只实现了下载到内部Flash的代码,我们可以通过分支语句判断CMD5_XMODEM_FLAG是否被置位,判断是下载到哪里:

if((UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) == 0)
{
	if(BootStaFlag&CMD5_XMODEM_FLAG)
	{
		uint32_t block_base = UpDataA.W25Q64_BlockNB * 64 * 1024;  // 每个块 64KB
		uint32_t page_offset = ((UpDataA.XmodemNB / 16) - 1) * FLASH_PAGE_SIZE;
		uint32_t w25q64_addr = block_base + page_offset;

		// 直接写入 2048 字节
		SPI_FLASH_BufferWrite(UpDataA.UpDatabuff, w25q64_addr, FLASH_PAGE_SIZE);
	}
	else
	{
		uint32_t flash_addr = FLASH_A_START_ADDR + ((UpDataA.XmodemNB / (FLASH_PAGE_SIZE/128)) - 1) * FLASH_PAGE_SIZE;// 计算要写入的 Flash 地址
		InternalFLASH_WriteWords(flash_addr, (uint32_t *)UpDataA.UpDatabuff, FLASH_PAGE_SIZE / 4);// 写入整页 Flash
	}
}

        我们来拆解一下设计逻辑,对于对外部Flash的写入是如何实现的,可以参考我们之前的工程:

 /**
  * @brief  对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
  * @param	pBuffer,要写入数据的指针
  * @param  WriteAddr,写入地址
  * @param  NumByteToWrite,写入数据长度
  * @retval 无
  */
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite);

【OTA】手把手写BootLoader程序·硬件SPI读写Flash芯片(W25Q64)_p25q64读写程序-CSDN博客

        我们需要完善几个概念:

  • Xmodem 一包是128 字节
  • STM32F103ZET6是2048字节一页(FLASH_PAGE_SIZE = 2048 字节),
  • W25Q64 一块是64KB,也就是64 * 1024 = 65536 字节

        我们其实可以按照每收到128字节写入一次,不过这样做太慢,并且需要频繁写入损耗太大,所以我们STM32和W25Q64 都是按页,Xmodem 一包是128 字节取16次刚好2048:

if((UpDataA.XmodemNB % (FLASH_PAGE_SIZE/128)) == 0)
{
}

        然后判断CMD5_XMODEM_FLAG有没有置位,当置位表示当前要写入 W25Q64 外部 Flash,如果没有则写入 STM32 内部 Flash:

	if(BootStaFlag&CMD5_XMODEM_FLAG)
	{

	}
	else
	{

	}

        内部Flash的设计实现上面已经讲过了,这里主要实现写外部Flash:

	if(BootStaFlag&CMD5_XMODEM_FLAG)
	{
		uint32_t block_base = UpDataA.W25Q64_BlockNB * 64 * 1024;  // 每个块 64KB
		uint32_t page_offset = ((UpDataA.XmodemNB / 16) - 1) * FLASH_PAGE_SIZE;
		uint32_t w25q64_addr = block_base + page_offset;

		// 直接写入 2048 字节
		SPI_FLASH_BufferWrite(UpDataA.UpDatabuff, w25q64_addr, FLASH_PAGE_SIZE);
	}
 /**
  * @brief  对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
  * @param	pBuffer,要写入数据的指针
  * @param  WriteAddr,写入地址
  * @param  NumByteToWrite,写入数据长度
  * @retval 无
  */
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite);

        这里主要需要搞懂地址怎么算?因为对于数据直接接收我们A区更新用到的结构体的数据,里面记录了更新A区时用于保存从W25Q64读取的数据:

typedef struct{
	uint8_t UpDatabuff[FLASH_PAGE_SIZE];//更新A区时用于保存从W25Q64读取的数据
	uint32_t W25Q64_BlockNB;						//用于记录从哪个W25Q64的块中读取的数据
	uint32_t XmodemTimer;     					//超时检测
	uint32_t XmodemNB;									//数据包计数
	uint32_t XmodemCRC;									//CRC校验
}UpDataA_CB;

        长度更简单,就是2048,因为只有我们接收完2048数据长度的数据,才会进到if语句当中。

        对于写入地址怎么算呢?这里我们需要先明确我们对于W25Q64的内存规划,假设W25Q64是一本书,我们知道书是有一个大目录,然后里面又跟着一堆小目录,大目录就相当于我们前面的分节,我们将W25Q64分成九个章节,每个章节存放一种代码,代码有根据里面的小目录一点一点往下排版。

        首先是块偏移(大目录):

uint32_t block_base = UpDataA.W25Q64_BlockNB * 64 * 1024;

块 1 的起始地址 = 1 * 64*1024 = 0x10000
块 2 = 2*64K = 0x20000
块 3 = 3*64K = 0x30000

        页偏移(小目录):

uint32_t page_offset = ((UpDataA.XmodemNB / 16) - 1) * FLASH_PAGE_SIZE;

每 16 包 = 1 页(2048)
收了 16 包 → 16/16 = 1 → 第 1 页
收了 32 包 → 32/16 = 2 → 第 2 页
偏移 = 页数 × 2048

        最终地址,也就是大目录加小目录:

uint32_t w25q64_addr = block_base + page_offset;

举个例子:块 1 + 第 0 页

块基地址 = 65536
页偏移 = 0
最终地址 = 65536 + 0 = 65536

块 1 + 第 1 页

地址 = 65536 + 2048 = 67584

        和写入内部Flash一样,对不够2048字节的部分也进行写入:

uint32_t block_base = UpDataA.W25Q64_BlockNB * 64 * 1024;
uint32_t page_offset = (UpDataA.XmodemNB / 16) * FLASH_PAGE_SIZE;
uint32_t w25q64_addr = block_base + page_offset;
uint16_t bytes_to_write = (UpDataA.XmodemNB % 16) * 128;

SPI_FLASH_BufferWrite(UpDataA.UpDatabuff, w25q64_addr, bytes_to_write);

        下载验证一下,首先先擦除A区:

        重启发现A分区已经擦除完毕,不能再跳转了:

        我们开始想外部Flash下载程序,输入命令5,输入下载到1区块:

        将bin文件下载进去,可以发现能够正常下载:

        至于如何验证呢?这就需要下一个命令。

7.7  使用外部Flash内程序

        也就是说,从 W25Q64 指定块读取固件并写入内部 Flash A区,首先置位命令6:

		else if((datalen == 1) && (data[0] == '6'))//使用外部Flash内程序
		{
			printf("使用外部Flash下载程序,输入需要使用的块编号(1~9)\r\n");
			BootStaFlag |= CMD_6_FLAG;
		}
#define CMD_6_FLAG  			0x00000040

        如果命令6置位则,开始判断需要写入那一块的数据:

	else if(BootStaFlag&CMD_6_FLAG)
	{
		if(datalen == 1)
		{
			if((data[0] >= 0x31)&&(data[0] <= 0x39))//ASCII码1~9的取值范围
			{
				UpDataA.W25Q64_BlockNB = data[0] - 0x30;
				Copy_From_W25Q64_To_InternalFlash(UpDataA.W25Q64_BlockNB);
				BootStaFlag &=~ CMD_6_FLAG;
			}
			else
			{
				printf("编号错误!!!\r\n");				
			}
		}
		else
		{
			printf("数据长度错误!!!\r\n");			
		}
	}

        对于 Copy_From_W25Q64_To_InternalFlash() 函数的实现,我们是这么设计的,首先声明几个变量,作用看注释即可:

	uint32_t w25q64_addr;       // W25Q64 读取地址
	uint32_t flash_addr;        // STM32 内部写入地址
	uint16_t read_len;          // 每次读取长度(2KB)
	uint32_t firmware_len;      // 固件总长度
	uint8_t  buf[2048];         // 数据缓存
	uint32_t i;	

        首先通过读取 Firelen 获取存储的固件长度:

	// 获取固件长度(从 OTA 信息读取)
	firmware_len = OTA_Info.Firelen[block_num];
	printf("固件长度:%d 字节\r\n", firmware_len);

        写入之前先擦除:

	// 擦除内部 Flash A区
	printf("开始擦除内部Flash A区...\r\n");
	InternalFLASH_ErasePages_Num(FLASH_A_START_PAGES, FLASH_A_PAGES_NUM);
	printf("内部Flash A区擦除完成!\r\n");

        获取STM32的内部Flash的起始地址和外部Flash的起始地址:

	flash_addr = FLASH_A_START_ADDR;      // STM32 内部起始地址 0x08010000
	w25q64_addr = block_num * 65536;      // W25Q64 块地址 = 块号 × 64KB

        循环读写数据:

	// 按 2KB 一页循环读写
	for(i=0; i<firmware_len; i+=2048)
	{
		// 最后一页不足 2048 字节,按实际长度读取
		if( (firmware_len - i) < 2048 )
		{
				read_len = firmware_len - i;
		}
		else
		{
				read_len = 2048;
		}

		// 从 W25Q64 读取 2KB 数据
		SPI_FLASH_BufferRead(buf, w25q64_addr + i, read_len);

		// 写入内部 Flash
		InternalFLASH_WriteWords(flash_addr + i, (uint32_t *)buf, read_len / 4);

		printf("写入进度:%d/%d\r\n", i + read_len, firmware_len);
	}

        读取完成延时重启:

	printf("从 W25Q64 拷贝固件到内部Flash 完成!\r\n");
	Delay_ms(100);
	NVIC_SystemReset(); // 重启运行新程序

        我们来继续验证一下,其他步骤先按照7.6操作一遍,然后输入命令6下载区块1的代码,可以发现正常下载:

验证代码:

基于STM32实现BootLoader程序串口IAP实现外部Flash更新代码资源-CSDN下载

完整OTA实现过程:

OTA_时光の尘的博客-CSDN博客

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐