STM32速成笔记—串口IAP

本文涉及到串口通信和Flash知识,对于这部分知识不熟悉的小伙伴可以到博主STM32速成笔记专栏查看。

文章目录

  • 一、串口IAP简介
    • [1.1 什么是IAP](#1.1 什么是IAP)
    • [1.2 STM32下载程序](#1.2 STM32下载程序)
  • 二、串口IAP有什么作用
  • 三、启动流程
    • [3.1 正常启动流程](#3.1 正常启动流程)
    • [3.2 加入IAP后的启动流程](#3.2 加入IAP后的启动流程)
  • 四、必备知识
    • [4.1 修改程序运行起始地址](#4.1 修改程序运行起始地址)
    • [4.2 设置中断向量表偏移](#4.2 设置中断向量表偏移)
    • [4.3 生成.bin文件](#4.3 生成.bin文件)
  • 五、串口IAP实现
    • [5.1 串口中断服务函数](#5.1 串口中断服务函数)
    • [5.2 Flash写入程序](#5.2 Flash写入程序)
    • [5.3 IAP程序](#5.3 IAP程序)
    • [5.4 main函数](#5.4 main函数)
  • 六、注意事项

一、串口IAP简介

1.1 什么是IAP

IAP,英文全程In Application Programming,在应用中编程。很好理解,就是在程序运行过程中我们进行程序的烧写,或者叫升级。

1.2 STM32下载程序

我们都知道,STM32可以利用串口下载程序,这是因为ST公司在产线上就在产品中内嵌了自举程序。所谓的自举程序,实际就是支持我们通过串口下载程序的代码。自举程序被存放在系统存储区,因此如果我们需要通过串口下载程序,需要将Boot0接高电平,Boot1接低电平,让程序从系统存储器开始运行,运行自举程序。下载完成后我们再将Boot0接地,让程序从主闪存存储器开始运行。自举程序是我们用户无法修改的。

二、串口IAP有什么作用

上面我们介绍了什么是IAP,那么这个IAP到底有什么作用呢?

首先介绍两个词------BootloaderApplication。Bootloader实际就是一段引导程序,单片机上电后先执行Bootloader程序,然后再执行用户编写的应用程序Application。介绍完这两个词,我们来介绍一下IAP有什么作用。比如我们生产了A,B两款产品。A产品是某个精密器件的一部分,B产品是一款物联网产品。我们的产品销售范围很广,远销海外。

某天A产品的某个客户反映了一个Bug,我们编写好了程序,需要进行程序更新,或者叫升级。利用IAP,我们可以在程序运行时,通过预留的通信接口直接烧写程序。而不需要再把整个设备拆开,像我们调试时那样下载程序。甚至我们可以直接给客户邮寄一个小设备,客户直接进行傻瓜式升级。

又过了一段时间,B产品的程序出现了一个Bug,风险等级比较低,但是依旧需要全体升级程序。我们总不能挨个产品派人去升级,成本极大。这时候又轮到我们的IAP出场了。它可以在所有设备在线运行的情况下,直接通过网络下发升级程序,实现在线升级,节约了大量的人力成本。

通过上面这两个例子,大家应该能够基本了解IAP的用处,使用IAP让我们不需要再使用调试器进行下载,甚至实现设备的在线升级。

三、启动流程

在介绍如何实现IAP之前,我们先来简单了解以下STM32的启动流程。

3.1 正常启动流程

这里的正常启动流程指的是,没有添加IAP的流程。

程序启动时首先开辟栈空间,配置栈顶指针。然后配置堆空间。配置完成后,建立中断向量表,在中断向量表中找到复位中断,开始执行复位中断服务函数,然后跳转到main函数中,执行用户代码。当用户代码中有中断请求时,会回到中断向量表,根据中断源执行相应的中断服务函数。

3.2 加入IAP后的启动流程

下面是加入IAP之后的启动流程。

可以看到,与上面不同的是,加入IAP后,执行完复位中断服务函数后直接进入IAP的main函数。在执行完IAP之后,跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main 函数。

由上面的两个启动过程我们可以看出

  • 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始。
  • 必须将新程序的中断向量表相应的移动,移动的偏移量为 x。

四、必备知识

4.1 修改程序运行起始地址

点击魔术棒,选择"Target",修改运行起始地址和代码大小。

4.2 设置中断向量表偏移

VTOR 寄存器存放的是中断向量表的起始地址。如果要设置中断向量表偏移,只需要在main函数最开始添加如下语句即可

c 复制代码
SCB->VTOR = FLASH BASE | 偏移量:

4.3 生成.bin文件

点击魔术棒,选择"User",按照如下配置,输入下面的内容

c 复制代码
fromelf --bin -o "[email protected]" "#L"

点击编译,不报错就可以,去设置的输出文件夹中就可以找到对应的.bin文件。

五、串口IAP实现

本次的目标是实现一个串口IAP,也就是编写Bootloader,在程序运行过程中实现程序的下载。Bootloader程序应该可以通过串口接收上位机发来的.bin文件(App程序),检查后将.bin文件写入到Flash特定位置,然后跳转到App程序运行。

5.1 串口中断服务函数

本次的串口中断服务函数与之前不同,这里单独贴出来。需要定义一个接收数组,接收数组的起始地址限制为0X20001000。接收数组最多可以接收55K字节,可以根据需要调整。但是需要注意的是,数组的大小需要比App程序要大,而且不能超过芯片的SRAM空间大小。

c 复制代码
/*
 *==============================================================================
 *函数名称:USART1_IRQHandler
 *函数功能:USART1中断服务函数
 *输入参数:无
 *返回值:无
 *备  注:无
 *==============================================================================
 */
u32 gReceCount = 0;   // 接收计数变量
// 接收数组
// 限制起始地址为0X20001000
// 保证偏移量为 0X200的倍数
// 是为了给App留SRAM空间
u8 gReceFifo[USART_RECE_MAX_LEN]__attribute__ ((at(0X20001000)));

void USART1_IRQHandler(void)  
{
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)   //接收到一个字节  
	{
		if (gReceCount < USART_RECE_MAX_LEN)
		{
			gReceFifo[gReceCount++] = USART1->DR;
		}
		else
		{
			printf ("APP code out of memory!\r\n");
		}
	}
}

5.2 Flash写入程序

关于Flash程序,这里就不在赘述,只是贴一下带检查的写入程序。其他具体内容可以到博主STM32速成笔记专栏查看。

c 复制代码
/*
 *==============================================================================
 *函数名称:Med_Flash_Write
 *函数功能:从指定地址开始写入指定长度的数据
 *输入参数:WriteAddr:写入起始地址;pBuffer:数据指针;
						NumToRead:写入(半字)数
 *返回值:无
 *备  注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
 *==============================================================================
 */

// 根据中文参考手册,大容量产品的每一页是2K字节
#if STM32_FLASH_SIZE < 256
	#define STM32_SECTOR_SIZE   1024   // 字节
#else 
	#define STM32_SECTOR_SIZE   2048
#endif

// 一个扇区的内存
u16 STM32_FLASH_BUF[STM32_SECTOR_SIZE / 2];

void Med_Flash_Write (u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
	u32 secpos;   // 扇区地址
	u16 secoff;   // 扇区内偏移地址(16位字计算)
	u16 secremain;   // 扇区内剩余地址(16位计算)	   
 	u16 i;    
	u32 offaddr;   // 去掉0X08000000后的地址
	
	// 判断写入地址是否在合法范围内
	if (WriteAddr < STM32_FLASH_BASE || (WriteAddr >= (STM32_FLASH_BASE + 1024 * STM32_FLASH_SIZE)))
	{
		return;   // 非法地址
	}
	
	FLASH_Unlock();   // 解锁
	offaddr = WriteAddr - STM32_FLASH_BASE;   // 实际偏移地址
	secpos = offaddr / STM32_SECTOR_SIZE;   // 扇区地址
	secoff = (offaddr % STM32_SECTOR_SIZE) / 2;   // 在扇区内的偏移(2个字节为基本单位)
	secremain = STM32_SECTOR_SIZE / 2 - secoff;   // 扇区剩余空间大小
	
	if (NumToWrite <= secremain)
	{
		secremain = NumToWrite;   // 不大于该扇区范围
	}
	
	while (1) 
	{
		// 读出整个扇区的内容
		Med_Flash_Read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
		
		// 校验数据
		for (i = 0;i < secremain;i ++)
		{
			// 需要擦除 
			if (STM32_FLASH_BUF[secoff + i] != 0XFFFF)
			{
				break; 
			}				
		}
		// 需要擦除
		if (i < secremain)
		{
			FLASH_ErasePage(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE);   // 擦除这个扇区
			
			// 复制
			for (i = 0;i < secremain;i ++)
			{
				STM32_FLASH_BUF[i + secoff] = pBuffer[i];	  
			}
			
			// 写入整个扇区
			Med_Flash_Write_NoCheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
		}
		else
		{
			// 写已经擦除了的,直接写入扇区剩余区间
			Med_Flash_Write_NoCheck(WriteAddr,pBuffer,secremain);
		}
		
		if (NumToWrite == secremain)
		{
			break;   // 写入结束了
		}
		// 写入未结束
		else
		{
			secpos ++;   // 扇区地址增1
			secoff = 0;   // 偏移位置为0 	 
			pBuffer += secremain;   // 指针偏移
			WriteAddr += secremain;   // 写地址偏移	   
			NumToWrite -= secremain;   // 字节(16位)数递减
			if (NumToWrite > (STM32_SECTOR_SIZE / 2))
			{
				secremain = STM32_SECTOR_SIZE / 2;   // 下一个扇区还是写不完
			}
			else
			{
				secremain = NumToWrite;   // 下一个扇区可以写完了
			}
		}	 
	}	
	FLASH_Lock();   // 上锁
}

5.3 IAP程序

IAP程序包含两部分,一部分是将接收到的.bin文件写入Flash,另一部分是跳转到App执行。

c 复制代码
/*
 *==============================================================================
 *函数名称:iap_write_appbin
 *函数功能:写入.bin文件
 *输入参数:appxaddr:App程序起始地址;appbuf:缓存App程序的数组;
						appsize:App程序大小
 *返回值:无
 *备  注:无
 *==============================================================================
 */
 // 对Flash操作的最小单位是16位
 u16 iapbuf[1024];   // 要写入Flash的内容
 
void iap_write_appbin (u32 appxaddr,u8 *appbuf,u32 appsize)
{
	u16 t;   // 临时循环变量
	u16 i = 0;   // 自增索引
	u16 temp;   // 临时计算变量
	
	u32 fwaddr = appxaddr;   // 当前写入的地址
	u8 *dfu = appbuf;   // 指向App代码数组首地址的指针
	
	// for循环,2K为单位进行写入
	for(t = 0;t < appsize;t += 2)
	{		
		temp = (u16)dfu[1] << 8;
		temp += (u16)dfu[0];	  
		dfu += 2;   // 偏移2个字节
		iapbuf[i++] = temp;	    
		if(i == 1024)
		{
			i = 0;
			Med_Flash_Write (fwaddr,iapbuf,1024);	
			fwaddr += 2048;   // 偏移2048  16=2*8所以要乘以2
		}
	}
	if(i)
	{
		Med_Flash_Write (fwaddr,iapbuf,i);   // 将最后的一些内容字节写进去
	}
}
/*
 *==============================================================================
 *函数名称:iap_load_app
 *函数功能:跳转到App
 *输入参数:appxaddr:App程序起始地址
 *返回值:无
 *备  注:无
 *==============================================================================
 */
iapfun jump2app;

void iap_load_app (u32 appxaddr)
{
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)   // 检查栈顶地址是否合法
	{ 
		jump2app=(iapfun)*(vu32*)(appxaddr+4);   // 用户代码区第二个字为程序开始地址(复位地址)		
		MSR_MSP(*(vu32*)appxaddr);   // 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
		jump2app();   // 跳转到APP
	}
}

5.4 main函数

main函数设计如下

c 复制代码
int main(void)
{
	u32 curRecCunt = 0;   // 当前接收数量
	
	// 设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	delay_init();   // 延时初始化
	uart_init(115200);   // 串口初始化
	
	printf ("Enter the Bootloader Code!\r\n");
	
	while(1)
  {
		// 如果接收到内容且传输完成
		if (curRecCunt == gReceCount && gReceCount != 0)
		{
			printf ("App Code reception complete!\r\n");
			printf ("The length of the received App code is %d!\r\n",gReceCount);
			
			// 开始准备写入Flash
			if(((*(vu32*)(0X20001000+4)) & 0xFF000000) == 0x08000000)   // 判断是否为0X08XXXXXX.
			{
				iap_write_appbin(FLASH_APP1_ADDR,gReceFifo,gReceCount);   // 更新FLASH代码  
				gReceCount = 0;   // 清零接收计数变量
				printf("Update complete!\r\n");
			}
			else 
			{ 
				printf("Update error!\r\n");
			}
			
			// 准备跳转App执行
			if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)   //判断是否为0X08XXXXXX.
			{
				printf("Enter App!\r\n");
				iap_load_app(FLASH_APP1_ADDR);   //执行FLASH APP代码
			}else 
			{
				printf("Enter error!\r\n");
			}	
		}
		else   // 未传输完成
		{
			curRecCunt = gReceCount;   // 更新当前接收数量
			// 延时一定要有,给串口中断留出时间
			// 如果没有会导致接收不完全
			delay_ms(10);
		}
	}
}

六、注意事项

需要注意的是,上面的程序中App程序是从0x8010000开始,需要配置好程序起始地址和中断向量表偏移

相关推荐
ALINX技术博客24 分钟前
【ALINX 实战笔记】FPGA 大神 Adam Taylor 使用 ChipScope 调试 AMD Versal 设计
笔记·fpga开发
关山煮酒33 分钟前
【数据挖掘笔记】兴趣度度量Interest of an association rule
笔记·数据挖掘
我不是帅戈38 分钟前
STM32单片机内存分配详细讲解
stm32·单片机·嵌入式·内存管理·.map文件
机器视觉知识推荐、就业指导1 小时前
Qt/C++面试【速通笔记九】—视图框架机制
c++·笔记·qt
s_little_monster1 小时前
【Linux】socket网络编程基础
linux·运维·网络·笔记·学习·php·学习方法
汇能感知1 小时前
光谱相机的图像预处理技术
经验分享·笔记·科技
小蜗笔记2 小时前
path环境变量满了如何处理,分割 PATH 到 Path1 和 Path2
笔记
代码小将4 小时前
Leetcode209做题笔记
java·笔记·算法
朗迹 - 张伟4 小时前
UE5 PCG学习笔记
笔记·学习·ue5
寻丶幽风6 小时前
论文阅读笔记——双流网络
论文阅读·笔记·深度学习·视频理解·双流网络