Предисловие
Решил начать цикл статей на тему бутлоадера для STM32. Возможно это послужит руководством для начинающих разработчиков, а может помочь самым настоящим демиургам в сфере embedded разработки.
Начну с главного, а именно со структуры повествования. Я планирую сделать несколько статей, которые будут строиться от глобальной постановки вопросы к локальной:
Bootloader. Part 1. Нюансы Cortex-M, устройство памяти stm32 и преднастройка
Bootloader. Part 2. Программа бутлоадера. Пора шить
Bootloader. Part 3. Защита от зависаний и как вернуться в бутлоадер для перепрошивки
Bootloader. Part 4. Шифрование
Как стартует МК
Разбор будем делать на примере популярного микроконтроллера (далее МК) STM32.
МК стартует с адреса 0x00000000 потом в 0x00000004. В этот участок памяти отображает (мапит) содержимое памяти из адреса в зависимости от того какую ножку из BOOT мы потянули.
Многие знают эту таблицу:
boot0 | boot1 | BOOT mode | Aliasing |
---|---|---|---|
x | 0 | Main Flash memory | Main Flash memory is selected as boot space |
0 | 1 | System memory | System memory is selected as boot space |
1 | 1 | Embedded SRAM | Embedded SRAM is selected as boot space |
Как раз она описывает из какого адреса будет мапиться память МК. Нас интересует чтобы память мапилась из сектора памяти где хранится основная прошивка (это первая строка таблицы), а именно адрес 0x08000000. Если хотите чтобы запустился бутлоадер от STM32 по uart (адрес 0x1FFF0000), то подцепите ножку boot1 к логической единице (но сегодня не об этом).
Адрес 0x08000000 - это адрес с которого начинается наша прошивка. По этому адресу хранится указатель на стек (регистр stack pointer - SP). А по адресу 0x08000004 хранится указатель на вектор сброса.
Чуть-чуть теории об архитектуре:
Когда наша программа отобразилась на адрес 0x00000000 (в нем отобразилось содержимое адреса 0x08000000) там будет все то же самое. Контроллер запускается и смотрит что находится в 0x00000000 записывает содержимое и уже знает что стек начинается (или конец RAM памяти) с некоего адреса, он начинается с 0x2xxxxxxx. Именно это и будет конец RAM памяти, где начинается стек.
После этого контроллер читает содержимое 0x00000004 адреса памяти (в нем отобразилось содержимое адреса 0x08000004). Здесь хранится указатель на функцию сброса, которая находится уже по какому-то 0x08xxxxxxx адресу. Тут он и начинает работать в нужной нам области памяти пользователя, а не в какой-то мнимой системной памяти.
Общие положения для бутлоадера
ТЗ для bootloader
Bootloader - это по сути свой часть всей прошивки, которая может/должна работать самостоятельно.
с него, обязательно, должна начаться наша программа (если умрет он значит камень превращается в кирпич (в большинстве случаев));
он должен быть неубиваемый (если умрет он, то у нас больше не будет доступа к камню);
он должен переключиться на основную прошивку (потому что он не выполняет бизнес логику);
Задача со звездочкой: никто не должен знать как мы перепрошиваем камень.
С бутлоадера должна начаться наша программа МК:
Это первый пункт нашего ТЗ. Так как же мы должны начать? Да все так же. Конфигурируем МК, генерируем код и пишем прошивку. Нет! С последним пунктом подождем. Для начала нужно еще поработать с памятью. Существует такой файл как STM32F765ZGTX_FLASH.ld или STM32F407VETX_FLASH.ld или STM32F091CCUX_FLASH.ld (в зависимости от выбранного контроллера начало файла может быть другим, но суть одна). Это файл линкер скрипта. В нем указывается как будет распределяться память в МК.
Вся память описывается в конструкции (для камня STM32F407VE):
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}
Здесь вся прошивка хранится в области FLASH, но мы хотим чтобы было как бы две прошивки (одна для бутлоадера, другая для основной прошивки). Поэтому для бутлоадера и основной прошивки мы должны выделить отдельное место под солнцем:
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 48K
BOOT_DATA (rx) : ORIGIN = 0x800C000, LENGTH = 16K
APP (rx) : ORIGIN = 0x8010000, LENGTH = 448K
}
_sapp = ORIGIN(APP);
_eapp = ORIGIN(APP) + LENGTH(APP);
_smem = ORIGIN(BOOT_DATA);
В области FLASH хранится Bootloader;
В области APP хранится основная программа (разметили его здесь, чтобы использовать данные по его расположению в бутлоадере и не задавать какие-то дефайны с адресами);
В области BOOT_DATA хранятся данные о прошивке. Особенно важна эта память для того, чтобы определить существует ли вообще прошивка;
Переменная
_sapp
нужна для определения начального адреса прошивки;Переменная
_eapp
нужна для определения конечного адреса прошивки;Переменная
_smem
нужна для определения начального адреса данных бутлоадера. Здесь будем хранить данные о прошивке. Они не сотрутся т.к. хранятся во flash памяти (будто они часть программы) это могут быть: версия, контрольная сумма файла прошивки и т.п. При стирании прошивки эту область также нужно стереть, чтобы понимать что прошивки нет.
Здесь есть тонкий момент. Нельзя распределять память как Бог на душу положит. Мы все же инженеры, и руководствуемся лишь мануалами. Поэтому, для этого камня, я распредилил память по секторам, как указано в мануале. Потому что когда мы будем стирать прошивку или раздел для данных бутлоадера, нам придется стереть целый сектор памяти. Для данного камня они распредляются исходя из мануала:
Sector | Block base address | Size |
---|---|---|
Sector 0 | 0x0800 0000 - 0x0800 3FFF | 16 Kbytes |
Sector 1 | 0x0800 0000 - 0x0800 3FFF | 16 Kbytes |
Sector 2 | 0x0800 8000 - 0x0800 BFFF | 16 Kbytes |
Sector 3 | 0x0800 C000 - 0x0800 FFFF | 16 Kbytes |
Sector 4 | 0x0801 0000 - 0x0801 FFFF | 64 Kbytes |
Sector 5 | 0x0802 0000 - 0x0803 FFFF | 128 Kbytes |
Sector 6 | 0x0804 0000 - 0x0805 FFFF | 128 Kbytes |
... | ... | ... |
Sector 11 | 0x080E 0000 - 0x080F FFFF | 128 Kbytes |
В нашем линкер скрипте:
Бутлоадер расположен с сектора 0;
Данные для бутлоадера хранятся с сектора 3;
Основная прошивка хранится с сектора 4;
Кажется все правильно...
Используем то что разметили в коде
Теперь определения _sapp, _eapp, _smem
можно использовать в коде (просто пример чтобы посмотреть на них):
extern uint8_t _sapp;
extern uint8_t _eapp;
extern uint8_t _smem;
printf("start address APP %d\r\n", (uint32_t*)&_sapp);
printf("end address APP %d\r\n", (uint32_t*)&_eapp);
printf("start address SHARED_MEMORY %d\r\n", (uint32_t*)&_smem);
Начинаем писать код бутлоадера. Я ставлю перед собой задачу сделать его универсальным, и предыдущие манипуляции мне в этом помогут.
Определим пару дефайнов для простого доступа к пространству памяти основной прошивки:
Дефайны:
extern uint8_t _sapp; // Переменная которая находится в начальном адреск прошивки
extern uint8_t _eapp; // Переменная которая находится в конечном адресе прошивки
extern uint8_t _smem; // Переменная которая находится в еачальном адресе общей области памяти
// Дефайны для них чтоб выделить сам адрес
#define FLASH_APP_START_ADDESS (uint32_t) & _sapp
#define FLASH_APP_END_ADDRESS (uint32_t) & _eapp
#define FLASH_MEM_ADDRESS (uint32_t) & _smem
Функция для проверки что основная прошивка валидна:
int fw_check(void)
{
extern void* _estack; // Это из линкера, генерируется автоматически и указывает на конец RAM (или стек)
if (((*(uint32_t*) FLASH_APP_START_ADDESS) & 0x2FFF8000) != &_estack) // Проверка первого адреса прошивки, значение в нем должно быть размером RAM (регистр SP)
return -1;
return 0;
}
Здесь следует помнить что в первом адресе любой прошивки хранится указатель на стек (регистр stack pointer - SP). Если он корректный, то считаем что прошивка существует и находится в нужном адресе. Но это, на самом деле, слабое доказательство оного. Здесь можно прописать сверку контрольной суммы, идентификаторы, которые записываются после полной загрузки прошивки и очищаются, если пришла команда стереть прошивку и т.п. Но пока нам хватит и такой проверки чтобы эту статью сильно не нагружать.
Следующее действие - это запуск основной программы:
void fw_go2APP(void)
{
uint32_t app_jump_address; // переменная для адреса прошивки
typedef void (*pFunction)(void); // объявляем пользовательский тип для функции которая запустит основную программу
pFunction Jump_To_Application; // и создаём переменную/функцию этого типа
HAL_Delay(100);
__disable_irq(); // запрещаем прерывания
app_jump_address = *(uint32_t*) (FLASH_APP_START_ADDESS + 4);
Jump_To_Application = (pFunction) app_jump_address; // приводим его к пользовательскому типу
__set_MSP(*(uint32_t*) FLASH_APP_START_ADDESS); // устанавливаем SP приложения (он вероятнее всего не изменится)
Jump_To_Application(); // запускаем приложение
}
Суть в том, чтобы перейти по вектору сброса.
Перед запуском прошивки необходимо выключить не только прерывания в общем смысле, но и деинициализировать их (если они были инициализированы), иначе они могут вызваться в прошивке (это можно выполнить и в этой функции).
Про процесс записи прошивки основной программы в следующей статье.
Общие положения для основной прошивки
Опять линкер скрипт...
Т.к. это вторая часть прошивки, то и для нее нужно указать где она будет находиться. В линкер скрипте правим:
MEMORY
{
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8010000, LENGTH = 448K
}
Здесь в области FLASH меняем начальный адрес и размер. Название остается, иначе придется перелопатить весь линкер скрипт, потому что там все определено именно для FLASH. Напомню что для бутлоадера мы определили эту область как APP чтобы были универсальные переменные для программы бутлодера. Потом из этого получится отдельный модуль, которому не нужно будет отдельно настраивать адреса, а линкер скрипт править нужно всегда для таких задач. Но и это можно сделать автоматически, например, с помощью cmake, но об этом как-нибудь в другой раз.
Таблица векторов прерываний
Это самый сложный и запутанный пункт из-за разных архитектур.
Требуется определить таблицу векторов прерываний. Что это вообще такое? Мы же вроде все определили... Дело в том, что когда любое прерывание срабатывает контроллер не в курсе куда по сути идти, это же не основной код программы, а аппаратный триггер, по которому контроллер начинает выполнять код каких-то функций-обработчиков прерываний. Указатели на эти функции хранятся в векторе прерываний и он всегда аппаратный. Штатно все эти функции находятся в начале прошивки, как и сброс, который мы самостоятельно вызываем в бутлоадере, чтобы запустить основную программу. Но аппаратная часть МК не в курсе, что они теперь не в начале всей флеш памяти, а с каким-то смещением, на которое сдвинута основная программа. Поэтому вызов прерываний приведет к тому, что программа выполнит обработчик прерываний, который мы определили в бутлоадере, если определили. А потом вернется к выполнению программы основной прошивки и лишь Вселенной ясно что будет дальше.
Поэтому перед нами стоит задача - дать МК понять куда надо идти если сработало какое-либо прерывание.
Cortex-M3 и Cortex-M4
Здесь требуется установить смещение для вектора прерываний в файле system_stm32f4xx.c (для f4 серии):
#define VECT_TAB_OFFSET 0x10000U /*!< Vector Table base offset field.
Адрес должен соответствовать начальному адресу прошивки. Вообще я немного против такого подхода, этот файл надо найти, раскомментировать дефайн в нем, потом прописать это значение. С помощью того же cmake или линкер скрипта можно чтобы это значение прописывалось в код при сборке. Поэтому в начале функции main пропишем:
int main(void)
{
SCB->VTOR = 0x80000000 | 0x10000; // задаем смещение
__enable_irq(); // а за одно и прерывания включим, которые выключили в бутлоадере
// остальной код...
}
Cortex-M0
Аппаратно эта группа ARM процессоров не поддерживает смещение векторов прерывания во флеше. Можно переопределить таблицу векторов только на RAM память. И отсюда весь головняк с Cortex-M0.
Т.о. нужно переписать всю таблицу в RAM и сказать МК, что теперь он смотрит на эту таблицу там (а не во Flash по адресу 0x08000004 по стандарту).
В линкер скрипте, после определения:
.fini_array :
{
. = ALIGN(4);
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
. = ALIGN(4);
} >FLASH
прописываем:
.ram_vector :
{
*(.ram_vector)
} >RAM
Это позволяет зарезервировать начало RAM чтобы там разместить таблицу векторов и больше ничего.
Заполнение таблицы должно быть в начале main():
#define VECTOR_TABLE_SIZE (31 + 1 + 7 + 9)//31 positive vectors, 0 vector, and 7 negative vectors (and extra 9 i dont know why)
#define SYSCFG_CFGR1_MEM_MODE__MAIN_FLASH 0 // x0: Main Flash memory mapped at 0x0000 0000
#define SYSCFG_CFGR1_MEM_MODE__SYSTEM_FLASH 1 // 01: System Flash memory mapped at 0x0000 0000
#define SYSCFG_CFGR1_MEM_MODE__SRAM 3 // 11: Embedded SRAM mapped at 0x0000 0000
volatile uint32_t __attribute__((section(".ram_vector,\"aw\",%nobits @"))) ram_vector[VECTOR_TABLE_SIZE];
extern volatile uint32_t g_pfnVectors[VECTOR_TABLE_SIZE];
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; // early enable to ensure clock is up and running when it comes to usage
for (uint32_t i = 0; i < VECTOR_TABLE_SIZE; i++) {//copy vector table
ram_vector[i] = g_pfnVectors[i];
}
SYSCFG->CFGR1 = (SYSCFG->CFGR1 & ~SYSCFG_CFGR1_MEM_MODE) | (SYSCFG_CFGR1_MEM_MODE__SRAM * SYSCFG_CFGR1_MEM_MODE_0); // remap 0x0000000 to RAM
__enable_irq();
// user code...
}
Тут все "просто":
ram_vector - это массив расположенный в начале RAM чтоб туда положить таблицу векторов;
g_pfnVectors - это вектора которые лежат вначале прошивки и их нужно переложить в ram_vector;
После того как все вектора скопированы надо сказать чтоб МК искал эти вектора в RAM памяти;
И включаем прерывания.
Послесловие
Вот мы реализовали простые настройки для двух прошивок бутлоадера и основной программы. В следующий раз поговорим про саму загрузку прошивки основной программы из бутлоадера.