Возможность обновления прошивки на серийно выпускаемых изделиях, или на единичных изделиях, находящихся в эксплуатации у заказчика трудно переоценить. Это не просто даёт возможность последующего устранения багов и расширения функционала, но и позволяет разработчику с более лёгким сердцем выпускать «еще сыроватый» продукт на рынок, если руководство того требует.
Поэтому важность наличия bootloader'а во вновь разрабатываемых устройствах в большинстве случаев не вызывает сомнений. В данной статье пойдет речь о разработке bootloader'а по интерфейсу USB на микроконтроллере Atmel SAM D21 с ядром Cortex M0+. А конкретно на SAMD21J18A. У микроконтроллеров SAM D20/21 нет предзаписанного бутлоадера, поэтому придётся заниматься его программной реализацией. На сайте Atmel можно найти Application notes, как сделать его с использованием стандартных интерфейсов (UART, I2C, SPI, USB). Под катом описание процесса создания USB-бутлоадера.
Адресное пространство в памяти микроконтроллеров серии SAMD20/21 устроено просто:
Энергонезависимая память организована рядами, каждый ряд содержит 4 страницы. Размер 1 страницы 64 байта. Энергонезависимая память стирается рядами, а записывается постранично. Это важно помнить.
Нижние (младшие) ряды в основном адресном пространстве энергонезависимой памяти могут быть использованы для бутлоадера (настраивается с помощью фьюзов BOOTPROT), а верхние ряды для эмуляции EEPROM.
Bootloader-секция защищена соответствующими этому адресному пространству lock-битами и фьюзами BOOTPROT.
Фьюзы BOOTPROT одновременно определяют размер bootloader-секции и защищают выделенную область памяти от чтения.
EEPROM может быть записана несмотря на защиту соответствующей ей области памяти.
Примечание: в качестве среды разработки используется Atmel Studio версии 6.2 (наследница AVR Studio) и фреймворк ASF (Atmel Software Framework)
В соответствии со стандартом USB для реализации шины необходимо очень точное тактирование. Мы будем использовать внешний кварц на 32 кГц как опору для DFLL (Digital Frequency Locked Loop). Выход DFLL будет использоваться как для тактирования USB модуля, так и всего контроллера. Для работы USB модуля необходимо настроить DFLL так, чтобы на выходе было ровно 48 МГц. Для стабильности и точности выходной частоты DFFL он должен быть сконфигурирован в режиме closed loop.
С помощью ASF wizard подключаем все необходимые нам модули, перечисленные выше.
Добавляем USB Host service в режиме mass storage.
После добавления драйвера в проект появляются несколько заголовочных и исполнительных файлов. Нам интересны 2 из них:
Для работы стека USB хоста прописываем в свойствах проекта два определения:
Для этого щелкаем правой кнопкой мыши по проекту, выбираем 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'ов.
Так же добавляем в этот файл строку:
Теперь требуется глобально определить переменную-счетчик в файле main.c, именно ее будем увеличивать при каждом вызове соответствующего обработчика:
Добавляем собственно обработчик прерывания (callback):
Добавляем FAT FS file system service (c помощью ASF wizard). Раскрываем модуль и выбираем режим RTC драйвера calendar_polled.
Для полноценного функционирования модуля файловой системы добавляем в начале main.c:
Имя файла (#define FIRMWARE_FILE «firmware.bin») должно совпадать с именем файла прошивки на подключаемой флешке.
Добавляем NVM-Non-volatile memory (driver). Кроме этого определяем необходимые константы и переменные в файле main.c:
Еще нужно сконфигурировать контроллер энергонезависимой памяти. Для этого добавляем конфигурационную структуру (глобально), считываем настройки по умолчанию, изменяем необходимое и устанавливаем (оформляем в отдельную функцию):
Все необходимые модули добавлены, можно писать код.
Стоит отметить, что если в бутлоадер использует ту же периферию, что и application, то ее нужно обязательно сбросить перед переходом в application. Сброс (reset) осуществляется специальными функциями в ASF.
Также замечу, что обращаться к USB устройству можно только после 1-2 секунд с момента его подключения к шине, так как до этого происходит инициализация устройства.
Краткий алгоритм работы (только bootloader) приведен на рисунке ниже:
В SAMD21J18A (как и в других контроллерах серии SAMD20/21) каждый ряд NVM состоит из 4 страниц, каждая из которых по 64 байта. Таким образом 200 рядов (которые мы выделяем под bootloader) это (200 * 4 * 64) байт = 51200 (0xC800) байт памяти. И application часть должна начинаться после 51200 байт flash памяти.
Разбиение flash памяти:
Секция bootloader:
Секция application:
Для того чтобы сформировать прошивку, начинающуюся с нужного нам адреса, а не с начала flash памяти, как это происходит по умолчанию, необходимо изменить файл линкера.
Сам файл можно найти в solution explorer. В нашем случае он называется samd21j18a_flash.ld:
Путь: src-asf-sam0-utils-linker scripts-samd21-gcc
В него необходимо внести изменения определений областей памяти:
Конфигурация по умолчанию:
должна быть заменена на
Теперь скомпиленный бинарник можно заливать через бутлоадер.
Поэтому важность наличия 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 устроено просто:
Энергонезависимая память организована рядами, каждый ряд содержит 4 страницы. Размер 1 страницы 64 байта. Энергонезависимая память стирается рядами, а записывается постранично. Это важно помнить.
Нижние (младшие) ряды в основном адресном пространстве энергонезависимой памяти могут быть использованы для бутлоадера (настраивается с помощью фьюзов BOOTPROT), а верхние ряды для эмуляции EEPROM.
Bootloader-секция защищена соответствующими этому адресному пространству lock-битами и фьюзами BOOTPROT.
Фьюзы BOOTPROT одновременно определяют размер bootloader-секции и защищают выделенную область памяти от чтения.
EEPROM может быть записана несмотря на защиту соответствующей ей области памяти.
Что потребуется для организации bootloader'а?
- Работа с памятью контроллера – за это отвечает контроллер энергонезависимой памяти Non-volatile memory (NVM);
- Работа с USB – за это отвечает контроллер USB;
- Работа с файловой системой – это под силу FATFS.
- И по мелочи: работа с портами ввода/вывода, тактирование.
Примечание: в качестве среды разработки используется 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 <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();
}
}
}
Подготовка файла прошивки
В SAMD21J18A (как и в других контроллерах серии SAMD20/21) каждый ряд NVM состоит из 4 страниц, каждая из которых по 64 байта. Таким образом 200 рядов (которые мы выделяем под bootloader) это (200 * 4 * 64) байт = 51200 (0xC800) байт памяти. И application часть должна начинаться после 51200 байт flash памяти.
Разбиение flash памяти:
Секция bootloader:
- Размер: 50 кбайт (51200 байт)
- Адресное пространство(flash память): 0x00000000 до 0x0000C7FF
Секция application:
- Размер: 206 кбайт (256кбайт-50кбайт)
- Адресное пространство(flash память): 0x0000C800 до 0x0003FFFF
Для того чтобы сформировать прошивку, начинающуюся с нужного нам адреса, а не с начала 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
Теперь скомпиленный бинарник можно заливать через бутлоадер.
Комментарии (6)
igor_suhorukov
31.08.2015 23:26Maple stm32 выложили код своего USB bootloader который работает как. А также nxp semiconductors выпускает чипы с аппаратным bootloader как USB Mass Storage Device и есть порт для других чипов
den1s1
31.08.2015 23:52+1Решение с USB бутлоадером достаточно очевидно, поэтому не удивительно, что большинство лидеров имеют его в своём портфолио.
Но нужно признать, что решение от NXP обладает изяществом в высшей степени.igor_suhorukov
01.09.2015 00:12Народ жалуется на «фичи» работы прошитого USB бутлоадера от NXP. Вроде как проблемы то ли под linux, то ли windows
den1s1
01.09.2015 19:06А вот тут уже проявляется преимущество самописного бутлоадера, в котором ты сам можешь избавиться от «фич»))
wertex
По опыту скажу, что в алгоритм стоит внести возможность принудительного перевода устройства в бут режим (например, замыканием какой-то ножки на землю). Если по какой-то причине в устройство запишется некорректная прошивка (Application section будет не пуста), то устройство уже никогда нельзя будет обновить, кроме как самим разработчиком.
marus-ka
Спасибо за ценный комментарий. В статье рассматривался пример (основные принципы написания кода), и алгоритм, конечно, требует правки для реальных условий работы. Можно ввести кнопку, джампер, просто ножку для перевода в бут режим, выбор зависит от конкретной реализации.