STM32 NXP 单片机MCU - bootloader不完全概述教程

术语介绍

  • bootloader: bootstrap loader的简称。中文名:引导(boot)加载(load)器(er),存在于大多数成熟的系统中。
  • application: 应用程序,本文将一个完整的设备代码划分为了bootloader+application两部分,方便区分。
  • STM32: ST意法半导体的32位微控制器(MCU)。
  • NXP: 著名的半导体公司,恩智浦,也生产各种微控制器(MCU)。
  • BIOS: basic input output system,存在于绝大多数的计算机主板上,每次开机都会进入BIOS
  • 程序升级:指使用代码更新代码自己,例如有这样一个函数,去官网下载更新包,然后覆盖安装自己
  • OTA: On-the-air,指远程程序升级。





简介

bootloader是什么?代码不能删除、更改自己,所以我们需要bootloader+application这种架构来设计我们的产品,使用bootloader来删除、更改application的代码。



计算机程序启动的顺序

STM32 NXP 单片机MCU - bootloader不完全概述教程_第1张图片
通过上图,我们可以看出

  • 一个app的启动流程包括了,开机->启动bootloader->启动操作系统->启动app
  • 操作系统可以对app进行更改、删除;bootloader可以对操作系统进行更改删除
  • app不能更新自己,操作系统也不能更新自己、bootloader也不能更新自己。

以一台安卓系统的手机为例,修改手机上的apps需要在安卓系统层面进行;修改安卓系统(刷机)需要进入bootloader(安卓系统上称作fastboot模式);修改bootloader则需要通过一定的工具才能进行。

所以说类比到MCU上,我们要实现能够对MCU的程序进行升级和更改,那么就需要一个前置程序,这个程序就叫做bootloader。





bootloader的基本实现

根据上述图中的表达可以看出,bootloader的本质也是一段程序,只不过这段程序相对简单:不需要复杂的例如实时聊天、录音等业务功能、不需要好看的交互界面等。其主要功能就是:

1. 引导下一段程序
2. 提供对下一段程序的修改接口
3. 提供更多的额外底层功能,例如修改CPU频率、显卡频率等

下面对上述几个功能的实现做一个简单的分析

bootloader本质就是一段程序,跑不跑RTOS都无所谓。



bootloader - 引导下一段程序

在引导下一段程序之前,先了解一下每段程序的执行原理。

  • 每段程序都有一个存放的位置,这个位置有非常多的选择,例如主板上的BIOS存放在主板上的芯片内部、单片机一般存放在flash中、app一般存放在RAM内存中(运行之前需要由ROM硬盘加载复制到RAM内存中,因为CPU无法对硬盘进行寻址
  • 每段程序都由CPU来执行,CPU通过读取程序指令,决定分配RAM、CPU、显卡(单片机没有)、磁盘等。
    STM32 NXP 单片机MCU - bootloader不完全概述教程_第2张图片

所以所谓的通过A引导下一段程序B,本质是A知道B的存储位置,强行让CPU跳转到B的存储位置继续执行。

顺便提一句,每次我们启动电脑的时候,实质是先运行主板上的固定位置的BIOS,然后BIOS中可以选择下一步引导系统的位置,机械硬盘、固态硬盘、U盘,系统起来后,我们还可以手动选择运行哪个程序。

因此引导的指令实际上很简单(大概是这样写的,具体指令忘了):

/* 汇编伪代码 */
LDR R0 0X08200000   // 装载跳转地址
BR  R0              // 跳转

但是写汇编就算了,所幸的是,C语言是可以直接操作内存的语言。(python、js这一类的语言不能直接操作内存,所以也就无法直接跳转)

先看一下linux的著名通用bootloader,u-boot的跳转代码:代码github链接
unsigned long do_go_exec(ulong (*entry)(int, char * const []), int argc,
				 char *const argv[])
{
     
	return entry (argc, argv);
}

static int do_go(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
{
     
	ulong	addr, rc;
	int     rcode = 0;

	if (argc < 2)
		return CMD_RET_USAGE;

	addr = simple_strtoul(argv[1], NULL, 16);

	printf ("## Starting application at 0x%08lX ...\n", addr);

	/*
	 * pass address parameter as argv[0] (aka command name),
	 * and all remaining args
	 */
	rc = do_go_exec ((void *)addr, argc - 1, argv + 1);
	if (rc != 0) rcode = 1;

	printf ("## Application terminated, rc = 0x%lX\n", rc);
	return rcode;
}

可以简单的看到,只需要把目的地地址address强制转换成一个函数,再执行这个函数就可以了。

再看一下恩智浦(NXP)官方的MCU-boot的代码:
/* NXP官方的MCU-BOOT中关键代码 */
static void jump_to_application(uint32_t applicationAddress, uint32_t stackPointer)
{
     
    shutdown_cleanup(kShutdownType_Shutdown);

    // Create the function call to the user application.
    // Static variables are needed since changed the stack pointer out from under the compiler
    // we need to ensure the values we are using are not stored on the previous stack
    static uint32_t s_stackPointer = 0;
    s_stackPointer = stackPointer;
    static void (*farewellBootloader)(void) = 0;
    farewellBootloader = (void (*)(void))applicationAddress;

    // Set the VTOR to the application vector table address.
    SCB->VTOR = (uint32_t)APP_VECTOR_TABLE;

    // Set stack pointers to the application stack pointer.
    __set_MSP(s_stackPointer);
    __set_PSP(s_stackPointer);

    // Jump to the application.
    farewellBootloader();
    // Dummy fcuntion call, should never go to this fcuntion call
    shutdown_cleanup(kShutdownType_Shutdown);
}
总而言之,总结一下,实现跳转功能的代码非常简单,只需要3行代码即可
// ...
// ... 通常你需要提前做一些准备工作,例如deinit RCC,copy code into RAM。
static void (*jump)(void) = NULL;  //声明一个jump函数
jump = (void (*)void)0x08200000;   //假设跳转到0x0820000处继续执行
jump();                            //let's jump!


bootloader - 修改下一段程序

一个合格的bootloader除了能够引导下一段程序,还应该能够修改下一段程序,例如最常见的就是升级下一段程序,专业术语叫做IAP,In Application Programming,实际上我们最经常听到的“帮我装个win10系统”,实际上也算IAP,只是这个词除了嵌入式以外不怎么常用。

修改下一段程序有两种思路:一种是直接把全部的程序代码复制到下一段程序的位置;另一种则是只复制需要升级的少部分的代码,称作差分升级。windows10升级的时候用的是哪一种我就不用多说了吧。(当然是后者)

在单片机上,升级很快,一般而言不需要进行差分升级,直接全量升级即可。直接copy代码到目的地即可,下面写一段我自己用到的从SD卡拷贝升级文件到目的地FLASH的代码:

  for(;;){
     
    file->read(&fil, read_buffer, sizeof(read_buffer), &br); // read file from SD card
    if(br == 0) break;   // end of read

    HAL_FLASH_Unlock();
    for(int i=0;i<sizeof(read_buffer)/FLASH_PSIZE_WORD;i++) {
     
      HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, APP_FLASH_START+i*FLASH_PSIZE_WORD+read_count*sizeof(read_buffer), (uint32_t)read_buffer+i*FLASH_PSIZE_WORD);
    } // copy file to flash
    HAL_FLASH_Lock();
	memset(read_buffer,0,sizeof(read_buffer));
    read_count++;

    log_w("copying ... %d * %d", sizeof(read_buffer), read_count);
  }


bootloader - 其他功能

其他功能就供大家自由发挥了,下面列举一些常见的例子:
1.恢复出厂程序
2.烧写/设置设备的序列号、ID、密钥
3.选择引导源:类似BIOS,可以选择硬盘、U盘、光驱等







参考:

一.linux开发之uboot移植(一)——初识uboot
常见bootloader介绍
https://github.com/u-boot/u-boot
MCUBOOT: MCU Bootloader for NXP microcontrollers




附录A:

NXP官方的bootloader关键引导代码:

/* NXP官方的MCU-BOOT中关键代码 */
static void jump_to_application(uint32_t applicationAddress, uint32_t stackPointer)
{
     
    shutdown_cleanup(kShutdownType_Shutdown);

    // Create the function call to the user application.
    // Static variables are needed since changed the stack pointer out from under the compiler
    // we need to ensure the values we are using are not stored on the previous stack
    static uint32_t s_stackPointer = 0;
    s_stackPointer = stackPointer;
    static void (*farewellBootloader)(void) = 0;
    farewellBootloader = (void (*)(void))applicationAddress;

    // Set the VTOR to the application vector table address.
    SCB->VTOR = (uint32_t)APP_VECTOR_TABLE;

    // Set stack pointers to the application stack pointer.
    __set_MSP(s_stackPointer);
    __set_PSP(s_stackPointer);

    // Jump to the application.
    farewellBootloader();
    // Dummy fcuntion call, should never go to this fcuntion call
    shutdown_cleanup(kShutdownType_Shutdown);
}


void shutdown_cleanup(shutdown_type_t shutdown)
{
     
    if (shutdown != kShutdownType_Reset)
    {
     
        // Clear (flush) the flash cache.
#if !BL_FEATURE_HAS_NO_INTERNAL_FLASH
#if !BL_DEVICE_IS_LPC_SERIES
        //flash_cache_clear(NULL);
    FTFx_CACHE_ClearCachePrefetchSpeculation(g_bootloaderContext.allFlashCacheState, true);
    FTFx_CACHE_ClearCachePrefetchSpeculation(g_bootloaderContext.allFlashCacheState, false);
#endif // !BL_DEVICE_IS_LPC_SERIES
#endif // #if !BL_FEATURE_HAS_NO_INTERNAL_FLASH
    }

    if (shutdown != kShutdownType_Cleanup)
    {
     
        // Shutdown all peripherals because they could be active
        uint32_t i;
        for (i = 0; g_peripherals[i].typeMask != 0; i++)
        {
     
            if (g_peripherals[i].controlInterface->shutdown)
            {
     
                g_peripherals[i].controlInterface->shutdown(&g_peripherals[i]);
            }
        }
    }

    // If we are permanently exiting the bootloader, there are a few extra things to do.
    if (shutdown == kShutdownType_Shutdown)
    {
     
        // Turn off global interrupt
        lock_acquire();

        // Shutdown microseconds driver.
        microseconds_shutdown();

        // Disable force ROM.
#if defined(RCM_FM_FORCEROM_MASK)
        RCM->FM = ((~RCM_FM_FORCEROM_MASK) & RCM->FM) | RCM_FM_FORCEROM(0);
#elif defined(SMC_FM_FORCECFG_MASK)
#if defined(SMC0)
        SMC0->FM = ((~SMC_FM_FORCECFG_MASK) & SMC0->FM) | SMC_FM_FORCECFG(0);
#else
        SMC->FM = ((~SMC_FM_FORCECFG_MASK) & SMC->FM) | SMC_FM_FORCECFG(0);
#endif
#endif // defined(RCM_FM_FORCEROM_MASK)

        // Clear status register (bits are w1c).
#if defined(RCM_MR_BOOTROM_MASK)
        RCM->MR = ((~RCM_MR_BOOTROM_MASK) & RCM->MR) | RCM_MR_BOOTROM(3);
#elif defined(SMC_MR_BOOTCFG_MASK)
#if defined(SMC0)
        SMC0->MR = ((~SMC_MR_BOOTCFG_MASK) & SMC0->MR) | SMC_MR_BOOTCFG(3);
#else
        SMC->MR = ((~SMC_MR_BOOTCFG_MASK) & SMC->MR) | SMC_MR_BOOTCFG(3);
#endif
#endif // defined(RCM_MR_BOOTROM_MASK)

        init_interrupts();

        // Set the VTOR to default.
        SCB->VTOR = kDefaultVectorTableAddress;

        // Restore clock to default before leaving bootloader.
        configure_clocks(kClockOption_ExitBootloader);

        // De-initialize hardware such as disabling port clock gate
        deinit_hardware();

        // Restore global interrupt.
        __enable_irq();

#if BL_FEATURE_BYPASS_WATCHDOG
        // De-initialize watchdog
        bootloader_watchdog_deinit();
#endif // BL_FEATURE_BYPASS_WATCHDOG
    }

    // Memory barriers for good measure.
    __ISB();
    __DSB();
}
/**
  \brief   Set Main Stack Pointer
  \details Assigns the given value to the Main Stack Pointer (MSP).
  \param [in]    topOfMainStack  Main Stack Pointer value to set
 */
__STATIC_INLINE void __set_MSP(uint32_t topOfMainStack)
{
     
  register uint32_t __regMainStackPointer     __ASM("msp");
  __regMainStackPointer = topOfMainStack;
}

你可能感兴趣的:(嵌入式,STM32,嵌入式,stm32,bootloader,nxp,单片机)