Aller au contenu

Mbed OS Bootloader

Introduction

Mbed OS Bootloader

In this codelab, we will first understand the basic principles for building a bootloader application on Mbed OS. Based on this, you will build a basic bootloader application that copies the new application in place. Ultimately, you will delve into more sophisticated methods for downloading and installing a new application in a more secure way.

What you’ll build

  • You will build a basic bootloader application for installing a new application while downloading it.
  • You will then separate the concern of downloading the application and installing it into two different components, for a more secure update mechanism.

What you’ll learn

  • The basic principles of a bootloader and of its implication on the Cortex-M4 Mbed OS program image.
  • You will understand how the boot sequence is modified for starting an application with a bootloader.

What you’ll need

The Bootloader Principle

A bootloader is an application whose primary goal is to allow a system or application software to be updated without the use of specialized hardware such as a JTAG programmer (or ST-Link for our development kit running the STM32 MCU). The bootloader downloads and manages the system or application images and installs them. In the remainder of this document, we will also call the system or application image the firmware.

Bootloaders can communicate using a variety of protocols for downloading firmwares and saving them into the non volatile memory of the embedded system. These protocols can be UART, Ethernet, USB or a wireless protocol for systems with wireless connectivity. Bootloaders are responsible for downloading new firmwares but also for verifying their integrity and authenticity.

Usually, systems with bootloaders have at least two firmware images coexisting on the non volatile memory of the MCU. They must also include code for branching installation of a new firmware image.

It is very common that the need for a bootloader mechanism is overlooked when conceiving embedded system software, because the bootloader is not the end product. However, the bootloader is an essential component of an embedded system. It allows to launch a system with software that only fulfills a subset of the final requirements and to add features to the product once it has already been launched to the market. And, even more importantly, it also allows the developers to fix bugs that are discovered over the lifetime of the product and to deploy corrections.

Writing a bootloader application requires an understanding of how a program image is built and how the MCU uses this memory. In this codelab, we will develop a simple bootloader and explain all components required for its development.

The Mbed OS Memory Model

From the lecture notes, recall that the Mbed OS memory model is as depicted in the figure below:

Mbed OS Memory Model

Mbed OS Memory Model

This is important to note that this is not the only applicable model and other memory models (where for instance the bootloader is not located before the application in ROM) are also possible. However, all Mbed OS tools for generating and managing bootloader applications assume that this memory model is applied.

The Mbed OS memory model for so called managed bootloaders assumes that:

  • The bootloader is located before the application in ROM, and
  • The application starts immediately after the bootloader.

Assuming this, a managed bootloader project automatically merges the bootloader image with the application image as part of the application image build process.

Boot sequences

In general, a boot sequence can have several stages, leading to the application. The different stages include several bootloader stages and the application itself. All stages may evolve over time and may thus be upgraded for adding new features and for correcting bugs. Upgrades are possible for boot sequences with two or more stages: any active stage can replace the next stage in the sequence and when the system restarts, it will then execute the updated components (including the updated application). Usually, the very first stage is not replaced because it takes control on startup and a faulty upgrade may thus be very difficult to recover.

Usually, for protecting against faults in a newly updated component, multiple versions of each stage are stored in the non volatile memory. Each stage is responsible for detecting faults in the next stage and for rolling back if a fault is discovered.

Our simple implementation

In this codelab, we will implement a simple boot sequence with two stages, the bootloader (that won’t be upgraded) and the application (that will be upgraded). It is important to note that usually most boot sequences are composed of three stages, with a root bootloader or boot selector that is not upgraded, together with a bootloader and an application stage, that are upgradable and have multiple versions stored on the device. So, in our implementation, the bootloader is both a boot selector and a bootloader capable of installing new images.

Make sure that USB is configured properly for your device

Read the instructions carefully under DISCO_H747 library. Without the documented changes, USB is not supported for the DISCO_H747I target and you won’t be able to use it for downloading firmware update candidates.

Modifying Your BikeComputer Application With a Bootloader

Adding a bootloader to your BikeComputer program

Before building your own bootloader application, it is useful to first modify your BikeComputer program so that it is merged with a bootloader image.

As explained earlier, the Mbed OS tools can manage bootloaders that fulfill the following requirements:

  • The bootloader comes before the application in ROM.
  • The application is located immediately after the bootloader.

In this case, the Mbed OS tools and build process can automatically merge the bootloader image with the application image. For adding a bootloader to your BikeComputer application, it is enough to specify the bootloader binary in the target_override section of the mbed_app.json application file as shown below

mbed_app.json
...
  "DISCO_H747I": {
    ...
    "target.bootloader_img":"./mbed-bootloader-advembsof.bin"
  }
...

The “target.bootloader_img” entry specified the path of the bootloader image. This image is distributed here and can be used for your target device. For this purpose, you must download the image and save it as “mbed-bootloader-advembsof.bin” at the root of your BikeComputer project.

If you build your application with the modified “mbed_app.json” file, you should observe the following output (or similar) in the output window:

Output window

Using ROM regions bootloader, application in this build.
Region bootloader: size 0x20000, offset 0x8000000
Region application: size 0xe0000, offset 0x8020000

With the bootloader configuration, the application is built for the address immediately after the bootloader. The build system does this automatically by redefining the MBED_APP_START and MBED_APP_SIZE in the linker script. In our case, the MBED_APP_START symbol is redefined to be MBED_APP_START=0x8020000. The build process also uses the following definitions

BUILD/DISCO_H747I/ARMC6/.profile-cxx
    "-DAPPLICATION_ADDR=0x8020000",
    "-DAPPLICATION_SIZE=0xe0000",
    "-DBOOTLOADER_ADDR=0x8000000",
    "-DBOOTLOADER_SIZE=0x20000",

At the end of the build process, the build image is automatically combined with the bootloader image to create the final image. From the “BikeComputer.elf” file, we may observe that the application itself is not located at ROM start anymore in the final image but rather at the 0x800a000 address:

** Section #1 'ER_IROM1' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
        Size   : 338400 bytes (alignment 8)
        Address: 0x8020000

This reflects the following code region memory layout:

Mbed OS Memory Model with a bootloader

Mbed OS Memory Model with a bootloader

If you flash your target device with this new program image, you should see the following output when the device starts

console

[DBG ][bootloader]: BikeComputer bootloader
[DBG ][bootloader]: Your bootloader should start application at address 0x08020000 (sp 0x24080000, pc 0x080204a1)

This logs demonstrates that the bootloader application is executed at startup. The bootloader application delivered as “mbed-bootloader-advembsof.bin” simply prints the expected address in memory for the BikeComputer program, the main stack pointer address and the expected PC for starting the BikeComputer program. It then loops forever (without starting the BikeComputer program).

Intermediate Wrap-up

Before moving to the next section, make sure that you have accomplished the following:

  • You have modified the “mbed_app.json” file of your BikeComputer application to include a bootloader.
  • You have downloaded the bootloader application and save it to the correct name and location.
  • You have understood how the code region memory layout has been modified.
  • You have flashed your target device with the updated application and observed that the bootloader application is executed at start time.

Writing Your First Bootloader Application

It is now time to create your own simple bootloader application. Creating a bootloader is not much different than creating a standard application. For creating your own bootloader application, you may thus:

  • Create a new application in your Mbed Studio environment and name it “mbed-os-bootloader”.
  • Copy the “mbed_app.json” and the “.mbedignore” files from your BikeComputer project.
  • Create a “main.cpp” file with an empty main() function.

A number of changes are required in the configuration of the bootloader application. First, you need to modify the “mbed_app.json” file by removing the "target.bootloader_img" property and adding the following property:

mbed_app.json
...
    "DISCO_H747I": {          
        "target.restrict_size": "0x20000",
        ...
    }
...

Adding this field for a bootloader application is required because it allows to restrict the bootloader code size to the specified size and to pad the bootloader application size to the size specified. It also defines the following symbols for the compilation in the bootloader application:

BUILD/DISCO_H747I/ARMC6/.profile-cxx
    "-DAPPLICATION_ADDR=0x8000000",
    "-DAPPLICATION_SIZE=0x20000",
    "-DPOST_APPLICATION_ADDR=0x8020000",
    "-DPOST_APPLICATION_SIZE=0xe0000",

By adding this field, the following code region layout is produced

Mbed OS Memory Model for the bootloader

Mbed OS Memory Model for the bootloader

This corresponds to the layout of the application shown in the previous step, but for this project the application is in fact the bootloader application. The use of the POST_APPLICATION_ADDR and POST_APPLICATION_SIZE symbols allows the bootloader to start the main program once it has finished its work.

Indeed, when the bootloader finishes its work, it needs to start the main application. To do this, it will power down any external components (e.g. serial port or network connection), and then make a call to the mbed_start_application() method with the base address of the next program image to start. Since Mbed OS applications are located immediately after the bootloader, one can use the POST_APPLICATION_ADDR as the starting address of the main application.

Based on this description, you can write your first bootloader application. This application does essentially nothing and always branches to the main application. Note that for restricting the dependencies and thus limiting the size of the bootloader application, logging is implemented by writing directly to the unbuffered serial output.

main.cpp
#include "mbed.h"

#include "mbed_trace.h"
#if MBED_CONF_MBED_TRACE_ENABLE
#define TRACE_GROUP "bootloader"
#endif // MBED_CONF_MBED_TRACE_ENABLE

#if MBED_CONF_MBED_TRACE_ENABLE
static UnbufferedSerial g_uart(CONSOLE_TX, CONSOLE_RX);

// Function that directly outputs to an unbuffered serial port in blocking mode.
static void boot_debug(const char *s)
{
    size_t len = strlen(s);
    g_uart.write(s, len);
    g_uart.write("\r\n", 2);
}
#endif

int main_initial()
{
#if MBED_CONF_MBED_TRACE_ENABLE
    mbed_trace_init();
    mbed_trace_print_function_set(boot_debug);
#endif // MBED_CONF_MBED_TRACE_ENABLE

    tr_debug("BikeComputer bootloader\r\n");

    // at this stage we directly branch to the main application
    void *sp = *((void **) POST_APPLICATION_ADDR + 0);  // NOLINT(readability/casting)
    void *pc = *((void **) POST_APPLICATION_ADDR + 1);  // NOLINT(readability/casting)
    tr_debug("Starting application at address 0x%08x (sp 0x%08x, pc 0x%08x)\r\n", POST_APPLICATION_ADDR, (uint32_t) sp, (uint32_t) pc);

    mbed_start_application(POST_APPLICATION_ADDR);

    return 0;
}

For integrating this bootloader in your BikeComputer program, you need to:

  • replace the image of the bootloader application by modifying the “target.bootloader_img” entry of the “mbed_app.json” file.
  • add a “target.app_offset” property in the “mbed_app.json” of your BikeComputer program as shown below. This change instructs the build system to locate the BikeComputer application at the offset 0x20000, after the bootloader image.
  • rebuild your BikeComputer program.
mbed_app.json
...
    "DISCO_H747I": {          
        "target.app_offset": "0x20000",
        "target.bootloader_img":"The path to your bootloader application image"
    }
...

You should the observe the following output on the console when starting your BikeComputer program on your target device:

console

[DBG ][bootloader]: BikeComputer bootloader
[DBG ][bootloader]: Your bootloader should start application at address 0x08020000 (sp 0x24080000, pc 0x080204a1)
[DBG ][main]: Program started
[DBG ][main]: Starting the bike system

As you can observe, the bootloader application is the application that is started at first in the boot sequence. This application simply branches to the BikeComputer program (pc at 0x080204a1) - remember the memory mapping presented in the Memory profiling codelab.

Intermediate Wrap-up

Before moving to the next section, make sure that you have accomplished the following:

  • You have created your first bootloader application, with the appropriate size.
  • You have understood the code region memory layout, both when building your bootloader application and when integrating your bootloader application into your BikeComputer program.
  • You have integrated your bootloader application into your BikeComputer program, you have been able to flash your target device and you can confirm that your BikeComputer program is starting correctly.

Checking Application Integrity And Authenticity

Verifying the integrity of the active application and of the updated firmware candidates is an important step of the bootloading process. This step is important for building safe and secure embedded applications.

Allowing firmware updates is an important feature of embedded systems. However, this also makes embedded systems vulnerable to attacks in which execution codes can be replaced or modified by malicious code during code updates. Furthermore, with knowledge of the memory and process architecture, the hacker can initiate an attack by inserting malicious code into the boot process.

Application Integrity and Application Metadata

Verifying the integrity and authenticity of an application can be implemented with a digital signature. The digital signature principle is explained in the figure below:

Digital Signature

Digital Signature

In our case, the signature is built for the application and the message is thus the application binary. This means that the signature must be computed at build time, transmitted to the device and verified by the device for making sure that the application update is not corrupted and authenticated. In this codelab, the signature will not be encrypted for the sake of simplicity and thus, only the integrity of the application will be verified. The principles for implementing authentication check remain however the same.

The Mbed OS tools allow the integration of an application metadata header in which information about the application is computed at build time. The application header is then prepended to the application and this metadata information can be used by the bootloader for verifying the integrity and authenticity of the application. For creating a metadata header, it is enough to add the following “mbed_lib.json” file at the root of both your bootloader and BikeComputer applications.

mbed_lib.json
{
  "name": "bootloader",
  "target_overrides": {
    "*": {
      "target.header_format": [
        ["magic", "const", "32be", "0x5a51b3d4"],
        ["version", "const", "32be", "2"],
        ["firmwareVersion", "timestamp", "64be", null],
        ["firmwareSize", "size", "64be", ["application"]],
        ["firmwareHash", "digest", "SHA256", "application"],
        ["hashpad", "const", "64be", "0"],
        ["hashpad", "const", "64be", "0"],
        ["hashpad", "const", "64be", "0"],
        ["hashpad", "const", "64be", "0"],
        ["campaign", "const", "64be", "0"],
        ["campaign", "const", "64be", "0"],
        ["firmwareSignatureSize", "const", "32be", "0"],
        ["headerCRC", "digest", "CRCITT32be", "header"]
      ]
    }
  }
}

The “mbed_lib.json” file is used by the build tools for computing the metadata application data and adding it to the application binaries. This file describes what fields must be added to the application header and how each field is configured. More details about the syntax used can be found on bootloader configuration.

When adding the metadata header in your application, you must also modify the “mbed_app.json” file of your BikeComputer application, by replacing the target.app_offset field by a target.header_offset field (with the same value) as shown below. This tells the build tools to insert the header at the given offet (and not the application itself).

mbed_app.json
...
    "DISCO_H747I": {          
        "target.header_offset": "0x20000"
    }
...

Adding the metadata header also defines the following symbols that can be used by the applications. Note that the HEADER_SIZE symbol does not represent the size with the required padding (for alignment purposes) and that it must thus be recomputed in the program code - which will be done later in this codelab.

BUILD/DISCO_H747I/ARMC6/.profile-cxx
...
    "-DHEADER_ADDR=0x8020000",
    "-DHEADER_SIZE=0x70",
...

Important

By default, the Mbed OS build tools do NOT merge the metadata header with the application binary for the update application (the “BikeComputer_update.bin” file). This file is the candidate application that will be downloaded to the target device and it must contain the metadata header. For doing so and prepending the binary with the metadata header, you need to modify the “mbed-os/tools/regions.py” file at line 35 as follows:

UPDATE_WHITELIST = (
     "header",
     "application"
)      

After performing all changes above, you should observe the following outputs when building the bootloader application:

Bootloader build

Using ROM regions application, header, post_application in this build.
Region application: size 0x20000, offset 0x8000000
Region header: size 0x70, offset 0x8020000
Region post_application: size 0xdff80, offset 0x8020080
...
Merging Regions
Filling region application with BUILD/DISCO_H747I/ARMC6\mbed-os-bootloader_application.bin
Space used after regions merged: 0xe178

and the following output when building the BikeComputer application:

BikeComputer build

Using ROM regions bootloader, header, application in this build.
Region bootloader: size 0x20000, offset 0x8000000
Region header: size 0x70, offset 0x8020000
Region application: size 0xdff80, offset 0x8020080
...
Merging Regions
Filling region bootloader with ../mbed-os-bootloader/BUILD/DISCO_H747I/ARMC6/mbed-os-bootloader.bin
Filling region header with BUILD/DISCO_H747I/ARMC6\bike-computer_header.hex
Filling region application with BUILD/DISCO_H747I/ARMC6\bike-computer_application.bin
Space used after regions merged: 0x73794
Merging Regions
Filling region header with BUILD/DISCO_H747I/ARMC6\bike-computer_update_header.hex
Filling region application with BUILD/DISCO_H747I/ARMC6\bike-computer_application.bin
Space used after regions merged: 0x53794

As you can observe, both the bootloader and BikeComputer programs receive the same information for building the metadata header. The bootloader will use this information for checking the application integrity while the information is used for building two different binaries of the BikeComputer program:

  • The first binary is named “BikeComputer.bin” is a merge of the bootloader binary, of the metadata header and of the BikeComputer application binary. This binary is used when flashing the application for the first time.
  • The second binary is named “BikeComputer_update.bin” is a merge of the metadata header and of the BikeComputer application binary. This binary is used as an application update that will be installed through an update client.

At this stage, you should be able to build the bootloader application and the BikeComputer applications with the metadata header - you should observe carefully the output at build time and check that your configuration is correct. You should be able to start your application and observe an output on the console similar to:

console

[DBG ][bootloader]: BikeComputer bootloader
[DBG ][bootloader]: Your bootloader should start application at address 0x08020080 (sp 0x24080000, pc 0x08020521)
[DBG ][main]: Program started
[DBG ][main]: Starting the bike system

The memory model of your application with the application metadata header is depicted in the figure below. This memory model must be well understood for programming the next steps of the bootloader application. From the bootloader perspective, the memory model is:

Mbed OS Memory Model for the bootloader (with header)

Mbed OS Memory Model for the bootloader (with header)

From the BikeComputer application perspective, it is

Mbed OS Memory Model for the application (with header)

Mbed OS Memory Model with a application (with header)

Note that the memory models are of course the same and that the only differences are in the names of the addresses, which are used in either the bootloader or the BikeComputer program.

Checking the Application Integrity At Boot Time

The very first step in the boot process is to verify the integrity of the active application. Now that the metadata has been prepended to the application and flashed to your target device, the bootloader application can use this information for checking the integrity of the active application.

The “update_client” library

For implementing this mechanism (and all other bootloading and update mechanisms), you must import the Update Client Mbed OS library into your bootloader application. This library provides components that allows an easier implementation of the bootloader and firmware update processes.

For integrating and develop your own components, you need to understand the following regarding the symbols used by the library:

  • All Mbed OS libraries may contain a “mbed_lib.json” file that defines symbols that are used in the library code and that may be redefined by applications using the library. For the “update-client” library, the “mbed_lib.json” file is shown below:
mbed_lib.json
{
    "name": "update-client",
    "config": {
        "storage-address": {
            "help": "When using address based storage (FlashIAP, Block Device), this is the starting address.",
            "value": "0"
        },
        "storage-size": {
            "help": "Total storage allocated for firmware candidates.",
            "value": "0"
        },
        "storage-locations": {
            "help": "Number of equally sized locations the storage space should be split into.",
            "value": "1"
        }
    }
}
  • The parameters of the “update-client” library for the bootloader and BikeComputer applications need to be defined in the “mbed_app.json” file of each application:

    mbed_app.json
    {
      "macros": [
        ...
        "MBED_BOOTLOADER_FLASH_BANK_SIZE=MBED_ROM_SIZE/2"
        ...
      ],
      ...
      "target_overrides": {
        "*": {
          ...
          "update-client.storage-address": "(MBED_BOOTLOADER_FLASH_BANK_SIZE)",
          "update-client.storage-size": "(MBED_BOOTLOADER_FLASH_BANK_SIZE)",
          "update-client.storage-locations": 2               
        },
        ...
      }
    }
    
    The meaning of each symbol should be self-explanatory. Help information in the “mbed_lib.json” file of the library should also help in the understanding of these symbols that all relate to the storage used for storing candidate applications downloaded by the update client. In more details here, these definitions instruct the bootloader and BikeComputer programs to:

    • Use half of the ROM size for storing firmware candidates and start to store firmware candidates at the position in the middle of the ROM size.
    • Store a maximum of two firmware candidates.

The “update_client” library implements several classes that will be used by both the bootloader and the BikeComputer applications:

  • The BlockDeviceApplication class defined in the library is a class representing an Mbed OS application binary stored on the non volatile memory of the device, with its metadata header. An Mbed OS application can be either the active application or a candidate application stored on the target device. The class provides various methods that are used by the bootloader application and the update client.
  • The CandidateApplications class defined in the library is a class representing all candidate applications - not active - that are stored on the device. The class is used by both the bootloader application and the BikeComputer application. It helps both applications to deal with candidate applications.
  • The USBSerialUC class is a class implementing an update client using the USB serial connection of the target device. This class is used by the BikeComputer program for receiving a binary update over USB serial and writing this binary to the internal flash as a candidate application. For using this class, you must add the "USE_USB_SERIAL_UC=1" macro in the “mbed_app.json” file of your BikeComputer application.
  • The “uc_error_codes.hpp” file contains the definition of the return codes used by the different classes.

Verifying Integrity in the Bootloader Application

As explained above, the very first step is to modify the bootloader application such that it verifies the integrity of the active application. For verifying the integrity, the bootloader application and the “update_client” library must be modified as follows:

  • In your bootloader application, create a FlashIAPBlockDevice instance as FlashIAPBlockDevice flashIAPBlockDevice(MBED_ROM_START, MBED_ROM_SIZE)and initialize it properly using the FlashIAPBlockDevice::init() method. This instance allows a program to read and write to/from the Flash memory.
  • Upon successful initialization of the FlashIAPBlockDevice instance, create an BlockDeviceApplication instance representing the active application. For creating this instance, you must specify the addresses of the metadata header and of the application itself. These addresses are defined at compile time with predefined symbols as explained earlier.
  • Once the BlockDeviceApplication instance is created, you may call its checkApplication() method for checking the integrity of the active application.

After implementing the steps above, you should observe an output similar to this one in the console:

console

[DBG ][bootloader]: Bootloader
[DBG ][bootloader]: Checking active application
[DBG ][BlockDeviceApplication]: Parsing header read at address 0x00020000 starting with 0x5a
[DBG ][BlockDeviceApplication]:  headerVersion 2, calculatedChecksum  0x48f17e8f, firmwareVersion 1701762601, firmwareSize 341640
[DBG ][BlockDeviceApplication]:  Application size is 341640
[DBG ][BlockDeviceApplication]:  Calculating hash (start address 0x00020080, size 341640)
[DBG ][bootloader]:  Active application is valid

Intermediate Wrap-up

Before moving to the next section, make sure that you have accomplished the following:

  • You have added the “mbed_lib.json” file at the root of both bootloader and BikeComputer applications. This is necessary for adding the application metadata header to your application.
  • You have understood how the memory model of your application is modified when integrating the header.
  • You have added the Update Client Mbed OS library to both applications.
  • You have integrated the active application integrity check into your bootloader and after integrating this bootloader application into your BikeComputer program, you have observed that the active application is validated.

Downloading And Storing Firmware Candidates

The next step required for allowing firmware updates is to establish a procedure for downloading and installing firmware updates. Downloading may happen using different protocols and, with our target device, we could use WiFi, Bluetooth or a serial connection over USB. For the sake of simplicity, we will use the latter one.

For storing firmware candidates, there may be several possibilities, depending on the target device. On our target device, we could:

  • Use the internal flash, by using a portion reserved for storing firmware candidates.
  • Use the QSPI NOR Flash memory that is installed on the DISCO target device. This memory offers a capacity of 64 MiB.

Again, for the sake of simplicity and because the QSPI NOR Flash memory may not be available on other devices, we will also store the firmware candidates on the internal flash. For this purpose, we will organize the internal flash memory as follows:

Mbed OS Memory Model with update candidates

Mbed OS Memory Model with update candidates

The implementation of a firmware downloader using the USBSerial interface is given in the USBSerialUC class. You must use this class for instantiating a downloader in your BikeComputer program and for launching it as a separate thread (using the start() method). The code to be added to the main() function of your BikeComputer program looks like:

main.cpp
...
FlashIAPBlockDevice flashIAPBlockDevice(MBED_ROM_START, MBED_ROM_SIZE);
update_client::USBSerialUC usbSerialUpdateClient(flashIAPBlockDevice);
update_client::UCErrorCode rc = usbSerialUpdateClient.start();
if (rc != update_client::UCErrorCode::UC_ERR_NONE) {
    tr_error("Cannot initialize update client: %d", rc);
} else {
    tr_info("Update client started");
}
...

Defining your own CandidateApplications class

In the implementation of the “update_client” library, the method CandidateApplications::getSlotForCandidate() returns the slot that should be used for storing the application candidate when downloading it. The implementation in the library always returns 0. You must modify this behavior and implement a method that always returns a slot that is not used (does not contain a valid candidate application) or the slot containing the oldest application candidate. Since the “update_client” library is delivered as an external library for your application, you must implement this change in your application and not in the library itself. For doing so, you must:

  • declare and define your own MyCandidateApplications class that inherits from update_client::MyCandidateApplications library.
  • override the CandidateApplications::getSlotForCandidate() method for implementing the required behavior.
  • redefine your own createCandidateApplications factory function. This factory function is used by the USBSerialUC class for creating an instance that allows to store the candidate applications when downloading it. It is defined in the “update_client” library using the MBED_WEAK symbol and you must redefine it in your own application for using your own MyCandidateApplications class in the USBSerialUC class.
  • the use of MBED_WEAK symbol allows to use your own implementation. Functions marked as weak in the source file will not be used if another implementation exists. The createCandidateApplicationsis thus marked as weak in the “candidate_applications.cpp” source file and if you define your own implementation of the createCandidateApplications function, it will be used instead of the one provided by the library.

Note that these changes are required only for your BikeComputer program and not for the bootloader application - the CandidateApplications::getSlotForCandidate() is not used by the bootloader application.

Once you have completed the implementation of your MyCandidateApplications class and of your own createCandidateApplications factory function, you may test the firmware downloader into your BikeComputer program. For downloading a firmware, you must:

  • connect your target device to your PC using the USB CN1 connector (see DISCO H747I hardware layout, page 11).
  • use a terminal on your PC for sending a firmware update as a binary file to the target device. The firmware update binary is the “BikeComputer_update.bin” file located in the “BUILD” directory. Note that this binary should be prepended with the metadata header, as explained earlier in the codelab.

When establishing the USBSerial connection, you should observe an output similar to the one below on your console:

Console


[DBG ][USBSerialUC]: Updater connected
[DBG ][USBSerialUC]: Application header size is 128
[DBG ][CandidateApplications]: Slot 0: application header address: 0x08080000 application address 0x08080080 (slot size 262144)
[DBG ][CandidateApplications]: Slot 1: application header address: 0x080c0000 application address 0x080c0080 (slot size 262144)
[DBG ][USBSerialUC]: Reading application info for slot 0
[DBG ][MbedApplication]: Magic -1, Version -1
[DBG ][USBSerialUC]: Using slot 0 and starting to write at address 0x08080000 with sector size 2048 (aligned 0) [DBG ][USBSerialUC]: Please send the update file

This output shows that there are two slots available for firmware candidates and it gives details about each slot. Once you start the download, you should observe the download progress. At the end, you must disconnect the USBSerial connection and you should observe an output similar to this one:

Console


[DBG ][MbedApplication]: Comparing applications at address 0x08040080 and 0x08080080
[DBG ][MbedApplication]: Checking application at address 0x08040080
[DBG ][MbedApplication]: headerVersion 2, firmwareVersion 1668178788, firmwareSize 97488
[DBG ][MbedApplication]: Application size is 97488
[DBG ][MbedApplication]: Calculating hash (start address 0x08040080, size 97488)
[DBG ][MbedApplication]: Checking application at address 0x08080080
[DBG ][MbedApplication]: headerVersion 2, firmwareVersion 1668178814, firmwareSize 97504
[DBG ][MbedApplication]: Application size is 97504
[DBG ][MbedApplication]: Calculating hash (start address 0x08080080, size 97504)
[DBG ][MbedApplication]: Both applications are valid
[DBG ][MbedApplication]: Firmware sizes differ
[DBG ][MbedApplication]: Firmware versions differ
[DBG ][MbedApplication]: Hash differ
[DBG ][USBSerialUC]: Nbr of bytes received 97640
[DBG ][USBSerialUC]: Waiting for connection

After downloading the application, the new firmware candidate is validated and compared to the active application. From the output above, you can see that the newly downloaded firmware is valid and different from the active one. After validating the downloaded firmware, the update client waits again for connection and another candidate firmware can be downloaded. If another firmware is downloaded, then it should be stored to the other slot and your target device should then have stored two copies of the candidate firmwares in the two available slots.

Note

Everytime you flash your target device, the entire ROM is erased.

So any firmware downloaded using a version of the program from a previous flash will also be erased.

Note

In this implementation, we assume that the maximum application size is never exceeded. A more robust implementation should check for a maximum application size value at download and installation time.

Intermediate Wrap-up

Before moving to the next section, make sure that you have accomplished the following:

  • You have modified the “mbed_app.json” file of both applications for adding the values of the fields required by the update client library.
  • You have understood how the memory model of your application is modified for allocating space for the firmware candidates.
  • You have completed the implementation of tyour MyCandidateApplications class and of your own createCandidateApplications factory function.
  • You have integrated the firmware downloader into your BikeComputer program.
  • You have performed a firmware update download successfully and observed that the downloaded firmware is valid.
  • You are able to download two firmware candidates to your device, using the two slots available for candidate applications.

In this version of the update client, the device is not automatically rebooted after the successful download of a firmware. You can optionally modify the update client so that it automatically reboots after a successful firmware download that is also validated. In the current implementation, reboot must be accomplished by hitting the reset button on the device.

Installing A Firmware Candidate At Boot Time

The last step of the update process is the installation of a firmware candidate. This task is accomplished by the bootloader application and not by the BikeComputer program itself. Recall the entire bootloader and application process:

Bootloader and application process

Bootloader and application process

In the previous steps, the integrity check task has been added to the bootloader application and the download firmware task has been added to the BikeComputer application. The remaining tasks are the one for checking for the availability of a new firmware candidate and the installation of this new firmware. Both tasks are accomplished by the bootloader before it eventually jumps to the main application.

For implementing the installation of the new firmware, you must add a number of steps in your bootloader application (in the main() function). This can be done as follows:

  • Create a CandidateApplications instance on the stack with the parameters corresponding to the memory configuration.
  • Check whether a newer, valid firmware candidate is available by calling the CandidateApplications::hasValidNewerApplication() method. This method returns true when a firmware candidate with a newer version is available and in this case, it also returns the slot index of this firmware candidate.
  • If a new firmware candidate is available, it must then be installed by calling the CandidateApplications::installApplication() method. Note that if the active application is not valid and a valid firmware candidate is stored, this firmware candidate must also be installed.

Final Wrap-up

For obtaining a fully functional bootloading process, make sure that you have accomplished the following:

  • You have added the code for checking the availability of new firmware candidates in the bootloader application.
  • You have added the code for installing a new valid firmware candidate.
  • You have validated the entire process and that you can thus update your BikeComputer application over a serial connection using an update client unning in the BikeComputer program itself.

Note

The implementation of the bootloader process should be more resilient to errors like the failure to install a new firmware. As already mentioned, it should also protect itself for not exceeding storage spaces and against attacks by authenticating the applications binaries.

Also, like any embedded system program, the BikeComputer program should integrate a watchdog mechanism for protecting itself from being locked (in this case in the download process).

These improvements are beyond the scope of this codelab and lesson.