В одной из прошлых статей мы писали про USB bootloader на микроконтроллере SAM D21 с ядром Cortex M0+. Использование флешки для обновления прошивки очень удобно, но подходит не для всех случаев. Например, если доступ к устройству ограничен или проблематичен, а связь с ним устанавливается удаленно. В подобных случаях, к разработке бутлоадера следует отнестись с особой тщательностью. Иначе, в случае ошибки высока вероятность получить «кирпич» и огромное число проблем себе на голову. Примером такого труднодоступного устройства может быть управляющая плата «умного» архитектурного светильника, висящего на фасаде здания на 7-м этаже.

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


В проекте, который лег в основу данной статьи, связь с удаленным устройством осуществляется по силовым проводам в соответствии со стандартом G3 PLC (Power Line Communication). На первый взгляд способ связи не должен иметь значения, т.к. для микроконтроллера он все равно сводится к обмену по UART/I2C/SPI. Однако, например, в нашем случае это накладывает ограничение в виде невозможности online обновления прошивки (когда бутлоадер устанавливает связь с сервером для получения новой прошивки), т.к. работа с PLC-модемом ресурсоемкая задача и размеров bootloader секции не хватает.
Таким образом, были сформированы следующие требования :
  • Процесс обновления происходит offline. Т.е. файл прошивки предварительно частями передается на целевое устройство и сохраняется во внешнюю flash-память. Запуск процесса обновления производится отдельной командой.
  • Управлением удаленным устройством и передачей файла с новой прошивкой занимается головное устройство (шлюз). Описание этих процессов в данной статье не рассматривается.
  • Для обмена информацией между application и bootloader секциями используется внешняя EEPROM. Например, для старта процесса обновления, application записывает в EEPROM соответствующий флаг и передает управление bootloader секции.
  • Перед обновлением обязательна проверка контрольной суммы прошивки, находящейся во внешней flash. В противном случае обновление не запускается, а управление возвращается application секции.
  • Первым всегда стартует bootloader. Помимо проверки команд в EEPROM, bootloader каждый раз перед передачей управления application секции проверяет корректность текущей прошивки во flash памяти контроллера с помощью CRC. CRC — хранится в виде последних двух байт флеша.



В статье про USB bootloader дана краткая теория по организации памяти семейств SAM D20/D21, и дублировать ее тут мы не будем. В качестве целевого микроконтроллера используется ATSAMD20G16. Проект будем собирать в IAR используя ASF (Atmel Software Framework).

Алгоритмы работы


Работа устройства всегда начинается с бутлоадера, затем в зависимости от команды в EEPROM, либо происходит обновление прошивки, либо просто проверяется корректность crc текущей прошивки. По окончании работы бутлоадера при отсутствии ошибок осуществляется переход в application:
#define APP_START_ADDRESS          0x00004000
// brief Function for starting application
// This function will configure the WDT module and enable it. The LED is
// kept toggling till WDT reset occurs.
static void start_application(void)
{
   // 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 
   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();
}


Общий алгоритм работы bootloader представлен на рисунке ниже:

Проверка CRC своей прошивки выполняется при каждом запуске микроконтроллера, чтобы избежать некорректной работы в случае повреждения основного кода. Что делать, если расчетное CRC не совпадает со считанным, решается в каждом конкретном приложении индивидуально. Можно бесконечно мигать красным светодиодом, можно залить резервную прошивку (тогда нужно предусмотреть место, где ее хранить).
Общий алгоритм работы application представлен на рисунке ниже. Здесь представлено только то, что непосредственно касается обновления прошивки.


Настройки проекта для application


Для реализации задуманного в проекте с application необходимо выполнить следующее:
  • считать средствами IAR контрольную сумму прошивки (CRC) и подставлять результат в виде последних двух байт получаемого бинарника
  • размещать application не с нулевого адреса памяти микроконтроллера, для того, чтобы оставить «место» для бутлоадера.

Делается это в настройках проекта.

Подсчет CRC


Таким настройкам соответствует полином 0x1021, при этом не надо забывать менять байты местами при подсчете с помощью кода, а также о том, что CRC средствами IAR считается по всей памяти, а не только по заполненной кодом. После подсчета CRC по содержимому всей памяти application в конце необходимо перевернуть результат путем «добавления» к контрольной сумме еще 2 нулевых байт (см. EWARM_DevelopmentGuide.ENU).

Код проверки CRC:

void TestChecksum()
{
   unsigned short calc = 0;
   unsigned char zeros[2] = {0, 0};
   /* Run the checksum algorithm */
    calc = slow_crc16(0,  (unsigned char *) ChecksumStart,(ChecksumEnd - ChecksumStart+1));
   /* Rotate out the answer */
   calc = slow_crc16(calc, zeros, 2);
   /* Test the checksum */
   if (calc != checksum)
  {
      abort(); /* Failure */
   }
}
unsigned short slow_crc16(unsigned short sum, unsigned char *p, unsigned int len)
{
   while (len--)
   {
      int i;
      unsigned char byte = *(p++);
      for (i = 0; i < 8; ++i)
     {
         unsigned long oSum = sum;
         sum <<= 1;
         if (byte & 0x80)
           sum |= 1;
        if (oSum & 0x8000)
           sum ^= 0x1021;
        byte <<= 1;
     }
   }
   return sum;
}


Настройки расположения кода для application

Указываем начало памяти и адрес таблицы векторов прерывания:



Код


Для работы нам понадобятся следующие модули ASF/МК:
  • NVM (контроллер энергонезависимой памяти),
  • SERCOM SPI (для общения со внешней flash памятью),
  • SERCOM I2C (для общения с внешней EEPROM памятью),

Кроме инициализации и настройки всех модулей необходимо обеспечить логику работы.
Полный код main.c
#include <asf.h>
#include «twi_driver.h»
#include «MCP7941x.h»
#include «at45db041d.h»
#include «init.h»
#include «utils.h»
//-----------------------------------------------------------------------------
//---------------глобальные переменные-----------------------------------------
//---------структуры экземпляров периферии-------------------------------------
extern struct spi_module spi_master_instance;
extern struct spi_slave_inst slave;
extern struct tc_module tc_instance_tc2;
extern struct i2c_master_module i2c_master_instance;

//------------------------периферия--------------------------------------------
//------------------------------i2c--------------------------------------------
unsigned char twi_out_data[9];
unsigned char twi_in_data[8];
//----------------------------SPI----------------------------------------------
extern unsigned char spi_in_data[ext_flash_page_size];
extern unsigned char spi_out_data[ext_flash_page_size];
unsigned char temp_buffer_write[ext_flash_page_size];
unsigned long rtc_cnt;
//-------------------counters for cycles---------------------------------------
unsigned int i,m;
unsigned char led_cnt;
//------------------------ работа с прошивкой----------------------------------
unsigned int page_addr;
unsigned int CRC;
unsigned int CRC_read;
unsigned char zero[2]={0,0};
unsigned int last_page_length;
unsigned int page_num;
unsigned int byte_amount;
unsigned int last_page_number;
const uint8_t flash_data[256];
//-----------------------------------------------------------------------------
//-------------------init----------------------------------------------------

void MC_init(void)
{
struct nvm_config config;

system_init();
SystemClock_Init();
configure_tc2();
configure_pins();
configure_spi_master();
configure_i2c_master();
nvm_get_config_defaults(&config);
nvm_set_config(&config);
}

void init_variables(void)
{
//------------------------------i2c--------------------------------------------
clear_twi_in_buffer();
clear_twi_out_buffer();
//----------------------------SPI----------------------------------------------
for(i=0;i<ext_flash_page_size;i++) temp_buffer_write[i]=0;
rtc_cnt=0;
//-------------------counters for cycles---------------------------------------
led_cnt=0;
//------------------------ other-----------------------------------------------
last_page_number=0;
}

void external_init(void)
{
unsigned char temp;
//----------------external eeprom---------------------
//проверяем, что память не защищена от записи
clear_twi_in_buffer();
twi_read_bytes(twi_in_data, 1, EEPROM_ADR, 0xff);
if(twi_in_data[0]!=0)
{
clear_twi_out_buffer();
twi_out_data[0]=0xff;
twi_out_data[1]=0;
twi_write_bytes(twi_out_data, 2, EEPROM_ADR, EEPROM_OWN_METERS_1_STATUS);
}
//----------------FLASH-------------------------------
// проверяем размер страницы внешней flash
temp=flash_wait(spi_delay);
// если 264, то устанавливаем 256
if((temp&0x01)!=0x01) set_page_size_256();
temp=0;
}

// brief Function for programming data to Flash
// This function will check whether the data is greater than Flash page size.
// If it is greater, it splits and writes pagewise.
// param address address of the Flash page to be programmed
// param buffer pointer to the buffer containing data to be programmed
// param len length of the data to be programmed to Flash in bytes

static void program_memory(uint32_t address, uint8_t *buffer, uint16_t len)
{
// Check if length is greater than Flash page size
if (len > NVMCTRL_PAGE_SIZE)
{
uint32_t offset = 0;
while (len > NVMCTRL_PAGE_SIZE)
{
// Check if it is first page of a row
if ((address & 0xFF) == 0)
{
//Erase row
nvm_erase_row(address);
}
// Write one page data to flash
nvm_write_buffer(address, buffer + offset, NVMCTRL_PAGE_SIZE);
// Increment the address to be programmed
address += NVMCTRL_PAGE_SIZE;
// Increment the offset of the buffer containing data
offset += NVMCTRL_PAGE_SIZE;
// Decrement the length
len -= NVMCTRL_PAGE_SIZE;
}
//Check if there is data remaining to be programmed
if (len > 0)
{
// Write the data to flash
nvm_write_buffer(address, buffer + offset, len);
}
}
else
{
// Check if it is first page of a row)
if ((address & 0xFF) == 0)
{
// Erase row
nvm_erase_row(address);
}
// Write the data to flash
nvm_write_buffer(address, buffer, len);
}
}

// brief Function for starting application
// This function will configure the WDT module and enable it. The LED is
// kept toggling till WDT reset occurs.
static void start_application(void)
{
// 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
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();
}

unsigned int slow_crc16(unsigned short sum, unsigned char *p,unsigned int len)
{
while (len--)
{
int i;
unsigned char byte = *(p++);

for (i = 0; i < 8; ++i)
{
unsigned long osum = sum;
sum <<= 1;
if (byte & 0x80)
sum |= 1;
if (osum & 0x8000)
sum ^= 0x1021;
byte <<= 1;
}
}
return sum;
}

void MC_reset(void)
{
tc_reset(&tc_instance_tc2);
i2c_master_reset(&i2c_master_instance);
spi_reset(&spi_master_instance);

}
//------------------------!!!!!!MAIN!!!!!!-------------------------------------
int main(void)
{
unsigned int temp;
unsigned int j;
unsigned char k;

init_variables();
MC_init();

for(i=0;i<20;i++)
{
LED_2_TOGGLE();
delay_ms(100);
}

system_interrupt_enable_global();
external_init();
//------------проверяем, есть ли команда на upgrade-------------------------
for(i=0;i<8;i++) twi_in_data[i]=0;
// считываем upgrade из внешней EEPROM
twi_read_bytes(twi_in_data,4, EEPROM_ADR,EEPROM_UPGRADE);
// команда на апгрейд
if(twi_in_data[0]==0x55)
{
for(i=0;i<20;i++)
{
LED_1_TOGGLE();
delay_ms(100);
}
// определяем длину прошивки и положение crc в ней
// так как мы передаем по PLC всю память, а не только код
// пока не придумала, как передавать только код
for(page_addr=2; page_addr<194; page_addr++)
{
//читаем страницу
continuous_low_freq_read(spi_in_data, page_addr,0,ext_flash_page_size);
temp=0;
for(j=0;j<ext_flash_page_size;j++)
{
if(spi_in_data[j]==0xff)
{
temp++;
}
else temp=0;
}
//первая пустая страница
if(temp==ext_flash_page_size)
{
//читаем предыдущую страницу
page_addr--;
continuous_low_freq_read(spi_in_data, page_addr,0,ext_flash_page_size);
last_page_number=page_addr;
//анализируем последнюю страницу с кодом с конца
//если последний байт не ff, то это CRC
if(spi_in_data[ext_flash_page_size-1]!=0xff)
{
CRC_read=spi_in_data[ext_flash_page_size-2];
CRC_read<<=8;
CRC_read|=spi_in_data[ext_flash_page_size-1];
}
//если последний байц ff, то ищем место crc
else
{
i=ext_flash_page_size-1;
while((spi_in_data[i]==0xff)&&(i>0))
{
i--;
}
CRC_read=spi_in_data[i];
CRC_read<<=8;
CRC_read|=spi_in_data[i-1];
// это уже положение последнего байта crc
last_page_length=i+1;
break;
}
}
}
}
//-------------- проверяем CRC текущей прошивки-----------------------------
else
{
k=0;
CRC=0;
CRC_read=0xffff;
// если не сходится, будем пытаться 5 раз
// если не сойдется, то мигаем красным светодиодом до посинения)
while((CRC!=CRC_read)&&(k<5))
{
k++;
CRC=0;
CRC_read=0;
// определяем длину прошивки и положение crc в ней
for(page_addr=0x4000; page_addr<0x10000 ;page_addr+=NVMCTRL_PAGE_SIZE)
{
//читаем страницу
nvm_read_buffer(page_addr,spi_in_data,NVMCTRL_PAGE_SIZE);
temp=0;
for(j=0;j<64;j++)
{
if(spi_in_data[j]==0xff)
{
temp++;
}
else temp=0;
}
//первая пустая страница
if(temp==NVMCTRL_PAGE_SIZE)
{
//читаем предыдущую страницу
page_addr-=NVMCTRL_PAGE_SIZE;
nvm_read_buffer(page_addr,spi_in_data,NVMCTRL_PAGE_SIZE);
last_page_number=page_addr;
//анализируем последнюю страницу с кодом с конца
//если последний байт не ff, то это CRC
if(spi_in_data[NVMCTRL_PAGE_SIZE-1]!=0xff)
{
CRC_read=spi_in_data[NVMCTRL_PAGE_SIZE-2];
CRC_read<<=8;
CRC_read|=spi_in_data[NVMCTRL_PAGE_SIZE-1];
}
//если последний байц ff, то ищем место crc
else
{
i=NVMCTRL_PAGE_SIZE-1;
while((spi_in_data[i]==0xff)&&(i>0))
{
i--;
}
CRC_read=spi_in_data[i];
CRC_read<<=8;
CRC_read|=spi_in_data[i-1];
// это уже положение последнего байта crc
last_page_length=i+1;
break;
}
}
}
// непосредственно считаем crc
for(page_addr=0x4000; page_addr<last_page_number+1; page_addr+=NVMCTRL_PAGE_SIZE)
{
//читаем страницу
nvm_read_buffer(page_addr,spi_in_data,NVMCTRL_PAGE_SIZE);
temp=0;
//меняем местами соседние байты в считанном
for(j=0;j<NVMCTRL_PAGE_SIZE;j+=2)
{
temp=spi_in_data[j];
spi_in_data[j]=spi_in_data[j+1];
spi_in_data[j+1]=temp;
}
if(page_addr==last_page_number)
{
// считаем отдельно crc части страницы до CRC линкера
CRC=slow_crc16(CRC,spi_in_data, last_page_length-2);
// и после CRC линкера
temp=NVMCTRL_PAGE_SIZE-last_page_length;
CRC=slow_crc16(CRC,&(spi_in_data[last_page_length]), temp);
}
//если на всей странице код
else
{
CRC=slow_crc16(CRC,spi_in_data,NVMCTRL_PAGE_SIZE);
}
}
for(i=0;i<NVMCTRL_PAGE_SIZE;i++) spi_in_data[i]=0xff;
while(page_addr<0x10000)
{
CRC=slow_crc16(CRC,spi_in_data,NVMCTRL_PAGE_SIZE);
page_addr+=NVMCTRL_PAGE_SIZE;
}
CRC=slow_crc16(CRC,zero,2);
}// end o fwhile((CRC!=CRC_read)&&(k<5))
// если crc не сошлось
if(CRC!=CRC_read)
{
while(1)
{
LED_1_TOGGLE();
delay_ms(500);
}
}
//если CRC сошлось
else
{
MC_reset();
start_application();
}
}
CRC=0;
////////////////////////////////////////////////////////////////////////////////
//------------------------------MAIN PROGRAM-----------------------------------
////////////////////////////////////////////////////////////////////////////////
while(1)
{
//считываем страницы для проверки CRC
for(page_addr=2; page_addr<last_page_number+1; page_addr++)
{
//читаем страницу
continuous_low_freq_read(spi_in_data, page_addr,0,ext_flash_page_size);
temp=0;
//меняем местами соседние байты в считанном
for(j=0;j<ext_flash_page_size;j+=2)
{
temp=spi_in_data[j];
spi_in_data[j]=spi_in_data[j+1];
spi_in_data[j+1]=temp;
}
if(page_addr==last_page_number)
{
// считаем отдельно crc части страницы до CRC линкера
CRC=slow_crc16(CRC,spi_in_data, last_page_length-2);
// и после CRC линкера
temp=ext_flash_page_size-last_page_length;
CRC=slow_crc16(CRC,&(spi_in_data[last_page_length]), temp);
}
//если на всей странице код
else
{
CRC=slow_crc16(CRC,spi_in_data,ext_flash_page_size);
}
}
//переводим адрес в байты из номеров страниц внешней флеш
page_addr<<=8;
for(i=0;i<NVMCTRL_PAGE_SIZE;i++) spi_in_data[i]=0xff;
while(page_addr<0xc200)
{
CRC=slow_crc16(CRC,spi_in_data,NVMCTRL_PAGE_SIZE);
page_addr+=NVMCTRL_PAGE_SIZE;
}
CRC=slow_crc16(CRC,zero,2);

// если CRC совпало
// считываем еще раз и записываем в свою flash
// иначе переходим в application с каким-нибудь флагом
// для отката прошивки CRC не считаем: не знаем ее размер и место CRC
// но по умолчанию они обе равны 0
if(CRC==CRC_read)
{
page_addr=0x4000;
for(i=0; i<15; i++)
{
// стираем и записываем новую прошивку
continuous_low_freq_read(spi_in_data,i,0,NVMCTRL_PAGE_SIZE);
program_memory(page_addr, spi_in_data, 256);
page_addr+=256;
delay_ms(10);
}
twi_out_data[0]=EEPROM_FW_RESULT;
twi_out_data[1]=0xda;
twi_write_bytes(twi_out_data,2, EEPROM_ADR,EEPROM_FW_RESULT);
}
else
{
twi_out_data[0]=EEPROM_FW_RESULT;
twi_out_data[1]=0xad;
twi_write_bytes(twi_out_data,2, EEPROM_ADR, EEPROM_FW_RESULT);
}
// стираем только команду на перепрошивку
// параметры прошивки не стираем
delay_ms(100);
twi_out_data[0]=EEPROM_UPGRADE;
twi_out_data[1]=0xff;
twi_write_bytes(twi_out_data,2, EEPROM_ADR, EEPROM_UPGRADE);
delay_ms(100);
for(j=2;j<2048;j++)
{
//стираем всю флеш, кроме первых двух страниц, в которых адреса забинденных
erase_page(j);
temp=flash_wait(spi_delay);
temp=0;
}
for(i=0;i<20;i++)
{
LED_1_TOGGLE();
delay_ms(300);
}
MC_reset();
start_application();

}// end of while(1)
}// end of main

Комментарии (3)


  1. GarryC
    02.10.2015 09:55

    Проверка CRC своей прошивки выполняется при каждом запуске микроконтроллера, чтобы избежать некорректной работы в случае повреждения основного кода. Что делать, если расчетное CRC не совпадает со считанным, решается в каждом конкретном приложении индивидуально. Можно бесконечно мигать красным светодиодом, можно залить резервную прошивку (тогда нужно предусмотреть место, где ее хранить)

    Прикольное предложение. Т.е. наша прошивка бракована, но при этом мы надеемся на предсказуемое поведение МК. Весьма оптимистичное поведение, любопытно, на чем оно основано?


    1. marus-ka
      02.10.2015 10:10

      Может и оптимистично слишком. Но в основном надежда на то, что если повредится основной код, то хотя бы бут будет работать. Тем более, что его можно защитить фьюзами от несанкционированной записи/стирания. Понятно, что если и бут повредится, то тут уж ничего не поделаешь.


      1. GarryC
        05.10.2015 11:46

        То есть бут в выделенной области программ? Тогда да, может получиться.