PX4 BootLoader原理简析
一、引言
看完本文,您会了解PX4 BootLoader中几个重要文件以及作用,掌握PX4 BootLoader的基本功能及原理。
PX4 BootLoader是一个涉及多系列主板的成熟BootLoader,它涉及非常多的文件和库。本节我们将挑选几个比较的重要文件进行介绍。这些文件名称及其简要作用如下:
文件名 | 主要作用 |
---|---|
Makefile | 编译主文件。包含PX系列各种设备的编译信息。 |
hw_config.h | 主配置文件。包含PX系列各种设备的配置信息。 |
main_f1.c | 主函数文件。不同系列的芯片有不同的主函数文件。其中,f1代表stm32f1系列。文件夹中还有main_f3.c、main_f4.c、main_f7.c。分别代表stm32f3系列的主函数文件、stm32f4系列的主函数文件和stm32f7系列的主函数文件。 |
bl.c | 公共函数文件。包含PX系列各种设备都会使用的公共函数。 |
二、Makefile文件简析
Makefile文件主要用于配置最终固件的编译。
首先,PX系列不同设备所使用的stm32芯片各不相同。而Makefile文件可以帮助我们筛选出使用stm32f1、stm32f3、stm32f4、stm32f7系列的PX设备。这样,我们可以很快的找到目标设备所对应的配置文件和主函数文件。
我们可以观察PX系列在编译时所链接的文件名称,即LINKER_FILE所对应的文件名。它揭示了该款设备使用的是什么系列的芯片。
其次,PX4有多种多样的设备。默认情况下,使用make命令会编译所有设备的固件,编译时间较长。因此,我们在确定使用哪个PX系列的设备后,可以通过Makefile文件进行调整。从而只编译该设备的固件,进而缩短编译时间,提高工作效率。具体来说,我们可以删除导出目录的对应设备名称,同时删除该设备的编译命令,即可在编译时取消编译该设备的固件。
三、hw_config.h文件简析
hw_config.h文件主要包含PX系列各类设备的配置信息。我们在Makefile中挑选好目标设备后,可以在这里查看该设备的配置文件。
该文件由上到下可以分为几部分。首先是接口信息和配置信息部分、第二是LED灯部分、第三是USART配置部分、第四是强制启用BootLoader部分、第五是flash相关配置部分。
一般情况下,PX4的开发者们已经对各种系列的设备做了完善的配置,我们并不需要修改hw.config.h配置文件中的内容。但是,如果我们有更高级的需求,就需要我们对照所要部署平台的stm32芯片使用手册来对照相应的芯片信息和管脚信息,从而实现高级的自定义修改。PX4 BootLoader中不同芯片的相关定义文件十分完善,我们可以善用全局搜索功能找到对应系列的定义文件,从而确认我们可以怎么修改。
下面以PX4_PIO_V1设备为例,简要介绍一下配置文件中各个设置的功能。以下为配置信息及其注释:
#elif defined(TARGET_HW_PX4_PIO_V1) || defined(TARGET_HW_PX4_PIO_V2)
# define APP_LOAD_ADDRESS 0x08001000 // 程序固件的烧录地址。
# define APP_SIZE_MAX 0xf000 // 程序固件的最大大小。如果不使用PX的BootLoader烧录程序固件的话可以不修改。
# define BOOTLOADER_DELAY 200 // BootLoader在200毫秒后自行启动。如果想执行BootLoader中的命令,最好修改得大一点。或者在BootLoader中重置该值。
# define BOARD_PIO
# define INTERFACE_USB 0 // 使能USB接口,此处为0表示不开启USB端口。
# define INTERFACE_USART 1 // 使能USART接口,此处为1表示开启USART端口。
# define USBDEVICESTRING "" // USB设备描述符,由于不开启USB端口,设备描述符可为空。
# define USBPRODUCTID -1 // USB产品ID,由于不开启USB端口,产品ID为-1。
# define OSC_FREQ 24 // 晶振频率。
# define BOARD_PIN_LED_ACTIVITY GPIO14 // 定义LED灯引脚。
# define BOARD_PIN_LED_BOOTLOADER GPIO15 // 定义LED灯引脚。
# define BOARD_PORT_LEDS GPIOB // 定义LED灯引脚。
# define BOARD_CLOCK_LEDS_REGISTER RCC_APB2ENR // 使能LED灯相关寄存器引脚。
# define BOARD_CLOCK_LEDS RCC_APB2ENR_IOPBEN // 使能LED灯相关IO引脚。
# define BOARD_LED_ON gpio_clear // 重定义开启LED灯。
# define BOARD_LED_OFF gpio_set // 重定义关闭LED灯。
# define BOARD_USART USART2 // 定义USART协议。
# define BOARD_USART_CLOCK_REGISTER RCC_APB1ENR // 使能USART相关寄存器的引脚。
# define BOARD_USART_CLOCK_BIT RCC_APB1ENR_USART2EN // 使能USART2相关比特率的引脚。
# define BOARD_PORT_USART GPIOA // 定义USART引脚。
# define BOARD_PIN_TX GPIO_USART2_TX // 定义USART发送引脚。
# define BOARD_PIN_RX GPIO_USART2_RX // 定义USART接收引脚。
# define BOARD_USART_PIN_CLOCK_REGISTER RCC_APB2ENR
# define BOARD_USART_PIN_CLOCK_BIT RCC_APB2ENR_IOPAEN
# define BOARD_FORCE_BL_PIN GPIO5 // 强制进入BootLoader的一些配置。一般不会用到,不做介绍
# define BOARD_FORCE_BL_PORT GPIOB
# define BOARD_FORCE_BL_CLOCK_REGISTER RCC_APB2ENR
# define BOARD_FORCE_BL_CLOCK_BIT RCC_APB2ENR_IOPBEN
# define BOARD_FORCE_BL_PULL GPIO_CNF_INPUT_FLOAT // depend on external pull
# define BOARD_FORCE_BL_VALUE BOARD_FORCE_BL_PIN
# define BOARD_FLASH_SECTORS 60 // 板载闪存扇区数。
# define BOARD_TYPE 10 // 主板类型标识符。
# define FLASH_SECTOR_SIZE 0x400 // 板载闪存扇区大小。
# define NO_OTP_SN_CHIP 1 // 有无OTP芯片。
同时,hw_config.h文件末尾还有一些所有设备都会使用的公共配置信息。其中最重要就是定义USART所使用的波特率。可以观察到,没有自行定义时波特时默认是115200。
// 定义USART所使用的波特率
#if defined(OVERRIDE_USART_BAUDRATE)
# define USART_BAUDRATE OVERRIDE_USART_BAUDRATE
#else
# define USART_BAUDRATE 115200
#endif
四、main_f1.c文件简析
main_f1.c是主函数文件,它是stm32f1芯片启动时的主函数。其中,f1代表stm32f1系列。文件夹中还有main_f3.c、main_f4.c、main_f7.c,分别代表stm32f3系列的主函数文件、stm32f4系列的主函数文件和stm32f7系列的主函数文件。
如果我们选择的开发板使用的是stmn32f1系列的芯片,那么开发板在上电后BootLoader程序会从main_f1.c最下面的main函数开始执行。因此,我们将以main_f1.c文件中的main函数为例对main函数做详细介绍。下面是对该函数详细的注释:
int
main(void)
{
unsigned timeout = 0;
/* 主板初始化函数。主要配置LED灯光、USART通信接口和I2C通信接口相关的时钟、引脚。*/
board_init();
/*判断是否启用BootLoader等待。如果启用,程序会先进入BootLoader,同时计时器启动,等待通过USART通信接口或USB通信接口与用户交互。如果在执行完一条命令后到达等待时间,则会离开BootLoader,开始执行用户程序。一般来说,USART通信接口或USB通信接口两者总有一个有定义。因此BootLoader等待在大多数情况下都是有效的。*/
#if defined(INTERFACE_USART) || defined (INTERFACE_USB)
/* XXX sniff for a USART connection to decide whether to wait in the bootloader? */
timeout = BOOTLOADER_DELAY;
#endif
/* Bootloader不支持stm32f1系列的开发板的I2C接口。因此如果遇到想要启用I2C接口,程序会报错。*/
#ifdef INTERFACE_I2C
# error I2C bootloader detection logic not implemented
#endif
/* if the app left a cookie saying we should wait, then wait */
if (should_wait()) {
timeout = BOOTLOADER_DELAY;
}
/* 如果启用了强制等待BootLoader功能并且该引脚也有信号,那么不会再自动离开BootLoader。 */
#ifdef BOARD_FORCE_BL_PIN
/* if the force-BL pin state matches the state of the pin, wait in the bootloader forever */
if (BOARD_FORCE_BL_VALUE == gpio_get(BOARD_FORCE_BL_PORT, BOARD_FORCE_BL_PIN)) {
timeout = 0xffffffff;
}
#endif
/* look for the magic wait-in-bootloader value in backup register zero */
/* if we aren't expected to wait in the bootloader, try to boot immediately */
/* 如果在hw_config.h中定义的BOOTLOADER_DELAY为0,即没有BootLoader等待时间,那么尝试直接跳转到用户程序。跳转成功会直接执行用户程序,跳转失败会进入BootLoader并一直待在里面。 */
if (timeout == 0) {
/* try to boot immediately */
jump_to_app();
/* if we returned, there is no app; go to the bootloader and stay there */
timeout = 0;
}
/* 初始化BootLoader需要用到的时钟。如果定义了USB接口,那么输出的时钟为48mhz。反之,输出的时钟为24mhz。 */
/* configure the clock for bootloader activity */
clock_init();
/* 初始化主板上的USART通信接口。 */
/* start the interface */
cinit(BOARD_INTERFACE_CONFIG, USART);
/* 我觉得英文注释就够了,对......吧? */
while (1) {
/* run the bootloader, possibly coming back after the timeout */
bootloader(timeout);
/* look to see if we can boot the app */
jump_to_app();
/* boot failed; stay in the bootloader forever next time */
timeout = 0;
}
}
五、bl.c文件简析
bl.c是公共函数文件,它包含PX系列各种设备都会使用的公共函数。在之前对main_f1.c文件的分析中我们已经发现,main_f1.c文件中最重要的函数是main()函数,而main()函数中比较最重要的函数又是jump_to_app()函数和bootloader()函数。这两个函数其实就是来自bl.c文件。main_f1.c文件会通过引用bl.h从而使用bl.c文件中的函数。因此,本节主要介绍jump_to_app()函数和bootloader()函数。
(一)jump_to_app()函数简析
我们首先介绍jump_to_app()函数。该函数的主要作用如下:
从逻辑图中我们可以看到,该函数主要有两个功能:一是校验固件,二是跳转到固件。其中,校验固件是否有效又分为两步:校验固件是否烧写完成和校验固件的跳转地址是否有效。
1.校验固件功能简析
我们先来介绍它是怎么校验固件是否有效的。PX4的BootLoader烧写固件时,会先将除BootLoader外的部分擦写为0xffffffff(处理的最小单位是32位,4个字节)。然后,在烧写固件时,会最后烧写固件前32位的值。因此,如果固件成功完成烧写,那么固件的前32位就不会是0xffffffff。反之,如果固件没有完成烧写,那么固件的前32位就是0xffffffff。jump_to_app()函数也就依此来判断固件是否烧写完成。
2.跳转到固件功能简析
然后,我们来介绍它是怎么校验固件的跳转地址是否有效的。首先我们要明白,APP_LOAD_ADDRESS是固件的烧写地址,即固件烧写时从这个地址开始烧写。但固件开始执行的地址并不是APP_LOAD_ADDRESS,即固件不是从烧写地址开始执行(有data segment和stack segment,汇编知识)。我们约定,数据段的第一个32位,即固件最开始的32位,存放堆栈段地址;数据段的第二个32位,即固件的32位-64位,存放固件执行地址。从下面的例图中我们可以看到,固件的前32位为0x200200000(小端序),即使用的堆栈段地址为0x20020000;固件的32位-64位为0x08001515,即固件开始执行时的地址为0x08001515。
因此,jump_to_app()函数会依据固件的跳转地址,即固件的32位-64位判断这个固件是否能成功跳转。只有这个地址在APP_LOAD_ADDRESS(跳转地址不可能跑到固件烧写地址之前)到APP_LOAD_ADDRESS+board_info.fw_size(boar_info.fw_size是hw_config.h中APP_SIZE_MAX,代表固件的最大大小)之间时,该固件才可以成功跳转,即固件有效。
3.函数详细注释
最后是对jump_to_app()函数的详细注释:
void
jump_to_app() // 跳转函数
{
// unsigned int 32位 代表四个字节,即为 int 类型;
// 0x 0800 4000 4*8=32位
const uint32_t *app_base = (const uint32_t *)APP_LOAD_ADDRESS; // 0x08004000 飞控固件的起始地址,定义在hw_config.h中
const uint32_t *vec_base = (const uint32_t *)app_base;
/*
* We refuse to program the first word of the app until the upload is marked
* complete by the host. So if it's not 0xffffffff, we should try booting it.
*/
//1. 根据飞控固件的烧写约定,检测固件是否有效
// 飞控固件烧写时,我们特意约定最后烧写固件的首地址(飞控固件使用的堆栈首地址)。
//若固件首地址为0xffffffff,表明固件烧写未完成(或固件无效)无法跳转,函数直接返回 */
if (app_base[0] == 0xffffffff) {
return;
}
/* 关于安全启动的代码,可以不用理会 */
#ifdef SECURE_BTL_ENABLED
......
#endif
/*
* The second word of the app is the entrypoint; it must point within the
* flash area (or we have a bad flash).
*/
//飞控固件的第二个32位代表飞控固件的入口地址。
//若此地址未在固件指定的地址范围内,则表示固件有问题无法跳转,函数直接返回
if (app_base[1] < APP_LOAD_ADDRESS) {
return;
}
// //烧写内容太大返回
if (app_base[1] >= (APP_LOAD_ADDRESS + board_info.fw_size)) {
return;
}
/* 2. 现在飞控固件有效,反向初始化以便把外设控制权交给飞控固件 */
/* just for paranoia's sake */
arch_flash_lock();
/* kill the systick interrupt */
arch_systic_deinit();
/* deinitialise the interface */
cfini();
/* reset the clock */
clock_deinit();
/* deinitialise the board */
board_deinit();
/* 3. 更改向量表地址至飞控固件的向量表 */
/* SCB_VTOR:寄存器,向量表偏移地址,地址0xE000ED08 */
/* APP_LOAD_ADDRESS:飞控固件起始地址。0x08004000(主控FMU),0x08001000(IO协处理器) */
// SCB_VTOR = APP_LOAD_ADDRESS;
/* switch exception handlers to the application */
arch_setvtor((uint32_t)vec_base);
/* extract the stack and entrypoint from the app vector table and go */
///* 4. 重置堆栈并跳转至飞控固件 */
do_jump(app_base[0], app_base[1]);
}
(二)bootloader()函数简析
接着,我们介绍bootloader()函数。该函数的主要功能如下:
还记得我们在main_f1.c文件中调用bootloader时传递的timeout参数吗?这里就派上了用场!在刚进入bootloader()函数时,它会启动一个定时器,然后尝试从USART、USB、I2C等通信端口读取数据并执行对应的命令。每次执行完命令后,它都会对比定时器和timeout的值。一旦定时器的值超过了timeout,bootloader()函数就会强制返回。
我们与BootLoader交互均采用16进制的字节数据。在bl.c文件头部可以看到这些字节数据所代表的意思。我们向BootLoader发送命令时结尾必须加上0x20,即PROTO_EOC,代表命令输入结束。
下面,我们分别来介绍bootloader()函数的各个功能。
1.同步功能简析
首先是同步功能,其命令为:0x21 0x20。这个功能主要是指定bootload()函数输出数据时所使用的通信端口。在main_f1.c文件的main()函数中,我们已经初始化了很多通信端口(本文中举例的设备型号只初始化了USART端口)。那么,bootloader()是怎么知道我们通信使用的具体是USB端口还是USART端口呢?
当我们通过某一接口连接到主板,进入BootLoader后,BootLoader会监听设备上的所有通信端口。此时,我们可以通过某一端口向BootLoader发送0x21和0x20两个字节的数据(0x21是PROTO_GET_SYNC代表同步命令;0x20是PROTO_EOC,代表结束命令,表示本次数据传送完毕。)。然后,bootloader()函数就会使用我们发送同步命令时所使用的端口与我们进行通信。
因此,我们也可以看出,同步功能是非常重要的。我们在进入BootLoader后一定要先发送0x21和0x20命令,这样我们才能与BootLoader正确的通信。
**假如我们在刚进入BootLoader时不发送同步命令,而是直接发送其他命令,那么我们很有可能无法收到BootLoader给我们的响应数据。**但是,PX4 BootLoader在设计时已经考虑了这一情况。事实上,我们在刚进入BootLoader后,无论发送的是什么数据,PX4的BootLoader都会记录接收到数据的端口。下一次直接使用该端口与我们进行通信。因此,如果我们不执行同步命令,直接执行其他命令,很有可能会出现第一次执行命令后没有响应,但第二次执行命令及以后成功获取到响应的情况。
在与BootLoader通信时我们需要勾选上16进制发送和16进制接收。可以观察到,我们向BootLoader发送了0x21和0x20,BootLoader向我们返回了0x12和0x10。结合上面的命令字节对照表,我们可以知道这是PROTO_INSYNC和PROTO_OK,即状态同步字节和“ok”字节。
// sync
//
// command: GET_SYNC/EOC
// reply: INSYNC/OK
//
case PROTO_GET_SYNC: // 若为同步命令PROTO_GET_SYNC
/* expect EOC */
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
SET_BL_FIRST_STATE(STATE_PROTO_GET_SYNC);
break;
2.获取设备ID功能简析
第二是获取设备ID功能,其命令为:0x22 <选项> 0x20。通过这个功能,我们可以获取到设备的BootLoader修订号、主板型号、主板修订号、固件最大大小、保留扇区中的内容。具体来说,我们需要先发送0x22表明我们即将要获取设备信息。紧接着,我们需要选择获取什么设备信息。各个选项如下:设备的BootLoader修订号(0x1)、主板型号(0x2)、主板修订号(0x3)、固件最大大小(0x4)、保留扇区中的内容(0x5)。最后,我们需要补上结束命令0x20。
// get device info
//
// command: GET_DEVICE/<arg:1>/EOC
// BL_REV reply: <revision:4>/INSYNC/EOC
// BOARD_ID reply: <board type:4>/INSYNC/EOC
// BOARD_REV reply: <board rev:4>/INSYNC/EOC
// FW_SIZE reply: <firmware size:4>/INSYNC/EOC
// VEC_AREA reply <vectors 7-10:16>/INSYNC/EOC
// bad arg reply: INSYNC/INVALID
//
case PROTO_GET_DEVICE: // 若为获取设备ID命令PROTO_GET_DEVICE 输入命令 0x22 、 继续输入 1 2 3 4 5
/* expect arg then EOC */
arg = cin_wait(1000); // 1个字节
if (arg < 0) {
goto cmd_bad;
}
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
switch (arg) {
case PROTO_DEVICE_BL_REV: // 1
// uart_cout(buf, len);
cout((uint8_t *)&bl_proto_rev, sizeof(bl_proto_rev));
break;
case PROTO_DEVICE_BOARD_ID:
cout((uint8_t *)&board_info.board_type, sizeof(board_info.board_type));
break;
case PROTO_DEVICE_BOARD_REV:
cout((uint8_t *)&board_info.board_rev, sizeof(board_info.board_rev));
break;
case PROTO_DEVICE_FW_SIZE:
cout((uint8_t *)&board_info.fw_size, sizeof(board_info.fw_size));
break;
case PROTO_DEVICE_VEC_AREA:
for (unsigned p = 7; p <= 10; p++) {
uint32_t bytes = flash_func_read_word(p * 4);
cout((uint8_t *)&bytes, sizeof(bytes));
}
break;
default:
goto cmd_bad;
}
SET_BL_STATE(STATE_PROTO_GET_DEVICE);
break;
3.擦写Flash准备烧写固件功能简析
第三是擦写Flash准备烧写固件功能,其命令为:0x23 0x20。正如之间校验固件功能简析中所提到的,使用BootLoader烧录固件时,需要先使用擦写Flash命令将固件所在区域重置为0xffffffff。具体来说,该功能会先检查配置文件中是否允许擦写固件区域,然后配置led灯使其常亮,接着解锁固件区域并进行擦写,然后熄灭led灯,通过读取固件区域中的数据和0xffffffff进行对比校验,校验完成后将led灯设置为闪烁。
// erase and prepare for programming
//
// command: ERASE/EOC
// success reply: INSYNC/OK
// erase failure: INSYNC/FAILURE
//
case PROTO_CHIP_ERASE: // 若为擦除flash与准备烧写飞控固件指令PROTO_CHIP_ERASE
/* expect EOC */
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
#if defined(TARGET_HW_PX4_FMU_V4) || defined(TARGET_HW_UVIFY_CORE)
if (check_silicon()) {
goto bad_silicon;
}
#endif
if ((bl_state & STATE_ALLOWS_ERASE) != STATE_ALLOWS_ERASE) {
goto cmd_bad;
}
// clear the bootloader LED while erasing - it stops blinking at random
// and that's confusing
led_set(LED_ON);
// erase all sectors
arch_flash_unlock();
for (int i = 0; flash_func_sector_size(i) != 0; i++) {
flash_func_erase_sector(i); // 擦除flash
}
// disable the LED while verifying the erase
led_set(LED_OFF);
// verify the erase
for (address = 0; address < board_info.fw_size; address += 4)
if (flash_func_read_word(address) != 0xffffffff) {
goto cmd_fail;
}
address = 0;
SET_BL_STATE(STATE_PROTO_CHIP_ERASE);
// resume blinking
led_set(LED_BLINK);
break;
4.烧写固件功能简析
第四是烧写固件功能,其命令为:0x27 <固件大小> <固件> <0x20>。我们需要先发送烧写固件命令0x27,然后发送固件长度,紧接着发送固件文件,最后以终止命令结束。
具体来说,它首先会检查固件长度是否符合要求,小于0、不为整数、大于最大固件长度均不符合要求。然后,将接收到的固件放入缓冲区(缓冲区默认为256字节,大小可能需要根据自编译的固件大小进行调整,如果使用PX官方固件不存在这个问题)。最后将固件写入闪存,并通过对比读取出的闪存和缓冲区中的数据来进行校验。
正如我们之前在校验固件功能中介绍的,烧写需要先保存固件第一个32位的值,最后再进行写入,为检查固件是否烧写完成提供依据。因此,执行完该功能后,固件实际并没有烧写完毕,固件中第一个32位的值还没有进行烧写。只有在我们执行“跳转到固件”功能后,固件中第一个32位的值才会正常烧写。因此,这个烧写固件的功能实际上是烧写不完全的。
// program bytes at current address
//
// command: PROG_MULTI/<len:1>/<data:len>/EOC
// success reply: INSYNC/OK
// invalid reply: INSYNC/INVALID
// readback failure: INSYNC/FAILURE
//
case PROTO_PROG_MULTI: // program bytes 若为flash烧写命令PROTO_PROG_MULTI
// expect count
arg = cin_wait(50); // 输入 arg 收到的第一个参数(长度arg)
if (arg < 0) {
goto cmd_bad;
}
// sanity-check arguments
if (arg % 4) { // //必须是整数
goto cmd_bad;
}
// 若当前编写地址(address)+第一个参数(长度arg)超出飞控固件最大范围board_info.fw_size
if ((address + arg) > board_info.fw_size) {
goto cmd_bad;
}
// 若收到的第一个参数(长度arg)超出缓冲区长度,按照错误命令处理
if ((unsigned int)arg > sizeof(flash_buffer.c)) {
goto cmd_bad;
}
// 根据长度arg逐字节接收数据,并存放在缓冲区flash_buffer中;若其中任何1个字节超时1s,按照错误命令处理,
// flash_buffer:共同体,代表接收缓冲区(256字节或32字)
for (int i = 0; i < arg; i++) {
c = cin_wait(1000); // 一直输入
if (c < 0) {
goto cmd_bad;
}
flash_buffer.c[i] = c; //flash 空间 c[256]
}
// 接收完有效数据后,若200ms内未接收到命令终止字(PROTO_EOC),按照错误命令处理,
if (!wait_for_eoc(200)) {
goto cmd_bad;
}
// 若当前编写地址address为0,则将烧写缓冲区flash_buffer首个32位保存在变量first_word中,并将缓冲区首改为0xFFFFFFFF */
if (address == 0) {
#if defined(TARGET_HW_PX4_FMU_V4) || defined(TARGET_HW_UVIFY_CORE)
if (check_silicon()) {
goto bad_silicon;
}
#endif
// save the first word and don't program it until everything else is done
first_word = flash_buffer.w[0];
// replace first word with bits we can overwrite later
flash_buffer.w[0] = 0xffffffff;
}
// 逐字烧写flash地址,若写入与读出不一致,进行命令失败处理,回告结构:(PROTO_INSYNC+PROTO_FAILED) */
arg /= 4;
for (int i = 0; i < arg; i++) {
// program the word
flash_func_write_word(address, flash_buffer.w[i]);
// do immediate read-back verify
// //判断写的和读取的是否一致
if (flash_func_read_word(address) != flash_buffer.w[i]) { // 回读验证
goto cmd_fail;
}
address += 4;
}
SET_BL_STATE(STATE_PROTO_PROG_MULTI);
break;
5.CRC校验功能简析
第五是CRC校验功能,其命令为:0x29 0x20。该功能会计算固件区域的CRC值并返回。
// fetch CRC of the entire flash area
//
// command: GET_CRC/EOC
// reply: <crc:4>/INSYNC/OK
//
case PROTO_GET_CRC: // 若为CRC32校验命令PROTO_GET_CRC
// expect EOC
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
// compute CRC of the programmed area
uint32_t sum = 0;
for (unsigned p = 0; p < board_info.fw_size; p += 4) {
uint32_t bytes;
if ((p == 0) && (first_word != 0xffffffff)) {
bytes = first_word;
} else {
bytes = flash_func_read_word(p);
}
sum = crc32((uint8_t *)&bytes, sizeof(bytes), sum);
}
cout_word(sum);
SET_BL_STATE(STATE_PROTO_GET_CRC);
break;
6.读取OTP区域功能简析
第六是读取OTP区域功能,其命令为:0x2a <地址> 0x20。该功能可以读取OTP指定地址的数据。
// read a word from the OTP
//
// command: GET_OTP/<addr:4>/EOC
// reply: <value:4>/INSYNC/OK
case PROTO_GET_OTP: // 若为读取OTP区域命令PROTO_GET_OTP
// expect argument
{
uint32_t index = 0;
if (cin_word(&index, 100)) {
goto cmd_bad;
}
// expect EOC
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
cout_word(flash_func_read_otp(index));
}
break;
7.获取MCU的UDID功能简析
第七是获取MCU的UDID功能,其命令为:0x2b <地址> 0x20。该功能可以获取在指定区域存储的芯片的UDID。芯片UDID所在地址需要用户自己指定。
// read the SN from the UDID
//
// command: GET_SN/<addr:4>/EOC
// reply: <value:4>/INSYNC/OK
case PROTO_GET_SN: // 若为获取MCU的UDID (Unique Device ID,或称为序列号) 0x2b
// expect argument
{
uint32_t index = 0;
if (cin_word(&index, 100)) {
goto cmd_bad;
}
// expect EOC
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
// expect valid indices 0, 4 ...ARCH_SN_MAX_LENGTH-4
if (index % sizeof(uint32_t) != 0 || index > ARCH_SN_MAX_LENGTH - sizeof(uint32_t)) {
goto cmd_bad;
}
cout_word(flash_func_read_sn(index));
}
SET_BL_STATE(STATE_PROTO_GET_SN);
break;
8.获取芯片ID功能简析
第八是获取芯片ID功能,其命令为:0x2c 0x20。该功能可以获取芯片ID并返回。
// read the chip ID code
//
// command: GET_CHIP/EOC
// reply: <value:4>/INSYNC/OK
case PROTO_GET_CHIP: { // 若为获取芯片ID和版本信息PROTO_GET_CHIP
// expect EOC
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
// //获取芯片独特的ID
cout_word(get_mcu_id());
SET_BL_STATE(STATE_PROTO_GET_CHIP);
}
break;
9.获取芯片描述信息功能简析
第九是获取芯片描述信息,其命令为:0x2e 0x20。该功能可以获取芯片中的描述信息。
// read the chip description
//
// command: GET_CHIP_DES/EOC
// reply: <value:4>/INSYNC/OK
case PROTO_GET_CHIP_DES: { // 若为获取芯片描述信息PROTO_GET_CHIP_DES
uint8_t buffer[MAX_DES_LENGTH];
unsigned len = MAX_DES_LENGTH;
// expect EOC
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
len = get_mcu_desc(len, buffer);
cout_word(len);
cout(buffer, len);
SET_BL_STATE(STATE_PROTO_GET_CHIP_DES);
}
break;
10.设置固件启动延时功能简析
第十是设置固件启动延时功能,其命令为:0x2d <延时时间> 0x20。该功能可以设定固件启动延时,即之前在hw_config_h中提到过的BOOTLOADER_DELAY。一旦程序进入BootLoader后的时间大于该值,程序就会自动退出BootLoader并尝试启动固件。延时时间最多为255秒。
case PROTO_SET_DELAY: { // 若为设置飞控固件启动延时PROTO_SET_DELAY
/*
Allow for the bootloader to setup a
boot delay signature which tells the
board to delay for at least a
specified number of seconds on boot.
*/
int v = cin_wait(100);
if (v < 0) {
goto cmd_bad;
}
uint8_t boot_delay = v & 0xFF;
if (boot_delay > BOOT_DELAY_MAX) {
goto cmd_bad;
}
// expect EOC
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
uint32_t sig1 = flash_func_read_word(BOOT_DELAY_ADDRESS);
uint32_t sig2 = flash_func_read_word(BOOT_DELAY_ADDRESS + 4);
if (sig1 != BOOT_DELAY_SIGNATURE1 ||
sig2 != BOOT_DELAY_SIGNATURE2) {
goto cmd_bad;
}
uint32_t value = (BOOT_DELAY_SIGNATURE1 & 0xFFFFFF00) | boot_delay;
flash_func_write_word(BOOT_DELAY_ADDRESS, value);
if (flash_func_read_word(BOOT_DELAY_ADDRESS) != value) {
goto cmd_fail;
}
}
break;
11.跳转到固件功能简析
第十一是跳转到固件功能,其命令为0x30 0x20。它与固件烧写功能紧密连接。在前面的介绍中我们已经知道,固件烧写功能不会烧写固件的前32位值。而该功能完成的就是固件烧写功能最后的工作,即烧写固件的前32位值并跳转到固件开始执行。
// finalise programming and boot the system
//
// command: BOOT/EOC
// reply: INSYNC/OK
//
case PROTO_BOOT: // 若为完成烧写并启动飞控固件PROTO_BOOT
// expect EOC
if (!wait_for_eoc(1000)) {
goto cmd_bad;
}
// 若变量first_word内容有效,烧写飞控固件首字并判断是否烧写成功
if (first_word != 0xffffffff && (bl_state & STATE_ALLOWS_REBOOT) != STATE_ALLOWS_REBOOT) {
goto cmd_bad;
}
// program the deferred first word
if (first_word != 0xffffffff) {
flash_func_write_word(0, first_word);
if (flash_func_read_word(0) != first_word) {
goto cmd_fail;
}
// revert in case the flash was bad...
first_word = 0xffffffff;
}
// send a sync and wait for it to be collected
sync_response(); // 运行正常发送PROTO_INSYNC+PROTO_OK
delay(100); // 等待100ms,函数返回
// quiesce and jump to the app
return;
12.Debug功能简析
最后一个功能是Debug功能,其命令为0x31。事实上该功能是留给用户自定义的。用户可以利用BootLoader中的代码自行编写Debug代码从而实现调试功能。
case PROTO_DEBUG:
// XXX reserved for ad-hoc debugging as required
break;