STM8 IAP升级程序设计详解 - IAR环境
1.STM8内存空间分配
首先我们在STM8L15x的官方手册中查看一下CPU的内存空间分配:
除了系统预留的部分我们实际用到的内存空间并不多,下面简单说明主要部分:
-- RAM 0x00 0000 - 0x00 0FFF(最大 4KB, 包含堆栈区,栈区最大513bytes):
-- Data EEPROM 0x00 1000 - 0x00 17FF(最大2KB):
STM8定义的专门用于保存掉电数据一块区域,操作方法与内部Flash大致相同。只是可以不用擦除就能直接写。
-- Option bytes 0x00 4800 - 0x00 48FF
-- GPIO and Perpheral registers 0x00 5000 - 0x00 57FF
GPIO 和 外设寄存器的的地址
-- Boot ROM 0x006000 - 0x67FF
-- CPU/SWIM/Debug/ITC Register 0x00 7F00 - 0x00 7FFF
-- Flash program memory 0x00 8000 - 0x01 7FFF (最大64KB)
小结:
在我们制作升级程序的时候需要将生成的 bootloader 和 app 的 bin 文件烧写到 Flash program memory 这块地址中去,其中 0x8000 - 0x807F 这块区域是中断向量表的地址,当发生中断时会强制 pc 指针指向该地址。对于我们烧写的 bin 文件,可以通过分析 .map 文件来了解其中的具体的内容。 对于每个完整的 bin 文件都应该由以下的段组成:
bin文件:中断向量表 + rodata段(const常量) + 系统、堆栈等的初始化代码 + 用户代码 + 初始值不为零的全局变量
当 Flash中只有一个 bin 文件时,Flash 中的内容和 bin 文件一致,但是 Flash 中可以存放多个 bin 文件。
顺便我们也简单说明一下程序运行时 Ram 中包含的内容:
RAM:初始值不为零的全局/静态变量(由flash重定位) + 初始值零的全局/静态变量 + 堆区 + 栈区 (降序栈,栈顶地址从sram的最高地址开始)
注意:
1. map 文件中,全局变量 / 静态变量 /常量 的地址是指程序运行时的地址,每个变量的地址在链接时规定好,所以虽然 Flash 中也有全局变量,静态变量等地址但是 map 文件中显示的并不是 Flash 中的地址。所以在汇编文件中看到的读写某个变量的值时,实际上是读写某个地址内的内容。
2. 程序含有(反汇编文件):代码段 + rodata段(const常量) + .data(数据段) + .bss(初始值零或者为初始化的全局/静态变量) + comment段(注释)
最后两项不包含在bin文件中
补充说明一下C语言程序的内存分区:
栈区:编译器自动分配释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。
堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
全局区:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
文字常量区:常量字符串就是放在这里的。程序结束后由系统释放。
程序代码区:存放函数体的二进制代码
2.IAP升级程序设计流程
IAP原理非常简单,首先在 bootloader 程序中接收(串口、IIC、SPI等)第二个程序的代码,并写入Flash中,然后跳转到第二个程序首地址,开始运行第二个程序,也就是说我们需要写两个程序:
1. BootLoader 程序
2. 用户APP
当 Flash 中存在两个 bin 文件时,程序是怎样运行的呢?我们又是如何在一个程序运行结束之后跳转到另一个程序中去呢?想要知道这些原理,首先我们需要先了解一下单片机的 中断机制 和 启动流程 :
2.1 STM8中断机制
在官方手册的第6章给出了 STM815xL 的中断向量表的定义:
在参考手册中一般都会列出单片机系统所有的中断向量及其对应的地址, 每个中断向量都存放着4个字节的数据(8位的跳转指令 + 24位的跳转地址),在中断发生时,会强制PC指针指向该中断向量的地址,然后取出该地址中的指令执行。
例如: 此时来了 USART2 的中断,通过上表我们知道 USART2 的中断向量存放在 0x00 8054 地址,此硬件会把PC指针强制 = 0x00 8054 也就是从这个地址里取指令执行,而这个地址中的内容是 0x82 + OFFSET_ADDR(16位), 0x82 是内部指令,意思是跳转到后面的地址执行,OFFSET_ADDR,就是 USART2 的中断服务函数的入口地址, 这样最终就跳转到了 USART2 的中断服务函数中去执行。
当然中断发生时还会有一些入栈操作,保存程序当前运行的地址,一些变量的值到栈中,当中断服务程序执行完成后会从栈中恢复到执行中断前程序的运行状态,从而保证主程序的正常运行。
注意:在 STM8 中,0x82 后面会跟着24位的地址(PCE + PCH + PCL),CPU 最大寻址 2^24 = 16M空间
2.2 单片机启动流程
2.1.1 内核初始化
在单片机上电后首先会进行一系列内核的初始化,关于这部分工作我们只需要了解即可,在内核初始化的过程中主要做了以下几件事情:
1.内核复位和 NVIC 寄存器部分清零
2.内核设置堆栈: 内核从向量表0地址读出堆栈地址,并设置主堆栈指针(SP_main)
3.设置PC和LR寄存器
a. LR设置未初始复位值0xffff ffff
b. STM32F4 的内部硬件机制亦会自动将 PC 指针定位到“中断向量表”处,把复位中断 Reset_Handler 的地址赋值给PC指针
2.1.2 复位中断函数 Reset_Handler
可以看到在内核复位的最后一步,将PC指针指向了复位中断向量,而复位中断服务函数中的内容才是我们真正需要关心的内容。
我们可以在 STM32F4 的 .s 汇编启动文件中看到以下内容:
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit //加载 SystemInit的地址到寄存器 R0 中
BLX R0 //跳转 R0 中的地址执行 (执行 SystemInit 函数)
LDR R0, =__main //加载 SystemInit的地址到寄存器 R0 中
BX R0 //跳转 R0 中的地址执行 (执行 __main 函数)
ENDP
上面的代码就是 Reset_Handler 的中断服务函数,可以看到在服务中断函数中先使用 IMPORT声明了两个函数 __main,SystemInit。
然后再跳转执行 SystemInit 和 __main函数。下面我们再来了解一下这两个函数具体干了些什么事情:
a. SystemInit 函数:
在 system_stm32f4xx.c 文件中我们可以看到该函数的定义,该函数主要干了以下两件事情:
1.初始化时钟(SYSCLK, HCLK, PCLK2 and PCLK1 prescalers)
2.配置中断向量表(中断向量表的定位是在 Flash 还是SRAM,是否需要偏移)
注意:可以通过 system_stm32f4xx 文件中的宏定义修改系统时钟频率(通过设置锁相环的相关系数),中断向量表的地址(位于SRAM还是Flsah,是否偏移,偏移地址多少等参数)
b. __main()函数(在IAR中是 __iar_program_start ):
该函数被封装进了编译器的库中,所以不同的IDE该函数的名称可以有所区别,但所实现的功能大致类似:
1.完成全局变量/静态变量/常量的初始化和重定位工作.
跳转进入__scatterload_rt2函数:通过设置四个寄存器来配置待copy内容(静态变量、全局变量、常量)的的加载域和运行域,设置待copy内容的大小,为后续__scatterload_cpy()函数服务。
跳转进入__scatterload_cpy函数,完成静态变量、全局变量、常量的从flash到SRAM的重定位。
跳转进入__scatterload_zeroinit函数,完成未初始化的全局变量的初始化,
2.初始化堆栈(这里指程序栈)和库函数
跳转进入 __user_steup_stackheap 函数,实现用户的堆栈的配置
__user_steup_stackheap 完成了以下调用:
a. __user_libspac__user_libspace 为C库保持了静态数据。这是一个96字节,0初始化的数据块,该块由C库创建。在C库初始化期间可以用来当做临时栈。
b. __user_initial_stackheap 用户的初始化堆栈函数
_fp_init和__rt_fp_status_addr(C库函数)两个函数调用实现浮点运算的支持
3.程序的跳转,进入main()函数。
跳转进入用户的main函数
注意:
1. 未初始化和初始值为零 全局变量/静态变量 一般在 RAM 中, 初始化值不为零的 全局变量/静态变量 一般在 FLASH 中。
2. 因为 Flash 不能随机写(只能写0,不能写1),所以一般会在程序运行之前将初始值重定位到 RAM 中
3. 全局变量和常量的地址在编译时都已经被分配好了(所以能够在 .map 文件中看到), 而局部变量则是程序运行时在栈中创建的,栈空间大小可以在 IDE 中设置。
4. 单片机启动时,不需要用将代码从 ROM 搬移到 RAM ,而 ARM 则需要。我们先看看单片机程序执行的过程,单片机执行分三个步骤,取执行->分析指令->执行指令。取指令的任务是:根据 PC 的值从程序存储器读出指令,送到指令寄存器。然后分析执行执行。这样单片机就从内部程序存储器去代码指令,从 RAM 存取相关数据。要知道 RAM 取数的速度是远高于 ROM 的,但是单片机因为本身运行频率不高,所以从 ROM 取指令慢并不影响。而 ARM 不同,CPU 运行的频率高,远大于从 ROM 读写的速度,所以一般有操作系统,都需要将代码部分拷贝到 RAM 中再执行。
2.3 中断重定向
现在我们知道了,单片机在复位时会强制PC指针指向复位中断服务函数,在复位中断函数中完成一系列的初始化,为用户准备好 C 语言的运行环境,最后再跳转到我用户的main 函数中执行我们自己编写的程序。当我们在做 IAP 升级的时候,我们想要的程序执行顺序应该是先执行 bootloader, 然后再跳转执行 App。通常情况是先通过 bootloader 的复位中断跳转到 bootloader 中的 main 函数中执行,执行完成后再通过 app 的复位中断函数中跳转到 app 的 main 函数中。当 bootloader 和 app 发生中断时都能单独跳转到各自的中断向量表中执行自己的中断服务函数。
这本身没有问题,问题是对于 STM8 而言当发生中断时(不管哪个程序),PC 指针总会指向第一个中断向量表相应的中断向量的地址 (因为 STM8 中断向量表固定在这里,并且不可以映射到别的地址,这是硬件决定的),当第二个程序发生了中断时,此时断服务程序肯定是在第二个程序里写的,中断服务函数的入口地址也在第二个程序范围内,但是发生中断时,PC指针不会指向第二个 bin 文件的中断向量表,而是指向 0x8000-0x8080 ,也就无法执行第二个程序中的中断服务函数。
有什么办法可以让我的程序发生中断时,PC指针指向我的中断向量表呢?于是我们想到了 0x82 这个内部操作码,0x82 + OFFSET 不是跳到 OFFSET 这个地址执行么?假设此时来了 USART2 的中断,bootloader 程序 USART2 中断向量地址是 0x00 8054,APP USART2 中断向量地址是 0x9054(假设APP是从0x9000开始存放),此时PC指针一定等于0x00 8054,这时候就要让它跳到 0x00 9054 就需要在 0x00 8054 这个地址放入:0x8200 9054,这样PC指针又跳回了APP的中断向量表,然后再从中断向量表中取出 USART2的中断服务函数并执行。这就是中断重定向,重定向之后就能再APP程序中随意使用中断了。
要实现中断重定向,需要重新定义 bootloader 中的中断向量表,原来 bootloader 中的中断向量表中(0x8000 - 0x8080) 本来存放的是 bootloader 中各个中断函数的入口地址,现在我们需要修改为 app 中断向量表的地址,但需要注意的是 bootloader 的复位中断向量不需要修改。如果这里修改为了 app 中的复位中断向量地址,那就会直接跳转到 app 的复位中断函数中,然后再执行 app 的 mian 函数,就无法执行 bootloader 中的程序了。
注意:对于STM32来说可以设置中断向量表的偏移地址,而STM8却不能设置偏移,只能通过重定向来使得我们的APP程序能够使用中断,但是重定向后的 bootloader 中就不够使用中断了。
-- 在 IAR 中重定向中断只需要在 bootloader 程序中定义以下数数组即可 (bootloader 为4K APP地址为 0x00 9000 时):
__root const long reintvec[]@".intvec"=
{ 0x82008080,0x82009004,0x82009008,0x8200900c,
0x82009010,0x82009014,0x82009018,0x8200901c,
0x82009020,0x82009024,0x82009028,0x8200902c,
0x82009030,0x82009034,0x82009038,0x8200903c,
0x82009040,0x82009044,0x82009048,0x8200904c,
0x82009050,0x82009054,0x82009058,0x8200905c,
0x82009060,0x82009064,0x82009068,0x8200906c,
0x82009070,0x82009074,0x82009078,0x8200907c,
};
2.4 IAR的 .ICF 链接文件
由于我们设计的APP程序需要设置起始地址为 0x9000 (假设预留的BootLoader空间为4k),这就需要我们修改 .icf链接文件中的一些内容。
下面简单介绍一下 IAR 的 .icf 文件,
通常每个芯片开发商都会针对每款芯片来编写一个 .icf 链接文件。通常这个.icf文件足以满足你的工程需要。
但有时也会需要改动,比如当你的项目要 重设程序的地址、添加外部RAM、定义变量的绝对地址等 就要修改一下icf。
首先我们先打开开发商的官方 .icf 文件查看一下:
下面是截取的主要内容:
先选择拷贝过来的 .icf文件为新的连接文件:
如果是 BootLoader 程序 .icf 文件需要做下面的修改(BootLoader 小于4KB时):
如果 BootLoader 的程序不超过4KB(0x8000 - 0x8FFF),只需更改 0x80 为 0x100 { ro section .intvec };因为重新映射了中断向量的定义,不然编译会报错。
如果是 APP 程序 .icf 文件则需要做以下修改 (STM8L151C8T6 是64KB的Flash,最大到0X17FFF) :
更多关于ICF文件的分析请参考文章 《IAR中ICF链接文件详细分析》
2.5 编写BootLoader 程序
在链接文件中规定好了, bootloader 和 app 的地址之后就可以编写我们的 bootload 程序了。前面已经介绍过了,bootloader 的实现可以很简单,只需要判断是否需要升级,如果不需要直接跳转到 app 的地址执行。如果需要升级,则获取 app 升级文件,然后写入到 flash 中,最后再跳转到 app 的地址执行。
这里我们需要解决4个问题:
1. 升级判断条件
可以通过按键,主动获取版本号比较等方式判断是否需要升级
2. 下载升级文件
传输bin文件,可以通过很多方式(串口,SPI,iic 等),这里我自己用 QT 写了一个串口 bin 文件的传输工具,将bin文件分包发送给单片机,单片机接收到数据后直接写入Flash即可。这里需要注意的是,普通的串口调试助手虽然能够发送bin文件,但是无法分包,对于 stm8 来说一般一次只能接收2-4k的内容,接收完成后需要写到flash中, 然后再去接收下一包,所以最好能自己写一个下载工具,规定自己的传输协议。实在不行可以考虑使用有传输延时的串口工具或者超级终端,每发送完一行数据之后延时一定时间再发送下一包的数据,这样才能保证接收到的数据能够正确写入flash中。
另外需要注意,在 bootloader 中不能够使用中断,所以串口接收只能通过 while 循环判断标志位来读取串口接收的数据。
3. 写入flash
每接收到一包数据之后写入flash,然后再偏移写入地址,接收下一包数据继续写入,直到最后一包数据写入完成后,再跳转执行。
这里需要注意的是,如果需要使用 stm8 的块擦除 FLASH_EraseBlock 和 块写入 FLASH_ProgramBlock 等函数,需要注意库函数中的这段注释的内容:
- For IAR Compiler:
1- Use the __ramfunc keyword in the function declaration to specify that it
can be executed from RAM.
This is done within the stm8l15x_flash.c file, and it's conditioned by
RAM_EXECUTION definition.
2- Uncomment the "#define RAM_EXECUTION (1)" line in the stm8l15x.h file, or
define it in IAR compiler preprocessor to enable the access for the
__ramfunc functions.
这里提示了需要将 FLASH_EraseBlock, FLASH_ProgramBlock 等函数用 __ramfunc 关键字声明,将这些函数定义到 ram 中,在 ram 中执行。
这里还说明了要使用 __ramfunc 关键字需要 在 stm8l15x.h 文件中取消 #define RAM_EXECUTION (1) 这行的注释
4. 跳转到app地址执行
可以直接使用官方例程中提供的汇编跳转代码:
void _ASM_JumpTo_App(void)
{
asm("LDW X, SP ");
asm("LD A, $FF");
asm("LD XL, A ");
asm("LDW SP, X ");
asm("JPF $9000");
}
总结
在前面补充讲了很多知识点,其实如果只是想实现 IAP升级功能只需要以下几个步骤即可:
1. 修改 .icf 链接文件,并且设置新的 .icf 链接文件为当前工程的链接文件;
2. 在 bootloader 中重定向中断向量表;
3. 自己编写或者下载一个 bin 文件的分包发送工具;
4. 接收下载的内容写入到 Flash 中 ;
5. 跳转执行app程序.