Известно, что софт можно дописывать вечно, а всякого рода недочёты на плате полностью исправляются ревизии так к третьей. И если с железом уже ничего не поделаешь, то для обновления микропрограмм придумали неплохой способ обхода ограничений пространства и времени — Bootloader.
Загрузчик — это удобно и полезно, не правда ли? А если загрузчик собственной реализации, то это еще более удобно, полезно и гибкои не стабильно. Ну и конечно же, очень круто!
Так же, это прекрасная возможность углубиться и изучить особенности используемой вычислительной машины — в нашем случае микроконтроллера STM32 с ядром ARM Cortex-M3.
На самом деле, загрузчик — это проще, чем кажется на первый взгляд. В доказательство, под cut'ом соберём свой собственный USB Mass Storage Bootloader!


Работать мы будем с самодельной платой на микроконтроллере (дальше — МК) STM32F103RET. Чтобы не переполнять публикацию лишними картинками, приведу усеченную схему этой железки:

При написании bootloader'а я руководствовался следующими принципами:
Погнали
Так как с собственной FLASH-памятью STM32 мы будем работать постоянно и часто, стоит сразу пояснить некоторые ключевые моменты, связанные с этим фактом.
В используемом МК содержится 512 Kbyte FLASH памяти. Она разбита на страницы по 2048 байт:

Для нас это означает, что записать несколько байт в произвольный адрес просто так не выйдет. При записи во FLASH возможно только обнулять нужные ячейки, а вот установка единиц выполняется с помощью операции стирания, минимально возможный объем для которой — одна страница. Для этого служит регистр FLASH_AR, в который достаточно записать любой адрес в пределах нужной нам страницы — и она будет заполнена байтами 0xFF. А еще нужно не забыть разблокировать FLASH перед операциями стирания/записи.
Виртуально разобьем FLASH на несколько областей, у каждой из которых будет свое, особое назначение:

USER_MEM будет соответствовать MSD_MEM по размеру. Это логично, т.к. два противоположных случая будут давать либо нехватку памяти в USER_MEM, либо избыток.
А теперь все то же самое, только для машины (и удобства программиста):
Договорившись о разбиении памяти на области, самое время прикинуть, как это все будет взаимодействовать. Нарисуем блок-схему:

Согласно такому алгоритму, bootloader имеет два основных режима, работающих независимо друг от друга, но имеющих общий ресурс — кусок памяти MSD_MEM. Однако, даже его использование происходит в разные моменты времени, что положительно влияет на стабильность работы bootloader'а и упрощает процесс программирования и отладки.
Рассмотрим каждый из режимов подробнее:
Запускается сразу же после входа в main() в случае, если выполнено соответствующее условие запуска — зажата кнопка. На моей плате это верхний ползунок двухпозиционного переключателя (который, кстати, заведен на ножки МК BOOT0 и BOOT1(PB2) — это позволяет задействовать аппаратный UART загрузчик МК в случае такой необходимости).
Работа в режиме Mass Storage взята из примеров от STMicroelectronics (STM32_USB-FS-Device_Lib_V4.0.0), которые можно скачать с их сайта. Там нам показывают, как нужно (или наоборот, не нужно — отношение к библиотекам от ST у народа не всегда положительное) работать с микроконтроллером и картой памяти, подключенной по интерфейсу SDIO в режиме USB MSD. В примере реализованы два Bulk In/Out Endpoint'а с длиной пакета 64 байта, а так же набор необходимых для работы SCSI команд. Выкидываем оттуда функции, связанные с SD картами или NAND памятью (mass_mal.c/.h) и заменяем их на работу с внутренней FLASH:
Если все сделано правильно, при подключении компьютер определит наше изделие, как USB Mass Storage Device и предложит его отформатировать, т.к. в области MSD_MEM лежит мусор. Стоит отметить, что в данном режиме работы МК является просто посредником между хостом и FLASH памятью, а операционная система самостоятельно решает, какие данные и по каким адресам будут лежать на нашем накопителе.
Отформатируем диск и посмотрим, как это отразилось на области MSD_MEM:

Объем совпадает, размер сектора Windows определила верный, нулевой сектор — загрузочный, расположение в памяти соответствует задуманному. Файлы пишутся, читаются, не исчезают после отключения питания — полноценная флешка на 200 Kbyte!
Запускается, если обновление прошивки не требуется. То есть, нормальный режим работы устройства. В нём нам предстоит совершить несколько базовых действий, необходимых для успешного запуска пользовательского ПО. Базовых — потому что при необходимости можно дополнять работу bootloader'а всякими фичами, такими как шифрование, проверка целостности, выводом отладочных сообщений и т.д.
Пусть мы уже создали средствами Windows файловую систему на USB накопителе и загрузили необходимое ПО. Теперь неплохо бы увидеть содержимое носителя «глазами» МК, а значит идем в гости к товарищу ChaN'у за FatFS (модуль простой файловой системы FAT, предназначенный для маленьких встраиваемых систем на микроконтроллерах). Скачиваем, закидываем в проект, прописываем функцию чтения с диска нужных данных:
disk_write() не понадобится и оставлена заглушкой, ибо смонтированная файловая система — Read Only. Это так же можно задать в конфигурационном файле ffconf.h, дополнительно отключив все ненужные и неиспользуемые функции.
Дальше все более-менее очевидно: монтируем файловую систему, открываем файл прошивки, начинаем читать. Изначально было реализовано так, что основное место хранения прошивки — MSD_MEM, а микроконтроллер каждый раз при включении перезаписывает свою FLASH память. Нет прошивки — отладочное сообщение об отсутствии и while(TRUE). Есть прошивка — закидываем её в USER_MEM. Однако очевидный минус такого решения — ресурс стирания/записи FLASH памяти имеет лимит и было бы глупо постепенно и осознанно убивать изделие.
Поэтому сравним «APP.BIN» и USER_MEM, тупо, байт за байтом. Возможно, сравнение хеш-сумм двух массивов выглядело бы более изящным решением, но уж точно не самым быстрым. Заглянем снова в main():
Если в процессе сравнения мы не дошли до конца цикла, значит прошивки различны и самое время обновить USER_MEM с помощью CopyAppToUserMemory(). Ну а потом неплохо бы уничтожить следы работы bootloader'а вызовом PeriphDeInit() и затем GoToUserApp(). Но это чуть позже, а пока — процесс копирования:
Копировать будем блоками по 512 байт. 512 — потому что я где-то видел, что при размере буфера больше этого значения f_read() может косячить. Я проверял этот момент — у меня все работало и с буфером большего размера. Но на всякий случай оставил 512 — почему бы и нет? Экономим RAM, да и на скорость не влияет, к тому же выполняется один раз — в момент включ��ния устройства и лишь при условии, что прошивку пора обновлять.
Предварительно стираем во FLASH памяти местечко под файл. Размер стираемой области равен количеству страниц в памяти, которые полностью займет «APP.BIN» + еще одна (которую не полностью). А еще, виртуально бьем файл прошивки на «body» и «tail», где «body» — максимально возможный кусок файла, в который входит целое количество блоков по 512 байт, а «tail» — все остальное.
Кажется, что все бинарные файлы прошивок кратны 4-м байтам. Я не был в этом уверен точно (и до сих пор), так что на всякий случай — если прошивка не кратна sizeof(u32) — дополняем её байтами 0xFF. Повторюсь: кажется, что этого не нужно делать — но операция безобидна для кратных sizeof(u32) бинарников, так что оставим.
Уже близко. Деинициализируем всю использованную периферию функцией PeriphDeInit() (а ее вообще почти ничего — GPIO для кнопки выбора режима и при желании UART для вывода отладочных сообщений; никаких прерываний не используется).
Заключительный жизненный этап загрузчика — начало исполнения пользовательской прошивки:
Всего-то 5 строк, но сколько всего происходит!
В ядре ARM Cortex M3, когда возникает какое-либо исключение, для него вызывается соответствующий обработчик. Что б�� определить начальный адрес обработчика исключений, используется механизм векторной таблицы. Таблица векторов представляет собой массив слов данных внутри системной памяти, каждое из которых является начальным адресом одного типа исключений. Таблица перемещаема и перемещение управляется специальным регистром VTOR в SCB (System Control Block)(В мануале звучит круче, но я сломался: The vector table is relocatable, and the relocation is controlled by a relocation register in the NVIC). После RESET'а значение этого регистра равно 0, то есть таблица векторов лежит по адресу 0x0 (для STM32F103 в стартап файле мы уже самостоятельно двигаем её на 0x08000000). И что очень важно для нас, порядок следования там следующий:

Все это вместе взятое, плюс немного магии с указателем на функцию, и Алиса прыгает вслед за кроликом.
Теперь проверим, работает ли оно вообще. Напишем простую программу мигания светодиодов, с циклами в main() и парочкой прерываний (SysTick и TIM4):
Кстати, надо не забыть исправить в проекте пару вещей, без которых ничего работать не будет:
И вот так этот код исполняется (ну, может не все видели, как моргают светодиоды...):
А вот так выглядит лог загрузки этой прошивки в МК через бутлоадер:
Подведём итоги. Бутлоадер получился! И даже работает. С выводом отладочных сообщений в UART он занимает 31684 байт FLASH памяти, без — 25608 байт. Не так уж и мало, если еще и учесть, сколько памяти нужно отдать под Mass Storage диск. Исходники и рабочий проект (Atollic TrueSTUDIO) можно посмотреть на Bitbucket.
Спасибо за внимание!
Загрузчик — это удобно и полезно, не правда ли? А если загрузчик собственной реализации, то это еще более удобно, полезно и гибко
Так же, это прекрасная возможность углубиться и изучить особенности используемой вычислительной машины — в нашем случае микроконтроллера STM32 с ядром ARM Cortex-M3.
На самом деле, загрузчик — это проще, чем кажется на первый взгляд. В доказательство, под cut'ом соберём свой собственный USB Mass Storage Bootloader!


Работать мы будем с самодельной платой на микроконтроллере (дальше — МК) STM32F103RET. Чтобы не переполнять публикацию лишними картинками, приведу усеченную схему этой железки:

При написании bootloader'а я руководствовался следующими принципами:
- Свой bootloader очень нужен и хватит откладывать это в TODO-лист, пора уже сесть и сделать;
- Bootloader должен иметь удобный для пользователя интерфейс загрузки программы. Никаких драйверов, сторонних программ, плат-переходников и жгутов МГТФ провода до целевого устройства. Что может быть проще автоматически определяемого USB флеш накопителя?
- Для работы в режиме bootloader'а микроконтроллеру необходима минимальная аппаратная обвязка (фактически, только USB, кварц и кнопка);
- Размер boot'а — не главное. Важен, конечно же, но не будем преследовать цель ужать его в пару килобайт. Без мук совести мы поднимем USB стек, поработаем с файловой системой, навтыкаем printf() через строку и вообще не будем особо ни в чем себе отказывать (hello, Standard Peripheral Libraries!);
Погнали
Немного о FLASH
Так как с собственной FLASH-памятью STM32 мы будем работать постоянно и часто, стоит сразу пояснить некоторые ключевые моменты, связанные с этим фактом.
В используемом МК содержится 512 Kbyte FLASH памяти. Она разбита на страницы по 2048 байт:
Для нас это означает, что записать несколько байт в произвольный адрес просто так не выйдет. При записи во FLASH возможно только обнулять нужные ячейки, а вот установка единиц выполняется с помощью операции стирания, минимально возможный объем для которой — одна страница. Для этого служит регистр FLASH_AR, в который достаточно записать любой адрес в пределах нужной нам страницы — и она будет заполнена байтами 0xFF. А еще нужно не забыть разблокировать FLASH перед операциями стирания/записи.
Виртуально разобьем FLASH на несколько областей, у каждой из которых будет свое, особое назначение:
- BOOT_MEM — область памяти, выделенная под bootloader;
- USER_MEM — тут мы будем хранить (и исполнять отсюда же) пользовательскую прошивку. Очевидно, что теперь она имеет ограничение в 200 Kbyte;
- MSD_MEM — а тут будет MASS STORAGE диск, куда можно закинуть прошивку средствами компьютера и вашей любимой ОС;
- OTHER_MEM — ну и оставим еще немного места на всякий случай;
USER_MEM будет соответствовать MSD_MEM по размеру. Это логично, т.к. два противоположных случая будут давать либо нехватку памяти в USER_MEM, либо избыток.
А теперь все то же самое, только для машины (и удобства программиста):
#define FLASH_PAGE_SIZE 2048 //2 Kbyte per page
#define FLASH_START_ADDR 0x08000000 //Origin
#define FLASH_MAX_SIZE 0x00080000 //Max FLASH size - 512 Kbyte
#define FLASH_END_ADDR (FLASH_START_ADDR + FLASH_MAX_SIZE) //FLASH end address
#define FLASH_BOOT_START_ADDR (FLASH_START_ADDR) //Bootloader start address
#define FLASH_BOOT_SIZE 0x00010000 //64 Kbyte for bootloader
#define FLASH_USER_START_ADDR (FLASH_BOOT_START_ADDR + FLASH_BOOT_SIZE) //User application start address
#define FLASH_USER_SIZE 0x00032000 //200 Kbyte for user application
#define FLASH_MSD_START_ADDR (FLASH_USER_START_ADDR + FLASH_USER_SIZE) //USB MSD start address
#define FLASH_MSD_SIZE 0x00032000 //200 Kbyte for USB MASS Storage
#define FLASH_OTHER_START_ADDR (FLASH_MSD_START_ADDR + FLASH_MSD_SIZE) //Other free memory start address
#define FLASH_OTHER_SIZE (FLASH_END_ADDR - FLASH_OTHER_START_ADDR) //Free memory sizeДоговорившись о разбиении памяти на области, самое время прикинуть, как это все будет взаимодействовать. Нарисуем блок-схему:

Согласно такому алгоритму, bootloader имеет два основных режима, работающих независимо друг от друга, но имеющих общий ресурс — кусок памяти MSD_MEM. Однако, даже его использование происходит в разные моменты времени, что положительно влияет на стабильность работы bootloader'а и упрощает процесс программирования и отладки.
- Первый режим отвечает за получение и хранение пользовательского ПО в области MSD_MEM, которая доступна, как внешний накопитель.
- Второй режим проверяет MSD_MEM на наличие файла с именем «APP.BIN», проверяет его целостность, подлинность, а так же перемещает в USER_MEM, если там пусто или если прошивка «APP.BIN» более свежая.
Рассмотрим каждый из режимов подробнее:
USB Mass Storage Device
Запускается сразу же после входа в main() в случае, если выполнено соответствующее условие запуска — зажата кнопка. На моей плате это верхний ползунок двухпозиционного переключателя (который, кстати, заведен на ножки МК BOOT0 и BOOT1(PB2) — это позволяет задействовать аппаратный UART загрузчик МК в случае такой необходимости).
int main(void)
int main(void)
{
Button_Config();
if(GPIO_ReadInputDataBit(BUTTON_PORT, BUTTON_PIN) == SET) //Bootloader or Mass Storage?
{
LED_RGB_Config();
USB_Config();
Interrupts_Config();
USB_Init();
while(TRUE);
}
//Bootloader mode
}
Работа в режиме Mass Storage взята из примеров от STMicroelectronics (STM32_USB-FS-Device_Lib_V4.0.0), которые можно скачать с их сайта. Там нам показывают, как нужно (или наоборот, не нужно — отношение к библиотекам от ST у народа не всегда положительное) работать с микроконтроллером и картой памяти, подключенной по интерфейсу SDIO в режиме USB MSD. В примере реализованы два Bulk In/Out Endpoint'а с длиной пакета 64 байта, а так же набор необходимых для работы SCSI команд. Выкидываем оттуда функции, связанные с SD картами или NAND памятью (mass_mal.c/.h) и заменяем их на работу с внутренней FLASH:
u16 MAL_Init(u8 lun)
u16 MAL_Init(u8 lun)
{
switch (lun)
{
case 0:
FLASH_Unlock();
break;
default:
return MAL_FAIL;
}
return MAL_OK;
}u16 MAL_Read(u8 lun, u32 memOffset, u32 *readBuff)
u16 MAL_Read(u8 lun, u32 memOffset, u32 *readBuff)
{
u32 i;
switch (lun)
{
case 0:
LED_RGB_EnableOne(LED_GREEN);
for(i = 0; i < MassBlockSize[0]; i += SIZE_OF_U32)
{
readBuff[i / SIZE_OF_U32] = *((volatile u32*)(FLASH_MSD_START_ADDR + memOffset + i));
}
LED_RGB_DisableOne(LED_GREEN);
break;
default:
return MAL_FAIL;
}
return MAL_OK;
}u16 MAL_Write(u8 lun, u32 memOffset, u32 *writeBuff)
u16 MAL_Write(u8 lun, u32 memOffset, u32 *writeBuff)
{
u32 i;
switch (lun)
{
case 0:
LED_RGB_EnableOne(LED_RED);
while(FLASH_GetStatus() != FLASH_COMPLETE);
FLASH_ErasePage(FLASH_MSD_START_ADDR + memOffset);
for(i = 0; i < MassBlockSize[0]; i += SIZE_OF_U32)
{
while(FLASH_GetStatus() != FLASH_COMPLETE);
FLASH_ProgramWord(FLASH_MSD_START_ADDR + memOffset + i, writeBuff[i / SIZE_OF_U32]);
}
LED_RGB_DisableOne(LED_RED);
break;
default:
return MAL_FAIL;
}
return MAL_OK;
}
Если все сделано правильно, при подключении компьютер определит наше изделие, как USB Mass Storage Device и предложит его отформатировать, т.к. в области MSD_MEM лежит мусор. Стоит отметить, что в данном режиме работы МК является просто посредником между хостом и FLASH памятью, а операционная система самостоятельно решает, какие данные и по каким адресам будут лежать на нашем накопителе.
Отформатируем диск и посмотрим, как это отразилось на области MSD_MEM:
Объем совпадает, размер сектора Windows определила верный, нулевой сектор — загрузочный, расположение в памяти соответствует задуманному. Файлы пишутся, читаются, не исчезают после отключения питания — полноценная флешка на 200 Kbyte!
Bootloader
Запускается, если обновление прошивки не требуется. То есть, нормальный режим работы устройства. В нём нам предстоит совершить несколько базовых действий, необходимых для успешного запуска пользовательского ПО. Базовых — потому что при необходимости можно дополнять работу bootloader'а всякими фичами, такими как шифрование, проверка целостности, выводом отладочных сообщений и т.д.
Пусть мы уже создали средствами Windows файловую систему на USB накопителе и загрузили необходимое ПО. Теперь неплохо бы увидеть содержимое носителя «глазами» МК, а значит идем в гости к товарищу ChaN'у за FatFS (модуль простой файловой системы FAT, предназначенный для маленьких встраиваемых систем на микроконтроллерах). Скачиваем, закидываем в проект, прописываем функцию чтения с диска нужных данных:
DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count)
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
u32 i;
for(i = 0; i < count * SECTOR_SIZE; i++)
{
buff[i] = *((volatile u8*)(FLASH_MSD_START_ADDR + sector * SECTOR_SIZE + i));
}
return RES_OK;
}
disk_write() не понадобится и оставлена заглушкой, ибо смонтированная файловая система — Read Only. Это так же можно задать в конфигурационном файле ffconf.h, дополнительно отключив все ненужные и неиспользуемые функции.
Дальше все более-менее очевидно: монтируем файловую систему, открываем файл прошивки, начинаем читать. Изначально было реализовано так, что основное место хранения прошивки — MSD_MEM, а микроконтроллер каждый раз при включении перезаписывает свою FLASH память. Нет прошивки — отладочное сообщение об отсутствии и while(TRUE). Есть прошивка — закидываем её в USER_MEM. Однако очевидный минус такого решения — ресурс стирания/записи FLASH памяти имеет лимит и было бы глупо постепенно и осознанно убивать изделие.
Поэтому сравним «APP.BIN» и USER_MEM, тупо, байт за байтом. Возможно, сравнение хеш-сумм двух массивов выглядело бы более изящным решением, но уж точно не самым быстрым. Заглянем снова в main():
int main(void)
int main(void)
{
Button_Config();
if(GPIO_ReadInputDataBit(BUTTON_PORT, BUTTON_PIN) == SET) //Bootloader or Mass Storage?
{
//USB MSD mode
}
FATFS_Status = f_mount(&FATFS_Obj, "0", 1);
if(FATFS_Status == FR_OK)
{
FILE_Status = f_open(&appFile, "/APP.BIN", FA_READ);
if(FILE_Status == FR_OK)
{
appSize = f_size(&appFile);
for(i = 0; i < appSize; i++) //Byte-to-byte compare files in MSD_MEM and USER_MEM
{
f_read(&appFile, &appBuffer, 1, &readBytes);
if(*((volatile u8*)(FLASH_USER_START_ADDR + i)) != appBuffer[0])
{
//if byte of USER_MEM != byte of MSD_MEM
break;
}
}
if(i != appSize)//=> was done "break" instruction in for(;;) cycle => new firmware in MSD_FLASH
{
CopyAppToUserMemory();
}
FILE_Status = f_close(&appFile);
FATFS_Status = f_mount(NULL, "0", 1);
PeriphDeInit();
GoToUserApp();
}
else //if FILE_Status != FR_OK
{
if(FILE_Status == FR_NO_FILE)
{
//No file error
}
else //if FILE_Status != FR_NO_FILE
{
//Other error
}
FATFS_Status = f_mount(NULL, "0", 1);
while(TRUE);
}
}
else //FATFS_Status != FR_OK
{
//FatFS mount error
while(TRUE);
}
}
Если в процессе сравнения мы не дошли до конца цикла, значит прошивки различны и самое время обновить USER_MEM с помощью CopyAppToUserMemory(). Ну а потом неплохо бы уничтожить следы работы bootloader'а вызовом PeriphDeInit() и затем GoToUserApp(). Но это чуть позже, а пока — процесс копирования:
void CopyAppToUserMemory(void)
void CopyAppToUserMemory(void)
{
f_lseek(&appFile, 0); //Go to the fist position of file
appTailSize = appSize % APP_BLOCK_TRANSFER_SIZE;
appBodySize = appSize - appTailSize;
appAddrPointer = 0;
for(i = 0; i < ((appSize / FLASH_PAGE_SIZE) + 1); i++) //Erase n + 1 pages for new application
{
while(FLASH_GetStatus() != FLASH_COMPLETE);
FLASH_ErasePage(FLASH_USER_START_ADDR + i * FLASH_PAGE_SIZE);
}
for(i = 0; i < appBodySize; i += APP_BLOCK_TRANSFER_SIZE)
{
/*
* For example, size of File1 = 1030 bytes
* File1 = 2 * 512 bytes + 6 bytes
* "body" = 2 * 512, "tail" = 6
* Let's write "body" and "tail" to MCU FLASH byte after byte with 512-byte blocks
*/
f_read(&appFile, appBuffer, APP_BLOCK_TRANSFER_SIZE, &readBytes); //Read 512 byte from file
for(j = 0; j < APP_BLOCK_TRANSFER_SIZE; j += SIZE_OF_U32) //write 512 byte to FLASH
{
while(FLASH_GetStatus() != FLASH_COMPLETE);
FLASH_ProgramWord(FLASH_USER_START_ADDR + i + j, *((volatile u32*)(appBuffer + j)));
}
appAddrPointer += APP_BLOCK_TRANSFER_SIZE; //pointer to current position in FLASH for write
}
f_read(&appFile, appBuffer, appTailSize, &readBytes); //Read "tail" that < 512 bytes from file
while((appTailSize % SIZE_OF_U32) != 0) //if appTailSize MOD 4 != 0 (seems not possible, but still...)
{
appTailSize++; //increase the tail to a multiple of 4
appBuffer[appTailSize - 1] = 0xFF; //and put 0xFF in this tail place
}
for(i = 0; i < appTailSize; i += SIZE_OF_U32) //write "tail" to FLASH
{
while(FLASH_GetStatus() != FLASH_COMPLETE);
FLASH_ProgramWord(FLASH_USER_START_ADDR + appAddrPointer + i, *((volatile u32*)(appBuffer + i)));
}
}
Копировать будем блоками по 512 байт. 512 — потому что я где-то видел, что при размере буфера больше этого значения f_read() может косячить. Я проверял этот момент — у меня все работало и с буфером большего размера. Но на всякий случай оставил 512 — почему бы и нет? Экономим RAM, да и на скорость не влияет, к тому же выполняется один раз — в момент включ��ния устройства и лишь при условии, что прошивку пора обновлять.
Предварительно стираем во FLASH памяти местечко под файл. Размер стираемой области равен количеству страниц в памяти, которые полностью займет «APP.BIN» + еще одна (которую не полностью). А еще, виртуально бьем файл прошивки на «body» и «tail», где «body» — максимально возможный кусок файла, в который входит целое количество блоков по 512 байт, а «tail» — все остальное.
Кажется, что все бинарные файлы прошивок кратны 4-м байтам. Я не был в этом уверен точно (и до сих пор), так что на всякий случай — если прошивка не кратна sizeof(u32) — дополняем её байтами 0xFF. Повторюсь: кажется, что этого не нужно делать — но операция безобидна для кратных sizeof(u32) бинарников, так что оставим.
Hello, User Application!
Уже близко. Деинициализируем всю использованную периферию функцией PeriphDeInit() (а ее вообще почти ничего — GPIO для кнопки выбора режима и при желании UART для вывода отладочных сообщений; никаких прерываний не используется).
Заключительный жизненный этап загрузчика — начало исполнения пользовательской прошивки:
void GoToUserApp(void)
void GoToUserApp(void)
{
u32 appJumpAddress;
void (*GoToApp)(void);
appJumpAddress = *((volatile u32*)(FLASH_USER_START_ADDR + 4));
GoToApp = (void (*)(void))appJumpAddress;
SCB->VTOR = FLASH_USER_START_ADDR;
__set_MSP(*((volatile u32*) FLASH_USER_START_ADDR)); //stack pointer (to RAM) for USER app in this address
GoToApp();
}
Всего-то 5 строк, но сколько всего происходит!
В ядре ARM Cortex M3, когда возникает какое-либо исключение, для него вызывается соответствующий обработчик. Что б�� определить начальный адрес обработчика исключений, используется механизм векторной таблицы. Таблица векторов представляет собой массив слов данных внутри системной памяти, каждое из которых является начальным адресом одного типа исключений. Таблица перемещаема и перемещение управляется специальным регистром VTOR в SCB (System Control Block)(В мануале звучит круче, но я сломался: The vector table is relocatable, and the relocation is controlled by a relocation register in the NVIC). После RESET'а значение этого регистра равно 0, то есть таблица векторов лежит по адресу 0x0 (для STM32F103 в стартап файле мы уже самостоятельно двигаем её на 0x08000000). И что очень важно для нас, порядок следования там следующий:
- Значение, лежащее по адресу 0x04 — это то место в программе, куда мы попадаем после Reset-исключения
- Значение, лежащее по адресу 0x00 — это начальное значение Main Stack Pointer для пользовательского приложения
Все это вместе взятое, плюс немного магии с указателем на функцию, и Алиса прыгает вслед за кроликом.
Теперь проверим, работает ли оно вообще. Напишем простую программу мигания светодиодов, с циклами в main() и парочкой прерываний (SysTick и TIM4):
Test programm for MSD bootloader
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_tim.h"
#include "misc.h"
#define SYSCLK_FREQ 72000000
#define TICK_1_KHz ((SYSCLK_FREQ / 1000) - 1)
#define TICK_1_MHz ((SYSCLK_FREQ / 1000000) - 1)
volatile u32 i, j;
int main(void)
{
GPIO_InitTypeDef GPIO_Options;
NVIC_InitTypeDef NVIC_Options;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
GPIO_Options.GPIO_Pin = GPIO_Pin_7;
GPIO_Options.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Options.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_Options);
GPIO_Options.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_Options.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Options.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_Options);
GPIOB->BSRR = GPIO_Pin_0 | GPIO_Pin_1; //LEDs off
GPIOA->BSRR = GPIO_Pin_7
TIM4->PSC = 720 - 1; //clock prescaller
TIM4->ARR = 60000 - 1; //auto-reload value
TIM4->CR1 |= TIM_CounterMode_Up;//upcounter
TIM4->DIER |= TIM_IT_Update; //update interrupt enable
TIM4->CR1 |= TIM_CR1_CEN; //timer start
NVIC_Options.NVIC_IRQChannel = TIM4_IRQn;
NVIC_Options.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_Options.NVIC_IRQChannelSubPriority = 0;
NVIC_Options.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_Options);
SysTick_Config(TICK_1_KHz);
while(1)
{
__disable_irq();
GPIOB->BSRR = GPIO_Pin_0 | GPIO_Pin_1; //Off
for(i = 0; i < 10; i++)
{
for(j = 0; j < 500000; j++); //Pause
GPIOA->ODR ^= GPIO_Pin_7; //Reverse
}
GPIOA->BSRR = GPIO_Pin_7; //Off
__enable_irq();
for(i = 0; i < 5000000; i++); //Pause
}
}
void SysTick_Handler(void)
{
volatile static u32 LED_Counter = 0;
if(LED_Counter >= 40)
{
GPIOB->ODR ^= GPIO_Pin_1; //Reverse
LED_Counter = 0;
}
LED_Counter++;
}
void TIM4_IRQHandler()
{
TIM4->SR = ~TIM_SR_UIF;
GPIOB->ODR ^= GPIO_Pin_0; //Reverse
}
Кстати, надо не забыть исправить в проекте пару вещей, без которых ничего работать не будет:
- Убрать из SystemInit() операцию перемещения таблицы векторов на какое либо значение (//SCB->VTOR = FLASH_BASE). Bootloader перемещает ее самостоятельно перед переходом в пользовательскую программу!
- В Linker script поменять начало нашей программы с адреса 0x08000000 на адрес начала USER_MEM (FLASH (rx): ORIGIN = 0x08010000, LENGTH = 200K);
И вот так этот код исполняется (ну, может не все видели, как моргают светодиоды...):
А вот так выглядит лог загрузки этой прошивки в МК через бутлоадер:
UART log message
---------------START LOG---------------
BOOT_MEM start addr: 0x08000000
BOOT_MEM size: 64K
USER_MEM start addr: 0x08010000
USER_MEM size: 200K
MSD_MEM start addr: 0x08042000
MSD_MEM size: 200K
OTHER_MEM start addr: 0x08074000
OTHER_MEM size: 48K
Total memory size: 512K
BOOTLOADER Mode…
FAT FS mount status = 0
Application file open status = 0
Difference between MSD_MEM and USER_MEM: 4 byte from 2212 byte
Start copy MSD_MEM to USER_MEM:
File size = 2212 byte
Body size = 2048 byte
Tail size = 164 byte
Sector 0 (0x08010000 — 0x08010800) erased
Sector 1 (0x08010800 — 0x08011000) erased
0 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010000 — 0x08010200
1 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010200 — 0x08010400
2 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010400 — 0x08010600
3 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010600 — 0x08010800
Tail read: read status = 0, 164 byte read, size of tail = 164
New size of tail = 164
164 byte programmed: 0x08010800 — 0x080108A4
File close status = 0
FAT FS unmount status = 0
DeInit peripheral and jump to 0x08010561…
BOOT_MEM start addr: 0x08000000
BOOT_MEM size: 64K
USER_MEM start addr: 0x08010000
USER_MEM size: 200K
MSD_MEM start addr: 0x08042000
MSD_MEM size: 200K
OTHER_MEM start addr: 0x08074000
OTHER_MEM size: 48K
Total memory size: 512K
BOOTLOADER Mode…
FAT FS mount status = 0
Application file open status = 0
Difference between MSD_MEM and USER_MEM: 4 byte from 2212 byte
Start copy MSD_MEM to USER_MEM:
File size = 2212 byte
Body size = 2048 byte
Tail size = 164 byte
Sector 0 (0x08010000 — 0x08010800) erased
Sector 1 (0x08010800 — 0x08011000) erased
0 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010000 — 0x08010200
1 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010200 — 0x08010400
2 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010400 — 0x08010600
3 cycle, read status = 0, 512 byte read
512 byte programmed: 0x08010600 — 0x08010800
Tail read: read status = 0, 164 byte read, size of tail = 164
New size of tail = 164
164 byte programmed: 0x08010800 — 0x080108A4
File close status = 0
FAT FS unmount status = 0
DeInit peripheral and jump to 0x08010561…
Подведём итоги. Бутлоадер получился! И даже работает. С выводом отладочных сообщений в UART он занимает 31684 байт FLASH памяти, без — 25608 байт. Не так уж и мало, если еще и учесть, сколько памяти нужно отдать под Mass Storage диск. Исходники и рабочий проект (Atollic TrueSTUDIO) можно посмотреть на Bitbucket.
Спасибо за внимание!