Команды ассемблера для stm32. STM32F4. Светодиоды на ассемблере

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 и условного суффикса. Весь список можно посмотреть тут.

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




Top