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 = trueMODE_TIMEOUT = falseCHANNEL_USB_ENABLED = trueCHANNEL_USART_ENABLED = trueWith 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 0x4000usercode_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.ldmake 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 :
0x40000x4000) 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);
}
}