灵动微课堂 (第248讲)|mm32-2nd-bootloader技术白皮书(4)——设计实现简单的2nd bootloader
什么是 2nd Bootloader? 引导加载程序(Bootloader)是为了能够正确执行应用程序,在执行应用程序之前对系统进行的一系列初始化操作,并且在完成一系列初始化后,能够引导 CPU 去执行应用程序的程序。 为什么 Bootloader 要在前面加上一个 2nd?因为在执行2nd Bootloader 之前,MCU 中就已经在执行了一个 Bootloader,可以称为 1st Bootloader,这个 Bootloader 的目的是等待片内时钟稳定,确定 MCU 的 BOOT 引脚电平,来了解自己应该执行哪块区域的代码——是用户代码,还是 ISP 代码,如图 1 所示。
图1 1st Bootloader 1st Bootloader 已经在 MCU 出厂的时候,固化在芯片内部,用户无法改动这块区域的内容。为了满足客制化需求,可以在在 1st Bootloader 的基础上,再进入一次 Bootloader,做一些客制化的事情,这就是 2nd Bootloader,如图2所示。
图2 简单的 2nd Bootloader 甚至,可以通过 2nd Bootloader,在 MCU 中存储两套应用程序,选择其一执行,就像电脑的双系统那样,也可以实现定制化的 ISP 功能,OTA 功能,如图3所示:
图3多功能的 2nd Bootloader
2nd Bootloader 的基本需求 当程序下载到 QSPI Flash 之后,为了能够执行 QSPI Flash 中的程序,就需要在 MCU 复位之后,把 QSPI 外设和 GPIO 引脚配置好,QSPI 外设和 GPIO 引脚看上去像是 BSP 相关的事情(确实是与 BSP 相关),能否放在应用程序的 “board_init()” 中呢?当然不行,把 QSPI 外设和 GPIO 引脚配置好这些事情要在进入应用程序之前完成,这样才能正常执行应用程序。 因此, 2nd Bootloader 第一个基本需求就是:初始化 QSPI 外设和使用到的 GPIO 引脚。 初始化好 QSPI 外设和 GPIO 引脚后,第二个问题随之而来,怎么跳转到应用程序中呢? 第二个基本需求就出来了:跳转并执行应用程序。
快速体验 MindSDK 提供了简单的 2nd Bootloader 的样例,方便用户开发: 在 demo_apps/basic 中选择 bootloader_qspi 工程,如图 4 所示。
图4 选择 demo_apps/basic/bootloader_qspi 工程 编译并下载到 PLUS-F5270 中即可使用,十分方便。 但本文将继续讲解 2nd Bootloader 的实现方式,以了解 2nd Bootloader 的工作原理,方便继续开发 2nd Bootloader。
软件实现 初始化 QSPI 外设和 GPIO 引脚 PLUS-F5270 使用到的 QSPI Flash 引脚如表 1 所示。
表1 PLUS-F5270使用的QSPI引脚 初始化系统时钟, QSPI & GPIO 外设时钟: void BOARD_InitBootClocks(void)
{
CLOCK_ResetToDefault();
/* QSPI. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_QSPI, true);
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_QSPI);
/* GPIOA. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOA, true);
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOA);
/* GPIOB. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOB, true);
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOB);
/* GPIOG. */
RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOG, true);
RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOG);
}
初始化 GPIO 引脚: void BOARD_InitPins(void)
{
GPIO_Init_Type gpio_init;
/* PB10 - QSPI_CS. */
gpio_init.Pins = GPIO_PIN_10;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio_init);
GPIO_PinAFConf(GPIOB, gpio_init.Pins, GPIO_AF_10);
/* PG7 - QSPI_SCK. */
gpio_init.Pins = GPIO_PIN_7;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOG, &gpio_init);
GPIO_PinAFConf(GPIOG, gpio_init.Pins, GPIO_AF_10);
/* PG6 - QSPI_D0. */
gpio_init.Pins = GPIO_PIN_6;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOG, &gpio_init);
GPIO_PinAFConf(GPIOG, gpio_init.Pins, GPIO_AF_10);
/* PA3 - QSPI_D1. */
gpio_init.Pins = GPIO_PIN_3;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init);
GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_10);
/* PB3 - QSPI_D2. */
gpio_init.Pins = GPIO_PIN_3;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio_init);
GPIO_PinAFConf(GPIOB, gpio_init.Pins, GPIO_AF_10);
/* PG8 - QSPI_D3. */
gpio_init.Pins = GPIO_PIN_8;
gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
gpio_init.Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOG, &gpio_init);
GPIO_PinAFConf(GPIOG, gpio_init.Pins, GPIO_AF_10);
}
初始化 QSPI 直接读模式:这里我们采用四线快读的方式访问 QSPI Flash。 void BOARD_InitExternalFlash(void)
{
/* init qspi hardware. */
QSPI_Init_Type qspi_init;
qspi_init.SckDiv = BOARD_QSPI_SCK_DIV;
qspi_init.CsHighLevelCycles = BOARD_QSPI_CS_HIGH_LEVEL_TIME;
qspi_init.RxSampleDelay = BOARD_QSPI_RX_DELAY_CYCLES;
qspi_init.SpiMode = QSPI_SpiMode_3; /* suggest using SPI Mode 3. */
/* init qspi. */
QSPI_Init(QSPI, &qspi_init);
/* enable qspi direct read mode. */
QSPI_DirectXferConf_Type direct_conf;
direct_conf.CmdBusWidth = BOARD_EXT_FLASH_CMD_BUS_WIDTH;
direct_conf.CmdValue = BOARD_EXT_FLASH_CMD_VALUE;
direct_conf.AddrBusWidth = BOARD_EXT_FLASH_ADDR_BUS_WIDTH;
direct_conf.AddrWordWidth = BOARD_EXT_FLASH_ADDR_WORD_WIDTH;
direct_conf.AltBusWidth = QSPI_BusWidth_None;
direct_conf.DummyCycles = BOARD_EXT_FLASH_DUMMY_CYCLES;
direct_conf.DataBusWidth = BOARD_EXT_FLASH_DATA_BUS_WIDTH;
QSPI_EnableDirectRead(QSPI, &direct_conf);
}
代码中涉及到的宏定义: /* APP BASE. */
#define BOARD_APP_BASE 0x90000000
/* QSPI. */
#define BOARD_QSPI_SCK_DIV 4u
#define BOARD_QSPI_RX_DELAY_CYCLES 1u
#define BOARD_QSPI_CS_HIGH_LEVEL_TIME 2u
/* cmd. */
#define BOARD_EXT_FLASH_CMD_BUS_WIDTH QSPI_BusWidth_1b
#define BOARD_EXT_FLASH_CMD_VALUE 0x6B /* fast read quad. */
#define BOARD_EXT_FLASH_ADDR_BUS_WIDTH QSPI_BusWidth_1b
#define BOARD_EXT_FLASH_ADDR_WORD_WIDTH QSPI_WordWidth_24b
#define BOARD_EXT_FLASH_DUMMY_CYCLES 8u
#define BOARD_EXT_FLASH_DATA_BUS_WIDTH QSPI_BusWidth_4b
QSPI Flash 的地址映射空间为 0x90000000 ~ 0x9FFFFFFF,共 256MB 大小,这段信息来自微控制器的 UM 地址映射章节,如图 5 所示。
图5 QSPI Flash 在 MCU中的地址映射 跳转执行应用程序 在跳转执行应用程序之前,先看下应用程序的二进制代码长什么样。 以 MindSDK 的 hello_world 工程为例,生成一份程序的二进制文件,打开 MDK 工程,点击魔术棒(Options for Target...),点击 User 列表,如图x所示,在指定位置(红框中的 User Command)加入下面这句话,并在前面打上对勾: fromelf.exe --bin -o "@L.bin" "#L" 然后编译工程,就能在工程文件所在的目录下找到生成的 bin 文件。
图6 生成二进制文件 可以使用如 VS Code 等软件查看这个二进制文件的内容,如图5所示:
图7 应用程序的二进制片段 看下应用程序的开头内容:开头是 “00 C0 01 30” 的数据,它是 0x3001C000 的小端模式表示,而 0x3001C000 是应用程序的 SP (堆栈指针)初始化值。后面紧接着的的 “09 17 00 08” 则代表 0x08001709,Reset_Handler() 的指针。前面一段内容,就是中断向量表,通过中断向量表可以知道这段应用程序的 SP 起始地址,Reset_Handler() 函数的指针,而这两个数据,再加上存放这些数据的起始地址,是能够正确执行应用程序的关键。
获取新的 SP 指针和 Reset_Handler() 函数指针:uint32_t * vtor;
void jump_to_app(uint32_t app_base)
{
vtor = (uint32_t *)app_base;
......
}
vtor[0] 就是 SP 指针,vtor[1] 就是 Reset_Handler() 函数指针。 可以发现,vtor 变量是以全局变量的方式进行存储的,而并不是在函数中定义变量(局部变量)保存,其原因是修改 MSP 和 PSP 指针后,局部变量会发生意外的变化,导致后面无法正确跳转程序。
修改中断向量表: void jump_to_app(uint32_t app_base)
{
......
__disable_irq();
SCB->VTOR = app_base;
__ISB();
__DSB();
......
}
修改中断向量表是为了保证应用程序的中断程序能够正确执行,可以做一个实验,将修改中断向量表的代码注释掉,编译下载这个 2nd Bootloader,发现应用程序开始是能正常执行的,但是呢,一旦触发了中断,程序就不知道跑到一个莫名的位置上了,这个莫名的位置就是 2nd Bootloader 的中断程序。 为了保证修改中断向量表,以及后面修改 MSP 和 PSP 指针时不会有中断干扰,需手动关闭所有中断;使用 __disable_irq() 关闭所有中断。 Cortex-M0 的中断向量表修改方法和 Cortex-M3,M4,Star-MC1 等内核不同,它没有 VTOR 寄存器,而是将应用程序的中断向量表复制到 SRAM 中,通过修改某一个寄存器的字段值(非 Cortex-M0 内核寄存器,需参考具体芯片的手册进行修改),将绝对地址 0x00000000 起始的映射区间(不同 MCU 的映射区间大小可能不同)由片内 Flash 起始地址变为 SRAM 的起始地址,而绝对地址 0x00000000 就是 Cortex-M0 内核芯片的中断向量表起始地址。 修改 MSP & PSP:void jump_to_app(uint32_t app_base)
{
......
__set_MSP(vtor[0]);
__set_PSP(vtor[0]);
......
}
当这两个指针改变后,再读取函数中的局部变量,会发现其值发生了变化,这是由于局部变量是通过 SP 指针加相对地址进行访问的,当SP 指针发生变化后,局部变量的实际存储位置也会发生变化,修改后,再访问局部变量的时候,由于还是通过堆栈指针加相对地址进行访问,则局部变量的绝对地址发生了变化,访问到的值就不再是实际值。 跳转执行应用程序: void jump_to_app(uint32_t app_base)
{
......
__enable_irq();
((void(*)(void))vtor[1])();
......
}
在跳转执行应用程序前,我们需要将前面已经关闭的中断再次打开,使其在跳转应用程序后,中断函数能够正确执行;使用 __enable_irq() 打开所有中断。 跳转执行应用程序的方法就是执行应用程序的 Reset_Handler() ,Reset_handler() 的指针就存储在中断向量表的第二个四字节地址中,因此我们读取vtor[1]的值,并将其强制转换成指向函数的指针,并执行这个函数即可实现应用程序的跳转。 跳转程序完整代码: uint32_t * vtor;
void jump_to_app(uint32_t app_base)
{
vtor = (uint32_t *)app_base;
__disable_irq();
SCB->VTOR = app_base;
__ISB();
__DSB();
__set_MSP(vtor[0]);
__set_PSP(vtor[0]);
__enable_irq();
((void(*)(void))vtor[1])();
}
初始化板子相关的事情后,就可以使用这个函数进行跳转程序: int main(void)
{
BOARD_Init();
/* jump to app. */
jump_to_app(BOARD_APP_BASE);
while (1)
{
}
}
至此,简单的 2nd Bootloader 就已经完成了,现在,有一个能下载程序到 QSPI Flash 的下载算法,有一个能跳转执行应用程序的 2nd Bootloader,至于验证,还需要等下一章编译出一个存储在 QSPI Flash 上的应用程序后,才能验证。
结语 2nd Bootloader 本质上其实就是一个应用程序,是一段由 1st Bootloader 引导执行的应用程序。它的好处是可以根据客户需求进行定制,自由修改。 本文只是先实现一个简单的 2nd Bootloader,其作用只是帮助 MCU 去执行存储在 QSPI Flash 中的应用程序。 本文使用的访问 QSPI Flash 方法为四线快读模式,其实某些 QSPI Flash 还支持 QPI 模式,发送 0x38(不同 QSPI Flash 的进入方法可能不同) 指令后,即可进入,发送 0xFF 后即可退出;进入 QPI 模式后,访问 QSPI Flash 的所有阶段均为 4 线模式,加快了访问 QSPI Flash 的速度,这一步操作我们也可以在 2nd Bootloader 中实现。不过这会出现一个问题:需要在下载算法中判断 QSPI Flash 是否进入了 QPI 模式,该用何种方式下载程序,这会增加下载算法的代码量。
|