Обсуждение статьи после прочтения или задать вопросы можно в VK: vk.com/topic-200545792_46641834
Так же теперь (2021 год) я написал небольшой редактор для программ на ассемблере, начинать читать можно с Редактор ассемблера для ARM микроконтроллеров для компилятора gnu as. Старт там же можно будет и создавать проект в более удобном формате нежели описано в этой и нескольких последующих статьях. При этом рекомендую все таки ознакомится со всеми статьями по этой тематике в моих публикациях, так как не везде я повторяю прошлые материалы.
Это моя первая статья для сообщества Хабрахабр и написать ее я решил про то что сейчас волнует меня самого: написание программ для микроконтроллеров STM32 (семейство АRМ) на языке ассемблера. Я использую отладочную плату на основе микроконтроллера STM32F407 (STM32F4 Discovery, Open407I-C), но статья будет не менее полезна и для программирования других микроконтроллеров STM32.
После поисков по интернету так и не удалось найти сколь нить понятного для новичка способа написания прошивок для STM32- и ARM- контроллеров вообще на языке ассемблера. Нет, конечно любой поисковик по сочетанию «STM32 ассемблер» дает очень много результатов, но после внимательного их изучения выясняется что 98% результатов поиска ведут на описание сред в которых по заверению производителей языком программирования микроконтроллеров является С, С++, Assembler.
Более того, как только Вы начинаете искать информацию о том как же все таки программировать на ассемблере в конкретной среде для конкретного микроконтроллера — выясняется, что «гуру не пишут проекты на ассемблере», и в средах ассемблер используется максимум для написания процедур и функций требующих максимального быстродействия, или генерации кода содержащего специфические команды микроконтроллера аналог которых не предлагается языком Си (С++ или библиотеками) in-line вставками.
Дальше было еще интереснее, я выяснил что компиляторов ассемблера на ARM платформу существует как минимум два: GNU AS, и ARMASM. И у них различные форматы исходных файлов… Нет, конечно команды ассемблера одинаковые (слава богу), но вот как они пишутся, как указываются операнды, особенно служебные токены — отличаются так, что компиляция без «танцев» исходных файлов GNU AS на ARMASM (и наоборот) становится невозможна. Причем если GNU AS используется в бесплатно распространяемых средах (например CooCox), то ARMASM идет в составе далеко не самой дешевой среды Keil MDK. Насколько мне удалось выяснить для кода размером не более 32 кб среда является бесплатной, но не думаю что 32 кб для 32-разрядного микроконтроллера является каким то выдающимся размером для прошивки: немного кода работающего с дисплеем, немного шрифтов под дисплей, образов иконок — и 32 кб может и не хватить. Сообщество любителей нашло выход — это использование отдельных образов кода по 32 кб и потом объединение их различными способами — но как то хочется нормальной разработки, а не попыток «впихнуть и распределить невпихуемое» (использование пиратских программ мне не интересно). Есть ли ограничение на размер кода у самого компилятора ARMASM или это ограничение Keil MDK — я не стал выяснять, не очень приятно проделать работу основываясь на данных что все бесплатно, и в середине какого нить уже не тестового проекта получить ошибку компиляции из-за ограничений по размеру…
Таким образом, несмотря на то что в сети проектов написанных под ARMASM больше (по крайней мере так мне показалось исходя из результатов поиска), я решил разобраться с GNU AS — который бесплатен для любых размеров прошивок.
В результате поисков ресурсов (другие 2% результатов по запросу «STM32 ассемблер») на тему ассемблера для STM32 я наткнулся на отсутствие какого то начального состояния у многочисленных авторов. У кого то хорошо описаны инструкции ARM, у кого то сделаны попытки раскрыть параметры ассемблера и линковщика, где то описана минимальная (по мнению автора статьи) конфигурация от которой можно оттолкнуться — но вся эта информация так раскидана — что для начального вхождения практически не пригодна, тем более когда в проекте появляется что-то новое и вы просто не понимаете какие исправления нужно внести чтобы все «заработало»… Особенно тяжело если вы раньше не задумывались о том что делает IDE при компиляции ваших проектов. Теоретически конечно все представляют, что есть ассемблер, линковщик и другие вспомогательные утилиты, но вот какие файлы настроек, ключи для запуска, файлы для работы нужны — все это как то на уровне «ликбеза».
Поэтому, цель первого этапа — создать с нуля минимальную конфигурацию программных и настроечных файлов для возможности написания программ на языке ассемблера.
Я постараюсь описать все более менее последовательно, чтобы Вы могли не только просто повторить то, что я сделал, но и сделать собственные настройки для своего микроконтроллера, а так же менять эти настройки когда это будет необходимо (это пожалуй самое главное!). Вообще не думаю что все что я написал нужно повторять — просто внимательно прочитайте! Это тот путь который вы должны были бы пройти если бы начинали разбираться во всем сами. Я не буду описывать каким образом я дошел до того или иного шага, это сборная «солянка» от поиска в интернете, просмотра примеров других авторов, разбирательства в файлах которые генерирует CoIDE (CooCox), это не интересно и не нужно, но то что узнал в ключевых моментах, и что реально будет нужно для написания своих проектов я постараюсь отметить.
Для начала необходимо скачать пакет разработчика GNU GCC (из него можно взять программы компилятора ассемблера), это можно сделать например отсюда (справа выбираем пакет в зависимости от операционной системы).
Этот пакет можно распаковать на диск (я распаковал в каталог gcc каталога CooCox, у меня этот пакет используется и для программирования в среде CooCox).
Нужные нам файлы я выделил:
Нам осталась самая малость: разобраться как скомпилировать и как распределить части нашей прошивки.
Здесь очень желательно чтобы вы представляли с чего начинается исполнение программы у микроконтроллера STM32F4, если это не откровение для вас можете пропустить пару абзацев:
При включении микроконтроллер:
Исходя из выявленных шагов находим в документации на контроллер в каких адресах располагается SRAM у STM32F4 (или у Вашего микроконтроллера, так как различные микроконтроллеры имеют различный объем памяти). Для моего микроконтроллера открываем «RM0090 Reference manual» (для других микроконтроллеров ST посмотрите здесь).
Смотрим оглавление на предмет «чего нить про память»:
Идем на страницу 59
Внимательно читаем! У нашего микроконтроллера есть два банка памяти SRAM1 и SRAM2 размерами 112 кб и 16 кб. Другая интересующая нас информация находится на странице 68.
У микроконтроллера проекта STM32F407 фактически 3 блока памяти расположенных в разных областях адресного пространства:
Не стоит обвинять разработчиков микроконтроллера в раскидывании памятью по адресному пространству — эти 3 разных блока памяти исполняют различные функции:
Возьмем банки памяти SRAM1 и SRAM2 и в их конце разместим стек. Вычислим адрес последней ячейки стека:
0x2000 0000 + 128kб = 0x2000 0000 + 0x0002 0000 = 0x2002 0000
Таким образом указатель стека нужно будет установить на значение 0x20020000. Получаем следующую «карту» памяти:
Со вторым пунктом проще — мы укажем адрес следующий после нашей таблицы переходов. Поскольку один указатель у нас занимает 4 байта, таких указателей у нас 2 — получается что программу мы можем начинать писать с адреса 0x08000008.
Получаем следующую табличку:
Теперь немного «шаманства». Дело в том что платформа ARM имеет очень большое количество микропроцессоров, с различными форматами команд. Для того чтобы определить какой формат команды используется используются 2 младших бита адреса команды. Так вот младший бит установленный в «1» показывает, что команда подлежащая исполнению указана в формате Thumb. В случае, если оба младших бита сброшены — микропроцессор считает что нужно исполнить команду в формате ARM. Для микроконтроллеров STM32 при осуществлении переходов допустим только набор команд Thumb! В случае если мы попробуем «заставить» микроконтроллер перейти на команду в формате ARM — произойдет ошибка!
Если посмотреть двухзначное представление полученного нами адреса то мы увидим что в нашем адресе «закодирована» система команд ARM.
Младший байт адреса (0x08) в двоичном виде:
А должно быть:
Фактически добавление младшего бита установленного в «1» — это просто увеличение адреса на 1.
В качестве программы будем использовать примитивный бесконечный цикл. Чтобы не отправлять Вас в «копание» в документации — это команда «B».
Если вы задумались об ассемблере, или писали на ассемблере для AVR (или i8080, или Z80), то наверное команда «JMP» восьмибитников вам знакома, так вот «B» ее аналог.
Получается что наша программа должна выглядеть следующим образом:
Займемся средой компиляции. Компилятор ассемблера имеет название arm-none-eabi-as.exe. Вызывая компилятор мы должны указать ему файл нашей программы, и файл в который он должен сохранить результат компиляции. Если запустить компилятор с ключем —help, можно найти какие для этого нужны ключи, берем минимально необходимый ключ -о:
Нашу программу мы разместим в файле main.asm. Файл-результат работы компилятора называется «объектный файл», общепринятое расширение таких файлов «.о» — пусть будет называться main.o. Таким образом компилятор мы вызовем строкой:
Но это еще не все (к сожалению) — дело в том что этот компилятор может быть использован для различных семейств и типов микроконтроллеров и процессоров и мы до сих пор не указали ему какой же микроконтроллер собираемся использовать. Исходя из того что нам сообщает сам компилятор его устроит указание семейства cortex-m4.
Указать можно двумя путями:
Дополнительно нужно указать систему команд которую мы будем использовать: «.thumb». Ну и еще дополнительно указывается параметр «.syntax unified» — этот параметр задает некоторые особенности при написании программы, например, разрешает перед числами не ставить префикс «#», иногда не писать вручную некоторые инструкции ассемблера (IT) — компилятор это будет делать за нас (в случае необходимости) и так далее (в этом будем разбираться позже). Почитать дополнительно об этом можно здесь.
Получаем следующую программу:
Теперь, запустив компилятор командой:
мы увидим что в папке появился файл main.o — таким образом компиляция прошла успешно, объектный файл создан.
Небольшое отступление:
Для полноты привожу содержимое файла make_project.bat на данном этапе:
Теперь из файла main.o при помощи программы-линковщика нужно сделать файл прошивки. Для программы-линковщика необходим файл настройки в котором собственно говоря и будет описано какую часть нашей программы куда нужно поместить.
Здесь могу рекомендовать почитать статью на этом же ресурсе habrahabr.ru/post/191058 — там можно найти один из примеров карты линковщика, есть и другие ресурсы где можно посмотреть какими бывают файлы линковщика, но к сожалению простых решений нигде не предлагается.
Один из самых простых файлов линковщика приведенный по ссылке выше выглядит так:
Для того чтобы понимать что, как и зачем, давайте попробуем составить свою карту линковщика с нуля, используя данную как подсказку (вместе со статьей автора ее написавшего, ну и подглядывайте в интернет — там море информации на эту тему).
ИТАК, первое и самое заметное: карта линковщика состоит как бы из двух больших секций MEMORY и SECTIONS
В первом блоке (MEMORY) указывается какая память установлена в микроконтроллере, в каких адресах, и какого размера. Адреса и размеры SRAM мы уже выясняли, а вот для FLASH памяти эту информацию нужно поискать в том же «RM0090 Reference manual» или просто вспомнить сколько flash памяти у вашего микроконтроллера (например по полному наименованию микроконтроллера).
После указания имени типа памяти (произвольно, кто то пишет ROM / RAM, кто то MEMORY, я пишу FLASH / SRAM) указываются режимы доступа к памяти: для FLASH памяти R — чтение, X-исполнение. И далее начальный адрес памяти в адресном пространстве и размер.
Должно получиться примерно так:
Далее идет определение областей или секций. Как вы могли уже прочитать по ссылке выше — основные секции прошивки имеют имена:
Имена этих секций в прошивке «забиты» в самих утилитах компиляции, поэтому произвольно их менять не получится. Количество секций в исходных файлах задается вами самостоятельно, и для языков высокого уровня сильно зависит от компилятора языка программы, для Си компиляторов количество секций нередко доходит до 10… Секции исходных файлов всегда приводятся к секциям прошивки.
Конструкция блока SECTIONS в общем виде строиться следующим образом:
Еще раз резюмирую: у нас есть два разных набора секций: один набор секций — это секции описанные в исходных файлах нашей программы, другой набор секций — это секции которые будут записаны в прошивке. Соответственно сначала объявляем секцию прошивки, и внутри указываем какие секции описанные в исходных файлов в нее входят. Секции прошивки называются так как называются, секции в исходных файлах — называем мы сами (в определенных пределах к сожалению, но об этом позже).
Пока вам (и мне) повезло, у нас только одна секция прошивки и одна секция исходного файла — исполняемый код.
Получаем следующее:
Примечания пишутся внутри скобок /* */, все примечания пишутся «для себя» и «чтобы не забыть». Этот текст карты линковщика я поместил в файл stm32f40_map.ld.
Я намеренно упростил нашу карту линковщика по максимуму чтобы вы могли понять идею ее написания. Несмотря на упрощение (которое наверняка мне не простят «гуру» программирования и напишут в комментариях, что так писать нельзя) эта карта линковщика для нашего примера полностью работоспособна (а иначе не стоило и мучаться с ее написанием)!
Теперь, коль у нас появились секции — совершенно очевидным становиться необходимость указания в прошивке что и к какой секции относиться! Делается это при помощи команды .section, секция у нас одна .text, правим наш main.asm.
Файл исходного текста нашей программы теперь выглядит вот так:
Линковщик это утилита arm-none-eabi-ld.exe, при попытке запросить помощь запуская линковщик с ключем -–help выходит не маленькая «портянка», но мы не будем строить монстроидальный запуск, нам нужно только сделать линковку полученной после ассемблера прошивки.
Используем ключ -T для указания файла карты памяти, и ключ -o для задания имени файла-прошивки, это будет файл с расширением .elf.
Чтобы руками не набирать правим наш make_project.bat:
После запуска у нас появляется файл с расширением .elf.
Теперь нужно сделать остановку и поразмыслить — а что же мы получили? Ну да, ассемблер «что то» скомпилировал (файл main.o), а линковщик на основании этого сделал «какую-то» прошивку (файл main.elf), но как нам проверить, что у нас получилось?
По логике действий мы должны залить прошивку в микроконтроллер и посмотреть на результат… И тут мы вспоминаем что прошивка то у нас ничего не делает — поэтому никакого мигающего светодиода не будет! Вообще ничего не будет! Внешне, по пинам микроконтроллера мы не узнаем работает он, или прошивка оказалась ошибочной — и он завис.
По нашей программе проверку работы микроконтроллера пока сделать нельзя. А что можно? — с файлом прошивки .elf — мы даже не можем посмотреть что и куда линковщик расположил (посмотреть хотелось бы не утилитами, а чем нить вроде того же FAR, чтобы уж «точно и железно», перед тем как мы пойдем дальше нужно быть уверенными что на текущий момент все сделано верно).
Значит нужно сделать из прошивки .elf прошивку в формате .bin. Бинарный формат прошивки это фактически байты которые будут загружены в контроллер.
Сделать это можно при помощи утилиты arm-none-eabi-objcopy.exe. Ключем -O мы задаем формат который мы хотим получить, допустимы 2 значения: binary и ihex. Правим наш make_project.bat:
Запускаем и получаем «вожделенный» файлик output.bin. Размер файла output.bin 10 байт, в FAR можно запустить его просмотр (F3) и в этом режиме переключиться в просмотр кода (F4):
Вот теперь нам есть что анализировать:
На этом текущее повествование прекратим и подведем итоги:
В следующей статье будем писать прошивку микроконтроллера чтобы реально проверить его работу — будем мигать светодиодом!
Часть 2: STM32F4: GNU AS: Мигаем светодиодом (Оживление)
Так же теперь (2021 год) я написал небольшой редактор для программ на ассемблере, начинать читать можно с Редактор ассемблера для ARM микроконтроллеров для компилятора gnu as. Старт там же можно будет и создавать проект в более удобном формате нежели описано в этой и нескольких последующих статьях. При этом рекомендую все таки ознакомится со всеми статьями по этой тематике в моих публикациях, так как не везде я повторяю прошлые материалы.
Это моя первая статья для сообщества Хабрахабр и написать ее я решил про то что сейчас волнует меня самого: написание программ для микроконтроллеров STM32 (семейство АRМ) на языке ассемблера. Я использую отладочную плату на основе микроконтроллера STM32F407 (STM32F4 Discovery, Open407I-C), но статья будет не менее полезна и для программирования других микроконтроллеров STM32.
После поисков по интернету так и не удалось найти сколь нить понятного для новичка способа написания прошивок для STM32- и ARM- контроллеров вообще на языке ассемблера. Нет, конечно любой поисковик по сочетанию «STM32 ассемблер» дает очень много результатов, но после внимательного их изучения выясняется что 98% результатов поиска ведут на описание сред в которых по заверению производителей языком программирования микроконтроллеров является С, С++, Assembler.
Более того, как только Вы начинаете искать информацию о том как же все таки программировать на ассемблере в конкретной среде для конкретного микроконтроллера — выясняется, что «гуру не пишут проекты на ассемблере», и в средах ассемблер используется максимум для написания процедур и функций требующих максимального быстродействия, или генерации кода содержащего специфические команды микроконтроллера аналог которых не предлагается языком Си (С++ или библиотеками) in-line вставками.
Дальше было еще интереснее, я выяснил что компиляторов ассемблера на ARM платформу существует как минимум два: GNU AS, и ARMASM. И у них различные форматы исходных файлов… Нет, конечно команды ассемблера одинаковые (слава богу), но вот как они пишутся, как указываются операнды, особенно служебные токены — отличаются так, что компиляция без «танцев» исходных файлов GNU AS на ARMASM (и наоборот) становится невозможна. Причем если GNU AS используется в бесплатно распространяемых средах (например CooCox), то ARMASM идет в составе далеко не самой дешевой среды Keil MDK. Насколько мне удалось выяснить для кода размером не более 32 кб среда является бесплатной, но не думаю что 32 кб для 32-разрядного микроконтроллера является каким то выдающимся размером для прошивки: немного кода работающего с дисплеем, немного шрифтов под дисплей, образов иконок — и 32 кб может и не хватить. Сообщество любителей нашло выход — это использование отдельных образов кода по 32 кб и потом объединение их различными способами — но как то хочется нормальной разработки, а не попыток «впихнуть и распределить невпихуемое» (использование пиратских программ мне не интересно). Есть ли ограничение на размер кода у самого компилятора ARMASM или это ограничение Keil MDK — я не стал выяснять, не очень приятно проделать работу основываясь на данных что все бесплатно, и в середине какого нить уже не тестового проекта получить ошибку компиляции из-за ограничений по размеру…
Таким образом, несмотря на то что в сети проектов написанных под ARMASM больше (по крайней мере так мне показалось исходя из результатов поиска), я решил разобраться с GNU AS — который бесплатен для любых размеров прошивок.
В результате поисков ресурсов (другие 2% результатов по запросу «STM32 ассемблер») на тему ассемблера для STM32 я наткнулся на отсутствие какого то начального состояния у многочисленных авторов. У кого то хорошо описаны инструкции ARM, у кого то сделаны попытки раскрыть параметры ассемблера и линковщика, где то описана минимальная (по мнению автора статьи) конфигурация от которой можно оттолкнуться — но вся эта информация так раскидана — что для начального вхождения практически не пригодна, тем более когда в проекте появляется что-то новое и вы просто не понимаете какие исправления нужно внести чтобы все «заработало»… Особенно тяжело если вы раньше не задумывались о том что делает IDE при компиляции ваших проектов. Теоретически конечно все представляют, что есть ассемблер, линковщик и другие вспомогательные утилиты, но вот какие файлы настроек, ключи для запуска, файлы для работы нужны — все это как то на уровне «ликбеза».
Поэтому, цель первого этапа — создать с нуля минимальную конфигурацию программных и настроечных файлов для возможности написания программ на языке ассемблера.
Я постараюсь описать все более менее последовательно, чтобы Вы могли не только просто повторить то, что я сделал, но и сделать собственные настройки для своего микроконтроллера, а так же менять эти настройки когда это будет необходимо (это пожалуй самое главное!). Вообще не думаю что все что я написал нужно повторять — просто внимательно прочитайте! Это тот путь который вы должны были бы пройти если бы начинали разбираться во всем сами. Я не буду описывать каким образом я дошел до того или иного шага, это сборная «солянка» от поиска в интернете, просмотра примеров других авторов, разбирательства в файлах которые генерирует CoIDE (CooCox), это не интересно и не нужно, но то что узнал в ключевых моментах, и что реально будет нужно для написания своих проектов я постараюсь отметить.
Для начала необходимо скачать пакет разработчика GNU GCC (из него можно взять программы компилятора ассемблера), это можно сделать например отсюда (справа выбираем пакет в зависимости от операционной системы).
Этот пакет можно распаковать на диск (я распаковал в каталог gcc каталога CooCox, у меня этот пакет используется и для программирования в среде CooCox).
Нужные нам файлы я выделил:
Теперь небольшое отступление для тех кто не задумывался о том как происходит преобразование написанной программы в исполняемый код.
Программа написанная в текстовом редакторе (это может быть как блокнот, так и среды разработки по типу CooCox, Keil) сначала компилируется ассемблером.
Все более менее серьезные программы состоят из различных частей которые размещаются в разных исходных файлах программы, эти части могут иметь различное назначение (код, константы, данные), располагаться в различных областях памяти (FLASH, SRAM, Backup SRAM и т. д.) — поэтому мало просто откомпилировать программу, нужно еще правильно расположить ее части в памяти микроконтроллера. Этим важным делом занимается линковщик — в зависимости от схемы линковки он располагает части программы по так называемым сегментам которые и представляют собой различные области памяти микроконтроллера.
Нам осталась самая малость: разобраться как скомпилировать и как распределить части нашей прошивки.
Здесь очень желательно чтобы вы представляли с чего начинается исполнение программы у микроконтроллера STM32F4, если это не откровение для вас можете пропустить пару абзацев:
При включении микроконтроллер:
- По адресу 0х0800 0000(*) загружает значение регистра указателя стека SP. Обычно стек начинается с конца доступной RAM и при заполнении движется от старших адресов к младшим
- По адресу 0x0800 0004 загружает значение регистра PC (Program counter) — то есть фактически осуществляет переход по адресу указанному по адресу 0x0800 0004
- Осуществляется исполнение программы по адресу загруженному в PC
(*) Адреса разделены пробелом для удобства чтения, в программах правильно писать конечно же 0x08000000
Исходя из выявленных шагов находим в документации на контроллер в каких адресах располагается SRAM у STM32F4 (или у Вашего микроконтроллера, так как различные микроконтроллеры имеют различный объем памяти). Для моего микроконтроллера открываем «RM0090 Reference manual» (для других микроконтроллеров ST посмотрите здесь).
Смотрим оглавление на предмет «чего нить про память»:
Идем на страницу 59
Внимательно читаем! У нашего микроконтроллера есть два банка памяти SRAM1 и SRAM2 размерами 112 кб и 16 кб. Другая интересующая нас информация находится на странице 68.
У микроконтроллера проекта STM32F407 фактически 3 блока памяти расположенных в разных областях адресного пространства:
- объединенный блок SRAM1 и SRAM2 начинающийся с адреса 0x2000 0000 и имеющий размер 112 кб + 16 кб (всего 128 кб)
- отдельный блок CCM — начинающийся с адреса 0x1000 0000. Согласно написанному «на борту» у контроллера 192 кб — следовательно блок CCM у нас на 64 кб.
- Отдельный блок backup SRAM размером в 4 кб
Не стоит обвинять разработчиков микроконтроллера в раскидывании памятью по адресному пространству — эти 3 разных блока памяти исполняют различные функции:
- SRAM1 и SRAM2 — хранение переменных и данных
- CCM — хранение переменных и данных, а так же программ исполняющихся из оперативной памяти (!)
- backup SRAM – хранение переменных и данных с питанием от внешней батареи энергонезависимого питания. В микроконтроллере STM32F4 нет энергонезависимой памяти для хранения констант, и предложен способ хранения переменных в ОЗУ с питанием контроллера от миниатюрной батарейки (типа часовой) с минимальным потреблением
Возьмем банки памяти SRAM1 и SRAM2 и в их конце разместим стек. Вычислим адрес последней ячейки стека:
0x2000 0000 + 128kб = 0x2000 0000 + 0x0002 0000 = 0x2002 0000
Таким образом указатель стека нужно будет установить на значение 0x20020000. Получаем следующую «карту» памяти:
Адрес | Значение |
0x08000000 | 0x20020000 |
Со вторым пунктом проще — мы укажем адрес следующий после нашей таблицы переходов. Поскольку один указатель у нас занимает 4 байта, таких указателей у нас 2 — получается что программу мы можем начинать писать с адреса 0x08000008.
Получаем следующую табличку:
Адрес | Значение | Примечание |
0x08000000 | 0x20020000 | Значение для загрузки в SP (указатель стека) |
0x08000004 | 0x08000008 | Значение для перехода по сбросу (включению) |
Значения указателей 4-х байтные, формат этот называется word (это вам должно быть известно из курса программирования на любом языке)
Теперь немного «шаманства». Дело в том что платформа ARM имеет очень большое количество микропроцессоров, с различными форматами команд. Для того чтобы определить какой формат команды используется используются 2 младших бита адреса команды. Так вот младший бит установленный в «1» показывает, что команда подлежащая исполнению указана в формате Thumb. В случае, если оба младших бита сброшены — микропроцессор считает что нужно исполнить команду в формате ARM. Для микроконтроллеров STM32 при осуществлении переходов допустим только набор команд Thumb! В случае если мы попробуем «заставить» микроконтроллер перейти на команду в формате ARM — произойдет ошибка!
Если посмотреть двухзначное представление полученного нами адреса то мы увидим что в нашем адресе «закодирована» система команд ARM.
Младший байт адреса (0x08) в двоичном виде:
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Значение | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
Полубайт | 0 | 8 |
А должно быть:
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Значение | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
Флаг типа команды | часть адреса | Флаг | ||||||
Полубайт | 0 | 9 |
Фактически добавление младшего бита установленного в «1» — это просто увеличение адреса на 1.
Запоминаем правило: при указании указателей в таблице векторов прерываний необходимо увеличивать значение адреса на 1
В качестве программы будем использовать примитивный бесконечный цикл. Чтобы не отправлять Вас в «копание» в документации — это команда «B».
Если вы задумались об ассемблере, или писали на ассемблере для AVR (или i8080, или Z80), то наверное команда «JMP» восьмибитников вам знакома, так вот «B» ее аналог.
Получается что наша программа должна выглядеть следующим образом:
.word 0x20020000 @ Указатель вершину стека
.word Reset + 1 @ Указатель на программу
Reset: B Reset
Займемся средой компиляции. Компилятор ассемблера имеет название arm-none-eabi-as.exe. Вызывая компилятор мы должны указать ему файл нашей программы, и файл в который он должен сохранить результат компиляции. Если запустить компилятор с ключем —help, можно найти какие для этого нужны ключи, берем минимально необходимый ключ -о:
arm-none-eabi-as.exe -o <файл результат> <файл исходник>
Нашу программу мы разместим в файле main.asm. Файл-результат работы компилятора называется «объектный файл», общепринятое расширение таких файлов «.о» — пусть будет называться main.o. Таким образом компилятор мы вызовем строкой:
arm-none-eabi-as.exe -o main.o main.asm
Но это еще не все (к сожалению) — дело в том что этот компилятор может быть использован для различных семейств и типов микроконтроллеров и процессоров и мы до сих пор не указали ему какой же микроконтроллер собираемся использовать. Исходя из того что нам сообщает сам компилятор его устроит указание семейства cortex-m4.
Указать можно двумя путями:
- Указать при компиляции при помощи ключа «-mcpu=cortex-m4»
- Указать в тексте программы строкой «.cpu cortex-m4»
Дополнительно нужно указать систему команд которую мы будем использовать: «.thumb». Ну и еще дополнительно указывается параметр «.syntax unified» — этот параметр задает некоторые особенности при написании программы, например, разрешает перед числами не ставить префикс «#», иногда не писать вручную некоторые инструкции ассемблера (IT) — компилятор это будет делать за нас (в случае необходимости) и так далее (в этом будем разбираться позже). Почитать дополнительно об этом можно здесь.
Получаем следующую программу:
@GNU AS – просто комментарий в котором указываем компилятор (для себя)
@ Настройки для компилятора:
.syntax unified
.thumb @ тип используемых инструкций Thumb
.cpu cortex-m4 @ семейство микроконтроллера
@ Наша программа:
@ Таблица указателей перехода которая должная быть размещена с адреса 0x08000000
.word 0x20020000 @ Стек
.word Reset+1 @ Адрес перехода при сбросе.
@ Внимание! Для корректности работы необходимо к адресу
@ прибавить "1" - это показывает процессору что команда
@ по адресу перехода будет в формате Thumb (а не ARM),
@ если этого не сделать, то микроконтроллер
@ будет уходить в ошибку (в прерывание Hard Fault)
@ Наша программа (должна быть размещена после таблицы указателей переходов)
Reset: B Reset
Теперь, запустив компилятор командой:
bin/arm-none-eabi-as.exe -o main.o main.asm
мы увидим что в папке появился файл main.o — таким образом компиляция прошла успешно, объектный файл создан.
Небольшое отступление:
Для удобства я поместил все программы пакета разработчика в отдельный каталог bin в папке проекта (объем всего около 5 мб), а сам запуск компилятора делаю из .bat файла make_project.bat, поскольку компилятор оставляет сообщения об ошибках в консоле — удобнее всего запускать bat файл из FAR.
Для полноты привожу содержимое файла make_project.bat на данном этапе:
CLS
:: Компиляция
bin\arm-none-eabi-as.exe -o main.o main.asm
Теперь из файла main.o при помощи программы-линковщика нужно сделать файл прошивки. Для программы-линковщика необходим файл настройки в котором собственно говоря и будет описано какую часть нашей программы куда нужно поместить.
Здесь могу рекомендовать почитать статью на этом же ресурсе habrahabr.ru/post/191058 — там можно найти один из примеров карты линковщика, есть и другие ресурсы где можно посмотреть какими бывают файлы линковщика, но к сожалению простых решений нигде не предлагается.
Один из самых простых файлов линковщика приведенный по ссылке выше выглядит так:
MEMORY {
rom(RX) : ORIGIN = 0x00000000, LENGTH = 0x8000
ram(WAIL) : ORIGIN = 0x10000000, LENGTH = 0x2000
} ENTRY(public_function)
SECTIONS {
.text : { *(.text) } > rom
_data_start = .;
.data : { *(.data) } > ram AT> rom
_bss_start = .;
.bss : { *(.bss) } > ram
_bss_end = .;
}
Для того чтобы понимать что, как и зачем, давайте попробуем составить свою карту линковщика с нуля, используя данную как подсказку (вместе со статьей автора ее написавшего, ну и подглядывайте в интернет — там море информации на эту тему).
ИТАК, первое и самое заметное: карта линковщика состоит как бы из двух больших секций MEMORY и SECTIONS
В первом блоке (MEMORY) указывается какая память установлена в микроконтроллере, в каких адресах, и какого размера. Адреса и размеры SRAM мы уже выясняли, а вот для FLASH памяти эту информацию нужно поискать в том же «RM0090 Reference manual» или просто вспомнить сколько flash памяти у вашего микроконтроллера (например по полному наименованию микроконтроллера).
После указания имени типа памяти (произвольно, кто то пишет ROM / RAM, кто то MEMORY, я пишу FLASH / SRAM) указываются режимы доступа к памяти: для FLASH памяти R — чтение, X-исполнение. И далее начальный адрес памяти в адресном пространстве и размер.
Должно получиться примерно так:
MEMORY {
FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1024kb
}
Далее идет определение областей или секций. Как вы могли уже прочитать по ссылке выше — основные секции прошивки имеют имена:
- .text – исполняемый код (размещается во FLASH)
- .data – переменные (размещаются в SRAM)
- .rodata – константы (размещаются во FLASH)
- .bss – переменные с нулевым значением при старте (SRAM)
Имена этих секций в прошивке «забиты» в самих утилитах компиляции, поэтому произвольно их менять не получится. Количество секций в исходных файлах задается вами самостоятельно, и для языков высокого уровня сильно зависит от компилятора языка программы, для Си компиляторов количество секций нередко доходит до 10… Секции исходных файлов всегда приводятся к секциям прошивки.
Конструкция блока SECTIONS в общем виде строиться следующим образом:
.text : { /* <--- Тип секции: исполняемый код, в прошивке */
*(.text); /*<-- Тип секции в main.asm */
*(.code); /*<-- Тип секции в main.asm */
*(.flashdata); /*<-- Тип секции в main.asm */
*(.datar); /*<-- Тип секции в main.asm */
} > FLASH /* Размещать в памяти FLASH */
Еще раз резюмирую: у нас есть два разных набора секций: один набор секций — это секции описанные в исходных файлах нашей программы, другой набор секций — это секции которые будут записаны в прошивке. Соответственно сначала объявляем секцию прошивки, и внутри указываем какие секции описанные в исходных файлов в нее входят. Секции прошивки называются так как называются, секции в исходных файлах — называем мы сами (в определенных пределах к сожалению, но об этом позже).
Пока вам (и мне) повезло, у нас только одна секция прошивки и одна секция исходного файла — исполняемый код.
Получаем следующее:
/* STM32F40x, flash 1 mb, sram 192 kb, bkpsram 4 kb */
MEMORY
{
/* FLASH - Программная flash память */
FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1024K
}
SECTIONS
{
.text : { /* Секция прошивки */
*(.text); /* Секция исходников */
} > FLASH
}
Примечания пишутся внутри скобок /* */, все примечания пишутся «для себя» и «чтобы не забыть». Этот текст карты линковщика я поместил в файл stm32f40_map.ld.
Я намеренно упростил нашу карту линковщика по максимуму чтобы вы могли понять идею ее написания. Несмотря на упрощение (которое наверняка мне не простят «гуру» программирования и напишут в комментариях, что так писать нельзя) эта карта линковщика для нашего примера полностью работоспособна (а иначе не стоило и мучаться с ее написанием)!
Теперь, коль у нас появились секции — совершенно очевидным становиться необходимость указания в прошивке что и к какой секции относиться! Делается это при помощи команды .section, секция у нас одна .text, правим наш main.asm.
Секция «вроде бы» должна заканчиваться командой «.end», я еще не выяснил до конца нужна это команда на самом деле или нет и поэтому добавил в конце.
Файл исходного текста нашей программы теперь выглядит вот так:
@GNU AS
@ Настройки компилятора arm-none-eabi-as.exe
.syntax unified
.thumb @ тип используемых инструкций Thumb
.cpu cortex-m4 @ процессор
@ Наша программа
.section .text
.word 0x20020000 @ Стек
.word Reset+1 @ Адрес перехода при сбросе.
@ Внимание! Для корректности работы необходимо к адресу
@ прибавить "1" - это показывает процессору что команда
@ по адресу перехода будет в формате Thumb (а не ARM),
@ если этого не сделать то младшие линейки (F0, F1)
@ будут уходить в ошибку (в прерывание Hard Fault)
@ Наша программа (должна быть размещена после таблицы указателей переходов)
Reset: B Reset
.end
Еще одно отступление: для нашего примера можно не указывать секции вовсе! Дело в том, что если секции не указаны, то при компиляции и последующей линковке будет выбрана «самая подходящая»… Конечно не стоит надеяться на компилятор и лучше самостоятельно указывать что и где должно быть размещено.
Линковщик это утилита arm-none-eabi-ld.exe, при попытке запросить помощь запуская линковщик с ключем -–help выходит не маленькая «портянка», но мы не будем строить монстроидальный запуск, нам нужно только сделать линковку полученной после ассемблера прошивки.
Используем ключ -T для указания файла карты памяти, и ключ -o для задания имени файла-прошивки, это будет файл с расширением .elf.
Чтобы руками не набирать правим наш make_project.bat:
CLS
:: Компиляция
bin\arm-none-eabi-as.exe -o main.o main.asm
:: Линковка
bin\arm-none-eabi-ld.exe -T stm32f40_map.ld -o main.elf main.o
После запуска у нас появляется файл с расширением .elf.
Теперь нужно сделать остановку и поразмыслить — а что же мы получили? Ну да, ассемблер «что то» скомпилировал (файл main.o), а линковщик на основании этого сделал «какую-то» прошивку (файл main.elf), но как нам проверить, что у нас получилось?
По логике действий мы должны залить прошивку в микроконтроллер и посмотреть на результат… И тут мы вспоминаем что прошивка то у нас ничего не делает — поэтому никакого мигающего светодиода не будет! Вообще ничего не будет! Внешне, по пинам микроконтроллера мы не узнаем работает он, или прошивка оказалась ошибочной — и он завис.
По нашей программе проверку работы микроконтроллера пока сделать нельзя. А что можно? — с файлом прошивки .elf — мы даже не можем посмотреть что и куда линковщик расположил (посмотреть хотелось бы не утилитами, а чем нить вроде того же FAR, чтобы уж «точно и железно», перед тем как мы пойдем дальше нужно быть уверенными что на текущий момент все сделано верно).
Значит нужно сделать из прошивки .elf прошивку в формате .bin. Бинарный формат прошивки это фактически байты которые будут загружены в контроллер.
Сделать это можно при помощи утилиты arm-none-eabi-objcopy.exe. Ключем -O мы задаем формат который мы хотим получить, допустимы 2 значения: binary и ihex. Правим наш make_project.bat:
CLS
:: Компиляция
bin\arm-none-eabi-as.exe -o main.o main.asm
:: Выполняем линковку
bin\arm-none-eabi-ld.exe -T stm32f40_def.ld -o main.elf main.o
::Выделяем из .elf файла - файл прошивки .bin
bin\arm-none-eabi-objcopy.exe -O binary main.elf output.bin
Запускаем и получаем «вожделенный» файлик output.bin. Размер файла output.bin 10 байт, в FAR можно запустить его просмотр (F3) и в этом режиме переключиться в просмотр кода (F4):
Вот теперь нам есть что анализировать:
- В .bin прошивке не указываются адреса по которым происходит «заливка» прошивки в микроконтроллер — этот параметр указывается при программировании (в нашем микроконтроллере STM32F407 начальный адрес должен быть 0x08000000)
- Мы планировали что прошивка начнется с таблицы указателей переходов, первый указатель — SP, на конец SRAM1 и SRAM2 – 0x20020000. В прошивке идут байты 00 00 02 20 — вспоминаем (или узнаем сейчас) что все данные идут в порядке «младший — старший») и переворачиваем наши байты прошивки переписывая их побайтно справа-на-лево: 20 02 00 00 — вектор SP указан верно !
- После указателя SP должен идти вектор сброса: смотрим: 09 00 00 08, переворачиваем и получаем адрес для перехода: 08 00 00 09 — тоже правильно!
- Далее идет код команды на языке ассемблера.
Я бы рад вам подсказать способ дизассемблирования команды по ее коду — но к сожалению пока такого ресурса не знаю… Поэтому доверяю тому что получилось и считаю чтосочетание E7 FE (я уже перевернул байты!) — это и есть наша строчка кода: «Reset: B Reset»
Нашел все таки документ в котором описано как кодируются команды ARM. Скачать можно по ссылке ARMv7-M Architecture Reference Manual.
Смотрим оглавление:
У нас код команды E7 FE — 16-ти битный, значит сразу идем на страницу 127, где будут определены старшие биты команд:
<img src="
Код нашей команды: 1110 0111 1111 1110, в указанном выше списке это последняя строчка «11100x Unconditional Branch, see B on page A7-207», значит идем на страницу 207.
Следовательно закодирована команда B относительным адресом 111 1111 1110. Как рассчитать адрес перехода можно понять из этого документа
у нас получается:
0x0800 0008 — 0x0800 000C= 0xFFFF FFFC (1111 1111 1111 1111 1111 1111 1111 1100) или -4
Кодируем -4 при помощи 12 бит: 1111 1111 1100. Теперь берем старшие 11 бит — и это будет поле смещения адреса:
1111 1111 1100 => 111 1111 1110
Наша команда полностью:
(код команды)=11100 (адрес)=111 1111 1110 => разобьем «по правильному»: 1110 0111 1111 1110 = E7 FE
На этом текущее повествование прекратим и подведем итоги:
- Мы нашли файлы необходимые для компиляции проектов на языке ассемблера для микроконтроллера STM32F407
- Выбрали необходимые ключи компиляции и настройки компилятора
- Начали разбираться с картой памяти линковщика, и выяснили какие ключи необходимо использовать для линковки
- Получили файл прошивки, преобразовали этот файл в более понятный нам binary
- Мы проверили правильность формирования таблицы переходов, кода программы, размещения программы в прошивке на файле формата binary
В следующей статье будем писать прошивку микроконтроллера чтобы реально проверить его работу — будем мигать светодиодом!
Часть 2: STM32F4: GNU AS: Мигаем светодиодом (Оживление)