STM32IAP学习笔记

单片机不同的程序下载方式

ICP

        ICP是指在电路中编程。使用厂家配套的软件或仿真器进行程序烧录,目前主流的有JTAG接口和SWD接口,常用的烧录工具为J-Link、ST-Link等。在程序开发阶段,通常在连接下载器的情况下直接使用编程软件进行程序下载调试。在MDK软件中可以选择不同的下载器。一般用于调试阶段,实际产品中的电路板一般会密封在外壳中,不便于用下载器烧录程序。

ISP

        在系统中编程。以STM32为例,其内置了一段Bootloader程序,可以通过更改BOOT引脚电平来运行这段程序,再通过ISP编程工具(flymcu通过串口烧录程序)将程序下载进去。下载完毕之后,再更改BOOT至正常状态,使得MCU运行所下载的程序。这种方法需要更改boot模式,实际工程中也不方便。

IAP

        在应用中编程。IAP可以使用微控制器支持的任一种通信接口(如I/O端口、USB、CAN、UART、I2C、SPI等)下载程序或数据到FLASH中。IAP允许用户在程序运行时重新烧写FLASH中的内容。但需要注意,IAP要求至少有一部分程序(Bootloader)已经使用ICP或ISP烧到FLASH中。一般情况下,产品的外壳都会留有通信接口,若能通过这种通信方式对程序进行升级,则可以省去拆装的麻烦。在此基础上,若引入远距离或无线数据传输方案,更可以实现远程编程或无线编程。 ​

        通常实现 IAP 功能时,需要在设计固件程序时编写两个项目代码,第一个项目程序(bootloader)不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码(APP)才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是bootloader开始运行,它作如下操作:

​①检查是否需要对APP代码进行更新 ​

②如果不需要更新则转到④ ​

③执行更新操作 ​

④跳转到APP执行 ​

        bootloader必须通过其它手段,如 JTAG 或 ISP 烧入;APP代码可以使用bootloader的 IAP 功能烧入,也可以和boot loader一起烧入,以后需要程序更新时再通过boot loader更新。 ​他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader(0x08000000)紧跟其后的就是 APP 程序

        判断是否需要更新APP的方法:

1.通过拨码开关、跳线帽等方式设定单片机某一引脚电平状态,程序通过读取引脚电平判断是否需要升级。此种方式需要接触板卡进行操作,当板卡被封闭在外壳中或安装于不便于操作位置时很难实现。

2.软件内设定一标志位(变量),通过判断标志位状态判断是否需要升级。该标志位状态掉电不能改变,故需要存储在外部EEPROM或单片机内部FLASH中。若存储在外部EEPROM,则需要增加额外的电路;若存储在单片机内部FLASH,由于FLASH每次写入都需要擦除一整页,会造成资源浪费。

3.单片机每次上电首先进入BootLoader程序,在BootLoader中等待一定时间,若上位机软件在该时间段内发起通讯,则停留在Bootloader程序中等待固件升级;若该时间段内无通讯,则跳转到正常的APP程序。该方式每次上电都要等待一定时间,需要考虑是否可以接受。

STM32程序运行流程

正常程序运行流程

        STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求,此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。

加入IAP后程序运行流程

        STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到bootloader的 main 函数,如图标号①所示,此部分同上图 一样;在执行完boot loader以后(即将新的 APP 代码写入 STM32的 FLASH,新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:

①新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;

②必须将新程序的中断向量表相应的移动,移动的偏移量为 x;

不同型号STM32的FLASH大小

        BootLoader程序和APP 程序存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 BootLoader,紧跟其后的就是 APP 程序。在进行FLASH空间划分之前,首先需要了解一下不同型号STM32单片机的FLASH大小。下图中的闪存容量就是FLASH的大小。

        对于不同容量的STM32F1系列产品,其FLASH页大小是不同的,具体的容量划分规则如下: ​ 小容量产品:FLASH容量在16K至32K字节之间。

中容量产品:FLASH容量在64K至128K字节之间。 ​

大容量产品:FLASH容量在256K至512K字 节之间。

对于小容量和中容量的产品,其页大小为1K,对于大容量产品,其页大小为2K

        用户程序写在flash的主存储器中,对于小容量产品来说,FLASH每页1K,最多32页,也就是32K。 对于中容量产品来说,FLASH每页1K,最多128页,也就是128K。对于大容量产品来说,FLASH每页2K,最多256页,也就是512K,大容量产品的flash模块组织如图:

 keil中配置bootloader和APP

配置boot loader

        在进行FLASH空间划分时,必须知道编写的程序占用FLASH空间大小。用MDK软件进行工程编译之后会生成一个.map文件,在该文件末尾可找到程序需要占用的FLASH空间。在实际设计过程中,主要是确定boot loader的占用空间,在FLASH起始位置给Bootloader留出足够的空间,以便确定APP的起始地址,也就是中断向量表的偏移量。在实际分配过程中,可以给Bootloader多一些空间,以便后续对Bootloader的功能拓展。

         在MDK软件配置项中,可以对程序的起始位置以及大小进行设置。对于BootLoader程序来说,只需要设置其Size,该值可根据刚才map文件中的值进行预估。0x08000000就是FLASH的起始地址,这里设置bootloader的大小为0x4000,也就是16k

 配置APP

        BootLoader程序按照正常的程序编写即可。而APP程序由于其下载位置与默认程序下载位置不同,故需要做一些特殊的配置。首先是APP 程序起始地址设置。起始位置即去除BootLoader程序之后剩余空间的首地址。一般设定为某一页的首地址,因为FLASH写入之前必须进行页擦除,在下图中按照Bootloader的size可得APP首地址为0x08004000。Size <= FLASH原始大小 - 偏移量(0x4000),例子中是STM32F103C8T6,FLASH大小为64K。上面分给Bootloader的大小为0x4000,也就是16K,那么size最大就是48k,也就是0xc000。如下图所示。

 接着设置中断向量表的偏移量,在主函数起始位置(也就是main函数的第一句话)添加:

SCB->VTOR = FLASH_BASE | 0x4000; //0x4000即BootLoader大小(偏移量)

bin文件生成

        IAP过程中传输的数据文件一般为后缀名为bin的文件,该文件内容与正常烧录进FLASH中的数据内容一致,便于程序升级。但是MDK软件并不能直接生成bin文件,需要进行一些配置。这里我们通过 MDK 自带的格式转换工具 fromelf.exe,来实现.axf 文件到.bin 文件的转换。该工具在 MDK 的安装目录\ARM\BIN40 文件夹里面。我们通过在 MDK 点击 Options for Target→User 选项卡,在 After Build/Rebuild 栏,勾选 Run #1,并写入:D:\MDK5.21A\ARM\ARMCC\bin\fromelf.exe --bin -o ..\OBJ\TEST.bin ..\OBJ\TEST.axf,如下图所示。加粗部分根据实际MDK安装路径进行修改,TEST.bin和TEST.axf中的TEST根据工程名称修改。

         除了上述方法生成bin文件还可以在网上下载hex2bin等软件来生成bin文件。

代码解析

        以正点原子的串口IAP实验为例,该实验通过串口接收APP的bin文件,通过按键控制固件更新和APP代码执行。

bootloader代码

串口接收部分
#define USART_REC_LEN  			41*1024 	//定义最大接收字节数 41K
u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000. 

void uart_init(u32 bound){
  //GPIO端口设置
  GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟
  
	//USART1_TX   GPIOA.9
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
   
  //USART1_RX	  GPIOA.10初始化
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  

  //Usart1 NVIC 配置
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器
  
   //USART 初始化设置

	USART_InitStructure.USART_BaudRate = bound;//串口波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

  USART_Init(USART1, &USART_InitStructure); //初始化串口1
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
  USART_Cmd(USART1, ENABLE);                    //使能串口1 

}

void USART1_IRQHandler(void)                	//串口1中断服务程序
	{
	u8 Res;
#if SYSTEM_SUPPORT_OS 		//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntEnter();    
#endif
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)
		{
		Res =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数据
		if(USART_RX_CNT<USART_REC_LEN)
		{
			USART_RX_BUF[USART_RX_CNT]=Res;//将读取到的数据存到缓冲区
			USART_RX_CNT++;			 									     
		} 		 
     } 
main函数
#define FLASH_APP1_ADDR		0x08004000  	//第一个应用程序起始地址(存放在FLASH)
int main(void)
 { 
	u8 t;
	u8 key;
	u16 oldcount=0;	//老的串口接收数据值
	u16 applenth=0;	//接收到的app代码长度
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
	delay_init();	    	 //延时函数初始化	  
	uart_init(115200);	 	//串口初始化为256000
	LED_Init();		  		//初始化与LED连接的硬件接口
 	KEY_Init();				//按键初始化 	  
	while(1)
	{
	 	if(USART_RX_CNT)//如果串口接收到的数据不为空
		{
			if(oldcount==USART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.
			{
				applenth=USART_RX_CNT;//得到数据长度
				oldcount=0;
				USART_RX_CNT=0;
				printf("用户程序接收完成!\r\n");
				printf("代码长度:%dBytes\r\n",applenth);
			}else oldcount=USART_RX_CNT;			
		}	  	 
		key=KEY_Scan(0);//按键扫描
		if(key==WKUP_PRES)			//WK_UP按键按下
		{
			if(applenth)//如果接收到的数据不为空
			{
				printf("开始更新固件...\r\n");	
 				if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
				{	 
					iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);//更新FLASH代码   
					printf("固件更新完成!\r\n");
				}else 
				{	   
					printf("非FLASH应用程序!\r\n");
				}
 			}else 
			{
				printf("没有可以更新的固件!\r\n");
			}									 
		} 
		if(key==KEY1_PRES)//KEY1按下
		{
			printf("开始执行FLASH用户代码!!\r\n");
			if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
			{	 
				iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
			}else 
			{
				printf("非FLASH应用程序,无法执行!\r\n");   
			}									   
		}
		   
		 
	}   	   
}
 检查地址合法性解析
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)
//0X20001000是串口接收数据缓冲区的起始地址,接收的数据是APP,0X20001000处存放的是栈顶地址,0X20001000 + 4处存放的是中断向量表起始地址,也就是复位中断服务函数的地址,对其进行强制类型转换,然后解引用,得到复位中断服务函数的地址,判断地址是不是在flash范围内
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)
//上面将串口缓冲区接收到的数据存到了flash中,FLASH_APP1_ADDR处存放的东西和上面0X20001000存放的东西是一样的
跳转到APP代码解析
typedef  void (*iapfun)(void);				//重命名一个函数指针类型.
iapfun     jump2app;                             //创建一个函数指针jump2app
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法.
	{ 
		jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		
		MSR_MSP(*(vu32*)appxaddr);					//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
		jump2app();									//跳转到APP.
	}
}		 
检查栈顶地址解析

        STM32的RAM起始地址为0x20000000,上述代码中appxaddr处存放的是栈顶地址,栈顶地址在RAM中,appxaddr+4处存放的是复位中断向量,在FLASH中

        appxaddr是存放栈顶地址的指针,所以对appxaddr解引用就得到了栈顶地址。由于栈顶地址应该在RAM中,

        1.对于128kRAM的芯片,堆栈地址的空间是0x20000000 - 0x2001FFFF,地址与上0x2FFE0000,就是得到bit17~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十七位不需要判断。

        2.对于48KRAM的芯片,堆栈地址空间是0x20000000 - 0x2000BFFF,地址应该与上0x2FFFC000,就是得到bit14~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十四位不需要判断。

        3.对于512KRAM的芯片,堆栈地址空间是0x20000000 - 0x2007FFFF,地址应该与上0x2FF80000,就是得到bit19~bit27位,只需判断这些位,如果有1,与的结果就不等于0x20000000,就表示地址不合法,低十九位不需要判断。

        ST32最早的时候内部ram的大小基本在128Kb以内,RAM小于128K的芯片使用0x2FFE0000也基本没问题,就一直在沿用。

APP跳转解析
jump2app=(iapfun)*(vu32*)(appxaddr+4);	
(vu32*)(appxaddr+4)//将复位中断向量地址值强制转化为32位的指针变量
*(vu32*)(appxaddr+4)//取出复位中断向量地址保存的数据=复位中断的中断服务程序入口地址
(iapfun)*(vu32*)(appxaddr+4)//将中断服务程序入口地址再转化成定义的函数指针类型
jump2app=(iapfun)*(vu32*)(appxaddr+4);//中断服务程序入口地址(iapfun类型的函数指针)赋值jump2app(定义的iapfun类型的函数指针其名为jump2app)
MSR_MSP(*(vu32*)appxaddr);		//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app();                   //调用函数且jump2app函数的起始地址在上面赋值,可实现PC指针的跳转;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值