以下以nrf52840的DFU为例,这里不采用NRF5340为例的原因是Nordicmcuboot的源码中对5340的DFU做了一些适配。

简介

Zephyr的DFU子系统提供了在运行时升级基于Zephyr的应用程序映像所需的框架。它目前由两个不同的模块组成:

  • subsys/dfu/boot/:引导加载程序的接口代码
  • subsys/dfu/img_util/: 映像管理代码

应用程序分区

首先需要确保应用程序的设备树中适配mcuboot的硬件层分区。

  • boot_partition:对于 MCUboot 本身
  • image_0_primary_partition:图像 0 的主插槽
  • image_0_secondary_partition:映像 0 的辅助插槽
  • scratch_partition:暂存槽

    1. &flash0 {
    2. partitions {
    3. compatible = "fixed-partitions";
    4. #address-cells = <1>;
    5. #size-cells = <1>;
    6. boot_partition: partition@0 {
    7. label = "mcuboot";
    8. reg = <0x000000000 0x0000C000>;
    9. };
    10. slot0_partition: partition@c000 {
    11. label = "image-0";
    12. reg = <0x0000C000 0x00067000>;
    13. };
    14. slot1_partition: partition@73000 {
    15. label = "image-1";
    16. reg = <0x00073000 0x00067000>;
    17. };
    18. scratch_partition: partition@da000 {
    19. label = "image-scratch";
    20. reg = <0x000da000 0x0001e000>;
    21. };
    22. /*
    23. * The flash starting at 0x000f8000 and ending at
    24. * 0x000fffff is reserved for use by the application.
    25. */
    26. /*
    27. * Storage partition will be used by FCB/LittleFS/NVS
    28. * if enabled.
    29. */
    30. storage_partition: partition@f8000 {
    31. label = "storage";
    32. reg = <0x000f8000 0x00008000>;
    33. };
    34. };
    35. };

    boot_partition节点: 存放mcuboot的程序
    slot0_partition节点: 存放运行的应用程序
    slot1_partition节点: 存放要升级的应用程序
    scratch_partition节点: 主要是为了拷贝slot1_partition节点的程序到slot0_partition节点中。

默认运行的应用程序插槽配置

上面FLASH分区分配好了之后,需要在chosen节点下设置默认运行的应用程序到底是哪个分区:

  1. zephyr,code-partition = &slot0_partition;

应用程序的配置

在应用程序的Kconfig中配置启用mcuboot

  1. CONFIG_BOOTLOADER_MCUBOOT=y

imgtool介绍

imgtool工具是mucboot提供的生成签名文件和对应用程序进行签名的工具。其中工具所在路径为bootloader/mcuboot/scripts/imgtool.py

生成签名文件

这里我们采用mcuboot提供的imgtool工具生成一个签名文件:

  1. ./bootloader/mcuboot/scripts/imgtool.py keygen -k filename.pem -t rsa-2048

-t代表的是生成签名文件的加密方式,目前支持如下方式:

  • rsa-2048
  • rsa-3072
  • ecdsa-p256
  • ed25519

签名

imgtool在签名时提供的常用参数

参数 描述 必须
-k, --key filename 签名文件路径 必须
--align [1&#124;2&#124;4&#124;8] 对其字节数 必须
-v, --version TEXT 版本号 必须
-s, --security-counter TEXT 计数器号 非必须
--pad-sig 在1.5版本之前,需要在ECDSA签名中添加0-2字节的填充 非必须
--header-size 中断矢量表的偏移大小,这个大小必须和Kconfig中的CONFIG_ROM_START_OFFSET一致 必须
--slot-size 运行插槽的大小 必须

指令格式:

  1. imgtool.py <参数> <需要签名的文件名> <签名后的文件名>

应用程序的签名

这里可以直接使用mcuboot提供的imgtool工具,还可以使用zephyr提供的west sign工具,其实west工具是对imgtool工具做了一个外层的封装,底层还是在使用imgtool
这里我们直接采用imgtool工具来进行应用程序的签名:

  1. imgtool.py sign --key ~/root-rsa-2048.pem --header-size 0 --align 8 --version 1.0 --slot-size 0x67000 ./zephyr/zephyr.bin zephyr.signed.bin

合并mcuboot和应用程序

我们在上一章介绍了如何集成mcuboot,这里不做过多的讲解,最终的目的其实就是将mcuboot应用程序合并为一个程序而已。
其实nordic提供了多应用构建的操作,所以我们只需要配置就可以将mcuboot和应用程序集成好,对于多应用配置,我们之后会专门讲解。目前多应用构建是nordic特有的功能。

DFU升级

  • 下载升级文件
  • 存储到image_0_secondary_partition
  • 设置升级标志

mcuboot升级原理

首先这里我们需要说明一下,需要升级文件永远存储在image_0_secondary_partition这个区域,当将文件下载到此区域之后,将此区域的交换标志置位,之后重启后mcuboot会检测image_0_secondary_partition的交换标志是否设置,如果设置那就将image_0_secondary_partitionimage_0_primary_partition交换,交换完成后,最终还是在image_0_primary_partition运行程序。

下载升级文件

下载文件的过程可以通过wifiBLE或有线连接的方式,这里不做太多介绍。

存储文件

我们需要将下载的文件存储到image_0_secondary_partition分区。
其实在zephyr\subsys\dfu\img_util\flash_img.c里提供的函数就是为了操作升级区域的。

API 含义
flash_img_init_id 根据提供的区域id初始化flash的上下文
flash_img_init 直接采用默认的区域id(secondary_partition)初始化flash上下文
flash_img_bytes_written 获取写入FLASH的字节数
flash_img_buffered_write 通过缓冲器的方式写入flash
flash_img_check 验证完整性

还可以直接通过区域操作的API来操作升级区域,区域操作API的路径zephyr/include/storage/flash_map.h:

API 含义
flash_area_open 根据区域id打开要操作的区域
flash_area_close 关于指定的区域
flash_area_read 从区域内读取数据
flash_area_write 从区域内写入数据
flash_area_erase 擦除区域
flash_area_check_int_sha256 验证区域完整性

示例:

  1. void test_collecting(void)
  2. {
  3. const struct flash_area *fa;
  4. struct flash_img_context ctx;
  5. uint32_t i, j;
  6. uint8_t data[5], temp, k;
  7. int ret;
  8. // 初始化flash img的变量,这里其实就在再找image_0_secondary_partition区域
  9. ret = flash_img_init(&ctx);
  10. //擦除该区域
  11. #ifdef CONFIG_IMG_ERASE_PROGRESSIVELY
  12. uint8_t erase_buf[8];
  13. (void)memset(erase_buf, 0xff, sizeof(erase_buf));
  14. // 这种方式是是另外一种获取升级区域的方式
  15. ret = flash_area_open(FLASH_AREA_ID(image_1), &fa);
  16. if (ret) {
  17. printf("Flash driver was not found!\n");
  18. return;
  19. }
  20. /* ensure image payload area dirt */
  21. for (i = 0U; i < 300 * sizeof(data) / sizeof(erase_buf); i++) {
  22. ret = flash_area_write(fa, i * sizeof(erase_buf), erase_buf,
  23. sizeof(erase_buf));
  24. zassert_true(ret == 0, "Flash write failure (%d)", ret);
  25. }
  26. /* ensure that the last page dirt */
  27. ret = flash_area_write(fa, fa->fa_size - sizeof(erase_buf), erase_buf,
  28. sizeof(erase_buf));
  29. zassert_true(ret == 0, "Flash write failure (%d)", ret);
  30. #else
  31. // 直接擦除整个区域
  32. ret = flash_area_erase(ctx.flash_area, 0, ctx.flash_area->fa_size);
  33. zassert_true(ret == 0, "Flash erase failure (%d)", ret);
  34. #endif
  35. //直接在该区域写入一个字节
  36. zassert(flash_img_bytes_written(&ctx) == 0, "pass", "fail");
  37. k = 0U;
  38. for (i = 0U; i < 300; i++) {
  39. for (j = 0U; j < ARRAY_SIZE(data); j++) {
  40. data[j] = k++;
  41. }
  42. // 在改区域通过缓冲区的方式写入数据
  43. ret = flash_img_buffered_write(&ctx, data, sizeof(data), false);
  44. zassert_true(ret == 0, "image colletion fail: %d\n", ret);
  45. }
  46. zassert(flash_img_buffered_write(&ctx, data, 0, true) == 0, "pass",
  47. "fail");
  48. // 通过指定区域id的方式获取区域
  49. ret = flash_area_open(FLASH_AREA_ID(image_1), &fa);
  50. if (ret) {
  51. printf("Flash driver was not found!\n");
  52. return;
  53. }
  54. k = 0U;
  55. for (i = 0U; i < 300 * sizeof(data); i++) {
  56. // 从该区域获取数据
  57. zassert(flash_area_read(fa, i, &temp, 1) == 0, "pass", "fail");
  58. zassert(temp == k, "pass", "fail");
  59. k++;
  60. }
  61. #ifdef CONFIG_IMG_ERASE_PROGRESSIVELY
  62. uint8_t buf[sizeof(erase_buf)];
  63. // 从该区域获取数据
  64. ret = flash_area_read(fa, fa->fa_size - sizeof(buf), buf, sizeof(buf));
  65. zassert_true(ret == 0, "Flash read failure (%d)", ret);
  66. zassert_true(memcmp(erase_buf, buf, sizeof(buf)) == 0,
  67. "Image trailer was not cleared");
  68. #endif
  69. }

其实这里还可以直接操作SPI FLASH的方式将数据存放在改区域,只是已经给我门提供了更方便的调用,完全没必要在去操作SPI FLASH的API了。

设置升级标志

其实和设置升级标志相关的函数都存在与zephyr\subsys\dfu\boot\mcuboot.c里。

API 含义
boot_read_bank_header 从指定区域里读取头信息
boot_is_img_confirmed 检查image_0_primary_partition区域的确认标志是否设置
boot_write_img_confirmed 设置image_0_primary_partition区域的确认标志
mcuboot_swap_type 获取MCUboot的交换类型
boot_request_upgrade 设置交换标志,意味着下次重启的时候,就会将image_0_secondary_partition的内容和image_0_primary_partition的内容做交换,然后依然运行image_0_primary_partition的内容,这样就完成了升级
boot_erase_img_bank 擦除指定区域

boot_request_upgrade的介绍

函数原型:

  1. int boot_request_upgrade(int permanent);

这个函数的permanent参数为false时,下次重启时虽然会运行我们需要升级的程序,但如果我们在升级程序内没有调用boot_write_img_confirmed这个函数话,那第二次重启的时候系统就会回滚到未升级之前的程序。
如果permanent参数为true时,那无论是否在升级程序中调用boot_write_img_confirmed这个函数,升级程序都不会回滚。
关于boot_request_upgrade的实现如下:

  1. int boot_request_upgrade(int permanent)
  2. {
  3. #ifdef FLASH_AREA_IMAGE_SECONDARY
  4. int rc;
  5. rc = boot_set_pending(permanent);
  6. if (rc) {
  7. return -EFAULT;
  8. }
  9. #endif /* FLASH_AREA_IMAGE_SECONDARY */
  10. return 0;
  11. }
  12. int
  13. boot_set_pending(int permanent)
  14. {
  15. return boot_set_pending_multi(0, permanent);
  16. }
  17. int
  18. boot_set_pending_multi(int image_index, int permanent)
  19. {
  20. const struct flash_area *fap;
  21. struct boot_swap_state state_secondary_slot;
  22. uint8_t swap_type;
  23. int rc;
  24. rc = boot_read_swap_state_by_id(FLASH_AREA_IMAGE_SECONDARY(image_index),
  25. &state_secondary_slot);
  26. if (rc != 0) {
  27. return rc;
  28. }
  29. switch (state_secondary_slot.magic) {
  30. case BOOT_MAGIC_GOOD:
  31. /* Swap already scheduled. */
  32. return 0;
  33. case BOOT_MAGIC_UNSET:
  34. rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
  35. if (rc != 0) {
  36. rc = BOOT_EFLASH;
  37. } else {
  38. rc = boot_write_magic(fap);
  39. }
  40. if (rc == 0 && permanent) {
  41. rc = boot_write_image_ok(fap);
  42. }
  43. if (rc == 0) {
  44. if (permanent) {
  45. swap_type = BOOT_SWAP_TYPE_PERM;
  46. } else {
  47. swap_type = BOOT_SWAP_TYPE_TEST;
  48. }
  49. rc = boot_write_swap_info(fap, swap_type, 0);
  50. }
  51. flash_area_close(fap);
  52. return rc;
  53. case BOOT_MAGIC_BAD:
  54. /* The image slot is corrupt. There is no way to recover, so erase the
  55. * slot to allow future upgrades.
  56. */
  57. rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
  58. if (rc != 0) {
  59. return BOOT_EFLASH;
  60. }
  61. flash_area_erase(fap, 0, fap->fa_size);
  62. flash_area_close(fap);
  63. return BOOT_EBADIMAGE;
  64. default:
  65. assert(0);
  66. return BOOT_EBADIMAGE;
  67. }
  68. }
  69. int
  70. boot_set_pending_multi(int image_index, int permanent)
  71. {
  72. const struct flash_area *fap;
  73. struct boot_swap_state state_secondary_slot;
  74. uint8_t swap_type;
  75. int rc;
  76. rc = boot_read_swap_state_by_id(FLASH_AREA_IMAGE_SECONDARY(image_index),
  77. &state_secondary_slot);
  78. if (rc != 0) {
  79. return rc;
  80. }
  81. switch (state_secondary_slot.magic) {
  82. case BOOT_MAGIC_GOOD:
  83. /* Swap already scheduled. */
  84. return 0;
  85. case BOOT_MAGIC_UNSET:
  86. rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
  87. if (rc != 0) {
  88. rc = BOOT_EFLASH;
  89. } else {
  90. rc = boot_write_magic(fap);
  91. }
  92. if (rc == 0 && permanent) {
  93. rc = boot_write_image_ok(fap);
  94. }
  95. if (rc == 0) {
  96. if (permanent) {
  97. swap_type = BOOT_SWAP_TYPE_PERM;
  98. } else {
  99. swap_type = BOOT_SWAP_TYPE_TEST;
  100. }
  101. rc = boot_write_swap_info(fap, swap_type, 0);
  102. }
  103. flash_area_close(fap);
  104. return rc;
  105. case BOOT_MAGIC_BAD:
  106. /* The image slot is corrupt. There is no way to recover, so erase the
  107. * slot to allow future upgrades.
  108. */
  109. rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
  110. if (rc != 0) {
  111. return BOOT_EFLASH;
  112. }
  113. flash_area_erase(fap, 0, fap->fa_size);
  114. flash_area_close(fap);
  115. return BOOT_EBADIMAGE;
  116. default:
  117. assert(0);
  118. return BOOT_EBADIMAGE;
  119. }
  120. }

这里我之所以贴出这个函数的实现是因为公版就采用的操作区域的方式来设置了三个标志:

  • 交换标志
  • 魔幻数
  • 确认标志

这样其实时很不友好的。