背景

使用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表示加载域。作用:告诉链接器:下面所有代码 / 常量数据,打包后烧录到 Flash 0x08000000 开始的区域。所有被大括号包裹的内容,最终会打包进 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);
#endif

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]
				
				mov sp,r0
				BX r1
					
                ENDP			


				
                 END
					 
					 

main.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
                
                 END
					 
					 

main.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,存入向量表时编译器自动 |10x08000089。 后面 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			


				
                 END
					 
					 

main.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
                
                 END
					 
					 

main.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
                
                 END
					 
					 

main.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
                
                 END
					 
					 

main.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			


				
                 END
					 
					 

main.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里的中断函数

 

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐