Начало » Микроконтроллеры » Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК

 
 

Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК

21.12.20   erbol   3 574   2  

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

Шаблонные файлы

Сформируем набор из четырёх шаблонных файлов, которые будут основой каждого проекта в дальнейшем, а заодно выясним, как код из текстового файла соотносится с памятью программ МК.
Создаём в Notepad++ папку с именем template, а в ней — 4 файла:
1. main.S, где непосредственно и будет размещаться программа.
2. Файл с расширением .h и именем выбранного вами МК (attiny85.h, atmega8.h, stm32f401.h или nrf52832.h), который принято называть хидер-файлом. В этот файл мы будем выносить вспомогательную информацию, в частности — системные макроопределения.
3. LinkerScript.ld, предназначенный для сценария компоновщика.
4. Makefile, призванный автоматизировать работу с командной строкой в консоли.

Приступим к наполнению созданных файлов.

Файл main.S и хидер-файл

Не зависимо от назначения и уровня сложности, любая программа должна содержать минимальный набор обязательных элементов, который, не смотря на всю легкомысленность своего содержания, вполне точно определяет Рисунок 28 из предыдущей части статьи:
1. Основная функция main, состоящая из части, инструкции которой исполняются лишь раз, и цикла, инструкции которого исполняются по кругу постоянно (назовём его циклом основной функции или main_loop).
2. Процедура указания на вершину стека.
3. Переход к началу основной функции при подаче питания или нажатии кнопки Reset, являющихся прототипами удара в колокол. Как вы помните, при наступлении любого прерывания ЦПУ, завершив исполнение текущей инструкции, отправляется по адресу соответствующего вектора прерывания и исполняет встреченные там инструкции. В частности, по сбросу/подаче питания ЦПУ переходит по адресу вектора сброса, где необходимо прописать указание перейти к началу основной функции. Адреса всех векторов прерываний строго определяются производителем МК.

Трансформируем приведённые выше три пункта в реальный код.

Основная функция main

Каркас main.S – практически одинаковый для всех рассматриваемых нами МК:
Рисунок 1. Каркас файла main.S.

Для начала рассмотрим общую структуру main.S. Как видите, файл разбит на две секции — данных и текста. В первой из них оформляются данные, подлежащие размещению в одной из областей памяти данных МК – SRAM. Как именно это осуществляется, вы узнаете чуть ниже. Вторая секция предназначена для размещения текста вашей программы.

Теперь, перейдём к деталям Рисунка 1.
1. Все записи, начинающиеся с точки – указания (директивы) компилятору:
.data и .text обозначают границы секций данных и текста, соответственно.
.org N обуславливает смещение на N байт относительно младшего адреса памяти программ следующего за директивой кода при загрузке его в МК.
.global объявляет следующую за ней сущность глобальной, т.е. доступной из любого другого файла. В данном случае это – функция main, которая всегда должна быть глобальной.
.end уведомляет компилятор, что ниже директивы отсутствуют как данные, так и текст, поэтому обработку файла можно завершить.
2. Обратите внимание, что наименования начала основной функции и цикла завершаются двоеточием. В ассемблере все записи с двоеточием в конце называют метками, а их назначение – указать адрес первой инструкции, следующей за меткой. Наличие в программе двух и более меток с одинаковыми именами запрещено.
3. Замкнуть цикл основной функции позволяет инструкция перехода в строке 14 с именем метки main_loop в качестве аргумента (или операнда). В случае с AVR это – инструкции RJMP, а с ARM – B.
4. Отдельные участки программы могут сопровождаться комментариями, которые обрамляют символами «/*» и «*/». Допускается оформление комментариев как в виде строки (на рисунке – строки 7, 8), так и в виде блока (строки 11, 12), в случае которого указанные символы прописываются только по краям блока. Отмечу, что комментарии, так же, как символ табуляции и пустые строки (на рисунке – 9, 13), игнорируются компилятором и призваны, наряду с подсветкой текста, облегчить программисту чтение кода. В отличие от текста кода, использование в тексте комментариев кириллицы не возбраняется.
5. Любой файл в ассемблере от GCC желательно завершать пустой строкой (на рисунке – строка 16), отсутствие которой не является ошибкой, но вызовет соответствующее предупреждение компилятора.

Чтобы текст в main.S не был для вас некоей абстракцией, увяжем его уже на этом этапе с памятью программ рассматриваемых нами МК.
После компиляции из секции текста любой программы остаётся лишь содержимое строк:
1. С инструкциями МК.
2. С числами, которые записываются в память программ непосредственно при помощи специальных директив (в частности, .word, пример применения которой вы увидите ниже).

Загружаться в МК указанные инструкции и числа будут, начиная с младшего адреса памяти программ, в том же порядке, в каком следуют сверху-вниз в программе.
В частности, из всего текста на Рисунке 1 компилятор оставит только инструкцию перехода. Поскольку аргумент директивы .org равен нулю, размещена инструкция (точнее – её двоичное представление) будет без смещения, т.е. прямо по младшему адресу памяти программ. Как вы помните из Раздела «О памяти и спецрегистрах микроконтроллеров» предыдущей части статьи, для AVR-8 это – адрес 0x0000, для STM32F401 – 0х00000000, для nRF52832 – 0x08000000. Операндом инструкции перехода вместо имени метки main_loop будет адрес первой после метки инструкции. На данный момент это – адрес самой инструкции перехода:

Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК
Рисунок 2. Размещение кода каркаса main.S в памяти программ МК.

Если проводить аналогию между Рисунком 2 и Рисунком 28 из предыдущей части статьи, то в последнем необходимо удалить всю программу, а в строке с нулевым адресом произвести запись «Перейти к адресу 0».

Указание на вершину стека и вектор сброса

Дальше в main.S для AVR и ARM начинаются различия.

AVR-8
В случае с ATtiny85 и ATmega8 указание на вершину стека обеспечивается записью старшего адреса SRAM в указатель стека SP, для чего необходимо:
1. Загрузить значение адреса в РОН посредством инструкции LDI.
2. Отправить число из РОН в регистр SP с помощью инструкции OUT.

И тут возникает пара нюансов.
Во-первых, не все РОН в AVR – равнозначны. В частности, в младшие РОН (c r0 по r15) нельзя записать непосредственно число, поэтому воспользуемся r16.
Во-вторых, старшие адреса SRAM наших МК – двухбайтные (для ATtiny85 – 0x025F, для ATmega8 – 0x045F). С целью уместить оба байта адреса указатель стека в обоих МК организован как два 8-битных регистра – SPH (Stack Pointer High) и SPL (Stack Pointer Low), в которые записываются старший и младший байт адреса, соответственно. При этом, r16 — однобайтный, как и все РОН. Следовательно, придётся разбить адрес на байты и копировать их в составляющие SP по очереди. Здесь нам и поможет свойство шестнадцатеричного числа, о котором говорилось в предыдущей части статьи: каждые два разряда, начиная с младшего, составляют один байт. Для нас это – пары 0x02/0x5F и 0x04/0x5F для ATtiny85 и ATmega8, соответственно.

Поскольку инструкция OUT оперирует только с относительными адресами регистров ввода-вывода, в число которых входит и SP, выпишем таковые из Таблицы «Register Summary» даташита (страница 200 — для ATtiny85, 309 — для ATmega8) для SPH (0x3E) и SPL (0x3D), а затем оформим процедуру указания на вершину стека:

LDI    r16, 0x5F
OUT    0x3D, r16   
LDI    r16, 0x02 /* 0x04 – для МК ATmega8 */
OUT    0x3E, r16

Перед вами – классический пример использования в коде магических чисел, требующий привлечения макроопределений. Пропишем их в самом верху main.S, который примет следующий вид:
/* Адреса и значения регистров SP */
SPL = 0x3D
   SPL_VALUE = 0x5F
SPH = 0x3E
   SPH_VALUE = 0x02 /* 0x04 – для МК ATmega8 */

.data

.text
   .org 0
   .global main
      main:
         /* Указать на вершину стека */  
         LDI    r16, SPL_VALUE
         OUT    SPL, r16   
         LDI    r16, SPH_VALUE
         OUT    SPH, r16  
      main_loop:
         RJMP   main_loop
.end


Поскольку процедура указание на вершину стека исполняется лишь раз, инструкции её помещаются до метки main_loop.
В рамках ассемблера от GCC существует ещё один способ разбиения двухбайтного слова на байты – с помощью модификаторов:
lo8(слово) для выделения младшего байта слова
и
hi8(слово) для выделения старшего байта слова.
Тогда, достаточно прописать макроопределение старшего адреса SRAM (к примеру, RAMEND), а в коде применить к нему модификаторы:
/* Адреса регистров SP */
SPL = 0x3D
SPH = 0x3E
/* Адрес вершины стека */ 
RAMEND = 0x025F /* 0x045F – для МК ATmega8 */ 

.data

.text
   .org 0
   .global main
      main:
         /* Указать на вершину стека */  
         LDI    r16, lo8(RAMEND)
         OUT    SPL, r16   
         LDI    r16, hi8(RAMEND)
         OUT    SPH, r16  
      main_loop:
         RJMP   main_loop
.end

Осталось добавить последний обязательный элемент main.S – прописать по адресу вектора сброса указание ЦПУ перейти к началу основной функции при сбросе/подаче питания. Реализуется это при помощи той же инструкции RJMP с именем метки main в качестве операнда:
RJMP main

Чтобы выяснить, где именно в памяти программ должна быть размещена эта инструкция, обратимся к Таблице «Reset and Interrupt Vectors» даташита (страницы 48 – для ATtiny85 и 46 – для ATtmega8):

Рисунок 3. Выдержка из таблицы векторов прерываний МК ATtiny85 и Atmega8.

Как видно из второго столбца, векторы сброса обоих МК располагаются ровно по младшему адресу памяти программ – 0х0000, т.е. там, откуда в данный момент начинается основная функция. Сдвигаем последнюю вниз, а на её место вставляем нашу инструкцию. Теперь уже аргументу директивы .org можно дать осмысленное имя с помощью макроопределения Reset_vector, приведя программу к следующему виду:
/* Таблица векторов */
Reset_vector = 0x00
/* Адреса регистров SP */ 
SPL = 0x3D 
SPH = 0x3E 
/* Адрес вершины стека */ 
RAMEND = 0x025F /* 0x045F – для МК ATmega8 */ 

.data

.text
   .org Reset_vector
         RJMP   main
	         
   .global main
      main:
         /* Указать на вершину стека */  
         LDI    r16, lo8(RAMEND)
         OUT    SPL, r16   
         LDI    r16, hi8(RAMEND)
         OUT    SPH, r16  
      main_loop:
         RJMP   main_loop
.end


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

Поскольку регистры ввода-вывода (так же, как и вектора прерываний) являются системными элементами МК, вынесем все принятые выше макроопределения в созданный ранее хидер-файл. Чтобы компилятор понимал, где именно находятся макроопределения, в самом верху main.S необходимо прописать ещё одну директиву – .include – с заключённым в двойные кавычки именем хидер-файла в качестве аргумента
.include "attiny85.h" /* или "atmega8.h" */

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

С учётом вышеизложенного шаблоны хидер-файл и main.S для обоих МК окончательно будут выглядеть так:
Рисунок 4. Шаблоны хидер-файла и main.S для МК AVR.

а результат их компиляции примет следующий вид:
Рисунок 5. Размещение кода шаблона main.S в памяти программ МК AVR.

Обратите внимание, что адрес в первом столбце увеличивается каждый раз на 2. Связано это с тем, что длина регистра памяти программ AVR-8 — 2 байта и в каждый из них может быть записана, как вы помните из предыдущей части статьи, только одна инструкция.

Cortex M-4
Чтобы понять, как в МК ARM организовать указание на вершину стека и переход к началу основной функции при подаче питания/нажатии кнопки Reset, взглянем на выдержку из таблицы векторов, приведённую в «Cortex-M4 Generic User Guide» (раздел 2.3.4. «Vector table» на странице 2-23).

Рисунок 6. Выдержка из таблицы векторов прерываний Cortex M-4.


Из Рисунка 6 следует, что в случае с STM32F401 и nRF52832:
1. Для указания на вершину стека достаточно записать по младшему адресу памяти программ (т.е. аргумент директивы .org равен 0) начальное значение указателя стека – старший адрес SRAM – которое затем будет автоматически перенесено в регистр SP.

2. Через четыре байта после младшего адреса памяти программ (аргумент .org4) необходимо разместить значение адреса, по которому ЦПУ должен отправиться после сброса/подачи питания. В нашем случае это – имя метки main.

Для непосредственной записи в память программ чисел, которыми по сути и являются значения адресов, в случае с ARM применяется упоминавшаяся выше директива .word. Перед тем, как использовать её, рассчитаем старший адрес SRAM, исходя из того, что младший адрес SRAM для обоих МК – 0x20000000, а объём – 64K байт (смотрите Рисунок 32 из предыдущей части статьи).
1. 64 х 1024 = 65536.
2. Учтём младший адрес, вычтя из числа в п.1 единицу и получив 65535 или 0xFFFF в шестнадцатеричной системе счисления.
3. 0x20000000 + 0xFFFF = 0x2000FFFF.

Дадим макроопределения вектору сброса и адресу вершины стека (Reset_vector и RAMEND, соответственно), разместив их в самом верху main.S, который в итоге должен бы выглядеть так:
/* Таблица векторов */
Reset_vector = 0x04
/* Адрес вершины стека */
RAMEND =  0x2000FFFF

.data

.text
   .org 0 
      /* Указать на вершину стека */
      .word RAMEND
   
   .org Reset_vector 
      .word main

   .global main
       main:
       
       main_loop:
          B main_loop  	 
.end

Однако, необходимо внести ещё несколько корректив.

Во-первых, следует принять во внимание следующую запись из того же раздела «Cortex-M4 Generic User Guide»: The least-significant bit of each vector must be 1, indicating that the exception handler is Thumb code.
Поясню, что это означает.

В предыдущей части статьи упоминалось, что в STM32F401 и nRF52832 из двух возможных наборов инструкций используется набор Thumb. Так вот, чтобы процессор понимал, что обработчики прерываний (в том числе – основная функция, как обработчик вектора сброса) оперируют инструкциями из указанного набора, младший бит адреса, по которому направляет любой вектор прерывания, должен быть равен 1. Делается это очень просто – прибавлением единицы к упомянутому адресу, что в случае вектора сброса будет выглядеть как
.org Reset_vector
   .word main + 1


Во-вторых, по тем же соображениям, что и в случае с AVR-8, разумнее перенести макроопределения в хидер-файл (stm32f401.h или nrf52832.h), а в самом верху main.S прописать директиву .include с именем хидер-файла в двойных кавычках в качестве аргумента. Встретив на своём пути такую директиву, компилятор, как уже говорилось выше, поместит на её место содержимое файла, на который директива указывает.

В-третьих, синтаксис ассемблера для ARM по умолчанию требует наличия символа «#» перед каждым числовым значением (#3, #17 и т.п.), что, согласитесь, утомительно. Избежать этого позволяет применение директивы .syntax unified, которую мы поместим в первую строку хидер-файла.

Тогда, шаблоны хидер-файлов и main.S примут следующий окончательный вид:
Рисунок 7. Шаблоны хидер-файла и main.S для МК ARM.

Компиляция этих файлов даст следующий результат:

Рисунок 8. Размещение кода шаблона main.S в памяти программ МК ARM.

Как видите, в младшие адреса памяти программ обоих МК записано значение RAMEND. Далее, через четыре байта, размещён увеличенный на 1 адрес первой (и пока – единственной) инструкции, следующей за меткой main – инструкции B. Ещё через 4 байта находится сама инструкция B, имеющая операндом свой же адрес.
Шаг адресации в 4 байта объясняется, как и в случае с AVR-8, длиной регистра памяти программ Cortex M-4.

Оформление переменных в секции данных main.S

На первых порах, пока от вашей программы не требуется обработка больших потоков информации, количества регистров общего назначения, при рациональном их использовании, вполне достаточно для хранения временных данных. Особенно это касается AVR-8, где РОН — аж 32 штуки. В этом случае можно спокойно использовать шаблонный main.S, никак не меняя его структуру.
Однако, рано или поздно наступает момент, когда РОН перестаёт хватать и приходится обращаться к регистрам SRAM.
Вопрос о том, как именно происходит обращение из программы к SRAM, мы рассмотрим позже, пока же проясним, к каким дополнениям в сектор данных main.S это приводит.
Чтобы облегчить понимание сути вопроса, приведу пример кода записи числа 49 в регистры SRAM по адресам 0x0060 (AVR-8) и 0x20000000 (Cortex M-4).
AVR-8
LDI  r17, 49
STS  0x0060, r17

Cortex M-4
LDR  r0, =0x20000000
LDR  r1, =49
STR  r1, [r0]

Оставим без внимания механизм записи и назначение инструкций, тем более, что эти вопросы будут рассмотрены самым подробным образом чуть позже. На данный момент существенно, что адреса SRAM, по которым должно быть отправлено число 49 указаны непосредственно. И дело вовсе не в присутствии в коде магических чисел, хотя хорошего в этом тоже мало.

Гораздо большей проблемой является то, что при такой форме записи программист должен:
• во-первых, быть уверенным, что регистр SRAM, куда планируется сохранить число, свободен на данный момент,
• во-вторых, помнить по какому именно адресу сохранено число, чтобы извлечь его в нужный момент.

С этим можно смириться, пока речь идёт о двух-трёх числах, адреса хранения которых можно запомнить или, в крайнем случае, записать в листочке. А, если нужно хранить сотни чисел?

Ситуацию легко упростить, если:
1. Переложить на плечи компилятора заботы по поиску свободных регистров SRAM, не зацикливаясь при этом на том, по какому именно адресу будет сохранено число. На самом деле, какая нам разница, если для хранения числа 49 вместо 0x0060 и 0x20000000 будут выбраны адреса 0x0062 и 0x20000004? Главное, чтобы само число не затерялось в дебрях SRAM.

2. Использовать знакомые уже вам метки, которые ещё называют переменными, когда дело касается SRAM.

Тогда, необходимо прописать в секции данных main.S переменную в формате:
имя переменной: единица измерения памяти   начальное значение переменной
где,
• имя переменной выбирается, исходя из её назначения,
• единица измерения памяти, отводимой под хранение числа, определяется директивами .byte (8 бит) или .word (16 бит — для AVR-8, 32 бита — для Cortex M-4).
• начальное значение переменной обычно проставляется равным нулю. Кстати, если вам придётся работать в других версиях ассемблера (например, avrasm2, входящий в состав Atmel Studio), имейте в виду, что там этот параметр может означать количество памяти в единицах измерения.

Предположим, что наше число 49, для хранения которого достаточно байта, отражает текущую скорость и, дав переменной имя currentSpeed, оформим её в секции данных:
.data
   currentSpeed: .byte 0

а вышеприведённый пример перепишем следующим образом:
AVR-8
LDI  r17, 49
STS  currentSpeed, r17

Cortex M-4
LDR  r0, =currentSpeed
LDR  r1, =49
STR  r1, [r0]

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

В случае, когда необходимо обеспечить доступ к переменной из других файлов проекта, её объявляют глобальной посредством той же, что и в случае с функцией main, директивы .global.
Нередко по ходу программы возникает потребность хранения в SRAM массива однотипных данных. Тогда, в синтаксис переменной через запятую записывается соответствующее количество начальных значений.

Например, дополнение к имеющейся currentSpeed глобальной переменной adcValue для хранения массива из 3 значений уровня напряжения, измеренных АЦП, будет выглядеть так:
.data
   currentSpeed: .byte 0

   .global adcValue
   adcValue: .byte 0, 0, 0

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

Makefile

Как вы помните из Рисунка 35 в предыдущей части статьи, написанная программа ассемблируется, компонуется, преобразовывается в hex-файл, а затем загружается в МК. Для этого потребуется набрать в консоли четыре соответствующие командные строки, сопроводив каждую нажатием клавиши Enter:
AVR
avr-as main.S -mmcu=mcu_name -o main.o
avr-ld main.o -T LinkerScript.ld  -o main.elf		
avr-objcopy main.elf -j .text -j .data -O ihex main.hex	
avrdude -p mcu_name -c usbasp -P usb -e -U flash:w:main.hex:i

ARM
arm-none-eabi-as main.S -mcpu=cortex-m4 -o main.o
arm-none-eabi-ld main.o -T LinkerScript.ld -o main.elf
arm-none-eabi-objcopy main.elf -S -O ihex main.hex
openocd -f interface/stlink.cfg -f target/mcu_name.cfg -c "init" -c "reset init" -c "flash write_image erase main.hex" -c "reset" -c "exit"

и заменив предварительно надпись «mcu_name» на «attiny85», «atmega8», «stm32f4x» или «nrf52», в зависимости от выбранного вами МК.
Однако, уже по ходу обучения вам придётся сотни раз компилировать программу и прошивать её в микроконтроллер. Чтобы не тратить каждый раз время на набор вышеприведённых командных строк, перенесём их, не забывая о замене «mcu_name», в Makefile, добавив первой строкой надпись «all:»
AVR
all:
   avr-as main.S -mmcu=mcu_name -o main.o
   avr-ld main.o -T LinkerScript.ld -o main.elf		
   avr-objcopy main.elf -j .text -j .data -O ihex main.hex	
   avrdude -p mcu_name -c usbasp -P usb -e -U flash:w:main.hex:i

ARM
all:
   arm-none-eabi-as main.S -mcpu=cortex-m4 -o main.o
   arm-none-eabi-ld main.o -T LinkerScript.ld -o main.elf
   arm-none-eabi-objcopy main.elf -S -O ihex main.hex
   openocd -f interface/stlink.cfg -f target/mcu_name.cfg -c "init" -c "reset init" -c "flash write_image erase main.hex" -c "reset" -c "exit"

Обратите внимание, что командные строки должны предваряться символом табуляции.
Теперь достаточно набрать в консоли «make» и нажать клавишу Enter, чтобы запустить процесс компиляции программы и загрузки её в МК.

Имейте в виду, что программа make ищет Makefile в той же папке, что и файл, активизированный на данный момент в в Notepad++. Поэтому, прежде, чем запускать компиляцию, убедитесь, что активным является один из четырёх шаблонных файлов.

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

LinkerScript.ld

Думаю, вы заметили, что во второй командной строке Makefile, ответственной за компоновку, прописан файл сценария – LinkerScript.ld.
Структура указанного файла — одинаковая для всех четырёх рассматриваемых нами МК.
MEMORY
{
  ROM(rx): ORIGIN = Flash low address, LENGTH = Flash size
  RAM(rw!x): ORIGIN = SRAM low address, LENGTH = SRAM size
}

SECTIONS
{	
  .text : 
  {
    *(.text);
  } > ROM
 
  .data : 
  {
    *(.data);
  } > RAM AT > ROM 
}
где, значения переменных для ATtiny85, ATmega8, STM32F401 и nRF52832, соответственно, равны:
Flash low address – 0x00, 0x00, 0x08000000 и 0x00,
Flash size – 8K, 8K, 256K и 512K,
SRAM low address – 0x800060, 0x800060, 0x20000000 и 0x20000000,
SRAM size – 512, 1K, 64K и 64K.

В более поздних частях статьи мы вернёмся к LinkerScript.ld, чтобы детальнее обсудить содержимое этого файла, а также дополнения в него, позволяющие сделать процесс компоновки более гибким и эффективным. Пока лишь скажу, что именно на вышеприведённые значения переменных ориентируется компоновщик GCC при размещении кода в памяти программ МК, а данных — в SRAM.

Наборы шаблонных файлов для всех рассматриваемых нами МК выложены в архив.

Проверка шаблонных файлов

Проверим работоспособность шаблонных файлов, подключив предварительно МК к программатору, а тот – к компьютеру.

В случае с AVR необходимо соединить одноимённые пины МК и USBasp из Рисунка 9.
Рисунок 9. Распиновка МК ATmega8, ATtiny85 и программатора USBasp.

Для загрузки программы в МК ARM достаточно 4 выводов st-link v2, приведённых на Рисунке 10.
Рисунок 10. Распиновка программатора st-link v2.

На модуле STM32F401 ответные пины с такими же названиями выведены в отдельную гребёнку.
В nRF52832 вывод тактирования (SWCLK) имеет название TCK, а данных (SWDIO) – TMS.
Если соединение произведено верно, а программное обеспечение установлено и настроено так, как это описано в Главе «Установка и настройка ПО» предыдущей части статьи, то набрав в окне консоли «make» и нажав клавишу Enter вы должны получить следующее:

1. В папке с шаблонными файлами – 3 новых файла с именем main и расширениями .o, .elf и .hex.

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

Знакомство с инструкциями МК

Для успешного написания даже самой маленькой программы, необходимо, прежде всего, понимать, как работает та или иная инструкция.
Вернёмся к Рисунку 28 из предыдущей части статьи и сгруппируем по определённым признакам действия ЦПУ и других участников процесса, включая мои, необходимые для успешного исполнения программы, а затем переложим результат в плоскость инструкций реального МК:

1. Для указания на вершину стека и настройки печатающей машинки ЦПУ потребуется записать соответствующие числа в спецрегистр SP и регистры периферии. Сделать это он может, как вы помните, не сразу, а в два этапа: сначала загрузить число в РОН, а затем отправить по назначению. Такие действия ЦПУ можно назвать трансфером числа.

2. Бухгалтер должен складывать и вычитать разные числа, изредка перемножая их и применяя, при необходимости, свою железную логику. Короче говоря, он будет совершать логические и арифметические операции.

3. В определённых случаях ЦПУ должен переходить от одной строки программы к другой. Причём, переходы могут быть как безусловными («Перейти к адресу», «Вернуться»), так и условными («Если лампа включена…, иначе перейти»).

4. Чтобы процесс не вышел за рамки дозволенного, я вынужден контролировать деятельность ЦПУ — притормаживать его время от времени, например, или вообще отправлять спать.

Именно на указанные четыре группы можно условно разбить наиболее важные и употребляемые инструкции микроконтроллеров, о которых идёт речь в данной работе:
1. Трансферные инструкции.
2. Инструкции логических и арифметических операций.
3. Инструкции перехода.
4. Инструкции контроля за процессором.

Таблицы инструкций

Наборы инструкций рассматриваемых нами МК представлены в Таблицах «Instruction Set Summary» на страницах за номером 202 (ATtiny85), 311 (ATmega8) даташитов и 3-2 «Cortex-M4 Generic User Guide» (STM32F401 и nRF52832).
Выясним на примере одной из них — ADD — синтаксис, назначение и результат применения инструкции.

AVR-8

Синтаксис инструкции:
ADD  Rd, Rr

Инструкция использует в качестве операндов два РОН.
Результат применения инструкции — сумма чисел, предварительно загруженных в Rd и Rr — сохраняется в Rd.
По исполнению инструкции, в зависимости от начальных значений Rd и Rr, могут быть автоматически установлены в 1 биты Z, C, N, V или H регистра статуса SREG.
Время исполнения инструкции — 1 тик генератора тактовых импульсов.

Cortex M-4

Синтаксис инструкции:
ADD  {Rd,} Rn, Op2

Инструкция может использовать до трёх операндов, включительно. При этом, Rd (опционально) и Rn – регистры общего назначения, а в качестве Op2 может выступать как РОН, так и константа.
Если в инструкции участвуют 3 операнда, результат её применения сохраняется в Rd. В случае же с 2 операндами роль Rd берёт на себя Rn, куда и помещается результат. Чтобы прояснить сказанное, приведу несколько вариантов применения инструкции:
ADD  r1, r2, r3
суммирует содержимое r2 и r3, сохраняя результат в r1.

ADD  r1, r2, 5
суммирует содержимое r2 c числом 5, сохраняя результат в r1.

ADD  r1, r2
суммирует содержимое r1 и r2, сохраняя результат в r1.

ADD  r1, 7
прибавляет 7 к содержимому r1.

В зависимости от начальных значений операндов, применение инструкции может привести к автоматической установке в 1 битов N, Z, C или V регистра статуса PSR.

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

Организация связи между МК и программой Terminal

Воспользуемся одним из протоколов общения МК с внешним миром — UART, применение которого даст нам возможность передавать текущие значения регистров МК через последовательный порт в программу Terminal. Но, прежде необходимо осуществить следующие подготовительные действия:
1. Подключить USB-UART адаптер к компьютеру и в Диспетчер устройств\Порты Windows выяснить номер COM-порта, отведённого под адаптер.
2. Запустить программу Terminal и:
а) в поле COM Port выбрать порт из предыдущего пункта и нажать кнопку Connect. Если последняя изменила название на Disconnect, значит соединение с адаптером установлено.
б) в поле Baud rate выбрать 4800 – для AVR-8 или 9600 – для Cortex-M4,
в) произвести остальные настройки программы согласно Рисунка 11.
Рисунок 11. Настройки программы Terminal.

г) проверить работоспособность программы и адаптера, соединив выводы RX и TX последнего, а затем набрав в поле отправляемых данных Terminal любой символ на латинице и нажав кнопку Send. Если отправленный символ отобразится в поле принимаемых данных, с программой и адаптером всё в порядке.
3. Соединить вывод TX адаптера с соответствующим пином МК (2 или PB3 – для ATtiny85, 3 или PD1 – для ATmega8, PA2 – для STM32F401, P0.8 – для nRF52832). Поскольку единства в маркировке выводов USB-UART адаптеров среди их производителей нет, может оказаться, что на вашем адаптере нужно использовать вывод RX.
4. Подключить МК через программатор к компьютеру, как было описано в предыдущем разделе.

Перейдём к Notepad++.
Создадим новую папку с именем UART, куда перенесём файлы для выбранного МК из папки c таким же именем, помещённой в архив.
Четыре из скопированных файлов – знакомые вам шаблонные файлы, а пятый – предварительно скомпилированный объектный файл uart.o, который содержит две функции:
1. uartInit, ответственную за настройку протокола UART.
2. uartSendByte, обеспечивающую отправку в последовательный порт числа, предварительно загруженного в строго определённый РОН (r16 – для AVR-8, r1 – для Cortex M-4).

Открыв шаблонные файлы, вы обнаружите, что в два из них внесены дополнения.
Во-первых, в main.S добавлены строки с инструкциями вызова (RCALL – для AVR-8, BL – для Cortex M-4) функций uartInit и uartSendByte, а также c уже известной вам инструкцией загрузки (LDI – для AVR-8, LDR – для Cortex M-4) числа в указанный выше РОН. На данный момент это – произвольное число 54:
AVR-8
/* Настроить UART */
RCALL   uartInit	
/* Загрузить в r16 данные и отправить по UART */
LDI     r16, 54
RCALL   uartSendByte

Cortex M-4
/* Настроить UART */
BL    uartInit	
/* Загрузить в r1 данные и отправить по UART */
LDR   r1, =54
BL    uartSendByte

Во-вторых, чтобы включить файл uart.o в последующую компоновку, его имя внесено в соответствующие командные строки Makefile:
AVR-8
avr-ld main.o uart.o -T LinkerScript.ld -o main.elf

Cortex M-4
arm-none-eabi-ld main.o uart.o -T LinkerScript.ld -o main.elf

Если набрать в консоли «make» и нажать клавишу Enter, в поле принимаемых данных Terminal должно отразиться число 54 в трёх представлениях – шестнадцатеричном, двоичном и десятичном.
Рисунок 12. Отражение в Terminal числа 54, переданного из МК по UART.


Должен упомянуть пару особенностей:

1. Функции UART написаны для дефолтных значений частоты тактирования МК. Если у вас в Terminal отразилось что-либо иное вместо 54, скорее всего на вашем МК установлена иная частота тактирования. В этом случае пишите в комментариях к статье, чтобы вместе решить проблему.

2. Регистры Cortex M-4 – четырёх-байтные (32 бита), в то время как по протоколу UART за раз можно отправить лишь 1 байт (8 бит). Поэтому, в случаях, когда потребуется вывести в Terminal все 32 бита, придётся делать это по-байтно. Как именно осуществить такую передачу, будет показано ниже.

Итак, в нашем распоряжении оказался инструмент, позволяющий видеть в Terminal текущее значение r16 — для AVR-8 и r1 — для Cortex M-4. Всё, что нам остаётся — выстраивать код таким образом, чтобы перед вызовом функции uartSendByte в указанных РОН оказывался результат применения исследуемой инструкции. В плоскости синтаксиса инструкций это означает, что r16 и r1 всегда должны выступать в роли Rd.

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

1. Чтобы примеры программ не были неоправданно громоздкими, впредь будет приводиться лишь меняющийся участок кода (ниже вызова функции uartInit и до метки main_loop) и лишь в исключительных случаях — вся программа целиком.

2. В пункте 2.1. «Таблицы инструкций» упоминалось, что одна и та же инструкция Cortex-M4 может использовать как 2, так и 3 операнда. С целью облегчить вам переход от AVR к ARM, все последующие примеры применения инструкций для Cortex-M4 будут приводиться, если позволяет синтаксис, в варианте с двумя операндами.

3. Пусть вас не смущает, если в ходе самостоятельной работы, уяснив синтаксис той или иной инструкции и получив в Terminal требуемый результат, вы так и не поймёте практическую пользу от применения этой инструкции. Для начального этапа обучения это — нормально. С опытом и временем всё встанет на свои места.

Трансферные инструкции

В эту группу входят инструкции, обеспечивающие загрузку числа в РОН и его движение по регистрам разного типа, включая:
1. Регистры периферии МК, чтобы настраивать требуемые модули и обмениваться с ними данными.
2. Регистры SRAM, когда имеющихся РОН станет недостаточно для хранения текущей информации.

Отмечу, что под движением числа из регистра А в регистр Б, не зависимо от типа регистра (РОН, регистр периферии или SRAM) и операции (запись или чтение), подразумевается не физическое его перемещение с обнулением А, а лишь копирование.

AVR-8

Прежде, чем перейти непосредственно к инструкциям, следует упомянуть одну особенность этих МК. Заключается она в том, что некоторые инструкции, оперирующие регистрами общего назначения, не работают с младшими РОН (с r0 по r15). Об этом, кстати, уже упоминалось по отношению к инструкции LDI при оформлении процедуры указания на вершину стека. Поэтому, в примерах, приводимых далее, будут использоваться, для единообразия, старшие РОН — с r16 по r31.

Начнём с хорошо известной вам инструкции загрузки числа в РОН LDI:
LDI  Rd, K
где, K может принимать значения от 0 по 255.

Инструкции копирования числа из одного РОН в другой
MOV  Rd, Rr

и 16-битного слова из одной пары соседних РОН в другую
MOVW  Rd, Rr
где, Rd и Rr – младшие РОН каждой пары.

В применении последней инструкции есть одно ограничение: младший РОН пары должен быть чётным.
Проверить работу вышеупомянутых инструкций можно с помощью следующего кода
/* Загрузить в r20 число 5 */ 
LDI   r20, 5
/* Загрузить в r21 число 6 */ 
LDI   r21, 6
/* Скопировать слово из пары r20-r21 в пару r16-r17 */
MOVW  r16, r20
/* Убедиться, что число 5 скопировано из r20 в r16 */
RCALL uartSendByte 
/* Убедиться, что число 6 скопировано из r21 в r17 */
MOV   r16, r17
RCALL uartSendByte 
последовательно получив в Terminal числа 5 и 6.

Раз уж мы затронули тему регистровых пар, остановимся на ней чуть подробнее.
Необходимость объединения двух РОН в пару возникает при работе с числами, разрядность которых больше восьми. К примеру, максимальное разрешение АЦП рассматриваемых нами МК составляет 10 и для обработки десяти-битного результата измерений потребуется два РОН.

Для применения одних инструкций, оперирующих регистровыми парами, подойдут любые соседние РОН из диапазона r16r31, как в случае с рассмотренной выше MOVW. Однако, есть и такие инструкции, которые работают только со строго определёнными парами, составленными из самых старших РОН: с r26 по r31. В частности, к ним относятся трансферные инструкции косвенной адресации, которые будут рассмотрены ниже.
Указанные пары, а также составляющие их регистры имеют уникальные имена:

Рисунок 13. Регистровые пары МК AVR-8.

В ассемблере от GCC имена пар из Рисунка 13 и РОН, их составляющих, зарезервированы как ключевые слова, что даёт возможность обращения к ним непосредственно по этим именам.

Вернёмся к трансферным инструкциям.
Для обращения к иным, кроме РОН, регистрам памяти данных предусмотрено несколько инструкций.
Универсальная пара инструкций LDS/STS, обеспечивает доступ к регистрам как ввода-вывода (в т.ч. периферии), так и SRAM.
STS   k, Rr
LDS   Rd, k
где k – абсолютный адрес регистра, в который нужно записать (либо считать из него) число.

Согласно Рисунка 31 из предыдущей части статьи, регистры ввода-вывода располагаются в памяти данных ATtiny85 и ATmega8 по абсолютным адресам с 0x20 по 0x5F. Далее, начиная с адреса 0x60, следуют регистры SRAM.

Выберем по одному регистру ввода-вывода и SRAM (например, с адресами 0x52 и 0x60, соответственно) и оформим код перемещения произвольного числа 49 по цепочке регистров разного типа, используя, для чистоты эксперимента, на каждом этапе разные РОН:
 /* Загрузить в r18 число 49 */
LDI   r18, 49
/* Записать в регистр ввода-вывода с адресом 0x52 число из r18 */
STS   0x52, r18
/* Считать в r17 число из регистра ввода-вывода с адресом 0x52 */
LDS   r17, 0x52
/* Записать в регистр SRAM с адресом 0x60 число из r17 */
STS   0x60, r17
/* Считать в r16 число из регистра SRAM с адресом 0x60 */
LDS   r16, 0x60
/* и отправить по UART в Terminal */
RCALL uartSendByte

Появление после компиляции и загрузки кода в МК числа 49 в поле принимаемых данных Terminal будет свидетельствовать об успешном прохождении его по всем регистрам, а следовательно — о корректной работе пары LDS/STS с регистрами как периферии, так и SRAM.

Для работы только с регистрами ввода-вывода существует ещё одна пара инструкций — IN/OUT, одну из которых мы применяли в процедуре указания на вершину стека. Обращаются они по относительным адресам регистров ввода-вывода и исполняется процессором быстрее, чем LDS/STS, но имеют ограничение по максимальному значению относительного адреса регистра, с которым оперируют — 0x3F. Однако, для обоих рассматриваемых нами МК самый большой относительный адрес для регистров ввода-вывода, как следует из Таблицы «Register Summary» даташита, и есть 0x3F, так что в дальнейшем для доступа к указанным регистрам будем пользоваться парой IN/OUT.

Вычтем число 0x20 из значения абсолютного адреса 0x52 в примере выше, получив относительный — 0x32, а сам пример перепишем следующим образом:
LDI   r18, 49
OUT   0x32, r18
IN    r17, 0x32
STS   0x60, r17
LDS   r16, 0x60
RCALL uartSendByte

Точно так же рассмотренные инструкции записи/чтения работают в отношение и других регистров ввода-вывода и SRAM. И если вы решите убедиться в этом, меняя адреса в примере, помните о следующих ограничениях:

1. Запись в регистры ввода-вывода OSCCAL и SPMCR (относительные адреса — 0x31 и 0x37, соответственно, для обоих МК) может повлиять на работу микроконтроллера, поэтому делать это стоит лишь глубоко понимая назначение указанных регистров.

2. Запись в регистры ввода-вывода UCSRA, UCSRB, UCSRC, UBRRL (Для ATmega8) и DDRB (для ATtiny85) скорее всего собьёт настройки протокола UART и, как следствие, приведёт к отражению в Terminal некорректных данных.

Облагородим вышеприведённый пример, дав имена регистру ввода-вывода посредством макроопределения и адресу SRAM — с помощью переменной.
Согласно упомянутой таблицы для обоих МК по относительному адресу 0x32 располагается один из регистров таймера — счётчик TCNT0. Вынесем макроопределение адреса TCNT0, как системного регистра, в хидер-файл, добавив к уже имеющимся там:
TCNT0 = 0x32

Оформим переменную counterValue так, как это было показано в пункте 1.1.3 «Оформление переменных в секции данных main.S»:
.data
   counterValue: .byte 0

и придадим коду завершённый вид:
LDI   r18, 49
OUT   TCNT0, r18
IN    r17, TCNT0
STS   counterValue, r17
LDS   r16, counterValue
RCALL uartSendByte


Ради интереса вы можете проверить, по какому именно адресу в SRAM будет зарезервировано место под переменную counterValue. В данном случае, поскольку других переменных до counterValue объявлено не было, компилятор зарезервирует первый свободный, т.е. младший регистр SRAM. Убедиться в этом можно, прописав при записи в SRAM имя переменной counterValue, а при чтении — непосредственное значение адреса 0x60:
LDI   r18, 49
OUT   TCNT0, r18
IN    r17, TCNT0
STS   counterValue, r17
LDS   r16, 0x60
RCALL uartSendByte

LDS и STS являются инструкциями прямой адресации, поскольку в качестве одно из операндов используют непосредственный адрес регистра, хоть и завуалированный, как в примере выше, под именем переменной. В случае только с регистрами SRAM можно использовать и косвенную адресацию посредством пар инструкций LD/ST, а также знакомых вам регистровых пар и модификаторов lo8()/hi8():
LD  Rd, X
ST  X, Rr

Первая из приведённых инструкций считывает в Rd, число, хранимое в SRAM по адресу, значение которого записано в регистровой паре X.
Вторая — записывает число из Rr в регистр SRAM c адресом, значение которого записано в регистровой паре X.
Поясню сказанное, немного дополнив предыдущий пример.
LDI   XL, lo8(counterValue)
LDI   XH, hi8(counterValue)

LDI   r18, 49
OUT   TCNT0, r18
IN    r17, TCNT0
ST    X, r17
LD    r16, X
RCALL uartSendByte

Как видите, предварительно в составляющие регистровой пары X записаны байты значения адреса переменной counterValue, а затем число 49 так же, как и в прошлый раз, перемещается по регистрам. Но, при этом запись в переменную counterValue и чтение из неё осуществляется не прямо, а косвенно — через X-пару.
При работе с массивами данных удобно использовать модификации пары LD/ST с пост-инкрементом и пре-декрементом:
/* Инструкции с пост-инкрементом */
LD  Rd, X+
ST  X+, Rr

/* Инструкции с пре-декрементом */
LD  Rd, -X
ST  -X, Rr

Работают такие модификации очень просто: значение адреса, хранимого в Х-паре, увеличивается на 1 после исполнения инструкций с пост-инкрементом и уменьшается на такую же величину до исполнения инструкций с пре-декрементом.
Допустим, нам надо хранить в SRAM три значения уровня напряжения (1, 3 и 5 Вольт), измеренного АЦП, а затем считывать их в нужный момент.
Оформим соответствующим образом переменную adcValue для хранения массива из трёх байтов:
.data 
   adcValue: .byte 0, 0, 0

Как и в случае с counterValue компилятор, по причине отсутствия в секции данных других переменных зарезервирует в SRAM под adcValue регистры с адресами 0x60, 0x61 и 0x62.
Тогда, код записи в SRAM значений напряжения и последующего их чтения будет выглядеть так:
/* Загрузить в Х-пару адрес первого по порядку байта adcValue — 0x60 */
LDI   XL, lo8(adcValue)
LDI   XH, hi8(adcValue)

/* Записать в SRAM по адресу 0х60 первое значение напряжения — 1 */
/* а затем увеличить значение адреса в Х-паре на 1 до 0х61 */
LDI   r18, 1
ST    X+, r18
/* Записать в SRAM по адресу 0х61 второе значение напряжения — 3 */
/* а затем увеличить значение адреса в Х-паре на 1 до 0х62 */
LDI   r18, 3
ST    X+, r18
/* Записать в SRAM по адресу 0х62 третье значение напряжения — 5 */
/* не увеличивая при этом значение адреса в Х-паре */
LDI   r18, 5
ST    X, r18

/* Считать в r16 число 5 из регистра SRAM, значение адреса */
/* которого 0х62 хранится в данный момент в X-паре */
LD    r16, X
RCALL uartSendByte
/* Уменьшить значение адреса в Х-паре на 1 до 0х61, а затем */ 
/* считать в r16 число 3 из регистра SRAM по полученному адресу */
LD    r16, -X
RCALL uartSendByte
/* Уменьшить значение адреса в Х-паре на 1 до 0х60, а затем */
/* считать в r16 число 1 из регистра SRAM по полученному адресу */
LD    r16, -X
RCALL uartSendByte

В Terminal отразятся значения напряжения в порядке, обратном порядку записи: 5, 3, 1.
Можно, пользуясь лишь инструкцией с пост-инкрементом, записать данные, а затем считать их в прямом порядке, для чего необходимо после записи вернуть в Х-пару младший из отведённых под adcValue адресов:
LDI   XL, lo8(adcValue)
LDI   XH, hi8(adcValue)

/* Записать в ascValue значения напряжения */
LDI   r18, 1
ST    X+, r18
LDI   r18, 3
ST    X+, r18
LDI   r18, 5
ST    X, r18

/* Вернуть в Х-пару значение младшего адреса adcValue */
LDI   XL, lo8(adcValue)
LDI   XH, hi8(adcValue)

/* Считать значения напряжения в прямом порядке */
LD    r16, X+
RCALL uartSendByte
LD    r16, X+
RCALL uartSendByte
LD    r16, X
RCALL uartSendByte

Если требуется произвольно считать лишь одно число из массива, необходимо при повторной загрузке в Х-пару значения младшего адреса переменной прибавить к нему порядковый номер записи этого числа в переменную. Например, в коде выше при записи в adcValue число 1 шло нулевым по счёту, 3 — первым, а 5 — вторым. Тогда, чтобы считать из массива число 5 потребуется следующий код:
LDI   XL, lo8(adcValue)
LDI   XH, hi8(adcValue)

/* Записать в массив нулевым по счёту число 1 */
LDI   r18, 1
ST    X+, r18
/* Записать в массив первым по счёту число 3 */
LDI   r18, 3
ST    X+, r18
/* Записать в массив вторым по счёту число 5 */
LDI   r18, 5
ST    X, r18

/* Вернуть в Х-пару значение младшего адреса adcValue */
/* и прибавить к нему порядковый номер числа 5 — 2 */
LDI   XL, lo8(adcValue + 2)
LDI   XH, hi8(adcValue + 2)

/* Считать число 5 из массива */
LD    r16, X
RCALL uartSendByte

Вы нисколько не пожалеете, потратив своё время на детальное изучение работы инструкций косвенной адресации, поскольку:
1. Облегчите себе знакомство с Cortex M-4, т.к. в этих МК вся адресация при обращении к регистрам SRAM и периферии — косвенная.
2. При переходе на языки программирования высокого уровня (С/С++) для вас станет простой и понятной не самая очевидная тема указателей.

В завершение — о паре инструкций, вскользь упоминавшихся в предыдущей части статьи:
PUSH Rr 
POP  Rd

Применение первой приводит к записи числа из РОН в SRAM по адресу, значение которого записано в указателе стека SP, а второй — к чтению в РОН числа, хранимого в SRAM по адресу, на который указывает SP. То есть, в отличие от пары STS/LDS, которая оперирует с младшими адресами SRAM (сектор heap), PUSH/POP записывает/считывает число по старшим адресам SRAM, в пределах сектора stack.
Подробнее о нюансах использования пары PUSH/POP мы поговорим в одной из глав следующей части статьи, посвящённой подпрограммам и обработчикам прерываний, поскольку при оформлении именно этих структурных единиц обычно применяются указанные инструкции.

Cortex M-4

Здесь, как ни странно, всё — проще и однообразнее: трансфер числа по регистрам разного типа можно организовать посредством всего лишь одной, известной вам, инструкции LDR и парной ей STR.

Как загружать число в РОН, вы уже знаете:
LDR  r0, =K
где, K — 32-битное число от 0 по 0xFFFFFFFF.

Кроме того, для этих целей можно воспользоваться инструкцией MOV. В этом случае число не предваряется символом «=», в отличие от инструкции LDR:
MOV  Rd, K

Есть ещё одна разница между этими инструкциями: максимальное длина числа, с которым может оперировать MOV – 16 бит (т.е. числа от 0 по 65535), в то время как для LDR доступны 32-битные числа.
Модификация инструкции MOV применяется и для копирования числа из одного РОН в другой:
MOV  Rd, Rn

Обращение к регистрам как SRAM, так и периферии реализуется совершенно одинаково: посредством косвенной адресации при помощи модификаций всё той же инструкции LDR и парной ей STR.
/* Запись числа */    
STR  Rt, [Rn]
/* Чтение числа */
LDR  Rt, [Rn]

Обратите внимание на особенность синтаксиса обеих инструкций: имя Rn должно быть заключено в квадратные скобки.
Читаются записи следующим образом:
«Записать число, загруженное в РОН Rt, в регистр периферии/SRAM, значение адреса которого предварительно загружено в РОН Rn»
и
«Считать в РОН Rt число из регистра периферии/SRAM, значение адреса которого предварительно загружено в РОН Rn».

Для оперирования массивами данных в SRAM предусмотрена ещё одна модификация пары инструкций LDR/STR:
/* Запись числа в массив */    
STR  Rt, [Rn, offset]

/* Чтение числа из массива */
LDR  Rt, [Rn, offset]

Работают эти модификации следующим образом.
STR записывает число из РОН Rt по адресу в SRAM, значение которого складывается из двух составляющих: значения предварительно загруженного в РОН Rn и значения offset, выраженного в количестве байтов.
LDR считывает в РОН Rt число, хранимое в SRAM по адресу, значение которого складывается из таких же составляющих, как и в случае с STR.
При этом, значение offset может прописываться как непосредственно в виде числа:
STR  Rt, [Rn, число]
LDR  Rt, [Rn, число]

так и в виде имени третьего РОН Ro, где и хранится значение offset:
STR  Rt, [Rn, Ro]
LDR  Rt, [Rn, Ro]

Всё сказанное выше станет более понятным при наличии практического результата применения той или иной инструкции.

Одна из последующих глав будет полностью посвящена вопросам настройки и обмену данными с различными модулями периферии путём записи в соответствующие регистры и чтению из них. Поэтому, сейчас рассмотрим на примерах нюансы работы вышеприведённых инструкций в отношении лишь SRAM, выписав предварительно из Рисунка 32 предыдущей части статьи значение младшего адреса этой области памяти для обоих МК — 0x20000000.

Ниже приведён пример записи и последующего чтения числа 49 по младшему адресу SRAM с использованием, для исключения сомнений, разных РОН:
/* Загрузить в r2 число 49 */
MOV  r2, 49
/* Загрузить в r0 значение младшего адреса SRAM */
LDR  r0, =0x20000000
/* Записать число из r2 в SRAM по адресу, 
   значение которого записано в r0 */
STR  r2, [r0]	
/* Считать в r1 число, хранимое по адресу, 
   значение которого записано в r0 */
LDR  r1, [r0]
/* и отправить в Terminal по UART */
BL   uartSendByte

Как видите, для загрузки в r2 числа 49 применена не LDR, а подходящая для размера этого числа инструкция MOV.
Пропишем в секции данных переменную с произвольным именем var (от variable) согласно пункта 1.1.3 «Оформление переменных в секции данных main.S»:
.data	
   var: .byte 0

и перепишем с учётом этого наш пример:
MOV  r2, 49
LDR  r0, =var
STR  r2, [r0]	
LDR  r1, [r0]
BL   uartSendByte

Поскольку SRAM был полностью свободен на момент объявления var, компилятор зарезервирует под переменную младший адрес указанной области памяти. Проверить это можно записав в r0 перед чтением непосредственное значение адреса — 0x20000000:
MOV  r2, 49
LDR  r0, =var
STR  r2, [r0]	
LDR  r0, =0x20000000
LDR  r1, [r0]
BL   uartSendByte

Перейдём к массивам данных.
Добавим в секцию данных, не удаляя var, переменную adcValue для хранения трёх байтов последовательно измеренных АЦП значений напряжения — 3, 5 и 12 Вольт.
.data	
   var: .byte 0
   adcValue: .byte 0, 0, 0

и оформим код записи указанных значений напряжения в SRAM с последующим чтением:
/* Загрузить в r0 адрес младшего байта adcValue */
LDR  r0, =adcValue
/* Загрузить число 3 в r2 */
MOV  r2, 3
/* Записать число из r2 в SRAM по адресу, значение которого равно:
   адрес младшего байта adcValue плюс 0 байт */
STR  r2, [r0, 0]
/* Загрузить число 5 в r2 */
MOV  r2, 5
/* Записать число из r2 в SRAM по адресу, значение которого равно:
   адрес младшего байта adcValue плюс 1 байт */
STR  r2, [r0, 1]
/* Загрузить число 12 в r2 */
MOV  r2, 12
/* Записать число из r2 в SRAM по адресу, значение которого равно:
   адрес младшего байта adcValue плюс 2 байта */
STR  r2, [r0, 2]

/* Считать в r1 число из SRAM по адресу, значение которого равно:
   адрес младшего байта adcValue плюс 0 байт */
LDR  r1, [r0, 0]
/* и отправить в Terminal по UART */
BL   uartSendByte
/* Считать в r1 число из SRAM по адресу, значение которого равно:
   адрес младшего байта adcValue плюс 1 байт */
LDR  r1, [r0, 1]
/* и отправить в Terminal по UART */
BL   uartSendByte
/* Считать в r1 число из SRAM по адресу, значение которого равно:
   адрес младшего байта adcValue плюс 2 байта */
LDR  r1, [r0, 2]
/* и отправить в Terminal по UART */
BL   uartSendByte


Обратите внимание на следующие детали:

1. Поскольку длина чисел 3, 5 и 12 не превышает 2 байта, при загрузке их в r2 применена инструкция MOV.

2. Перед чтением чисел из SRAM нет необходимости повторно загружать в r0 значение младшего адреса adcValue, поскольку оно уже хранится там.

3. В секции текста нет упоминания о переменной var. Тем не менее, она объявлена в секции данных, причём до adcValue. Поэтому, компилятор зарезервирует для неё младший адрес 0х20000000, а под adcValue выделит следующие три регистра SRAM с адресами 0x20000001, 0x20000002 и 0x20000003.

Проверить это утверждение можно, применив модификацию инструкции LDR для чтения по непосредственному адресу:
/* Загрузить в r0 адрес младшего байта adcValue */
LDR  r0, =adcValue
/* Записать числа в SRAM */
MOV  r2, 3
STR  r2, [r0, 0]	
MOV  r2, 5
STR  r2, [r0, 1]				
MOV  r2, 12					
STR  r2, [r0, 2]

/* Считать через непосредственное указание адреса в SRAM */
/* число 3 */
LDR  r0, =0x20000001
LDR  r1, [r0]	
BL   uartSendByte	
/* число 5 */
LDR  r0, =0x20000002
LDR  r1, [r0]	
BL   uartSendByte	
/* число 12 */
LDR  r0, =0x20000003
LDR  r1, [r0]	
BL   uartSendByte

В случае использования имени третьего РОН в качестве offset наш пример примет следующий вид:
/* Загрузить в r0 адрес младшего байта adcValue */
LDR  r0, =adcValue

/* Загрузить в r2 число 3 */
MOV  r2, 3
/* а в r3 значение offset для хранения этого числа – 0 */
MOV  r3, 0
/* и сохранить число 3 в SRAM */
STR  r2, [r0, r3]	
/* Загрузить в r2 число 5 */
MOV  r2, 5
/* а в r3 значение offset для хранения этого числа – 1 */
MOV  r3, 1
/* и сохранить число 5 в SRAM */
STR  r2, [r0, r3]	
/* Загрузить в r2 число 12 */
MOV  r2, 12
/* а в r3 значение offset для хранения этого числа – 2 */
MOV  r3, 2
/* и сохранить число 12 в SRAM */
STR  r2, [r0, r3]	

/* Считать число 3 */
MOV  r3, 0
LDR  r1, [r0, r3]	
BL   uartSendByte
/* Считать число 5 */
MOV  r3, 1
LDR  r1, [r0, r3]	
BL   uartSendByte
/* Считать число 12 */
MOV  r3, 2
LDR  r1, [r0, r3]	
BL   uartSendByte

Вариант с хранением значения offset в третьем регистре – более гибкий, поскольку даёт возможность, лишь подбирая значение в r3, посредством одной и той же конструкции записывать в SRAM
STR  r2, [r0, r3]

или считывать оттуда
LDR  r2, [r0, r3]
нужный в данный момент элемент массива.

Пара инструкций
PUSH {reglist}
POP  {reglist}

Операнд reglist должен быть заключён в фигурные скобки и может содержать один РОН
PUSH {r0}
POP  {r0}

их список, оформленный через запятые
PUSH {r1, r3}
POP  {r1, r3}

или диапазон
PUSH {r0 - r4}
POP  {r0 - r4}

Работает эта пара так же, как и в случае с AVR-8: PUSH записывает число из РОН в сектор SRAM, именуемый стеком, по адресу, значение которого содержится в спецрегистре SP, а POP — считывает число из стека в РОН, следуя указаниям SP.

Есть и отличие, заключающееся в том, что операндом этих инструкций, помимо РОН, в Cortex M-4 могут выступать спецрегистры LR (регистр связей) и PC (программный счётчик). Это позволяет использовать пару PUSH/POP для организации переходов в подпрограмму и обратно. Именно в главе об оформлении кода подпрограмм, а также обработчиков прерываний, мы остановимся подробнее на этих инструкциях в следующей части статьи.

Инструкции логических и арифметических операций

Для начала повторим логические операции И, ИЛИ, ИСКЛЮЧАЮЩЕЕ ИЛИ, пройденные в пункте 4.4 «Логические операции и битовые маски» предыдущей части статьи, закрепив заодно на практике тему битовых масок и макроопределений.

AVR-8
AND  Rd, Rr
OR   Rd, Rr 
EOR  Rd, Rr 

Cortex M-4
AND  Rd, Rn
ORR  Rd, Rn 
EOR  Rd, Rn 

В качестве Rd, как вы помните, необходимо использовать r16 (AVR-8) и r1 (Cortex M-4), выбор же Rr и Rn – произвольный (пусть это будут r17 и r2, соответственно).
Пропишем в верхней части main.S (между директивами .include и .data) следующие макроопределения:
YELLOW_LAMP = 7
RED_LAMP = 4

Тогда, код формирования масок для попеременного включения/выключения гипотетических жёлтой и красной ламп, подключённых к выводам 7 и 4, с последующей отправкой их в Terminal будет выглядеть так:
AVR-8
/* Маска включения жёлтой лампы */
LDI   r16, (1 << YELLOW_LAMP)
RCALL uartSendByte		
/* Маска включения красной лампы дополнительно к жёлтой */
LDI   r17, (1 << RED_LAMP)
OR    r16, r17
RCALL uartSendByte						
/* Маска выключения жёлтой лампы без 
   изменения состояния красной */
LDI   r17, ~(1 << YELLOW_LAMP)
AND   r16, r17
RCALL uartSendByte	
/* Маска выключения красной лампы */
LDI   r17, (1 << RED_LAMP)
EOR   r16, r17
RCALL uartSendByte

Cortex M-4
/* Маска включения жёлтой лампы */
LDR  r1, =(1 << YELLOW_LAMP)
BL   uartSendByte		
/* Маска включения красной лампы дополнительно к жёлтой */
LDR  r2, =(1 << RED_LAMP)
ORR  r1, r2
BL   uartSendByte		
/* Маска выключения жёлтой лампы без 
   изменения состояния красной */
LDR  r2, =~(1 << YELLOW_LAMP)
AND  r1, r2
BL   uartSendByte		
/* Маска выключения красной лампы */
LDR  r2, =(1 << RED_LAMP)
EOR  r1, r2
BL   uartSendByte

Заметьте, что обновлять каждый раз значения r16 и r1 не требуется, поскольку, как вам известно из предыдущей части статьи, регистры памяти данных, включая РОН, сохраняют своё последнее значение до отключения питания.

После компиляции кода и загрузки в МК посредством комбинации make/Enter вы должны получить следующий результат:

Рисунок 14. Представление масок в Terminal.

Рассмотрим ещё несколько инструкций из этой же группы для каждого ядра.

AVR-8

Знакомые вам инструкции логического сдвига на 1 бит влево/вправо:
LSL  Rd
LSR  Rd

Пара инструкций CLR/SER для сброса в 0 или установки в 1, соответственно, всех битов РОН:
CLR  Rd
SER  Rd

Увеличить или уменьшить на 1 содержимое РОН позволяют инструкции INC/DEC, соответственно:
INC  Rd
DEC  Rd

Инструкции ADD/SUB для сложения/вычитания содержимого двух РОН:
ADD  Rd, Rr
SUB  Rd, Rr

Соответствующая инструкция пары ADIW/SBIW применяется, когда необходимо прибавить число K к содержимому регистровой пары или отнять его:
ADIW  Rdl, K
SBIW  Rdl, K

Ниже представлен пример использования отдельных из вышеприведённых инструкций:
/* Очистить r17 и r26 */
CLR   r17	
CLR   r26	
/* Загрузить числа 5 и 3 в r17 и r26, соответственно */ 
LDI   r17, 5
LDI   r26, 3
/* Сдвинуть влево число 3 в r26, получив 6 */
LSL   r26
/* Сложить содержимое r17 и r26, сохранив в r26 результат — 11 */
ADD   r26, r17
/* Увеличить на 1 до 12 содержимое r26 */ 
INC   r26
/* Вычесть число 2 из содержимого r26, обращаясь 
   к нему, как к младшему байту Х-пары */
SBIW  XL, 2				
/* Скопировать в r16 число 10 из r26, обращаясь 
   к последнему, как к отдельному РОН */
MOV   r16, r26
/* и отправить его в Terminal по UART */
RCALL uartSendByte


Cortex M-4

Инструкция обнуления диапазона битов РОН:
BFC  Rd, lsb, width
где,
lsb — младший бит диапазона,
width — ширина диапазона в битах.

В нижеследующем примере, число 15 из r1 трансформируется в 9 путём очистки двух битов, начиная с первого:
/* Загрузить в r1 число 15 или 0b1111 */
MOV  r1, 15
/* Обнулить 2 бита числа в r1, начиная с 1-го бита */
BFC  r1, 1, 2
/* Отправить в Terminal полученное число 0b1001 или 9 */
BL   uartSendByte

Инструкции логического сдвига на 1 бит влево/вправо:
LSL  Rd, Rs|n 
LSR  Rd, Rs|n

Количество сдвигов может быть записано как во второй РОН Rs, так и непосредственно в виде числа n.
Именно с помощью инструкции LSR можно реализовать по-байтную передачу в Terminal многобайтного числа. Учитывая, что по UART отправляется младший байт числа из r1, необходимо после первого вызова функции uartSendByte сдвигать вправо 8 раз содержимое указанного РОН и вновь вызывать функцию отправки, повторяя эти действия до тех пор, пока не будут переданы все байты.

В отличие от AVR-8 в Cortex M4 можно не только складывать и вычитать два числа, но и перемножать их:
/* Инструкция сложения */
ADD  Rd, Op2
/* Инструкция вычитания */
SUB  Rd, Op2
/* Инструкция умножения */
MUL  Rd, Op2

Во всех трёх инструкциях вторым операндом может выступать как непосредственно число, так и второй РОН, содержащий это число.

Для наглядности приведу пример применения некоторых из упомянутых инструкций с подробными комментариями:
/* Загрузить в r1 четырёх-байтное число 0x22198000 */
LDR  r1, =0x22198000
/* Загрузить в r2 множитель - число 2 */
MOV  r2, 2
/* Умножить содержимое r1 и r2, сохранив в r1 результат - 0x44330000 */
MUL  r1, r2
/* Загрузить в r2 двух-байтное число 0x2211 */
MOV  r2, 0x2211
/* Сложить содержимое r1 и r2, сохранив в r1 результат - 0x44332211 */
/* и отправить по UART его младший байт - 0x11 */
ADD  r1, r2
BL   uartSendByte
/* Загрузить в r2 значение количества сдвигов - 8 */
MOV  r2, 8
/* Сдвинуть 8 раз вправо число в r1, получив число 0x443322 */
/* и отправить по UART его младший байт  - 0x22 */	
LSR  r1, r2
BL   uartSendByte
/* Сдвинуть 8 раз вправо число в r1, получив число 0x4433 */
/* и отправить по UART его младший байт - 0x33 */	
LSR  r1, r2
BL   uartSendByte
/* Сдвинуть 8 раз вправо число в r1, используя */
/* непосредственную запись количества сдвигов (8) */
/* и отправить по UART полученное число - 0x44 */
LSR  r1, 8
BL   uartSendByte

Компиляция и загрузка в МК программы с такой вставкой приведёт, как видно из комментариев, к последовательному отражению в Terminal четырёх 8-битных чисел — 0x11, 0x22, 0x33 и 0х44.

Инструкции перехода

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

Придётся:
1. Отказаться от прерываний, в связи с невозможность перехода к обработчиками и возврата из них.
2. По тем же причинам, что и выше, отказаться от подпрограммы.
3. Выбросить сигнальную лампу, поскольку мы не можем обусловить действия ЦПУ в зависимости от её состояния. Как следствие, ЦПУ будет печать либо «А», либо «Б», ну или обе буквы последовательно одну за другой.
4. Смириться с тем фактом, что мы не можем зациклить действия ЦПУ.
5. Исключить прихлопы, притопы и приседания. Дело в том, что формулировка этих заданий — свёрнутая, а в развёрнутом виде будет содержать и условие, и инструкцию возврата. Например, развёртка инструкции «Сделать три притопа» выглядит как:
а) Притопнуть.
б) Сравнить общее количество притопов с числом 3.
в) Если сравниваемые числа равны, прекратить притопы, в противном случае — вернуться к пункту а).

В общем, вновь созданное управление, так и не успев приступить к работе, будет, скорее всего, распущено, поскольку польза от него — практически нулевая, как и от реальной программы без инструкций перехода.
В плоскости устройства МК цель применения всех инструкций перехода, как условных, так и безусловных, одна — изменить должным образом значение программного счётчика PC, который, как вы помните, содержит адрес следующей инструкции, подлежащей к исполнению ЦПУ.

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

Инструкции безусловного перехода


AVR-8
Инструкция, которую мы уже использовали, чтобы замкнуть цикл основной функции, а также для перехода к началу основной функции при сбросе/подаче питания:
RJMP  k
где, k — адрес перехода, который, для удобства и читабельности кода, заменяется меткой.

Точно так же, как и в случае с данными, для переходов по программе можно использовать косвенную адресацию с помощью следующей инструкции:
IJMP
применение которой приводит к копированию в регистр PC значения адреса, предварительно записанного в Z-пару, с последующим переходом ЦПУ по скопированному адресу.

Знакомая вам инструкция вызова подпрограммы
RCALL  k
где, k – адрес первой инструкции подпрограммы, выражаемый в коде меткой,

и возврата из неё
RET
которая прописывается в коде уже самой подпрограммы последней.

Посредством инструкции ICALL осуществляется косвенный вызов подпрограммы через регистровую пару Z. Знание механизма работы этой инструкции облегчит вам понимание темы указателя на функцию в языках высокого уровня С/С++.

Инструкция возврата из обработчика прерывания
RETI
так же, как и RET, записывается в самом конце обработчика.

Cortex M-4
Известная вам инструкция, позволяющая, среди прочего, организовать цикл:
B label
где, label — метка, указывающая на адрес первой инструкции, следующей за ней.

Пара инструкций для вызова подпрограммы и возврата из неё:
BL  label
BX  Rm
где,
label — метка, указывающая на адрес первой инструкции подпрограммы.
Rm, регистр содержащий адрес возврата, обычно — спецрегистр LR.

Инструкция BX применяется также и для возврата из обработчика прерывания.

Инструкции условного перехода

К этой подгруппе относятся инструкции, обуславливающие ветвление программы. Проще говоря, они позволяют реализовать в коде конструкции типа: «Если условие выполняется, перейти по адресу А, иначе — по адресу Б».
В принципе, все встречающиеся в программе «если» можно свести к одному из двух сравнений чисел:
1) Если одно число равно/не равно второму, в т.ч. нулю.
2) Если одно число больше/меньше второго.

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

Указание на вершину у нас уже реализовано, а включение печатающей машинки и трёхкратное постукивание по ней вполне правдоподобно изображает функция uartInit, поэтому займёмся циклом основной функции.

Перепишем действия ЦПУ через переходы и вышеуказанные сравнения и при этом:
а) Договоримся о замене букв «А» и «Б» на латинские «А» и «B», поскольку кириллицу ассемблер от GCC, увы, не понимает.
б) Чтобы продемонстрировать оба варианта сравнений, усложним задачу выбора между двумя буквами до «Если яркость сигнальной лампы меньше 15, перейти к набору «В», иначе — набрать «А».
в) Поскольку мы пока ещё не знаем, как оформляется подпрограмма, будем прописывать её содержимое в развёрнутом виде непосредственно в цикле основной функции.
г) Пропишем в определённых местах текста метки, не забывая о запрете давать им одинаковые имена, а переходы будем адресовать по имени соответствующей метки, подразумевая первую строку, следующую за ней.

Тогда цикл основной функции примет следующий вид:
main_loop:
   1. Сравнить значение текущей яркости лампы с пороговым 15.
   2. Если первое число меньше второго, перейти к метке type_B, 
      иначе — идти дальше по программе.
type_A:
   3. Набрать букву «А»
doClap_A:
   4. Хлопнуть в ладоши.
   5. Сравнить общее количество прихлопов с числом 2.
   6. Если сравниваемые числа не равны, продолжить прихлопы, перейдя
      к метке doClap_A, иначе — идти дальше по программе.  
doSquat_A:
   7. Притопнуть.
   8. Сравнить общее количество притопов с числом 3.
   9. Если сравниваемые числа не равны, продолжить притопы, перейдя 
      к метке doSquat_A, иначе — перейти к метке main_loop. 
type_B:
  10. Набрать букву «В».
doClap_B:
  11. Хлопнуть в ладоши.
  12. Сравнить общее количество прихлопов с числом 2.
  13. Если сравниваемые числа не равны, продолжить прихлопы, перейдя 
      к метке doClap_B, иначе — идти дальше по программе.
doSquat_B:
  14. Притопнуть.
  15. Сравнить общее количество притопов с числом 3.
  16. Если сравниваемые числа не равны, продолжить притопы, перейдя 
      к метке doSquat_B, иначе — перейти к метке main_loop.

Остаётся лишь выразить всё это дело на языке инструкций рассматриваемых нами МК.
Но, прежде введём макроопределения текущего и порогового значений яркости лампы, а также максимального количества прихлопов/притопов разместив их в самом верху main.S:
LAMP_BRIGHTNESS_CURRENT_VALUE = 12
LAMP_BRIGHTNESS_THRESHOLD = 15
CLAPS_MAX_NUM = 2
SQUATS_MAX_NUM = 3

Макроопределение LAMP_BRIGHTNESS_CURRENT_VALUE вводится по той причине, что нам пока ещё не доступен АЦП микроконтроллера. Поэтому, придётся эмулировать измерение текущего значения яркости лампы записью значения указанного макроопределения в один из РОН для последующего сравнения со значением LAMP_BRIGHTNESS_THRESHOLD.
Помимо этого, заранее изменим в настройках Terminal тип представления символа с «HEX» на «ASCII», чтобы после загрузки программы в МК наблюдать в Terminal именно «А» и «В», а не их шестнадцатеричные представления.

AVR-8
Для эмуляции измерения текущего значения яркости лампы используем r20:
LDI  r20,  LAMP_BRIGHTNESS_CURRENT_VALUE

Cравнение двух значений яркости и переход, связанный с результатом сравнения, оформим при помощи связки, состоящей из инструкции сравнения содержимого РОН с константой
CPI  Rd, K

и инструкции
BRLO k
читаемой как «Перейти по адресу k, если первое из сравниваемых чисел меньше второго».

CPI   r20, LAMP_BRIGHTNESS_THRESHOLD
BRLO  type_B

Код набора букв — очень простой и короткий:
type_A:
   LDI   r16, 'A'
   RCALL uartSendByte

type_B:
   LDI   r16, 'B'
   RCALL uartSendByte

Цикл прихлопов выразим через инструкцию сравнения чисел в двух РОН
CP   Rd, Rr

и инструкцию, которая читается как «Перейти, если числа в сравниваемых РОН не равны»
BRNE k
где, k – адрес перехода.

Визуализируем прихлопы, добавив отправку в Terminal символа «с» при каждом хлопке, и оформим код:
   /* Очистить r17 под хранение общего количества сделанных прихлопов */
   CLR   r17
   /* Загрузить в r18 максимальное количество прихлопов */
   LDI   r18, CLAPS_MAX_NUM
doClap:
   /* Хлопнуть в ладоши,  увеличив на 1 содержимое r17 */
   INC   r17
   /* Отправить в Terminal  символ «с» */
   LDI   r16, 'c'
   RCALL uartSendByte	
   /* Сравнить общее количество сделанных прихлопов с максимальным */
   CP    r17, r18
   /* Если не равны, продолжить прихлопы, перейдя к метке doClap
      иначе — идти дальше по программе */
   BRNE  doClap
где, doClap будет меняться на doClap_A или doClap_B, в зависимости от места расположения приведённого фрагмента кода.

Для разнообразия, воспользуемся при оформлении процедуры притопов другой связкой — из знакомой уже вам инструкции безусловного перехода RJMP и инструкции
CPSE  Rd, Rr
отличие которой от CP заключается в том, что CPSE не только сравнивает числа в РОН Rd и Rr, но и заставляет ЦПУ пропустить следующую за ней инструкцию, если сравниваемые числа равны.

С той же целью, что и в предыдущем случае, будем отправлять в Terminal символ «s» с каждым притопом.
Окончательно выглядеть всё это будет так:
   /* Очистить r17 под хранение общего количества сделанных притопов */
   CLR   r17
   /* Загрузить в r18 максимальное количество притопов */
   LDI   r18, SQUATS_MAX_NUM
doSquat:
   /* Притопнуть,  увеличив на 1 содержимое r17 */
   INC   r17	
   /* Отправить в Terminal символ «s» */
   LDI   r16, 's'
   RCALL uartSendByte
   /* Сравнить общее количество сделанных притопов с максимальным
      Если не равны, исполнить инструкцию RJMP doSquat, иначе 
      пропустить её и исполнить следующую — RJMP main_loop */
   CPSE  r17, r18
   RJMP  doSquat
   RJMP  main_loop

с соответствующей заменой doSquat на doSquat_A или doSquat_B.

Если собрать все приведённые выше фрагменты в единую программу, компиляция и загрузка её в МК приведёт к отражению в Terminal последовательности:
Bccsss, в случае, когда значение LAMP_BRIGHTNESS_CURRENT_VALUE меньше значения LAMP_BRIGHTNESS_THRESHOLD.
Аccsss — в обратном случае.

Но, поскольку действия ЦПУ вынесены нами в цикл основной программы, то последний, после загрузки кода в МК, начнёт, в полном соответствии со своим названием loop, лупить в Terminal одну такую последовательность за другой, причём слитно и с такой скоростью, что вызовет рябь в ваших глазах и, в конце концов, подвесит сам Terminal.

Чтобы избежать всего этого, предпримем следующие меры:
1. Скачаем в папку текущего проекта объектный файл delayAVR.o из архива.
2. Добавим имя указанного файла во вторую строку Makefile:
avr-ld main.o uart.o delayAVR.o -T LinkerScript.ld -o main.elf

3. Добавим в код перед каждой строкой «RJMP main_loop» следующие строки:
LDI   r16, 32
RCALL uartSendByte
RCALL delay

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

С учётом вышеизложенного, придадим программе окончательный вид:
.include "attiny85.h" /* или "atmega8.h" */

LAMP_BRIGHTNESS_CURRENT_VALUE = 12
LAMP_BRIGHTNESS_THRESHOLD = 15
CLAPS_MAX_NUM = 2
SQUATS_MAX_NUM = 3

  .data

  .text
    .org Reset_vector
      RJMP main

    .global main
     main:
         /* Указать на вершину стека */
         LDI   r16, lo8(RAMEND)
         OUT   SPL, r16
         LDI   r16, hi8(RAMEND)
         OUT   SPH, r16
         /* Включить и настроить печатающую машинку */
         RCALL uartInit
     main_loop:
         /* Измерить текущую яркость лампы 
            и сравнить её значение с пороговым */
         LDI   r20, LAMP_BRIGHTNESS_CURRENT_VALUE
         CPI   r20, LAMP_BRIGHTNESS_THRESHOLD
         /* Если первое число меньше второго, перейти к метке type_B,
            иначе - идти дальше по программе */
         BRLO  type_B
       type_A:
         /* Набрать букву «А» */
         LDI   r16, 'A'
         RCALL uartSendByte
         /* Хлопнуть в ладоши 2 раза */
         CLR   r17
         LDI   r18, CLAPS_MAX_NUM
       doClap_A:
         INC   r17
         LDI   r16, 'c'
         RCALL uartSendByte	
         CP    r17, r18
         BRNE  doClap_A
         /* Притопнуть 3 раза */
         CLR   r17
         LDI   r18, SQUATS_MAX_NUM
       doSquat_A:
         INC   r17	
         LDI   r16, 's'
         RCALL uartSendByte
         CPSE  r17, r18
         RJMP  doSquat_A
         /* Вывести в Terminal символ пробела */
         LDI   r16, 32
         RCALL uartSendByte
         /* Обеспечить задержку в 1 секунду */
         RCALL delay
         /* Перейти к началу цикла основной функции */
         RJMP  main_loop
       type_B:
         /* Набрать букву «В» */
         LDI   r16, 'B'
         RCALL uartSendByte
         /* Хлопнуть в ладоши 2 раза */
         CLR   r17
         LDI   r18, CLAPS_MAX_NUM
       doClap_B:
         INC   r17
         LDI   r16, 'c'
         RCALL uartSendByte	
         CP    r17, r18
         BRNE  doClap_B
         /* Притопнуть 3 раза */
         CLR   r17
         LDI   r18, SQUATS_MAX_NUM
       doSquat_B:
         INC   r17	
         LDI   r16, 's'
         RCALL uartSendByte
         CPSE  r17, r18
         RJMP  doSquat_B
         /* Вывести в Terminal символ пробела */
         LDI   r16, 32
         RCALL uartSendByte
         /* Обеспечить задержку в 1 секунду */
         RCALL delay
         /* Перейти к началу цикла основной функции */
         RJMP  main_loop
.end

Компиляция и загрузка программы в МК приведёт к следующей картине в Terminal:
Рисунок 15. Результат работы ЦПУ, в зависимости от соотношения текущего и порогового значений яркости лампы.


Уверен, вы догадались, что меняя в макроопределениях значения максимального количества прихлопов/притопов, можно регулировать соответствующее количество символов «с» и «s» последовательности.

Cortex M-4
В STM32F401 и nRF52832 проверку условий «равно/не равно» и «больше/меньше» с последующим переходом можно организовать посредством связки из инструкции CMP
CMP  Rn, Op2

и одной из четырёх следующих инструкций:
/* Перейти, если сравниваемые числа равны */ 
BEQ  label
/* Перейти, если сравниваемые числа не равны */ 
BNE  label
/* Перейти, если первое число меньше второго */ 
BLT  label
/* Перейти, если первое число больше второго */ 
BGT  label
где, label – адрес перехода, выраженный через метку.

Рассмотрим вариант связки CMP с инструкциями BNE и BLT, оставив вам для самостоятельной работы второй – с BEQ и BGT.

Фрагмент эмуляции измерения яркости лампы оформим через РОН r4:
MOV  r4, LAMP_BRIGHTNESS_CURRENT_VALUE

Код перехода к метке type_B в случае, когда текущее значение яркости меньше, чем пороговое, будет иметь следующий вид:
CMP  r4, LAMP_BRIGHTNESS_THRESHOLD
BLT  type_B

Поскольку символы «A» и «B», набираемые ЦПУ, по сути своей являются числами, то и отправлять их по UART следует таким же способом:
type_A:
   MOV  r1, 'A'	
   BL   uartSendByte

type_B:
   MOV  r1, 'B'	
   BL   uartSendByte

Так же, как и в случае с AVR-8, отметим, для наглядности, в Terminal факт каждого прихлопа и притопа символами «с» и «s», соответственно. При этом, в силу применения одних и тех же инструкций, код самих процедур прихлопов и притопов оформляется практически одинаково:
    /* Очистить r3 под хранение общего количества сделанных прихлопов */
   LDR  r3, =0
   /* Загрузить в r2 максимальное количество прихлопов */
   MOV  r2, CLAPS_MAX_NUM
doClap:
   /* Хлопнуть в ладоши, увеличив на 1 содержимое r3 */
   ADD  r3, 1
   /* Отправить в Terminal символ «с» */
   MOV  r1, 'c'
   BL   uartSendByte
   /* Сравнить общее количество сделанных прихлопов с максимальным */
   CMP  r3, r2
   /* Если не равны, продолжить прихлопы, перейдя к метке doClap
      иначе — идти дальше по программе */
   BNE  doClap

    /* Очистить r3 под хранение общего количества сделанных притопов */
   LDR  r3, =0
   /* Загрузить в r2 максимальное количество притопов */
   MOV  r2, SQUATS_MAX_NUM
doSquat:
   /* Притопнуть, увеличив на 1 содержимое r3 */
   ADD  r3, 1
   /* Отправить в Terminal символ «s» */
   MOV  r1, 's'
   BL   uartSendByte
   /* Сравнить общее количество сделанных притопов с максимальным */
   CMP  r3, r2
   /* Если не равны, продолжить притопы, перейдя к метке doSquat
      иначе — перейти к метке main_loop */
   BNE  doSquat
   B    main_loop

По тем же причинам, что и в случае с AVR-8, обеспечим отправку в Terminal символа пробела после каждой последовательности «набор буквы — притопы — прихлопы» и задержку в 1 секунду между такими последовательностями для чего:
1. Скачаем в папку текущего проекта объектный файл delayCortex.o из архива.
2. Добавим имя указанного файла во вторую строку Makefile:
arm-none-eabi-ld main.o uart.o delayCortex.o -T LinkerScript.ld -o main.elf

3. Поместим в код перед каждой инструкцией «B main_loop» следующие строки:
MOV r1, 32
BL  uartSendByte				
BL  delay

Тогда, main.S примет следующий вид:
.include "stm32f401.h" /* или "nrf52832.h" */

LAMP_BRIGHTNESS_CURRENT_VALUE = 12
LAMP_BRIGHTNESS_THRESHOLD = 15
CLAPS_MAX_NUM = 2
SQUATS_MAX_NUM = 3

  .data

  .text
    .org o
      /* Указать на вершину стека */
      .word RAMEND
      
    .org Reset_vector
      .word main + 1
     
    .global main 
      main:
          /* Включить и настроить печатающую машинку */
          BL   uartInit
      main_loop:
          /* Измерить текущую яркость лампы 
             и сравнить её значение с пороговым */
          MOV  r4, LAMP_BRIGHTNESS_CURRENT_VALUE
          CMP  r4, LAMP_BRIGHTNESS_THRESHOLD
          /* Если первое число меньше второго, перейти к метке type_B,
             иначе - идти дальше по программе */
          BLT  type_B
        type_A:
          /* Набрать букву «А» */
          MOV  r1, 'A'
          BL   uartSendByte
          /* Хлопнуть в ладоши 2 раза */
          LDR  r3, =0
          MOV  r2, CLAPS_MAX_NUM
        doClap_A:
          ADD  r3, 1
          MOV  r1, 'c'
          BL   uartSendByte	
          CMP  r3, r2
          BNE  doClap_A
          /* Притопнуть 3 раза */
          LDR  r3, =0
          MOV  r2, SQUATS_MAX_NUM
        doSquat_A:
          ADD  r3, 1	
          MOV  r1, 's'
          BL   uartSendByte
          CMP  r3, r2
          BNE  doSquat_A
          /* Вывести в Terminal символ пробела */
          MOV  r1, 32
          BL   uartSendByte
          /* Обеспечить задержку в 1 секунду */
          BL   delay
          /* Перейти к началу цикла основной функции */
          B    main_loop
        type_B:
          /* Набрать букву «B» */
          MOV  r1, 'B'
          BL   uartSendByte
          /* Хлопнуть в ладоши 2 раза */
          LDR  r3, =0
          MOV  r2, CLAPS_MAX_NUM
        doClap_B:
          ADD  r3, 1
          MOV  r1, 'c'
          BL   uartSendByte	
          CMP  r3, r2
          BNE  doClap_B
          /* Притопнуть 3 раза */
          LDR  r3, =0
          MOV  r2, SQUATS_MAX_NUM
        doSquat_B:
          ADD  r3, 1	
          MOV  r1, 's'
          BL   uartSendByte
          CMP  r3, r2
          BNE  doSquat_B
          /* Вывести в Terminal символ пробела */
          MOV  r1, 32
          BL   uartSendByte
          /* Обеспечить задержку в 1 секунду */
          BL   delay
          /* Перейти к началу цикла основной функции */
          B    main_loop
.end

Загрузка в МК откомпилированной программы даст абсолютно тот же результат, что и на Рисунке 15.

Инструкции контроля за процессором

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

AVR-8

Инструкция, встречая которую ЦПУ пропускает один тик тактового генератора, ничего при этом не делая:
NOP

Подавляющее большинство случаев применения этой инструкции — синхронизация того или иного процесса с точностью до одного тактового периода. К примеру, в настоящей статье по причине отсутствия аппаратной реализации UART для МК ATtiny85 использовался программный вариант этого протокола. Чтобы скорость отправки данных соответствовала одному из принятых стандартов — 4800 bps — в коде функции uartSendByte применялась инструкция NOP для обеспечения требуемого периода отправки одного бита.

Следующая инструкция отправляет процессор в сон:
SLEEP

Прежде, чем применять её, неплохо бы знать, как будить спящий МК.

Cortex M-4

Очень полезная инструкция
BKPT
которая позволяет останавливать ЦПУ в нужной точке программы при её отладке.

Инструкции для отправки МК в разные варианты сна:
WFE
WFI


Файлы

Файловый сервис недоступен. Зарегистрируйтесь или авторизуйтесь на сайте.


Заключение

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

На каком-то по счёту круге вы:

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

2. Обнаружите, что некоторые инструкции, если не дублируют другу друга, то взаимозаменяемы, в связи с чем выберете из них 10-15 интуитивно наиболее понятных, с которыми и будете работать чаще всего в последующем.

3. Начнёте «чувствовать» не только инструкции, но и микроконтроллер, явственно представляя себе, как «перемещается» ЦПУ из секции текста main.S в секцию данных, либо от одной метки к другой при исполнении той или иной инструкции.

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

Спасибо за внимание!

Камрад, рассмотри датагорские рекомендации

🌻 Купон до 1000₽ для новичка на Aliexpress

Никогда не затаривался у китайцев? Пришло время начать!
Камрад, регистрируйся на Али по нашей ссылке. Ты получишь скидочный купон на первый заказ. Не тяни, условия акции меняются.

🌼 Полезные и проверенные железяки, можно брать

Куплено и опробовано читателями или в лаборатории редакции.



 

Читательское голосование

Нравится

Статью одобрили 19 читателей.

Для участия в голосовании зарегистрируйтесь и войдите на сайт с вашими логином и паролем.
 

Поделись с друзьями!

 

 

Связанные материалы

 

Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 6... Продолжим разбор теоретических основ, без которых невозможно полноценное создание программ....
Схема на Датагоре. Новая статья Грызем микроконтроллеры. Урок 3.... Эту статью я начну с провокационного вопроса… А какую конструкцию на основе МК хотите создать ВЫ?...
Схема на Датагоре. Новая статья Грызем микроконтроллеры. Урок 2.... Предлагаю продолжить изучение микроконтроллеров… Второй урок будет посвящен по большей части...
Схема на Датагоре. Новая статья PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен... PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен пер. с англ. Евстифеева А. В. — М.:...
Схема на Датагоре. Новая статья Сборка и программирование мобильных роботов в домашних условиях. Жимарши Ф.... Сборка и программирование мобильных роботов в домашних условиях. Фредерик Жимарши. Издательство: НТ...
Схема на Датагоре. Новая статья Coil32 v9.0 - программа для расчета катушек индуктивности... Всем, кто занимался изготовлением (и ремонтом) приемников, передатчиков, акустических систем, ИБП,...
Схема на Датагоре. Новая статья Создание электронных книг в формате DjVu и PDF... Создание электронных книг в формате DjVu и PDF В данной публикации собраны статьи нескольких...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 7... Поговорим о прерываниях. Слово прерывание говорит само за себя, происходит остановка какого — то...
Схема на Датагоре. Новая статья Анатомия микроконтроллеров ATmega - 3. Прерывания.... Итак, наши светодиоды мигают, но мы не можем никак повлиять на программу, давайте добавим в схему...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 1... Каждый человек, который только начинает осваивать программирование микроконтроллеров, да и вообще...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 4... Сегодня рассмотрим программу “бегущих огней” и “бегущих теней”. Примеры “бегущих огней” можно найти...
Схема на Датагоре. Новая статья Микроконтроллеры. Связь с внешним миром. Часть 2... Здравствуйте, дорогие Датагорцы! По некоторым слабо зависящим от меня причинам не мог участвовать в...
 

Общаемся по статье 💬

Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК

Комментарии, вопросы, ответы, дополнения, отзывы

 

<
Читатель Датагора

galrad

Комментарий # 1 от 22-12-20, 0:50.
  • С нами с 23.08.2011
  • 104 комментария
  • 12 публикаций
 
С удовольствием прочитал статью, как всегда основательно и лаконично! Наконец то коснулись особенностей makefile, для многих его оформление и команды остаются за гранью понимания.

Но, мне кажется начинающие с ходу не осилят этот материал, нужно иметь определенную практику в программировании микроконтроллеров или знающего человека рядом, для понимания нюансов...

С другой стороны, для тех, кто переходит с 8 битных контроллеров на 32 битные, эта статья будет несомненным помощником.

Почему так думаю? Сужу по своему племяннику, который хорошо воспринял первую часть, но запутался во второй, мне пришлось разъяснять, казалось бы, простые вещи...Зато для меня эта статья как бальзам на душу, давно не встречаются хорошие статьи по программированию!

Как всегда, с благодарностью к Ерболу! 👍

<
Читатель Датагора

erbol

Комментарий # 2 от 22-12-20, 2:01.
  • С нами с 11.12.2014
  • 100 комментариев
  • 15 публикаций
 
Спасибо, Радик! 😊
Не легко уместить в рамках одной статьи все нюансы, не раздув её до книги приличных размеров: приходится сознательно отказываться от чего-либо. Собственно, не мне это объяснять человеку, выдавшему серию блестящих работ по программированию МК. Остаётся лишь надеяться, что твой труд, вкупе с информацией из других источников, даст возможность кому-то лучше понять тему МК.

Информация
Вы не можете участвовать в комментировании. Вероятные причины:
— Администратор остановил комментирование этой статьи.
— Вы не авторизовались на сайте. Войдите с паролем.
— Вы не зарегистрированы у нас. Зарегистрируйтесь.
— Вы зарегистрированы, но имеете низкий уровень доступа. Получите полный доступ.