Understanding the S0 vs S1 MCUBoot images and slots

Hello,

We are currently helping one of our client develop a product using the nRF5340.

DK versions:

  • nRF5340-DK
  • PCA10095
  • 2.0.1

Host OS:

  • Ubuntu 22.04.2
  • Linux 6.5.0-41-generic

nRF SDK and toolchains version:

  • nRF SDK 2.6.1
  • toolchains 2.6.1

We need to implement a custom upgrade process, where the application, or the bootloader if possible, would check if an upgrade file exists on the SD-Card, and flash if existing and valid. Both bootloader and application must be upgradable.

We currently have implemented a dual bootloader setup, with an application which reads the update file from the UART (no SD-card at the moment, so purely evaluation purpose) and uses DFUTarget to flash the memory chunk by chunk. This application can upgrade itself without any issue using the build/zephyr/app_update.bin file as the upgrade file sent via UART. I attach you the application and the prj.conf. 

Now I'm trying to understand how MCUBoot is upgraded. From what I read, it uses both S0 and S1 slots, and only boots the latest valid MCUBoot flashed in one of those. 

I can successfully upgrade MCUBoot by alternating with the s0 and s1 update file that I send to my test application implementing DFUTarget. I need clarification about the usage of those two images and how they are flashed:

  • Why is it needed to switch between build/zephyr/signed_by_mcuboot_and_b0_s0_image_update.bin and build/zephyr/signed_by_mcuboot_and_b0_s1_image_update.bin?
  • How will it then be possible to manage multiple devices when deployed on field?
  • Should we always send both and do a check of which on is currently in use to select the correct one to flash? 
  • Why nothing happens when sending s0 image when the device currently uses s0 as the bootloader slot?

Thanks a lot in advance for you help, and please correct me if I'm wrong on any topic I covered asking my questions!

Best regards

David TRUAN

#include <stdio.h>
#include <string.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/crc.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/reboot.h>

#include <dfu/dfu_target.h>
#include <dfu/dfu_target_mcuboot.h>

#include <errno.h>

#include <zephyr/device.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/storage/flash_map.h>
#include <logging.h>

#define PRG_BUFSIZ 32000
#define VERSION 1
#define CHUNK_SZ	1024

#define MCUBOOT_BUF_SZ	512

static const struct device *uart_dev;

/* The buffer to receive  */
static char mcuboot_buf[MCUBOOT_BUF_SZ] __aligned(4);

/**
 * @brief Helper to read the update size using UART
 *
 * @param uart_dev the UART device to use to read
 * @return int32_t >= 0 on success, -1 otherwise
 */
int32_t read_update_size_uart(const struct device *uart_dev) {

	int err;
	uint32_t prg_size = 0;
	uint32_t bytes_read = 0;
	uint8_t byte;


	do {

		err = uart_poll_in(uart_dev, &byte);

		if (err == 0) {

			prg_size |= (int)byte << (bytes_read * 8);
			bytes_read++;

		} else if (err == -1) {

			continue;
		} else {
			log_error("Error detected %d", err);
			return -1;
		}

	} while (bytes_read != sizeof(uint32_t));


	return prg_size;
}

/**
 * @brief Callback handler for DFUTarget events
 *
 * @param evt the event which called the callback
 */
static void dfu_target_callback_handler(enum dfu_target_evt_id evt) {

	switch (evt) {

	case DFU_TARGET_EVT_TIMEOUT:

		printf("DFU_TARGET_EVT_TIMEOUT");
		break;

	case DFU_TARGET_EVT_ERASE_DONE:

		printf("DFU_TARGET_EVT_ERASE_DONE");
		break;

	case DFU_TARGET_EVT_ERASE_PENDING:

		printf("DFU_TARGET_EVT_ERASE_PENDING");
		break;
	}
}

int main(void) {

	static char prg_data_buf[CHUNK_SZ];
	char *prg_data;
	int update_size_full;
	int err;
	int total_bytes_read;

	uart_dev = DEVICE_DT_GET(DT_NODELABEL(uart0));
	prg_data = prg_data_buf;
	total_bytes_read = 0;
	update_size_full = 0;

	k_msleep(1000);

    log_set_level(LOG_LEVEL_DEBUG);


	log_info("current version: %d board: %s", VERSION, CONFIG_BOARD);
	log_info("Waiting for playload...");

	/* Retrieve the update size */
	update_size_full = read_update_size_uart(uart_dev);
	if (update_size_full == -1) {

		log_error("Error while getting update size!");

		return 1;
	}

	log_info("Update size: %d", update_size_full);

	/* Set the buffer used by DFUTarget in MCUBoot mode */
	if ((err = dfu_target_mcuboot_set_buf(mcuboot_buf, MCUBOOT_BUF_SZ)) != 0) {

		log_error("dfu_target_mcuboot_set_buf error: %d", err);

		return -1;
	}

	/* Initialize the DFUTarget library with our image info */
	if ((err = dfu_target_init(DFU_TARGET_IMAGE_TYPE_ANY_APPLICATION, 0, update_size_full, dfu_target_callback_handler)) != 0) {

		log_error("dfu_target_init error: %d", err);

		return -1;
	}


	while (1) {
		err = uart_poll_in(uart_dev, prg_data);

		if (err == 0) {

			prg_data++;
			total_bytes_read++;

			if (total_bytes_read == update_size_full) {

				log_info("Received the full file!");

				dfu_target_write(prg_data_buf, total_bytes_read % CHUNK_SZ);

				break;
			}


			if (total_bytes_read % CHUNK_SZ == 0) {
				dfu_target_write(prg_data_buf, CHUNK_SZ);
				prg_data = prg_data_buf;
			}

		} else if (err == -ENOSYS) {

			log_error("Error RX");

			return 1;
		} else if (err == -EBUSY) {

			log_error("Error UART busy!");

			return 1;
		}
	}

	/* Ask DFU target to clean its data */
	if ((err = dfu_target_done(true)) != 0) {

		log_error("dfu_target_done failed: %d", err);

		return 1;
	} else {

		log_info("DFU target download done!");
	}

	/* Indicate to DFUTarget that the update is ready
	and reboot to boot into the updated app if successful */
	if ((err = dfu_target_schedule_update(-1)) != 0) {

		log_error("dfu_target_schedule_update failed: %d", err);

		return 1;
	} else {

		log_info("DFU target successfully scheduled the update!");

		sys_reboot(SYS_REBOOT_WARM);
	}

	return 0;
}
5488.prj.conf

Parents
  • Hi Andrea,

    Thanks a lot for these explanations!

    It is now much clearer. I also had a better look at the source code and the FOTA example. I copied the read_s0_active() function to check which one is currently in use and I'll always store both s0 and s1 variants in the upgrade package, which will then be chosen depending on read_s0_active()

    Now I have another question related: 

    For the upgrade solution we're building, what is the nRF/Zephyr planned way to do so?

    • Have the upgrade code residing in the Application code, which would run only once per boot when the app is booted.
    • Have the upgrade code residing in the second stage bootloader (MCUBoot), also ran only once per boot. Is it even possible to "add" some processing to MCUBoot without modifying the SDK to much? I read about the MCUBoot hooks and will have to dig a bit here, but is it planned to do such operations here?

    Again thanks for your time and clear answer!

    Best regards,

    David TRUAN

Reply
  • Hi Andrea,

    Thanks a lot for these explanations!

    It is now much clearer. I also had a better look at the source code and the FOTA example. I copied the read_s0_active() function to check which one is currently in use and I'll always store both s0 and s1 variants in the upgrade package, which will then be chosen depending on read_s0_active()

    Now I have another question related: 

    For the upgrade solution we're building, what is the nRF/Zephyr planned way to do so?

    • Have the upgrade code residing in the Application code, which would run only once per boot when the app is booted.
    • Have the upgrade code residing in the second stage bootloader (MCUBoot), also ran only once per boot. Is it even possible to "add" some processing to MCUBoot without modifying the SDK to much? I read about the MCUBoot hooks and will have to dig a bit here, but is it planned to do such operations here?

    Again thanks for your time and clear answer!

    Best regards,

    David TRUAN

Children
No Data
Related