以下以nrf52840
的DFU为例,这里不采用NRF5340
为例的原因是Nordic
在mcuboot
的源码中对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
:暂存槽&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x000000000 0x0000C000>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000C000 0x00067000>;
};
slot1_partition: partition@73000 {
label = "image-1";
reg = <0x00073000 0x00067000>;
};
scratch_partition: partition@da000 {
label = "image-scratch";
reg = <0x000da000 0x0001e000>;
};
/*
* The flash starting at 0x000f8000 and ending at
* 0x000fffff is reserved for use by the application.
*/
/*
* Storage partition will be used by FCB/LittleFS/NVS
* if enabled.
*/
storage_partition: partition@f8000 {
label = "storage";
reg = <0x000f8000 0x00008000>;
};
};
};
boot_partition
节点: 存放mcuboot的程序slot0_partition
节点: 存放运行的应用程序slot1_partition
节点: 存放要升级的应用程序scratch_partition
节点: 主要是为了拷贝slot1_partition
节点的程序到slot0_partition
节点中。
默认运行的应用程序插槽配置
上面FLASH
分区分配好了之后,需要在chosen
节点下设置默认运行的应用程序到底是哪个分区:
zephyr,code-partition = &slot0_partition;
应用程序的配置
在应用程序的Kconfig
中配置启用mcuboot
CONFIG_BOOTLOADER_MCUBOOT=y
imgtool介绍
imgtool
工具是mucboot
提供的生成签名文件和对应用程序进行签名的工具。其中工具所在路径为bootloader/mcuboot/scripts/imgtool.py
生成签名文件
这里我们采用mcuboot
提供的imgtool
工具生成一个签名文件:
./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|2|4|8] |
对其字节数 | 必须 |
-v, --version TEXT |
版本号 | 必须 |
-s, --security-counter TEXT |
计数器号 | 非必须 |
--pad-sig |
在1.5版本之前,需要在ECDSA签名中添加0-2字节的填充 | 非必须 |
--header-size |
中断矢量表的偏移大小,这个大小必须和Kconfig中的CONFIG_ROM_START_OFFSET 一致 |
必须 |
--slot-size |
运行插槽的大小 | 必须 |
指令格式:
imgtool.py <参数> <需要签名的文件名> <签名后的文件名>
应用程序的签名
这里可以直接使用mcuboot
提供的imgtool
工具,还可以使用zephyr
提供的west sign
工具,其实west
工具是对imgtool
工具做了一个外层的封装,底层还是在使用imgtool
。
这里我们直接采用imgtool
工具来进行应用程序的签名:
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_partition
和image_0_primary_partition
交换,交换完成后,最终还是在image_0_primary_partition
运行程序。
下载升级文件
下载文件的过程可以通过wifi
、BLE
或有线连接的方式,这里不做太多介绍。
存储文件
我们需要将下载的文件存储到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 | 验证区域完整性 |
示例:
void test_collecting(void)
{
const struct flash_area *fa;
struct flash_img_context ctx;
uint32_t i, j;
uint8_t data[5], temp, k;
int ret;
// 初始化flash img的变量,这里其实就在再找image_0_secondary_partition区域
ret = flash_img_init(&ctx);
//擦除该区域
#ifdef CONFIG_IMG_ERASE_PROGRESSIVELY
uint8_t erase_buf[8];
(void)memset(erase_buf, 0xff, sizeof(erase_buf));
// 这种方式是是另外一种获取升级区域的方式
ret = flash_area_open(FLASH_AREA_ID(image_1), &fa);
if (ret) {
printf("Flash driver was not found!\n");
return;
}
/* ensure image payload area dirt */
for (i = 0U; i < 300 * sizeof(data) / sizeof(erase_buf); i++) {
ret = flash_area_write(fa, i * sizeof(erase_buf), erase_buf,
sizeof(erase_buf));
zassert_true(ret == 0, "Flash write failure (%d)", ret);
}
/* ensure that the last page dirt */
ret = flash_area_write(fa, fa->fa_size - sizeof(erase_buf), erase_buf,
sizeof(erase_buf));
zassert_true(ret == 0, "Flash write failure (%d)", ret);
#else
// 直接擦除整个区域
ret = flash_area_erase(ctx.flash_area, 0, ctx.flash_area->fa_size);
zassert_true(ret == 0, "Flash erase failure (%d)", ret);
#endif
//直接在该区域写入一个字节
zassert(flash_img_bytes_written(&ctx) == 0, "pass", "fail");
k = 0U;
for (i = 0U; i < 300; i++) {
for (j = 0U; j < ARRAY_SIZE(data); j++) {
data[j] = k++;
}
// 在改区域通过缓冲区的方式写入数据
ret = flash_img_buffered_write(&ctx, data, sizeof(data), false);
zassert_true(ret == 0, "image colletion fail: %d\n", ret);
}
zassert(flash_img_buffered_write(&ctx, data, 0, true) == 0, "pass",
"fail");
// 通过指定区域id的方式获取区域
ret = flash_area_open(FLASH_AREA_ID(image_1), &fa);
if (ret) {
printf("Flash driver was not found!\n");
return;
}
k = 0U;
for (i = 0U; i < 300 * sizeof(data); i++) {
// 从该区域获取数据
zassert(flash_area_read(fa, i, &temp, 1) == 0, "pass", "fail");
zassert(temp == k, "pass", "fail");
k++;
}
#ifdef CONFIG_IMG_ERASE_PROGRESSIVELY
uint8_t buf[sizeof(erase_buf)];
// 从该区域获取数据
ret = flash_area_read(fa, fa->fa_size - sizeof(buf), buf, sizeof(buf));
zassert_true(ret == 0, "Flash read failure (%d)", ret);
zassert_true(memcmp(erase_buf, buf, sizeof(buf)) == 0,
"Image trailer was not cleared");
#endif
}
其实这里还可以直接操作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的介绍
函数原型:
int boot_request_upgrade(int permanent);
这个函数的permanent
参数为false
时,下次重启时虽然会运行我们需要升级的程序,但如果我们在升级程序内没有调用boot_write_img_confirmed
这个函数话,那第二次重启的时候系统就会回滚到未升级之前的程序。
如果permanent
参数为true
时,那无论是否在升级程序中调用boot_write_img_confirmed
这个函数,升级程序都不会回滚。
关于boot_request_upgrade
的实现如下:
int boot_request_upgrade(int permanent)
{
#ifdef FLASH_AREA_IMAGE_SECONDARY
int rc;
rc = boot_set_pending(permanent);
if (rc) {
return -EFAULT;
}
#endif /* FLASH_AREA_IMAGE_SECONDARY */
return 0;
}
int
boot_set_pending(int permanent)
{
return boot_set_pending_multi(0, permanent);
}
int
boot_set_pending_multi(int image_index, int permanent)
{
const struct flash_area *fap;
struct boot_swap_state state_secondary_slot;
uint8_t swap_type;
int rc;
rc = boot_read_swap_state_by_id(FLASH_AREA_IMAGE_SECONDARY(image_index),
&state_secondary_slot);
if (rc != 0) {
return rc;
}
switch (state_secondary_slot.magic) {
case BOOT_MAGIC_GOOD:
/* Swap already scheduled. */
return 0;
case BOOT_MAGIC_UNSET:
rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
if (rc != 0) {
rc = BOOT_EFLASH;
} else {
rc = boot_write_magic(fap);
}
if (rc == 0 && permanent) {
rc = boot_write_image_ok(fap);
}
if (rc == 0) {
if (permanent) {
swap_type = BOOT_SWAP_TYPE_PERM;
} else {
swap_type = BOOT_SWAP_TYPE_TEST;
}
rc = boot_write_swap_info(fap, swap_type, 0);
}
flash_area_close(fap);
return rc;
case BOOT_MAGIC_BAD:
/* The image slot is corrupt. There is no way to recover, so erase the
* slot to allow future upgrades.
*/
rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
if (rc != 0) {
return BOOT_EFLASH;
}
flash_area_erase(fap, 0, fap->fa_size);
flash_area_close(fap);
return BOOT_EBADIMAGE;
default:
assert(0);
return BOOT_EBADIMAGE;
}
}
int
boot_set_pending_multi(int image_index, int permanent)
{
const struct flash_area *fap;
struct boot_swap_state state_secondary_slot;
uint8_t swap_type;
int rc;
rc = boot_read_swap_state_by_id(FLASH_AREA_IMAGE_SECONDARY(image_index),
&state_secondary_slot);
if (rc != 0) {
return rc;
}
switch (state_secondary_slot.magic) {
case BOOT_MAGIC_GOOD:
/* Swap already scheduled. */
return 0;
case BOOT_MAGIC_UNSET:
rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
if (rc != 0) {
rc = BOOT_EFLASH;
} else {
rc = boot_write_magic(fap);
}
if (rc == 0 && permanent) {
rc = boot_write_image_ok(fap);
}
if (rc == 0) {
if (permanent) {
swap_type = BOOT_SWAP_TYPE_PERM;
} else {
swap_type = BOOT_SWAP_TYPE_TEST;
}
rc = boot_write_swap_info(fap, swap_type, 0);
}
flash_area_close(fap);
return rc;
case BOOT_MAGIC_BAD:
/* The image slot is corrupt. There is no way to recover, so erase the
* slot to allow future upgrades.
*/
rc = flash_area_open(FLASH_AREA_IMAGE_SECONDARY(image_index), &fap);
if (rc != 0) {
return BOOT_EFLASH;
}
flash_area_erase(fap, 0, fap->fa_size);
flash_area_close(fap);
return BOOT_EBADIMAGE;
default:
assert(0);
return BOOT_EBADIMAGE;
}
}
这里我之所以贴出这个函数的实现是因为公版
就采用的操作
区域的方式来设置了三个标志:
- 交换标志
- 魔幻数
- 确认标志
这样其实时很不友好的。