Некоторые основные команды ассемблера
Во-первых, загрузка большого числа (32 бита) в регистр.
LDR Rn, =число.
Rn — название регистра (R0, R1, ...), число — то, что хотим загрузить.
Например:
LDR R0, =0x40023830 ; Адрес регистра RCC_AHB1ENR
Для красоты и читабельности число можно заменить обозначением:
; Загрузим в регистр R0 адрес регистра RCC_AHB1ENR
LDR R0, =RCC_AHB1ENR
Обозначение должно быть объявлено в начале файла примерно так:
RCC_AHB1ENR EQU 0x40023830
Так можно обозвать любые константы и использовать их в коде.
Во-вторых, загрузка из памяти по заданному в регистре адресу.
LDR Rn, [Rm]
Rn — куда читаем,
Rm — адрес, откуда читаем.
Например, хотим прочитать, что записано в регистре RCC_AHB1ENR, адрес которого у нас хранится в регистре R0.
; Прочитаем в регистр R1 его содержимое
LDR R1, [R0]
Запись происходит аналогично, только команда называется STR:
; Запишем обратно
STR R1, [R0]
Между чтением и записью необходимо установить бит, отвечающий за тактирование порта 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, [Rm, #смещение]
STR Rn, [Rm, #смещение]
То есть к адресу, записанном в Rm добавляется некоторое смещение прям вот так. Что удобно, смещение от базового адреса периферии для каждого регистра указано прямо в Referense Manual. Конечно, базовый адрес тоже указан.
Пример:
LDR R1, [R0, #GPIO_MODER_OFFSET] ; GPIO_MODER_OFFSET — смещение от базового адреса периферии GPIO до регистра MODER: 0x00000000
; Test STM32F4-Assembler project
AREA |.text|, CODE, READONLY
RCC_AHB1ENR EQU 0x40023830
RCC_AHB1ENR_GPIODEN EQU 0x00000008 ; 3 бит
GPIOD_BASE EQU 0x40020C00
GPIO_MODER_OFFSET EQU 0x00000000
GPIO_MODER_MODER12_0 EQU 0x01000000
GPIO_MODER_MODER13_0 EQU 0x04000000
GPIO_MODER_MODER14_0 EQU 0x10000000
GPIO_MODER_MODER15_0 EQU 0x40000000
GPIO_PUPDR_OFFSET EQU 0x0000000C
GPIO_PUPDR_PUPDR12_0 EQU 0x01000000
GPIO_PUPDR_PUPDR13_0 EQU 0x04000000
GPIO_PUPDR_PUPDR14_0 EQU 0x10000000
GPIO_PUPDR_PUPDR15_0 EQU 0x40000000
GPIO_BSRRL_OFFSET EQU 0x00000018 ; Сброс
GPIO_BSRRH_OFFSET EQU 0x0000001A ; Установка
DELAY_COUNTER EQU 0x000F4240 ; 1000000UL
GPIO_BSSR_PIN12 EQU 0x00001000 ; 12 бит
GPIO_BSSR_PIN13 EQU 0x00002000 ; 13 бит
GPIO_BSSR_PIN14 EQU 0x00004000 ; 14 бит
GPIO_BSSR_PIN15 EQU 0x00008000 ; 15 бит
; Инициализация системы тактирования
; EXPORT SystemInit
;SystemInit PROC
;
; BX LR ; Возвращаемся обратно
;
; ENDP
; Программная задержка
; R0 - величина задержки
delay PROC
; Вычитаем единицу
SUBS R0, R0, #1
; Пока не обнулилась, крутим дальше
BNE delay
BX LR
ENDP
EXPORT __main
__main PROC
NOP
; Разрешим тактирование порта D
; Загрузим в регистр R0 адрес регистра RCC_AHB1ENR
LDR R0, =RCC_AHB1ENR
; Прочитаем в регистр R1 его содержимое
LDR R1, [R0]
; Установим бит тактирования порта D
ORR R1, R1, #RCC_AHB1ENR_GPIODEN
; Запишем обратно
STR R1, [R0]
; Установим режим работы выводов (выход)
LDR R0, =GPIOD_BASE
; Прочитаем в регистр R1 его содержимое
LDR R1, [R0, #GPIO_MODER_OFFSET]
; Установим необходимые биты для выводов 12-15
ORR R1, R1, #GPIO_MODER_MODER12_0
ORR R1, R1, #GPIO_MODER_MODER13_0
ORR R1, R1, #GPIO_MODER_MODER14_0
ORR R1, R1, #GPIO_MODER_MODER15_0
; Запишем обратно
STR R1, [R0, #GPIO_MODER_OFFSET]
; Подтяжка к питанию
LDR R0, =GPIOD_BASE
LDR R1, [R0, #GPIO_PUPDR_OFFSET]
; Установим необходимые биты для выводов 12-15
ORR R1, R1, #GPIO_PUPDR_PUPDR12_0
ORR R1, R1, #GPIO_PUPDR_PUPDR13_0
ORR R1, R1, #GPIO_PUPDR_PUPDR14_0
ORR R1, R1, #GPIO_PUPDR_PUPDR15_0
; Запишем обратно
STR R1, [R0, #GPIO_PUPDR_OFFSET]
; В регистр R1 запишем адрес GPIOD
LDR R1, =GPIOD_BASE
; В регистр R2 запишем величину задержки
LDR R2, =DELAY_COUNTER
__mainloop
; Зажжём 12 бит
MOV R4, #GPIO_BSSR_PIN12 ; 12 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 12 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; Зажжём 13 бит
MOV R4, #GPIO_BSSR_PIN13 ; 13 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 13 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; Зажжём 14 бит
MOV R4, #GPIO_BSSR_PIN14 ; 14 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 14 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; Зажжём 15 бит
MOV R4, #GPIO_BSSR_PIN15 ; 15 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 15 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; На очередной круг
B __mainloop
ENDP
END
Во-первых, загрузка большого числа (32 бита) в регистр.
LDR Rn, =число.
Rn — название регистра (R0, R1, ...), число — то, что хотим загрузить.
Например:
LDR R0, =0x40023830 ; Адрес регистра RCC_AHB1ENR
Для красоты и читабельности число можно заменить обозначением:
; Загрузим в регистр R0 адрес регистра RCC_AHB1ENR
LDR R0, =RCC_AHB1ENR
Обозначение должно быть объявлено в начале файла примерно так:
RCC_AHB1ENR EQU 0x40023830
Так можно обозвать любые константы и использовать их в коде.
Во-вторых, загрузка из памяти по заданному в регистре адресу.
LDR Rn, [Rm]
Rn — куда читаем,
Rm — адрес, откуда читаем.
Например, хотим прочитать, что записано в регистре RCC_AHB1ENR, адрес которого у нас хранится в регистре R0.
; Прочитаем в регистр R1 его содержимое
LDR R1, [R0]
Запись происходит аналогично, только команда называется STR:
; Запишем обратно
STR R1, [R0]
Между чтением и записью необходимо установить бит, отвечающий за тактирование порта 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, [Rm, #смещение]
STR Rn, [Rm, #смещение]
То есть к адресу, записанном в Rm добавляется некоторое смещение прям вот так. Что удобно, смещение от базового адреса периферии для каждого регистра указано прямо в Referense Manual. Конечно, базовый адрес тоже указан.
Пример:
LDR R1, [R0, #GPIO_MODER_OFFSET] ; GPIO_MODER_OFFSET — смещение от базового адреса периферии GPIO до регистра MODER: 0x00000000
К слову, базовый адрес GPIOD: 0x40020C00. Все адреса можно подглядеть в даташите или Reference Manual.
Безусловный переход.
Бесконечный цикл делается с помощью этой команды. Выполнение продолжится с метки, указанной в аргументе:
B метка
Метка может располагаться как до команды, так и после неё.
Последнее, что требуется: задержка. Можно её оформить в виде отдельной процедуры.
Процедура оформляется так:
; Программная задержка
; R0 - величина задержки
delay PROC
; Какой-то полезный код
BX LR ; Возрат (типа return)
ENDP
BX LR — переход по адресу, записанному в регистре LR (R14). В этот регистр автоматически кладётся адрес команды, следующей за вызовом подпрограммы. Соответственно, эта команда осуществляет выход из процедуры.
Если есть вложенные процедуры, содержимое регистра надо сохранять, так как он перезаписывается автоматически при выполнении команд вызова подпрограммы вне зависимости от его содержания =) Но об этом потом.
Код задержки прост: принимаем число в регистре R0 и вычитаем его до тех пор, пока оно не станет 0.
; Вычитаем единицу
SUBS R0, R0, #1
; Пока не обнулилась, крутим дальше
BNE delay
Флаг S у процедуры SUB обозначает, что при выполнении команды будут выставлены флаги статуса, соответствующие результату выполнения. Ну, то есть, не ноль ли результат, было ли переполнение или заём и т.д.
Условный переход формируется из команды B и условного суффикса. Весь список можно посмотреть тут: Conditional execution.
В данном случае, выполняется проверка на «не ноль» (NE = Not Equal).
И последнее, вызов подпрограммы.
BL метка
метка — название подпрограммы. Пример:
BL delay
Именно команда перехода с суффиксом L сохраняет адрес следующей за ней команды в регистре LR.
Пример кода, моргающего светодиодами:
; Test STM32F4-Assembler project
AREA |.text|, CODE, READONLY
RCC_AHB1ENR EQU 0x40023830
RCC_AHB1ENR_GPIODEN EQU 0x00000008 ; 3 бит
GPIOD_BASE EQU 0x40020C00
GPIO_MODER_OFFSET EQU 0x00000000
GPIO_MODER_MODER12_0 EQU 0x01000000
GPIO_MODER_MODER13_0 EQU 0x04000000
GPIO_MODER_MODER14_0 EQU 0x10000000
GPIO_MODER_MODER15_0 EQU 0x40000000
GPIO_PUPDR_OFFSET EQU 0x0000000C
GPIO_PUPDR_PUPDR12_0 EQU 0x01000000
GPIO_PUPDR_PUPDR13_0 EQU 0x04000000
GPIO_PUPDR_PUPDR14_0 EQU 0x10000000
GPIO_PUPDR_PUPDR15_0 EQU 0x40000000
GPIO_BSRRL_OFFSET EQU 0x00000018 ; Сброс
GPIO_BSRRH_OFFSET EQU 0x0000001A ; Установка
DELAY_COUNTER EQU 0x000F4240 ; 1000000UL
GPIO_BSSR_PIN12 EQU 0x00001000 ; 12 бит
GPIO_BSSR_PIN13 EQU 0x00002000 ; 13 бит
GPIO_BSSR_PIN14 EQU 0x00004000 ; 14 бит
GPIO_BSSR_PIN15 EQU 0x00008000 ; 15 бит
; Инициализация системы тактирования
; EXPORT SystemInit
;SystemInit PROC
;
; BX LR ; Возвращаемся обратно
;
; ENDP
; Программная задержка
; R0 - величина задержки
delay PROC
; Вычитаем единицу
SUBS R0, R0, #1
; Пока не обнулилась, крутим дальше
BNE delay
BX LR
ENDP
EXPORT __main
__main PROC
NOP
; Разрешим тактирование порта D
; Загрузим в регистр R0 адрес регистра RCC_AHB1ENR
LDR R0, =RCC_AHB1ENR
; Прочитаем в регистр R1 его содержимое
LDR R1, [R0]
; Установим бит тактирования порта D
ORR R1, R1, #RCC_AHB1ENR_GPIODEN
; Запишем обратно
STR R1, [R0]
; Установим режим работы выводов (выход)
LDR R0, =GPIOD_BASE
; Прочитаем в регистр R1 его содержимое
LDR R1, [R0, #GPIO_MODER_OFFSET]
; Установим необходимые биты для выводов 12-15
ORR R1, R1, #GPIO_MODER_MODER12_0
ORR R1, R1, #GPIO_MODER_MODER13_0
ORR R1, R1, #GPIO_MODER_MODER14_0
ORR R1, R1, #GPIO_MODER_MODER15_0
; Запишем обратно
STR R1, [R0, #GPIO_MODER_OFFSET]
; Подтяжка к питанию
LDR R0, =GPIOD_BASE
LDR R1, [R0, #GPIO_PUPDR_OFFSET]
; Установим необходимые биты для выводов 12-15
ORR R1, R1, #GPIO_PUPDR_PUPDR12_0
ORR R1, R1, #GPIO_PUPDR_PUPDR13_0
ORR R1, R1, #GPIO_PUPDR_PUPDR14_0
ORR R1, R1, #GPIO_PUPDR_PUPDR15_0
; Запишем обратно
STR R1, [R0, #GPIO_PUPDR_OFFSET]
; В регистр R1 запишем адрес GPIOD
LDR R1, =GPIOD_BASE
; В регистр R2 запишем величину задержки
LDR R2, =DELAY_COUNTER
__mainloop
; Зажжём 12 бит
MOV R4, #GPIO_BSSR_PIN12 ; 12 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 12 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; Зажжём 13 бит
MOV R4, #GPIO_BSSR_PIN13 ; 13 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 13 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; Зажжём 14 бит
MOV R4, #GPIO_BSSR_PIN14 ; 14 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 14 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; Зажжём 15 бит
MOV R4, #GPIO_BSSR_PIN15 ; 15 бит
STR R4, [R1, #GPIO_BSRRL_OFFSET]
; Задержка
MOV R0, R2
BL delay
; Погасим 15 бит
STR R4, [R1, #GPIO_BSRRH_OFFSET]
; На очередной круг
B __mainloop
ENDP
END
Если вы используете 32-битные константы, которые используются по нескольку раз, то лучше их сложить после тела функции и грузить в регистры командой LDR. Так объем программы получится меньше. Посмотрите код любой сишной функции в отладчике, там сделано именно так.
ОтветитьУдалить