PX4 Bootloader原理简析

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所对应的文件名。它揭示了该款设备使用的是什么系列的芯片。

Makefile中挑选使用stm32不同系列的设备
其次,PX4有多种多样的设备。默认情况下,使用make命令会编译所有设备的固件,编译时间较长。因此,我们在确定使用哪个PX系列的设备后,可以通过Makefile文件进行调整。从而只编译该设备的固件,进而缩短编译时间,提高工作效率。具体来说,我们可以删除导出目录的对应设备名称,同时删除该设备的编译命令,即可在编译时取消编译该设备的固件。

Makefile中删除不需要设备的导出目录
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()函数。该函数的主要作用如下:

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。

固件前32位和32-64位中的数据

因此,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()函数。该函数的主要功能如下:

bootloader()函数主要功能

还记得我们在main_f1.c文件中调用bootloader时传递的timeout参数吗?这里就派上了用场!在刚进入bootloader()函数时,它会启动一个定时器,然后尝试从USART、USB、I2C等通信端口读取数据并执行对应的命令。每次执行完命令后,它都会对比定时器和timeout的值。一旦定时器的值超过了timeout,bootloader()函数就会强制返回。

我们与BootLoader交互均采用16进制的字节数据。在bl.c文件头部可以看到这些字节数据所代表的意思。我们向BootLoader发送命令时结尾必须加上0x20,即PROTO_EOC,代表命令输入结束。

BootLoader命令和数据对照

下面,我们分别来介绍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;
  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值