В предыдущей части статьи мы провели подготовительную работу и вкратце разобрали принципы работы микроконтроллера, а завершающий её рисунок 35 определил маршрут нашего дальнейшего движения.
Остановимся подробнее на первом из этапов этого пути — программировании.
Содержание статьи / Table Of Contents
↑ Шаблонные файлы
Сформируем набор из четырёх шаблонных файлов, которые будут основой каждого проекта в дальнейшем, а заодно выясним, как код из текстового файла соотносится с памятью программ МК.Создаём в 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. Размещение кода каркаса 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. Через четыре байта после младшего адреса памяти программ (аргумент .org – 4) необходимо разместить значение адреса, по которому ЦПУ должен отправиться после сброса/подачи питания. В нашем случае это – имя метки 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 и для обработки десяти-битного результата измерений потребуется два РОН.
Для применения одних инструкций, оперирующих регистровыми парами, подойдут любые соседние РОН из диапазона r16 — r31, как в случае с рассмотренной выше 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 проверку условий «равно/не равно» и «больше/меньше» с последующим переходом можно организовать посредством связки из инструкции CMPCMP 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
↑ Файлы
🎁kod.zip 21.88 Kb ⇣ 123↑ Заключение
В заключение данной части статьи хотелось бы убедительно порекомендовать вам не экономить время и усилия на изучение инструкций МК. Постарайтесь не ограничиваться одним проходом, а пробовать несколько разных вариантов применения одной и той же инструкции.На каком-то по счёту круге вы:
1. Научитесь ещё до применения соответствующей инструкции предвидеть, какая трансформация произойдёт с числом и где оно в результате окажется.
2. Обнаружите, что некоторые инструкции, если не дублируют другу друга, то взаимозаменяемы, в связи с чем выберете из них 10-15 интуитивно наиболее понятных, с которыми и будете работать чаще всего в последующем.
3. Начнёте «чувствовать» не только инструкции, но и микроконтроллер, явственно представляя себе, как «перемещается» ЦПУ из секции текста main.S в секцию данных, либо от одной метки к другой при исполнении той или иной инструкции.
Всё это будет говорить о том, что вы готовы к следующему шагу на этапе программирования — настройке периферии и реализации первого полноценного проекта.
Но об этом в следующей части! Продолжение следует.
Спасибо за внимание!
Камрад, рассмотри датагорские рекомендации
🌼 Полезные и проверенные железяки, можно брать
Опробовано в лаборатории редакции или читателями.