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.
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 :
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.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.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 connectionmake 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.
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 :
MODE_INPUT
and MODE_TIMEOUT
discussed above.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 :
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).ld_scripts/
folder :bootloader.ld
, at the FLASH
line, set LENGTH
to the new value, here 0x6000
instead of 0x4000
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
)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
make clean
, make
and make bootloader
to ensure everything is compiled from scratchThe 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.
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.
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 :
.hex
file convenientThe 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.
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 Pointer0x00000004
: Reset Handler0x00000008
: NMI Handler0x0000000C
: HardFault Handler0x00000010
: other exception handlers...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).
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 :
0x4000
0x4000
) into the sp
register0x4004
) into the pc
registerAfter 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
.
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 :
-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 absoluteThe 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 programbootloader.ld
is used when compiling the bootloader : only the first part of the Flash is available for this codeusercode_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 accordinglyThe 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.
#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
#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); } }