Итак, мы создали новый проект, выполнили основные настройки, создали и подключили к проекту файл, в котором хотим написать на ассемблере какую-нибудь простенькую программу.
Что дальше? Дальше, собственно говоря, можно писать программу, используя набор команд thumb-2, поддерживаемый ядром Cortex-M3. Список и описание поддерживаемых команд можно посмотреть в документе под названием Cortex-M3 Generic User Guide (глава The Cortex-M3 Instruction Set), который можно найти на вкладке Books в менеджере проекта, в Keil uVision 5. Подробно о командах thumb-2 будет написано в одной из следующих частей этой статьи, а пока поговорим о программах для STM32 в общем.
Как и любая другая программа на ассемблере, программа для STM32 состоит из команд и псевдокоманд, которые будут транслированы непосредственно в машинные коды, а также из различных директив, которые в машинные коды не транслируются, а используются в служебных целях (разметка программы, присвоение константам символьных имён и т.
Например, разбить программу на отдельные секции позволяет специальная директива — AREA. Она имеет следующий синтаксис: AREA Section_Name {,type} {, attr} …, где:
Директива EQU наверняка всем хорошо знакома, поскольку встречается в любом ассемблере и предназначена для присвоения символьных имён различным константам, ячейкам памяти и т.
д. Она имеет следующий синтаксис: Name EQU number и сообщает компилятору, что все встречающиеся символьные обозначения Name нужно заменять на число number. Скажем, если в качестве number использовать адрес ячейки памяти, то в дальнейшем к этой ячейке можно будет обращаться не по адресу, а используя эквивалентное символьное обозначение (Name).Директива GET filename вставляет в программу текст из файла с именем filename. Это аналог директивы include в ассемблере для AVR. Её можно использовать, например, для того, чтобы вынести в отдельный файл директивы присвоения символьных имён различным регистрам. То есть мы выносим все присвоения имён в отдельный файл, а потом, чтобы в программе можно было пользоваться этими символьными именами, просто включаем этот файл в нашу программу директивой GET.
Разумеется, кроме перечисленных выше есть ещё куча всяких разных директив, полный список которых можно найти в главе Directives Reference документа Assembler User Guide, который можно найти в Keil uVision 5 по следующему пути: вкладка Books менеджера проектов -> Tools User’s Guide -> Complete User’s Guide Selection -> Assembler User Guide.
Большинство команд, псевдокоманд и директив в программе имеют следующий синтаксис:
{label} SYMBOL {expr} {,expr} {,expr} {; комментарий}
{label} — метка. Она нужна для того, чтобы можно было определить адрес следующей за этой меткой команды. Метка является необязательным элементом и используется только когда необходимо узнать адрес команды (например, чтобы выполнить переход на эту команду). Перед меткой не должно быть пробелов (то есть она должна начинаться с самой первой позиции строки), кроме того, имя метки может начинаться только с буквы.
SYMBOL — команда, псевдокоманда или директива. Команда, в отличии от метки, наоборот, должна иметь некоторый отступ от начала строки даже если перед ней нет метки.
{expr} {,expr} {,expr} — операнды (регистры, константы…)
; — разделитель. Весь текст в строке после этого разделителя воспринимается как комментарий.
Ну а теперь, как и обещал, простейшая программа:
AREA START, CODE, READONLY dcd 0x20000400 dcd Program_start ENTRY Program_start b Program_start END |
В этой программе у нас всего одна секция, которая называется START. Эта секция размещается во flash-памяти (поскольку для неё использован атрибут readonly).
Первые 4 байта этой секции содержат адрес вершины стека (в нашем случае 0x20000400), а вторые 4 байта — адрес точки входа (начало исполняемого кода). Далее следует сам код. В нашем простейшем примере исполняемый код состоит из одной единственной команды безусловного перехода на метку Program_start, то есть снова на выполнение этой же команды.
Поскольку секция во флеше всего одна, то в scatter-файле для нашей программы в качестве First_Section_Name нужно будет указать именно её имя (то есть START).
В данном случае у нас перемешаны данные и команды. Адрес вершины стека и адрес точки входа (данные) записаны с помощью директив dcd прямо в секции кода. Так писать конечно можно, но не очень красиво. Особенно, если мы будем описывать всю таблицу прерываний и исключений (которая получится достаточно длинной), а не только вектор сброса. Гораздо красивее не загромождать код лишними данными, а поместить таблицу векторов прерываний в отдельную секцию, а ещё лучше — в отдельный файл.
Аналогично, в отдельной секции или даже файле можно разместить и инициализацию стека. Мы, для примера, разместим всё в отдельных секциях:AREA STACK, NOINIT, READWRITE SPACE 0x400 ; пропускаем 400 байт Stack_top ; и ставим метку AREA RESET, DATA, READONLY dcd Stack_top ; адрес метки Stack_top dcd Program_start ; адрес метки Program_start AREA PROGRAM, CODE, READONLY ENTRY ; точка входа (начало исполняемого кода) Program_start ; метка начала программы b Program_start END |
Ну вот, та же самая программа (которая по прежнему не делает нифига полезного), но теперь выглядит намного нагляднее. В scatter-файле для этой программы нужно указать в качестве First_Section_Name имя RESET, чтобы эта секция располагалась во flash-памяти первой.
Для того, чтобы написать первую программу, которая могла бы делать хоть что-то полезное осталось изучить архитектуру контроллера, его карту памяти и методы работы с этой памятью, чем мы в дальнейшем и займёмся.
Пишем простую многопоточную программку на ARM ассемблере.
Ассемблер не плохой язык программирования. Одновременно и мощный и сложный, но не такой сложный как можно подумать. Предлагаю развенчать мифы об этом. Вспомнить наши корни и попробовать написать программу на ассемблере под
, в качестве подопытного будем использовать отладку с алиэкспрес для микроконтроллера stm32f103c8
.
Если заинтересовал — читайте дальше.
Проект будем делать в Keil. Создаем пустой проект и не добавляем ни одного пакета.
Окунёмся же сразу в пучину ассемблерных команд. И по ходу пьессы буду объяснять некоторые тонкости.
AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE 0x00000100 __initial_sp PRESERVE8 THUMB AREA RESET, DATA, READONLY __Vectors DCD __initial_sp ; Указатель вершины основного стека DCD Start ; Указатель на начало программы SPACE (12 * 4) ; Пропуск 12-и ненужных векторов DCD PendSV_Handler ; DCD SysTick_Handler ; ; тут ещё могут быть прерывания PRESERVE8 AREA |.text|, CODE, READONLY Start PROC BL INIT_MCU ; Инициализация микроконтроллера INF_LOOP B . ; Вечный цикл ENDP
Этот код ничего не делает.
Читайте сверху вниз.
Сначала размечается область для стэка в оперативной памяти, за подробностями обращайтесь к документации (ссылки оставлю в конце заметки). В области STACK
резервируются 0x100
байт памяти. __initial_sp
— это метка(указатель) на вершину стэка. Стек здесь растёт в сторону уменьшения адреса.
Следующая область — это область с данными доступными только для чтения (располагается в ПЗУ). Порядок констант должен быть именно таким и ни каким иначе. Перным словом идёт указатель на стэк, вторым указатель на точку входа в программу, затем таблица векторов прерываний.
PRESERVE8
— необходимо для выравнивания.
Далее идёт область с кодом, что и понятно исходя из описания области. В ней как раз располагаются все алгоритмы, подпрограммы и функции. И наша функция не исключения, в ней вызывается функция для инициализации микроконтроллера, а затем переходим в бесконечный цикл. Метка INF_LOOP
здесь не обвязательно, так как переход осуществляется командой B .
, она нужна будет в будущём при работе с несколькими потоками.
Разобрали простейщий код — пора приступать к более значительной задаче, будем управлять 4-я потоками, два из них будут выводить в USART1
разный текст, и два будут мигать светодиодами с разной частотой.
Сразу говорю, полный код я не дам, я лишь расскажу как всё это делается.
И так, как же будет выглядеть подпрограмма реализующая наш поток:
TASK_LED PROC ; Для BitBand под рукой всегда должны быть нолик и единичка MOV R11, #0 MOV R12, #1 ; BitBand для PC13.ODR MOV32 R0, (GPIOC_ODR & 0x00FFFFFF) * 0x20 + 0x42000000 + 13 * 4 ; Цикл с мигалкой SCL STR R12, [R0] ; Выключаем светодиод BL Delay ; Вызов функции задержки STR R11, [R0] ; Включаем светодиод BL Delay ; Вызов функции задержки B SCL BX LR ENDP
Здесь тотже самый вечный цикл. Это необходимо, что бы задача не прекращала своё выполнение, если этого не будет, то функция выполниться и вернётся по адресу записанному в LR
.
Для того, чтобы переход в функцию при переключени контекста сработал нормально необходимо понимать, что происходит при возврате из прерывания, так как именно из прерыванию будет передаваться управления задачам.
А именно, при возникновении прерывания в стек автоматически сохраняются некоторые регистры, в таком порядке: xPSR
, PC
, LR
, R12
, R3
, R2
, R1
, R0
. Следовательно стек задачи нужно стразу инициализировать нужными значениями:
FILL_STACK_TASK PROC PUSH {LR} ; Значения по-умолчанию для заполнения стэков MOV32 R1, #0x01000000 ; Значение для xPSR MOV32 R3, #0x0 ; LDR R4, =INF_LOOP ; Возврат из задачи в бесконечный цикл ; Регистры сохраняемые при возникновении прерывания STR R1, [R0, #-0x04] ! ; xPSR = 0x01000000 STR R2, [R0, #-0x04] ! ; PC = &TASK & 0xfffffffe STR R4, [R0, #-0x04] ! ; LR = INF_LOOP STR R3, [R0, #-0x04] ! ; R12 = 0 STR R3, [R0, #-0x04] ! ; R3 = 0 STR R3, [R0, #-0x04] ! ; R2 = 0 STR R3, [R0, #-0x04] ! ; R1 = 0 STR R3, [R0, #-0x04] ! ; R0 = 0 ; Дополнительные регистры тоде можно сохранить POP {PC} ENDP
Здесь в R0
хранится указатель на стек этой задачи, в ресистре R2
— указатель на процедуру задачи, с наложеной маской 0xfffffffe
(Это магия возврата из прерываний, в документации на ядро можно почитать более подробно), как уже заметили ригистр связи LR
инициализируем указателем на наш бесконечный цикл, на случай если выйдем из процедуры, если этого не сделать можно словить фатальную ошибку, а так нет, просто будем вхолостую гонять вечный цикл вместо выполнения этой задачи.
Идём дальше, мы реализовали одну из задач, реализовали процедуру инициализации стека, осталось научиться переключать контекст. Первое что приходит на ум, это переключать его в прерывании с определенным интервалом времени, пусть будет милисекунда. Системный таймер? И да и нет. Если просто переключить контекст в прерывании системного таймера, то можно также словить фатальную ошибку. Необходимо использовать специальное прерывание, которое есть в ядре ARM Cortex-M3
— PendSV
.
PendSV
прерывание должно иметь самый низкий приоритет, что бы оно гарантированно было выполнено после всех остальных прерываний. И ещё это программное прерывание, это означает что его нужно принудительно вызывать:
MOV32 R0, ICSR MOV32 R1, SCB_ICSR_PENDSVSET STR R1, [R0]
Вызвать можно из другого прерывания или нет. Но мы будем вызыватьего из прерывания системного таймера.
Переключение контекста будет выполняться по следующему алгоритму:
SP
на указатель стека первой задачи и завершаем прерывание, всё, теперь мы выполняем первую задачу до возникновения следующего прерывания переключения контекста.SP
, взять адрес следующего стека и применить его, выйти из прерывания. Теперь мы выполняем следующую задачу и так далее.Код реализующий это может выглядить примерно так:
PendSV_Handler PROC PUSH {r4-r11} ; Сохраняем дополнительные регистры ; Сбрасываем флаг прерывания MOV32 R0, ICSR MOV32 R1, SCB_ICSR_PENDSVCLR STR R1, [R0] ; Получаем ячейку с нужным стэком LDR R0, =Task_Runing LDR R1, [R0] ; Номер текущего стэка AND R1, R1, #0x3 ; Очищаем лишнее по маске ; Получаем указатель на указатель на текущий стек LDR R2, =Task_SP ; Указатель на массив указателей стеков MOV R12, #4 ; Количество байт в слове MLA R12, R12, R1, R2 ; ADR = 4* N + ADRSP[] LDR R3, [R12] ; Адрес стека для нужной задачи ; Проверяем первый ли это запуск планировщика LDR R4, =Task_IsRuning LDR R5, [R4] CMP R5, #1 BEQ IS_NOFIRST ; Это первый запуск планировщика MOV R5, #1 STR R5, [R4] MSR MSP, R3 ; Переключаем стэк на первую задачу B EXIT_PSV IS_NOFIRST ; Не первый раз мы здесь MRS R6, MSP ; получаем SP STR R6, [R12] ; Номер следующего стэка ADD R1, R1, #1 AND R1, R1, #0x3 STR R1, [R0] MOV R12, #4 MLA R12, R12, R1, R2 ; Адрес стека для задачи LDR R3, [R12] ; Переключаем стэк на первую задачу MSR MSP, R3 EXIT_PSV POP {r4-r11} ; Восстанавливаем дополнительные регистры BX LR ALIGN 4 ENDP PRESERVE8
Да и кстати нужно не забыть выделить память для всех наших переменных, что-то вроде этого:
;-----------------------СЕКЦИЯ СО СТЭКАМИ ЗАДАЧ---------------------------------------------------- AREA STACK_TASK, DATA, READWRITE, ALIGN=3 ; Секция со стеками задач Stack_A_Mem SPACE 0x00000100 ; Stack_A ; указатель на вершину A Stack_B_Mem SPACE 0x00000100 ; Stack_B ; указатель на вершину B Stack_C_Mem SPACE 0x00000100 ; Stack_C ; указатель на вершину C Stack_D_Mem SPACE 0x00000100 ; Stack_D ; указатель на вершину D ;-----------------------СЕКЦИЯ С ПЕРЕМЕННЫМИ------------------------------------------------------- AREA VAR, DATA, READWRITE, ALIGN=3 Task_IsRuning DCD 0 ; Номер текущей задачи Task_Runing DCD 0 ; Номер текущей задачи Task_SP DCD Stack_A, Stack_B, Stack_C, Stack_D ; Указатели стека ALIGN
Вроде это всё,если есть вопросы, пишите. Повторю ещё раз — код привезенный в этой заметке не полный проект, а лишь небольшой материал для дальнейшего изучения.
Как обычно, хорошего кодинга и поменьше багов.
Please enable JavaScript to view the comments powered by Disqus.Пока мы готовимся к конференции ST Developers Conference 2018, которая состоится 5 сентября в Санта-Кларе, штат Калифорния, а учащиеся готовятся вернуться в школу, мы запускаем новую серию сообщений в блогах, демонстрирующих, что университеты делают с нашими продуктами и технологиями, чтобы подготовить новое поколение инженеров. Увидев, что возможно и что другие делают для продвижения инноваций, мы надеемся, что те, кто сможет посетить нашу конференцию, воодушевятся новыми идеями и будут искать точные решения, а те, кто не сможет прийти, будут вдохновлены работать над новыми и интересными решениями. проекты. Чтобы начать эту новую серию сообщений в блоге, мы углубимся в курс « Встроенные системы с микроконтроллерами ARM Cortex-M на языке ассемблера и C », который преподает в Университете штата Мэн профессор Ифэн Чжу, который использует 32L476GDISCOVERY, 32L152CDISCOVERY и STM32F4DISCOVERY в качестве основных педагогических средств .
Этот курс является фантастическим дополнением к онлайн-курсу по SensorTile, который в настоящее время набирает популярность во многих университетах и побуждает студентов создавать впечатляющие проекты, способные потрясти многие дисциплины. Действительно, Знакомство со встроенными системами с помощью SensorTile не требует знания C, но дает учащимся потрясающую основу. С другой стороны, курс профессора Чжу строится на основе этих вводных концепций, чтобы выйти за рамки основ и подготовить инженеров к выдающимся достижениям и решению проблем, которые могут лишь немногие. Те, кто не может посетить его лекцию, все равно могут взять его учебник и пройти некоторые лабораторные работы на его веб-сайте. Существует также канал на YouTube, который будет освещать некоторые концепции и поможет студентам пройти через некоторые из наиболее важных аспектов курса.
Курс по встраиваемым системам на языке ассемблера может кому-то показаться нелогичным, если мы посмотрим на текущие тенденции. Рейтинг самых популярных языков программирования, составленный такими сайтами, как Redmonk, даже не включает эту низкоуровневую парадигму в топ-20, а ассемблер находится рядом с Fortran или F#. Почему язык по-прежнему важен для разработчиков, работающих над встраиваемыми системами? Как объяснил профессор Чжу:
Профессор Ифэн Чжу«Разработчикам по-прежнему приходится использовать ассемблер, когда они выполняют ручную реализацию функций, не имеющих эквивалента на C, или при работе с ядром, и другого пути просто нет. Студенты и профессионалы также получают гораздо более глубокое понимание встраиваемых систем. ST предоставляет очень хорошие слои абстракции на аппаратном уровне, которые, например, значительно облегчают использование классов USB или Bluetooth. Однако мы хотим, чтобы студенты инженерных специальностей понимали, что происходит за кулисами.
Именно поэтому курс профессора Чжу является таким прекрасным дополнением к другим подходам к нашим платформам. Знакомство со встроенными системами с помощью SensorTile может помочь разработчикам получить представление о встроенных системах на C. Наша конференция разработчиков, состоявшаяся в прошлом году, показала, что некоторые инструменты могут позволить инженерам начать писать встроенное приложение, просто используя Javascript, в то время как другие компании, такие как MicroEJ есть целые платформы для плат ST на Java. Профессор Чжу и его курс расширяют возможности, гарантируя, что разработчики могут использовать язык программирования низкого уровня для достижения своих целей с максимальной эффективностью .
Многие современные платформы и библиотеки делают машинное обучение очень доступным. Однако природа встроенных систем часто требует, чтобы разработчики вносили специальные и сложные настройки для соответствия конкретному микроконтроллеру (MCU) или пограничному корпусу. Например, инженеры могут захотеть изменить библиотеку, чтобы использовать другой тип операций, чтобы адаптироваться к конкретной архитектуре и сэкономить ресурсы, особенно если потеря точности не так важна для текущей задачи. Точно так же они могут захотеть изменить код, чтобы воспользоваться преимуществами конкретных SIMD-инструкций (Single Instruction Multiple Data), которые могут значительно ускорить вычисления. Это означает, что для того, чтобы по-настоящему насладиться революционными инновациями, разработчикам иногда приходится настраивать библиотеки на очень низком уровне, а сборка может быть секретным оружием в гонке за следующей революцией.
Лекция и учебник профессора Чжу также увлекательны тем, что они подходят к этой теме с неожиданной стороны. Понятно, что цель занятия — научить использовать ассемблер во встраиваемых системах. Однако вместо того, чтобы просто погрузиться в изучение низкоуровневого языка программирования , профессор объясняет, что студентам зачастую выгоднее начать с C, чем переходить на ассемблер 9.0006 . Дело в том, чтобы помочь студентам понять, как все работает, поэтому он выбрал нашу продукцию. Как он объясняет, ST предлагает удобные наборы, например те, что служат в его лаборатории. Настройка платформы, а затем нажатие кнопки и загорание светодиода — это немедленная награда, которая значительно улучшает процесс обучения.
Точно так же Профессор Чжу также использует наши библиотеки с открытым исходным кодом, чтобы помочь студентам реконструировать конкретные подходы и стратегии . Например, обычно запускается цикл while, который будет выполняться до тех пор, пока система не обнаружит флаг. К сожалению, если флаг никогда не появляется, цикл while является просто бесконечным циклом. Изучая библиотеки ST, студенты узнают, как написать счетчик, который положит конец оператору потока управления, если условие, которое они ищут, никогда не наступит. Изучение библиотек ST также помогает учащимся писать более портативные приложения, помимо того, что учит их находить решения типичных проблем, с которыми они сталкиваются при разработке своих встраиваемых систем.
Использование микроконтроллеров STM32 также привело к введению новых тем. Как сказал нам профессор Чжу, перед использованием наших компонентов класс не погружался в деревья часов. Однако, , благодаря нашей экосистеме и таким инструментам, как STM32CubeMX, учащиеся теперь могут экспериментировать с частотами, чтобы оптимизировать энергоэффективность своей системы . Таким образом, учителя могут бросить вызов ученикам, чтобы убедиться, что они понимают, как найти наилучшие компромиссы. Как он объясняет:
«STM32CubeMX — лучший отраслевой инструмент для визуализации и настройки дерева часов. Это действительно интуитивно понятно, и это действительно показывает студентам, как они могут использовать низкоуровневое программирование для установки регистров, которые, в свою очередь, будут регулировать частоты.
Профессор Чжу также удивил нас, когда рассказал, как он использовал STM32CubeMX в своем классе. Традиционно программное обеспечение помогает инженерам настроить свои периферийные устройства и дерево часов, а затем сгенерировать заголовочный файл на C. Однако профессор делает все немного по-другому:
«Мы используем графический интерфейс STM32CubeMX в качестве руководства для изучения дерева часов и для назначения контактов MCU для определенных функций. Таким образом, он обеспечивает необходимое визуальное руководство и может помочь предотвратить конфликт выводов. Однако студенты не создают файл C, а пишут все на ассемблере. Следовательно, STM32CubeMX работает как интересный трамплин для изучения того, как инициализировать систему с помощью языка низкого уровня».
EducationST Developers ConferenceST Technologies in Universities
Цель этой статьи — написать минималистическую программу на ассемблере ARM и доказать, что она корректно работает на отладочной плате STM32-h203.
Аппаратное оборудование:
Программные средства:
Следующие инструменты, входящие в состав GNU Binutils:
Ассемблер GNU (газ) для компиляции
GNU Linker (ld) для компоновки
GNU objcopy для преобразования из формата elf в двоичный формат
GNU objdump для проверки вывода GNU Assembler и GNU Linker
GNU nm для перечисления символов в объектных файлах, т. е. вывод GNU Assembler
OpenOCD для прошивки
Мы будем использовать версию кросс-компилятора GNU для ПК на «голом железе», указанную в предыдущем разделе. Это означает, что мы будем запускать инструменты на ПК под управлением Linux, но скомпилируем их для работы на процессоре ARM Cortex-M3 без какой-либо операционной системы.
Имя инструментов кросс-компилятора включает arm-none-eabi
, имя ассемблера GNU arm-none-eabi-как
например. Я думаю, это означает:
arm
— ARM является целью кросс-компиляции нет
— цель работает голое железо , т.е. без операционной системы eabi
— встроенный бинарный интерфейс приложенияСсылки:
sudo apt-get установить binutils-arm-none-eabi
Первый шаг — написать нашу программу на ассемблере.
Программа должна:
3
в регистре 2. 4
в регистре 3..большой палец .section isr_vector .слово 0 .слово _start + 1 .слово _nmi_handler + 1 .слово _hard_fault + 1 .слово _memory_fault + 1 .слово _bus_fault + 1 .слово _usage_fault + 1 .текст .глобальный _start _начинать: мов р2, #3 мов р3, #4 добавить r4, r2, r3 останавливаться: б стоп _дурачок: _nmi_handler: _hard_fault: _memory_fault: _bus_fault: _usage_fault: добавить r0, #1 добавить r1, #1 б _пустышка
Программа написана с использованием набора инструкций Thumb, который является набором инструкций, поддерживаемым процессорами ARM Cortex-M3, см. главу 3 в Руководстве по программированию STM32F10xxx. Давайте пройдемся по нашей программе, начиная сверху.
.thumb
— это директива ассемблера ARM, идентичная директиве ассемблера ARM .code 16
. .code 16
выбирает набор инструкций Thumb. Директива ассемблера начинается с точки .
, он не будет генерировать инструкцию машинного языка. См. раздел 3.5 Разделы документации по газу.
.section isr_vector
поместит следующий код в раздел с именем isr_vector
. См. раздел 7.78. Имя раздела в документации по газу. Мы будем использовать разделы на шаге компоновщика позже, чтобы получить наш код в правильном порядке в конечном выходном файле.
Директива .word
используется для ввода данных в выходной файл. Процессор на макетной плате STM32-h203 — это STM32F103RBT6 от STMicroelectronics. Он использует 32-битные слова. См. раздел 2.1.5 Типы данных
в Руководстве по программированию STM32F10xxx.
Мы используем директиву .word
для установки вектора сброса микроконтроллера. Вектор сброса STM32F103RBT6 определен в разделе 2.3.4 Руководства по программированию STM32F10xxx, он определяет начальные адреса для различных типов исключений. Одним из важных примеров является то, что происходит при перезагрузке системы. Начальный адрес кода считывается с адреса 0x0000 0004
. Младшие биты каждого начального адреса в векторе должны быть равны 1 , это указывает на то, что обработчики исключений реализованы с помощью кода Thumb. Первые 7 записей в векторе сброса перечислены ниже. Первая запись не является начальным адресом, вместо этого она устанавливает начальное значение указателя стека. Мы установим его равным 0, так как мы не используем стек. Указатель стека обязателен для инициализации при компиляции кода C.
Адрес Описание ======= =========== 0x0000 0000 Значение начального указателя стека (SP) 0x0000 0004 Исключение сброса 0x0000 0008 НМИ 0x0000 000C Серьезная ошибка 0x0000 0010 Ошибка управления памятью 0x0000 0014 Ошибка шины 0x0000 0018 Ошибка использования
Компоновщик GNU ( arm-eabi-none-ld
) предположит, что точка входа определена с помощью символа с именем _start
. Точка входа — это место, где находится первая инструкция для выполнения в программе. Мы определяем этот символ с помощью _start:
, но мы также должны сделать его видимым для компоновщика с помощью директивы .global
. См. раздел 7.36 в документации GNU Linker.
Инструкция mov
копирует значение операнд в регистр. mov r2, #3
переместит десятичное значение 3
в регистр 2. Операнд может быть константой или регистром. См. раздел 3.3.3 Гибкий второй операнд в Руководстве по программированию STM32F10xxx.
Инструкция add
добавляет значения r2
и r3
и сохраняет их в r4
.
стоп:
определяет метку стоп
. Метка — это символ, представляющий текущее значение счетчика местоположения. См. раздел 5.1 в документации GNU Assembler.
Инструкция b
означает ответвление. Счетчик программ переместится на позицию метки stop
в коде. Цель состоит в том, чтобы иметь бесконечный цикл после завершения нашей программы.
Мы реализуем один общий обработчик несбрасываемых исключений. Мы делаем это, определяя несколько меток в одном месте.
_манекен: _nmi_handler: _hard_fault: _memory_fault: _bus_fault: _usage_fault:
Увеличиваем регистры r0
и r1
с единицей в бесконечном цикле, если общий обработчик. Мы можем проверить значение r0
и r1
при запуске программы, чтобы обнаружить, что произошла ошибка.
Мы используем GNU Assembler для компиляции нашего ассемблерного кода. Входной файл называется add.s
, а выходной — add.o
.
arm-none-eabi-as -o add.o add.s
Выходной файл ассемблера add.o
является объектным файлом . Мы можем проверить объектный файл с помощью инструмента GNU objdump.
Опция --disassemble, -d
покажет мнемонику ассемблера для машинных инструкций в объектном файле. Это может быть интересно при компиляции C-файла. В нашем случае мы должны получить именно ту программу, которую написали на ассемблере.
$ arm-none-eabi-objdump --disassemble add.o add.o: формат файла elf32-littlearm Разборка раздела .text: 00000000 <_start>: 0: 2203 движения r2, #3 2: 2304 движения r3, #4 4: 18d4 добавляет r4, r2, r3 00000006 <стоп>: 6: e7fe б.н 6 <стоп> 00000008 <_bus_fault>: 8: 3001 добавляет r0, #1 a: 3101 добавляет r1, #1 c: e7fc b.n 8 <_bus_fault>
Опция --syms, -t
напечатает таблицу символов файла. Это похоже на то, что предоставляет инструмент нм
.
$ arm-none-eabi-objdump --syms add.o add.o: формат файла elf32-littlearm ТАБЛИЦА СИМВОЛОВ: 00000000 л д .текст 00000000 .текст 00000000 л д .данные 00000000 .данные 00000000 л д .bss 00000000 .bss 00000000 l d isr_vector 00000000 isr_vector 00000008 л. текст 00000000 _nmi_handler 00000008 л .текст 00000000 _hard_fault 00000008 л .текст 00000000 _memory_fault 00000008 л .текст 00000000 _bus_fault 00000008 л .текст 00000000 _usage_fault 00000006 л .текст 00000000 стоп 00000008 л .текст 00000000 _dummy 00000000 l d .ARM.атрибуты 00000000 .ARM.атрибуты 00000000 г .текст 00000000 _start
Первая строка в таблице символов:
00000000 л д .текст 00000000 .текст
Следует интерпретировать так:
00000000
значение символа (адрес) l
означает, что это местный символ d
означает, что это символ отладки .text
— это раздел, с которым связан символ. *UND*
здесь будет означать, что раздел не определен в текущем объектном файле. 00000000
— это выравнивание для общих символов и размер для других символов. Я не уверен в определении общего символа , но я думаю, что это означает символы, используемые в нескольких объектных файлах. .text
— имя символаДругая строка в таблице символов:
00000000 г .текст 00000000 _start
Следует интерпретировать так:
00000000
значение символа (адрес) г
означает, что это глобальный символ (пробел)
вместо d
означает, что это обычный символ .text
— это раздел, с которым связан символ. 00000000
— это выравнивание для общих символов и размер для других символов. _start
— имя символаМы можем использовать инструмент GNU nm для вывода списка символов в объектном файле.
$ arm-none-eabi-nm add.o 00000008 t _bus_fault 00000008 т _dummy 00000008 t _hard_fault 00000008 t _memory_fault 00000008 т _nmi_handler 00000000 Т_старт 00000006 т стоп 00000008 т _usage_fault
Первый столбец — значение символа.
Во втором столбце указан тип символа. Нижний регистр означает, что символ является локальным. Верхний регистр означает, что символ является глобальным. Тип символа t, T
означает, что символ находится в текстовой (кодовой) части.
В третьем и последнем столбце отображается имя символа.
Сценарий компоновщика сообщит компоновщику, в какие области памяти поместить различные участки кода. См. главу 3 о сценариях компоновщика в документации GNU Linker. В нашей программе есть разделы isr_vector
и .text
.
СЕКЦИЙ { . = 0x0; /* От 0x00000000 */ .текст: { *(isr_vector) /* Таблица векторов обработки прерываний */ *(.text) /* Код программы */ } }
SECTIONS
— это команда, описывающая структуру памяти выходного файла. См. раздел 3.3 Пример простого скрипта компоновщика и раздел 3.6 Команда SECTIONS в документации GNU Linker.
В команде SECTIONS делаем следующее.
.
установлен на 0. isr_vector
и .text
должны быть помещены в раздел вывода .text
. *(isr_vector)
означает, что мы должны выбрать isr_vector
из всех входных файлов. В нашем случае у нас есть только один входной файл, то есть add.o
. Местоположение (адрес памяти) начала выходного раздела .text
будет равно 0x0, так как это значение счетчика местоположения при определении раздела .text
. Ввод для компоновщика: add.o
и скрипт компоновщика stm32.ld
. Выход add.elf
в формате эльфа.
arm-none-eabi-ld -Tstm32.ld -o add.elf add.o
Параметр -T
указывает компоновщику заменить сценарий компоновщика по умолчанию нашим заказным stm32. ld
. См. раздел 2.1 Параметры командной строки в документации GNU Linker.
Мы можем проверить вывод компоновщика, то есть add.elf
, с помощью GNU objdump и GNU nm. Подобно тому, что мы сделали с объектным файлом ( доп.о
) выше.
Давайте сначала дизассемблируем с помощью objdump.
$ arm-none-eabi-objdump --disassemble add.elf add.elf: формат файла elf32-littlearm Разборка раздела .text: 00000000 <_start-0x1c>: 0: 00000000 .слово 0x00000000 4: 0000001d .слово 0x0000001d 8: 00000025 .слово 0x00000025 с: 00000025 .слово 0x00000025 10: 00000025 .слово 0x00000025 14: 00000025 .слово 0x00000025 18: 00000025 .слово 0x00000025 0000001c <_start>: 1c: 2203 мовс r2, #3 1e: 2304 движения r3, #4 20: 18d4 добавляет r4, r2, r3 00000022 <стоп>: 22: e7fe б.н 22 <стоп> 00000024 <_bus_fault>: 24: 3001 добавляет r0, #1 26: 3101 добавляет r1, #1 28: e7fc b. n 24 <_bus_fault>
Далее мы можем просмотреть таблицу символов с помощью nm.
$ arm-none-eabi-nm add.elf 00000024 t _bus_fault 00000024 т _dummy 00000024 t _hard_fault 00000024 t _memory_fault 00000024 т _nmi_handler 0000001c Т_старт 00000022 т стоп 00000024 т _usage_fault
Мы можем иметь числовую сортировку символов по их адресам, используя опцию --numeric-sort, -v
.
$ arm-none-eabi-nm --numeric-sort add.elf 0000001c Т_старт 00000022 т стоп 00000024 t _bus_fault 00000024 т _dummy 00000024 t _hard_fault 00000024 t _memory_fault 00000024 т _nmi_handler 00000024 т _usage_fault
Выходной формат компоновщика — эльф. Мы можем конвертировать из эльфа в бинарник, используя GNU objcopy.
$ arm-none-eabi-objcopy -O двоичный файл add.elf add.bin
Теперь мы можем создать makefile
со всеми командами сборки.
все: add. s stm32.ld очистить @echo "Бегущая цель все" arm-none-eabi-as -o add.o add.s arm-none-eabi-ld -Tstm32.ld -o add.elf add.o arm-none-eabi-objcopy -O двоичный файл add.elf add.bin print_symbols: все @echo "Выполняемая цель print_symbols" arm-none-eabi-nm --numeric-sort add.elf чистый: @echo "Бегущая цель чиста" гм -ф *.о гм -ф *.эльф м-ф *.bin
См. Использование OpenOCD для прошивки ARM Cortex M3 для получения информации о том, как прошивать.
Запустите сервер openocd в одном командном окне
$ openocd -f openocd.cfg
Подключиться к серверу openocd с помощью telnet в другом командном окне
$ телнет локальный хост 4444
Остановить выполнение цели, если она запущена
Стереть содержимое на флэш-памяти
stm32f1x mass_erase 0
Flash add. bin
flash write_bank 0 add.bin 0
Выполнить программу
сброс запуска
Убедитесь, что для r2
установлено значение 3, для r3
— 4, а для r4
— 7
остановка рег
Запустите программу add.bin в соответствии с предыдущим разделом
Остановить выполнение в бесконечном цикле по адресу 0x00000022
> остановка tm32f1x.cpu: целевое состояние: остановлено цель остановлена из-за запроса отладки, текущий режим: Thread xPSR: 0x01000000 ПК: 0x00000022 msp: 00000000
Установка значений регистра
регистр 2 0 рег 3 0 рег 4 0
Возобновить выполнение, чтобы значения регистров были установлены
резюме
Снова остановить выполнение и проверить, что r2, r3 и r4 равны нулю
остановка рег
Запустить программу еще раз
сброс запуска
Остановить выполнение
Убедитесь, что для r2
установлено значение 3, для r3
— 4, а для r4
— 7
рег. ==== arm v7m регистрирует (0) г0 (/32): 0x00000020 (1) г1 (/32): 0x00000000 (2) г2 (/32): 0x00000003 (3) г3 (/32): 0x00000004 (4) г4 (/32): 0x00000007 (5) г5 (/32): 0x2000006E (6) г6 (/32): 0x00000020 (7) г7 (/32): 0x00000014 (8) r8 (/32): 0x37FEFFFE (9) r9 (/32): 0xFFEDFFFC (10) r10 (/32): 0xB3AA944C (11) r11 (/32): 0x88CAD384 (12) r12 (/32): 0xFBF8FFFF (13) сп (/32): 0x00000000 (14) лр (/32): 0xFFFFFFFF (15) ПК (/32): 0x00000022 (16) xPSR (/32): 0x01000000 (17) МСП (/32): 0x00000000 (18) псп (/32): 0xD080DE44 (19) примаска (/1): 0x00 (20) basepri (/8): 0x00 (21) маска ошибки (/1): 0x00 (22) контроль (/2): 0x00
Мы доказали, что наша минималистичная программа работает правильно, проверив значения регистров.
В этой статье я несколько раз ссылался на Руководство по программированию STM32F10xxx.
При написании этой статьи я использовал следующие статьи в дополнение к документации GNU binutils, на которую я ссылался выше: