前言
(1)如果有嵌入式企业需要招聘湖南区域日常实习生,任何区域的暑假Linux驱动/单片机/RTOS的实习岗位,可C站直接私聊,或者邮件:zhangyixu02@gmail.com,此消息至2025年1月1日前均有效
(2)上一章节我们简单的科普了学习Bootloader/IAP所需的的前置知识,那么本章节主要是一个简单的Bootloader引导程序,将Bootloader引导进入APP程序中。
(3)上一章节博客地址:Bootloader/IAP零基础入门(0) —— Bootloader/IAP的前置知识
(4)系列教程仓库链接:GitHub仓库
(5)注意:本章节只是做一个Bootloader引导进入APP的程序教程。因此Bootloader会做的很简陋!不可以用于实际项目!
前期准备
制作APP程序
(1)因为我开发板上有两个
LED
,因此GPIO
配置如下。设置SYS
和RCC
的部分我不进行赘述。
(2)生成工程之后,MDK中写入程序如下:
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_Delay(1000);
}
/* USER CODE END 3 */
(3)程序烧录进去之后,我们能够看到开发板上两个
LED
每隔1s进行闪烁。
制作Bootloader程序
(1)首先我们打开串口1的初始化。设置
SYS
和RCC
的部分我不进行赘述。
(2)工程创建出来之后,打开微库。
(3)加入头文件,按
Ctrl+F
搜索Private includes
,补充如下代码。
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
(4)串口重映射,按
Ctrl+F
搜索Private user code
,补充如下代码。
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
/* USER CODE END 0 */
(5)添加应用代码,按
Ctrl+F
搜索USER CODE BEGIN 2
,补充如下代码。
/* USER CODE BEGIN 2 */
printf("-------------------------------------\r\n");
printf("---www.zyxbeyourself.blog.csdn.net---\r\n");
printf("-------------------------------------\r\n");
printf("--------------Bootloader-------------\r\n");
printf("-------------------------------------\r\n");
printf("--------zhangyixu02@gmail.com--------\r\n");
printf("-------------------------------------\r\n");
/* USER CODE END 2 */
(6)上机测试,复位之后能够看到串口打印如下数据
工程合成
Keil MDK的配置
(1)我们打算给
Bootloader
准备10KB
的内存大小。因此我们将IROM1
的Size
设置为0x2000
。
(2)因为
Bootloader
为10KB
,所以APP
程序的IROM1
的Size
设置为0x7E000
,Start
设置为0x8002000
。
Bootloader需要添加的引导程序
(1)在
Bootloader
中加入引导程序。在前面的
uint32_t AppStartFlash = 0x8002000UL; //app的flash起始地址
__set_MSP(*(volatile uint32_t *)AppStartFlash); //设置APP栈顶地址(用户代码区的第一个字用于存放栈顶地址)
typedef void (*AppFunction)(void); //定义一个函数指针类型
AppFunction AppEnter = (AppFunction)*(volatile uint32_t *)(AppStartFlash + 4); //app的flash起始地址+4才是复位中断程序
AppEnter();
Bootloader代码讲解
设置APP栈顶指针的作用
栈的作用
(1)首先我们需要知道栈顶指针的作用,这样才能更好的知道明白为啥需要设置栈顶指针。
(2)我们先看下面的这段代码。我们会发现,Test_A()
函数会调用Test_B()
函数,而Test_B()
函数里面存在一个10字节的数组,这个数组是局部变量。
<1>现在我们根据汇编代码进行分析。首先,Test_A()
函数会调用BL
指令跳转到Test_B()
函数中。
<2>进入Test_B()
函数,首先是使用PUSH
命令将r1
,r2
,r3
,lr
这四个寄存器的值压入栈中。这个过程就是保护上一次函数调用的环境。
<3>之后是MOVS
指令,将r0
寄存器的值清零。
<4>然后是3个STR
指令,将r0的值存放进入栈地址偏移0,4,8的地址中。
<5>然后使用POP
命令,栈中的r1
,r2
,r3
,lr
这四个寄存器进行恢复。(因为第四步,所以会导致r1
,r2
,r3
这三个寄存器被清零。因为STM32F103
栈是向下生长的,当需要为函数调用或其他堆栈操作分配空间时,堆栈指针(SP
)的值会减小)
(3)从这个例子中我们可以知道,当发生函数调用的时候,栈有两个作用:
- 存储局部变量的值。
- 函数调用的返回地址和一些必要的相关信息存储。
/*--- C代码 ---*/
void Test_B()
{
volatile uint8_t arr[10] = {0};
//...省略
}
void Test_A()
{
Test_B();
}
/*--- 汇编代码 ---*/
i.Test_A
Test_A
BL Test_B
i.Test_B
Test_B
PUSH {r1-r3,lr}
MOVS r0,#0
STR r0,[sp,#0]
STR r0,[sp,#4]
STR r0,[sp,#8]
POP {r1-r3,pc}
(4)除了上面我说的两个作用。在程序发生中断的时候,我们需要保护当前程序的环境。对于
M3M4
内核的芯片,会自动将寄存器的值压入栈中,保护当前的环境。
(5)在OS
中,发生任务调度,也需要栈来保存任务的执行状态。(其实任务调度本质上就是利用的中断,不过加了一些复杂的处理机制)
(6)因此,我们可以得出结论栈有四大作用:
- 存储局部变量的值。
- 函数调用的返回地址和一些必要的相关信息存储。
- 用于中断的现场保护。
- OS中保存任务的执行状态。
Bootloader的栈顶和APP的栈顶区别是什么
(1)上面我们知道了栈的作用之后,我们就需要考虑一个问题。在上一章节博客中,我们不是说
M3M4
内核启动的时候,会自动从0地址(STM32F103
是0x08000000
,这个涉及内存映射的知识,自行了解)取出4字节数据,作为栈顶指针吗?既然Bootloader
已经设置好了栈顶指针,而且Bootloader
和APP
的RAM
都是从0x20000000
开启,为什么我们这里还需要重新设置一次栈顶指针呢?
(2)在前文我们说了,芯片启动,完成芯片的系统初始化之后,其实是跳转到__main
函数,而不是main
函数。在__main
函数中会进行数据段的搬运和BSS
段的清零。Bootloader
和APP
的RAM
虽然都是从0x20000000
开启,但是他们的数据段和BSS
段大小都不一样,值也不同。因此需要重新设置一次栈顶指针的值。
(3)扩展知识:这个栈顶指针我们是怎么知道的呢?
<1>栈顶指针的值其实是由编译器给出的,编译器在编译过程中,会产生一个__initial_sp
变量,这个变量存放着栈顶指针的值。我们在.s
文件中能够看到下面这行代码,也就是将__initial_sp
这个变量的值存入首地址。
__Vectors DCD __initial_sp ; Top of Stack
<2>知道
__initial_sp
值的方法有两种,一种是看反汇编的Flash起始地址的4字节数据。另外一种是看map文件,如下:
__set_MSP()函数的作用
(1)有了上面的基础,我们就知道了设置栈的作用。现在还需要科普一个知识。对于
M3M4
内核而言,为了对RTOS
做支持,因此存在两个堆栈指针。对于裸机程序,我们只需要知道我们只用到了MSP
这个堆栈指针。
(2)然后上一章节在讲解STM32F103
的启动流程的时候,我也说了,程序的首地址存放的是栈顶指针地址。AppStartFlash
作为APP
的Flash
首地址,因此该地址存放着APP
的栈顶地址值。于是我们通过一下方法取出APP
的栈顶地址值,然后将这个值赋予给MSP
寄存器。
uint32_t AppStartFlash = 0x8002000UL; //app的flash起始地址
__set_MSP(*(volatile uint32_t *)AppStartFlash); //设置APP栈顶地址(用户代码区的第一个字用于存放栈顶地址)
从Bootloader引导进入APP程序分析
(1)这一部分需要一定的函数指针的知识。首先,我们先定义一个无返回值,无需传入参数的函数指针
AppFunction()
。
(2)然后上一章节,讲解STM32F103
的启动流程的时候说了,首地址的4字节存放栈顶指针地址,紧接着的4字节就是复位函数的地址。如果我们需要跳转进入APP
程序,就需要跳转进入APP的复位中断函数地址处。于是我们这里取出AppStartFlash
偏移4字节地址的值,然后跳转过去即可。
typedef void (*AppFunction)(void); //定义一个函数指针类型
AppFunction AppEnter = (AppFunction)*(volatile uint32_t *)(AppStartFlash + 4); //app的flash起始地址+4才是复位中断程序
AppEnter();
参考
(1)B站:STM32的IAP技术,基于CAN总线的STM32F103 BootLoader设计
(2)CM3权威指南