Билдим под stm32duino с помощью CMake (и отгребаем от линкера)

в 15:04, , рубрики: arduino, cmake, QtCreator, stm32, stm32duino, билд система, программирование микроконтроллеров, система сборки, системы сборки

image

Всем привет! Как часто вы задумываетесь как код написанный в красивой IDE превращается в набор байт, удобоваримый для процессора или микроконтроллера? Вот я лично не часто. Работает себе и хорошо. Нажал кнопку Add File — IDE сама добавила исходник в проект, сама вызывает компилятор и линковщик. Пиши себе код и не думай о мелочах.

Но иногда все эти неявные штуки вылезают на поверхность и начинают сниться в кошмарах. Оказывается даже в простейшем Hello World под капотом крутится много всяких разных и очень интересных шестеренок, которые обеспечивают плавное и комфортное движение. Мир микроконтроллеров не исключение.

Сегодня речь пойдет о замене билд системы для моего проекта. По различным причинам мне было тесно с Ардуино и пришлось искать что нибудь где можно развернуться. Под катом описан мой опыт перехода от билд системы Ардуино к сборке прошивки под микроконтроллер STM32 и фреймворк stm32duino с помощью CMake.

Билд система Ардуино

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

Итак, я бы выделил в Ардуино следующие вещи:

  • Arduino IDE — редактор, компилятор и набор библиотек в одном флаконе. Позволяет быстренько стартовать с небольшим проектом или же собрать проект скачанный из интернета. Нет необходимости в инсталляции дополнительных средств — все есть из коробки.
  • Ардуино платы — опенсорсный дизайн плат на процессорах ATMega (или других от Atmel). Платы более-менее стандартизированы и под них существует множество шилдов и периферии.
  • Ардуино фреймворк — набор C++ классов, интерфейсов и библиотек, которые скрывают низкоуровневую логику и регистры микроконтроллера. Пользователю предоставляют удобный достаточно высокоуровневый интерфейс для работы.

Каждая из этих частей является независимой от остальных. Так в ArduinoIDE можно писать и под другие микроконтроллеры (например под ESP8266), или наоборот отказаться от ArduinoIDE и познавать все прелести Ардуино где нибудь в Atmel Studio, Eclipse или даже vim. Некоторые платы достаточно не похожи на платы Arduino (например полетные контроллеры квадрокоптеров), но при этом в них легко можно вливать Ардуино скетчи. И наоборот, под платы Ардуино можно программировать на голом C или ассемблере. Что касается Ардуино фреймворка, то можно отметить, что он портирован на многие микроконтроллеры (STM32, TI) что опускает порог вхождения в этот мир до приемлемого уровня.

image
Посредине плата на stm32f103c6 (по факту stm32f103cb), справа arduino nano. Фото отсюда (тут чуть более глубоко затронут вопрос stm32duino)

В своем проекте я практически сразу отказался от Arduino IDE в пользу более привычной мне Atmel Studio. От плат Ардуино я также отказался в пользу STM32 как более мощной платформы с чуточку бОльшим количеством периферии и памяти. От Ардуино фреймворка отказываться очень не хотелось бы — он достаточно красив, удобен и предоставляет все что нужно. Я использую stm32duino — порт ардуино под микроконтроллеры STM32. А вот SPL (православная абстракция над регистрами и начинкой контроллера от самой ST) как то не зашла — код слишком громоздкий и некрасивый.

Но есть еще одна очень неявная часть ардуино — билд система. Эта неявность является большим плюсом для маленьких проектов, когда не хочется думать и разбираться как же это все работает. Просто добавляешь файлы и библиотеки и оно все собирает. Для новичков самое оно. Но когда проект хоть немного разрастается, добавляется несколько библиотек и требуется тонкая подстройка компилятора, билд система ардуино начинает мешать. Вот список вещей которые мешали в моем проекте

  • Невозможность разложить исходники по директориям

    Если исходников всего пару штук то и ориентироваться в них просто. Если количество файлов в проекте переваливает за несколько десятков то скидывать их все в кучу не очень хорошая идея. Удобно разные компоненты или независимые части проекта разложить в разные директории и, при необходимости, прописать include path’ы.

    К сожалению в Ардуино исходники нельзя разложить по разным директориям, или хоть как нибудь сгруппировать файлы в IDE. Билд система ардуино подразумевает, что все файлы находятся в одной директории. Более того, исходник в проект как раз и подключается именно фактом нахождения файла в той же директории что и основной файл скетча. Кстати, саму директорию, в которой лежит скетч, также нельзя называть как попало — она должна называться в точности также как и скетч.

  • Библиотеки нужно инсталлировать

    И хотя это разовая операция, нужно писать отдельную инструкцию по сборке проекта: скачайте библиотеки вот отсюда, положите вот туда, настройте вот так.

    Иногда нужно сделать какое нибудь изменение в библиотеке (починить ошибку или подрихтовать под себя) тогда нужно, как минимум, сделать форк на гитхабе. А в инструкции по сборке проекта большими буквами прописать, что оригинал библиотеки работать не будет.

    А по другому никак. Библиотеки нельзя положить в систему контроля версий рядом с исходниками своего проекта — их просто не подключишь в проект.

  • Настройка параметров библиотек выполняется в самой библиотеке

    В библиотеках для больших компов все конфигурируется дефайнами. Библиотека поставляется целиком, а клиент использует ее на свое усмотрение.

    С ардуино библиотеками все не так. Обычно библиотека содержит какой нибудь хедер файл, который нужно подпилить под себя, включить или выключить определенные настройки, подправить ссылки на периферию. А что, если у меня несколько проектов, которые используют эту библиотеку, но с разными параметрами? Библиотека ведь проинсталлирована в общую директорию и изменения для одного проекта повлияют на другой.

    И, опять же, открытым остается вопрос контроля версий настроечного файла

  • Проект всегда пересобирается целиком

    Даже если поменялась какая то мелочь в одном файле нужно все равно ждать пока перекомпилятся все файлы. В моем случае это около минуты.

    Все это усугубляется отсутствием какого либо видимого прогресса. То ли работает, то ли уже нет — не ясно. В случае успешного билда билдсистема пишет размер прошивки. Но если случилась ошибка (например, превышен размер флеш памяти) то билд система об этом не сообщит вообще никак.

  • Наконец, нельзя гибко менять ключи компиляции

    Даже настройки оптимизации нельзя поменять. А уж про тонкую настройку можно забыть. Да что там говорить — нельзя прописать include path или дефайн!

Изучаем пациента

image

Итак, проблемы билд системы ардуино понятны. Ясное дело тут нужно придумать что нибудь получше. Но прежде чем переходить на другую систему сборки нужно вначале разобраться а как же сама ардуино билдит свои проекты. А работает она очень интересно.

Первым делом система сборки пытается сбилдить файлик скетча (.ino) с ключиком -o nul (ничего не пишем, собираем только ошибки компиляции). Если компилятор не находит какой нибудь хедер, билдсистема ищет нет ли там этого хедера в установленных библиотеках. Если есть — добавляет ключик -I (дополнительный include path) и пытается сбилдить еще разок. Если система сборки не находит еще одного хедера — операция повторяется. Строка с путями для ключа -I накапливается и применяется к следующему файлу в проекте.

Кроме хитрости с ключиком -I система сборки будет также собирать и саму библиотеку. Для этого она пробует скомпилировать и все *.c и *.cpp файлы в директории библиотеки. Зависимости по хедерам для библиотек решаются уже описанным способом. Библиотека компилируется целиком, даже если не все файлы из нее используется. И хорошо, если линкер потом выкинет лишнее. А если нет?

Так, если в библиотеке объявлен какой нибудь глобальный объект (например класс Serial или SPI), то код для этого объекта всегда попадает в прошивку, даже если этот код на самом деле не используется. Т.е. нечаянно добавленная директива #include может добавить к прошивке пару лишних кило.

Но это еще не все. Когда все зависимости собраны процесс компиляции запускается еще разок, уже с правильными ключами компиляции, кодогенерации, оптимизации и всего такого прочего. В общем, каждый файл компилируется минимум 2 раза, а особо неудачные файлы (типа .ino) могут компилироваться столько раз сколько библиотек подключено в проект.

Всякие системные штуки (вектора прерываний, базовая инициализация платы) также компилируются вместе с проектом. Благо некоторая часть файлов, которые компилируются потом собираются в .a статические библиотеки. Но это, на самом деле, шаманство от создателей stm32duino — для других библиотек все собирается пофайлово.

Я думаю для мелкого ардуиновского проекта с 1-2 библиотеками такой подход к сборке не создает слишком большой оверхед. Но мой проект из 25 cpp файлов и 5 библиотек стал компилироваться почти минуту. Знаете сколько раз запускаются компилятор, линкер и другие программы из toolchain? 240 раз!!!

Тем не менее в самой сборке ничего военного нет. Я опасался каких-то скрытых механизмов сборки, которые без ардуино невозможно будет повторить. На деле же все собирается последовательным вызовом компилятора и линковщика с определенным набором ключей. А значит все тоже самое можно повторить и в другой системе сборки.

Стоит отметить, что я исследовал работу билд системы при сборке под STM32 и фреймворк stm32duino. Но при сборке под классический ардуино на контроллере ATMega все практически тоже самое (только чуточку попроще)

Пробуем CMake

image

Коллега по цеху порекомендовал посмотреть на CMake — там уже есть готовые тулчены для сборки Ардуино проектов. Так, я установил CMake, взял минимальный проект из примеров, подпилил чуток CMakeList.txt. Но когда я запустил CMake, то получил креш в ld.exe на этапе конфигурации (когда проверяются компиляторы, линкеры и подобные штуки). Попытка понять что же именно происходит не увенчалась успехом — тот же самый комманд лайн запущенный отдельно выполнялся без проблем. Как это объехать я так и не понял.

В поисках решения я внимательно изучил файлы тулчейнов и понял, что я вообще не туда копаю. Тогда как настоящая ардуино имеет плагинную систему, в которую можно добавлять любые платы с компиляторами (не обязательно AVR), один из изучаемых мною тулчейнов заточен исключительно под AVR. Причем доступны только 2-3 вида стандартных плат.

Второй тулчейн выглядел получше. Он парсил ардуиновские файлы типа boards.txt и шарился по директориям variants, собирая все доступные варианты сборки, плат и процессоров. Тем не менее что-то меня в этом смутило. Показалось, что на выходе я опять получу монолит в стиле ардуино. Не очень понятно было заработает ли ARM компилятор вместо AVR. К тому же все равно не ясно как контролировать библиотеки.

В общем ничего плохого сказать не могу про этот тулчейн. Просто, так и не победив креш при генерации мейк файлов, я решил поискать решение в другом месте.

CooCox

Я люблю писать код в хорошем IDE. В интернете часто упоминают самые популярные IDE для STM32 — Keil, IAR и CooCox. Первые 2 платные и с нелестными отзывами на тему юзабилити. А вот CooCox весьма хвалят. Подкупало также то, что он построен на базе Eclipse, а мне давно с ним хотелось познакомиться. Мне показалось, что это позволяет делать тонкую настройку процесса сборки. Забегая вперед, скажу, что увы, я ошибался.

Установив CooCox я был весьма удивлен, что он идет без компилятора. В целом это объяснимо — IDE отдельно а компилятор отдельно. Просто это как непривычно после многих лет в Visual Studio. Ну да ладно, это просто решается установкой компилятора отдельным инсталятором, хотя я, поначалу, взял компилятор из Atmel Studio.

image

И тут начались проблемы. Я битый час пытался завести моргалку на светодиоде. В интернете нашел с десяток вариантов разной степени компилябельности. Казалось бы, где там можно ошибиться? Ан-нет. Компилируем, заливаем — не работает. Я пересоздавал проект несколько раз — не помогало. В итоге пошел по самой нубской инструкции, четко и дословно выполняя все указания. Прошивку залил через UART и системный бутлоадер и лампочка радостно заморгала.

Хотелось бы обвинить stm32duino/libmaple USB бутлоадер, который и был причиной проблемы, но разумнее признать мое непонимание процесса инициализации на контроллерах STM32. Разобравшись в вопросе я описал свои “открытия” в отдельной статье. В двух словах проблема была в некорректном начальном адресе прошивки. При использовании бутлоадера прошивку нужно собирать с другим начальным адресом. Такого параметра, вообще-то, нет в классическом ардуино, но он весьма важен для микроконтроллеров STM32.

Мысли про STM32 бутлоадеры

Прошиваться через UART, конечно, можно, но это тот еще геморрой. Нужно постоянно переключать Boot перемычки и много кликать мышкой в ST Flash Demonstrator. Китайский ST-Link у меня есть, можно было бы его подключить, шить через него и дебажиться. Наверное, я еще дойду до этого, когда мне действительно понадобится внутрисхемная отладка.

Но на данный момент я рассматриваю прошивку через USB как самую удобную по нескольким причинам. Все равно у меня подключен USB шнур для питания и USB serial. Я так же собираюсь в скором времени активно ковырять USB Mass Storage. В конце концов в готовом устройстве также будет выведен USB. Так зачем использовать дополнительные компоненты, вместо того, чтобы прошиваться через USB?

Возвращаясь к теме конфигурационного менеджмента, и конкретнее где и как хранить библиотеки. Я тяготею к варианту “все свое ношу с собой”. Все изменения библиотек (включая их конфигурацию) я бы хотел версионировать в своем репозитории. Конечно же есть классические юниксовые патчи, но это выглядит как прошлый век. Но что использовать взамен? Хотелось бы решение в стиле “вытянул исходники одной командой, сбилдил, залил в МК”.

Я долго пытался разбираться с подмодулями и поддеревьями в гите. Мне, как человеку никогда с гитом плотно не работавшим, ничего не понятно. Я даже прочитал несколько первых глав Git Book, но запутался еще больше. Попросил помощи у более опытных в этих делах коллег, но те еще больше загибали пальцы и становилось еще менее понятно. Решил делать через поддеревья. Пока не представляю чем это мне грозит.

Поддеревья мне понравились тем, что я получал нужные файлы прямо в моем репозитории. Причем с возможностью их редактирования. Submodule, все же, подразумевает хранение изменений где-то в другом репозитории. Если для багофиксов это разумно (можно легко сделать форк а потом запушать фиксы в основной репозиторий), то для хранения настроек библиотек это, по меньшей мере, странно. Версионировать изменения для одного проекта в разных репозиториях не хотелось бы.

Так я затянул stm32duino для контроллеров серии STM32F1xx к себе в репо. Затянул целиком, с примерами и библиотеками. Если честно, то структура мне не нравится, и уж точно все оттуда мне не нужно. Но я не знаю как сделать по другому и как это потом мержить. Компенсировал тем, что разложил все файлики как мне нравится в CooCox проекте.

В итоге я затянул все нужные файлы в проект, выставил те же самые дефайны, что и Ардуино, после чего начался процесс линковки. Шел по ошибкам. Если не находился какой-то символ я искал в каком исходнике он объявлен и добавлял только этот исходник. Это был весьма интересный опыт, т.к. я по ходу разбирался что и где лежит (я имею в виду stm32duino/libmaple) и как оно там устроено.

image

Как я уже говорил, сборка под ARM (коим является STM32) сложнее чем сборка под AVR. Все дело в том, что компилятор там более общий и позволяет очень тонко себя конфигурировать. С одной стороны это хорошо и можно тюнить процесс сборки, с другой это очень усложняет конфигурацию. Так, тут дополнительно используется скрипты линковщика — специальные файлы, которые описывают какие секции должны быть в результирующем бинарнике, где эти секции располагаются и сколько места занимают.

Оказалось низкоуровневый код может ссылаться на символы, которые объявляются в скриптах линковщика и наоборот. Если линковщик вызывать со стандартными настройками, то эти символы не находятся. Чтобы решить эту проблему на странице настроек линковщика я снял галку ‘Use Memory Layout from Memory Window” — открылось поле scatter file. Туда я прописал скрипт линковщика, который использовался в билд системе ардуино (STM32F1/variants/generic_gd32f103c/ld/bootloader_20.ld). После этого все сразу слинковалось.

image

Но тут меня ждал неприятный сюрприз — собранная прошивка заняла сходу 115кб (на ардуино было 56к). При чем все символы там были перемешаны, а прошивка содержала очень много C++ рантайма — rtti, манглеры, abi, эксепшены и кучу еще всего ненужного. Пришлось сравнивать командлайны линкера, которые делает ардуино и CooCox и по каждому ключу курить документацию.

Оказалось в CooCox нельзя выбирать между C и C++ компиляторами. Из-за этого невозможна отдельная настройка каждого из компиляторов. Так С++ код компилируется с помощью gcc (а не g++) и по умолчанию генерирует тонну кода, отсылки на rtti, ABI и еще чего-то. И, похоже, это не так-то просто закостылять. Есть поле для дополнительных флагов компилятора, но если туда добавить что нибудь типа -fno-rtti, то gcc сразу начинает ругаться, мол что ты мне подсовываешь флаги от g++?

image

В интернетах народ предлагал CooCox второй версии — он уже имеет больше настроек, умеет различать C и C++ код и вызывает правильный компилятор для каждого из типов файлов. Но при этом все равно нельзя настроить отдельно ключи для каждого из компиляторов. Так что ключик -fno-rtti по прежнему нельзя добавить только для g++ — он так же передается и gcc, который на него злобно ругается.

А вообще второй CooCox как то не зашел — слишком много гламурного UI, при этом в ущерб удобству. Первый, кстати, тоже не супер в плане UI — миллион настроек, а шрифт в консоли билда поменять нельзя (при том, что в полноценном Eclipse можно).

Опять CMake

Раз уж такие известные тулы как CooCox не позволяют сделать банальную настройку компиляторов, то ну его нафиг. Будем спускаться на самый низкий уровень и писать все руками. Ну как руками… С мейкфайлами, конечно.

Последний раз я писал голые мейкфайлы почти 10 лет назад. Уже позабывал все тонкости, но точно помню, что дело это крайне неблагодарное. Дурацкий синтаксис и очень легко ошибиться. А еще крайне не портируемо. Решил еще разок попробовать CMake, но уже не с ардуино тулчейном, а STM32. При чем по всем правилам: отдельно установил эклипс, компилятор, CMake, MinGW32 для make.

На тулчейны особо не смотрел. На глаза попалось это, откуда я почерпнул общую идею, но сам тулчейн взял отсюда. Решил его не инсталлировать в общую кучу, а таскать с собой. К тому же мне там не все нужно, а только 2 файла — gcc_stm32.cmake где объявляются общие переменные и процедуры, и gcc_stm32f1.cmake где описываются параметры моего контроллера.

Все библиотеки у меня лежат по директориям, которые (теоретически) я буду синхронизировать с основными репозиториями (когда разберусь как :)). Поэтому добавлять CMakeList.txt в каждую библиотеку было как то некомильфо. Я решил сделать один общий CMakeList.txt в директории с библиотеками и в нем описать сборку всех библиотек. Каждая библиотека собирается в архив (статическую библиотеку) а потом все вместе линкуется в бинарник.

CMake скрипт для сборки библиотек (один скрипт на все библиотеки)

# Not pushing this file down to libraries in order to keep source tree as is (not populating with extra files, such as CMakeList.txt)
#
# Below each section represents a library with its own settings



###################
# NeoGPS
###################

SET(NEOGPS_SRC
	NeoGPS/DMS.cpp
	NeoGPS/GPSTime.cpp
	NeoGPS/Location.cpp
	NeoGPS/NeoTime.cpp
	NeoGPS/NMEAGPS.cpp
)
ADD_LIBRARY(NeoGPS STATIC ${NEOGPS_SRC})



###################
# FreeRTOS
###################

SET(FREERTOS_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/STM32duino/libraries/FreeRTOS821)

SET(FREERTOS_SRC
	${FREERTOS_SRC_DIR}/MapleFreeRTOS821.cpp
	${FREERTOS_SRC_DIR}/utility/heap_1.c
	${FREERTOS_SRC_DIR}/utility/list.c
	${FREERTOS_SRC_DIR}/utility/port.c
	${FREERTOS_SRC_DIR}/utility/queue.c
	${FREERTOS_SRC_DIR}/utility/tasks.c
)
ADD_LIBRARY(FreeRTOS STATIC ${FREERTOS_SRC})



###################
# Adafruit GFX library
###################

ADD_LIBRARY(AdafruitGFX STATIC 
	AdafruitGFX/Adafruit_GFX.cpp 
)



###################
# Adafruit SSD1306 library
###################

ADD_LIBRARY(AdafruitSSD1306 STATIC 
	STM32duino/libraries/Adafruit_SSD1306/Adafruit_SSD1306_STM32.cpp
)
TARGET_INCLUDE_DIRECTORIES(AdafruitSSD1306 PRIVATE
	STM32duino/libraries/Wire
	STM32duino/libraries/SPI/src  	#In fact it should not depend on it
	AdafruitGFX
)

В сборке основной части есть несколько нетривиальных вещей, которые я описал ниже. Пока приведу CMakeLists.txt целиком.

Скрипт сборки основной части проекта

# Build rules for GPS logger target.
# App specific compiler/linker settings are also defined here

SET(SOURCE_FILES
	# Screens and screen management stuff
	Screens/AltitudeScreen.cpp
	Screens/AltitudeScreen.h
	Screens/CurrentPositionScreen.cpp
	Screens/CurrentPositionScreen.h
	Screens/CurrentTimeScreen.cpp
	Screens/CurrentTimeScreen.h
	Screens/DebugScreen.cpp
	Screens/DebugScreen.h
	Screens/OdometerActionScreen.cpp
	Screens/OdometerActionScreen.h
	Screens/OdometerScreen.cpp
	Screens/OdometerScreen.h
	Screens/ParentScreen.cpp
	Screens/ParentScreen.h
	Screens/SatellitesScreen.cpp
	Screens/SatellitesScreen.h
	Screens/Screen.cpp
	Screens/Screen.h
	Screens/ScreenManager.cpp
	Screens/ScreenManager.h
	Screens/SelectorScreen.cpp
	Screens/SelectorScreen.h
	Screens/SettingsGroupScreen.cpp
	Screens/SettingsGroupScreen.h
	Screens/SpeedScreen.cpp
	Screens/SpeedScreen.h
	Screens/TimeZoneScreen.cpp
	Screens/TimeZoneScreen.h

	8x12Font.cpp
	Buttons.cpp
	FontTest.cpp
	GPSDataModel.cpp
	GPSLogger.cpp
	GPSOdometer.cpp
	GPSSatellitesData.cpp
	GPSThread.cpp
	IdleThread.cpp
	TimeFont.cpp
	Utils.cpp
)

INCLUDE_DIRECTORIES(
	.
	${GPSLOGGER_LIBS_DIR}/AdafruitGFX
	${GPSLOGGER_LIBS_DIR}/NeoGPS
	${GPSLOGGER_LIBS_DIR}/STM32duino/libraries/Adafruit_SSD1306
	${GPSLOGGER_LIBS_DIR}/STM32duino/libraries/SPI/src
	${GPSLOGGER_LIBS_DIR}/STM32duino/libraries/FreeRTOS821
	)


# Do not link to libc or newlib-nano - we are not using anything from that
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nosys.specs")

ADD_EXECUTABLE(GPSLogger ${SOURCE_FILES})
TARGET_LINK_LIBRARIES(GPSLogger
	NeoGPS
	FreeRTOS
	AdafruitGFX
	AdafruitSSD1306
	ArduinoLibs
	STM32duino
)

STM32_SET_TARGET_PROPERTIES(GPSLogger)
STM32_PRINT_SIZE_OF_TARGETS(GPSLogger)

# Additional handy targets
STM32_ADD_HEX_BIN_TARGETS(GPSLogger)
STM32_ADD_DUMP_TARGET(GPSLogger)

Что касается параметров самого CMake. В примерах stm32-cmake предлагается указывать какой именно тулчейн файл использовать. Делается это отдельным ключиком во время вызова CMake для генерации мейкфайлов. Но я не планирую собирать проект под разные платформы и компиляторы. Поэтому я просто прописал ссылку на нужный тулчейн файл в главном CMakeLists.txt.

# Load the toolchain file that uses vars above
SET(CMAKE_TOOLCHAIN_FILE cmake/gcc_stm32.cmake)

А вот компилятор автоматически не угадывается. Точнее, угадывался бы на юниксе (он ищется по дефлотныму пути в /usr), но на винде его нужно указывать явно. В общем мой комманд лайн (не забываем менять виндовые слеши на юниксовые — CMake их не любит):

cmake -G "MinGW Makefiles" "-DTOOLCHAIN_PREFIX=C:/Program Files (x86)/GNU Tools ARM Embedded/5.4 2016q3"  .

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

Проблема: кросс зависимости между библиотекам

Фреймворк stm32duino довольно большой. Еще на этапе создания CMakeList.txt для библиотек я попробовал разделить его на 2 отдельные библиотеки — одну часть, которая эмулирует API Arduino (собственно stm32duino) и libmaple, которая представляет собой Hardware Abstraction Layer (HAL) и скрывает низкоуровневые штуки микроконтроллера. Во всяком случае мне показалось это логичным. Но оказалось это проблематично слинковать. Я думал, что stm32duino построен поверх libmaple, но во втором так же нашлись вызовы в верхний слой.

Прикол в том, что линкер собирает библиотеки по одной и к предыдущим не возвращается. Если во второй библиотеке есть вызов в первую библиотеку, то линкер не понимает, что нужно опять линковать первую библиотеку. Поэтому возникают unresolved символы. Пришлось в проекте объединить две библиотеки в одну.

Потом я, правда, узнал, что у линкера есть специальные ключики -Wl,--start-group / --end-group которые меняют поведение линкера. Они как раз и заставляют его по нескольку раз проходить по библиотекам. Пока не пробовал.

CMake скрипт для сборки stm32duino и libmaple

###################
# STM32duino, libmaple and system layer
# Has to be as a single library, otherwise linker does not resolve all crossreferences
# Unused files are commented on the list
###################

SET(LIBMAPLE_SRC
	STM32duino/variants/generic_stm32f103c/board.cpp
	STM32duino/variants/generic_stm32f103c/wirish/boards.cpp
	STM32duino/variants/generic_stm32f103c/wirish/boards_setup.cpp
	STM32duino/variants/generic_stm32f103c/wirish/start.S
	STM32duino/variants/generic_stm32f103c/wirish/start_c.c
 	STM32duino/variants/generic_stm32f103c/wirish/syscalls.c

	STM32duino/cores/maple/cxxabi-compat.cpp
#	STM32duino/cores/maple/ext_interrupts.cpp
	STM32duino/cores/maple/HardwareSerial.cpp
#	STM32duino/cores/maple/HardwareTimer.cpp
#	STM32duino/cores/maple/IPAddress.cpp
	STM32duino/cores/maple/main.cpp
#	STM32duino/cores/maple/new.cpp
	STM32duino/cores/maple/Print.cpp
#	STM32duino/cores/maple/pwm.cpp
#	STM32duino/cores/maple/Stream.cpp
#	STM32duino/cores/maple/tone.cpp
	STM32duino/cores/maple/usb_serial.cpp
#	STM32duino/cores/maple/wirish_analog.cpp
	STM32duino/cores/maple/wirish_digital.cpp
#	STM32duino/cores/maple/wirish_math.cpp
#	STM32duino/cores/maple/wirish_shift.cpp
	STM32duino/cores/maple/wirish_time.cpp
#	STM32duino/cores/maple/WString.cpp

#	STM32duino/cores/maple/hooks.c
	STM32duino/cores/maple/itoa.c

#	STM32duino/cores/maple/stm32f1/util_hooks.c
#	STM32duino/cores/maple/stm32f1/wiring_pulse_f1.cpp
	STM32duino/cores/maple/stm32f1/wirish_debug.cpp
	STM32duino/cores/maple/stm32f1/wirish_digital_f1.cpp

	STM32duino/cores/maple/libmaple/adc.c
	STM32duino/cores/maple/libmaple/adc_f1.c
#	STM32duino/cores/maple/libmaple/bkp_f1.c
#	STM32duino/cores/maple/libmaple/dac.c
#	STM32duino/cores/maple/libmaple/dma.c
#	STM32duino/cores/maple/libmaple/dma_f1.c
#	STM32duino/cores/maple/libmaple/exc.S
#	STM32duino/cores/maple/libmaple/exti.c
#	STM32duino/cores/maple/libmaple/exti_f1.c
	STM32duino/cores/maple/libmaple/flash.c
#	STM32duino/cores/maple/libmaple/fsmc_f1.c
	STM32duino/cores/maple/libmaple/gpio.c
	STM32duino/cores/maple/libmaple/gpio_f1.c
	STM32duino/cores/maple/libmaple/i2c.c
	STM32duino/cores/maple/libmaple/i2c_f1.c
	STM32duino/cores/maple/libmaple/iwdg.c
	STM32duino/cores/maple/libmaple/nvic.c
#	STM32duino/cores/maple/libmaple/pwr.c
	STM32duino/cores/maple/libmaple/rcc.c
	STM32duino/cores/maple/libmaple/rcc_f1.c
#	STM32duino/cores/maple/libmaple/spi.c
#	STM32duino/cores/maple/libmaple/spi_f1.c
	STM32duino/cores/maple/libmaple/systick.c
	STM32duino/cores/maple/libmaple/timer.c
#	STM32duino/cores/maple/libmaple/timer_f1.c
	STM32duino/cores/maple/libmaple/usart.c
	STM32duino/cores/maple/libmaple/usart_f1.c
	STM32duino/cores/maple/libmaple/usart_private.c
	STM32duino/cores/maple/libmaple/util.c

	STM32duino/cores/maple/libmaple/stm32f1/performance/isrs.S
	STM32duino/cores/maple/libmaple/stm32f1/performance/vector_table.S

	STM32duino/cores/maple/libmaple/usb/stm32f1/usb.c
	STM32duino/cores/maple/libmaple/usb/stm32f1/usb_cdcacm.c
	STM32duino/cores/maple/libmaple/usb/stm32f1/usb_reg_map.c

	STM32duino/cores/maple/libmaple/usb/usb_lib/usb_core.c
	STM32duino/cores/maple/libmaple/usb/usb_lib/usb_init.c
	STM32duino/cores/maple/libmaple/usb/usb_lib/usb_mem.c
	STM32duino/cores/maple/libmaple/usb/usb_lib/usb_regs.c
)

ADD_LIBRARY(STM32duino STATIC ${LIBMAPLE_SRC})

TARGET_INCLUDE_DIRECTORIES(STM32duino PRIVATE
	STM32duino/system/libmaple/usb/stm32f1
	STM32duino/system/libmaple/usb/usb_lib
)

TARGET_COMPILE_DEFINITIONS(STM32duino PRIVATE 
	-DVECT_TAB_ADDR=${VECT_TAB_ADDR} 
	-DGENERIC_BOOTLOADER 
	-DBOARD_maple
)

Проблема: не линкуются системные вызовы

Дальше не получалось нормально слинковаться со стандартной библиотекой — не хватало _sbrk(), _open()/_close(), _read()/_write() и некоторых других, которые почему-то торчат из стандартной библиотеки.

На самом деле у них есть тривиальная реализация в STM32duinovariantsgeneric_stm32f103cwirishsyscalls.c, но она не линковалась по той же самой причине: в момент линковки stm32duino эти функции (они объявлены как weak) никому не нужны и выкидываются. Стандартная библиотека подключается неявно в самом конце процесса линковки и начинает требовать эти символы, а линковщик назад не возвращается. Можно, конечно, файл syscalls.c отдельно прилинковать после всех остальных, но чисто из спортивного интереса я стал разбираться откуда оно вообще лезет.

Тривиальная реализация некоторых системных вызовов

__weak int _open(const char *path, int flags, ...) {
    return 1;
}

__weak int _close(int fd) {
    return 0;
}

__weak int _fstat(int fd, struct stat *st) {
    st->st_mode = S_IFCHR;
    return 0;
}

__weak int _isatty(int fd) {
    return 1;
}

__weak int isatty(int fd) {
    return 1;
}

__weak int _lseek(int fd, off_t pos, int whence) {
    return -1;
}

__weak unsigned char getch(void) {
    return 0;
}

__weak int _read(int fd, char *buf, size_t cnt) {
    *buf = getch();

    return 1;
}

__weak void putch(unsigned char c) {
}

Как я понял, компилятор arm-gcc довольно могуч и может компилировать под добрую половину существующих микроконтроллеров и процессоров. Но поскольку это могут быть и мелкие микроконтроллеры с маленьким количеством памяти, так и толстые “камни” на которых линукс можно крутить, то и собирать нужно с учетом возможностей платформы. Поэтому в комплекте с линкером есть набор *.specs файлов, которые описывают что именно нужно линковать для той или иной платформы (это вдобавок к скриптам линкера).

Так по умолчанию к проекту прилинковывается стандартная библиотека, в данном случае это newlib-nano. Только если libc на больших компах опирается на системные вызовы и ядро, то в случае newlib-nano эти системные вызовы должен предоставить пользователь. Поэтому линковщик и требует объявления этих самых _sbrk(), _open()/_close(), _read()/_write().

Проблема решилась добавлением ключа --specs=nosys.specs в настройки линковщика. Этот ключик указывает линковщику на specs файлик где отключена линковка части стандартной библиотеки.

# Do not link to libc or newlib-nano - we are not using anything from that
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nosys.specs")

Проблема: не линкуется код инициализации

В какой то момент вылезла проблема с функцией _init(). Из newlib-nano есть вызов этой функции, но в коде этот символ нигде не объявлен. Мне кажется идея дизайна тут в следующем.

  • После ресета или подачи питания выполняется самый низкоуровневый старт. В случае libmaple это STM32duinovariantsgeneric_stm32f103cwirishstart.S. Задача этого кода передать управление в сишную функцию инициализации
    start.S

            .type __start__, %function
    __start__:
            .fnstart
            ldr r1,=__msp_init
            mov sp,r1
            ldr r1,=start_c
            bx r1
            .pool
            .cantunwind
            .fnend

  • Следующий шаг это подготовить память. Тут мы чистим секцию .bss (переменные заполненые нулями) и заполняем начальными значениями переменные из секции .data, Код находится в файле STM32duinovariantsgeneric_stm32f103cwirishstart_c.c
    start_c.c

    void __attribute__((noreturn)) start_c(void) {
        struct rom_img_cfg *img_cfg = (struct rom_img_cfg*)&_lm_rom_img_cfgp;
        int *src = img_cfg->img_start;
        int *dst = (int*)&__data_start__;
        int exit_code;
    
        /* Initialize .data, if necessary. */
        if (src != dst) {
            int *end = (int*)&__data_end__;
            while (dst < end) {
                *dst++ = *src++;
            }
        }
    
        /* Zero .bss. */
        dst = (int*)&__bss_start__;
        while (dst < (int*)&__bss_end__) {
            *dst++ = 0;
        }
    
        /* Run initializers. */
        __libc_init_array();
    
        /* Jump to main. */
        exit_code = main(0, 0, 0);
        if (exit) {
            exit(exit_code);
        }
    
        /* If exit is NULL, make sure we don't return. */
        for (;;)
            continue;
    }

  • После этого передается управление в функцию __libc_init_array() из newlib-nano. В этой функции происходит вызов инициализаторов, включая функции premain() и init() для инициализации платы, а также конструкторы глобальных объектов.
    __libc_init_array()

    /* Iterate over all the init routines.  */
    void
    __libc_init_array (void)
    {
      size_t count;
      size_t i;
    
      count = __preinit_array_end - __preinit_array_start;
      for (i = 0; i < count; i++)
        __preinit_array_start[i] ();
    
      _init ();
    
      count = __init_array_end - __init_array_start;
      for (i = 0; i < count; i++)
        __init_array_start[i] ();
    }

    premain() и init()

    // Force init to be called *first*, i.e. before static object allocation.
    // Otherwise, statically allocated objects that need libmaple may fail.
     __attribute__(( constructor (101))) void premain() {
        init();
    }
    
    void init(void) {
        setup_flash();
        setup_clocks();
        setup_nvic();
        systick_init(SYSTICK_RELOAD_VAL);
        wirish::priv::board_setup_gpio();
        setup_adcs();
        setup_timers();
        wirish::priv::board_setup_usb();
        wirish::priv::series_init();
        boardInit();
    }

  • При этом __libc_init_array() позволяет сделать хук и вызвать пользовательскую функцию между определенными этапами инициализации. И эта функция должна называется _init(). Как и в случае с другими системными вызовами из предыдущего раздела ожидается, что эта функция должна предоставляться кем то другим.
  • Дальше идет вызов main(), но нам это сейчас не интересно.

Как я понял функция _init() должна подставляться согласно выбранной платформе, например ее реализация есть тут: <ARM Toolchain>libgccarm-none-eabi5.4.1armv7-mcrti.o. Ардуино каким-то неявным образом подключает этот объектник, но у меня этот файл не подтягивался. Скорее всего это потому, что я отключил часть стандартной библиотеки ключиком --specs=nosys.specs. По рекомендации отсюда просто добавил пустую реализацию в код libmaple.

void __attribute__ ((weak)) _init(void)  {}

По хорошему, эта функция не должна быть пустая. Она должна делать то, что делает premain() — инициализировать плату. Но авторы stm32duino решили по другому.

В общем, все слинковалось, но прошивка по прежнему была очень жирной — больше 120кб. Нужно было разбираться что же там лишнего. Для этого нужно было внимательно изучить скрипты тулчейна и дизассемблировать то, что уже собралось.

Проблема: неверно определены кодовые секции

Первое, что бросилось в глаза это стартовый адрес. Стартовый адрес был то ли 0x00000000, то ли 0x00008000, но точно не такой как нужно — 0x08002000. После внимательного изучения STM32 CMake тулчейна я понял, что нужные параметры выставляются в скрипте линковщика, который также идет с тулчейном. Только этот скрипт по умолчанию не используется, а включается отдельной командой STM32_SET_TARGET_PROPERTIES(). Стартовый адрес пофиксился, и прошивка даже похудела до 100к.

# Flash offset due to bootloader
SET(VECT_TAB_ADDR "0x08002000")
SET(STM32_FLASH_ORIGIN "0x08002000")

ADD_EXECUTABLE(GPSLogger ${SOURCE_FILES})
STM32_SET_TARGET_PROPERTIES(GPSLogger)

Теперь в коде отсутствовала таблица векторов прерываний в начале прошивки. Судя описанию секций в файле таблица располагается в отдельной секции .isr_vector, только размер у нее почему то нулевой.

Я битый час пытался разобраться в линкер скриптах. Но там происходит какое-то очень сильное низкоуровневое колдунство: определяются какие-то секции, идет какая-то отсылка к коду. В частности, как я понял, секция с таблицей векторов прерываний должна описываться определенным образом где-то в коде (скорее всего в CMSIS, которого у меня-то и нет). Т.е. моя ошибка была в том, что я пытаться использовать generic скрипт линковщика с кодом инициализации от libmaple, а у них секции кода и данных называются и располагаются по разному.

Решение состояло в явном указании скрипта линковки от libmaple (STM32duino/variants/generic_stm32f103c/ld/bootloader_20.ld)

# Using Maple's linker script that corresponds maple/stm32duino code
SET(STM32_LINKER_SCRIPT ${GPSLOGGER_LIBS_DIR}/STM32duino/variants/generic_stm32f103c/ld/bootloader_20.ld)
LINK_DIRECTORIES(${GPSLOGGER_LIBS_DIR}/STM32duino/variants/generic_stm32f103c/ld)

Проблема: неверные настройки оптимизации

Но вот облом. Линковщик сказал, что он не может вместить сгенерированный код в мою флешку. Помимо того, что в прошивку попадает куча всякой лишней гадости, оказалось, что CMake по умолчанию собирает в дебажной конфигурации. А в дебажной конфигурации оптимизация -O2 (оптимизация по скорости), тогда как в релизной -Os (оптимизация по размеру). Я переключился на релизную конфигурацию, но с ней все равно не сложилось: тулчейн выставляет флаг -flto (Link Time Optimization), с которым не билдится половина stm32duino.

В общем я взял все самое лучше от двух конфигураций. От релизной конфигурации я взял ключ -Os. От дебажной я взял -g, которые добавляют отладочную информацию в бинарь. В прошивку эта информация все равно не идет, а дизассемблировать намного удобнее.

# TODO: It would be nice to use -flto for all of these 3 settings (except for asm one)
SET(CMAKE_C_FLAGS_RELEASE "-Os -g" CACHE INTERNAL "c compiler flags release")
SET(CMAKE_CXX_FLAGS_RELEASE "-Os -g" CACHE INTERNAL "cxx compiler flags release")
SET(CMAKE_ASM_FLAGS_RELEASE "-g" CACHE INTERNAL "asm compiler flags release")
SET(CMAKE_EXE_LINKER_FLAGS_RELEASE "" CACHE INTERNAL "linker flags release")

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

  • -DDEBUG_LEVEL=DEBUG_NONE отключает логгирование внутри libmaple. Выставление дефайна убирает примерно килобайт из результирующей прошивки
  • -fno-rtti -fno-exceptions — убирает огромную кучу кода (те самые RTTI, исключения, ABI и многое другое). Естественно, эти флаги я скормил только g++
  • Комбинация -fno-unroll-loops -ffast-math -ftree-vectorize просто генерит чуток более компактный код (100-200 байт на всю прошивку)

Вот мои ключики, которые я, в итоге, передаю компилятору:

SET(CMAKE_C_FLAGS "-mthumb -fno-builtin -mcpu=cortex-m3 -Wall -std=gnu99 -ffunction-sections -fdata-sections -fomit-frame-pointer -mabi=aapcs -fno-unroll-loops -ffast-math -ftree-vectorize -nostdlib -march=armv7-m --param max-inline-insns-single=500" CACHE INTERNAL "c compiler flags")
SET(CMAKE_CXX_FLAGS "-mthumb -fno-builtin -mcpu=cortex-m3 -Wall -std=c++11 -ffunction-sections -fdata-sections -fomit-frame-pointer -mabi=aapcs -fno-unroll-loops -ffast-math -ftree-vectorize -fno-rtti -fno-exceptions -nostdlib -fno-use-cxa-atexit -march=armv7-m --param max-inline-insns-single=500" CACHE INTERNAL "cxx compiler flags")
SET(CMAKE_ASM_FLAGS "-mthumb -mcpu=cortex-m3 -x assembler-with-cpp" CACHE INTERNAL "asm compiler flags")
SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--gc-sections -mthumb -mcpu=cortex-m3 -march=armv7-m -mabi=aapcs -Wl,--warn-common -Wl,--warn-section-align" CACHE INTERNAL "executable linker flags")

Победа над системой сборки

Итог — 48 килобайт на прошивку. В варианте Ардуино было 56кб. Разница объясняется отсутствием malloc/free и сопутствующих функций, которые я все равно не использовал. Полная пересборка занимает 17с против минуты на ардуино. Инкрементальная сборка всего 1-2 секунды.

Момент истины и пробуем залить то, что получилось в чип. Если честно я был ОООООЧЕНЬудивлен, что после того такого долгого танца в компании с бубном, гуглом и CMake’ом прошивка завелась сразу. Я ожидал еще неделю жесткого дебага в режиме черного ящика в попытках понять, почему же эта железяка хоть лампочкой взморгнуть не хочет.

Для красоты добавил вызов STM32_PRINT_SIZE_OF_TARGETS() — теперь после сборки в консоли пишется статистика по памяти. Сразу видно не скакнуло ли потребление памяти.

image

Еще добавил таргет на дизассемблирование прошивки (на данный момент он уже законтрибьючен в основную ветку stm32-cmake). Очень удобно билдить и сразу проверять что же там такое компилятор наделал, что прошивка резко потолстела.

FUNCTION(STM32_ADD_DUMP_TARGET TARGET)
    IF(EXECUTABLE_OUTPUT_PATH)
      SET(FILENAME "${EXECUTABLE_OUTPUT_PATH}/${TARGET}")
    ELSE()
      SET(FILENAME "${TARGET}")
    ENDIF()
    ADD_CUSTOM_TARGET(${TARGET}.dump DEPENDS ${TARGET} COMMAND ${CMAKE_OBJDUMP} -x -D -S -s ${FILENAME} | ${CMAKE_CPPFILT} > ${FILENAME}.dump)
ENDFUNCTION()

QtCreator

Весь вышеописанный процесс сборки я делал просто в консоли. Теперь пришла пора подключить IDE. Я уже упоминал, что хотел бы познакомиться с Eclipse, но когда я его установил он мне показался жутко монстрообразным. Но самое главное я с наскока не понял концепцию воркспейсов. В моем понимании если я открываю проект, то сразу вижу все файлы из этого проекта. В Eclipse пришлось делать много лишних телодвижений. Короче говоря он просто не зашел, но я планирую обязательно к нему вернуться в другой раз.

Я вспомнил, что я когда-то программировал в Qt Creator и это, вроде, было неплохо. Установив Qt Creator 4.2.2 я стал гуглить как подключить CMake проект. Согласно инструкциям в интернете предлагалось просто открыть CMakeLists.txt файл и следовать инструкциям в визарде. Первым делом он предложил насетапить инструментарий (kits). Вполне разумно, учитывая жуткую кастомность сборки.

image

Qt Creator ругается на такой kit, мол не выставлен компилятор. Но если выставить, то он все равно ругается — компилятор который выставляет сам CMake (arm-gcc) не совпадает с выбранным в соответствующем поле (даже если он такой же). Впрочем, все билдит он нормально.

При настройке проекта возник один очень нетривиальный момент. При импорте CMake проекта импортировался только CMakeLists.txt. Никакие исходники в проект не добавлялись. Я штудировал интернет несколько вечеров — безрезультатно. У всех все работает, а у меня нет. Думал проблема в кастомном тулчейне, но простейший hello world под MinGW32 точно так же не импортировался.

Решение подсказал сам QtCreator. Ворнинг в тултипе на созданный мною kit гласил, что я выбрал неверный тип CMake генератора. Нужно выбирать Code Blocks — без этого Qt Creator не может парсить проекты. После установки правильного генератора все файлы и поддиректории появились в окне проекта.

image

Теперь можно удобно навигироваться по файлам, но автокомплит пока еще не работал. На самом деле как бы странно это ни звучало, но Qt Creator попросту игнорил инклуд пасы указанные в CMakeLists.txt. Т.е. сборка проходит отлично, а в редакторе бОльшая часть #include’ов подсвечивает как ошибка (No such file). Работают только инклуды из той же самой директории. Я излазил все настройки, несколько часов гуглил, но ответа так и не нашел. Для QMake проектов инклуд пасы работают, а для CMake — нет. В общем писать код можно, но отсутствие автокомплита немного напрягает.

Одной из самых удобных штук, которые делают текстовый редактор полноценной IDE — автоматическая заливка и запуск прошивки. Я это сделал так. Основной билд у меня вызывает таргет GPSLogger.bin, а не классический all. Этот таргет делает из .elf файла .bin, который уже можно вливать в контроллер. Deployment процедура запускает “заливатор” прошивки.

image

Обратите внимание на трюк с вызовом ping. Его задача обеспечить паузу в 1 сек между заливкой и запуском. Иначе микроконтроллер не успевает перезагрузиться и проинициализировать USB.

Странности QtCreator

К слову, это единственный способ организовать задержку из предложеных тут который сработал в QtCreator. Ни powershell, ни timeout работать не захотели.

В качестве “запускатора” (Run) оказалось удобно использовать эмулятор терминала. Эта штука позволяет ловить дебажный вывод в serial. Т.е. прошивка после загрузки, на самом деле, запускается сама. Мы только выбираем хотим ли мы смотреть, что пишется в сериал или нет.

А вот про отладку сегодня не будет. Во-первых про это уже много раз написано, не думаю, что я выдумаю что нибудь новое. Во-вторых, я не использую отладчики — в этом проекте я не использую навороченную периферию, которая требует внутрисхемной отладки. У меня используется только пара UART’ов да I2C. Так что вполне можно дебажиться принтами в лог. Ну а в-третих, я уже месяц не писал код — все вожусь с системами сборки и IDE. Не хотелось бы тратить еще неделю-другую на погружение в отладчики. Кому нужно — погуглите. Там есть много статей про прикручивание отладчиков к разным IDE.

Заключение

Я не ставил себе за цель написать туториал по сборке. Скорее тут описаны мои боль и страдания в попытках найти хорошую билд систему для своего проекта. В этой статье я рассказал свой опыт перехода на CMake для сборки своего проекта на контроллере STM32.

Я использую фреймворк stm32duino, который основан на libmaple. Это не очень стандартный фреймворк и потому у меня возникло некоторое количество трудностей и нетривиальных моментов. По ходу работы я разобрался в некоторых тонкостях работы линковщика: как происходит инициализация платы, структура libc и от чего она зависит, кто и как распределяет память в кодовых секциях. Обычно эти штуки скрываются в недрах CRT, компилятора и IDE, но в данном случае мне пришлось в явном виде поковырять эти нюансы.

Стоит отметить, что бОльшая часть шаманства, описанного в этой статье относится к сборке именно под STM32. В классическом ардуино и сборке под ATMega все попроще. Но многие идеи остаются неизменными независимо от платформы. Думаю, вдумчивый читатель также найдет много полезного в моей статье.

Вероятно, некоторые вещи я не до конца раскрыл или привел ошибочные суждения. Что ж, я не исключаю вероятность ошибок. В любом случае я открыт для конструктивной критики.

Автор: grafalex

Источник

Поделиться

* - обязательные к заполнению поля