LV02-03-STM32内部FLASH实现IAP编程

摘要:

  这篇笔记主要是 STM32 的在线应用编程相关笔记。若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用平台
Windows windows11
Keil uVision5 uVision V5.29.0.0
JLINK驱动 Windows版本 V688
正点原子战舰V3开发板 STM32F103ZET6
点击查看本文更新记录

2022-9-24 更新内容

创建笔记。

点击查看本文参考资料
参考方向参考原文
ST官方文档Reference manual
ST官方文档STM32F10xxx Flash memory microcontrollers
正点原子STM32开发指南STM32F1开发指南-库函数版本_V3.3

一、STM32编程方式

1.在线编程

1.1基本概念

  ICP,全称为In-Circuit Programming,即在线编程。是通过 JTAG或者SWD 协议或者系统加载程序(Bootloader)下载用户应用程序到微控制器中。

  这种方式就是我们平时用的下载程序方式,就是通过JTAG下载器将我们通过Keil生成的hex文件下载到STM32开发板中。

1.2下载流程

image-20220925104053454

2.应用编程

2.1基本概念

  IAP,全称为In Application Programming,即在应用程序中编程,就是应用编程。它是通过任何一种通信接口(如IO口、USB、CAN、UART、I2C、或者SPI等)下载程序或者应用数据到存储器中。也就是说,STM32允许用户在应用程序中重新烧写闪存存储器中的内容。但是,IAP 需要至少有一部分程序已经使用 ICP 方式烧到闪存存储器中(Bootloader)。通过这种方式可以在不需要操作硬件平台的情况下实现升级(远程)。

2.2下载流程

image-20220925105311951

二、IAP编程

1.IAP简介

  前边已经提过,IAP(In Application Programming)即在应用编程, IAP 是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。

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

  • (1)检查是否需要对第二部分代码进行更新
  • (2)如果不需要更新则转到(4)
  • (3)执行更新操作
  • (4)跳转到第二部分代码执行

  第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP 代码更新。

  我们将第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP 程序,它们存放在 STM32中FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP 程序(注意,如果 FLASH 容量足够,是可以设计很多 APP 程序的)。

2.一般程序执行

  正常情况下,程序执行的过程如下图所示:

  STM32 的内部闪存(FLASH)地址起始于 0x0800 0000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临, STM32 的内部硬件机制会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序 。

(1)STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;

(2)在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;

(3)我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;

(4)根据中断源进入相应的中断服务程序,如图标号④所示;

(5)在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示 。

3.加入APP应用

  我们加入APP后,程序执行的过程如下图:

image-20220925120342387

(1)STM32 复位后, 还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,

(2)在执行完 IAP 以后(即将新的 APP 代码写入 STM32的 FLASH,新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,

(3)同样 main 函数为一个死循环,此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。在 main 函数执行过程中,如果CPU 得到一个中断请求, PC 指针仍强制跳转到地址 0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;

(4)程序根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;

(5)在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。

  通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:

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

三、APP制作

  STM32 的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行, 我们使用的STM32F103ZET6有512KB的FLASH和64KB的SRAM,FLASH的地址为 0x0800 0000 ~ 0x0807 FFFF,SRAM的地址为 0x2000 0000 ~ 0x2000 FFFF。

1.APP起始地址设置

1.1 FLASH

  【Options for Target】→【Target】

image-20220925122206196

  默认的条件下,图中 IROM1 的起始地址(Start)一般为 0x0800 0000,大小(Size)为 0x80000,即从 0x08000000 开始的 512K 空间为我们的程序存储(因为我们的 STM32F103ZET6 的 FLASH大小是 512K)。

  而当前图中,我们设置IROM1的起始地址(Start)为 0X0801 0000,即偏移量为 0X10000(64K 字节),因而,留给 APP用的 FLASH 空间(Size)只有 0x80000 - 0x10000= 0x70000(448K字节)大小了。

  设置好 Start 和 Szie,就完成 APP 程序的起始地址设置。这里的 64K 字节,需要我们根据 Bootloader 程序大小进行选择,理论上我们只需要确保 APP 起始地址在 Bootloader 之后,并且偏移量为 0x200 的倍数即可。这里我们选择 64K(0x10000)字节,留了一些余量,可以方便 Bootloader 以后的升级修改。

1.2 SRAM

  若APP在SRAM中,我们设置的方式也类似:

image-20220925122830611

  这里我们将 IROM1 的起始地址(Start)定义为: 0x20001000,大小为 0xC000(48K 字节),即从地址 0x20000000 偏移 0x1000 开始,存放 APP 代码。因为整个 STM32F103ZET6 的 SRAM大 小 为 64K 字 节 , 所 以 IRAM1 ( SRAM ) 的 起 始 地 址 变 为 0x2000D000( 0x20001000 + 0xC000=0x2000D000 ) , 大 小 只 有 0x3000 ( 12K 字 节 )。

  这样,整个STM32F103ZET6 的 SRAM 分配情况为:最开始的 4K 给 Bootloader 程序使用,随后的 48K 存放 APP 程序,最后12K,用作 APP 程序的内存。这个分配关系我们可以根据自己的实际情况修改,不一定和这里的设置一模一样,不过也需要注意,保证偏移量为 0X200 的倍数(我们这里为 0X1000)。

1.3 设置成功判定

  那怎么知道设置后是否成功呢?我们可以看存储器映像中的信息,就是前边学习FLASH时说过的 .map 文件中的 Memory Map of the image 部分:

1
2
3
4
5
Image Entry point : 0x08000131

Load Region LR_IROM1 (Base: 0x08000000, Size: 0x00007770, Max: 0x00080000, ABSOLUTE)

Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x00007730, Max: 0x00080000, ABSOLUTE)

需要注意的是,有时候我们更改了 IROM1 后,这里并没有任何变化,此时我们需要修改这里:

image-20220925145628360

重新设定之后,重新编译查看即可。

2.中断向量表偏移

  在系统启动的时候,会首先调用 SystemInit() 函数初始化时钟系统,同时SystemInit() 还完成了中断向量表的设置,我们可以打开 SystemInit() 函数,看看函数体的结尾处有这样几行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @brief Setup the microcontroller system
* Initialize the Embedded Flash Interface, the PLL and update the
* SystemCoreClock variable.
* @note This function should be used only after reset.
* @param None
* @retval None
*/
void SystemInit (void)
{
// ... ...
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}

  从这里可以看出VTOR寄存器存放的是中断向量表的起始地址。默认的情况 VECT_TAB_SRAM 是没有定义,所以执行:

1
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;

  另外,SystemInit()函数的执行是在启动文件的复位中断中:

1
2
3
4
5
6
7
8
9
10
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP

【注意】

(1)这里只是举一个例子,看一下中断向量表怎么设置,后边直接设置这个 SCB->VTOR 即可。

(2)FLASH中APP和SRAM中APP的中断向量表地址设置与前边APP偏移地址相关联,注意要跟前边对应。

2.1 FLASH

  对于 FLASH中的APP,我们设置为 FLASH_BASE+偏移量 0x10000,所以我们可以在 FLASH中APP 的 main 函数最开头处添加如下代码实现中断向量表的起始地址的重设:

1
SCB->VTOR = FLASH_BASE | 0x10000;

2.2 SRAM

当使用 SRAM APP 的时候, 我们设置起始地址为:SRAM_BASE + 0x1000,同样的方法,我们在 SRAM APP 的 main 函数最开始处,添加下面代码:

1
SCB->VTOR = SRAM_BASE | 0x1000;

这样,我们就完成了中断向量表偏移量的设置。

3. bin文件的生成

  我们使用的 MDK 默认生成的文件是 .hex 文件,并不方便我们用作 IAP更新,我们希望生成的文件是 .bin 文件,这样可以方便进行 IAP 升级。MDK 自带的格式转换工具 fromelf.exe,可以实现 .axf 文件到 .bin 文件的转换。该工具在 MDK 的安装目录的这个文件夹下:

1
2
# windows 下的路径格式
\ARM\ARMCC\bin
image-20220925124632044

  fromelf.exe 转换工具的语法格式为:

1
fromelf [options] input_file

其中 options 有很多选项可以设置 ,其他的没有研究过,这里就直接用下边的命令格式即可(这是我当时安装MDK的路径):

1
C:\LenovoSoft\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o ..\OBJ_dir\xxx.bin ..\OBJ_dir\xxx.axf

那在哪里使用这个命令呢?

  • (1)确认生成的可执行文件名和输出文件的文件夹路径
image-20220925125440893
  • (2)添加命令
image-20220925125638731
1
C:\LenovoSoft\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o  ..\OBJ\TOUCH.bin ..\OBJ\TOUCH.axf
  • (3)编译工程

  编译整个工程,编译结束后,我们会看到如下提示:

image-20220925125826599

然后我们就可以看到生成的 .bin 文件啦。

4.APP生成总结

  • (1)设置 APP 程序的起始地址和存储空间大小

对于在 FLASH 里面运行的 APP 程序和 SRAM 里面运行的 APP 程序,他们的设置类似,但是要注意各个地址。

  • (2)设置中断向量表偏移量

  主要就是APP的main函数中设置 SCB->VTOR 的值 。

  • (3)设置编译后运行 fromelf.exe,生成.bin 文件

  通过在 User 选项卡,设置编译后调用 fromelf.exe,根据.axf 文件生成.bin 文件,用于IAP 更新。

四、Bootloader编写

  前边我们已经完成了APP的制作,那么现在要做的就是通过Bootloader程序将APP程序写入到相应的地址,并跳转执行。

1. APP程序跳转

  主要是实现一个跳转的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef  void (*iapfun)(void); // 定义一个函数指针类型
iapfun jump2app; // 定义一个函数指针

// 跳转到相应APP执行函数
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
/* 用户代码区第二个字为程序开始地址(复位地址,进入主函数是从复位中断处理程序中进入) */
jump2app=(iapfun)*(vu32*)(appxaddr+4);
/* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
MSR_MSP(*(vu32*)appxaddr);
/* 跳转到APP */
jump2app();
}
}

【函数说明】该函数用于跳转到指定的地址执行相应的APP程序,程序先判断栈顶地址是否合法,在得到合法的栈顶地址后,通过
MSR_MSP 函数(该函数在 sys.c 文件)设置栈顶地址,最后通过一个虚拟的函数(jump2app)跳转到 APP 程序执行代码,实现 IAP→APP 的跳转 。

【函数参数】

  • appxaddr : u32类型,其实就是 unsigned int 类型,表示APP存放的地址。

【返回值】none

【使用格式】none

【注意】none

2. APP程序接收

  我们这里通过串口来接收APP应用程序(.bin)文件。这里只列出部分关键代码。此部分位于 SYSTEM 文件夹中的 usart.c 和 usart.h 文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//注意,读取USARTx->SR能避免莫名其妙的错误
#define USART_REC_LEN 55*1024 //定义最大接收字节数 55K
u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000))); //接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.
//接收状态
//bit[15] 接收完成标志
//bit[14] 接收到0x0d
//bit[13:0] 接收到的有效字节数目
u16 USART_RX_STA=0; // 接收状态标记
u16 USART_RX_CNT=0; // 接收的字节数

// uart1初始化函数
void uart_init(u32 bound)
{
// ... ...
}
// 串口1中断服务程序
void USART1_IRQHandler(void)
{
u8 res;
if(USART1->SR & (1<<5))//接收到数据
{
res = USART1->DR;
if(USART_RX_CNT < USART_REC_LEN)
{
USART_RX_BUF[USART_RX_CNT] = res;
USART_RX_CNT++;
}
}
}

  我们指定 USART_RX_BUF 的地址是从 0X20001000 开始,该地址也就是 SRAM中APP程序的起始地址。 然后串口1的中断服务程序(USART1_IRQHandler() 函数)里面,将串口发送过来的数据,全部接收到 USART_RX_BUF,并通过 USART_RX_CNT 计数,这样接收数据的同时,还可以获取整个文件的大小。

3. APP程序写入

  由于我们通过串口直接将接收到的数据写入了SRAM中,所以这里并不需要将 .bin 文件再写入到SRAM,若是这是一个FALSH中的APP,我们就还需要将该 .bin 文件写入到FLASH相应的地址中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
u16 iapbuf[1024];
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{
u16 t;
u16 i=0;
u16 temp;
u32 fwaddr=appxaddr;//当前写入的地址
u8 *dfu=appbuf;
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;
STMFLASH_Write(fwaddr,iapbuf,1024);
fwaddr+=2048;//偏移2048 16=2*8.所以要乘以2.
}
}
if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.
}

【函数说明】该函数用于将FLASH APP写入到FLASH中指定的区域中。

【函数参数】

  • appxaddr : u32 类型,其实就是 unsigned int 类型,表示APP存放的地址。
  • appbuf :u8 类型,表示APP的应用程序文件,就是前边我们接收到的 .bin 文件。
  • appsize :u32 类型,表示应用程序大小(以字节为单位).

【返回值】none

【使用格式】none

【注意】STMFLASH_Write()函数需要自己实现,前边的《STM32内部FLASH读写》这篇笔记中有说明。

4. APP执行实例

1
2
3
4
5
6
7
8
9
10
//执行FLASH APP
if(((*(vu32*)(0x08010000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(0x08010000);//执行FLASH APP代码
}
// 执行SRAM APP
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x20000000)//判断是否为0X20XXXXXX.
{
iap_load_app(0X20001000);//SRAM地址
}