The ability to update the firmware on commercially available products, or on individual products that are in operation at the customer, is difficult to overestimate. This not only makes it possible to subsequently eliminate bugs and expand functionality, but also allows a developer with a lighter heart to release a “still damp” product on the market, if the management requires it.

Therefore, the importance of having a bootloader in newly developed devices in most cases is beyond doubt. This article will discuss the development of a bootloader via a USB interface on an Atmel
SAM D21 microcontroller with a Cortex M0 + core. Specifically, on
SAMD21J18A . SAM D20 / 21 microcontrollers do not have a pre-written bootloader, so you will have to deal with its software implementation. On the Atmel website, you can find
Application notes , how to make it using standard interfaces (UART, I2C, SPI, USB). Under the cat description of the process of creating a USB-bootloader.
Formulation of the problem
- It is necessary to develop the most simple, from the end user's point of view, method of updating the device firmware. To do this, you will need to copy a file with a new firmware to an ordinary USB flash drive, insert the USB flash drive into the device and press the reset button (or reset the power). After that, the bootloader starts, checks for the presence of the file with the firmware on the flash drive and fills the contents of this file as an application
- As a “protection against a fool”, we use the previously known special name of the firmware file to exclude the accidental coincidence of names with other files on a flash drive. Moreover, if the "attacker" will independently create a third-party file with the same name as expected, the device will try to use it as a firmware. Of course, in this case, the performance of the device will be impaired, but it can later be restored by slipping the USB flash drive with the correct firmware
- The USB interface of the device's microcontroller is used as the USB interface.
- The device does not have a permanent connection to the Internet to download the new firmware by itself.
- We believe that connecting a PC to the device and updating the firmware using a third-party utility is more difficult for the end user.
A bit of theory and preparation
Memory
The address space in the memory of the SAMD20 / 21 series microcontrollers is simple:

Non-volatile memory is organized in rows, each row contains 4 pages. The size of 1 page is 64 bytes. Non-volatile memory is erased in rows, and is written page by page. It is important to remember.
The lower (lower) rows in the main address space of non-volatile memory can be used for the bootloader (configured using BOOTPROT fyuz), and the upper rows for EEPROM emulation.
The bootloader section is protected by the BOOTPROT lock bits and fuses corresponding to this address space.
BOOTPROT fuses simultaneously determine the size of the bootloader section and protect the allocated memory area from reading.
EEPROM can be recorded despite the protection of the corresponding memory area.
What will be required to organize a bootloader?
- Work with the controller's memory - the non-volatile memory controller (NVM) is responsible for this;
- Working with USB - the USB controller is responsible for this;
- Working with the file system is the power of FATFS.
- And the little things: working with I / O ports, clocking.
Note: Atmel Studio version 6.2 (inherited from AVR Studio) and the ASF framework (Atmel Software Framework) are used as the development environment.')
USB subtleties
In accordance with the USB standard, very precise clocking is required to implement the bus. We will use external 32 kHz quartz as a support for the DFLL (Digital Frequency Locked Loop). The DFLL output will be used both for clocking the USB module and the entire controller. For the USB module to work, you must configure the DFLL so that the output is exactly 48 MHz. For stability and accuracy of the DFFL output frequency, it must be configured in closed loop mode.

We collect the project
Using the
ASF wizard, we connect all the modules we need listed above.
USB host
Add
USB Host service in
mass storage mode.

After the driver is added to the project, several header and executive files appear. We are interested in 2 of them:
- conf_usb_host.h - configures USB and configures interrupt handlers (Callback),
- conf_access.h - configures the abstract level for working with memory.
For the operation of the USB host stack, we register two definitions in the project properties:
USB_MASS_STORAGE_ENABLE=true ACCESS_MEM_TO_RAM_ENABLED=true
To do this, right-click on the project, select
Properties -> Toolchain -> ARM / GNU C Compiler -> Symbols .
We comment on the line
"#define Lun_usb_unload - NULL" in
USB LUNs Definitions in the
conf_access.h file to prevent errors during compilation.
To track connected devices on the USB bus, an interrupt handler (callback) on the
Start of Frame event is introduced. This interrupt occurs only once each time a SOF is sent, and since SOF is sent every 1 ms when the device is connected to the bus, this event can be used as a timer.
We register the interrupt handler in the
conf_usb_host.h file.
To do this, we add the prototype of the
main_usb_sof_event () function at the beginning of the
conf_usb_host.h file after all #include.
void main_usb_sof_event(void);
We also add a line to this file:
# define UHC_SOF_EVENT() main_usb_sof_event()
Now we need to globally define a counter variable in the
main.c file, we will increase it each time we call the corresponding handler:
volatile static uint16_t main_usb_sof_counter = 0;
Add the interrupt handler itself (callback):
void main_usb_sof_event(void) { main_usb_sof_counter++; }
File system
Add
FAT FS file system service (with the help of the ASF wizard). We open the module and select the
calendar_polled driver's RTC mode.
For the full functioning of the file system module, add at the beginning of
main.c :
#include "string.h" #define MAX_DRIVE _VOLUMES #define FIRMWARE_FILE "firmware.bin" const char firmware_filename[] = {FIRMWARE_FILE}; static FATFS fs; static FILE file_object;
The file name (#define FIRMWARE_FILE "firmware.bin") must match the name of the firmware file on the connected USB flash drive.
Work with non-volatile memory
Add NVM-Non-volatile memory (driver). In addition, we define the necessary constants and variables in the main.c file:
#define APP_START_ADDRESS (NVMCTRL_ROW_SIZE * 200) uint8_t page_buffer[NVMCTRL_PAGE_SIZE];
Still need to configure the controller non-volatile memory. To do this, add the configuration structure (globally), read the default settings, change the necessary and set (make out a separate function):
struct nvm_config nvm_cfg; void nvm_init(void) { nvm_get_config_defaults(&nvm_cfg); nvm_cfg.manual_page_write=false; nvm_set_config(&nvm_cfg); }
All the necessary modules are added, you can write code.
Code
It is worth noting that if the bootloader uses the same peripherals as the application, then it must be reset before switching to the application. A reset is performed by special functions in the ASF.
Also note that you can access the USB device only after 1-2 seconds from the moment it is connected to the bus, since before this the device is initialized.
A brief operation algorithm (bootloader only) is shown in the figure below:

Main code #include <asf.h> #include <exp_io.h> #include <led.h> #include "string.h" //------------------------------------------------------------------------------------------------------------------------------ #define MAX_DRIVE _VOLUMES #define FIRMWARE_FILE "Modbus_RTU_TCP.bin" #define APP_START_ADDRESS (NVMCTRL_ROW_SIZE * 200) //------------------------------------------------------------------------------------------------------------------------------ const char firmware_filename[] = {FIRMWARE_FILE}; // FATFS variables static FATFS fs; static FIL file_object; // NVM uint8_t page_buffer[NVMCTRL_PAGE_SIZE]; struct nvm_config nvm_cfg; //USB volatile static uint16_t main_usb_sof_counter = 0; //------------------------------------------------------------------------------------------------------------------------------ void main_usb_sof_event(void) { main_usb_sof_counter++; } static void check_boot_mode(void) { uint32_t app_check_address; uint32_t *app_check_address_ptr; // Check if WDT is locked if (!(WDT->CTRL.reg & WDT_CTRL_ALWAYSON)) { //Disable the Watchdog module WDT->CTRL.reg &= ~WDT_CTRL_ENABLE; } app_check_address = APP_START_ADDRESS; app_check_address_ptr = (uint32_t *)app_check_address; if (*app_check_address_ptr == 0xFFFFFFFF) { // No application; run bootloader return; } // Pointer to the Application Section void (*application_code_entry)(void); // Rebase the Stack Pointer __set_MSP(*(uint32_t *)APP_START_ADDRESS); // Rebase the vector table base address TODO: use RAM SCB->VTOR = ((uint32_t)APP_START_ADDRESS & SCB_VTOR_TBLOFF_Msk); // Load the Reset Handler address of the application application_code_entry = (void (*)(void))(unsigned *)(*(unsigned *)(APP_START_ADDRESS + 4)); //Jump to user Reset Handler in the application application_code_entry(); } void delay_ms(uint32_t ms) { volatile int a=0; for(uint32_t i=0; i<ms; i++) { for( int j=0; j<2000; j++) a++; } } void nvm_init(void) { nvm_get_config_defaults(&nvm_cfg); nvm_cfg.manual_page_write=false; nvm_set_config(&nvm_cfg); } void init_IO(void) { ExpIO_Init(); LED_Init(); } int main (void) { volatile uint16_t z=0; uint32_t fw_size ; UINT bytes_read = 0; enum status_code error_code; uint32_t current_page; uint32_t curr_address = 0; // Erase flash rows to fit new firmware uint16_t rows_clear; uint16_t i; check_boot_mode(); system_init(); init_IO(); nvm_init(); uhc_start(); while (1) { if(65000==z) { LED(GREEN,0); delay_ms(1000); LED(GREEN,1); z = 0; } z++; // Wait 2 seconds before trying to access the USB drive if (main_usb_sof_counter > 2000) { main_usb_sof_counter = 0; volatile uint8_t lun = LUN_ID_USB; // Mount drive memset(&fs, 0, sizeof(FATFS)); FRESULT res = f_mount(lun, &fs); if (FR_INVALID_DRIVE == res) { continue; } res = f_open(&file_object,firmware_filename, FA_READ); if (res == FR_NOT_READY) { // LUN not ready f_close(&file_object); continue; } if (res != FR_OK) { // LUN test error f_close(&file_object); continue; } // Get size of file fw_size = f_size(&file_object); bytes_read = 0; if (fw_size != 0) { current_page = APP_START_ADDRESS /NVMCTRL_PAGE_SIZE; curr_address = 0; // Erase flash rows to fit new firmware rows_clear = fw_size / NVMCTRL_ROW_SIZE; for (i = 0; i < rows_clear; i++) { do { error_code = nvm_erase_row( (APP_START_ADDRESS) +(NVMCTRL_ROW_SIZE * i)); } while (error_code == STATUS_BUSY); } do { //Read data from USB stick to the page buffer f_read(&file_object,page_buffer,NVMCTRL_PAGE_SIZE,&bytes_read ); bytes_read=64; curr_address += bytes_read; // Write page buffer to flash do { error_code = nvm_write_buffer(current_page * NVMCTRL_PAGE_SIZE, page_buffer, bytes_read); } while (error_code == STATUS_BUSY); current_page++; } while (curr_address < fw_size); } f_close(&file_object); system_interrupt_disable_global(); uhc_stop(1); NVIC_SystemReset(); } } }
Preparing the firmware file
In the
SAMD21J18A (as in the other controllers of the SAMD20 / 21 series), each NVM row consists of 4 pages, each of which is 64 bytes. Thus, 200 rows (which we allocate for the bootloader) are (200 * 4 * 64) bytes = 51200 (0xC800) bytes of memory. And the application part should start after 51200 bytes of flash memory.
Splitting flash memory:
Bootloader section:
- Size: 50 KB (51200 bytes)
- Address space (flash memory): 0x00000000 to 0x0000C7FF
Application section:
- Size: 206 KB (256KB-50KB)
- Address space (flash memory): 0x0000C800 to 0x0003FFFF
In order to generate the firmware starting from the address we need, and not from the beginning of the flash memory, as it happens by default, you need to change the linker file.
The file itself can be found in the solution explorer. In our case, it is called
samd21j18a_flash.ld :
Path: src-asf-sam0-utils-linker scripts-samd21-gcc
It is necessary to make changes to the definitions of memory areas:
Default configuration:
rom (rx) : ORIGIN = 0x00000000,
LENGTH = 0x00040000
should be replaced by
rom (rx) : ORIGIN = 0x0000C800,
LENGTH = 0x00033800
Now the compiled binary can be uploaded through the bootloader.