Мигаем светодиодом в STM32 на ассемблере. STM32F4. Светодиоды на ассемблере

Некотрое время назад захотелось мне освоить ассемблер и после прочтения соответствующей литературы пришло время практики. Собственно о ней и пойдет дальше речь. Первое время я практиковался на Arduino Uno (Atmega328p), теперь решил двигаться дальше и взялся за STM32. В руки ко мне попала STM32F103C8 собственно на ней и будут проходить дальнейшие эксперименты.

Инструменты

Я использовал следующие инструменты:

  • Notepad++ - для написания кода
  • GNU Assembler - компилятор
  • STM32 ST-LINK Utility + ST-LINK V2 - для прошивки кода на микроконтроллер и отладки

Начало

Основная цель программирования на ассемблере для меня - это обучение. Так как никогда не знаешь где наткнешься на очередную интересную проблему, то было решено писать все с нуля. Первостепенной задачей было понять как работает вектор прерываний. В отличие от Atmega в STM32 вектор прерываний не содержит инструкций перехода:

Jmp main

В нем прописываются конкретные адреса и во время прерывания процессор сам подставляет прописанный в векторе адрес в PC регистр. Вот пример моего вектора прерываний:

Org 0x00000000 SP: .word STACKINIT RESET: .word main NMI_HANDLER: .word nmi_fault HARD_FAULT: .word hard_fault MEMORY_FAULT: .word memory_fault BUS_FAULT: .word bus_fault USAGE_FAULT: .word usage_fault .org 0x000000B0 TIMER2_INTERRUPT: .word timer2_interupt + 1

Хочу обратить внимание читателя, что первой строкой идет не reset вектор, а значения которым будет инициализироваться стэк. Сразу следом за ним идет reset вектор после которого следуют 5 обязательных векторов прерываний (NMI_HANDLER – USAGE_FAULT).

Разработка

Первое на чем я застрял был синтаксис ARM ассемблера. Еще во время изучения вектора прерываний я встретил упоминания того, что у ARM существует 2 вида инструкций Thumb и не Thumb. И что Cortex-M3 (STM32F103C8 именно Cortex-M3) поддерживает только набор Thumb инструкций. Я писал инструкции строго по документации, но ассемблер на них почемуто ругался.

unshifted register required

Выяснилось, что в начале программы надо поставить

это говорит ассемблеру что можно использовать Thumb и не Thumb инструкции одновременно.

Следующее с чем я столкнулся были отключенные по умолчанию GPOI порты. Чтобы они заработали, кроме всего прочего надо выставить соответствующие значения в RCC (reset and clock control) регистрах. Я использовал PORT C, его можно включить установив бит 4 (нумерация битов идет с нуля) в RCC_APB2ENR (peripherial clock enable register 2).

Дальше мигание светодиодом. Прежде всего, как и в Arduino надо установить пин за запись. Это делается через GPIOx_CRL (control register low) или GPIOx_CRH (control register high). Тут надо отменить что за каждый пин отвечают 4 бита в одном из этих регистров (регистры 32 битные). 2 бита (MODEy) определяют максимальную скорость передачи данных и 2 бита (CNF) конфигурацию пина. Я использовал PORT C пин 14, для этого выставил в GPIOx_CRH регистре биты = 10 и биты = 00.

Чтобы диод горел надо в GPIOx_ODR (output data register) выставить соответствующий бит. В моем случае бит 14. На этом можно было бы закончить этот простой пример, сделав функцию задержки и поставив это все в цикл, но я этого сделать не смог. Я решил настроить прерывания по таймеру… Как выяснилось это было зря, прежде всего потому, что таймеры слишком быстрые для такого рода задач.

Не стану подробно описывать настройку таймера, кому интересно код есть на

Счетчик - 32 битная переменная, которая должна была находиться в SRAM. И тут меня ждали очередные грабли. Когда я программировал на Atmega чтобы поместить переменную в SRAM я через.org задавал адрес начала памяти, куда собственно помещался блок с данными. Сейчас, немного почитав об инициализации памяти, я не уверен, что это было правильно, но это работало. И я решил провернуть тоже самое с STM32. Адрес начала памяти в STM32F103C8 – 0x20000000. И когда я сделал.org по этому адресу, то получил бинарник на 512мб. Это отправило меня на пару вечеров «курить мануалы». Я все еще не на 100% понимаю как это работает, но на сколько я понял.data секция помещает значения, которыми надо инициализировать переменные в исполняемый файл, но во время выполнения программист должен сам инициализировать значения переменных в памяти. Поправьте меня пожалуйста если я не прав. В итоге я создал переменную так:

Section .bss .offset 0x20000000 flash_counter: .word

Инициализировал ее в начале main функции и LED замигал. Надеюсь эта статья комуто поможет. Если есть вопросы буду рад на них ответить.

14 августа 2017 в 11:33

Мигаем светодиодом в STM32 на ассемблере

  • Компьютерное железо

Некотрое время назад захотелось мне освоить ассемблер и после прочтения соответствующей литературы пришло время практики. Собственно о ней и пойдет дальше речь. Первое время я практиковался на Arduino Uno (Atmega328p), теперь решил двигаться дальше и взялся за STM32. В руки ко мне попала STM32F103C8 собственно на ней и будут проходить дальнейшие эксперименты.

Инструменты

Я использовал следующие инструменты:
  • Notepad++ - для написания кода
  • GNU Assembler - компилятор
  • STM32 ST-LINK Utility + ST-LINK V2 - для прошивки кода на микроконтроллер и отладки

Начало

Основная цель программирования на ассемблере для меня - это обучение. Так как никогда не знаешь где наткнешься на очередную интересную проблему, то было решено писать все с нуля. Первостепенной задачей было понять как работает вектор прерываний. В отличие от Atmega в STM32 вектор прерываний не содержит инструкций перехода:

Jmp main
В нем прописываются конкретные адреса и во время прерывания процессор сам подставляет прописанный в векторе адрес в PC регистр. Вот пример моего вектора прерываний:

Org 0x00000000 SP: .word STACKINIT RESET: .word main NMI_HANDLER: .word nmi_fault HARD_FAULT: .word hard_fault MEMORY_FAULT: .word memory_fault BUS_FAULT: .word bus_fault USAGE_FAULT: .word usage_fault .org 0x000000B0 TIMER2_INTERRUPT: .word timer2_interupt + 1
Хочу обратить внимание читателя, что первой строкой идет не reset вектор, а значения которым будет инициализироваться стэк. Сразу следом за ним идет reset вектор после которого следуют 5 обязательных векторов прерываний (NMI_HANDLER – USAGE_FAULT).

Разработка

Первое на чем я застрял был синтаксис ARM ассемблера. Еще во время изучения вектора прерываний я встретил упоминания того, что у ARM существует 2 вида инструкций Thumb и не Thumb. И что Cortex-M3 (STM32F103C8 именно Cortex-M3) поддерживает только набор Thumb инструкций. Я писал инструкции строго по документации, но ассемблер на них почемуто ругался. Выяснилось, что в начале программы надо поставить это говорит ассемблеру что можно использовать Thumb и не Thumb инструкции одновременно.

Следующее с чем я столкнулся были отключенные по умолчанию GPOI порты. Чтобы они заработали, кроме всего прочего надо выставить соответствующие значения в RCC (reset and clock control) регистрах. Я использовал PORT C, его можно включить установив бит 4 (нумерация битов идет с нуля) в RCC_APB2ENR (peripherial clock enable register 2).

Дальше мигание светодиодом. Прежде всего, как и в Arduino надо установить пин за запись. Это делается через GPIOx_CRL (control register low) или GPIOx_CRH (control register high). Тут надо отменить что за каждый пин отвечают 4 бита в одном из этих регистров (регистры 32 битные). 2 бита (MODEy) определяют максимальную скорость передачи данных и 2 бита (CNF) конфигурацию пина. Я использовал PORT C пин 14, для этого выставил в GPIOx_CRH регистре биты = 10 и биты = 00.

Чтобы диод горел надо в GPIOx_ODR (output data register) выставить соответствующий бит. В моем случае бит 14. На этом можно было бы закончить этот простой пример, сделав функцию задержки и поставив это все в цикл, но я этого сделать не смог. Я решил настроить прерывания по таймеру… Как выяснилось это было зря, прежде всего потому, что таймеры слишком быстрые для такого рода задач.

Не стану подробно описывать настройку таймера, кому интересно код есть на Github . Задумка была проста, в цикле отправлять процессор в Idle, по таймеру выходить из Idle зажигать/гасить светодиод и опять в Idle. Но таймер срабатывал гораздо быстрее чем я успевал сделать все вышеуказанное из-за чего пришлось ввести дополнительный счетчик.

Счетчик - 32 битная переменная, которая должна была находиться в SRAM. И тут меня ждали очередные грабли. Когда я программировал на Atmega чтобы поместить переменную в SRAM я через.org задавал адрес начала памяти, куда собственно помещался блок с данными. Сейчас, немного почитав об инициализации памяти, я не уверен, что это было правильно, но это работало. И я решил провернуть тоже самое с STM32. Адрес начала памяти в STM32F103C8 – 0x20000000. И когда я сделал.org по этому адресу, то получил бинарник на 512мб. Это отправило меня на пару вечеров «курить мануалы». Я все еще не на 100% понимаю как это работает, но на сколько я понял.data секция помещает значения, которыми надо инициализировать переменные в исполняемый файл, но во время выполнения программист должен сам инициализировать значения переменных в памяти. Поправьте меня пожалуйста если я не прав. В итоге я создал переменную так:

Section .bss .offset 0x20000000 flash_counter: .word
Инициализировал ее в начале main функции и LED замигал. Надеюсь эта статья комуто поможет. Если есть вопросы буду рад на них ответить.

Кто-то любит пирожки, а кто-то - нет.

Пустой код, конечно, хорошо, но он ничего полезного не делает.

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


Для этого нужно знать несколько команд.
Во-первых, загрузка большого числа (32 бита) в регистр.
LDR Rn, =число.

Rn - название регистра (R0, R1, ...), число - то, что хотим загрузить.
Например:
LDR R0, =0x40023830 ; Адрес регистра RCC_AHB1ENR

Для красоты и читабельности число можно заменить обозначением:
; Загрузим в регистр R0 адрес регистра RCC_AHB1ENR
LDR R0, =RCC_AHB1ENR

Обозначение должно быть объявлено в начале файла примерно так:
RCC_AHB1ENR EQU 0x40023830

Так можно обозвать любые константы и использовать их в коде.

Rn - куда читаем,
Rm - адрес, откуда читаем.

Запись происходит аналогично, только команда называется STR:
; Запишем обратно
STR R1,

Между чтением и записью необходимо установить бит, отвечающий за тактирование порта D.
На помощь приходят команда логики OR.
ORR Rd, Ra, Rb
ORR Rd, Ra, #число

Rd - куда записывается результат;
Ra - первый операнд;
Rb - второй операнд;
число - вариант записи второго операнда прямо числом, если число удовлетворяет ряду требований (см. документацию).
Есть и иные варианты, но фиг с ними пока.

Пример: установка бита.
; Установим бит тактирования порта D
ORR R1, R1, #RCC_AHB1ENR_GPIODEN ; RCC_AHB1ENR_GPIODEN объявлен как 0x00000008, бит 3.

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

Очень помогает в борьбе с этим косвенная адресация.
Типа как оператор -> в языке Си.

Записывается:
LDR Rn,
STR Rn,

То есть к адресу, записанном в Rm добавляется некоторое смещение прям вот так. Что удобно, смещение от базового адреса периферии для каждого регистра указано прямо в Referense Manual. Конечно, базовый адрес тоже указан.

Пример:
LDR R1, ; GPIO_MODER_OFFSET - смещение от базового адреса периферии GPIO до регистра MODER: 0x00000000

К слову, базовый адрес GPIOD: 0x40020C00. Все адреса можно подглядеть в или .

Безусловный переход.
Бесконечный цикл делается с помощью этой команды. Выполнение продолжится с метки, указанной в аргументе:
B метка

Метка может располагаться как до команды, так и после неё.

Последнее, что требуется: задержка.
Можно её оформить в виде отдельной процедуры.

Процедура оформляется так:
; Программная задержка
; R0 - величина задержки
delay PROC
; Какой-то полезный код
BX LR ; Возрат (типа return)
ENDP

BX LR - переход по адресу, записанному в регистре LR (R14). В этот регистр автоматически кладётся адрес команды, следующей за вызовом подпрограммы. Соответственно, эта команда осуществляет выход из процедуры.

Если есть вложенные процедуры, содержимое регистра надо сохранять, так как он перезаписывается автоматически при выполнении команд вызова подпрограммы вне зависимости от его содержания =) Но об этом потом.

Код задержки прост: принимаем число в регистре R0 и вычитаем его до тех пор, пока оно не станет 0.
; Вычитаем единицу
SUBS R0, R0, #1
; Пока не обнулилась, крутим дальше
BNE delay

Флаг S у процедуры SUB обозначает, что при выполнении команды будут выставлены флаги статуса, соответствующие результату выполнения. Ну, то есть, не ноль ли результат, было ли переполнение или заём и т.д.

Условный переход формируется из команды B и условного суффикса. Весь список можно посмотреть тут.

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

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

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



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: