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

    Возможность обновления прошивки на серийно выпускаемых изделиях, или на единичных изделиях, находящихся в эксплуатации у заказчика трудно переоценить. Это не просто даёт возможность последующего устранения багов и расширения функционала, но и позволяет разработчику с более лёгким сердцем выпускать «еще сыроватый» продукт на рынок, если руководство того требует.

    Поэтому важность наличия 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 <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

    Теперь скомпиленный бинарник можно заливать через бутлоадер.
    Rainbow
    Поставки электронных компонентов, инжиниринг
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 6

      +2
      По опыту скажу, что в алгоритм стоит внести возможность принудительного перевода устройства в бут режим (например, замыканием какой-то ножки на землю). Если по какой-то причине в устройство запишется некорректная прошивка (Application section будет не пуста), то устройство уже никогда нельзя будет обновить, кроме как самим разработчиком.
        0
        Спасибо за ценный комментарий. В статье рассматривался пример (основные принципы написания кода), и алгоритм, конечно, требует правки для реальных условий работы. Можно ввести кнопку, джампер, просто ножку для перевода в бут режим, выбор зависит от конкретной реализации.
        0
        Maple stm32 выложили код своего USB bootloader который работает как. А также nxp semiconductors выпускает чипы с аппаратным bootloader как USB Mass Storage Device и есть порт для других чипов
          +1
          Решение с USB бутлоадером достаточно очевидно, поэтому не удивительно, что большинство лидеров имеют его в своём портфолио.
          Но нужно признать, что решение от NXP обладает изяществом в высшей степени.
            0
            Народ жалуется на «фичи» работы прошитого USB бутлоадера от NXP. Вроде как проблемы то ли под linux, то ли windows
              0
              А вот тут уже проявляется преимущество самописного бутлоадера, в котором ты сам можешь избавиться от «фич»))

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое