В данной статье хотел бы написать о своем опыте создания загрузчика для STM32 с шифрованием прошивки. Я являюсь индивидуальным разработчиком, поэтому нижеприведенный код может не соответствовать каким-либо корпоративным стандартам
В процессе работы ставились следующие задачи:
Код писался в Keil uVision с использованием библиотек stdperiph, fatFS и tinyAES. Подопытным микроконтроллером был STM32F103VET6, но код может быть легко адаптирован под другой контроллер STM. Контроль целостности обеспечивается алгоритмом CRC32, контрольная сумма расположена в последних 4 байтах файла с прошивкой.
В статье не описано создание проекта, подключение библиотек, инициализация периферии и прочие тривиальные этапы.
Для начала стоит определиться с тем, что такое загрузчик. Архитектура STM32 подразумевает плоскую адресацию памяти, когда в одном адресном пространстве находится Flash-память, RAM, регистры периферии и всё остальное. Загрузчик — это программа, которая начинает выполняться при запуске микроконтроллера, проверяет, нужно ли выполнить обновление прошивки, если нужно — выполняет его, и запускает основную программу устройства. В данной статье будет описан механизм обновления с SD-карты, но можно использовать любой другой источник.
Шифрование прошивки производится алгоритмом AES128 и реализовано при помощи библиотеки tinyAES. Она представляет из себя всего два файла, один с расширением .c, другой с расширением .h, поэтому проблем с её подключением возникнуть не должно.
После создания проекта следует определиться с размерами загрузчика и основной программы. Для удобства размеры следует выбирать кратно размеру страницы памяти микроконтроллера. В данном примере загрузчик будет занимать 64 Кб, а основная программа займет оставшиеся 448 Кб. Загрузчик будет размещаться в начале Flash-памяти, а основная программа сразу после загрузчика. Это следует указать в настройках проекта в Keil. Загрузчик у нас начинается с адреса 0x80000000 (именно с него STM32 начинает выполнение кода после запуска) и имеет размер 0x10000, указываем это в настройках.

Основная программа будет начинаться с 0x08010000 и заканчиваться на 0x08080000 для удобства сделаем define со всеми адресами:
Так же внесем в программу ключи шифрования и инициализационный вектор AES. Данные ключи лучше сгенерировать случайным образом.
В данном примере вся процедура обновления прошивки построена в виде конечного автомата. Это позволяет в процессе обновления отображать что-то на экране, сбрасывать Watchdog и выполнять любые другие действия. Для удобства сделаем define с основными состояниями автомата, чтобы не путаться в числах:
После инициализации периферии нужно проверить необходимость обновления прошивки. В первом состоянии производится попытка чтения SD-карты и проверка наличия файла на ней.
Теперь нам нужно провести проверку прошивки на корректность. Здесь сначала идет код проверки контрольной суммы, выполняющийся при окончании чтения файла, а потом само чтение. Возможно, так писать не следует, напишите в комментариях что вы об этом думаете. Чтение производится по 2 Кб для удобства работы с Flash-памятью, т.к. у STM32F103VET6 размер страницы памяти 2 Кб.
Теперь, если прошивка не повреждена, то нужно её снова прочитать, но на этот раз уже записать во Flash — память.
Теперь для красоты создадим состояния для обработки ошибки и успешного обновления:
Функцию запуска основной программы ExecMainFW() стоит рассмотреть подробнее. Вот она:
Сразу после запуска startup файл все переинициализировал, поэтому основная программа должна вновь выставить указатель на вектор прерывания внутри своего адресного пространства:
В проекте основной программы нужно указать правильные адреса:

Вот, собственно, и вся процедура обновления. Прошивка проверяется на корректность и шифруется, все поставленные задачи выполнены. В случае потери питания в процессе обновления устройство, конечно, закирпичится, но загрузчик останется нетронутым и процедуру обновления можно будет повторить. Для особо ответственных ситуаций можно заблокировать на запись страницы, в которых находится загрузчик через Option bytes.
Однако, в случае с SD-картой можно организовать для самого себя в загрузчике одно приятное удобство. Когда тестирование и отладка новой версии прошивки завершена, можно заставить само устройство по какому-то особому условию (например, кнопка или джампер внутри) зашифровать и выгрузить на SD-карту готовую прошивку. В таком случае останется только извлечь SD-карту из устройства, вставить в компьютер и выложить прошивку в интернет на радость пользователям. Сделаем это в виде ещё двух состояний конечного автомата:
Вот, собственно и всё, что я хотел рассказать. В завершении статьи хотел бы пожелать вам после создания подобного загрузчика не забыть включить защиту от чтения памяти микроконтроллера в Option bytes.
tinyAES
FatFS
В процессе работы ставились следующие задачи:
- Обеспечить обновление прошивки пользователем устройства с SD-карты.
- Обеспечить контроль целостности прошивки и исключить запись некорректной прошивки в память контроллера.
- Обеспечить шифрование прошивки для исключения клонирования устройства.
Код писался в Keil uVision с использованием библиотек stdperiph, fatFS и tinyAES. Подопытным микроконтроллером был STM32F103VET6, но код может быть легко адаптирован под другой контроллер STM. Контроль целостности обеспечивается алгоритмом CRC32, контрольная сумма расположена в последних 4 байтах файла с прошивкой.
В статье не описано создание проекта, подключение библиотек, инициализация периферии и прочие тривиальные этапы.
Для начала стоит определиться с тем, что такое загрузчик. Архитектура STM32 подразумевает плоскую адресацию памяти, когда в одном адресном пространстве находится Flash-память, RAM, регистры периферии и всё остальное. Загрузчик — это программа, которая начинает выполняться при запуске микроконтроллера, проверяет, нужно ли выполнить обновление прошивки, если нужно — выполняет его, и запускает основную программу устройства. В данной статье будет описан механизм обновления с SD-карты, но можно использовать любой другой источник.
Шифрование прошивки производится алгоритмом AES128 и реализовано при помощи библиотеки tinyAES. Она представляет из себя всего два файла, один с расширением .c, другой с расширением .h, поэтому проблем с её подключением возникнуть не должно.
После создания проекта следует определиться с размерами загрузчика и основной программы. Для удобства размеры следует выбирать кратно размеру страницы памяти микроконтроллера. В данном примере загрузчик будет занимать 64 Кб, а основная программа займет оставшиеся 448 Кб. Загрузчик будет размещаться в начале Flash-памяти, а основная программа сразу после загрузчика. Это следует указать в настройках проекта в Keil. Загрузчик у нас начинается с адреса 0x80000000 (именно с него STM32 начинает выполнение кода после запуска) и имеет размер 0x10000, указываем это в настройках.

Основная программа будет начинаться с 0x08010000 и заканчиваться на 0x08080000 для удобства сделаем define со всеми адресами:
#define MAIN_PROGRAM_START_ADDRESS 0x08010000 #define MAIN_PROGRAM_END_ADDRESS 0x08080000
Так же внесем в программу ключи шифрования и инициализационный вектор AES. Данные ключи лучше сгенерировать случайным образом.
static const uint8_t AES_FW_KEY[] = {0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF}; static const uint8_t AES_IV[] = {0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA};
В данном примере вся процедура обновления прошивки построена в виде конечного автомата. Это позволяет в процессе обновления отображать что-то на экране, сбрасывать Watchdog и выполнять любые другие действия. Для удобства сделаем define с основными состояниями автомата, чтобы не путаться в числах:
#define FW_START 5 #define FW_READ 1000 #define FW_WRITE 2000 #define FW_FINISH 10000 #define FW_ERROR 100000
После инициализации периферии нужно проверить необходимость обновления прошивки. В первом состоянии производится попытка чтения SD-карты и проверка наличия файла на ней.
uint32_t t; /* Временная переменная */ uint32_t fw_step; /* Индекс состояния конечного автомата */ uint32_t fw_buf[512]; /* Буфер для считанного блока прошивки */ uint32_t aes_buf[512]; /* Буфер для расшифрованного блока прошивки равен */ /* Буферы равны размеру страницы Flash-памяти*/ uint32_t idx; /* Текущий адрес в памяти */ char tbuf[64]; /* Временный буфер для sprintf */ FATFS FS; /* Структура библиотеки fatFS - файловая система */ FIL F; /* Структура библиотеки fatFS - файл */ case FW_READ: /* Чтение прошивки */ { if(f_mount(&FS, "" , 0) == FR_OK) /* Пробуем смонтировать SD-карту*/ { /* Проверяем, есть ли файл с прошивкой. */ if(f_open(&F, "FIRMWARE.BIN", FA_READ | FA_OPEN_EXISTING) == FR_OK) { f_lseek(&F, 0); /* Переходим в начало файла */ CRC_ResetDR(); /* Сбрасываем аппаратный счетчик CRC */ lcd_putstr("Обновление прошивки", 1, 0); /* Выводим сообщение на экран */ /* Устанавливаем адрес чтения на начало основной программы */ idx = MAIN_PROGRAM_START_ADDRESS; fw_step = FW_READ + 10; /* Переходим к следующему состоянию */ } else {fw_step = FW_FINISH;} /* Если файла нет - завершаем загрузчик */ } else {fw_step = FW_FINISH;} /* Если нет SD-карты - завершаем загрузчик */ break; }
Теперь нам нужно провести проверку прошивки на корректность. Здесь сначала идет код проверки контрольной суммы, выполняющийся при окончании чтения файла, а потом само чтение. Возможно, так писать не следует, напишите в комментариях что вы об этом думаете. Чтение производится по 2 Кб для удобства работы с Flash-памятью, т.к. у STM32F103VET6 размер страницы памяти 2 Кб.
case FW_READ + 10: /* Проверка корректности файла с прошивкой */ { /* В процессе показываем на экране, сколько байт считано */ sprintf(tbuf, "Проверка: %d", idx - MAIN_PROGRAM_START_ADDRESS); lcd_putstr(tbuf, 2, 1); if (idx > MAIN_PROGRAM_END_ADDRESS) /* Если прочитаи весь файл прошивки */ { f_read(&F, &t, sizeof(t), &idx); /* Считываем 4 байта контрольной суммы */ /* Записываем считанные 4 байта в регистр данных периферийного блока CRC */ CRC_CalcCRC(t); if(CRC_GetCRC() == 0) /* Если результат 0, то файл не поврежден */ { /* Устанавливаем адрес записи на адрес начала основной программы */ idx = MAIN_PROGRAM_START_ADDRESS; f_lseek(&F, 0); /* Переходим в начало файла */ fw_step = FW_READ + 20; /* Переходим к следующему состоянию */ break; } else { lcd_putstr("Файл поврежден", 3, 2); /* Выводим сообщение на экран */ fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */ break; } } f_read(&F, &fw_buf, sizeof(fw_buf), &t); /* Считываем 2 Кб из файла в буфер */ if(t != sizeof(fw_buf)) /* Если не получилось считать */ { lcd_putstr("Ошибка чтения", 3, 2); fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */ break; } /* Расшифровываем считанный блок прошивки */ AES_CBC_decrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV); for(t=0;t<NELEMS(aes_buf);t++) /* Записываем блок в регистр CRC */ { CRC_CalcCRC(aes_buf[t]); /* Запись ведем по 4 байта */ } idx+=sizeof(fw_buf); /* Сдвигаем адрес на следующие 2 Кб */ break; }
Теперь, если прошивка не повреждена, то нужно её снова прочитать, но на этот раз уже записать во Flash — память.
case FW_READ + 20: // Flash Firmware { /* В процессе показываем на экране, сколько байт записано */ sprintf(tbuf, "Запись: %d", idx - MAIN_PROGRAM_START_ADDRESS); lcd_putstr(tbuf, 4, 2); if (idx > MAIN_PROGRAM_END_ADDRESS) /* Когда записали всю прошивку */ { lcd_putstr("Готово", 7, 3); /* Выводим сообщение на экран */ f_unlink("FIRMWARE.BIN"); /* Удаляем файл прошивки с SD-карты */ fw_step = FW_FINISH; /* Завершаем загрузчик */ break; } f_read(&F, &fw_buf, sizeof(fw_buf), &t); /* Считываем блок 2 Кб */ if(t != sizeof(fw_buf)) /* Если не получилось считать */ { lcd_putstr("Ошибка чтения", 3, 3); /* Выводим сообщение на экран */ fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */ break; } /* Расшифровываем считанный блок прошивки */ AES_CBC_decrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV); FLASH_Unlock(); /* Разблокируем FLash-память на запись */ FLASH_ErasePage(idx); /* Стираем страницу памяти */ for(t=0;t<sizeof(aes_buf);t+=4) /* Записываем прошивку по 4 байта */ { FLASH_ProgramWord(idx+t, aes_buf[t/4]); } FLASH_Lock(); /* Блокируем прошивку на запись */ idx+=sizeof(fw_buf); /* Переходим к следующей странице */ break; }
Теперь для красоты создадим состояния для обработки ошибки и успешного обновления:
case FW_ERROR: { /* Можно что-то сделать при ошибке обновления */ break; } case FW_FINISH: { ExecMainFW(); /* Запускаем основную программу */ /* Дальнейший код выполнен не будет */ break; }
Функцию запуска основной программы ExecMainFW() стоит рассмотреть подробнее. Вот она:
void ExecMainFW() { /* Устанавливаем адрес перехода на основную программу */ /* Переход производится выполнением функции, адрес которой указывается вручную */ /* +4 байта потому, что в самом начале расположен указатель на вектор прерывания */ uint32_t jumpAddress = *(__IO uint32_t*) (MAIN_PROGRAM_START_ADDRESS + 4); pFunction Jump_To_Application = (pFunction) jumpAddress; /*Сбрасываем всю периферию на APB1 */ RCC->APB1RSTR = 0xFFFFFFFF; RCC->APB1RSTR = 0x0; /*Сбрасываем всю периферию на APB2 */ RCC->APB2RSTR = 0xFFFFFFFF; RCC->APB2RSTR = 0x0; RCC->APB1ENR = 0x0; /* Выключаем всю периферию на APB1 */ RCC->APB2ENR = 0x0; /* Выключаем всю периферию на APB2 */ RCC->AHBENR = 0x0; /* Выключаем всю периферию на AHB */ /* Сбрасываем все источники тактования по умолчанию, переходим на HSI*/ RCC_DeInit(); /* Выключаем прерывания */ __disable_irq(); /* Переносим адрес вектора прерываний */ NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS); /* Переносим адрес стэка */ __set_MSP(*(__IO uint32_t*) MAIN_PROGRAM_START_ADDRESS); /* Переходим в основную программу */ Jump_To_Application(); }
Сразу после запуска startup файл все переинициализировал, поэтому основная программа должна вновь выставить указатель на вектор прерывания внутри своего адресного пространства:
__disable_irq(); NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS); __enable_irq();
В проекте основной программы нужно указать правильные адреса:

Вот, собственно, и вся процедура обновления. Прошивка проверяется на корректность и шифруется, все поставленные задачи выполнены. В случае потери питания в процессе обновления устройство, конечно, закирпичится, но загрузчик останется нетронутым и процедуру обновления можно будет повторить. Для особо ответственных ситуаций можно заблокировать на запись страницы, в которых находится загрузчик через Option bytes.
Однако, в случае с SD-картой можно организовать для самого себя в загрузчике одно приятное удобство. Когда тестирование и отладка новой версии прошивки завершена, можно заставить само устройство по какому-то особому условию (например, кнопка или джампер внутри) зашифровать и выгрузить на SD-карту готовую прошивку. В таком случае останется только извлечь SD-карту из устройства, вставить в компьютер и выложить прошивку в интернет на радость пользователям. Сделаем это в виде ещё двух состояний конечного автомата:
case FW_WRITE: { if(f_mount(&FS, "" , 0) == FR_OK) /* Пробуем смонтировать SD-карту*/ { /* Пробуем создать файл */ if(f_open(&F, "FIRMWARE.BIN", FA_WRITE | FA_CREATE_ALWAYS) == FR_OK) { CRC_ResetDR(); /* Сбрасываем блок CRC */ /* Устанавливаем адрес чтения на начало основной программы */ idx = MAIN_PROGRAM_START_ADDRESS; fw_step = FW_WRITE + 10; /* Переходим к следующему состоянию */ } else {fw_step = FW_ERROR;} /* Переходим к шагу обработки ошибки */ } else {fw_step = FW_ERROR;} /* Переходим к шагу обработки ошибки */ break; } case FW_WRITE + 10: { if (idx > MAIN_PROGRAM_END_ADDRESS) /* Если выгрузили всю прошивку */ { t = CRC_GetCRC(); f_write(&F, &t, sizeof(t), &idx); /* Дописываем в конец файла контрольную сумму */ f_close(&F); /* Закрываем файл, сбрасываем кэш */ fw_step = FW_FINISH; /* Завершаем зарузчик */ } /* Считываем 2 Кб прошивки из Flash-памяти в буфер */ memcpy(&fw_buf, (uint32_t *)idx, sizeof(fw_buf)); for(t=0;t<NELEMS(fw_buf);t++) /* Вычисляем CRC для считанного блока */ { CRC_CalcCRC(fw_buf[t]); } /* Шифруем прошивку */ AES_CBC_encrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV); /* Записываем зашифрованный блок в файл */ f_write(&F, &aes_buf, sizeof(aes_buf), &t); idx+=sizeof(fw_buf); /* Сдвигаем адрес считываемого блока */ break; }
Вот, собственно и всё, что я хотел рассказать. В завершении статьи хотел бы пожелать вам после создания подобного загрузчика не забыть включить защиту от чтения памяти микроконтроллера в Option bytes.
Ссылки
tinyAES
FatFS
