当前位置: 首页 > news >正文

单片机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); #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里的中断函数

http://www.cnnetsun.cn/news/4141391.html

相关文章:

  • RedisDesktopManager Windows 版:免费 Redis 可视化管理工具完整指南
  • 从零构建开源AI助手:本地化部署、工具扩展与RAG集成实战
  • BMS开发实战指南:从STM32到Simulink,构建新能源汽车电池管理系统核心技能
  • MyBatis-Plus 动态分表实战:基于 DynamicTableNameInnerInterceptor 的多端数据隔离方案
  • 三步保存视频号视频:免费开源资源嗅探下载工具的完整上手指南
  • LenovoLegionToolkit 电源模式不同步?30秒自检 + 完整修复流程
  • C++ Vector核心解析与面试高频考点实战
  • 威海教师评职称需要满足哪些条件?2026年最新政策解读
  • 2026四大AI论文写作软件深度横评|从降重到润色,各有所长别盲选
  • 网络资源如何3步抓取?res-downloader 完整实战指南
  • Spring Boot电脑硬件资产管理系统:从零部署到全流程实战
  • 手机怎么把 Kimi 对话导出,AI 导出鸭适配移动端一键完整导出对话记录,对比多种转换方式选出高效操作办法
  • sklearn逻辑回归实战:TF-IDF文本分类全流程解析与调优指南
  • AI Agent 面试题 387:Agent的工作记忆在多步推理中扮演什么角色?
  • 后端开发者指南:用LangGraph构建可控AI工作流与多智能体系统
  • 考研复试准备全攻略:专业复习与面试技巧
  • SPT-AKI 存档编辑器:13 项功能与运行要求
  • KMS_VL_ALL_AIO完整教程:3分钟免费激活Windows和Office
  • 网盘直链下载助手教程:免费脚本 3 分钟装好,8 大网盘一键取直链
  • YDWE:魔兽争霸3地图编辑器二次开发,给War3地图作者的手艺活装上Lua
  • 毕业论文格式难题终结:MathType安装、目录样式与图片显示的底层逻辑与系统解决方案
  • PCL2启动器全攻略:从零搭建Minecraft模组光影环境
  • Video2X 使用手册:把模糊老视频放大到 4K、把 30 帧补成 60 帧,一次讲透
  • 告别终端多开:从Tmux到IDE集成,构建高效命令行工作流
  • IPv6 Toolkit 完整指南:面向 IPv6 网络安全评估与故障排查的命令行工具包
  • 抖音下载器教程:3步搞定无水印下载,批量保存创作者全部作品
  • 麻将游戏开发框架:majiang-cocos-creator 如何用 Cocos Creator 搭出完整牌局
  • 一文读懂用户脚本如何绕过视频网站年龄限制:前端绕过机制深度解析
  • 跳出AI模型期望的享乐跑步机:从追逐新模型到榨取现有价值
  • NAppGUI资源编译器nrc详解:图片、文本、多语言消息一键打包进可执行文件