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

  • Hi,

    The short, but hopefully good enough answer, to the questions is that it acts similarly to Direct XiP DFU, i.e that there are no swapping of the image between the slot where the existing image resides and the new update image resides. The architecture with s0 and s1 slots, which you've probably seen already, is illustrated in https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/samples/bootloader/README.html#flash_memory_layout 

    NSIB, the first stage bootloader, checks s0 and s1 for any second stage bootloader image present. If there is an image present in both of these slot, it will check the version number on these two images and then select the slot with the highest/newest revision number. 

    This is somewhat elaborated on here https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/config_and_build/bootloaders/bootloader.html#immutable-bootloader 

    For your first iteration of firmware you will typically have a second stage bootloader present in S0. When you then need to update your second stage bootloader you would upload the new mcuboot image that is built for S1 and upload it to S1. Since this image has a newer version than the old one, NSIB will then select to boot MCUboot from S1 instead of S0.

    The reason for this is because of how NSIB is implemented w.r.t revert and fallback protection AFAIK to better protect the device from downgrade attacks.

    This link should also give a pointer to what the various build output files are (including the s0/s1 variants) https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/config_and_build/configuring_app/output_build_files.html 

    Let me know if this short answer answers your question and/or if you have any additional followups/things I've explained poorly.

    Kind regards,
    Andreas

  • 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

  • Hi,

    Glad to hear that it was helpful!

    believe that the answer to your follow up question is answered by the DFU/FOTA lesson on our academy pages https://academy.nordicsemi.com/courses/nrf-connect-sdk-intermediate/lessons/lesson-8-bootloaders-and-dfu-fota/, but I don't remember the contents from cover to cover, so I'll chime in with some additional comments as well:

    Dax-id said:
    • 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?

    My assumption is that here you refer to the "upgrade code" as either the code to initiate DFU and perform it or "the new update image". I'm going to assume it is the first one, so correct me if I'm wrong and I'll explain for the other case as well

    This is explained in https://academy.nordicsemi.com/courses/nrf-connect-sdk-intermediate/lessons/lesson-8-bootloaders-and-dfu-fota/topic/device-firmware-update-dfu-essentials/ where it is stated that you can choose either to have the upgrade code initiating the update in the bootloader or in the application. In either case you will have to have some sort of way to enter the bootloader mode, for instance with a button press, power cycle where you hold a button for certain seconds when you power on or through a BLE SMP server initiating the DFU mode.

    I think for now the course I linked is the best entry point since it has explanations, graphics crosslinks to our official documentation as well as hands on exercises for you to have a look at that are more minimal than the samples in the SDK which supports DFU

    Why nothing happens when sending s0 image when the device currently uses s0 as the bootloader slot?

    I also noticed this question in your description. This is implied in my previous description but I'll state it explicetly as well: An new image will not be "accepted" as a new image by the bootloader to run from/swap into, both for the case of application image and second stage bootloader image, unless there has been made changes to the firmware (it can be as little as a log/print statement modification or simply changing the fw version number) due to a checksum comparison. 

    Let me know if this answers your questions

    Kind regards,
    Andreas

  • Oh and by the way, I just noticed this was a private case. FYI in the future this is a typical case that would've benefited from being a public case since it's a question that many other developers are asking as well :) 

    Nothing wrong with creating a private case, specially as your first one, but private cases are typically meant for cases where you share sensitive information with us w.r.t debugging and development

    Kind regards,
    Andreas

  • Thanks for these additional details.

    In our setup, we really need the upgrade to come from the SD-Card, so I'm thinking placing the upgrade code in the application is the way to go, while keeping a safety-net by enabling Serial recovery in the SBL.

    We are still discussing with our client to know if an upgradable SBL is really needed or not in the project.

    Concerning the sending s0 image while SBL runs from s0, I still don't fully understand why it won't work if I changed the SBL that I'm sending. In my tests, I changed both a log print in MCUBoot as well as incrementing the FW_INFO_FIRMWARE_VERSION config. Is it because it then has nothing to compare to validate that the image has changed?

    Best regards

    David TRUAN

Related