单片机(STM32,GD32,NXP等)中BootLoader的严谨实现详解

Bootloader(引导加载程序)的主要任务是引导加载并运行应用程序,我们的软件升级逻辑也一般在BootLoader中实现。本文将详细介绍BootLoader在单片机中的实现,包括STM32、GD32、NXP Kinetis等等的所有单片机,因为无论是什么样的芯片,它实现的逻辑都是一样的。

注意,本篇文章主要是介绍实现一个严谨的BootLoader需要掌握的基本知识和需要考虑的细节,如果不注意一些细节,应用层的代码很可能会受到影响。

  • 对于Linux的BootLoader来说其实也是一样的,但它还需要初始化MMU、引导内核等等,这里我们不做过多的讨论。

文章目录

  • 1 基础知识
    • 1.1 NOR Flash和NAND Flash
    • 1.2 程序数据段
    • 1.3 程序镜像文件格式
    • 1.4 Flash相关函数需要放入RAM中执行?
  • 2 BootLoader实现实例
    • 2.1 查看芯片的Flash映射
    • 2.2 Flash的擦写
      • 2.2.1 Flash擦写的代码
      • 2.2.2 重定位Flash擦写的代码到SRAM中
    • 2.3 MPU、低功耗和时钟的操作
    • 2.4 BootLoader内存分配
    • 2.5 链接脚本修改
    • 2.7 上下文保持一致
    • 2.8 获取SP和PC
    • 2.9 跳转APP
    • 2.9 BootLoader完整流程
  • 3 待优化
  • 4 总结

1 基础知识

1.1 NOR Flash和NAND Flash

NOR Flash和NAND Flash是两种常见的非易失性存储器(Flash Memory)类型,它们在内部结构、使用场景和性能方面存在一些显著的区别。以下是它们之间的一些主要区别:

  1. 内部结构:
    • NOR Flash: NOR Flash的内部结构类似于传统的存储器单元,支持随机访问。因此,它适用于需要快速随机访问的应用场景,例如执行代码(XIP,eXecute In Place)。
    • NAND Flash: NAND Flash的内部结构更适合大容量、顺序读写的应用场景。它采用页和块的结构,通常需要使用控制器来管理读写操作。
  2. 执行方式(XIP - Execute In Place):
    • NOR Flash: 由于其支持随机访问,NOR Flash 可以直接在存储器中执行代码(XIP),无需将代码加载到RAM中。
    • NAND Flash: 通常需要将代码从NAND Flash加载到RAM中才能执行,因为它不太适合随机访问。
  3. 位反转(Bit Inversion):
    • NOR Flash: NOR Flash 通常不需要位反转,即代码可以直接在Flash中运行,无需进行位翻转。
    • NAND Flash: 由于NAND Flash内部的存储单元是多级存储,读取时可能需要对数据进行位反转,以确保正确的数据解析。
  4. 擦写次数:
    • NOR Flash: NOR Flash 的擦写次数相对较高,通常可以达到数百万次,使其更适用于作为代码存储器。
    • NAND Flash: NAND Flash 的擦写次数相对较低,通常在几千次到几百万次之间,取决于具体的 NAND Flash 类型。因此,对于频繁擦写的应用,可能需要考虑其他存储器类型。

总体而言,选择使用NOR Flash还是NAND Flash取决于具体的应用场景和需求。NOR Flash适用于需要随机访问和高擦写次数的应用,例如嵌入式系统中的代码存储。NAND Flash适用于大容量存储和顺序访问的应用,例如存储大型文件和媒体内容。


注意事项:
对于STM32等单片机来说,它们都内置了NOR Flash,都是支持XIP的。但对于一些高端的单片机来说,如I.MX RT系列的MCU,在硬件上就需要自己接Flash,用户可以接NOR也可以接NAND,对应了不同的引导方式,具体就需要查看芯片手册了。

当然,对于单片机的绝大多数场景来说,代码放在NOR Flash中跑的概率比较高,所以本篇文章介绍的也是基于NOR Flash的BootLoader的实现。

  • 对于Linux来说,由于单单编译出来的内核本身就很大,而NOR Flash的成本较高,所以更常见的是将程序存储在一些NON-XIP的介质中,如EMMC、SD卡、NAND Flash,然后上电后将程序拷贝到SDRAM中运行。当然上电拷贝的程序也需要实现,一般芯片会自带一个很小的NOR Flash,里面存放一些固定的启动代码,当然不同厂商芯片的实现不同。

1.2 程序数据段

在程序中,通常会涉及到不同的段,这些段在内存中有着不同的属性和用途。以下是一些常见的程序段及其作用:

  1. 代码段(Text): 通常是只读的

    存储程序的执行代码,包括可执行指令和常量数据。在程序运行时,代码段的内容会被加载到内存中,并且在执行期间不可修改。

  2. 数据段(Data): 包括初始化数据(initialized data)和未初始化数据(uninitialized data)

    存储程序中的全局变量和静态变量。初始化数据在程序启动时会被初始化,而未初始化数据在程序启动时不会被初始化,其初始值为零或未定义。

  3. 只读数据段(Read-Only Data,rodata):

    存储常量数据,如字符串常量、只读常量等。在程序运行时,rodata段的内容不能被修改。

  4. 未初始化数据段(BSS):

    存储未初始化的全局变量和静态变量。在程序启动时,BSS段的内容被初始化为零或未定义的值。

  5. 栈(Stack):

    存储函数的局部变量和函数调用的状态信息。栈是一个先进后出(FILO)的数据结构,用于支持函数调用和返回。

  6. 堆(Heap):

    用于存储动态分配的内存,例如通过 malloc()new分配的内存。堆的管理通常由程序员负责,需要手动分配和释放内存。

1.3 程序镜像文件格式

对于不同的IDE来说,编译后生成的程序的镜像格式都不太一样,常见的有以下几种:

  • AXF:用于基于ARM的微控制器。它包含可执行代码、数据和调试信息。AXF文件通常在开发和调试过程中使用
  • HEX:由十六进制数及其对应的内存地址组成,可以将程序解析和编程到目标设备的内存中
  • S19:以特定格式的ASCII字符表示二进制数据。S19文件包含数据和内存地址,常用于编程旧的微控制器和EEPROM
  • ELF:包含可执行代码、数据和其他加载和执行程序所需的信息,可用于调试、分析和部署到目标设备
  • SREC:类似于S19的文件格式。它以ASCII字符表示二进制数据,但遵循不同的格式
  • BIN:BIN文件是直接包含可执行机器代码的二进制文件。它们通常用于以原始二进制格式存储最终编译的代码。

不管什么格式,都是为不同下载器或者调试而服务的,经过解析后下载进MCU内部FLASH的数据还是bin格式

1.4 Flash相关函数需要放入RAM中执行?

嵌入式Flash由多个块(block)组成,每个块包含了在该块内进行读取、擦除和写入时所需的电路。大多数闪存都存在一个限制:不允许在同一块内在执行擦/写操作的同时,执行读取操作(比如CPU从Flash读取指令运行代码)

举个例子,如果有一段代码在block1中执行,那在这个代码的执行期间,不允许对block1中的任何部分进行擦/写,这可能会导致读写冲突,进而引发错误。

以下是两个解决办法
(1)从不同的Flash块执行命令
如果MCU有多个Flash块,可以将擦/写Flash的代码放置在一个块中,而将其它代码或数据存储在另一个块中。
(2)从SRAM执行Flash命令
如果MCU只有一个Flash块,或用户在每个可用块内都要存放代码和写入,在这些场景中,可以将Flash命令移到SRAM中执行。

2 BootLoader实现实例

这里我将以NXP的Kinetis K系列芯片为例进行BootLoader的实现,我使用的芯片为MK64FN1M0xxx12,官方的开发板为FRDM-K64F

  • 不同MCU的BootLoader实现原理都相同,希望大家能学到一些通用的知识,而不是特定于某个单片机的。

接下来我们就来在一个新的平台中,如何一步一步地通过阅读芯片手册来实现BootLoader。

2.1 查看芯片的Flash映射

如下图所示:
在这里插入图片描述
所以在我们使用的芯片中有自带Flash,而且分为了两个block,其中block 0的范围是0x00000~0x7FFFF;block 1的范围是0x80000~0xFFFFF,也就是两个block各有512KB。另外,在上电后程序将从0地址取值运行。

2.2 Flash的擦写

2.2.1 Flash擦写的代码

在前面的程序镜像文件格式中,我们知道更新程序无非就是将原始的bin文件写到Flash中,所以最重要的一步就是看看芯片内置Flash如何通过程序进行擦写。

首先我们要知道,在写Flash之前必须保证所有的内存为0xFF,这是因为写操作只能将电平从1改为0,所以我们在写入Flash之前,必须要先对Flash进行擦除(一般是以块为单位进行)。

不同的芯片有不同的Flash控制器,这个一般在SDK中有提供相应的Flash驱动,这里不就做详细地分析了。在MK64中,初始化完Flash后可以调用下面两个函数来擦除和写入Flash:

status_t mem_erase(uint32_t address, uint32_t length);
status_t mem_write(uint32_t address, uint32_t length, const uint8_t *buffer);

2.2.2 重定位Flash擦写的代码到SRAM中

在MK64内存映射中,我们知道MK64中有两个block,每个block为512KB,就有前面所说的“Flash相关函数需要放入RAM中执行”的问题,那么第一个解决方案(单独将Flash函数放到第二个block上)其实不太实用,而且很麻烦。所以我们更多使用的是将Flash相关函数重定位到SRAM中执行。

对于MK64的Flash来说,由于是内部的Flash,对于Flash的读写操作来说,只需要更改FTFE寄存器即可。比如如果要擦除某个sector,只需要将这个sector的相关信息填充到FTFE对应的寄存器中,然后将FTFE_FSTAT寄存器的第7位CCIF置1,即可根据我们填充的参数来启动Flash操作。
在这里插入图片描述
所以我们实际上只需要填充好相应的Flash操作寄存器,然后将CCIF位置为1,然后硬件会将CCIF清零,然后我们再等待CCIF置1即可。对于填充寄存器部分,由于没有运行代码,所以可以在Flash中运行,而对于操作CCIF标志位的部分,我们需要将其重定位到SRAM中运行,以下是CCIF位操作的代码:

void flash_run_command(FTFx_REG_ACCESS_TYPE ftfx_fstat)
{
    // clear CCIF bit
    *ftfx_fstat = FTFx_FSTAT_CCIF_MASK;

    // Check CCIF bit of the flash status register, wait till it is set.
    // IP team indicates that this loop will always complete.
    while (!((*ftfx_fstat) & FTFx_FSTAT_CCIF_MASK))
    {
    }
}

我们只要保证这个函数在SRAM中运行就行了,所以我们先将这个函数编译出来,然后通过.map内存映射文件,将去bin文件反汇编objdump,然后找到这个函数在汇编上的机器码,我们这里保存为数组:

const static uint16_t s_flashRunCommandFunctionCode[] = {
    0x2180, /* MOVS  R1, #128 ; 0x80 */
    0x7001, /* STRB  R1, [R0] */
    /* @4: */
    0x7802, /* LDRB  R2, [R0] */
    0x420a, /* TST   R2, R1 */
    0xd0fc, /* BEQ.N @4 */
    0x4770  /* BX    LR */
};

然后我们再初始化Flash的时候,将这个机器码拷贝到SRAM中即可,然后使用一个函数指针指向拷贝到的位置,就可以调用这个函数了:

// 声明函数callFlashRunCommand(对应上面的flash_run_command)
static void (*callFlashRunCommand)(FTFx_REG_ACCESS_TYPE ftfx_fstat);
// 声明保存二进制代码的数组
#define kFLASH_ExecuteInRamFunctionMaxSizeInWords 16U
static uint32_t s_flashRunCommand[kFLASH_ExecuteInRamFunctionMaxSizeInWords];
// 拷贝二进制码到数组中
memcpy((void *)&s_flashRunCommand, (void *)s_flashRunCommandFunctionCode, sizeof(s_flashRunCommandFunctionCode));
// 将callFlashRunCommand函数指针指向数组地址
callFlashRunCommand = (void (*)(FTFx_REG_ACCESS_TYPE ftfx_fstat))((uint32_t)s_flashRunCommand + 1);

这样后续调用callFlashRunCommand函数,就和flash_run_command函数是一个效果,但是callFlashRunCommand 就是在RAM中运行的了。前面说了Flash的所有操作,擦除、写入等等函数,最终都是会置CCIF位来启动Flash控制器进行操作,所以最后只要保证擦除、写入等封装好的函数最后调用的是callFlashRunCommand函数启动即可。


细心的人可能发现上面强制转换时s_flashRunCommand还加了1:
在ARM架构中,函数指针的值通常是奇数。这是因为ARM处理器使用Thumb指令集,而Thumb指令集中的指令是16位的,因此函数的地址通常是2的倍数。由于函数指针的最低位是用来指示Thumb指令集的状态的,所以函数指针的值通常是奇数。

然而,实际上函数在内存中的存储地址是偶数。因为Thumb指令集中的指令是16位的,而ARM处理器要求指令在内存中的地址是4的倍数。因此,当你想要获取函数在内存中的真实地址时,你需要将函数指针的值加上1,以得到实际的偶数地址。

简而言之,通过执行 “+1” 操作,你可以将奇数的函数指针值调整为函数实际在内存中的偶数地址,以正确访问函数的二进制代码。这是在处理ARM函数指针时经常需要考虑的一种调整。


当然,如果你的MCU还支持对Flash的数据进行缓存的话,那就还需要将清除缓存的函数重定位到SRAM中:

// 函数原型:这里不做详细分析了,实际就是控制寄存器
void flash_cache_clear_command(FTFx_REG32_ACCESS_TYPE ftfx_reg)
{
    *ftfx_reg = (*ftfx_reg & ~FMC_PFB01CR_CINV_WAY_MASK) | FMC_PFB01CR_CINV_WAY(~0);
    *ftfx_reg |= FMC_PFB0CR_S_INV_MASK;
    __ISB();
    __DSB();
}

// 函数二进制
const static uint16_t s_flashCacheClearCommandFunctionCode[] = {
    0x6801,         /* LDR  R1, [R0] */
    0x22f0,         /* MOVS R2, #240    ; 0xf0 */
    0x0412,         /* LSLS R2, R2, #16 */
    0x430a,         /* ORRS R2, R2, R1 */
    0x6002,         /* STR  R2, [R0] */
    0xf3bf, 0x8f6f, /* ISB */
    0xf3bf, 0x8f4f, /* DSB */
    0x4770          /* BX   LR */
};

// 声明函数指针
static void (*callFlashCacheClearCommand)(FTFx_REG32_ACCESS_TYPE ftfx_reg);
// 声明数组
#define kFLASH_ExecuteInRamFunctionMaxSizeInWords 16
static uint32_t s_flashCacheClearCommand[kFLASH_ExecuteInRamFunctionMaxSizeInWords];
// 拷贝函数
memcpy((void *)s_flashCacheClearCommand, (void *)s_flashCacheClearCommandFunctionCode, sizeof(s_flashCacheClearCommandFunctionCode));
// 设置函数指针
callFlashCacheClearCommand = (void (*)(FTFx_REG32_ACCESS_TYPE ftfx_reg))((uint32_t)flashCacheClearCommand + 1);
// 调用例子
callFlashCacheClearCommand((FTFx_REG32_ACCESS_TYPE)&MCM->PLACR);

在每次擦除、写完Flash之后,都需要调用这个函数flush一下cache。

  • 注意:在我这个例子中使用机器码的方式手动拷贝这些函数到SRAM中,大家可以简单地在链接脚本中定义一个段,同时把这个段链接到SRAM中,然后在函数声明的地方加上__attribute__((section("段名")))

2.3 MPU、低功耗和时钟的操作

1、MPU
对于MPU来说,在我之前的文章中有详细地介绍MPU内存保护单元详解及例子,感兴趣的可以看一下。

MPU是Cortex-M系列芯片都有的一个特性,它涉及到Cache的一些问题,如果使能的话,对于一些直接与硬件接触的操作,如我们希望在BootLoader中实现通过USB获取固件并升级,而USB一般使用了DMA,这样的话数据的一致性会受到影响。当然我们可以使用CMSIS中的SCB_CleanDCache等函数在执行DMA之前清理一下D-Cache,但这些都太麻烦了,这里建议在BootLoader中直接关掉MPU

2、低功耗
MK64芯片支持低功耗模式,为了防止在固件升级的过程中进入低功耗而引发Flash的未知状态,我们需要将低功耗模式关闭。当然有的芯片是自动开启低功耗,有的则是没有开启低功耗,我的建议还是以防万一,在上电时关闭一下低功耗。
MK64中通过SMC(System Mode Controller,系统模式控制器)中的PMCTRL中的RUNM位控制低功耗模式:
在这里插入图片描述
我们在上电之后将这两个位置为0即可,表示进入正常运行模式。

3、时钟
我们在BootLoader中可能会使用到一些外设,我们可以在启动时就将所有GPIO的时钟打开。当然也可以在使用的时候再单独打开,比如要使用串口,在串口初始化函数中初始化时钟也行。

在MK654中通过SIM(System Integration Module,系统集成模块)的SCGC5寄存器可以控制GPIOA~GPIOE时钟的使能。
在这里插入图片描述

2.4 BootLoader内存分配

首先我们要规定一下BootLoader的大小,假设我们给BootLoader留40KB的大小(需要保证编译出来的BootLoader的bin文件小于40KB),那么在0~0xA000部分就存放BootLoader的代码,从0xA000开始就存放应用程序的代码。当然我们的程序大小不能超过block1,因为block1和block2的内存虽然在逻辑上是连续的,但是CPU无法从block1读取一半指令,又从block2读取一半指令执行。如下图所示:
在这里插入图片描述

2.5 链接脚本修改

对于APP来说,它的偏移现在在0xA000处,所以我们要在IDE中修改链接脚本,将程序链接到0xA000处,我这里使用的是IAR,只需要更改它的链接文件.icf中的__ICFEDIT_intvec_start__即可(变量名可能不同,具体参考你目录下的链接脚本):

define symbol __ICFEDIT_intvec_start__ = 0x0000A000;  /*-User Application Base-*/

对于Keil和IAR,我同样写过文章分析其链接脚本的格式,大家可以参考一下:

  • IAR中ICF链接文件详解和实例分析
  • KEIL中SCF分散加载链接文件详解和实例分析

2.7 上下文保持一致

我们必须保证程序在进BootLoader前是什么状态,在进APP前就应该是什么状态。

我的真实经历是,同事在BootLoader中使用UART升级,打开了UART中断,但退出BootLoader时没有关闭这个中断。于是在APP的初始化函数中,将数据段复制到RAM中的时候,这个中断就会影响拷贝的值。比如你在程序中初始化了一个char *a = "123";,但实际上a的值可能为1a3

详细的步骤如下:
1、清理Flash的缓存:一般Flash有一个flush类似的函数,保证之前的Flash操作都执行完毕
2、清除所有中断标志位:主要是控制NVIC寄存器,参考代码如下:

__STATIC_INLINE void NVIC_ClearEnabledIRQs(void)
{
    NVIC->ICER[0] = 0xFFFFFFFF;
    NVIC->ICER[1] = 0xFFFFFFFF;
    NVIC->ICER[2] = 0xFFFFFFFF;
    NVIC->ICER[3] = 0xFFFFFFFF;
    NVIC->ICER[4] = 0xFFFFFFFF;
    NVIC->ICER[5] = 0xFFFFFFFF;
    NVIC->ICER[6] = 0xFFFFFFFF;
    NVIC->ICER[7] = 0xFFFFFFFF;
}

__STATIC_INLINE void NVIC_ClearAllPendingIRQs(void)
{
    NVIC->ICPR[0] = 0xFFFFFFFF;
    NVIC->ICPR[1] = 0xFFFFFFFF;
    NVIC->ICPR[2] = 0xFFFFFFFF;
    NVIC->ICPR[3] = 0xFFFFFFFF;
    NVIC->ICPR[4] = 0xFFFFFFFF;
    NVIC->ICPR[5] = 0xFFFFFFFF;
    NVIC->ICPR[6] = 0xFFFFFFFF;
    NVIC->ICPR[7] = 0xFFFFFFFF;
}
  • 执行上面两个函数即可,但上面的代码是基于Cortex-M4或Cortex-M7内核的,其它内核自行参考内核手册的NVIC章节编写。

3、设置VTOR为默认值

kDefaultVectorTableAddress = 0
SCB->VTOR = kDefaultVectorTableAddress;

4、恢复时钟
比如程序中用到了USB的话,系统时钟速率在之前应该配置地很高,这里需要恢复最初始的时钟配置。同时如果前面开启了所有GPIO的时钟的话,这里也要全部关闭。比如使用了UART,打开了对应GPIO的时钟的话,需要在此关闭。

对于MK64来说,如果打开USB的话,配置时钟的时候还使能了这些位,都需要关闭。
在这里插入图片描述
5、使能中断
这和我们刚刚清理的中断标志位不一样,在上电后默认总中断的相应是使能的,为了进一步处理中断请求并继续系统的正常运行,需要重新使能系统对于中断的相应。

__enable_irq()

6、内存屏障
最后我们确保指令和数据的一致性以及正确的执行顺序,这里是保证在APP跳转之前我们的这些设置都起作用了。当然这里的__DSB可以省略,因为我们前面更改的都是强有序内存(这些系统内存即使不使能MPU也是强有序的)。这里更多地考虑的是平台之间的兼容,如代码从Cortex-M4移动到Cortex-M7一样可以使用。

__ISB();
__DSB();

2.8 获取SP和PC

在更新完固件后,我们需要跳转到位于0xA000处的APP,现在的问题是,APP的堆栈指针是什么,应该将PC指针设置为多少才能跳转到APP中。
获取SP和PC
如下图所示,实际上固件的0地址存放的就是堆栈指针,在上电后硬件将设置MSP(主堆栈指针)的值为bin文件0偏移处的值。
在这里插入图片描述
我们再来看一下APP的.s启动文件:
在这里插入图片描述
可以看到第一个果然是堆栈指针,这里的CSTACK可以在链接脚本中指定。同时我们发现第二个向量是Reset_Handler函数的地址,我们将PC值设置为Reset_Handler的值不就可以跳转到APP了吗?获取这两个值的函数如下:

#define APP_VECTOR_TABLE ((uint32_t *)0xA000)
static void get_user_application_entry(uint32_t *appEntry, uint32_t *appStack)
{
	*appEntry = APP_VECTOR_TABLE[1];
    *appStack = APP_VECTOR_TABLE[0];
}

2.9 跳转APP

前面获取了SP(appStack)和PC(appEntry),这里就派上用场了。但在跳转APP之前,我们还需要做两件事:
1、设置堆栈指针
因为前面说的是上电的时候硬件会设置SP,所以仅仅设置的是BootLoader中的SP,对于APP的堆栈指针需要我们自己设置:

__set_MSP(appStack);
__set_PSP(appStack);
  • 主堆栈指针和线程堆栈指针都需要设置。

2、设置向量表地址
同样地,上电后硬件设置的是BootLoader的向量表,我们要将其设置为APP的向量表位置:

#define APP_VECTOR_TABLE ((uint32_t *)0xA000)
SCB->VTOR = (uint32_t)APP_VECTOR_TABLE;

最后我们就可以跳转到APP了,声明一个函数指针,然后指向Reset_Handler,然后执行即可更改PC指针为Reset_Handler

static void (*farewellBootloader)(void) = 0;
farewellBootloader = (void (*)(void))appEntry;
farewellBootloader();

2.9 BootLoader完整流程

下面来列举一下BootLoader的实现步骤:
1、退出低功耗:如果芯片支持的话,需要关闭
2、关闭MPU:建议关闭,否则代码中需要兼容Cache
3、开启所有GPIO的时钟:非必要,可在用到具体某个外设时再打开
4、配置系统时钟树:建议使用芯片内部的时钟作为主时钟源
5、初始化Flash:包括Flash参数的配置、Flash时钟的配置、拷贝代码到SRAM
6、更新固件
实际上就是可以通过UART、SDCARD、USB等各种外设(记得初始化这些外设的引脚)获取最新的固件,然后调用mem_erasemem_write函数将固件写入Flash中。
7、清理上下文:上下文保持一致
8、获取SP和PC值,设置MSP/PSP/VTOR
9、跳转APP

3 待优化

另外,对于固件升级来说,还有两点需要考虑。
1、可靠升级:如果在固件升级的过程中,已经把0xA000处之前的APP擦掉了,准备写入新的固件,但此时如果突然设备断电,那么就没有程序了,原来的程序也不能运行,所以我们还需要保证BootLoader的可靠。

2、加密:现在反汇编的技术已经很成熟了。我最近使用的I.MX RT1170直接硬件自带了OTFAD引擎,可以边解密AES-128加密的代码边运行,可见加密的重要性。而对于这些普通的MCU来说,我们也可以自己设计加密算法。对于MK64来说,有AES解密的引擎,但没有这个功能的MCU也没关系,我们也可以自己解密。可以参考我写的两篇关于AES的文章:

  • AES加密(1):AES基础知识和计算过程
  • AES加密(2):AES代码实现解析

我这里给大家提供一个思路,下图是我在MK64平台中实现的BootLoader:
在这里插入图片描述
这里我使用了AES-128加密,从串口/SDCARD直接边读取加密固件,边解密原始固件到0x80000开始处的位置。同时我在APP的头字段中包含一些字段(很多中断向量表都是空的,可以用来存储一些Boot信息),其中包括CRC字段,解密完后可以用于校验固件的合法性。校验完后将APP从0x80000处拷贝到0xA000处,这样就也保证了可靠升级。最后再校验一次0xA000处的CRC,就表示升级成功了。

4 总结

本文介绍了对于实现一个BootLoader需要考虑的方面,其实本文更多的是想传递一种严谨的思想,而不是从网上随便复制一段代码就去用。在你严谨地做事的同时,就会考虑到更多的东西,比如这里你可能还会学到MPU、低功耗、内存屏障等知识,正是对这一个个知识的好奇、深入理解并积累,同时保持严谨的态度,你才会在不知不觉中成为“高手”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/237692.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

​pathlib --- 面向对象的文件系统路径​

3.4 新版功能. 源代码 Lib/pathlib.py 该模块提供表示文件系统路径的类,其语义适用于不同的操作系统。路径类被分为提供纯计算操作而没有 I/O 的 纯路径,以及从纯路径继承而来但提供 I/O 操作的 具体路径。 如果以前从未用过此模块,或不确定…

07.仿简道云公式函数实战-逻辑函数-AND

1.AND函数 AND 函数可用于表示:当所有参数逻辑值为 true 时,返回 true;只要有任何一个参数逻辑值为 false,则返回false。 2. 函数用法 AND(语文成绩>90,数学成绩>90,英语成绩>90) 3.函数示例 1)AND(A,B)&a…

Ruff智慧路灯方案让城市管理更智慧,物联网助力城市数智化运营

随着5G、物联网、人工智能等新一代信息技术的发展,路灯管理也被赋予了更多可能,路灯已经从简单的照明工具逐步成为智慧城市发展的新入口。 据了解,当前我国路灯照明耗电量约占总量的15%。传统路灯存在耗能大、设备故障无法定位、路灯状态难以…

安装LLaMA-Factory微调chatglm3,修改自我认知

安装git clone https://github.com/hiyouga/LLaMA-Factory.git conda create -n llama_factory python3.10 conda activate llama_factory cd LLaMA-Factory pip install -r requirements.txt 之后运行 CUDA_VISIBLE_DEVICES0 python src/train_web.py,按如下配置…

algorithm graphics

绘制地图坐标路线_哔哩哔哩_bilibili neo4j test-CSDN博客

如何使用Java在Excel中添加动态数组公式?

本文由葡萄城技术团队发布。转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。 前言 动态数组公式是 Excel 引入的一项重要功能,它将 Excel 分为两种风格:Excel 365 和传统 …

Linux基础项目开发2:物联网监控——视频监控方案介绍(一)

前言: 这次我们来做一个关于视频监控的基础小项目,需要我们用到网络的相关知识,还会学到好多优秀的网络协议,下面让我们开始对物联网视频监控进行一个大体框架的介绍吧 目录 项目内容: 1.视频监控方案介绍 2.视频监控…

ROB的结构与作用

在流水线的提交(Commit)阶段,之所以能够将乱序执行的指令变回程序中指定的顺序状态,主要是通过重排序缓存(Reorder Buffer, ROB)来实现的 ROB本质上是一个FIFO在它当中存储了一条指令的相关信息,例如这条指令的类型、结果、目的寄存器和异常…

SpringCloud面试题——Sentinel

一:什么是Sentinel? Sentinel是一个面向分布式架构的轻量级服务保护框架,实现服务降级、服务熔断、服务限流等功能 二:什么是服务降级? 比如当某个服务繁忙,不能让客户端的请求一直等待,应该立刻返回给客户端一个备…

安装程序无法自动安装Virtual Machine Communication Interface Sockets(VSock)驱动程序

环境情况: 物理机win10系统 虚拟机windowserver08系统 vmware 16.0的版本 问题触发: 在虚拟机win7系统上安装vmware tools出现提示,报错信息“安装程序无法自动安装Virtual Machine Communication Interface Sockets(VSock&a…

Android 11.0 systemui锁屏页面时钟显示样式的定制功能实现

1.前言 在11.0的系统ROM定制化开发中,在进行systemui的相关开发中,当开机完成后在锁屏页面就会显示时间日期的功能,由于 开发产品的需求要求时间显示周几上午下午接下来就需要对锁屏显示时间日期的相关布局进行分析,然后实现相关功能 效果图如图: 2.systemui锁屏页面时钟显…

Ubuntu Destktop 22.04 设置 ssh 超时时间

Ubuntu Destktop 22.04 使用 ssh 连接服务器时,发现一段时间不操作就会自动断开连接,解决方法如下: 打开 /etc/ssh/ssh_config 文件: sudo vim /etc/ssh/ssh_config在文件最后添加: # ssh 客户端会每隔 30 秒发送一…

4fiddler抓包工具的使用

一、定义 1.1 抓包的定义 说明:客户端向服务器发送请求以及服务器响应客户端的请求,都是以数据包来传递的。 抓包(packet capture):通过工具拦截客户端与服务器交互的数据包 1.2 fiddler的介绍 Fiddler是一个http协议调试代理工具&#…

将数组转换为字符串 numpy.array_repr()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 将数组转换为字符串 numpy.array_repr() [太阳]选择题 请问关于以下代码最后输出的数据类型是? import numpy as np a np.array([0, 1, 2]) print("【显示】a ",a) pr…

Node CLI 之 Yargs (2)

什么是 yargs? yargs 是一个用于处理命令行参数的 Node.js 库 安装 yargs npm install yargs简单例子 不定义任何选项,直接便可以使用 定义命令 const yargs require(yargs)yargs.command(hello, Prints hello world, (yargs) > {}, (argv) >…

docker-compose的介绍与使用

一、docker-compose 常用命令和指令 1. 概要 默认的模板文件是 docker-compose.yml,其中定义的每个服务可以通过 image 指令指定镜像或 build 指令(需要 Dockerfile)来自动构建。 注意如果使用 build 指令,在 Dockerfile 中设置…

2020年第九届数学建模国际赛小美赛C题亚马逊野火解题全过程文档及程序

2020年第九届数学建模国际赛小美赛 C题 亚马逊野火 原题再现: 野火是指发生在乡村或荒野地区的可燃植被中的任何不受控制的火灾。这样的环境过程对人类生活有着重大的影响。因此,对这一现象进行建模,特别是对其空间发生和扩展进行建模&…

docker mysql8 设置不区分大小写

docker安装Mysql8.0的坑之lower_case_table_names_docker mysql lower_case_table_names-CSDN博客https://blog.csdn.net/p793049488/article/details/108365929 docker run ‐di ‐‐nametensquare_mysql ‐p 33306:3306 ‐e MYSQL_ROOT_PASSWORD123456 mysql

区块链实验室(32) - 下载arm64的Prysm

Prysm是Ethereum的共识层。 1. 下载prysm.sh curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod x prysm.sh2. 下载x86版prysm共识客户端 ./prysm.sh beacon-chain --download-only3.下载arm64版prysm共识客…