单片机bootloader总结
背景
使用stm32f103c8t6,RAM地址0x20000000,大小0x5000,flash地址为0x08000000,大小为0x10000。
新建两个工程,分别是bootloader和app,两个工程目录结构一致,大概如下。
keil设置
首先将两个工程都改为扇区擦除,然后再更改ROM的起始和大小,将64KB分为两个32KB。
bootloader
app
为啥这样改了就能指定将程序存储在指定flash位置呢,因为分散加载文件的指导(.sct文件)。keil默认勾选了该选项,所以在对话框更改ROM后,rebuild后就会自动生成sct文件,程序将会按照这个sct文件设置布局,如果取消勾选,就能使用自定义的sct文件。
sct文件解释
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00010000 { ; 加载域:烧录Flash起始地址 ER_IROM1 0x08000000 0x00010000 { ; 运行域(Flash) *.o (RESET, +First) ; RESET段(向量表)放最头部 *(InRoot$$Sections) ; Keil ARM 编译器内置特殊符号。 .ANY (+RO) ; 代码、常量 .ANY (+XO) ; execute-only只执行段(现代ARM编译器特性,纯代码无数据)兼容新版编译器 } RW_IRAM1 0x20000000 0x00005000 { ; 第二个运行域(RAM) .ANY (+RW +ZI) ; 全局变量、bss零初始化区 } }第一行LR_IROM1表示加载域。作用:告诉链接器:下面所有代码 / 常量数据,打包后烧录到 Flash0x08000000开始的区域。所有被大括号包裹的内容,最终会打包进 bin/hex 文件。
ER_IROM1表示运行域,这个运行域跟LR_IROM1加载域相同,因为大部分单片机的片内Flash都是Nor Flash,是支持随机寻址的(擦除需要按区擦除),所以程序就在Flash运行。.ANY(+RO)表示代码段和const常量。
RESET段(向量表)放最头部啥意思呢,这个RESET段就是start.s里定义的的RESET段,存放向量表,也可以叫向量表段,类型为数据,只读。
RW_IRAM1也是运行域,被括号包含的内容是运行在RAM里的,包括RW和ZI段,RW段就是普通的全局变量,ZI段也叫bss段,是未初始化(或者初始化为0)的全局变量。这些变量一般都会随着程序运行而修改,所以只能在RAM运行
方法1(汇编跳转)
uart
uaer.c
typedef unsigned int uint32_t; typedef struct { volatile uint32_t SR; volatile uint32_t DR; volatile uint32_t BRR; volatile uint32_t CR1; volatile uint32_t CR2; volatile uint32_t CR3; volatile uint32_t GTPR; } USART_InitTypeDef; void uart_init(void){ volatile unsigned int *p; //RCC的APB2EN寄存器 p=(volatile unsigned int *)(0x40021000+0x18); *p |= 0x4004; //GPIOA的CR寄存器 p=(volatile unsigned int *)(0x40010800+0x04); //使能GPIOA与USART1 //PA9 *p &= ~((3<<4) | (3<<6));//清除对应位 *p |= (1<<4) | (2<<6);//复用推挽,输出10M模式 //PA10 *p &= ~((3<<8) | (3<<10)); *p |= (0<<8) | (1<<10); USART_InitTypeDef *j=(USART_InitTypeDef *)0x40013800; j->BRR |= (4<<4) | 5;//115200 j->CR1 |=(1<<13) | (0<<12) | (0<<10) | (1<<2) | (1<<3);//串口使能,1起始8数据n停止,无校验,接收使能,发送使能 j->CR2 &= ~(3<<12);//1个停止位 } int Send_Data(char data){ USART_InitTypeDef *j=(USART_InitTypeDef *)0x40013800; while((j->SR &(1<<7))==0); j->DR=data; return 0; } int Receive_Data(void){ USART_InitTypeDef *j=(USART_InitTypeDef *)0x40013800; while((j->SR &(1<<5))==0); return j->DR; } void putstr(char *str) { USART_InitTypeDef *j=(USART_InitTypeDef *)0x40013800; while(*str) { //Send_Data(*str); while((j->SR &(1<<7))==0); j->DR=*str; str++; } }uart.h
#ifndef _UART_H #define _UART_H void uart_init(void); int Send_Data(char data); int Receive_Data(void); void putstr(char *str); #endifbootloader
start.s
PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0 DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT main LDR SP, =(0x20000000+0x5000) BL main ENDP start_app PROC EXPORT start_app [WEAK] mov sp,r0 BX r1 ENDP ENDmain.c
#include "uart.h" void delay(int d) { while(d--); } extern void start_app(unsigned int sp,unsigned int pc); int main() { unsigned int *p=(unsigned int *)0x8008000; unsigned int sp=*p; unsigned int pc=*(p+1);//得到向量表第二项的地址 void (*app)(void);//函数指针 uart_init(); putstr("BootLoader\r\n"); start_app(sp,pc); while(1) { } return 0; }app
start.s
PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD (0x20000000+0x5000) DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT app_main BL app_main ENDP ENDmain.c
#include "uart.h"
void delay(int d)
{
while(d--);
}
int app_main()
{
while(1)
{
putstr("App...\r\n");
delay(1000000);
}
return 0;
}
代码理解
bootloader的start.s
__Vectors就是向量表的定义
第一项是0,也就是说主栈指针为0,即flash首地址的4字节,值为0
硬件上电自动执行:MSP = *(0x08000000)→ MSP = 0 所以调试可以看到 SP=0。
这是非法栈!但是马上进入 Reset_Handler 第一行:LDR SP,=0x20005000立刻覆盖修复,短暂 SP=0 不会死机。
⚠️ 标准工程不会这么写,标准是向量表第一项直接填栈顶地址,硬件自动加载正确 SP(当前app的start.s就是这样的)
第二项是Reset_Handler函数地址,可以看到flash里显示的Reset_Handler地址是0x08000089,但是PC寄存器显示的是0x08000088,这是因为ARM Thumb 规则: 函数地址最低位必须置 1,表示使用 Thumb 指令集。Reset_Handler 在 Flash 的地址是0x08000088,存入向量表时编译器自动|1→0x08000089。 后面BX跳转依靠这个 bit 切换
但是bootloader的main.c里,可以看到并没有pc = pc - 1的代码,因为指令会读取 R1 最低位,切换内核进入 Thumb。 一切合法,不需要代码手动 pc = pc - 1
查看app的flash部分,可以看到第一项是栈指针,第二项是Reset_Handler的地址+1
app程序如何硬件debug
如何调试app程序呢,首先将app代码修改一下,增加uart初始化,然后点击debug,再将sp改为0x20005000,将pc改为0x08008009-1(Reset_Handler地址),然后就能正常调试app啦
方法2(重定位Vector)
keil设置依旧不变,串口代码也不变
bootloader
start.s
PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0 DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT main LDR SP, =(0x20000000+0x5000) BL main ENDP start_app PROC EXPORT start_app [WEAK] LDR sp,[r0] ;r0 is 0x08008000 LDR r1,[r0,#4] LDR r3,=(0xE000ED08) STR r0,[r3] BX r1 ENDP ENDmain.c
#include "uart.h" void delay(int d) { while(d--); } extern void start_app(unsigned int new_vector); int main() { unsigned int new_vector=0x08008000; uart_init(); putstr("BootLoader\r\n"); start_app(new_vector); while(1) { } return 0; }app
start.s
PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD (0x20000000+0x5000) DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT app_main BL app_main ENDP ENDmain.c
#include "uart.h" void delay(int d) { while(d--); } int app_main() { uart_init(); while(1) { putstr("App...\r\n"); delay(1000000); } return 0; }代码理解
这种方法的bootloader,采用的是重定向向量表,c8t6的VTOR寄存器为0xE000ED08,将这个寄存器设为0x08008000,即系统硬件会从0x08008000处去读取向量表
方法三(拷贝app到ram运行)
注意:bootloader部分和方法二完全相同,甚至都不用重新烧录bootloader
相对跳转和绝对跳转
编译器默认情况下BL指令优先使用相对跳转,如果涉及到函数指针的地方,则会使用绝对跳转,所以,如果程序程序的链接地址为内存,并且函数里没有指针相关的操作,那么,凭借相对跳转,app可能也能正常运行。不过注意,要改向量表才行
加载域和执行域
加载域可以说就是程序存储的地方,执行域就是程序执行的地方,之前的程序,除开RW段和ZI段,其余的段加载域都等于执行域。
keil设置
取消勾选,使用自定义sct文件
sct文件
LR_IROM1 0x08008000 0x00008000 { ; load region size_region ER_IROM1 0x20000000 0x00005000 { ; load address = execution address *.o (RESET, +First) .ANY (+RO) .ANY (+XO) .ANY (+RW +ZI) } }start.s
PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD (0x20000000+0x5000) DCD 0x08008009 ; Reset Handler AREA |.text|, CODE, READONLY READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT app_main IMPORT copy_to_ram IMPORT |Image$$ER_IROM1$$Length| adr r0, Reset_Handler bic r0, r0, #0xFF ;清除低8位,得到0x08008000 ldr r1, =__Vectors ; r1=0x20000000 ldr r2, = |Image$$ER_IROM1$$Length| ; LENGTH BL copy_to_ram BL app_main ENDP ENDmain.c
#include "uart.h" void delay(int d) { while(d--); } void copy_to_ram(unsigned int *src, unsigned int *dst, unsigned int len){ int i = 0; for(i = 0; i < len / 4 + 1; i++){ dst[i] = src[i]; } } int app_main() { uart_init(); while(1) { putstr("App...\r\n"); delay(1000000); } return 0; }代码解释
这里向量表第二项,不能写Reset_Handler,因为改了sct文件,现在链接地址都是0x2的内存地址,这时候还没把app拷贝到内存,所以要手动指定为Reset_Handler在flash里的存储位置,先在flash里执行Reset_Handler,调用copy_to_ram将app拷贝到ram,然后执行main函数就是在ram里了。
查看dis反汇编文件
错误写法
方法四(bootloader拷贝app到ram)
在方法三的基础上,删除app部分自我拷贝的代码,保留sct文件,在bootloader里增加拷贝app的代码
sct文件
LR_IROM1 0x08008000 0x00008000 { ; load region size_region ER_IROM1 0x20000000 0x00005000 { ; load address = execution address *.o (RESET, +First) .ANY (+RO) .ANY (+XO) .ANY (+RW +ZI) } }app
start.s
PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD (0x20000000+0x5000) DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT app_main BL app_main ENDP ENDmain.c
#include "uart.h" void delay(int d) { while(d--); } int app_main() { uart_init(); while(1) { putstr("App...\r\n"); delay(1000000); } return 0; }增加头部
编译app后,将app的bin文件使用mkimage工具加上头部,-a表示load地址,-e表示入口地址
生成后,用STM32 ST-LINK Utility工具烧录到flash的0x08008000
用法
bootloader
start.s
PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0 DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT main LDR SP, =(0x20000000+0x5000) BL main ENDP start_app PROC EXPORT start_app [WEAK] LDR sp,[r0] LDR r1,[r0,#4] LDR r3,=(0xE000ED08) STR r0,[r3] BX r1 ENDP ENDmain.c
里面要解析app代码的头部,将除开头部的app拷贝到内存,
#include "uart.h" typedef unsigned int __be32; typedef unsigned char uint8_t; #define IH_NMLEN 32 /* Image Name Length */ typedef struct image_header { __be32 ih_magic; /* Image Header Magic Number */ __be32 ih_hcrc; /* Image Header CRC Checksum */ __be32 ih_time; /* Image Creation Timestamp */ __be32 ih_size; /* Image Data Size */ __be32 ih_load; /* Data Load Address */ __be32 ih_ep; /* Entry Point Address */ __be32 ih_dcrc; /* Image Data CRC Checksum */ uint8_t ih_os; /* Operating System */ uint8_t ih_arch; /* CPU architecture */ uint8_t ih_type; /* Image Type */ uint8_t ih_comp; /* Compression Type */ uint8_t ih_name[IH_NMLEN]; /* Image Name */ } image_header_t; void delay(int d) { while(d--); } unsigned int be32_to_cpu(unsigned int data) { uint8_t b0 = (data >> 24) & 0xff; uint8_t b1 = (data >> 16) & 0xff; uint8_t b2 = (data >> 8) & 0xff; uint8_t b3 = data & 0xff; return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0; } void copy_to_ram(unsigned int *src, unsigned int *dst, unsigned int len){ int i = 0; for(i = 0; i < len / 4 + 1; i++){ dst[i] = src[i]; } } extern void start_app(unsigned int new_vector); void relocate_and_start_app(unsigned int pos) { /* 读出头部 */ int i; unsigned int size; unsigned int load; image_header_t *head = (image_header_t *)pos; unsigned int new_pos = pos + sizeof(image_header_t); /* 解析头部 */ size = be32_to_cpu(head->ih_size); load = be32_to_cpu(head->ih_load); /* 复制app */ copy_to_ram((unsigned int *)new_pos, (unsigned int *)load, size); /* 跳转执行 */ start_app(load); //start_app(new_pos)也可以 } int main() { unsigned int app_position=0x08008000; uart_init(); putstr("BootLoader\r\n"); relocate_and_start_app(app_position); while(1) { } return 0; }实验结果
注意
如果有些芯片不支持重定位向量表,比如产生中断只能去flash起始地址查找。也有办法。可以实现bootloader的向量表对应的函数,在里面直接调用app向量表对应的函数
比如可以在bootloader里声明一个结构体,结构体成员是很多个函数指针,定义一个结构体指针,结构体指针指向app的向量表地址(比如0x08008000),然后就能逐个实现函数,在函数里面通过结构体指针调用到app里的中断函数
