Все знают, что жизнь начинается с main, но самое интересное начинается со старта микроконтроллера. Рассмотрим, что происходит после старта с некой абстрактной платкой на базе stm32l452. Очнувшись, процессор судорожно ищет в начале своей памяти (0x08000000) указатель на стек (SP). Загрузив его и немного успокоившись, осматривает вектор прерываний дальше. Следующим пунктом идет адрес функции, которую наш контроллер испытывает непреодолимое желание безотлагательно исполнить. Все просто, осталось просто подсунуть нужные данные, чтоб исполнилось именно наше желание, т.е. main. Попробуем собрать с помощью GCC самую маленькую и бесполезную прошивку. Пусть содержимое файла dummy.cpp будет таким:
int main () { return 0; }
Посмотрим, что мы можем слепить из этого arm-none-eabi-g++ dummy.cpp --specs=nosys.specs -fno-exceptions -fno-rtti -O0 --std=c++17 -o dummy arm-none-eabi-objcopy -O binary dummy dummy.bin Ошибок нет! Мы получили бинарник, который заведет наш контроллер? Не совсем. Заглянем в начало сгенерированного файла 00000000 0d c0 a0 e1 f8 df 2d e9 Если бы я был процессором, то с такой прошивкой в регистр SP загрузил бы 0xe1a0c00d, а обработчик сброса бы пошел искать по адресу 0xe92ddff8. Думаю, ничего хорошего из этого не вышло бы. Да, все собралось без ошибок, но процессор просто не знает как попасть в наш прекрасный main. Собственно, не хватает того, что и делает прошивку прошивкой - вектора прерываний. Процессор для обработки прерывания обращается в соответствующую ячейку памяти в начале своей постоянной памяти (хотя вектор потом можно переложить в другое место). Как добавить этот кусок в начало? Использовать скрипт для линковки! Еще любезно предоставленный вендором код для инициализации нашей платы.
На будущее, добавим функцию SystemInit, ее потребует код обработчика сброса.
extern "C" { void SystemInit(void) {} }
Нам пришлось обернуть функцию в extern т.к. собираем мы этот код как плюсовый. arm-none-eabi-g++ dummy.cpp startup_stm32l452xx.s --specs=nosys.specs -fno-exceptions -fno-rtti -T STM32L452RETx_FLASH.ld -O0 --std=c++17 -o dummy В итоге мы получили вменяемый адрес указателя стека и начального обработчика. 00000000 00 80 02 20 7d 03 00 08 SP - 0x20028000; обработчик должен располагаться по адресу 0x0800037d, посмотрим, что там на самом деле. 0800037c <Reset_Handler>: Тут прописан Reset_Handler и наш дорогой обработчик по совместительству. Наипервейшая функция, вызываемая после включения процессора. Природа интересно устроена, как говорилось в одном анекдоте, глазки у кошки именно там, где дырочки в шкурке. Скрипт линкера задает описание секции .isr_vector, где конкретно она будет располагаться во flash-памяти;
/* The startup code goes first into FLASH */ .isr_vector : { . = ALIGN(8); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(8); } >FLASH startup файл описывает содержание секции, ее богатый внутренний мир. .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors
g_pfnVectors: .word _estack .word Reset_Handler
Флаг секции "а" намекает, что эта секция может загружаться в память; progbits говорит о том, что в секции данные и/или инструкции для инициализации. _estack - конец оперативной памяти, как можно определить по названию. Естественно, этот феномен описан в скрипте. _estack = 0x20028000; /* end of RAM */ Таким вот нехитрым способом можно заставить микроконтроллер работать. То есть, все же добраться до main и уже там где-то споткнуться о какой-нибудь нелепый баг. |