8-900-374-94-44
[email protected]
Slide Image
Меню

Программирование stm32 на ассемблере: Программирование на ассемблере (Часть 1) / Хабр

Содержание

Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 2. Основы ассемблера, структура и синтаксис программы. Простейшая программа

Итак, мы создали новый проект, выполнили основные настройки, создали и подключили к проекту файл, в котором хотим написать на ассемблере какую-нибудь простенькую программу.

Что дальше? Дальше, собственно говоря, можно писать программу, используя набор команд 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} …, где:

  1. Section_name — имя секции.
  2. type — тип секции. Для секции, содержащей данные нужно указывать тип DATA, а для секции, содержащей команды — тип CODE.
  3. attr — дополнительные атрибуты. Например, атрибуты readonly или readwrite указывают в какой памяти должна размещаться секция, атрибут align=0..31 указывает каким образом секция должна быть выровнена в памяти, атрибут noinit используется для выделения областей памяти, которые не нужно инициализировать или инициализирующиеся нулями (при использовании этого атрибута можно не указывать тип секции, поскольку он может использоваться только для секций данных).

Директива 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-памяти первой.

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

  1. Часть 1. Установка MDK, создание проекта, основы Keil uVision
  2. Часть 2. Основы ассемблера, структура и синтаксис программы. Простейшая программа
  3. Часть 3. Карта памяти контроллеров STM32, доступ к отдельным битам памяти
  4. Часть 4. Регистры, старт и режимы работы контроллеров STM32
  5. Часть 5. Как залить прошивку в контроллер
  6. Часть 6. Настройка системы тактирования
  7. Часть 7. Работа с портами ввода-вывода
  8. Часть 8. Процедуры на ассемблере для STM32
  9. Часть 9. Система прерываний
  10. Часть 10. CMSIS, использование стандартных библиотек и функций
  11. Часть 11. Подключение и использование драйверов из пакета StdPeriph
  12. Часть 12. Работа с модулями USART и UART.
  13. Часть 13. Работа с модулями ADC
  14. Часть 14. Использование DMA
  15. Часть 15. Таймеры. Глава 1 — Введение. Простейшие таймеры
  16. Часть 15. Таймеры. Глава 2 — Таймеры общего назначения TIM9 — TIM14
  17. Часть 15. Таймеры. Глава 3 — Таймеры общего назначения TIM2 — TIM5
  18. Часть 15. Таймеры. Глава 4 — Продвинутые таймеры TIM1, TIM8
  19. Часть 16. Создание устройства USB HID в Keil uVision при помощи библиотечного компонента USB
  20. Приложение 1. Набор инструкций THUMB-2 и особенности их использования
  21. Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103
  22. Приложение 3. Драйвера и функции библиотеки StdPeriph

Немного про ARM ассемблер. Пишем многопоточную программу.

Пишем простую многопоточную программку на ARM ассемблере.

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

ARM Cortex-M3, в качестве подопытного будем использовать отладку с алиэкспрес для микроконтроллера 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-M3PendSV.

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
Литература:
  1. RM0008 Reference manual, — Описание микроконтроллера
  2. PM0056 Programming manual — Описание ядра Cortex®-M3 и его команд.
  3. ARM and Thumb instruction
PS

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

Как обычно, хорошего кодинга и поменьше багов.

Please enable JavaScript to view the comments powered by Disqus.

Программирование встроенной системы STM32 на… сборке?!

Пока мы готовимся к конференции 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. Профессор Чжу и его курс расширяют возможности, гарантируя, что разработчики могут использовать язык программирования низкого уровня для достижения своих целей с максимальной эффективностью .

Сборка на STM32: секретное оружие машинного обучения
Учебник для курса Встроенные системы с микроконтроллерами ARM Cortex-M
на языке ассемблера и C встроенные системы, особенно в случае машинного обучения. Действительно, грядут новые датчики с интегрированными деревьями решений, и System-on-Chips с нейронной сетью уже на подходе. Профессор Уильям Кайзер из Калифорнийского университета в Лос-Анджелесе недавно рассказал, как встроенные системы, такие как SensorTile, могут помочь в обучении приложений и снабжать инженеров данными в течение многих лет. Однако, как объяснил профессор Чжу, вычислительная мощность и ресурсы памяти на этих платформах весьма ограничены. Следовательно, низкоуровневые языки программирования предлагают эффективность и оптимизацию, чтобы команды могли использовать алгоритмы обучения и логического вывода, уравновешивая потребность в производительности и присущие встроенные системы ограничения .

Многие современные платформы и библиотеки делают машинное обучение очень доступным. Однако природа встроенных систем часто требует, чтобы разработчики вносили специальные и сложные настройки для соответствия конкретному микроконтроллеру (MCU) или пограничному корпусу. Например, инженеры могут захотеть изменить библиотеку, чтобы использовать другой тип операций, чтобы адаптироваться к конкретной архитектуре и сэкономить ресурсы, особенно если потеря точности не так важна для текущей задачи. Точно так же они могут захотеть изменить код, чтобы воспользоваться преимуществами конкретных SIMD-инструкций (Single Instruction Multiple Data), которые могут значительно ускорить вычисления. Это означает, что для того, чтобы по-настоящему насладиться революционными инновациями, разработчикам иногда приходится настраивать библиотеки на очень низком уровне, а сборка может быть секретным оружием в гонке за следующей революцией.

Реверс-инжиниринг ST
Представление дерева часов в STM32CubeMX

Лекция и учебник профессора Чжу также увлекательны тем, что они подходят к этой теме с неожиданной стороны. Понятно, что цель занятия — научить использовать ассемблер во встраиваемых системах. Однако вместо того, чтобы просто погрузиться в изучение низкоуровневого языка программирования , профессор объясняет, что студентам зачастую выгоднее начать с C, чем переходить на ассемблер 9.0006 . Дело в том, чтобы помочь студентам понять, как все работает, поэтому он выбрал нашу продукцию. Как он объясняет, ST предлагает удобные наборы, например те, что служат в его лаборатории. Настройка платформы, а затем нажатие кнопки и загорание светодиода — это немедленная награда, которая значительно улучшает процесс обучения.

Точно так же Профессор Чжу также использует наши библиотеки с открытым исходным кодом, чтобы помочь студентам реконструировать конкретные подходы и стратегии . Например, обычно запускается цикл while, который будет выполняться до тех пор, пока система не обнаружит флаг. К сожалению, если флаг никогда не появляется, цикл while является просто бесконечным циклом. Изучая библиотеки ST, студенты узнают, как написать счетчик, который положит конец оператору потока управления, если условие, которое они ищут, никогда не наступит. Изучение библиотек ST также помогает учащимся писать более портативные приложения, помимо того, что учит их находить решения типичных проблем, с которыми они сталкиваются при разработке своих встраиваемых систем.

Лучший инструмент для обучения деревьям часов

Использование микроконтроллеров STM32 также привело к введению новых тем. Как сказал нам профессор Чжу, перед использованием наших компонентов класс не погружался в деревья часов. Однако, , благодаря нашей экосистеме и таким инструментам, как STM32CubeMX, учащиеся теперь могут экспериментировать с частотами, чтобы оптимизировать энергоэффективность своей системы . Таким образом, учителя могут бросить вызов ученикам, чтобы убедиться, что они понимают, как найти наилучшие компромиссы. Как он объясняет:

«STM32CubeMX — лучший отраслевой инструмент для визуализации и настройки дерева часов. Это действительно интуитивно понятно, и это действительно показывает студентам, как они могут использовать низкоуровневое программирование для установки регистров, которые, в свою очередь, будут регулировать частоты.

Профессор Чжу также удивил нас, когда рассказал, как он использовал STM32CubeMX в своем классе. Традиционно программное обеспечение помогает инженерам настроить свои периферийные устройства и дерево часов, а затем сгенерировать заголовочный файл на C. Однако профессор делает все немного по-другому:

«Мы используем графический интерфейс STM32CubeMX в качестве руководства для изучения дерева часов и для назначения контактов MCU для определенных функций. Таким образом, он обеспечивает необходимое визуальное руководство и может помочь предотвратить конфликт выводов. Однако студенты не создают файл C, а пишут все на ассемблере. Следовательно, STM32CubeMX работает как интересный трамплин для изучения того, как инициализировать систему с помощью языка низкого уровня».

  • Узнайте больше о встроенных системах с микроконтроллерами ARM Cortex-M на языке ассемблера и C
  • Узнайте больше о нашей конференции ST Developers Conference 2018

EducationST Developers ConferenceST Technologies in Universities

Запуск минималистичной программы, написанной на ассемблере, на макетной плате STM32-h203

Введение

Цель этой статьи — написать минималистическую программу на ассемблере 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 — встроенный бинарный интерфейс приложения

Ссылки:

  • GNU ARM Embedded Toolchain на сайте разработчиков ARM

Как установить GNU binutils для ARM

 sudo apt-get установить binutils-arm-none-eabi
 

Код ассемблера

Первый шаг — написать нашу программу на ассемблере.

Программа должна:

  • Сохранение значения 3 в регистре 2.
  • Сохранить значение 4 в регистре 3.
  • Сложить регистр 2 и регистр 3, сохранить результат в регистре 4.
    .большой палец
    .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
 

Как проверить вывод ассемблера с помощью objdump

Выходной файл ассемблера 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 — имя символа

Как распечатать таблицу символов в объектном файле с помощью инструмента nm

Мы можем использовать инструмент 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 делаем следующее.

  1. Счетчик местоположения , обозначенный точкой . установлен на 0.
  2. Разделы ввода 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.

Как проверить вывод компоновщика с помощью objdump и nm

Мы можем проверить вывод компоновщика, то есть 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

Теперь мы можем создать 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

См. Использование OpenOCD для прошивки ARM Cortex M3 для получения информации о том, как прошивать.

Прошить add.bin и запустить его

  1. Запустите сервер openocd в одном командном окне

     $ openocd -f openocd.cfg
     
  2. Подключиться к серверу openocd с помощью telnet в другом командном окне

     $ телнет локальный хост 4444
     
  3. Остановить выполнение цели, если она запущена

  4. Стереть содержимое на флэш-памяти

     stm32f1x mass_erase 0
     
  5. Flash add. bin

     flash write_bank 0 add.bin 0
     
  6. Выполнить программу

     сброс запуска
     
  7. Убедитесь, что для r2 установлено значение 3, для r3 — 4, а для r4 — 7

     остановка
     рег
     

Повторный запуск программы после установки значений регистров вручную на нули

  1. Запустите программу add.bin в соответствии с предыдущим разделом

  2. Остановить выполнение в бесконечном цикле по адресу 0x00000022

     > остановка
     tm32f1x.cpu: целевое состояние: остановлено
     цель остановлена ​​из-за запроса отладки, текущий режим: Thread
     xPSR: 0x01000000 ПК: 0x00000022 msp: 00000000
     
  3. Установка значений регистра

     регистр 2 0
     рег 3 0
     рег 4 0
     
  4. Возобновить выполнение, чтобы значения регистров были установлены

     резюме
     
  5. Снова остановить выполнение и проверить, что r2, r3 и r4 равны нулю

     остановка
     рег
     
  6. Запустить программу еще раз

     сброс запуска
     
  7. Остановить выполнение

  8. Убедитесь, что для 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
     
  9. Мы доказали, что наша минималистичная программа работает правильно, проверив значения регистров.

Каталожные номера

В этой статье я несколько раз ссылался на Руководство по программированию STM32F10xxx.

Ссылки

При написании этой статьи я использовал следующие статьи в дополнение к документации GNU binutils, на которую я ссылался выше: