What's a bootloader?

In the context of embedded programming, a bootloader is a small program which is executed first when the microcontroller starts and which can do two things : either take control of the execution in order to offer some features (this is call bootloader mode), or pass the execution to another program stored in memory (called the user program). The features offered in bootloader mode usually involve overwriting the user program with a new version that can be sent through different means.

libtungsten offers a customizable bootloader that can be used to update the microcontroller's program via USB or a serial port. As discussed in the JTAG/SWD vs Bootloader page, this is extremely useful because it allows you to test your program quickly by simply hooking your development board (such as a Carbide) to one of your computer's USB port. Of course, libtungsten also provides the computer-side tool to upload your code into the microcontroller, modestly called codeuploader. All these features are discussed below and are offered through the library Makefile using rules such as make flash-bootloader and make upload.

For more advanced applications, the bootloader can be customized to provide interesting features such as Over-The-Air update of your device's firmware by interfacing it with a Bluetooth or WiFi chipset. Take a look at bootloader.cpp and bootloader_config.h for more information.

Usage

In order to use the bootloader and upload some code into the microcontroller, you first need to make sure that a bootloader is actually flashed into the microcontroller, and you need to know what is its configuration to start it. If you have a JTAG/SWD adapter, take a look at the bootloader_config.h file and use make flash-bootloader. Otherwise, if this is a board shipped with a pre-installed bootloader, the bootloader configuration should be given by the supplier but will usually be :

  • MODE_INPUT = true
  • MODE_TIMEOUT = false
  • CHANNEL_USB_ENABLED = true
  • CHANNEL_USART_ENABLED = true

With the default bootloader, bootloader mode will be activated at start-up when any of these 4 conditions is met :

  • If MODE_INPUT is set and the specified line is asserted. On a Carbide, this is usually done by keeping the button pressed while turning on the board. Other use cases can also be implemented, see the related comment in the bootloader configuration file for more information.
  • If MODE_TIMEOUT is set, the bootloader will always be activated at start-up, but will exit after the configured time of inactivity (3 seconds by default). This can be useful if there is no other way to start the bootloader (for example, the microcontroller is embedded in a project and only a serial port is accessible) and the added boot time is not a problem. In this scenario, the main idea is to power up the whole system while quickly executing make upload to connect to the bootloader before the timeout expires.
  • If the bootloader detects by different means that there is no user program available. For example, the bootloader is smart enough to detect if a previous upload failed and will activate itself to prevent executing an incorrect program.
  • If the FUSE_BOOTLOADER_FORCE is set. This is used by Core::resetToBootloader().

When the bootloader is started, use the provided Makefile rules to upload your program :

  • make upload to use the USB connection
  • make upload-serial to use a serial port (you may need to customize the SERIAL_PORT parameter in your Makefile according to the USB-to-Serial adapter you are using).

If you are using a Carbide board and the related Carbide module, there is a convenient feature which will make your life easier : if you pass true to the autoBootloaderReset parameter of Carbide::init(), the module will initialize the USB driver in device mode with a control handler which is able to reset the board in bootloader mode when a special request is sent by codeuploader. This way, you can simply connect your Carbide via USB and upload your code quickly, without needing to reset the board every time. If your program needs to use the USB driver for your own purposes, you can still use this feature by implementing this logic into your own program : see the definition of Carbide::usbControlHandler() and copy it into your own control handler.

Customization

Like everything else in the library, the bootloader can be customized to your liking. First, take a look at the bootloader_config.h file, the comments will guide you to change the more common options. For example, you can :

  • Enable/disable and configure different ways to enter bootloader mode, such as MODE_INPUT and MODE_TIMEOUT discussed above.
  • Enable/disable and configure the channels that the bootloader will listen on to receive program updates, such as USB and a serial port.
  • Enable/disable and configure information LEDs that the bootloader will use to provide feedback on the process :
    • LED_BL will blink when bootloader mode is activated.
    • LED_WRITE will flash when a page of data is being written in the Flash.
    • LED_ERROR will be turned on when an error arises and the upload process can't continue. Reset the board to retry.

If you have more advanced requirements, such as other modes or other channels, look at the bootloader source code in bootloader.cpp. This code is relatively simple and you should be able to adapt it to your needs easily. Keep in mind that the bootloader is simply a program built upon the library, juste like your own code, and therefore uses the same modules and functions. You can debug it in the same way you would debug your own code by setting DEBUG=true in the bootloader's Makefile and using the make debug-bootloader rule.

There is however one thing to be careful about : the size of the bootloader. It is by default limited to 16384 bytes (0x4000), which is enough for the default bootloader but can be a bit short if you add features and enable the DEBUG=true Makefile option (this option adds a significant weight to the final program). If you need to increase the space reserved for the bootloader, here is what you need to do :

  • Increase the BOOTLOADER_N_FLASH_PAGES parameter in bootloader.cpp. The default value is 32 and each flash page is 512 bytes long, which sums up to the 16384 bytes (0x4000) stated above. For example, let's set this option to 48 to increase the reserved space to 24576 bytes (0x6000).
  • Update the linker configuration files accordingly in the ld_scripts/ folder :
    • in bootloader.ld, at the FLASH line, set LENGTH to the new value, here 0x6000 instead of 0x4000
    • in usercode_bootloader_ls2x.ld, at the FLASH line, set ORIGIN to the same value (0x6000 instead of 0x4000) and LENGTH to the total size given in usercode_ls2x.ld (0x20000) minus the value of ORIGIN (here, 0x20000 - 0x6000 = 0x1A000 instead of 0x1C000)
    • in usercode_bootloader_ls4x.ld and usercode_bootloader_ls8x.ld, make the same modifications to the FLASH line with the correct total length for each file : ORIGIN = 0x6000 for both, LENGTH=0x3A000 for usercode_bootloader_ls4x.ld and LENGTH=0x7A000 for usercode_bootloader_ls8x.ld
  • Use make clean, make and make bootloader to ensure everything is compiled from scratch
  • flash the new bootloader and upload a program

Internal working

The operating principle of the bootloader is relatively simple, but with a few interesting quirks. If you want to learn more about the functioning of the microcontroller, open the bootloader source file (bootloader.cpp) and keep reading.

Resources initialization

First, you can notice that the main() doesn't call Core::init() first. This due to the fact that the bootloader should interfere as little as possible with the user code to ensure a consistant behaviour of the program whether a bootloader is used or not, and therefore should not initialize modules. The only functions used are the functions related to the fuses in the Flash module and the basic features of the GPIO module, which don't need any initialization. If the bootloader is not activated, the execution will jump to the user program right away (see below) and the global impact is minimal. However, if the bootloader is activated it will need more resources and features to operate, so in this case Core::init() is called. Furthermore, you can see that in order to exit bootloader mode (after the user program is successfully uploaded for example) a software reset is performed to make sure all resources are correctly released.

Upload process

The code upload process is not the most interesting part : it simply initialize the configured channels (USB and/or USART) and wait for data. The USB channel is activated when a control packet is received (see usbControlHandler() and the serial channel is activated when the 3-letter code SYN is received on the port. The received data is expected to be in the IHEX format, which is exactly the content line-by-line of the .hex file given to OpenOCD when flashing with a JTAG/SWD adapter. Even though not the most efficient (ASCII encoding means that more data need to be transfered), using this format directly offers other advantages :

  • each line specifies the address at which the data has to be stored, which allows discontinuities in the memory allocation
  • each line has its own checksum for data integrity
  • in prevision of a future update allowing the board to be enumerated as a file system, it would make copy/pasting the firmware update as the .hex file convenient

User program execution

The most interesting part is the way the bootloader is able to execute the user program, because it requires to understand how the ARM architecture works when the first clock cycles are sent to the CPU. We'll start by a quick overview of this.

The Vector Table

An important concept to understand is the vector table. This table is a list of 32-bit addresses with hard-coded meaning in the CPU wich must be properly initialized in order for the execution to start. At reset it is located at the very beginning of the Flash array, at address 0x00000000. The first value of this table is the initialization value of SP, the Stack Pointer register (the stack is a part of the RAM that is used to store local variables and save execution context when going into nested function calls). The following values are the pointers to the Exception Handlers, the functions that will be executed when an exception is triggered. An exception is a generic name for an event that happens in the CPU, which can either be one of the standard ARM exceptions or an interrupt.

There are 15 standard ARM exceptions defined in chapter B1.5.1 of the ARMv7-M Architecture Reference Manual, but the most important ones are Reset (exception number 1), NMI (Non-Maskable Interrupt, exception number 2) and HardFault (exception number 3). HardFault is triggered when the CPU enters an incoherent state (for exemple, trying to execute code from a invalid memory address). Some of the other exceptions are more specific types of faults that will escalate to a HardFault if not handled properly. Reset is the exception that is always triggered when the execution starts.

The beginning of the Flash memory therefore has to look like this in order to properly start the microcontroller :

  • 0x00000000 : Stack Pointer
  • 0x00000004 : Reset Handler
  • 0x00000008 : NMI Handler
  • 0x0000000C : HardFault Handler
  • 0x00000010 : other exception handlers...
The first action of the CPU is to copy the Stack Pointer address into its sp register and the Reset Handler address into its pc register (which always points to the instruction currently being executed), and start executing code from there.

One interesting feature of the ARM architecture is that this vector table can be moved later during execution by writing an offset into the special VTOR register. In fact, this is done by the library in Core::init() to put the vector table from Flash to RAM where there is room to add more interrupt handlers : the default vector table (defined in startup.cpp) only has room for the first 16 addresses (the Stack Pointer + the 15 standard exception handlers).

Emulating reset behaviour

Executing the user code transparently sums up to trying to reproduce the hard-coded reset behaviour of the CPU, aimed at another address. The bootloader puts the user code at the end of the space reserved for itself, which is by default at offset 0x4000. There are therefore three steps to reproduce :

  • set the VTOR register to point to the user code's vector table at offset 0x4000
  • put the Stack Pointer address (first 32-bit value of the vector table at address 0x4000) into the sp register
  • put the Reset Handler address (second 32-bit value of the vector table at address 0x4004) into the pc register

After these steps, the next instruction executed is the first instruction of the user code's Reset Handler, using a new vector table and a new stack. The stack used by the bootloader will be lost and its memory probably overwritten by the user program, which is completely fine because the execution will never go back to the bootloader. The user program will be executed just like it would have been if it had been flashed at address 0x00000000.

Addresses offset

We now have a way to jump the execution to the user program. However, there is one last thing to take into account : addresses offset. The most obvious way to design a bootloader system like this would be to take a normally-compiled program and upload it via the bootloader which would simply copy it starting at address 0x4000. This, however, wouldn't work, because the user code might contain hard-coded absolute addresses. After offsetting the code, these addresses would end up being completely wrong and the program would likely crash.

There are two ways to avoid this problem :

  • using a flag (-fpic) to tell the compiler to generate a position-independant code output : all call and jump adresses are relative (to the current position) instead of absolute
  • statically compiling at the memory address that the program will effectively run from

The first solution looks attractive but still requires a bit of work and can cause some subtle problems. More specifically, the vector table must be updated because it always contains absolute adresses (even in PIC), so the bootloader would need to compute the new adresses with the offset for each row in the vector table that resides in the Flash memory space. Furthermore, the compiler is designed to ensure some alignement requirements for sections and instructions, which might get wrong after the offset.

This is why the easiest way to avoid this problem is to compile the user program directly in the correct memory space, with the proper offset. This is the main reason for the BOOTLOADER option in the Makefile : it instructs the compiler (or more precisely, the linker) to use a different configuration script according to the case.

Let's assume we are using an ATSAM4LS4 microcontroller. The main linker script (all the linker scripts are located in the ld_scripts/ directory), which defines how the memory (Flash and RAM) is used, is common.ld. But this file is never called directly, it is always imported by the three other configuration files which distribute the Flash memory space for each scenario :

  • usercode_ls4x.ld is used when BOOTLOADER=false and the program is flashed directly : it allocates the whole memory to the program
  • bootloader.ld is used when compiling the bootloader : only the first part of the Flash is available for this code
  • usercode_bootloader_ls4x.ld is used when BOOTLOADER=true : the beginning of the flash is offset by the amount of space reserved for the bootloader, and the total length is shrunk accordingly

The bootloader doesn't add any offset : it puts the received data exactly at the location required by the .hex file. If the BOOTLOADER flag was ommited during compilation, the file will try to write its code at the beginning of the Flash and the bootloader will return an AREA_PROTECTED error, for which the codeuploader will automatically show a indication to set the BOOTLOADER option and compile again.

Code

bootloader_config.h

#ifndef _BOOTLOADER_CONFIG_H_

#include <gpio.h>
#include <usart.h>
#include <usb.h>
#include <stdint.h>

// ## Bootloader configuration

// If Input mode is enabled, the bootloader will check the value of the PIN_INPUT pin
// at startup and will be activated only if this pin is in the given PIN_INPUT_STATE.
// An optional pulling can be activated to default to a given state if no voltage is applied
// on the input.
// A common scenario is to set the input on a button wired to short the line to ground when
// pressed, with PIN_INPUT_STATE = GPIO::LOW and PIN_INPUT_PULLING = GPIO::Pulling::PULLUP.
// With this configuration, the pullup will raise the line to HIGH by default, and the
// bootloader will be activated only if the line is forced to LOW by pressing the button
// during startup.
const bool MODE_INPUT = true;
const GPIO::Pin INPUT_PIN = GPIO::PA04;
const GPIO::PinState INPUT_PIN_STATE = GPIO::LOW;
const GPIO::Pulling INPUT_PIN_PULLING = GPIO::Pulling::PULLUP;

// If Timeout mode is enabled, the bootloader will always be activated when the chip
// is powered up, but will automatically exit and reset after the given TIMEOUT_DELAY
// (in milliseconds) if no connection was detected.
const bool MODE_TIMEOUT = false;
const unsigned int TIMEOUT_DELAY = 3000; // ms

// Enable the USB channel to allow the bootloader to register as a USB device on a connected
// host. This is the easiest way to upload programs to the microcontroller using the
// codeuploader tool included with the library.
const bool CHANNEL_USB_ENABLED = true;
const uint16_t USB_VENDOR_ID = USB::DEFAULT_VENDOR_ID;
const uint16_t USB_PRODUCT_ID = USB::DEFAULT_PRODUCT_ID;

// Enable the USART channel to allow the bootloader to receive instructions via a serial
// connection. The port, pins and baudrate can be customized. See pins_sam4l_XX.cpp
// for the list of available pins.
const bool CHANNEL_USART_ENABLED = true;
const USART::Port USART_PORT = USART::Port::USART0;
const int USART_BAUDRATE = 115200;
const GPIO::Pin USART_PIN_RX = {GPIO::Port::A, 11, GPIO::Periph::A};
const GPIO::Pin USART_PIN_TX = {GPIO::Port::A, 12, GPIO::Periph::A};
const int USART_TIMEOUT = 3000;

// The bootloader can flash some LEDs to show its status. These can be enabled/disabled
// and customized here. The LED_POLARITY option specifies the state to set to turn the
// LED on.
const bool LED_BL_ENABLED = true;
const GPIO::Pin PIN_LED_BL = GPIO::PA01; // Green led on Carbide
const bool LED_WRITE_ENABLED = true;
const GPIO::Pin PIN_LED_WRITE = GPIO::PA02; // Blue led on Carbide
const bool LED_ERROR_ENABLED = true;
const GPIO::Pin PIN_LED_ERROR = GPIO::PA00; // Red led on Carbide
const GPIO::PinState LED_POLARITY = GPIO::LOW;
const unsigned int LED_BL_BLINK_DELAY_STANDBY = 200; // ms
const unsigned int LED_BL_BLINK_DELAY_CONNECTED = 50; // ms

#endif

bootloader.cpp

#include <stdint.h>
#include <core.h>
#include <scif.h>
#include <pm.h>
#include <gpio.h>
#include <tc.h>
#include <usart.h>
#include <usb.h>
#include <flash.h>
#include <string.h>
#include "bootloader_config.h"

// This is the bootloader for the libtungsten library. It can be configured to either open an
// UART (serial) port or to connect via USB, and to be activated with an external input (such
// as another microcontroller or a button) and/or a timeout. Most of the behaviour can be customized
// to your liking.


// USB request codes (Host -> Device)
enum class Request {
    START_BOOTLOADER,
    CONNECT,
    GET_STATUS,
    WRITE,
    GET_ERROR,
};

// USB status codes (Device -> Host)
enum class Status {
    READY,
    BUSY,
    ERROR,
};

// USB error codes (Device -> Host)
enum class BLError {
    NONE,
    CHECKSUM_MISMATCH,
    PROTECTED_AREA,
    UNKNOWN_RECORD_TYPE,
    OVERFLOW,
};

// Currently active channel
enum class Channel {
    NONE,
    USART,
    USB,
};

// Currently active mode
enum class Mode {
    NONE,
    INPUT,
    TIMEOUT,
    OTHER,
};

// Number of flash pages reserved to the bootloader. If this value is modified, please update
// the FLASH/LENGTH parameter in ld_scripts/bootloader.ld and the FLASH/ORIGIN parameter in the
// three ld_scripts/usercode_bootloader_lsxx.ld files accordingly.
// For BOOTLOADER_N_FLASH_PAGES = 32, the total bootloader size is 32 * 512 (size of a flash page
// in bytes) = 16384 = 0x4000.
const int BOOTLOADER_N_FLASH_PAGES = 32;

const int BUFFER_SIZE = 128;
char _buffer[BUFFER_SIZE];
volatile int _currentPage = -1;
volatile int _frameCounter = 0;
volatile int _bufferCursor = 0;
volatile bool _bufferFull = false;
volatile Status _status = Status::READY;
volatile bool _exitBootloader = false;
volatile bool _connected = false;
volatile BLError _error = BLError::NONE;
volatile Channel _activeChannel = Channel::NONE;
volatile Mode _activeMode = Mode::NONE;
bool _onePageWritten = false;
uint16_t _extendedSegmentAddress = 0;
uint16_t _extendedLinearAddress = 0;


int usbControlHandler(USB::SetupPacket &lastSetupPacket, uint8_t* data, int size);
unsigned int parseHex(const char* buffer, int pos, int n);
void writePage(int page, const uint8_t* buffer);


int main() {
    bool enterBootloader = false;

    // In TIMEOUT mode, enter bootloader mode except if the core was reset after a timeout
    if (Flash::getFuse(Flash::FUSE_BOOTLOADER_SKIP_TIMEOUT)) {
        // Reset the fuse and do not enter bootloader
        Flash::writeFuse(Flash::FUSE_BOOTLOADER_SKIP_TIMEOUT, false);
    } else if (MODE_TIMEOUT) {
        enterBootloader = true;
        _activeMode = Mode::TIMEOUT;
    }


    // In INPUT mode, enter bootloader mode if the input is asserted
    if (MODE_INPUT) {
        GPIO::init();
        GPIO::enableInput(INPUT_PIN, INPUT_PIN_PULLING);
        // Wait for a few cycles to let the pullup the time to raise the line
        for (int i = 0; i < 1000; i++) {
            __asm__("nop");
        }
        if (GPIO::get(INPUT_PIN) == INPUT_PIN_STATE) {
            enterBootloader = true;
            _activeMode = Mode::INPUT;
        }
    }

    // Force entering bootloader in these cases :
    // - the reset handler pointer or the stack pointer don't look right (the memory is empty, after the flashing of a new bootloader?)
    // - there is no available firmware according to the FW_READY fuse (a previous upload failed ?)
    // - the BOOTLOADER_FORCE fuse is set (after a call to Core::resetToBootloader() ?)
    uint32_t userStackPointerAddress = (*(volatile uint32_t*)(BOOTLOADER_N_FLASH_PAGES * Flash::FLASH_PAGE_SIZE_BYTES));
    uint32_t userResetHandlerAddress = (*(volatile uint32_t*)(BOOTLOADER_N_FLASH_PAGES * Flash::FLASH_PAGE_SIZE_BYTES + 0x04));
    if (userStackPointerAddress == 0x00000000 || userStackPointerAddress == 0xFFFFFFFF
            || userResetHandlerAddress == 0x00000000 || userResetHandlerAddress == 0xFFFFFFFF
            || !Flash::getFuse(Flash::FUSE_BOOTLOADER_FW_READY)
            || Flash::getFuse(Flash::FUSE_BOOTLOADER_FORCE)) {
        enterBootloader = true;
        _activeMode = Mode::OTHER;
    }
    

    if (enterBootloader) {
        // Init the basic core systems
        Core::init();

        // Set main clock to the 12MHz RC oscillator
        SCIF::enableRCFAST(SCIF::RCFASTFrequency::RCFAST_12MHZ);
        PM::setMainClockSource(PM::MainClockSource::RCFAST);

        // Enable serial port
        if (CHANNEL_USART_ENABLED) {
            USART::setPin(USART_PORT, USART::PinFunction::RX, USART_PIN_RX);
            USART::setPin(USART_PORT, USART::PinFunction::TX, USART_PIN_TX);
            USART::enable(USART_PORT, USART_BAUDRATE);
        }

        // Enable USB in Device mode
        if (CHANNEL_USB_ENABLED) {
            USB::initDevice(USB_VENDOR_ID, USB_PRODUCT_ID);
            USB::setControlHandler(usbControlHandler);
        }

        // Enable LED
        if (LED_BL_ENABLED) {
            GPIO::enableOutput(PIN_LED_BL, LED_POLARITY);
        }
        if (LED_WRITE_ENABLED) {
            GPIO::enableOutput(PIN_LED_WRITE, !LED_POLARITY);
        }
        if (LED_ERROR_ENABLED) {
            GPIO::enableOutput(PIN_LED_ERROR, !LED_POLARITY);
        }

        // Reset the BOOTLOADER_FORCE fuse
        if (Flash::getFuse(Flash::FUSE_BOOTLOADER_FORCE)) {
            Flash::writeFuse(Flash::FUSE_BOOTLOADER_FORCE, false);
        }

        // Initialize the page buffer used to cache the data to write to the flash
        const int PAGE_BUFFER_SIZE = Flash::FLASH_PAGE_SIZE_BYTES;
        uint8_t pageBuffer[PAGE_BUFFER_SIZE];
        memset(pageBuffer, 0, PAGE_BUFFER_SIZE);

        // Wait for instructions on any enabled channel
        Core::Time lastTimeLedToggled = 0;
        GPIO::PinState ledState = !LED_POLARITY;
        Core::Time lastUSARTActivity = 0;
        while (!_exitBootloader) {
            // Blink rapidly
            if (LED_BL_ENABLED && Core::time() > lastTimeLedToggled + (_connected ? LED_BL_BLINK_DELAY_CONNECTED : LED_BL_BLINK_DELAY_STANDBY)) {
                ledState = !ledState;
                GPIO::set(PIN_LED_BL, ledState);
                lastTimeLedToggled = Core::time();
            }

            if (!_connected) {
                // Timeout
                if (_activeMode == Mode::TIMEOUT && Core::time() > TIMEOUT_DELAY) {
                    _exitBootloader = true;
                }

                // In USART mode, if "SYN" is received, the codeuploader is trying to connect
                if (CHANNEL_USART_ENABLED && USART::available(USART_PORT) >= 3) {
                    if (USART::peek(USART_PORT, "SYN", 3)) {
                        // Flush
                        char tmp[3];
                        USART::read(USART_PORT, tmp, 3);

                        // Answer
                        USART::write(USART_PORT, "ACK");

                        // Connected!
                        _connected = true;
                        _activeChannel = Channel::USART;
                        lastUSARTActivity = Core::time();

                    } else {
                        // Flush a byte
                        USART::read(USART_PORT);
                    }
                }

                // In USB mode, _connected is updated by interrupt in controlHandler()

            } else { // Connected
                // Read incoming data in USART mode
                if (_activeChannel == Channel::USART) {
                    while (USART::available(USART_PORT)) {
                        lastUSARTActivity = Core::time();

                        char c = USART::read(USART_PORT);
                        if (_bufferCursor == 0) {
                            if (c == ':') {
                                // Start of a new frame
                                _buffer[0] = c;
                                _bufferCursor++;
                            } // Otherwise, ignore the byte

                        } else {
                            if (c == '\n') {
                                // End of frame
                                _bufferFull = true;
                                _frameCounter++;
                                break;
                            } else {
                                _buffer[_bufferCursor] = c;
                                _bufferCursor++;
                                if (_bufferCursor > BUFFER_SIZE) {
                                    // Error
                                    _status = Status::ERROR;
                                    _error = BLError::OVERFLOW;
                                    if (LED_ERROR_ENABLED) {
                                        GPIO::set(PIN_LED_ERROR, LED_POLARITY);
                                    }
                                    if (_activeChannel == Channel::USART) {
                                        USART::write(USART_PORT, (char)('0' + static_cast<int>(_error)));
                                    }
                                    while (1); // Stall
                                }
                            }
                        }
                    }

                    // Timeout to disconnect the serial connection after some
                    // time of inactivity. This allows the procedure to be restarted
                    // if the transfer fails, without the need to reset the bootloader.
                    if (Core::time() >= lastUSARTActivity + USART_TIMEOUT) {
                        _connected = false;
                        _frameCounter = 0;
                        _bufferCursor = 0;
                        _bufferFull = false;
                        memset(pageBuffer, 0, PAGE_BUFFER_SIZE);
                    }
                }
                
                // Handle errors that might have happened in the interrupt handler
                if (_error != BLError::NONE) {
                    _status = Status::ERROR;
                    if (LED_ERROR_ENABLED) {
                        GPIO::set(PIN_LED_ERROR, LED_POLARITY);
                    }
                    while (1); // Stall
                }

                // Handle a frame
                if (_bufferFull && _buffer[0] == ':') {
                    // cf https://en.wikipedia.org/wiki/Intel_HEX
                    int cursor = 1;
                    int nBytes = parseHex(_buffer, cursor, 2);
                    cursor += 2;
                    uint32_t addr = parseHex(_buffer, cursor, 4) + _extendedSegmentAddress * 16 + (_extendedLinearAddress << 16);
                    int page = addr / Flash::FLASH_PAGE_SIZE_BYTES;
                    int offset = addr % Flash::FLASH_PAGE_SIZE_BYTES;
                    cursor += 4;
                    uint8_t recordType = parseHex(_buffer, cursor, 2);
                    cursor += 2;

                    // Verify checksum
                    uint8_t checksum = parseHex(_buffer, cursor + 2 * nBytes, 2);
                    uint8_t s = 0;
                    for (int i = 0; i < nBytes + 4; i++) {
                        s += parseHex(_buffer, 2 * i + 1, 2);
                    }
                    s = ~s + 1;
                    if (s != checksum) {
                        // Error
                        _status = Status::ERROR;
                        _error = BLError::CHECKSUM_MISMATCH;
                        if (LED_ERROR_ENABLED) {
                            GPIO::set(PIN_LED_ERROR, LED_POLARITY);
                        }
                        if (_activeChannel == Channel::USART) {
                            USART::write(USART_PORT, (char)('0' + static_cast<int>(_error)));
                        }
                        while (1); // Stall
                    }

                    // Command 0x00 is a data frame
                    if (recordType == 0x00) {
                        // Bootloader's flash domain is protected
                        if (page < BOOTLOADER_N_FLASH_PAGES) {
                            // Error
                            _status = Status::ERROR;
                            _error = BLError::PROTECTED_AREA;
                            if (LED_ERROR_ENABLED) {
                                GPIO::set(PIN_LED_ERROR, LED_POLARITY);
                            }
                            if (_activeChannel == Channel::USART) {
                                USART::write(USART_PORT, (char)('0' + static_cast<int>(_error)));
                            }
                            while (1); // Stall
                        }

                        // Change page if necessary
                        if (page != _currentPage) {
                            if (_currentPage != -1) {
                                writePage(_currentPage, pageBuffer);
                            }

                            // Reset page buffer
                            _currentPage = page;
                            memset(pageBuffer, 0, PAGE_BUFFER_SIZE);
                        }

                        // Save data
                        if (offset + nBytes <= PAGE_BUFFER_SIZE) {
                            for (int i = 0; i < nBytes; i++) {
                                pageBuffer[offset + i] = parseHex(_buffer, cursor + 2 * i, 2);
                            }
                        } else { // Write across pages
                            int nBytesPage1 = PAGE_BUFFER_SIZE - offset;
                            int nBytesPage2 = nBytes - nBytesPage1;
                            for (int i = 0; i < nBytesPage1; i++) {
                                pageBuffer[offset + i] = parseHex(_buffer, cursor + 2 * i, 2);
                            }
                            writePage(_currentPage, pageBuffer);
                            offset = 0;
                            _currentPage++;
                            for (int i = 0; i < nBytesPage2 - offset; i++) {
                                pageBuffer[i] = parseHex(_buffer, cursor + 2 * (nBytesPage1 + i), 2);
                            }
                        }

                    } else if (recordType == 0x01) {
                        // End of file
                        writePage(_currentPage, pageBuffer);

                        // The firmware has been completely uploaded, set the FW_READY fuse
                        Flash::writeFuse(Flash::FUSE_BOOTLOADER_FW_READY, true);

                        // Exit the booloader to reboot
                        _exitBootloader = true;

                    } else if (recordType == 0x02) {
                        _extendedSegmentAddress = parseHex(_buffer, cursor, 4);
                        
                    } else if (recordType == 0x03) {
                        // CS and IP pointer : ignore

                    } else if (recordType == 0x04) {
                        _extendedLinearAddress = parseHex(_buffer, cursor, 4);

                    } else if (recordType == 0x05) {
                        // EIP pointer : ignore

                    } else {
                        // Error
                        _status = Status::ERROR;
                        _error = BLError::UNKNOWN_RECORD_TYPE;
                        if (LED_ERROR_ENABLED) {
                            GPIO::set(PIN_LED_ERROR, LED_POLARITY);
                        }
                        if (_activeChannel == Channel::USART) {
                            USART::write(USART_PORT, (char)('0' + static_cast<int>(_error)));
                        }
                        while (1); // Stall
                    }

                    _bufferFull = false;
                    _bufferCursor = 0;
                    memset(_buffer, 0, BUFFER_SIZE);

                    // In USART mode, send acknowledge every frame
                    if (_activeChannel == Channel::USART) {
                        USART::write(USART_PORT, (char)('0' + static_cast<int>(_error)));
                        _frameCounter = 0;
                    }
                    _status = Status::READY;
                }
            }
        }

        // Reset the chip to free all resources
        Flash::writeFuse(Flash::FUSE_BOOTLOADER_SKIP_TIMEOUT, true);
        Core::reset();
        while (1);

    } else {
        // Update VTOR to point to the vector table of the user program
        (*(volatile uint32_t*) Core::VTOR) = BOOTLOADER_N_FLASH_PAGES * Flash::FLASH_PAGE_SIZE_BYTES;

        // Load the stack pointer register at offset 0 of the user's vector table
        // See ARMv7-M Architecture Reference Manual, section B1.5.3 The vector Table
        volatile uint32_t* sp = (volatile uint32_t*)(BOOTLOADER_N_FLASH_PAGES * Flash::FLASH_PAGE_SIZE_BYTES);
        __asm__("LDR sp, %0" : : "m" (*sp));

        // Execute the user code by jumping to offset 4 of the user's vector table (ResetHandler)
        // See ARMv7-M Architecture Reference Manual, section B1.5.2 Exception number definition
        volatile uint32_t* pc = (volatile uint32_t*)(BOOTLOADER_N_FLASH_PAGES * Flash::FLASH_PAGE_SIZE_BYTES + 0x04);
        __asm__("LDR pc, %0" : : "m" (*pc));
    }
}

// Handler called when a CONTROL packet is sent over USB
int usbControlHandler(USB::SetupPacket &lastSetupPacket, uint8_t* data, int size) {
    Request request = static_cast<Request>(lastSetupPacket.bRequest);

    if (lastSetupPacket.direction == USB::EPDir::IN || lastSetupPacket.wLength == 0) {
        if (request == Request::START_BOOTLOADER) {
            lastSetupPacket.handled = true;
            // Bootloader is already started

        } else if (request == Request::CONNECT) {
            lastSetupPacket.handled = true;
            _connected = true;
            _currentPage = -1;
            _frameCounter = 0;
            _activeChannel = Channel::USB;

        } else if (request == Request::GET_STATUS) {
            lastSetupPacket.handled = true;
            if (data != nullptr && size >= 1) {
                data[0] = static_cast<int>(_status);
                return 1;
            }

        } else if (request == Request::GET_ERROR) {
            lastSetupPacket.handled = true;
            if (data != nullptr && size >= 1) {
                data[0] = static_cast<int>(_error);
                return 1;
            }
        }

    } else { // OUT
        if (request == Request::WRITE && !lastSetupPacket.handled) {
            // This is a data packet, copy its content to the buffer
            lastSetupPacket.handled = true;
            if (size <= BUFFER_SIZE) {
                _status = Status::BUSY;
                memcpy(_buffer, data, size);
                _bufferFull = true;
            } else {
                _status = Status::ERROR;
                _error = BLError::OVERFLOW;
            }
        }
    }

    return 0;
}

// Parse an hex number in text format and return its value
unsigned int parseHex(const char* buffer, int pos, int n) {
    unsigned int r = 0;
    for (int i = 0; i < n; i++) {
        char c = buffer[pos + i];
        if (c >= '0' && c <= '9') {
            r += c - '0';
        } else if (c >= 'A' && c <= 'F') {
            r += c - 'A' + 10;
        } else if (c >= 'a' && c <= 'f') {
            r += c - 'a' + 10;
        } else {
            return 0;
        }

        if (i < n - 1) {
            r <<= 4;
        }
    }
    return r;
}

// Write a page to flash memory
void writePage(int page, const uint8_t* buffer) {
    if (!_onePageWritten) {
        // If this is the first time a page is written, this means that
        // the flash doesn't contain a valid firmware anymore : disable
        // the FW_READY fuse
        _onePageWritten = true;
        Flash::writeFuse(Flash::FUSE_BOOTLOADER_FW_READY, false);
    }
    if (LED_WRITE_ENABLED) {
        GPIO::set(PIN_LED_WRITE, LED_POLARITY);
    }
    Flash::writePage(page, (uint32_t*)buffer);
    if (LED_WRITE_ENABLED) {
        GPIO::set(PIN_LED_WRITE, !LED_POLARITY);
    }
}