USB bootloader на микроконтроллере: обновление прошивки с флешки

Возможность обновления прошивки на серийно выпускаемых изделиях, или на единичных изделиях, находящихся в эксплуатации у заказчика трудно переоценить. Это не просто даёт возможность последующего устранения багов и расширения функционала, но и позволяет разработчику с более лёгким сердцем выпускать «еще сыроватый» продукт на рынок, если руководство того требует.
d5a700eb4f04415cb63c505ee763182b.png
Поэтому важность наличия bootloader’а во вновь разрабатываемых устройствах в большинстве случаев не вызывает сомнений. В данной статье пойдет речь о разработке bootloader’а по интерфейсу USB на микроконтроллере Atmel SAM D21 с ядром Cortex M0+. А конкретно на SAMD21J18A. У микроконтроллеров SAM D20/21 нет предзаписанного бутлоадера, поэтому придётся заниматься его программной реализацией. На сайте Atmel можно найти Application notes, как сделать его с использованием стандартных интерфейсов (UART, I2C, SPI, USB). Под катом описание процесса создания USB-бутлоадера.

Постановка задачи


  • Необходимо разработать наиболее простой, с точки зрения конечного пользователя, способ обновления прошивки устройства. Для этого потребуется скопировать на обыкновенную флешку файл с новой прошивкой, воткнуть флешку в устройство и нажать кнопку reset (или пересбросить питание). После этого стартует bootloader, проверяет наличие файла с прошивкой на флешке и заливает содержимое этого файла в качестве application
  • В качестве «защиты от дурака» используем заранее известное специальное название файла прошивки, что бы исключить случайное совпадение имен с другими файлами на флешке. При этом если «злоумышленник» самостоятельно создаст сторонний файл с именем, совпадающим с ожидаемым, устройство будет пытаться использовать его в качестве прошивки. Разумеется, в этом случае работоспособность устройства будет нарушена, но её можно будет впоследствии восстановить подсунув флешку с корректной прошивкой
  • В качестве USB интерфейса используется аппаратный USB микроконтроллера устройства
  • Устройство не имеет постоянного подключения к интернету, что бы самостоятельно скачать новую прошивку
  • Считаем что подключение ПК к устройству и обновление прошивки с помощью сторонней утилиты является более сложным для конечного пользователя


Немного теории и подготовки


Память
Адресное пространство в памяти микроконтроллеров серии SAMD20/21 устроено просто:
Организация памяти samd20
Энергонезависимая память организована рядами, каждый ряд содержит 4 страницы. Размер 1 страницы 64 байта. Энергонезависимая память стирается рядами, а записывается постранично. Это важно помнить.
Нижние (младшие) ряды в основном адресном пространстве энергонезависимой памяти могут быть использованы для бутлоадера (настраивается с помощью фьюзов BOOTPROT), а верхние ряды для эмуляции EEPROM.
Bootloader-секция защищена соответствующими этому адресному пространству lock-битами и фьюзами BOOTPROT.
Фьюзы BOOTPROT одновременно определяют размер bootloader-секции и защищают выделенную область памяти от чтения.
EEPROM может быть записана несмотря на защиту соответствующей ей области памяти.

Что потребуется для организации bootloader’а?


  1. Работа с памятью контроллера — за это отвечает контроллер энергонезависимой памяти Non-volatile memory (NVM);
  2. Работа с USB — за это отвечает контроллер USB;
  3. Работа с файловой системой — это под силу FATFS.
  4. И по мелочи: работа с портами ввода/вывода, тактирование.

Примечание: в качестве среды разработки используется Atmel Studio версии 6.2 (наследница AVR Studio) и фреймворк ASF (Atmel Software Framework)

Тонкости USB
В соответствии со стандартом USB для реализации шины необходимо очень точное тактирование. Мы будем использовать внешний кварц на 32 кГц как опору для DFLL (Digital Frequency Locked Loop). Выход DFLL будет использоваться как для тактирования USB модуля, так и всего контроллера. Для работы USB модуля необходимо настроить DFLL так, чтобы на выходе было ровно 48 МГц. Для стабильности и точности выходной частоты DFFL он должен быть сконфигурирован в режиме closed loop.
Тактирование

Собираем проект


С помощью ASF wizard подключаем все необходимые нам модули, перечисленные выше.USB Host
Добавляем USB Host service в режиме mass storage.
После добавления драйвера в проект появляются несколько заголовочных и исполнительных файлов. Нам интересны 2 из них:

  • conf_usb_host.h — конфигурирует USB и настраивает обработчики прерываний (Callback),
  • conf_access.h — конфигурирует абстрактный уровень для работы с памятью.


Для работы стека USB хоста прописываем в свойствах проекта два определения:

USB_MASS_STORAGE_ENABLE=true
ACCESS_MEM_TO_RAM_ENABLED=true


Для этого щелкаем правой кнопкой мыши по проекту, выбираем Properties → Toolchain → ARM/GNU C Compiler → Symbols.
Комментируем строку »#define Lun_usb_unload — NULL» в USB LUNs Definitions в файле conf_access.h для предотвращения ошибок при компиляции.
Для отслеживания подключенных устройств на шине USB вводится обработчик прерывания (callback) по событию Start of Frame. Это прерывание происходит только один раз при каждой посылке SOF, а так как SOF посылается раз в 1 мс, когда устройство подключено к шине, то это событие можно использовать как таймер.
Обработчик прерывания прописываем в файле conf_usb_host.h.
Для этого добавляем прототип функции main_usb_sof_event () в начале файла conf_usb_host.h после всех #include’ов.

void main_usb_sof_event(void);


Так же добавляем в этот файл строку:

# define UHC_SOF_EVENT() main_usb_sof_event()


Теперь требуется глобально определить переменную-счетчик в файле main.c, именно ее будем увеличивать при каждом вызове соответствующего обработчика:

volatile static uint16_t main_usb_sof_counter = 0;


Добавляем собственно обработчик прерывания (callback):

void main_usb_sof_event(void)
{
   main_usb_sof_counter++;
}


Файловая система
Добавляем FAT FS file system service (c помощью ASF wizard). Раскрываем модуль и выбираем режим RTC драйвера calendar_polled.
Для полноценного функционирования модуля файловой системы добавляем в начале main.c:

#include "string.h"
#define MAX_DRIVE _VOLUMES
#define FIRMWARE_FILE "firmware.bin"
const char firmware_filename[] = {FIRMWARE_FILE};
/* FATFS variables */
static FATFS fs;
static FILE file_object;


Имя файла (#define FIRMWARE_FILE «firmware.bin») должно совпадать с именем файла прошивки на подключаемой флешке.Работа с энергонезависимой памятью
Добавляем NVM-Non-volatile memory (driver). Кроме этого определяем необходимые константы и переменные в файле main.c:

#define APP_START_ADDRESS (NVMCTRL_ROW_SIZE * 200)
uint8_t page_buffer[NVMCTRL_PAGE_SIZE];


Еще нужно сконфигурировать контроллер энергонезависимой памяти. Для этого добавляем конфигурационную структуру (глобально), считываем настройки по умолчанию, изменяем необходимое и устанавливаем (оформляем в отдельную функцию):

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);
}


Все необходимые модули добавлены, можно писать код.

Код


Стоит отметить, что если в бутлоадер использует ту же периферию, что и application, то ее нужно обязательно сбросить перед переходом в application. Сброс (reset) осуществляется специальными функциями в ASF.
Также замечу, что обращаться к USB устройству можно только после 1–2 секунд с момента его подключения к шине, так как до этого происходит инициализация устройства.
Краткий алгоритм работы (только bootloader) приведен на рисунке ниже:
Алгоритм

Основной код
#include 
#include 
#include 
#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 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();
          }
        }
}



Подготовка файла прошивки


В SAMD21J18A (как и в других контроллерах серии SAMD20/21) каждый ряд NVM состоит из 4 страниц, каждая из которых по 64 байта. Таким образом 200 рядов (которые мы выделяем под bootloader) это (200×4 * 64) байт = 51200 (0xC800) байт памяти. И application часть должна начинаться после 51200 байт flash памяти.
Разбиение flash памяти:
Секция bootloader:

  • Размер: 50 кбайт (51200 байт)
  • Адресное пространство (flash память): 0×00000000 до 0×0000C7FF


Секция application:

  • Размер: 206 кбайт (256 кбайт-50 кбайт)
  • Адресное пространство (flash память): 0×0000C800 до 0×0003FFFF


Для того чтобы сформировать прошивку, начинающуюся с нужного нам адреса, а не с начала flash памяти, как это происходит по умолчанию, необходимо изменить файл линкера.
Сам файл можно найти в solution explorer. В нашем случае он называется samd21j18a_flash.ld:
Путь: src-asf-sam0-utils-linker scripts-samd21-gcc
В него необходимо внести изменения определений областей памяти:
Конфигурация по умолчанию:
rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x00040000
должна быть заменена на
rom (rx) : ORIGIN = 0x0000C800, LENGTH = 0x00033800
Теперь скомпиленный бинарник можно заливать через бутлоадер.

© Geektimes