learn_arm_cortex_m0_asm

Программирование микроконтроллера на языке ассемблера

Регистры процессора Arm Cortex M0 (M0+)

Регистр - это ячейка памяти находящаяся прямо внутри процессора. Это не оперативная память!

Регистр не имеет адреса, но у каждого регистра есть порядковый номер. В инструкциях для процессора содержатся закодированные номера регистров, к которым должен обратиться процессор  для выполнения очередной инструкции.

Рис. 1 Регистры процессора Arm Cortex M0 (M0+).

Процессор Arm Cortex M0 (M0+) имеет основной банк из 16 регистров. Регистры размером 32 бита каждый. Кроме того есть несколько специальных регистров.

Сейчас нас интересует основной банк регистров. Из 16 регистров основного банка 12 регистров являются регистрами общего назначения (регистры R0 - R12), которые программист может использовать по своему усмотрению. Регистры R13 - R15 имеют специальное фиксированное назначение.

Рассмотрим подробнее регистры основного банка.

Большинство инструкций процессора могут работать только с регистрами R0 - R7. Это связано с тем номер регистра в этих инструкциях кодируется 3мя битами, в 3 бита момещается максимальный номер 7.

Регистр R13 - это указатель стека. Что такое стек мы рассмотрим в следующем уроке.

Регистр R14 - это Link регистр, который хранит в себе адрес возврата из функции. Зачем это нужно мы тоже узнаем на следующем уроке.

Регистр R15 - это регистр счетчика инструкций. Он хранит в себе адрес следующей инструкции, которая будет исполнена процессором.

Программирование на языке ассемблера

Любая программа для микроконтроллера в конечном итоге превращается в список инструкций для процессора.

Такой список инструкций можно получить скомпилировав программу, написанную на языке Си.

В результате компиляции получается бинарный файл, содержащий машинный код, понятный процессору. В машинном коде закодированы те самые инструкции для процессора.

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

Для каждой инструкции на языке ассемблера есть соотвествующия инструкция в виде машинного кода.

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

Утилита ассемблер намного проще компилятора, она по сути заменяет каждую инструцию на языке ассемблера соотвествующим машинным кодом.

Процессор Arm Cortex M0 (M0+) имеет архитектуру ARMv6-M и работает с набором инструкций, который называется Thumb. В этом наборе есть как 16-и битные инструкции так и 32х битные инструкции.

Рис. 2 Набор инструкций процессора Arm Cortex M0 (M0+).

В общем виде инструкции на языке ассемблера записываются так:

mnemonic operand1, operand2, ...

Где mnemonic - это сокращение обозначающее инструкцию, operand1 и operand2 - это операнды, которых может быть один, два или больше.

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

mnemonic1 operand1, operand2, ...
mnemonic2 operand1, operand2, ...
mnemonic3 operand1, operand2, ...

Кроме того в программе могут присутствовать метки (labels), которые указывают на адрес инструкции после метки. Они нужны для записи циклов, переходов и другого.

Например, в данной записи

label:
    mnemonic operand1, operand2, ...

метка label указывает на инструкцию следующую сразу за ней. Эти метки можно использовать вместо операндов, например может быть так:

label:
    mnemonic operand1, operand2, ...
    mnemonic label

В данном случае в качестве операнда для второй инструкции подставляется метка label, содержащая адрес первой инструкции.

Простейшая программа на языке ассемблера для нашего микроконтроллера выглядит примерно так:

.syntax unified
.cpu cortex-m0plus
.thumb

.section .isr_vector, "a", %progbits
.word _estack
.word Reset_Handler

.section .text
.global Reset_Handler
.type Reset_Handler, %function

Reset_Handler:
loop:
    b loop

Пока не будем вникать во все строки этой программы. Сейчас нас интересуют только две последние строчки.

В самой последней строке программы записана инструкция b loop.

Если сравнить последнюю строчку с общим видом записи инструкций, 

mnemonic operand1, operand2, ...

то становится понятно, что b - это mnemonic, а loop это operand1.

Инструкция b - это инструкция безусловного перехода (b - мнемоника для англ. слова branch). Это значит что процессор должен перейти на указанный адрес в первом операнде и выполнить инструкцию, лежащую по этому адресу. 

Общий вид инструкции b.

В данном случае метка loop указывает на адрес той же инструкции b loop.

Это означает, что процессор будет циклически выполнять одну и ту же инструкцию. То есть процессор будет работать в бесконечном пустом цикле.

По сути эта программа состоит из одной ассемблерной инструкции.

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

Reset_Handler:
    movs r0, #5
    movs r1, #7
    adds r0, r0, r1

loop:
    b loop

Эта программа складывает два числа: 5 + 7. После этого попадает в бесконечный цикл.

Здесь применены новые для нас инструкции movs, adds.

Общий вид инструкции movs с кодированием числа внутри инструкции.
Общий вид инструкции adds.

Инструкция movs r0, #5 записывает в регистр r0 число 5. При этом стоит отметить, что число 5 закодировано в самой инструкции.

Инструкция movs r1, #7 аналогично предыдущей инструкции записывает в регистр r1 число 7.

Инструкция adds r0, r0, r1 Складывает R0 + R1 и записывает результат в R0.

Рассмотрим еще несколько простых программ на языке ассемблера.

Reset_Handler:
    movs r0, #20
    movs r1, #3
    subs r0, r0, r1

loop:
    b loop

Эта программа выполняет вычитание чисел: 20 - 3.

Новая инструкция subs, вот её описание:

Общий вид инструкции subs.

Рассмотрим программу чуть посложнее.

Reset_Handler:
    movs r0, #10
    movs r1, #10

    cmp r0, r1
    beq equal

not_equal:
    movs r2, #0
    b loop

equal:
    movs r2, #1

loop:
    b loop

Эта программа решает такое задание: если r0 == r1, записать в r2 число 1, иначе 0.

Новые инструкции: cmp, beq.

Вот их описание:

Общий вид инструкции cmp.

Общий вид инструкции beq.

Инструкция cmp сравнивает два операнда и если они равны, то в специальном регистре выставляется флаг z.

Инструкция beq проверяет, есть ли в специальном регистре флаг z. Если флаг z есть, то процессор переходит на инструкцию указанную в операнде. В данном случае процессор переходит на адрес инструкции, записанный в метке equal.

Еще похожая программа, но с инструкцией bne, a не beq.

Reset_Handler:
    movs r0, #0

count_loop:
    adds r0, r0, #1
    cmp r0, #10
    bne count_loop

loop:
    b loop

Эта программа увеличивает r0 от 0 до 10 в цикле.

Инструкция bne смотрит на флаг z, и если он не выставлен, то заставляет процессор перейти по адресу инструкции, указанному в операнде.

Суффикс для инструкции b. b + ne = bne

Получается, что пока число в регистре R0 не достигло 10 процессор возвращается на адрес инструкции, записанному в метке count_loop.

Задание 1

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

Задание 2

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

Задание 3

Посчитать сумму чисел от 1 до 5, используя цикл.

Задание 4

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

Задание 5

В r0 и r1 записать числа. Найти максимальное из этих чисел с помощью инструкций и записать результат в r2.