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

Программирование stm32 на ассемблере – Мигаем светодиодом в STM32 на ассемблере / Habr

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

Некотрое время назад захотелось мне освоить ассемблер и после прочтения соответствующей литературы пришло время практики. Собственно о ней и пойдет дальше речь. Первое время я практиковался на 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
Выяснилось, что в начале программы надо поставить
.syntax unified
это говорит ассемблеру что можно использовать 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 регистре биты [25:24] = 10 и биты [27:26] = 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 замигал. Надеюсь эта статья комуто поможет. Если есть вопросы буду рад на них ответить.

habr.com

Программирование 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

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

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. Приложение 1. Набор инструкций THUMB-2 и особенности их использования
  18. Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103
  19. Приложение 3. Драйвера и функции библиотеки StdPeriph

radiohlam.ru

Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 8. Процедуры на асcемблере для STM32

Для уменьшения размеров кода и улучшения его читабельности, группы инструкций, выполняющие какую-либо элементарную задачу, могут быть оформлены в виде «процедур». Удобство процедур в том, что мы можем обращаться к ним из любого места программы, любое число раз.

Объявляются процедуры с помощью ключевых слов PROC / ENDP или FUNCTION / ENDFUNC. Keil-овский ассемблер для ARM-ов не делает различий между процедурами и функциями. При этом, по желанию, можно разместить функцию в какую-то специально выделенную область памяти. Выглядит объявление процедур следующим образом:

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

Следующий вопрос, — как в ARM-контроллерах осуществляется переход из основной программы в процедуру, а также возврат из процедуры в основную программу. Тут возможны несколько вариантов:

  • Использование команды BL label для перехода к процедуре и команды BX LR для возврата в основную программу.
  • Команда BL label выполняет переход на начало процедуры (заносит в PC адрес первой команды процедуры) и записывает в регистр LR адрес следующей за BL label команды (чтобы знать куда потом вернуться).

                 ...   ; основная программа
                 bl Procedure1 ; вызов процедуры
                 ...   ; продолжение основной программы
    AREA MyProcedure, CODE, READONLY
    Procedure1   PROC
                 ...   ; тело процедуры
                 ...
                 bx LR ; возврат в основную программу
                 ENDP

    ... ; основная программа bl Procedure1 ; вызов процедуры ... ; продолжение основной программы AREA MyProcedure, CODE, READONLY Procedure1 PROC ... ; тело процедуры ... bx LR ; возврат в основную программу ENDP

    Вызванная процедура сама может вызывать какие-то процедуры, такие вызовы называются вложенными. Если при этом использовать описанный выше механизм, то каждый новый вызов будет затирать содержимое регистра LR и вернуться назад, к месту первого вызова, будет невозможно.

  • В этом случае можно сохранить адрес возврата в стеке командой PUSH. Тогда, при правильном управлении стеком, можно будет перед возвратом восстановить оттуда нужный адрес возврата и воспользоваться командой BX LR. Более того, можно вместо двух команд POP{LR}, BX LR восстанавливать адрес возврата сразу в PC, используя команду POP{PC}
  •              ...           ; основная программа
                 bl Procedure1 ; вызов процедуры
                 ...           ; продолжение основной программы
    AREA MyProcedure, CODE, READONLY
    Procedure1   PROC
                 push {LR}
                 ...           ; тело процедуры
                 bl Procedure2 ; вложенный вызов процедуры, который затрёт LR
                 ...
                 pop {PC}      ; возврат в основную программу (в стеке у нас LR)
                 ENDP

    ... ; основная программа bl Procedure1 ; вызов процедуры ... ; продолжение основной программы AREA MyProcedure, CODE, READONLY Procedure1 PROC push {LR} ... ; тело процедуры bl Procedure2 ; вложенный вызов процедуры, который затрёт LR ... pop {PC} ; возврат в основную программу (в стеке у нас LR) ENDP

  • Третий вариант — использовать для выхода из процедуры команду LDR или LDM с PC в качестве регистра-приёмника. Например, этими командами можно восстанавливать значение прямо из стека (аналогично предыдущему варианту), только нужно понимать, что сам стек при этом останется неизменным (то есть указатель стека не изменится).
  •              ...           ; основная программа
                 bl Procedure1 ; вызов процедуры
                 ...           ; продолжение основной программы
    AREA MyProcedure, CODE, READONLY
    Procedure1   PROC
                 push {LR}
                 ...           ; тело процедуры
                 bl Procedure2 ; вложенный вызов процедуры, который затрёт LR
                 ...
                 LDR PC,[SP]   ; читаем значение PC из стека (там у нас LR), но
                               ; сам стек при этом не меняется
                 ENDP

    ... ; основная программа bl Procedure1 ; вызов процедуры ... ; продолжение основной программы AREA MyProcedure, CODE, READONLY Procedure1 PROC push {LR} ... ; тело процедуры bl Procedure2 ; вложенный вызов процедуры, который затрёт LR ... LDR PC,[SP] ; читаем значение PC из стека (там у нас LR), но ; сам стек при этом не меняется ENDP

В принципе, для переходов можно использовать любые команды обработки данных, позволяющие сохранить результат в регистр, например MOV или AND и тому подобные. Не смотря на то, что в руководствах по командам THUMB-2 написано, что в них нельзя использовать PC в качестве регистра-приёмника и Keil-овский ASM будет при компиляции писать Warning, на самом деле всё будет работать. Это конечно уже для гиков и бог его ещё знает что там будет, скажем, с конвейером, но теоретически вы вполне можете скомпилить и запустить что-то подобное (я, например, запускал, чисто ради интереса):

                 dcd Procedure1   ; адрес процедуры
                 dcd Continue1    ; адрес продолжения
    ENTRY
                 ldr R1,[PC,#-12] ; теперь у нас в R1 адрес начала процедуры,
                 ldr R2,[PC,#-12] ; а в R2 адрес продолжения
                 nop
                 ...              ; основная программа
                 mov PC,R1        ; вызов процедуры
    Continue1
                 ...              ; продолжение основной программы
    AREA MyProcedure, CODE, READONLY
    Procedure1   PROC
                 ...              ; тело процедуры
                 ...
                 mov PC,R2        ; возврат из процедуры
                 ENDP

    dcd Procedure1 ; адрес процедуры dcd Continue1 ; адрес продолжения ENTRY ldr R1,[PC,#-12] ; теперь у нас в R1 адрес начала процедуры, ldr R2,[PC,#-12] ; а в R2 адрес продолжения nop ... ; основная программа mov PC,R1 ; вызов процедуры Continue1 ... ; продолжение основной программы AREA MyProcedure, CODE, READONLY Procedure1 PROC ... ; тело процедуры ... mov PC,R2 ; возврат из процедуры ENDP

Подробнее про процедуры можно почитать в книжке Assembly Language Programming, Vincent Mahout, главы 3.3.5 (стр.35) и 7.1 (стр.119). Книжка эта на английском, русский перевод мне не встречался.

  1. Часть 1. Установка MDK, создание проекта, основы Keil uVision
  2. Часть 2. Команды и директивы ассемблера, структура и синтаксис программы. Первая программа для STM32
  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. Приложение 1. Набор инструкций THUMB-2 и особенности их использования
  18. Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103
  19. Приложение 3. Драйвера и функции библиотеки StdPeriph

radiohlam.ru

Мигаем светодиодом в 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 инструкций. Я писал инструкции строго по документации, но ассемблер на них почемуто ругался.

unshifted register required

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

.syntax unified

это говорит ассемблеру что можно использовать 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 регистре биты [25:24] = 10 и биты [27:26] = 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 замигал. Надеюсь эта статья комуто поможет. Если есть вопросы буду рад на них ответить.

Автор: mksma

Источник

www.pvsm.ru

Embedded systems: STM32 Assembler

ADRADR R0, label +/- 12bit
ADR.W R0, label +/- 32bit
Загрузка адреса метки в регистр.
LDR
STR
LDR R0, [R1, #8bit const.]! - прединкремент
LDR R0, [R1], #8bit const. - постинкремент
LDRB R0, [R1] - без инкремента
STRB R0, [R1], #1
Загрузка/сохранение регистра в режиме
адресации со смещением.
B=байт, SB=байт со знаком (только загрузка)
H=полуслово, SH=полуслово со знаком (только загрузка)
LDR
STR
LDR R0, [R1, R2, {LSL#0..3}]
STR R0, [R1, R2, {LSL#0..3}]
Смещение задаётся третьим регистром.
B, SB, H, SH работают аналогично
LDR
LDRD
LDR R0, label
LDRD R0, R1, label
Смещение задаётся адресом метки.
B, SB, H, SH работают аналогично
Невозможно применить к STR/STRD.
LDRT
STRT
Непривилегированный доступ.
Аналогичны простому LDR/STR.
LDRD
STRD
LDRD R0, R1, [R2, #10bit const.]! - прединкремент
LDRD R0, R1, [R2], #10bit const. - постинкремент
LDRD R0, R1, [R2] - без инкремента
STRD R0, R1, [R2]
Загрузка/сохранение двух регистров в режиме
адресации со смещением.
Константа должна быть кратна 4.
LDM
STM
LDM R0, {R1-R3}
LDM R0!, {R1-R3} - постинкремент R0
IA, DB, FD, EA - см. описание
Загрузка/сохранение множества регистров.
IA - с увеличением адреса
DB - с уменьшением адреса.
PUSH
POP
PUSH {R0, R2-R7, R12}
POP {R0, R2-R7, R12}
Загрузка/чтение из стека
LDREX
STREX
LDREX R1, [R2, #10bit const.]
STREX R0, R1, [R2, #10bit const.]
Эксклюзивное чтение/запись регистра.
B=байт, H=полуслово.
CLREXCLREX (без параметров)Сброс признака эксклюзивного доступа.

golf2109.blogspot.com

Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 4. Регистры, старт и режимы работы контроллеров STM32

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

В этой части мы поговорим о том, с чем нам чаще всего придётся оперировать при работе с контроллером — о рабочих регистрах ядра Cortex-M3, о режимах его работы и о том, как контроллер включается.

Итак, в ядре Cortex-M3 имеется 13 регистров общего назначения — R0..R12, регистр, используемый для хранения указателя стека, — R13, регистр связи — R14, счётчик команд — R15 и 5 регистров специального назначения.

Регистры общего назначения разделяются на младшие регистры — R0..R7 и старшие регистры — R8..R12. Разница между ними в том, что некоторые 16-тибитные команды набора thumb-2 умеют работать только с младшими регистрами, а со старшими — не умеют.

Регистров R13 вообще-то говоря два, а не один. Первый называется MSP — указатель основного стека, а второй PSP — указатель стека процесса. Однако в каждый момент доступен только один из этих регистров. Какой именно — определяется в одном из регистров специального назначения. Зачем такое надо? Это сделано для возможности организации защиты операционной системы (ага, на этот контроллер можно поставить ОС, если хочется) от кривых прикладных программ. MSP используется обработчиками исключительных ситуаций и всеми программами, использующими привилегированный уровень выполнения (например ядро ОС), а PSP — используется программами, не требующими привилегированного уровня выполнения (например, прикладными программами от которых мы хотим защитить ядро ОС). Указатели стека всегда должны быть выровнены на границу 32-хбитного слова, т.е. два их младших бита всегда должны быть сброшены в ноль.

Регистр R14 называется LR (link register) — регистр связи и используется для запоминания адреса возврата при вызове подпрограмм.

Регистр R15 называется PC (program counter) — счётчик команд и используется для хранения адреса текущей выполняемой команды.

Теперь о специальных регистрах.

Регистр xPSR содержит флаги результатов выполнения арифметических и логических операций, состояние выполнения программы и номер обрабатываемого в данный момент прерывания. Иногда об этом регистре пишут во множественном числе. Это сделано потому, что к трём его частям можно обращаться как к отдельным регистрам. Эти «подрегистры» называются: APSR — регистр состояния приложения (тут как раз хранятся флаги), IPSR — регистр состояния прерывания (содержит номер обрабатываемого прерывания) и EPSR — регистр состояния выполнения. Полностью структура регистра xPSR приведена на рисунке ниже.

Флаги в регистре APSR стандартные:

  1. N (negative flag) — отрицательный результат операции
  2. Z (zero flag) — нулевой результат операции
  3. C (carry flag) — флаг переноса/займа
  4. V (overflow flag) — флаг переполнения
  5. Q (saturation flag) — флаг насыщения

В регистре PRIORITY MASK используется только нулевой бит (PRIMASK), который будучи установлен в единицу запрещает все прерывания с конфигурируемым приоритетом. После включения бит PRIMASK сброшен в ноль — все прерывания разрешены.

В регистре FAULT MASK также использует только нулевой бит (FAULTMASK), который будучи установлен в единицу запрещает все прерывания и исключения, кроме немаскируемого прерывания (NMI). После включения бит FAULTMASK сброшен в ноль — все прерывания разрешены.

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

Регистр CONTROL используется для управления одним из режимов процессора — режимом потока. Нулевой бит этого регистра (nPRIV) определяет уровень выполнения (привилегированный — Privilegied, или непривилегированный — Unprivilegied), а первый бит (SPSEL) — используемый указатель стека (MSP или PSP). Разница между привилегированным и непривилегированным уровнями выполнения состоит в том, что для привилегированного уровня доступны все области памяти и все команды процессора, а для непривилегированного уровня некоторые области памяти закрыты (например, регистры специального назначения, кроме APSR, системная область) и, соответственно, команды для доступа в эти обасти — запрещены. Попытка выполнения запрещённых команд, пытающихся обратиться в закрытые области памяти вызывает генерацию исключения отказа.

Теперь о режимах работы процессора.

Процессор Cortex-M3 имеет два режима работы: режим потока (Thread) и режим обработчика (Handle). Режим Handle используется для обработки исключительных ситуаций, а режим Thread — для выполнения всего остального кода. Переключение из одного режима в другой происходит автоматически. Как мы уже говорили, когда разбирались с регистром CONTROL, в режиме Thread процессор может использовать как привилегированный уровень выполнения, так и непривилегированный, в режиме Handle — только привилегированный. Аналогично, в режиме Thread может использоваться как основной стек (MSP), так и стек процесса (PSP), а в режиме Handle — только основной стек.

Важно понимать, что, например, переключившись в режиме Thread с привилегированного уровня в непривилегированный, мы потеряем доступ в регистр CONTROL и обратно сможем переключиться только в режиме Handle. В режиме Handle бит nPRIV регистра CONTROL доступен для чтения/записи, но не влияет на текущий режим выполнения. Это позволяет изменить уровень выполнения, который будет у программы, когда процессор выйдет из режима обработчика в режим потока. Бит SPSEL в режиме Handle для записи недоступен и всегда читается как ноль, а при выходе из режима обработчика в режим потока восстанавливается автоматически. Все варианты переходов между различными режимами и уровнями выполнения иллюстрирует ориентированный граф, представленный на рисунке ниже:

Далее поговорим о том, как контроллер стартует.

Стартует контроллер всегда на внутреннем генераторе, на частоте 8 Мгц. Откуда брать тактовый сигнал в дальнейшем, на сколько его умножать или делить — настраивается в программе. Если в программе этого не сделать, то хоть десять внешних кварцев повесьте, контроллер так и будет работать от внутреннего генератора 8 МГц.

При старте контроллер анализирует сочетание уовней на двух своих ногах — BOOT0, BOOT1, и, в зависимости от этого сочетания, начинает загрузку либо из flash-памяти, либо из ОЗУ, либо из системной области памяти. Это делается с помощью уже описанного нами ранее механизма псевдонимизации. По идее загрузка всегда начинается с нулевого адреса, просто в зависимости от
сочетания на ногах BOOT0, BOOT1 начальные адреса памяти назначаются псевдонимами одной из трёх областей: flash, встроенного ОЗУ или системной области. Справа приведена табличка, в которой указано, какая именно область проецируется в начальные адреса памяти в зависимости от сочетания ног BOOT0, BOOT1.

При этом в системной области производителем зашита специальная программа (bootloader), которая позволяет запрограммировать flash-память. Но об этом позже.

Первым делом контроллер считывает 32-х битное слово по адресу 0x00000000 и помещает его в регистр R13 (указатель стека). Далее он считывает 32-х битное слово по адресу 0x00000004 и помещает его в регистр R15 (счётчик команд). Последнее действие вызывает переход на начало программного кода и дальше начинается выполнение программы.

Слово по адресу 0x00000004 (адрес начала основной программы) называется вектор сброса. Вообще в памяти контроллера после указателя стека по адресу 0x00000000, начиная с адреса 0x00000004 должна лежать таблица векторов исключений и прерываний, первый вектор в которой — это вектор сброса, а остальные вектора — адреса процедур обработчиков различных исключений и прерываний. В простейших программах, если вы не собираетесь обрабатывать исключения и прерывания, все остальные вектора, кроме вектора сброса, могут отсутствовать. Хотелось бы обратить внимание, что в таблице векторов указываются именно адреса начала обработчиков, а не команды перехода на эти обработчики, как, например, в 8-ми битных пиках или атмелах.

Надеюсь понятно, что если ногами BOOT0, BOOT1 начальная область памяти установлена псевдонимом, например, flash-памяти, то считывание по адресу 0x00000000 реально приведёт к считыванию адреса 0x08000000 (начало flash-памяти), а считывание адреса 0x00000004 — к считыванию адреса 0x08000004 и так далее.

Ну и в завершение скажу, что стартует контроллер всегда в режиме потока с привилегированным уровнем выполнения.

  1. Часть 1. Установка MDK, создание проекта, основы Keil uVision
  2. Часть 2. Команды и директивы ассемблера, структура и синтаксис программы. Первая программа для STM32
  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. Приложение 1. Набор инструкций THUMB-2 и особенности их использования
  18. Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103
  19. Приложение 3. Драйвера и функции библиотеки StdPeriph

radiohlam.ru

Мигаем светодиодом в 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 инструкций. Я писал инструкции строго по документации, но ассемблер на них почемуто ругался.
unshifted register required
Выяснилось, что в начале программы надо поставить
.syntax unified
это говорит ассемблеру что можно использовать 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 регистре биты [25:24] = 10 и биты [27:26] = 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 замигал. Надеюсь эта статья комуто поможет. Если есть вопросы буду рад на них ответить.

sohabr.net

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *