在微控制器(MCU)中,Bootloader(引导加载程序)是一个非常重要的组成部分,它负责在系统启动时执行一系列初始化操作,并为后续的应用程序提供一个合适的运行环境。
1. Bootloader介绍
Bootloader是微控制器启动时运行的第一个程序,它位于存储器的特定位置,通常是Flash存储器的开始部分。Bootloader的设计目的是在没有操作系统的情况下,独立完成一系列关键任务,为系统的进一步启动做好准备。
Bootloader的作用
硬件初始化:
初始化微控制器的硬件资源,包括但不限于时钟设置、复位外设、配置中断控制器等,以确保系统处于一个稳定且可预测的状态。
加载和执行应用程序:
将应用程序从非易失性存储器(如Flash)加载到RAM中,并跳转到应用程序的入口点(通常是main()函数),使应用程序得以执行。
固件更新:
提供一种机制,使得用户可以通过不同的通信接口(如UART、USB、CAN等)上传新的固件到MCU的Flash存储器中,从而实现远程更新或现场编程。
这一功能对于需要在生命周期内不断升级的设备尤为重要。
故障恢复:
在应用程序崩溃或出现错误时,Bootloader 可以充当一个恢复机制,例如重启系统或将系统恢复到一个已知良好的状态。
安全和认证:
现代Bootloader 可以实现安全启动,确保只有经过认证的固件才能被加载执行。这有助于保护系统免受恶意软件的攻击。
配置和设置:
提供用户界面或命令行工具,允许用户在启动时进行一些基本的系统配置,如选择不同的启动模式或设定启动参数。
Bootloader的分类
基本Bootloader
功能:
初始化硬件。
加载并启动用户应用程序。
特点:
简单且占用资源少。
不支持固件更新。
通常在上电或复位时运行。
应用场景:
固件更新不频繁的应用。
资源受限的低功耗设备。
代码示例:
#include "stm32f4xx.h" // 示例使用STM32F4系列
void SystemClock_Config(void);
void JumpToApplication(uint32_t appAddress);
int main(void) {
// 系统时钟配置
SystemClock_Config();
// 启用全局中断
__enable_irq();
// 检查是否有有效的应用程序
if (*((uint32_t*)0x08008000) == 0x20000000) {
// 跳转到应用程序
JumpToApplication(0x08008000);
} else {
// 如果没有有效的应用程序,则保持在Bootloader中
while (1) {
// 可以添加错误处理代码
}
}
}
void JumpToApplication(uint32_t appAddress) {
// 设置堆栈指针
__set_MSP(*(__IO uint32_t*)appAddress);
// 获取复位向量地址
void (*app_entry)(void) = (void (*)(void))(*(uint32_t*)(appAddress + 4));
// 跳转到应用程序
app_entry();
}
可升级Bootloader
功能:
除了基本的启动功能外,还支持通过某种接口(如UART、USB、I2C、SPI等)进行固件更新。
特点:
支持在线编程(In-System Programming, ISP)或在应用编程(In-Application Programming, IAP)。
通常包含安全机制,防止未经授权的固件更新。
可以通过多种通信协议进行固件更新,如YModem、XModem、CAN、以太网等。
应用场景:
物联网设备,需要定期更新固件。
工业控制设备,需要远程维护和更新。
智能家居设备,需要持续的功能改进和安全更新。
代码示例:
#include "stm32f4xx.h"
#include "usart.h" // 假设有一个USART库用于串口通信
void SystemClock_Config(void);
void JumpToApplication(uint32_t appAddress);
void UpdateFirmware(uint8_t *data, size_t length);
int main(void) {
// 系统时钟配置
SystemClock_Config();
// 启用全局中断
__enable_irq();
// 初始化串口
USART_Init();
// 检查是否接收到固件更新请求
if (USART_ReceiveCommand() == 'U') {
// 接收新的固件数据
uint8_t *newFirmware = (uint8_t *)malloc(length);
USART_ReceiveData(newFirmware, length);
// 更新固件
UpdateFirmware(newFirmware, length);
// 释放内存
free(newFirmware);
}
// 检查是否有有效的应用程序
if (*((uint32_t*)0x08008000) == 0x20000000) {
// 跳转到应用程序
JumpToApplication(0x08008000);
} else {
// 如果没有有效的应用程序,则保持在Bootloader中
while (1) {
// 可以添加错误处理代码
}
}
}
void UpdateFirmware(uint8_t *data, size_t length) {
// 擦除Flash区域
FLASH_EraseSector(FLASH_SECTOR_0, VOLTAGE_RANGE_3);
// 写入新固件
for (size_t i = 0; i < length; i += 2) {
FLASH_ProgramHalfWord(0x08008000 + i, ((uint16_t)data << 8) | data[i+1]);
}
}
双区Bootloader
功能:
使用两个独立的存储区域(通常是Flash),一个用于当前运行的应用程序,另一个用于新版本的固件。
特点:
支持无缝固件更新,即在更新过程中不会中断应用程序的运行。
更新完成后,Bootloader会切换到新的固件区域。
通常包括校验机制(如CRC校验)来确保新固件的完整性。
如果新固件验证失败,可以回滚到旧版本。
应用场景:
医疗设备,需要高可靠性。
关键任务系统,不能容忍停机时间。
需要连续运行的工业控制系统。
代码示例:
#include "stm32f4xx.h"
#include "usart.h" // 假设有一个USART库用于串口通信
#define APP1_ADDR 0x08008000
#define APP2_ADDR 0x08010000
#define ACTIVE_APP_FLAG_ADDR 0x080FF000
void SystemClock_Config(void);
void JumpToApplication(uint32_t appAddress);
void UpdateFirmware(uint8_t *data, size_t length, uint32_t targetAddr);
uint32_t GetActiveAppAddress(void);
void SetActiveAppAddress(uint32_t addr);
int main(void) {
// 系统时钟配置
SystemClock_Config();
// 启用全局中断
__enable_irq();
// 初始化串口
USART_Init();
// 检查是否接收到固件更新请求
if (USART_ReceiveCommand() == 'U') {
// 接收新的固件数据
uint8_t *newFirmware = (uint8_t *)malloc(length);
USART_ReceiveData(newFirmware, length);
// 更新固件
uint32_t activeAddr = GetActiveAppAddress();
uint32_t targetAddr = (activeAddr == APP1_ADDR) ? APP2_ADDR : APP1_ADDR;
UpdateFirmware(newFirmware, length, targetAddr);
// 设置活动的应用程序地址
SetActiveAppAddress(targetAddr);
// 释放内存
free(newFirmware);
}
// 获取活动的应用程序地址
uint32_t appAddr = GetActiveAppAddress();
// 检查是否有有效的应用程序
if (*((uint32_t*)appAddr) == 0x20000000) {
// 跳转到应用程序
JumpToApplication(appAddr);
} else {
// 如果没有有效的应用程序,则保持在Bootloader中
while (1) {
// 可以添加错误处理代码
}
}
}
void UpdateFirmware(uint8_t *data, size_t length, uint32_t targetAddr) {
// 擦除Flash区域
FLASH_EraseSector((targetAddr - 0x08000000) / 0x5000, VOLTAGE_RANGE_3);
// 写入新固件
for (size_t i = 0; i < length; i += 2) {
FLASH_ProgramHalfWord(targetAddr + i, ((uint16_t)data << 8) | data[i+1]);
}
}
uint32_t GetActiveAppAddress(void) {
return *(uint32_t*)ACTIVE_APP_FLAG_ADDR;
}
void SetActiveAppAddress(uint32_t addr) {
*(uint32_t*)ACTIVE_APP_FLAG_ADDR = addr;
}
加密Bootloader
功能:
在基本或可升级Bootloader的基础上增加了加密和安全特性。
特点:
使用加密算法(如AES、RSA)保护固件免受篡改。
支持数字签名验证,确保固件来自可信源。
可能包含安全密钥管理和认证机制。
保护敏感数据和代码。
应用场景:
支付终端,需要高度的安全性。
智能卡,需要防止未授权访问和篡改。
安全通信设备,需要保护数据传输的安全性。
代码示例:
#include "stm32f4xx.h"
#include "usart.h" // 假设有一个USART库用于串口通信
#include "aes.h" // 假设有一个AES库用于加密/解密
#define AES_KEY {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}
void SystemClock_Config(void);
void JumpToApplication(uint32_t appAddress);
void UpdateFirmware(uint8_t *data, size_t length, uint32_t targetAddr);
uint32_t GetActiveAppAddress(void);
void SetActiveAppAddress(uint32_t addr);
bool VerifySignature(uint8_t *data, size_t length, uint8_t *signature, size_t sigLength);
int main(void) {
// 系统时钟配置
SystemClock_Config();
// 启用全局中断
__enable_irq();
// 初始化串口
USART_Init();
// 检查是否接收到固件更新请求
if (USART_ReceiveCommand() == 'U') {
// 接收新的固件数据
uint8_t *newFirmware = (uint8_t *)malloc(length);
USART_ReceiveData(newFirmware, length);
// 接收签名数据
uint8_t *signature = (uint8_t *)malloc(sigLength);
USART_ReceiveData(signature, sigLength);
// 验证签名
if (VerifySignature(newFirmware, length, signature, sigLength)) {
// 更新固件
uint32_t activeAddr = GetActiveAppAddress();
uint32_t targetAddr = (activeAddr == APP1_ADDR) ? APP2_ADDR : APP1_ADDR;
UpdateFirmware(newFirmware, length, targetAddr);
// 设置活动的应用程序地址
SetActiveAppAddress(targetAddr);
}
// 释放内存
free(newFirmware);
free(signature);
}
// 获取活动的应用程序地址
uint32_t appAddr = GetActiveAppAddress();
// 检查是否有有效的应用程序
if (*((uint32_t*)appAddr) == 0x20000000) {
// 跳转到应用程序
JumpToApplication(appAddr);
} else {
// 如果没有有效的应用程序,则保持在Bootloader中
while (1) {
// 可以添加错误处理代码
}
}
}
bool VerifySignature(uint8_t *data, size_t length, uint8_t *signature, size_t sigLength) {
// 使用AES或其他加密算法验证签名
uint8_t calculatedSig[sigLength];
AES_Encrypt(data, length, AES_KEY, calculatedSig);
// 比较计算出的签名和接收到的签名
for (size_t i = 0; i < sigLength; ++i) {
if (calculatedSig != signature) {
return false;
}
}
return true;
}
多阶段Bootloader
功能:
分多个阶段执行,每个阶段完成特定的任务。
特点:
第一阶段通常是简单的Bootloader,负责初始化基本硬件并加载第二阶段的Bootloader。
第二阶段Bootloader可能更复杂,支持更多的功能,如固件更新、安全验证等。
适用于需要复杂初始化过程或高度定制化的应用。
应用场景:
复杂的嵌入式系统,需要分阶段初始化。
需要高度定制化和灵活性的应用。
高性能计算平台,需要复杂的启动过程。
代码示例:
第一阶段Bootloader (Stage1)
#include "stm32f4xx.h"
void SystemClock_Config(void);
void JumpToAddress(uint32_t address);
int main(void) {
// 系统时钟配置
SystemClock_Config();
// 设置堆栈指针
__set_MSP(*(__IO uint32_t*)0x20000000); // 假设堆栈位于SRAM起始位置
// 从外部Flash加载第二阶段Bootloader到内部SRAM
// 这里仅是概念演示,实际代码需要根据具体的Flash通信协议编写
// 例如使用SPI读取数据并写入到SRAM
// ...
// 假设第二阶段Bootloader已经加载到0x20001000
JumpToAddress(0x20001000);
}
void JumpToAddress(uint32_t address) {
void (*app_entry)(void) = (void (*)(void))(*(uint32_t*)address);
app_entry();
}
第二阶段Bootloader (Stage2)
#include "stm32f4xx.h"
#include "usart.h" // 假设有一个USART库用于串口通信
void SystemClock_Config(void);
void InitializePeripherals(void);
void JumpToApplication(uint32_t appAddress);
void UpdateFirmware(uint8_t *data, size_t length, uint32_t targetAddr);
int main(void) {
// 系统时钟配置
SystemClock_Config();
// 初始化外设
InitializePeripherals();
// 检查是否接收到固件更新请求
if (USART_ReceiveCommand() == 'U') {
// 接收新的固件数据
uint8_t *newFirmware = (uint8_t *)malloc(length);
USART_ReceiveData(newFirmware, length);
// 更新固件
UpdateFirmware(newFirmware, length, 0x08010000);
// 释放内存
free(newFirmware);
}
// 检查是否有有效的应用程序
if (*((uint32_t*)0x08008000) == 0x20000000) {
// 跳转到应用程序
JumpToApplication(0x08008000);
} else {
// 如果没有有效的应用程序,则保持在Bootloader中
while (1) {
// 可以添加错误处理代码
}
}
}
// 其他函数定义...
自定义Bootloader
功能:
根据具体应用需求定制的Bootloader。
特点:
功能和实现方式完全取决于应用需求。
可以结合上述各种类型的特性。
提供最大程度的灵活性和定制化。
应用场景:
特殊应用场景,如特定硬件平台或专有协议。
需要特定功能和高度定制化的应用。
科研项目或原型开发,需要灵活的启动和配置选项。
代码示例:
支持通过UART进行固件更新的自定义Bootloader,并且要求对固件进行CRC校验。
#include "stm32f4xx.h"
#include "usart.h" // 假设有一个USART库用于串口通信
#include <string.h>
#define APP_START_ADDR 0x08008000
#define BOOTLOADER_START_ADDR 0x08000000
#define FLASH_PAGE_SIZE 0x500
uint32_t CalculateCRC32(uint8_t *data, size_t length);
void SystemClock_Config(void);
void InitializePeripherals(void);
void JumpToApplication(uint32_t appAddress);
void EraseFlashPage(uint32_t page_addr);
void WriteToFlash(uint32_t addr, uint16_t data);
int main(void) {
// 系统时钟配置
SystemClock_Config();
// 初始化外设
InitializePeripherals();
// 检查是否接收到固件更新请求
if (USART_ReceiveCommand() == 'U') {
// 接收固件大小
size_t firmware_size;
USART_ReceiveSize(&firmware_size);
// 分配缓冲区接收固件数据
uint8_t *firmware_data = (uint8_t *)malloc(firmware_size);
USART_ReceiveData(firmware_data, firmware_size);
// 计算CRC32
uint32_t crc = CalculateCRC32(firmware_data, firmware_size);
// 接收预期的CRC32值
uint32_t expected_crc;
USART_ReceiveCRC(&expected_crc);
// 校验CRC
if (crc == expected_crc) {
// 固件有效,开始更新
for (size_t i = 0; i < firmware_size; i += 2) {
uint32_t flash_addr = APP_START_ADDR + i;
if (i % FLASH_PAGE_SIZE == 0) {
EraseFlashPage(flash_addr);
}
WriteToFlash(flash_addr, ((uint16_t)firmware_data << 8) | firmware_data[i+1]);
}
// 更新完成后,跳转到应用程序
JumpToApplication(APP_START_ADDR);
} else {
// CRC校验失败
while (1) {
// 错误处理
}
}
// 释放内存
free(firmware_data);
}
// 检查是否有有效的应用程序
if (*((uint32_t*)APP_START_ADDR) == 0x20000000) {
// 跳转到应用程序
JumpToApplication(APP_START_ADDR);
} else {
// 如果没有有效的应用程序,则保持在Bootloader中
while (1) {
// 可以添加错误处理代码
}
}
}
// 计算CRC32
uint32_t CalculateCRC32(uint8_t *data, size_t length) {
// 实现CRC32计算
// 这里只是一个占位符
return 0;
}
// 其他函数定义...
Bootloader的实现
实现一个Bootloader通常需要完成以下步骤:
硬件初始化:
设置CPU寄存器、初始化时钟、配置中断等,确保硬件处于正确的工作状态。
设置堆栈和堆空间:
分配足够的内存空间作为堆栈和堆,以便程序能够正常运行。
加载应用程序:
从Flash读取应用程序代码并将其拷贝到RAM中,准备执行。
跳转到应用程序入口点:
跳转到应用程序的入口点(如main()函数),启动应用程序。
固件更新逻辑:
如果Bootloader支持固件更新,需要实现接收新固件、验证固件完整性、写入新固件到Flash等操作。
常见的MCU Bootloader实例
ST-Link Bootloader:
用于STM32系列MCU,支持通过SWD或JTAG接口进行编程。
USB DFU Bootloader:
利用USB设备固件更新(Device Firmware Update)协议,通过USB接口进行固件更新。
TinyUSB Bootloader:
基于TinyUSB库实现的Bootloader,支持通过USB接口更新固件。
Blinky Bootloader:
适用于AVR和ARM Cortex-M系列MCU的开源Bootloader。
本次bootloader学习是基于双区bootloader,即bootloader程序和app程序存放在不同区域,由bootloader程序跳转到app程序运行。
通过几个示例对不同场景不同bootloader用法的学习:
bootloader直接跳转到app运行:
应用场景:bootloader和app程序都存放在XIP类型的Flash上,即可以在Flash上运行程序。
示例说明:bootloader初始化UART后打印“bootloader”,然后跳转到app程序,打印app程序内容。
bootloader跳转到app后,app把自己复制到RAM中运行:
应用场景:bootloader和app程序都存放在XIP类型的Flash上,即可以在Flash上运行程序。(程序复制到内存中运行可以提高CPU运行效率,参考文章:28335在程序在FLASH里运行和在RAM里运行的区别?)。
示例说明:bootloader初始化UART后打印“bootloader”,然后跳转到app程序,app把自己复制到RAM中运行,打印app程序内容。
bootloader把app复制到RAM中,再跳转到RAM中的app运行:
应用场景:bootloader程序都存放在XIP类型的Flash上,即可以在Flash上运行程序,app程序存放在非XIP类型的Flash上(如MCU外挂的SPI Flash)
示例说明:bootloader初始化UART后打印“bootloader”,然后把app复制到内存中,最后跳转到内存中app的位置运行,打印app程序内容。
bootloader把app复制到RAM中,并重新设置vector(异常向量表),再跳转到RAM中的app运行:
应用场景:bootloader程序都存放在XIP类型的Flash上,即可以在Flash上运行程序,app程序存放在非XIP类型的Flash上(如MCU外挂的SPI Flash)。
注:前提是硬件支持重新设置vector,例如app是一个FreeRTOS程序,需要使用到Timer中断,即Timer异常,要想使app正常运行就需要先设置好vector。
示例说明:bootloader初始化UART后打印“bootloader”,然后把app复制到内存中并重新设置vector,最后跳转到内存中app的位置运行,打印app程序内容。
bootloader把app复制到RAM中,实现vector(异常向量表)跳转到app的vector功能,再跳转到RAM中的app运行:
应用场景:bootloader程序都存放在XIP类型的Flash上,即可以在Flash上运行程序,app程序存放在非XIP类型的Flash上(如MCU外挂的SPI Flash)。
注:前提是硬件不支持重新设置vector,例如app是一个FreeRTOS程序,需要使用到Timer中断,即Timer异常,要想使app正常运行就需要先设置好vector,但由于无法重新设置vector,这时就需要在原先vector中实现:触发异常时跳转到app vector对应的异常去运行的功能。
此示例参考:韦东山老师教程
示例说明:bootloader初始化UART后打印“bootloader”,然后把app复制到内存中并实现vector跳转到app vector的功能,最后跳转到内存中app的位置运行,打印app程序内容。
2. 补充-异常向量概念
异常向量是一组固定的内存地址,这些地址指向处理特定类型异常的函数或代码段。当处理器检测到一个异常时,它会自动跳转到相应的异常向量所指示的位置,执行在那里存放的异常处理程序。
异常向量的作用:
异常处理:
异常向量允许处理器在检测到异常时,立即跳转到预先定义好的处理程序中,从而快速响应并处理异常情况。
这些处理程序可以进行错误处理、日志记录、系统重启等操作,以保证系统的稳定性和可靠性。
硬件初始化:
在某些情况下,异常向量也可以用于硬件初始化。例如,在系统上电或复位时,处理器可能会首先跳转到一个固定的异常向量地址,执行一些基本的硬件初始化操作。
系统安全:
异常向量还可以用于实现安全机制。例如,当检测到非法访问或错误指令时,处理器可以跳转到异常向量处的安全处理程序,防止系统受到进一步的损害。
调试支持:
异常向量通常也是调试过程中非常有用的工具。当系统运行时发生异常,开发人员可以通过查看异常向量及其处理程序来定位问题的原因。
异常类型:
常见的异常类型包括但不限于:
复位异常(Reset Exception):
系统复位时发生的异常,通常用于初始化系统。
未定义指令异常(Undefined Instruction Exception):
当处理器遇到一条无法识别的指令时触发的异常。
软件中断异常(Software Interrupt Exception):
通过软件指令(如SWI)引发的异常,通常用于系统调用。
预取中止异常(Prefetch Abort Exception):
当处理器尝试从非法地址预取指令时触发的异常。
数据中止异常(Data Abort Exception):
当处理器尝试访问非法地址的数据时触发的异常。
IRQ异常(IRQ Interrupt Exception):
处理器接收到中断请求(Interrupt Request)时触发的异常。
FIQ异常(FIQ Interrupt Exception):
快速中断请求(Fast Interrupt Request)时触发的异常,通常用于高优先级的中断处理。
异常向量表(Exception Vector Table):
异常向量表是一个固定长度的数组,每个元素对应一个异常类型,并指向处理该异常的函数地址。在系统启动时,异常向量表通常被加载到一个特定的内存区域(通常是RAM的低地址部分)。处理器在检测到异常时,会根据异常类型查找相应的向量地址,并跳转到那里执行处理程序。
ARM Cortex-M 系列的异常向量:
以ARM Cortex-M系列为例,其异常向量表通常包含以下条目:
复位向量(Reset Vector)
NMI(Non-Maskable Interrupt)向量
硬故障(Hard Fault)向量
内存管理故障(Memory Management Fault)向量
总线故障(Bus Fault)向量
使用故障(Usage Fault)向量
保留(Reserved)向量
SVCall(System Call)向量
监控器(Monitor)向量
PendSV(Pending SV)向量
Systick(SysTick)向量
IRQ中断向量表
异常向量的配置:
在编写MCU程序时,通常需要配置异常向量表。这可以通过以下方式实现:
初始化代码:
在系统初始化代码中设置异常向量表,确保处理器能够正确响应各种异常。
链接器脚本:
使用链接器脚本定义异常向量表的内存布局,确保异常向量表被放置在正确的内存位置。
硬件配置:
在某些情况下,可能需要通过硬件配置(如寄存器设置)来指定异常向量表的位置。
3. 示例1-APP无异常向量
3.1 源码
bootloader工程中只有4个文件,start.s、main.c、uart.c、uart.h,各文件源码如下:
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
; AREA定义了一个新的段(section),RESET表示这个段的名字,DATA表示数据段,READONLY表示这个段是只读的。
; EXPORT声明了一个全局符号__Vectors,使得其他模块可以引用这个符号。
; __Vectors是异常向量表的起始地址。
; DCD是一个伪指令,用于定义一个32位的常量(双字)。
; 第一个DCD定义了一个初始堆栈指针(SP)的值,这里设置为0。在实际应用中,通常会设置一个有效的RAM地址作为初始堆栈指针。
; 第二个DCD定义了复位处理程序的地址,即Reset_Handler。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD 0
DCD Reset_Handler ; Reset Handler
; AREA定义了一个新的段(section),.text表示代码段,CODE表示该段包含可执行代码,READONLY表示该段是只读的。
AREA |.text|, CODE, READONLY
; Reset_Handler是复位处理程序的入口点。
; EXPORT声明了Reset_Handler为全局可见,[WEAK]表示这是一个弱符号,如果没有其他定义,这个定义将被使用。
; IMPORT声明了一个外部符号main,表示main是一个外部函数,将在链接时被解析。
; LDRSP,=(0x20000000+0x5000):这条指令设置了堆栈指针(SP),指向内存地址0x20000000+0x5000,即0x20005000。这通常是一个RAM区域,用于存储栈数据。
; BLmain:这条指令通过跳转(BranchandLink)调用main函数,main函数应该是应用程序的入口点。
; ENDP结束了Reset_Handler过程的定义。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT main
LDR SP, =(0x20000000+0x5000)
BL main
ENDP
; END指令表示整个汇编文件的结束
END
main.c:
#include "uart.h"
void delay(int d) //简单的延时函数
{
while(d--);
}
int main()
{
void (*app)(void); //app地址
uart_init(); //初始化串口
putstr("bootloader\r\n"); // 串口打印bootloader
/* start app */
app = (void (*)(void))0x08008001; //将0x08008000地址赋给app,最低位等于1是Thumb指令集表示地址的特性
app(); //跳转到app程序,实现bootloader程序跳转
return 0;
}
uart.c(UART直接对寄存器操作,参考下面STM32F10XXX参考手册中截图):
#include "uart.h"
typedef unsigned int uint32_t;
typedef struct
{
volatile uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
volatile uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
volatile uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
volatile uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
volatile uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
volatile uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
volatile uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;
void uart_init(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //USART1寄存器地址
volatile unsigned int *pReg;
/* 使能GPIOA/USART1模块 */
/* RCC_APB2ENR */
pReg = (volatile unsigned int *)(0x40021000 + 0x18); //RRC寄存器地址
*pReg |= (1<<2) | (1<<14); //使能APB2总线上USART1和GPIOA
/* 配置引脚功能: PA9(USART1_TX), PA10(USART1_RX)
* GPIOA_CRH = 0x40010800 + 0x04
*/
pReg = (volatile unsigned int *)(0x40010800 + 0x04);
/* PA9(USART1_TX) */
*pReg &= ~((3<<4) | (3<<6)); //把GPIOA_CRH寄存器中MODE9和CNF9清零
*pReg |= (1<<4) | (2<<6); //配置为复用功能推挽输出,最大输出速度为10MHz
/* PA10(USART1_RX) */
*pReg &= ~((3<<8) | (3<<10)); //把GPIOA_CRH寄存器中MODE10和CNF10清零
*pReg |= (0<<8) | (1<<10); //配置为浮空输入
/* 设置波特率
* 115200 = 8000000/16/USARTDIV
* USARTDIV = 4.34
* DIV_Mantissa = 4
* DIV_Fraction / 16 = 0.34
* DIV_Fraction = 16*0.34 = 5
* 真实波特率:
* DIV_Fraction / 16 = 5/16=0.3125
* USARTDIV = DIV_Mantissa + DIV_Fraction / 16 = 4.3125
* baudrate = 8000000/16/4.3125 = 115942
*/
#define DIV_Mantissa 4
#define DIV_Fraction 5
usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction);
/* 设置数据格式: 8n1 */
usart1->CR1 = (1<<13) | (0<<12) | (0<<10) | (1<<3) | (1<<2);
usart1->CR2 &= ~(3<<12);
/* 使能USART1 */
}
int getchar(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<5)) == 0);
return usart1->DR;
}
int putchar(char c)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<7)) == 0);
usart1->DR = c;
return c;
}
void putstr(char *str)
{
while (*str)
{
putchar(*str);
str++;
}
}
uart.h:
#ifndef _UART_H
#define _UART_H
void uart_init(void);
int getchar(void);
int putchar(char c);
void putstr(char *str);
#endif
参考芯片手册中寄存器操作初始化串口:
app工程中也有4个文件start.s、main.c、uart.c、uart.h,各文件源码如下。
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
; 以下注释说明了向量表的定义,但在实际代码中并未定义。向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
; AREA RESET, DATA, READONLY
; EXPORT __Vectors
;
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0(初始堆栈指针),第二个条目指向Reset_Handler。
;__Vectors DCD 0
; DCD Reset_Handler ; Reset Handler
;
; AREA定义了一个新的段(section),.text表示代码段,CODE表示该段包含可执行代码,READONLY表示该段是只读的。
AREA |.text|, CODE, READONLY
; Reset_Handler是复位处理程序的入口点。
; EXPORT声明了Reset_Handler为全局可见,[WEAK]表示这是一个弱符号,如果没有其他定义,这个定义将被使用。
; IMPORT声明了一个外部符号mymain,表示mymain是一个外部函数,将在链接时被解析。
; LDRSP,=(0x20000000+0x5000):这条指令设置了堆栈指针(SP),指向内存地址0x20000000+0x5000,即0x200005000。这通常是一个RAM区域,用于存储栈数据。
; BLmymain:这条指令通过跳转(BranchandLink)调用mymain函数,mymain函数应该是应用程序的入口点。
; ENDP结束了Reset_Handler过程的定义。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT mymain
LDR SP, =(0x20000000+0x5000) ; 0x08008000
BL mymain
ENDP
; END指令表示整个汇编文件的结束
END
main.c:
#include "uart.h"
void delay(int d)
{
while(d--);
}
/* 循环打印字母A-Z */
int mymain()
{
char c = 'A';
while (1)
{
putchar(c++);
delay(1000000);
if (c == 'Z')
c = 'A';
}
return 0;
}
注:app的main.c中如果把mymain改成main时无法正常跳转,使用mymain时编译的代码和使用main时编译的代码反汇编不同,在示例2-APP有异常向量中,无论使用mymain还是使用main都能成功跳转,猜测原因是没有设置异常向量表且函数未main时,系统会默认设置异常向量表;使用mymain时,系统不会主动设置异常向量表。
app的main.c使用main时的反汇编:
========================================================================
** ELF Header Information
File Name: Objects\led_c.axf
Machine class: ELFCLASS32 (32-bit)
Data encoding: ELFDATA2LSB (Little endian)
Header version: EV_CURRENT (Current version)
Operating System ABI: none
ABI Version: 0
File Type: ET_EXEC (Executable) (2)
Machine: EM_ARM (ARM)
Image Entry point: 0x08008081
Flags: EF_ARM_HASENTRY + EF_ARM_ABI_FLOAT_SOFT (0x05000202)
ARM ELF revision: 5 (ABI version 2)
Conforms to Soft float procedure-call standard
Built with
Component: ARM Compiler 5.06 update 7 (build 960) Tool: armasm [4d35fa]
Component: ARM Compiler 5.06 update 7 (build 960) Tool: armlink [4d3601]
Header size: 52 bytes (0x34)
Program header entry size: 32 bytes (0x20)
Section header entry size: 40 bytes (0x28)
Program header entries: 1
Section header entries: 15
Program header offset: 12608 (0x00003140)
Section header offset: 12640 (0x00003160)
Section header string table index: 14
========================================================================
** Program header #0 (PT_LOAD) [PF_X + PF_W + PF_R + PF_ARM_ENTRY]
Size : 484 bytes (388 bytes in file)
Virtual address: 0x08008000 (Alignment 4)
========================================================================
** Section #1 'ER_RO' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
Size : 388 bytes (alignment 4)
Address: 0x08008000
$t
!!!main
__main
0x08008000: f000f802 .... BL __scatterload ; 0x8008008
0x08008004: f000f82c ..,. BL __rt_entry ; 0x8008060
!!!scatter
__scatterload
__scatterload_rt2
__scatterload_rt2_thumb_only
0x08008008: a00a .. ADR r0,{pc}+0x2c ; 0x8008034
0x0800800a: e8900c00 .... LDM r0,{r10,r11}
0x0800800e: 4482 .D ADD r10,r10,r0
0x08008010: 4483 .D ADD r11,r11,r0
0x08008012: f1aa0701 .... SUB r7,r10,#1
__scatterload_null
0x08008016: 45da .E CMP r10,r11
0x08008018: d101 .. BNE 0x800801e ; __scatterload_null + 8
0x0800801a: f000f821 ..!. BL __rt_entry ; 0x8008060
0x0800801e: f2af0e09 .... ADR lr,{pc}-7 ; 0x8008017
0x08008022: e8ba000f .... LDM r10!,{r0-r3}
0x08008026: f0130f01 .... TST r3,#1
0x0800802a: bf18 .. IT NE
0x0800802c: 1afb .. SUBNE r3,r7,r3
0x0800802e: f0430301 C... ORR r3,r3,#1
0x08008032: 4718 .G BX r3
$d
0x08008034: 00000140 @... DCD 320
0x08008038: 00000150 P... DCD 336
$t
!!handler_zi
__scatterload_zeroinit
0x0800803c: 2300 .# MOVS r3,#0
0x0800803e: 2400 .$ MOVS r4,#0
0x08008040: 2500 .% MOVS r5,#0
0x08008042: 2600 .& MOVS r6,#0
0x08008044: 3a10 .: SUBS r2,r2,#0x10
0x08008046: bf28 (. IT CS
0x08008048: c178 x. STMCS r1!,{r3-r6}
0x0800804a: d8fb .. BHI 0x8008044 ; __scatterload_zeroinit + 8
0x0800804c: 0752 R. LSLS r2,r2,#29
0x0800804e: bf28 (. IT CS
0x08008050: c130 0. STMCS r1!,{r4,r5}
0x08008052: bf48 H. IT MI
0x08008054: 600b .` STRMI r3,[r1,#0]
0x08008056: 4770 pG BX lr
.ARM.Collect$$libinit$$00000000
__rt_lib_init
0x08008058: b51f .. PUSH {r0-r4,lr}
.ARM.Collect$$libinit$$00000002
.ARM.Collect$$libinit$$00000004
.ARM.Collect$$libinit$$0000000A
.ARM.Collect$$libinit$$0000000C
.ARM.Collect$$libinit$$0000000E
.ARM.Collect$$libinit$$00000011
.ARM.Collect$$libinit$$00000013
.ARM.Collect$$libinit$$00000015
.ARM.Collect$$libinit$$00000017
.ARM.Collect$$libinit$$00000019
.ARM.Collect$$libinit$$0000001B
.ARM.Collect$$libinit$$0000001D
.ARM.Collect$$libinit$$0000001F
.ARM.Collect$$libinit$$00000021
.ARM.Collect$$libinit$$00000023
.ARM.Collect$$libinit$$00000025
.ARM.Collect$$libinit$$0000002C
.ARM.Collect$$libinit$$0000002E
.ARM.Collect$$libinit$$00000030
.ARM.Collect$$libinit$$00000032
.ARM.Collect$$libinit$$00000033
__rt_lib_init_alloca_1
__rt_lib_init_argv_1
__rt_lib_init_atexit_1
__rt_lib_init_clock_1
__rt_lib_init_cpp_1
__rt_lib_init_exceptions_1
__rt_lib_init_fp_1
__rt_lib_init_fp_trap_1
__rt_lib_init_getenv_1
__rt_lib_init_heap_1
__rt_lib_init_lc_collate_1
__rt_lib_init_lc_ctype_1
__rt_lib_init_lc_monetary_1
__rt_lib_init_lc_numeric_1
__rt_lib_init_lc_time_1
__rt_lib_init_preinit_1
__rt_lib_init_rand_1
__rt_lib_init_return
__rt_lib_init_signal_1
__rt_lib_init_stdio_1
__rt_lib_init_user_alloc_1
0x0800805a: bd1f .. POP {r0-r4,pc}
.ARM.Collect$$libshutdown$$00000000
__rt_lib_shutdown
0x0800805c: b510 .. PUSH {r4,lr}
.ARM.Collect$$libshutdown$$00000002
.ARM.Collect$$libshutdown$$00000004
.ARM.Collect$$libshutdown$$00000006
.ARM.Collect$$libshutdown$$00000009
.ARM.Collect$$libshutdown$$0000000C
.ARM.Collect$$libshutdown$$0000000E
.ARM.Collect$$libshutdown$$00000011
.ARM.Collect$$libshutdown$$00000012
__rt_lib_shutdown_cpp_1
__rt_lib_shutdown_fini_1
__rt_lib_shutdown_fp_trap_1
__rt_lib_shutdown_heap_1
__rt_lib_shutdown_return
__rt_lib_shutdown_signal_1
__rt_lib_shutdown_stdio_1
__rt_lib_shutdown_user_alloc_1
0x0800805e: bd10 .. POP {r4,pc}
.ARM.Collect$$rtentry$$00000000
.ARM.Collect$$rtentry$$00000002
.ARM.Collect$$rtentry$$00000004
__rt_entry
__rt_entry_presh_1
__rt_entry_sh
0x08008060: f000f814 .... BL __user_setup_stackheap ; 0x800808c
0x08008064: 4611 .F MOV r1,r2
.ARM.Collect$$rtentry$$00000009
.ARM.Collect$$rtentry$$0000000A
__rt_entry_li
__rt_entry_postsh_1
0x08008066: f7fffff7 .... BL __rt_lib_init ; 0x8008058
.ARM.Collect$$rtentry$$0000000C
.ARM.Collect$$rtentry$$0000000D
__rt_entry_main
__rt_entry_postli_1
0x0800806a: f000f867 ..g. BL main ; 0x800813c
0x0800806e: f000f832 ..2. BL exit ; 0x80080d6
.ARM.Collect$$rtexit$$00000000
__rt_exit
0x08008072: b403 .. PUSH {r0,r1}
.ARM.Collect$$rtexit$$00000002
.ARM.Collect$$rtexit$$00000003
__rt_exit_ls
__rt_exit_prels_1
0x08008074: f7fffff2 .... BL __rt_lib_shutdown ; 0x800805c
.ARM.Collect$$rtexit$$00000004
__rt_exit_exit
0x08008078: bc03 .. POP {r0,r1}
0x0800807a: f000f853 ..S. BL _sys_exit ; 0x8008124
0x0800807e: 0000 .. MOVS r0,r0
.text
Reset_Handler
0x08008080: f8dfd004 .... LDR sp,[pc,#4] ; [0x8008088] = 0x20005000
0x08008084: f000f85a ..Z. BL main ; 0x800813c
$d
0x08008088: 20005000 .P. DCD 536891392
$t
.text
__user_setup_stackheap
0x0800808c: 4675 uF MOV r5,lr
0x0800808e: f000f82b ..+. BL __user_libspace ; 0x80080e8
0x08008092: 46ae .F MOV lr,r5
0x08008094: 0005 .. MOVS r5,r0
0x08008096: 4669 iF MOV r1,sp
0x08008098: 4653 SF MOV r3,r10
0x0800809a: f0200007 ... BIC r0,r0,#7
0x0800809e: 4685 .F MOV sp,r0
0x080080a0: b018 .. ADD sp,sp,#0x60
0x080080a2: b520 . PUSH {r5,lr}
0x080080a4: f000f824 ..$. BL __user_initial_stackheap ; 0x80080f0
0x080080a8: e8bd4020 .. @ POP {r5,lr}
0x080080ac: f04f0600 O... MOV r6,#0
0x080080b0: f04f0700 O... MOV r7,#0
0x080080b4: f04f0800 O... MOV r8,#0
0x080080b8: f04f0b00 O... MOV r11,#0
0x080080bc: f0210107 !... BIC r1,r1,#7
0x080080c0: 46ac .F MOV r12,r5
0x080080c2: e8ac09c0 .... STM r12!,{r6-r8,r11}
0x080080c6: e8ac09c0 .... STM r12!,{r6-r8,r11}
0x080080ca: e8ac09c0 .... STM r12!,{r6-r8,r11}
0x080080ce: e8ac09c0 .... STM r12!,{r6-r8,r11}
0x080080d2: 468d .F MOV sp,r1
0x080080d4: 4770 pG BX lr
.text
exit
0x080080d6: b510 .. PUSH {r4,lr}
0x080080d8: 4604 .F MOV r4,r0
0x080080da: f3af8000 .... NOP.W
0x080080de: 4620 F MOV r0,r4
0x080080e0: e8bd4010 ...@ POP {r4,lr}
0x080080e4: f7ffbfc5 .... B.W __rt_exit ; 0x8008072
.text
__user_libspace
__user_perproc_libspace
__user_perthread_libspace
0x080080e8: 4800 .H LDR r0,[pc,#0] ; [0x80080ec] = 0x20000000
0x080080ea: 4770 pG BX lr
$d
0x080080ec: 20000000 ... DCD 536870912
$t
.text
__user_initial_stackheap
0x080080f0: b500 .. PUSH {lr}
0x080080f2: f04f0016 O... MOV r0,#0x16
0x080080f6: b085 .. SUB sp,sp,#0x14
0x080080f8: 4669 iF MOV r1,sp
0x080080fa: aa01 .. ADD r2,sp,#4
0x080080fc: 600a .` STR r2,[r1,#0]
0x080080fe: beab .. BKPT #0xab
0x08008100: 9801 .. LDR r0,[sp,#4]
0x08008102: 2800 .( CMP r0,#0
0x08008104: bf02 .. ITTT EQ
0x08008106: 4805 .H LDREQ r0,_RW_Limit ; [0x800811c] = 0x20000060
0x08008108: 1dc0 .. ADDEQ r0,r0,#7
0x0800810a: f0200007 ... BICEQ r0,r0,#7
0x0800810e: 9903 .. LDR r1,[sp,#0xc]
0x08008110: 9a02 .. LDR r2,[sp,#8]
0x08008112: 9b04 .. LDR r3,[sp,#0x10]
0x08008114: b005 .. ADD sp,sp,#0x14
0x08008116: bd00 .. POP {pc}
$d
0x08008118: 00000009 .... DCD 9
_RW_Limit
0x0800811c: 20000060 `.. DCD 536871008
$t
.text
__I$use$semihosting
__use_no_semihosting_swi
0x08008120: 4770 pG BX lr
.text
__semihosting_library_function
0x08008122: 0000 .. MOVS r0,r0
.text
_sys_exit
0x08008124: 4901 .I LDR r1,[pc,#4] ; [0x800812c] = 0x20026
0x08008126: 2018 . MOVS r0,#0x18
0x08008128: beab .. BKPT #0xab
0x0800812a: e7fe .. B 0x800812a ; _sys_exit + 6
$d
0x0800812c: 00020026 &... DCD 131110
$t
i.delay
delay
0x08008130: bf00 .. NOP
0x08008132: 1e01 .. SUBS r1,r0,#0
0x08008134: f1a00001 .... SUB r0,r0,#1
0x08008138: d1fb .. BNE 0x8008132 ; delay + 2
0x0800813a: 4770 pG BX lr
i.main
main
0x0800813c: 2441 A$ MOVS r4,#0x41
0x0800813e: e00a .. B 0x8008156 ; main + 26
0x08008140: 4620 F MOV r0,r4
0x08008142: 1c62 b. ADDS r2,r4,#1
0x08008144: b2d4 .. UXTB r4,r2
0x08008146: f000f809 .... BL putchar ; 0x800815c
0x0800814a: 4803 .H LDR r0,[pc,#12] ; [0x8008158] = 0xf4240
0x0800814c: f7fffff0 .... BL delay ; 0x8008130
0x08008150: 2c5a Z, CMP r4,#0x5a
0x08008152: d100 .. BNE 0x8008156 ; main + 26
0x08008154: 2441 A$ MOVS r4,#0x41
0x08008156: e7f3 .. B 0x8008140 ; main + 4
$d
0x08008158: 000f4240 @B.. DCD 1000000
$t
i.putchar
putchar
0x0800815c: 4904 .I LDR r1,[pc,#16] ; [0x8008170] = 0x40013800
0x0800815e: bf00 .. NOP
0x08008160: 680a .h LDR r2,[r1,#0]
0x08008162: f0020280 .... AND r2,r2,#0x80
0x08008166: 2a00 .* CMP r2,#0
0x08008168: d0fa .. BEQ 0x8008160 ; putchar + 4
0x0800816a: 6048 H` STR r0,[r1,#4]
0x0800816c: 4770 pG BX lr
$d
0x0800816e: 0000 .. DCW 0
0x08008170: 40013800 .8.@ DCD 1073821696
$d.realdata
Region$$Table$$Base
0x08008174: 08008184 .... DCD 134250884
0x08008178: 20000000 ... DCD 536870912
0x0800817c: 00000060 `... DCD 96
0x08008180: 0800803c <... DCD 134250556
Region$$Table$$Limit
** Section #2 'ER_ZI' (SHT_NOBITS) [SHF_ALLOC + SHF_WRITE]
Size : 96 bytes (alignment 4)
Address: 0x20000000
** Section #3 '.debug_abbrev' (SHT_PROGBITS)
Size : 1476 bytes
** Section #4 '.debug_frame' (SHT_PROGBITS)
Size : 508 bytes
** Section #5 '.debug_info' (SHT_PROGBITS)
Size : 1216 bytes
** Section #6 '.debug_line' (SHT_PROGBITS)
Size : 332 bytes
** Section #7 '.debug_loc' (SHT_PROGBITS)
Size : 100 bytes
** Section #8 '.debug_macinfo' (SHT_PROGBITS)
Size : 44 bytes
** Section #9 '.debug_pubnames' (SHT_PROGBITS)
Size : 85 bytes
** Section #10 '.symtab' (SHT_SYMTAB)
Size : 3120 bytes (alignment 4)
String table #11 '.strtab'
Last local symbol no. 124
** Section #11 '.strtab' (SHT_STRTAB)
Size : 3344 bytes
** Section #12 '.note' (SHT_NOTE)
Size : 24 bytes (alignment 4)
** Section #13 '.comment' (SHT_PROGBITS)
Size : 1760 bytes
** Section #14 '.shstrtab' (SHT_STRTAB)
Size : 156 bytes
app的main.c使用mymain时的反汇编:
========================================================================
** ELF Header Information
File Name: Objects\led_c.axf
Machine class: ELFCLASS32 (32-bit)
Data encoding: ELFDATA2LSB (Little endian)
Header version: EV_CURRENT (Current version)
Operating System ABI: none
ABI Version: 0
File Type: ET_EXEC (Executable) (2)
Machine: EM_ARM (ARM)
Image Entry point: 0x08008001
Flags: EF_ARM_HASENTRY + EF_ARM_ABI_FLOAT_SOFT (0x05000202)
ARM ELF revision: 5 (ABI version 2)
Conforms to Soft float procedure-call standard
Built with
Component: ARM Compiler 5.06 update 7 (build 960) Tool: armasm [4d35fa]
Component: ARM Compiler 5.06 update 7 (build 960) Tool: armlink [4d3601]
Header size: 52 bytes (0x34)
Program header entry size: 32 bytes (0x20)
Section header entry size: 40 bytes (0x28)
Program header entries: 1
Section header entries: 14
Program header offset: 5868 (0x000016ec)
Section header offset: 5900 (0x0000170c)
Section header string table index: 13
========================================================================
** Program header #0 (PT_LOAD) [PF_X + PF_R + PF_ARM_ENTRY]
Size : 80 bytes
Virtual address: 0x08008000 (Alignment 4)
========================================================================
** Section #1 'ER_RO' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
Size : 80 bytes (alignment 4)
Address: 0x08008000
$t
.text
Reset_Handler
0x08008000: f8dfd004 .... LDR sp,[pc,#4] ; [0x8008008] = 0x20005000
0x08008004: f000f808 .... BL mymain ; 0x8008018
$d
0x08008008: 20005000 .P. DCD 536891392
$t
i.delay
delay
0x0800800c: bf00 .. NOP
0x0800800e: 1e01 .. SUBS r1,r0,#0
0x08008010: f1a00001 .... SUB r0,r0,#1
0x08008014: d1fb .. BNE 0x800800e ; delay + 2
0x08008016: 4770 pG BX lr
i.mymain
mymain
0x08008018: 2441 A$ MOVS r4,#0x41
0x0800801a: e00a .. B 0x8008032 ; mymain + 26
0x0800801c: 4620 F MOV r0,r4
0x0800801e: 1c62 b. ADDS r2,r4,#1
0x08008020: b2d4 .. UXTB r4,r2
0x08008022: f000f809 .... BL putchar ; 0x8008038
0x08008026: 4803 .H LDR r0,[pc,#12] ; [0x8008034] = 0xf4240
0x08008028: f7fffff0 .... BL delay ; 0x800800c
0x0800802c: 2c5a Z, CMP r4,#0x5a
0x0800802e: d100 .. BNE 0x8008032 ; mymain + 26
0x08008030: 2441 A$ MOVS r4,#0x41
0x08008032: e7f3 .. B 0x800801c ; mymain + 4
$d
0x08008034: 000f4240 @B.. DCD 1000000
$t
i.putchar
putchar
0x08008038: 4904 .I LDR r1,[pc,#16] ; [0x800804c] = 0x40013800
0x0800803a: bf00 .. NOP
0x0800803c: 680a .h LDR r2,[r1,#0]
0x0800803e: f0020280 .... AND r2,r2,#0x80
0x08008042: 2a00 .* CMP r2,#0
0x08008044: d0fa .. BEQ 0x800803c ; putchar + 4
0x08008046: 6048 H` STR r0,[r1,#4]
0x08008048: 4770 pG BX lr
$d
0x0800804a: 0000 .. DCW 0
0x0800804c: 40013800 .8.@ DCD 1073821696
** Section #2 '.debug_abbrev' (SHT_PROGBITS)
Size : 1476 bytes
** Section #3 '.debug_frame' (SHT_PROGBITS)
Size : 224 bytes
** Section #4 '.debug_info' (SHT_PROGBITS)
Size : 1220 bytes
** Section #5 '.debug_line' (SHT_PROGBITS)
Size : 332 bytes
** Section #6 '.debug_loc' (SHT_PROGBITS)
Size : 100 bytes
** Section #7 '.debug_macinfo' (SHT_PROGBITS)
Size : 44 bytes
** Section #8 '.debug_pubnames' (SHT_PROGBITS)
Size : 87 bytes
** Section #9 '.symtab' (SHT_SYMTAB)
Size : 304 bytes (alignment 4)
String table #10 '.strtab'
Last local symbol no. 13
** Section #10 '.strtab' (SHT_STRTAB)
Size : 248 bytes
** Section #11 '.note' (SHT_NOTE)
Size : 20 bytes (alignment 4)
** Section #12 '.comment' (SHT_PROGBITS)
Size : 1524 bytes
** Section #13 '.shstrtab' (SHT_STRTAB)
Size : 156 bytes
可以看到使用mymain时反汇编会‘干净’很多。
app工程中uart.c、uart.h和bootloader中uart.c、uart.h内容是一样的。
uart.c:
#include "uart.h"
typedef unsigned int uint32_t;
typedef struct
{
volatile uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
volatile uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
volatile uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
volatile uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
volatile uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
volatile uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
volatile uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;
void uart_init(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800; //USART1寄存器地址
volatile unsigned int *pReg;
/* 使能GPIOA/USART1模块 */
/* RCC_APB2ENR */
pReg = (volatile unsigned int *)(0x40021000 + 0x18); //RRC寄存器地址
*pReg |= (1<<2) | (1<<14); //使能APB2总线上USART1和GPIOA
/* 配置引脚功能: PA9(USART1_TX), PA10(USART1_RX)
* GPIOA_CRH = 0x40010800 + 0x04
*/
pReg = (volatile unsigned int *)(0x40010800 + 0x04);
/* PA9(USART1_TX) */
*pReg &= ~((3<<4) | (3<<6)); //把GPIOA_CRH寄存器中MODE9和CNF9清零
*pReg |= (1<<4) | (2<<6); //配置为复用功能推挽输出,最大输出速度为10MHz
/* PA10(USART1_RX) */
*pReg &= ~((3<<8) | (3<<10)); //把GPIOA_CRH寄存器中MODE10和CNF10清零
*pReg |= (0<<8) | (1<<10); //配置为浮空输入
/* 设置波特率
* 115200 = 8000000/16/USARTDIV
* USARTDIV = 4.34
* DIV_Mantissa = 4
* DIV_Fraction / 16 = 0.34
* DIV_Fraction = 16*0.34 = 5
* 真实波特率:
* DIV_Fraction / 16 = 5/16=0.3125
* USARTDIV = DIV_Mantissa + DIV_Fraction / 16 = 4.3125
* baudrate = 8000000/16/4.3125 = 115942
*/
#define DIV_Mantissa 4
#define DIV_Fraction 5
usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction);
/* 设置数据格式: 8n1 */
usart1->CR1 = (1<<13) | (0<<12) | (0<<10) | (1<<3) | (1<<2);
usart1->CR2 &= ~(3<<12);
/* 使能USART1 */
}
int getchar(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<5)) == 0);
return usart1->DR;
}
int putchar(char c)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<7)) == 0);
usart1->DR = c;
return c;
}
void putstr(char *str)
{
while (*str)
{
putchar(*str);
str++;
}
}
uart.h:
#ifndef _UART_H
#define _UART_H
void uart_init(void);
int getchar(void);
int putchar(char c);
void putstr(char *str);
#endif
3.2 程序烧录
根据STM32F103C8T6 (Flash 64K RAM 20K),64K = 0x10000,32K = 0x8000,20K = 0x5000。Flash的起始地址是0x8000000,RAM的起始地址是0x20000000。
希望将Flash平静分成2部分,前半部分烧录bootloader,后半部分烧录app。因此:
bootloader的起始地址是0x8000000,size是0x8000,RAM起始地址是0x20000000,size是0x5000(为什么RAM的size不是0x2800也可以,暂时没有深究)。
app的起始地址是0x8008000,size是0x8000,RAM起始地址是0x20000000,size是0x5000
烧录之后复位MCU:
从bootloader工程跳转到app工程成功!
4. 示例2-APP有异常向量
4.1 源码
bootloader工程中只有4个文件,start.s、main.c、uart.c、uart.h,其中只有main.c文件和示例1-APP无异常向量不同。
main.c:
#include "uart.h"
void delay(int d)
{
while(d--);
}
int main()
{
unsigned int *p = (unsigned int *)0x08008004;
unsigned int val = *p; /* 从app的异常向量表中获取第二项跳转,因为app的Reset_Handler地址是0x08008004,取出该地址的函数并执行 */
void (*app)(void);
uart_init();
putstr("bootloader\r\n");
/* start app */
app = (void (*)(void))val;
app();
return 0;
}
app工程也有4个文件,start.s、main.c、uart.c、uart.h,其中uart.c、uart.h文件和示例1-APP无异常向量相同。
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
;向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0(初始堆栈指针),第二个条目指向Reset_Handler。
__Vectors DCD 0
DCD Reset_Handler ; Reset Handler
; AREA定义了一个新的段(section),.text表示代码段,CODE表示该段包含可执行代码,READONLY表示该段是只读的。
AREA |.text|, CODE, READONLY
; Reset_Handler是复位处理程序的入口点。
; EXPORT声明了Reset_Handler为全局可见,[WEAK]表示这是一个弱符号,如果没有其他定义,这个定义将被使用。
; IMPORT声明了一个外部符号mymain,表示mymain是一个外部函数,将在链接时被解析。
; LDRSP,=(0x20000000+0x5000):这条指令设置了堆栈指针(SP),指向内存地址0x20000000+0x5000,即0x200005000。这通常是一个RAM区域,用于存储栈数据。
; BLmymain:这条指令通过跳转(BranchandLink)调用mymain函数,mymain函数应该是应用程序的入口点。
; ENDP结束了Reset_Handler过程的定义。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT main
LDR SP, =(0x20000000+0x5000) ; 0x08008000
BL main
ENDP
; END指令表示整个汇编文件的结束
END
main.c:
#include "uart.h"
void delay(int d)
{
while(d--);
}
/* 循环打印字母A-Z */
int main()
{
char c = 'A';
while (1)
{
putchar(c++);
delay(1000000);
if (c == 'Z')
c = 'A';
}
return 0;
}
]
示例2-APP有异常向量与示例1-APP无异常向量对比差异:
4.2 程序烧录
程序烧录配置和示例1-APP无异常向量也相同。
从bootloader工程跳转到app工程成功!
5. 示例3-使用汇编跳转
5.1 源码
bootloader工程中只有4个文件,start.s、main.c、uart.c、uart.h,其中main.c、start.s文件和示例1-APP无异常向量不同。
main.c
#include "uart.h"
/* 导入start_app函数 */
extern void start_app(unsigned int sp, unsigned int pc);
void delay(int d)
{
while(d--);
}
int main()
{
unsigned int *p = (unsigned int *)0x08008000;
unsigned int sp = *p; /* 将app地址的值赋给SP */
unsigned int pc = *(p+1); /* 将Reset_Handler赋给PC */
uart_init();
putstr("bootloader\r\n");
/* start app */
start_app(sp, pc); /* r0=sp, r1=pc */
return 0;
}
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
;向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0(初始堆栈指针),第二个条目指向Reset_Handler。
__Vectors DCD 0
DCD Reset_Handler ; Reset Handler
; AREA定义了一个新的段(section),.text表示代码段,CODE表示该段包含可执行代码,READONLY表示该段是只读的。
AREA |.text|, CODE, READONLY
; Reset_Handler是复位处理程序的入口点。
; EXPORT声明了Reset_Handler为全局可见,[WEAK]表示这是一个弱符号,如果没有其他定义,这个定义将被使用。
; IMPORT声明了一个外部符号mymain,表示mymain是一个外部函数,将在链接时被解析。
; LDRSP,=(0x20000000+0x5000):这条指令设置了堆栈指针(SP),指向内存地址0x20000000+0x5000,即0x200005000。这通常是一个RAM区域,用于存储栈数据。
; BLmymain:这条指令通过跳转(BranchandLink)调用mymain函数,mymain函数应该是应用程序的入口点。
; ENDP结束了Reset_Handler过程的定义。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT main
LDR SP, =(0x20000000+0x5000) ; 0x08008000
BL main
ENDP
start_app PROC
EXPORT start_app ;导入start_app函数
mov sp, r0 ;将寄存器r0的值赋给堆栈指针SP
BX r1 ;这是一条分支并交换指令,根据r1的内容切换到相应的处理器模式(ARM或Thumb)并跳转到r1所指向的地址,r1通常包含一个函数指针,指向要执行的代码,即app
ENDP
; END指令表示整个汇编文件的结束
END
app工程也有4个文件,start.s、main.c、uart.c、uart.h,其中start.s文件和示例2-APP无异常向量不同,只是将栈SP的初始化改变了位置,即使使用示例2-APP无异常向量中的start.s也能正常执行。
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
;向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0x20000000+0x5000(初始堆栈指针),第二个条目指向Reset_Handler。
__Vectors DCD 0x20000000+0x5000
DCD Reset_Handler ; Reset Handler
; AREA定义了一个新的段(section),.text表示代码段,CODE表示该段包含可执行代码,READONLY表示该段是只读的。
AREA |.text|, CODE, READONLY
; Reset_Handler是复位处理程序的入口点。
; EXPORT声明了Reset_Handler为全局可见,[WEAK]表示这是一个弱符号,如果没有其他定义,这个定义将被使用。
; IMPORT声明了一个外部符号mymain,表示mymain是一个外部函数,将在链接时被解析。
; LDRSP,=(0x20000000+0x5000):这条指令设置了堆栈指针(SP),指向内存地址0x20000000+0x5000,即0x200005000。这通常是一个RAM区域,用于存储栈数据。
; BLmymain:这条指令通过跳转(BranchandLink)调用mymain函数,mymain函数应该是应用程序的入口点。
; ENDP结束了Reset_Handler过程的定义。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT main
;LDR SP, =(0x20000000+0x5000) ; 0x08008000
BL main
ENDP
; END指令表示整个汇编文件的结束
END
示例3-使用汇编跳转与示例2-APP有异常向量对比差异:
5.2 程序烧录
程序烧录配置和示例1-APP无异常向量也相同。
从bootloader工程跳转到app工程成功!
6. 示例4-重定位vector
6.1 源码
在bootloader中设置了异常向量,在跳转到应用工程中时,重新设置异常向量。
重定位Vector使用bootloader启动一个较为复杂的工程(包含FreeRTOS),bootloader工程与示例3-使用汇编跳转中main.c和start.s文件不同,uart.c和uart.h相同。
main.c:
#include "uart.h"
extern void start_app(unsigned int new_vector);
void delay(int d)
{
while(d--);
}
int mymain()
{
unsigned int new_vector = 0x08008000; //获取app工程的起始地址
uart_init();
putstr("bootloader\r\n");
/* start app */
start_app(new_vector); //在汇编中重定位vector
return 0;
}
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
;向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0(初始堆栈指针),第二个条目指向Reset_Handler。
__Vectors DCD 0
DCD Reset_Handler ; Reset Handler
; AREA定义了一个新的段(section),.text表示代码段,CODE表示该段包含可执行代码,READONLY表示该段是只读的。
AREA |.text|, CODE, READONLY
; Reset_Handler是复位处理程序的入口点。
; EXPORT声明了Reset_Handler为全局可见,[WEAK]表示这是一个弱符号,如果没有其他定义,这个定义将被使用。
; IMPORT声明了一个外部符号mymain,表示mymain是一个外部函数,将在链接时被解析。
; LDRSP,=(0x20000000+0x5000):这条指令设置了堆栈指针(SP),指向内存地址0x20000000+0x5000,即0x200005000。这通常是一个RAM区域,用于存储栈数据。
; BLmymain:这条指令通过跳转(BranchandLink)调用mymain函数,mymain函数应该是应用程序的入口点。
; ENDP结束了Reset_Handler过程的定义。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT main
LDR SP, =(0x20000000+0x5000) ; 0x08008000
BL main
ENDP
start_app PROC
EXPORT start_app ;导入start_app函数
; 设置vector基地址为0x08008000,可参考《Cortex_M3权威指南》
ldr r3, =0xE000ED08
str r0, [r3] ; 将r0的内容存储到VTOR寄存器中,r0=0x08008000
ldr sp, [r0] ;初始化栈,从r0读取数据并将其设置为栈指针
ldr r1, [r0, #4] ; 读取0x08008004地址的数据,这个地址是新向量表中的复位处理程序地址
BX r1 ;这是一条分支并交换指令,根据r1的内容切换到相应的处理器模式(ARM或Thumb)并跳转到r1所指向的地址,r1通常包含一个函数指针,指向要执行的代码,即app
ENDP
; END指令表示整个汇编文件的结束
END
app工程是一个带有FreeRTOS的工程,在启动时打印"Hello World!",然后在task中累加打印StartDefaultTask[n],源码参考附件。
示例4-重定位vector与示例3-使用汇编跳转对比差异:
6.2 程序烧录
程序烧录配置和示例1-APP无异常向量也相同。
从bootloader工程跳转到app工程成功!
7. 示例5-APP自我复制
7.1 源码
从bootloader跳转到app工程后,app将自己的代码复制到内存中运行。
示例5-APP自我复制的bootloader工程代码与示例4-重定位vector的bootloader工程完全相同。
app工程中main.c和start.s文件与示例3-使用汇编跳转不同,uart.c和uart.h相同。
main.c:
#include "uart.h"
static char buf[100] = "www.100ask.net";
void copy_myself(int *from, int *to, int len)
{
// 从哪里到哪里, 多长 ?
int i;
for (i = 0; i < len/4+1; i++)
{
to = from;
}
}
void delay(int d)
{
while(d--);
}
int mymain()
{
char c = 'A';
int (*fp)(char c);
fp = putchar;
putstr(buf);
while (1)
{
fp(c++);
putchar(c++);
delay(1000000);
if (c == 'Z')
c = 'A';
}
return 0;
}
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
;向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0x20000000+0x5000(初始堆栈指针),第二个条目指向Reset_Handler,指令设置复位处理程序的地址为0x08008008。
__Vectors DCD 0x20000000+0x5000
DCD 0x08008009 ;Reset_Handler ; Reset Handler
AREA |.text|, CODE, READONLY ;定义一个只读代码段,名为 .text
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK] ;导出 Reset_Handler 符号,并标记为弱符号,允许其他地方重定义
IMPORT mymain ;导入外部符号 mymain, copy_myself 和 |Image$$ER_IROM1$$Length|
IMPORT copy_myself
IMPORT |Image$$ER_IROM1$$Length|
adr r0, Reset_Handler ; r0=0x08008000
bic r0, r0, #0xff ; 清除低8位,得到0x08008000
ldr r1, =__Vectors ; r1=0x20000000
ldr r2, = |Image$$ER_IROM1$$Length| ; LENGTH
BL copy_myself ; 调用 copy_myself 函数,实现APP自我复制程序到内存中
;LDR SP, =(0x20000000+0x5000)
;BL mymain
ldr pc, =mymain
ENDP
END
散列文件app.sct:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x20000000 0x5000 { ; load region size_region 加载地址,即程序将被加载到的目标内存地址。
ER_IROM1 0x20000000 0x5000 { ; load address = execution address 执行地址,即程序实际运行时的地址。这里与加载地址相同,表示代码将在加载地址处直接执行。
*.o (RESET, +First)
.ANY (+RO)
.ANY (+XO)
.ANY (+RW +ZI)
}
}
示例5-APP自我复制与示例3-使用汇编跳转对比差异:
7.2 程序烧录
Keil中程序烧录配置和示例1-APP无异常向量也相同。
bootloader工程固件可以直接使用Keil中的STLink烧录到STM32中,app工程固件使用Keil中的STLink烧录时会报错,需要使用STM32CubeProgrammer工具(或者其他烧录工具)使用STLink烧录到指定的0x08008000地址上,keil中需要加载散列文件:
从bootloader工程跳转到app工程成功!
8. 示例6-BootLoader根据头部信息复制APP
bootloader把app代码复制到内存中运行,比如芯片Flash太小,外挂一个非XIP类型的Flash存放app代码时,就需要bootloader帮忙把app的代码复制到内存中运行。
原理:
使用uboot中的mkimage.exe工具把app工程编译生成的app.bin文件增加一个头部生成app_with_uboot_header.bin文件
然后把app_with_uboot_header.bin文件通过STM32CubeProgrammer(或其他烧录工具)烧录到Flash的0x08008000地址上
bootloader读取0x08008000头部结构体中的加载地址和代码大小,把app代码复制到内存中
bootloader复制完app代码到内存后跳转到app代码运行
头部格式:
typedef unsigned int __be32;
typedef unsigned char uint8_t;
#define IH_MAGIC 0x27051956 /* Image Magic Number */
#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;
需要用到的ih_load(app的加载地址)和ih_size(app的代码大小)
mkimage.exe工具可以在gitbash中使用,例如将app.bin增加头部生成app_with_uboot_header.bin:
./mkimage.exe -n "stm32F103_app" -a 0x20000000 -e 0x20000008 -d app.bin app_with_uboot_header.bin
1
mkimage.exe用法:
$ ./mkimage.exe
Error: Missing output filename
Usage: /Users/Administrator/Desktop/1/mkimage -l image
-l ==> list image header information
/Users/Administrator/Desktop/1/mkimage [-x] -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_file...] image
-A ==> set architecture to 'arch'
-O ==> set operating system to 'os'
-T ==> set image type to 'type'
-C ==> set compression type 'comp'
-a ==> set load address to 'addr' (hex)
-e ==> set entry point to 'ep' (hex)
-n ==> set image name to 'name'
-d ==> use image data from 'datafile'
-x ==> set XIP (execute in place)
/Users/Administrator/Desktop/1/mkimage [-D dtc_options] [-f fit-image.its|-f auto|-F] [-b <dtb> [-b <dtb>]] [-i <ramdisk.cpio.gz>] fit-image
<dtb> file is used with -f auto, it may occur multiple times.
-D => set all options for device tree compiler
-f => input filename for FIT source
-i => input filename for ramdisk file
Signing / verified boot not supported (CONFIG_FIT_SIGNATURE undefined)
/Users/Administrator/Desktop/1/mkimage -V ==> print version information and exit
Use '-T list' to see a list of available image types
8.1 源码
示例6-BootLoader根据头部信息复制APP的app工程代码与示例5-APP自我复制的app工程相比仅屏蔽了start.s中复制app代码到内存的部分,main.c内容不变。
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
;向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0x20000000+0x5000(初始堆栈指针),第二个条目指向Reset_Handler,指令设置复位处理程序的地址为0x08008008。
__Vectors DCD 0x20000000+0x5000
DCD Reset_Handler ; Reset Handler
AREA |.text|, CODE, READONLY ;定义一个只读代码段,名为 .text
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK] ;导出 Reset_Handler 符号,并标记为弱符号,允许其他地方重定义
IMPORT mymain ;导入外部符号 mymain, copy_myself 和 |Image$$ER_IROM1$$Length|
;IMPORT copy_myself
;IMPORT |Image$$ER_IROM1$$Length|
;adr r0, Reset_Handler ; r0=0x08008000
;bic r0, r0, #0xff ; 清除低8位,得到0x08008000
;ldr r1, =__Vectors ; r1=0x20000000
;ldr r2, = |Image$$ER_IROM1$$Length| ; LENGTH
;BL copy_myself ; 调用 copy_myself 函数,实现APP自我复制程序到内存中
;LDR SP, =(0x20000000+0x5000)
;BL mymain
ldr pc, =mymain
ENDP
END
主要修改在bootloader工程,除了start.s和main.c的修改外,在uart.c和uart.h中还增加了1个打印16进制数据的函数,用于打印地址。
uart.c中增加的内容:
const unsigned char hex_tab[]={'0','1','2','3','4','5','6','7',\
'8','9','a','b','c','d','e','f'};
void puthex(unsigned int val)
{
/* val: 0x12345678 */
int i;
int index;
putstr("0x");
for (i = 7; i >=0 ; i--)
{
index = (val >> (i*4)) & 0xf;
putchar(hex_tab[index]);
}
}
uart.h中增加的内容:
void puthex(unsigned int val);
main.c:
#include "uart.h"
typedef unsigned int __be32;
typedef unsigned char uint8_t;
#define IH_MAGIC 0x27051956 /* Image Magic Number */
#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;
unsigned int be32_to_cpu(unsigned int x)
{
unsigned char *p = (unsigned char *)&x;
unsigned int le;
le = (p[0] << 24) + (p[1] << 16) + (p[2] << 8) + (p[3]);
return le;
}
extern void start_app(unsigned int new_vector);
void delay(int d)
{
while(d--);
}
void copy_app(int *from, int *to, int len)
{
// 从哪里到哪里, 多长 ?
int i;
for (i = 0; i < len/4+1; i++)
{
to = from;
}
}
void relocate_and_start_app(unsigned int pos)
{
image_header_t *head;
unsigned int load;
unsigned int size;
unsigned int new_pos = pos+sizeof(image_header_t);
/* 读出头部 */
head = (image_header_t *)pos;
/* 解析头部 */
load = be32_to_cpu(head->ih_load);
size = be32_to_cpu(head->ih_size);
putstr("load = ");
puthex(load);
putstr("\r\n");
putstr("size = ");
puthex(size);
putstr("\r\n");
/* 把程序复制到RAM */
copy_app((int *)new_pos, (int *)load, size);
/* 跳转执行 */
start_app(new_pos);
}
int mymain()
{
unsigned int app_pos = 0x08008000;
uart_init();
putstr("bootloader\r\n");
/* start app */
relocate_and_start_app(app_pos);
//start_app(new_vector);
return 0;
}
start.s:
PRESERVE8 ;伪指令,用于指示编译器在接下来的代码中保留8位汇编语法。
THUMB ;伪指令,用于指示编译器接下来的代码将以Thumb模式编译。Thumb模式是ARM架构的一种低功耗模式,使用16位指令集。
;向量表通常会放置在内存的最低地址处(通常是0x00000000),并在复位时指向复位处理程序。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
; DCD是一个伪指令,用于定义一个32位的常量(双字)。如果这段代码被启用,它将定义一个向量表,第一个条目为0(初始堆栈指针),第二个条目指向Reset_Handler。
__Vectors DCD 0
DCD Reset_Handler ; Reset Handler
; AREA定义了一个新的段(section),.text表示代码段,CODE表示该段包含可执行代码,READONLY表示该段是只读的。
AREA |.text|, CODE, READONLY
; Reset_Handler是复位处理程序的入口点。
; EXPORT声明了Reset_Handler为全局可见,[WEAK]表示这是一个弱符号,如果没有其他定义,这个定义将被使用。
; IMPORT声明了一个外部符号mymain,表示mymain是一个外部函数,将在链接时被解析。
; LDRSP,=(0x20000000+0x5000):这条指令设置了堆栈指针(SP),指向内存地址0x20000000+0x5000,即0x200005000。这通常是一个RAM区域,用于存储栈数据。
; BLmymain:这条指令通过跳转(BranchandLink)调用mymain函数,mymain函数应该是应用程序的入口点。
; ENDP结束了Reset_Handler过程的定义。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT main
LDR SP, =(0x20000000+0x5000) ; 0x08008000
BL main
ENDP
start_app PROC
EXPORT start_app ;导入start_app函数
; 设置vector基地址为0x08008000,可参考《Cortex_M3权威指南》
ldr r3, =0xE000ED08
str r0, [r3] ; 将r0的内容存储到VTOR寄存器中,r0=0x08008000
ldr sp, [r0] ; 从新向量表读值
ldr r1, [r0, #4] ; 从新向量表+4读值
BX r1 ;这是一条分支并交换指令,根据r1的内容切换到相应的处理器模式(ARM或Thumb)并跳转到r1所指向的地址,r1通常包含一个函数指针,指向要执行的代码,即app
ENDP
; END指令表示整个汇编文件的结束
END
示例6-BootLoader根据头部信息复制APP与示例5-APP自我复制对比差异:
8.2 程序烧录
Keil中程序烧录配置和示例1-APP无异常向量也相同。
bootloader工程固件可以直接使用Keil中的STLink烧录到STM32中,app工程app.bin固件用mkimage.exe生成的app_with_uboot_header.bin固件使用Keil中的STLink烧录时会报错,需要使用STM32CubeProgrammer工具(或者其他烧录工具)使用STLink烧录到指定的0x08008000地址上,keil中需要加载散列文件:
从bootloader工程跳转到app工程成功!
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/studyingdda/article/details/143265494
|
|