В начало | Зарегистрироваться | Заказать наши киты почтой
 
 
 
 

Ассемблер для микроконтроллера с нуля. Часть 1. Начало пути

📆13 ноября 2020   ✒️erbol   🔎30.178   💬26  

Приветствую всех сограждан и читателей журнала Датагор! Пользуясь кучей времени, предоставленной коронавирусом (даже в нашествии такой гадости можно, при желании, найти положительные моменты), решил поднять и пересмотреть записи по микроконтроллерам (МК), которые я делал в разное время для своих детей. В итоге родилась идея объединить разрозненные материалы. Настоящая работа не является учебным курсом по программированию МК, хотя некоторые его элементы будут присутствовать. Скорее, это попытка осветить путь от написания программы до её загрузки в микроконтроллер. Я лишь расскажу о доступных практически для каждого средствах, необходимых для прохождения этого пути, расставлю «вешки» по всему маршруту и намечу направления. Соединять их вам придётся самостоятельно.

Не смотря на то, что знаком с МК я уже достаточно давно, знания мои в этой области далеки не то чтобы от совершенства, но даже от полноты. Это явилось причиной серьёзных сомнений: браться ли за столь обширную тему? В конце концов верх взяла мысль о том, что для кого-то эта информация (пусть и не полная) окажется важной и полезной.

Содержание статьи / Table Of Contents

Предисловие

Изначально, записывая «шпаргалки» для детей, я определил пару условий в изложении, чтобы оно, с одной стороны, не отпугнуло читающего в самом начале пути, а с другой — дало бы общее понимание работы микроконтроллеров и снизило барьеры при переходе от одного типа МК к другому:
• Рассматривать несколько МК разных архитектур и, желательно, разных производителей. При этом, давать такое объяснение принципа работы, которое подходило для всех рассматриваемых МК, но не было совсем уж популистским.
• При разборе практических примеров, обойтись без использования специализированной среды программирования (IDE). Тема МК сама по себе не самая примитивная, а необходимость вникать параллельно в работу нескольких IDE для разных МК оптимизма начинающему никак не прибавляет.
Эти же принципы, несколько расширив их, я решил перенести на статью.

Необходимые детали, устройства и ПО

Объектами изучения нам послужат микроконтроллеры:

AVR-8


ATTINY85-20PU DIP8



ATMEGA8A-PU


ARM (Cortex M-4)


Макетная плата STM32F401



Ebyte SoC nRF52832 тестовая плата

Если точно такой платы не найдёте, можно купить модуль nRF52832 и распаять отладочную плату.

nRF52832

Минимум пинов, которые необходимо вывести:
• GND,
• VCC,
• SWDIO и SWDCLK для программирования и отладки,
• Reset,
• несколько GPIO.
В файловый архив выложен мануал этого модуля.

Программаторы


USBasp


st-link v2


USB-UART адаптер CH340


Разное


Макетная плата


Соединительные провода разного типа


Потенциометры


Светодиоды


Установка и настройка ПО

В качестве среды программирования мы будем использовать обычный блокнот Notepad++, компилировать написанную программу посредством компилятора GCC от GNU. С отладкой кода нам помогут программы Tetminal и PuTTY, а с его загрузкой в МК — avrdude и openocd.

Среда программирования

1. Скачать и установить Notepad++.
2. При необходимости выбрать русский язык в Settings/Preferences/General/Localization.
3. В Плагины/Управление плагинами, во вкладке Доступные выставить галочки напротив Explorer и NppExec и нажать кнопку Установить.
4. Выставить галочки напротив Плагины/Explorer/Explorer, Плагины/NppExec/Show Console и Follow $(CURRENT_DIRECTORY). Слева и снизу от окна редактора появятся окна проводника и консоли, соответственно.
5. В Опции/Определение стилей выбрать подходящий стиль и для языков С, Makefile и Assembler настроить подходящие цвета и размеры шрифтов.
6. Чтобы настройки стиля вступили в силу в Синтаксисы выбрать A/Assembly, C/C или M/Makefile при работе с соответствующим файлом.


Компилятор и загрузчики

1. Создать на удобном для вас диске папки:
• GNU,
• GNU/AVR,
• GNU/AVR/avrdude,
• GNU/ARM,
• GNU/MinGW.
2. Скачать и распаковать в папку GNU/ARM файлы Arm GNU Toolchain 6.3.1 — Windows и openocd-20200701.7z.
Переименовать распакованные папки в armGnuToolchain и OpenOCD.
3. Скачать и распаковать в папку GNU/AVR файл AVR 8-bit Toolchain v3.62 — Windows.
Переименовать распакованную папку в avrGnuToolchain.
4. Скачать и распаковать в папку GNU/AVR/avrdude файл avrdude-6.1-mingw32.zip.
5. Скачать файл mingw-get-setup и запустить его, указав GNU/MinGW как папку для установки. В ходе установки будет запущен MinGW Installation Manager, в котором достаточно выбрать базовый пакет (Basic Setup) и нажать Installation/Apply Changes.


Если с установкой возникли проблемы, можно скачать готовый вариант папки MinGW из архива.
6. В Панель управления\Система\Дополнительные параметры системы\Переменные среды\Переменные среды для пользователя\Path\ добавить пути к папкам:
• GNU\ARM\armGnuToolchain\bin
• GNU\ARM\OpenOCD\bin
• GNU\AVR\avrGnuToolchain\bin
• GNU\AVR\avrdude
• GNU\MinGW\msys\1.0\bin


7. Перезагрузить компьютер.

ПО для отладки

Скачать и распаковать на удобном для вас диске программы Terminal 1.9b и PuTTY
В архив выложены rar-файлы всех упомянутых выше программ.

Драйверы программаторов

Драйвер USBasp также выложен в архив, а ST-link v2 устанавливается автоматически при первом подключении к компьютеру. Оба программатора после установки должны отобразиться в «Диспетчере устройств» Windows.

Немного теории

Как бы мне ни хотелось сразу перейти к практике, придётся сделать отступление в теорию. Попытаюсь ограничиться её минимумом, который облегчит понимание практического материала в последующем. Более того, уже в этой главе мы начнём знакомиться с некоторыми реальными командами МК AVR и ARM. Добавлю, что местами буду приводить англоязычный вариант терминов и аббревиатур: все-же чтения даташитов и мануалов вам не избежать, а их издатели упрямо не желают переходить на великий и могучий.

Регистры

В моём восприятии, как программиста, микроконтроллер — две большие кучи регистров. В первую кучу (память программ) мы загружаем программу в виде последовательностей нулей и единиц. При этом, значительная доля содержания программы сводится к своевременной записи правильного набора нулей и единиц в нужный регистр второй кучи (памяти данных), либо чтению из него этих наборов. В случае языка ассемблер мы имеем дело, по большей части, именно с регистрами памяти данных, поэтому выясним для начала, что они из себя представляют.
Уверен, вам известен такой элемент, как D-триггер (далее — триггер) и его основные свойства:
1. Выход триггера T может находиться только в одном из двух логических состояний — 1 (на выходе — напряжение питания) или 0 (на выходе — земля).
2. Значение (0 или 1) со входа D переносится (записывается) на выход T триггера по фронту синхронизирующего (тактового) сигнала С и сохраняется до следующей записи либо отключения питания.

Ассемблер для микроконтроллера с нуля. Часть 1. Начало пути


Если взглянуть на триггер в плоскости информации, можно сказать, что он хранит 1 бит данных со значением 0 или 1.
Соединив параллельно несколько триггеров, мы и получим регистр, разрядность или битность которого определяется количеством составляющих его триггеров. Совокупность линий данных триггеров регистра принято называть шиной данных. Тактовый сигнал для всех триггеров регистра — единый, т. е. перенос значений с линий шины данных на выходы (запись в регистр) происходит одновременно для всех триггеров. Нумерация битов регистра ведётся справа-налево, начиная с нулевого. Обычно, битность регистров МК кратна восьми (8, 16, 32).


Пример 8-битного регистра приведён на Рисунке 5.


Если необходимо увеличить объём хранимой информации, регистры объединяют в массивы. В этом случае, чтобы обеспечить запись в определённый регистр массива (или чтения из него), требуются линии выбора, которые все вместе именуются шиной адреса. Номера или адреса регистров массива также начинаются с нулевого значения. Пример массива из 4 регистров, в котором с целью экономии входы и выходы триггеров сведены в одну двунаправленную шину, представлен на Рисунке 6.


Чтобы не загромождать рисунок, впредь будем изображать регистр без линий шин данных/адреса и тактирования, обозначая его как rn (где, n — адрес регистра), а хранимые в нём данные — помещать внутрь квадратов, символизирующих триггеры.


Не берусь утверждать, что регистры МК организованы на базе именно D-триггеров. Более того, физическая суть записи в регистры памяти программ (flash-память) — совершенно иная. Тем не менее, принципы хранения и движения оперативной информации в микроконтроллерах я постарался передать верно.

О единицах измерения информации

В завершение — о единицах измерения хранимой в регистрах информации и некоторых общепринятых терминах.

Восемь бит информации составляют 1 байт. Биты и байты с самым маленьким номером в заданном диапазоне часто называют младшими, а с самым большим номером — старшими. То же самое, кстати, относится и к адресам. Для обозначения многобайтных данных иногда применяют термин «слово» («word»).

В случае, когда речь идёт о тысячах байт, может возникнуть лёгкая путаница. Дело в том, что исторически использовалась единица 1 килобайт, равная 1024 байт.
Думаю, поборники «чистоты во всём», возмущённые тем, что «кило» — 1024, а не 1000, добились, в конце концов, принятия двух единиц измерения:
1 килобайт (KB) — 1000 байт
1 кибибайт (KiB) — 1024 байт.
Не уверен, что жизнь программистов после этого стала стремительно улучшаться, и поэтому не стал бы тратить ваше время на подобную информацию, однако с таким многообразием единиц измерения согласились, кажется, не все.
При прочтении даташитов, вы убедитесь, что производители МК по-прежнему используют приставку K, подразумевая 1024 байт. Кроме того, в следующих главах нам предстоит делать расчёт адресов регистров, исходя из их общего количества, поэтому давайте договоримся: в рамках данной статьи 1K байт — это 1024 байт.

Системы счисления

Поскольку регистры ничего, кроме 0 и 1, содержать не могут, можно сказать, что микроконтроллер оперирует в поле двоичной системы счисления.
На Рисунке 7 нули и единицы, записанные в регистр, образуют 8-битное двоичное число 10101010. В программировании двоичное число предваряют символами 0b: 0b10101010.

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

Обычно, десятичные числа используют в программе, когда речь идёт о численном выражении какой-либо величины (например, 5 секунд или 12 тонн).

В случае шестнадцатеричной системы счисления перед числом прописываются символы 0x. Одно из бесспорных преимуществ шестнадцатеричного представления заключается в том, что каждые два разряда числа, начиная с младшего, составляют 1 байт. К примеру, байты числа 0×12A57F — 0×12, 0хA5 и 0×7F. О пользе этого свойства вы узнаете в главе, посвящённой практике. Добавлю, что эта система, помимо программы, широко используется в документации МК: адреса регистров в даташитах и мануалах представлены в шестнадцатеричной форме.

Для уверенности в том, что набранное вами десятичное или шестнадцатеричное число отражает требуемую комбинацию 0 и 1 в двоичном представлении, надо бы знать, как переводится число из одной системы счисления в другую. Однако, не буду забивать вам головы информацией о методах такого перевода: полагаю, что для начала вполне достаточно использовать калькулятор Windows.

Как видите, число, записанное в регистр на Рисунке 7 — 170 в десятичной и 0xAA в шестнадцатеричной системах счисления.

Логические операции и битовые маски

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


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

Логический сдвиг влево

Запись операции читается как «сдвинуть число m влево n раз». На Рисунке 9 приведён пример 2-кратного сдвига влево числа m = 3 (0b00000011), записанного в регистр r0.


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

Логический сдвиг вправо

Запись операции читается как «сдвинуть число m вправо n раз», а сама операция работает так же, как и предыдущая, но в обратном направлении.
Если на Рисунке 9 поменять местами верхний и нижний регистры, получится иллюстрация двукратного сдвига вправо числа 12 (0b00001100).

Операция НЕ

С применением этой операции значение каждого бита числа меняется на противоположное, т.е инвертируется, поэтому её часто называют инверсией. На рисунке 10 результат инверсии числа A = 15 (0b00001111) из регистра r0 записан в r1 как число С = 240 (0b11110000).


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

Операция И

Результат равен 1 только если оба бита пары равны 1.


Операция ИЛИ

Результат равен 1 если хотя бы один из двух бит пары равен 1.


Операция ИСКЛЮЧАЮЩЕЕ ИЛИ

Результат равен 1 только, если один из двух бит пары равен 1, а другой — 0.


Отмечу, что форма записи логической операций в Таблице 1 и соответствующая ассемблерная команда (инструкция) МК — не одно и то же. Кроме того, на иллюстрациях операций между двумя регистрами (И, ИЛИ, ИСКЛЮЧАЮЩЕЕ ИЛИ) результат, для наглядности, записывается в третий регистр, в реальности же он, обычно, сохраняется в первом из двух, участвующих в операции регистров.
В Таблице 2 приведены некоторые инструкции логических операций МК AVR и ARM с кратким описанием их работы.


О практической пользе логических операций

Чтобы вам было легче понять, а мне — объяснить суть вопроса, представим 8-битный регистр (Data Register, DR), входящий в состав микроконтроллера и ответственный за связь последнего с внешним миром. Подключим к выводам DR лампочки и договоримся о двух вещах:

1. Мы не можем делать с DR ничего, кроме записи/чтения числа в/из него, да и то не напрямую, а только через вспомогательный регистр r0. К чему такие сложности, вы поймёте из следующего раздела главы.

2. Логика нашего устройства — прямая т. е. число 1 в n-м бите DR включает соответствующую лампу, а 0 — выключает.

Включим жёлтую лампу на выводе 7, записав через r0 в регистр DR число 0b10000000 (128 в десятичной системе счисления), и пусть себе горит. Поскольку речь пойдёт о логике, забудем на время об электрической грамотности и изобразим наше устройство следующим образом:


Как включить красную лампу, подключённую к выводу 4, а затем выключить её, не меняя состояние включённой ранее жёлтой лампы или, если обобщить, как изменить состояние одного или нескольких битов регистра DR, не меняя состояния остальных? Легко сообразить, что для включения красной лампы надо добавить 1 в четвёртый бит и полученное число 0b10010000 (144) записать в DR.


Чтобы выключить красную лампу, не меняя состояния жёлтой, необходимо вернуть в DR число 128. Казалось бы, всё просто и можно вполне обойтись без логических операций. Но, обратим внимание на следующую не очевидную деталь: формируя числа для включения/выключения лампы, мы не должны забывать, что в седьмом бите в обоих случаях должна быть единица. Запомнить номер одного бита — 7 — не сложно так же, как и его состояние, тем более, что оно неизменно. К тому же, регистр у нас — всего один, да и картинка перед глазами облегчает дело.

В реальности картинок, как вы понимаете, нет, а регистров может быть сотни, к тому же 32-битных. Более того, по ходу программы их значение может меняться не один раз. Выходит, программист, чтобы поменять состояние одного бита, должен помнить текущее состояние оставшихся 31 битов для формирования правильного числа. И так — для сотен регистров. И это ещё не самое худшее. Не редко бывает так, что состояние одного или нескольких битов регистра определяется внешними устройствами (кнопками, сенсорами и т. д.) и тогда мы можем даже не знать, каково их текущее состояние.

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

Операция ИЛИ позволяет записать 1 в любой бит числа, не меняя состояния остальных битов, для чего потребуется:
1. Считать текущее значение DR в r0.
2. Записать в r1 число, в котором значение требуемого бита равно 1, а остальных — 0. В случае с красной лампой это — 4-й бит, а число — 0b00010000.
3. Применить операцию ИЛИ посредством инструкции OR r0, r1 (или ORR r0, r1 — для ARM), результат работы которой, как видно из Таблицы 2, запишется в r0.
4. Скопировать полученное число из r0 в DR.


Очевидно, операция дополнительно к жёлтой не включит ни одной лампы, кроме красной. Более того, если бы мы вдруг забыли, что красная лампа уже включена (1 в четвёртом бите DR до операции) и, тем не менее, провели операцию, это было бы лишь повторное включение и без того включенной лампы, что — не смертельно.
Если включить воображение, можно сказать, что в ходе операции число в r1 накладывается, подобно маске, на число в r0 для получения требуемого результата, поэтому далее будем использовать этот термин.

Операция ИСКЛЮЧАЮЩЕЕ ИЛИ меняет значение требуемого бита на противоположное, не затрагивая остальные биты, если:
1. Считать текущее значение DR в r0.
2. Записать в r1 такую же, как и в предыдущем случае, маску — 0b00010000.
3. Применить операцию ИСКЛЮЧАЮЩЕЕ ИЛИ посредством инструкции EOR r0, r1.
4. Скопировать полученное число из r0 в DR.
На Рисунке 17 приведён пример включения и последующего выключения красной лампы без изменения состояния жёлтой посредством двойного применения инструкции EOR r0, r1.


С помощью операции И можно обнулить любой бит числа, не меняя состояния остальных, для чего нужно:
1. Считать текущее значение DR в r0.
2. Записать в r1 маску, в которой значение требуемого бита равно 0, а остальных — 1, т. е. 0b11101111 в нашем случае.
3. Применить операцию И посредством инструкции AND r0, r1.
4. Скопировать полученное число из r0 в DR.


Как легко убедиться, будь любая другая лампа, помимо жёлтой, включена до операции, её состояние не изменилось бы и после. Опять же, если красная лампа будет изначально выключена (0 в четвёртом бите DR до операции), мы, применив по забывчивости операцию И, всего лишь получим попытку её повторного выключения.

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

• 0b00010001 — для операций ИЛИ/ИСКЛЮЧАЮЩЕЕ ИЛИ,
• 0b11101110 для операции И.

Есть ещё одна польза от применения логической операции И — возможность проверить текущее состояние любого бита регистра. Предположим, что включение/выключение красной лампы обусловлено положением внешнего переключателя, подключённого ко 2-му выводу DR. Тогда, следует периодически:
1. Считывать текущее значение DR в r0.
2. Записывать в регистр r1 маску, в которой 2-й бит равен 1, а остальные — 0, т. е. 0b00000100.
3. Применять операцию И посредством инструкции AND r0, r1.


Вы можете, меняя на Рисунке 19 содержимое r0 до операции, убедиться, что после операции в него всегда будет возвращаться число 0 (0b00000000), кроме единственного варианта — когда состояние 2-го бита DR, а значит и r0, до операции, равно 1. В этом случае в r0 после операции запишется число 0b00000100, что и будет сигналом для включения красной лампы. Во всех остальных случаях её следует выключать.
Приведённый пример проверки состояния справедлив и для комбинации битов. То есть, если бы включение красной лампы определяла комбинация из единиц в 0-м и 5-м битах регистра DR, то маской и числом в r0 после операции, обуславливающим включение красной лампы, будет 0b00100001.

Операции логического сдвига

Приведу самый простой пример их использования. Если записать в регистр r0 число 1 (0b00000001), а затем последовательно выполнять 7 инструкций сдвига влево (LSL r0) и 7 вправо (LSR r0), получится эффект «бегущего огня».

Ещё об одном распространённом варианте применения операции сдвига. Вы наверняка заметили из Рисунка 9, что единичный сдвиг влево равноценен умножению на 2, а вправо — делению на 2. Учитывая, что сдвиг исполняется МК быстрее, чем умножение/деление, программисты зачастую используют первую операцию взамен второй, когда скорость работы программы критична.
Операция сдвига влево, наряду с операцией НЕ, используется, помимо прочего, для формирования битовых масок, о чём — ниже.

Оптимизация маски, запись в регистр

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

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


Мнемоники (LDI и LDR) инструкции схожи, поскольку в их основе лежит одно и то же слово «load». Поэтому впредь наряду с «запись в регистр» будем использовать выражение «загрузка в регистр».
Глядя на число в правой части Рисунка 20. попытайтесь оценить, сколько непередаваемых эмоций может принести вам работа в лоб с 32-битными масками. Вообще то, общепринятые правила позволяют не отражать незначащие разряды, т. е. число 16 можно записать в обоих случаях как 0b10000, а компилятор сам дополнит недостающие слева нули в соответствии с разрядностью МК.

Ну, а если, всё же, все разряды значащие? Тогда никуда не денешься: придётся набирать этих 8- или 32-главых монстров в тексте программы или на калькуляторе, чтобы получить их десятичный (шестнадцатеричный) аналог. В любом случае риск поставить не туда 0 или 1 остаётся.

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


Приглядевшись повнимательнее к Рисунку 21, вы заметите две вещи:
1. В результате операции получилось число 16 (0b00010000), т. е. маска для включения красной лампы.
2. Число n в форме записи (т.е. — 4) фактически указывает на номер бита, в котором должен оказаться после операции младший бит сдвигаемого числа (в нашем случае — единица). В этом контексте можно сказать, что результатом операции n-кратного сдвига влево числа 1 всегда будет число, в котором n-й бит равен единице, а остальные — нулю. Имейте в виду, что все эти фокусы не проходят со сдвигом вправо.

Так вот, мы можем в инструкциях МК из Рисунка 20 вместо двоичного представления числа 16 записать форму 4-кратного сдвига влево числа 1:
,
а компилятор, встретив такую конструкцию, заменит её на число, в котором 4-й бит равен единице, а остальные — нулю, т. е. приведёт всё к виду на Рисунке 20.

Как быть с маской для погашения красной лампы — числом 0b11101111? Вы, скорее всего, уже поняли, что оно — инверсия предыдущей маски 0b00010000. Это позволяет использовать конструкцию

при компиляции, которой, с учётом скобок, определяющих последовательность действий, произойдёт следующее:
1. Формирование числа 0b00010000.
2. Инверсия числа из п. 1 в искомую маску 0b11101111.

В случае, если необходимо менять состояние одновременно несколько битов, не затрагивая остальные, соответствующие числа для маски выражается так:


Принимая во внимание скобки, компилятор в первом случае:
1. Сформирует число, в котором k-й бит равен 1, а остальные — 0.
2. Сформирует число, в котором m-й бит равен 1, а остальные — 0.
3. Сформирует число, в котором n-й бит равен 1, а остальные — 0.
4. Применит операцию ИЛИ между числами из п. п.1 и 2., в результате чего получится новое число с единицами в битах k и m и нулями — в остальных.
5. Применит операцию ИЛИ между числами из п. 3 и 4. с получение окончательного числа с единицами в битах k, m и n и нулями — в остальных.

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

Макроопределения

Остался один не обязательный, но очень полезный штрих.
Само по себе число в строке кода не несёт полезной информации, если только оно не количественное выражение чего-либо (2 слонёнка, 5 мартышек и т. п.). Увидев в чужой программе конструкцию подобную той, что на Рисунке 22, вы сможете догадаться лишь, что автор собирается что-то делать с 4-м битом. Замена формы логической операции на число 16 только прибавит загадочности. Наверное, потому и называют такие числа магическими, что выяснить их назначение можно только с помощью магии. Даже собственный код недельной давности, утыканный магическими числами, может ввести вас в ступор, а если вы рискнёте передать его в таком виде коллеге по цеху или заказчику, будьте готовы узнать о себе много нового и интересного.

Ситуацию можно улучшить, добавив к строке комментарий, что, мол, готовится включение красной лампы. Но, комментарий в каждой строке — перебор в другую сторону. Поэтому в случае на Рисунке 22. предпочтительнее использовать условное название или макроопределение для числа 4.
Все используемые в тексте программы макроопределения прописываются до их применения и в ассемблере от GCC имеют следующий общий вид:

НАЗВАНИЕ = значение


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

Для нас такая запись, согласитесь, более информативна, чем предыдущая, а компилятор все встреченные в коде макроопределения RED_LAMP заменит числом 4.

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

В заключение, чтобы вы могли в полной мере ощутить пользу от применения логических конструкций и макроопределений, приведу пример двух вариантов (с использованием чисел в форме двоичной и записи логических операций) части кода необходимого для включения/выключения внешних устройств, которые подключены к следующим выводам регистра DR микроконтроллера ARM:

• зелёный светодиод — к 0-му,
• синий светодиод — к 17-му,
• мотор — к 31-му.

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


Краткое описание работы микроконтроллера

На Рисунке 27 приведена блок-схема модели микроконтроллера.


Несмотря на то, что это — всего лишь модель, к тому же очень упрощённая, постараюсь на её примере дать функциональную картину МК, в объёме, достаточном для первоначального этапа, и в общих чертах справедливую как для AVR, так и для ARM. Случаи же, когда детали устройства и работы реального микроконтроллера и модели принципиально не совпадают, будут освещены по ходу текущей главы, либо — в практической части статьи.

Как видите, модель состоит из трёх основных блоков. Два из них (ядро и периферия) физически размещаются внутри корпуса МК, а третий (выводы МК) — снаружи. Для удобства объяснения и облегчения восприятия во всех трёх блоках выделены одним цветом отдельные элементы, которые так или иначе связаны между собой.

Блок выводов МК

Через этот блок МК общается с внешними устройствами. В контексте этого общения задача программиста заключается в том, чтобы в заданное время на определённом выводе МК:
• обеспечить наличие логических 1 или 0 (напряжения питания или 0В, соответственно),
• считать текущее состояние — логические 1 или 0.
• считать значение аналогового сигнала. Как правило, выводы МК для реализации этой функции жёстко определены. В нашей модели такой вывод — с порядковым номером 0.

Блок периферии

Назначение этого блока заключается в непосредственном исполнении задачи, определённой выше:
• Порт ввода/вывода (ПВВ, GPIO) обеспечивает запись на выводы МК, либо считывание с них цифрового сигнала — логических 1 или 0,
АЦП (ADC) измеряет уровень аналогового напряжения на выводе 0 МК.
Таймер считает поступающие на него тактовые импульсы. Зная период такта, мы можем вычислить общую продолжительность счёта, что даёт возможность с точность до одного тактового периода задавать время чтения информации с выводов МК, либо записи на них.

Обратиться к элементам блока периферии (настроить их или записать/считать данные) мы можем только через соответствующие регистры, расположенные в памяти данных ядра.
В реальных МК модулей периферии значительно больше, а их функциональные возможности — шире. Более того, они могут дублироваться (3 порта, 2 таймера, 5 АЦП и т. д.), но всё это не меняет сути дела.

Ядро

В нашей модели в состав ядра входят:
• Центральный процессор (ЦПУ). Именно для него мы пишем программу. В моменты, когда требуется произвести арифметическую или логическую операцию, ЦПУ привлекает арифметико-логическое устройство (АЛУ).
• Память программ. Сюда загружается написанная нами программа. В нашем случае память состоит из двадцати двух 16-битных регистров с адресами от 0 до 21. Программа это — набор инструкций, понятных ЦПУ. Длина инструкции нашего МК составляет 16 бит, поэтому в каждый регистр может быть записана лишь одна.
Содержимое памяти программ сохраняется даже, если питание МК отключено.
• Память данных. Как следует из названия, здесь хранятся данные. О том, какие именно — чуть позже. Организована память данных в виде 22 регистров, длиной 8 бит каждый.
Информация в памяти данных сохраняется только, если МК запитан, иначе все её регистры обнуляются. В реальном МК при сбросе/отключении питания отдельные регистры памяти данных могут принимать ненулевое значение, определённое производителем.
• Программный счётчик (ПС, PC) содержит адрес инструкции, которую ЦПУ должен исполнить следующей.

Генератор и шины

Помимо упомянутых блоков на схеме присутствуют:
• Генератор тактовых импульсов (ГТИ). Этот узел запускает работу МК и задаёт её скорость. Единственное, что действительно следует знать о ГТИ реальных МК на начальном этапе, так это — возможность выбора элемента, определяющего его частоту — внутренняя RC-цепочка или внешний кварцевый резонатор. О том, как осуществить этот выбор, мы поговорим в одной из последующих глав.
• 8-битная шина данных, через которую блоки МК обмениваются информацией. Ширина шины данных определяет разрядность МК.
• 16-битная шина команд, по которой ЦПУ считывает инструкции из памяти программ.

Взаимодействие элементов МК

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

Память данных

Как вы видите, первыми в этой памяти расположены два регистра общего назначения (РОН, GPR), знакомые вам r0 и r1. В реальных МК регистров общего назначения — более десяти. Я не случайно выделил эти регистры, ЦПУ и АЛУ одним синим цветом. Дело в том, что ни ЦПУ, ни АЛУ не имеют прямого доступа ко всем остальным, кроме РОН, регистрам памяти данных. Не существует команды для ЦПУ «записать число 5 в регистр данных ПВВ с адресом 3». Для реализации этой операции потребуется минимум две инструкции:
1. Загрузить число 5 в r0.
2. Скопировать число из r0 в регистр с адресом 3.

Точно также АЛУ не может сложить прямо числа, которые записаны, к примеру, в регистрах с адресами 10 и 11 или применить к ним логическую операцию. Для этого необходимо считать числа из указанных регистров в r0 и r1 и уже между ними проводить требуемую операцию.

Следом за РОН идут регистры периферии. В рассматриваемой модели их — по два на каждый модуль (регистр настройки и данных).

Регистры данных

Назначение регистров данных следующее:

ПВВ

По сути это — регистр DR из предыдущего раздела главы. Когда мы выводим данные вовне, значение (1 или 0) в n-ном бите регистра данных обуславливает логическое состояние (1 или 0, соответственно) на выводе МК с таким же номером. В случае же чтения данных извне ситуация обратная: логические 1 или 0 на n-ном выводе МК отражаются как 1 или 0 в бите регистра данных c порядковым номером n.

Таймер

По мере счёта значение регистра данных таймера увеличивается от нуля до 0b11111111 (255), а затем опять сбрасывается в 0. И так — до тех пор, пока тактирование таймера не будет отключено.

АЦП

Сюда АЦП записывает двоичное представление измеренного на выводе 0 МК значения аналогового напряжения.

Регистры настроек

Теперь — о регистрах настроек. Биты этих регистров отвечают за следующее:

ПВВ

Если значение n-го бита — 1, вывод МК с таким же номером работает как выход, 0 — как вход.

Таймер

• 0-й бит. Если значение бита — 1, тактирование таймера включено, 0 — выключено.
• 1-й и 2-й биты. Если комбинация их значений — 00, то частота тактирования таймера равна частоте ГТИ, 01 — частота ГТИ/2, 10 — частота ГТИ/64, 11 — частота ГТИ/128.
• 3-й бит. Если значение бита — 1, разрешено прерывание таймера — сигнала о том, что он досчитал до своего максимума и сбросился в ноль, 0 — прерывание запрещено.
• 4-й — 7-й биты не используются, т. е. зарезервированы.

АЦП

• 0-й бит. Если значение бита — 1, тактирование АЦП включено, 0 — выключено.
• 1-й и 2-й биты. Этими битами также, как и у таймера, регулируется частота тактирования АЦП.
• 3-й бит. 1 — разрешен сигнал (прерывание) АЦП о том, что измерение завершено и результат преобразования сохранён в регистре данных АЦП. 0 — прерывание запрещено.
• 4-й бит. С записью 1 в этот бит стартует измерение. Значение бита автоматически сбрасывается в 0 по завершению измерения.
• 5-й — 7-й биты зарезервированы.

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

Пример записи в регистры настроек периферии

Предположим, что мы решили собрать устройство на базе нашего МК, которое каждые 255 секунд с максимальной скоростью измеряет аналоговый сигнал от фоторезистора, подключённого к выводу 0, и, в зависимости от уровня освещённости, включает/выключает лампу на выводе 4.

Примем частоту ГТИ за 128Гц. Тогда в регистры настроек периферии нужно записать через РОН следующие числа:
ПВВ
0-й вывод МК должен работать как вход (значение соответствующего бита регистра настроек — 0), а 4-й — как выход (значение бита — 1). Поскольку направление работы остальных выводов нам не важно, настроим их как входы. Получаем число 0b00010000.

Таймер
• 0-й бит. Разрешаем тактирование — 1.
• 1-й и 2-й биты. Делим частоту ГТИ на 128, т. е. частота тактирования таймера будет 1Гц. Тогда, чтобы переполниться (досчитать до 255) и выдать прерывание ему понадобится как раз 255 секунд. Комбинация значений битов — 11.
• 3-й бит. Разрешаем прерывание таймера — 1.
Искомое число — 0b00001111.

АЦП
• 0-й бит. Разрешаем тактирование — 1.
• 1-й и 2-й бит. Нам нужна максимальная скорость измерения, т. е. частота тактирования АЦП. Отказываемся от деления частоты ГТИ. Комбинация — 00.
• 3-й бит. Разрешаем прерывание АЦП — 1.
В итоге — число 0b00001001.

Алгоритм программы будет выглядеть так:
1. Настраиваем периферию.
2. В цикле, при каждом прерывании от таймера записываем 1 в 4-й бит регистра настроек АЦП, запуская тем самым измерение освещённости. Чтобы не затереть при этом уже записанное в этот регистр число 0b00001001, применяем логическую операцию ИЛИ и маску 0b000010000.
3. По прерыванию от АЦП считываем значение из регистра данных АЦП. Если оно меньше порогового (которое, например, равно 40), включаем лампу, записав 1 в 4-й бит регистра данных ПВВ, в противном случае — гасим.

Специальные регистры

Регистр статуса SREG (Status register)

После регистров периферии располагаются два специальных регистра.
Полную информацию о назначении битов регистра статуса SREG (Status register) можно легко найти в сети, мы же обсудим лишь те из них, которые пригодятся в практических примерах.

Бит I. Чуть выше мы говорили о битах в регистрах настройки периферии, разрешавших прерывания таймера и АЦП. Эти биты называют битами локального разрешения прерывания. Бит I — в принципе разрешает использовать механизм прерываний, т. е. это — бит глобального разрешения прерываний, без установки программистом в 1 которого локальные разрешения прерываний силу иметь не будут.
Следует отметить, что бит I — особенность МК AVR. В ARM для глобального контроля за прерываниями выделен целый модуль, называемый Nested Vectored Interrupt Controller (NVIC).

Биты Z и N также доступны программисту как для чтения, так и для записи. Однако, для нас, в первую очередь, интересно их свойство автоматически устанавливаться в 1 в определённых случаях:

Бит Z устанавливается в 1 автоматически, если в результате какой-либо операции АЛУ образуется ноль. К примеру, нам нужно узнать, равно ли значение регистра данных таймера 48. Для этого:
1. Считываем значение регистра данных таймера в r0.
2. Загружаем в r1 число 48.
3. Вычитаем значение одного РОН из другого.
4. Если в результате вычитания Z примет значение 1, числа равны.

К автоматической записи в бит N единицы приводит образование отрицательного числа после какой-либо операции АЛУ, что даёт возможность использовать его для проверки условий «больше-меньше». Если в результате вычитания значений двух РОН бит N устанавливается в 1, вычитаемое больше уменьшаемого, и наоборот. Именно этот бит помог бы нам сравнить текущий уровень освещённости с пороговым в вышеприведённом примере.

Указатель стека - SP (Stack Pointer)

Второй специальный регистр в памяти данных — указатель стека SP (Stack Pointer). Функция этого регистра настолько сильно взаимосвязана с работой программы, что будет правильнее, если я расскажу вам о нём ниже, при обсуждении памяти программ. Скажу лишь, что сразу после сброса/подачи питания в SP должен быть записан адрес старшего регистра памяти данных (в нашей модели это — 21), поэтому оба регистра окрашены в единый серый цвет.

SRAM, STACK и HEAP

Осталось выяснить, для чего служит область памяти SRAM.
Представьте, что в рассмотренном выше примере устройство должно реагировать не на мгновенное значение освещённости, а на среднее от результатов 5 измерений. Куда размещать массив данных до их усреднения?
Для этого и пригодится сектор SRAM, названный кучей (heap). В общем случае данные размещаются по направлению от младшего адреса (10) кучи к старшему (19).

Функция сектора стек (stack) опять же тесно связана с работой программы и о ней — ниже.

Память программ, стек и программный счётчик

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

Согласно документа, названного для солидности «Основная функция (main)», ЦПУ, придя утром на работу должен включить и настроить печатающую машинку, стукнув по ней три раза, а затем в цикле набирать букву «А» или «Б», в зависимости от того, включена сигнальная лампа на стене или отключена. После набора каждой буквы необходимо совершить два прихлопа и три притопа.
Природа одарила меня ленью и, чтобы не повторять два раза инструкции о притопах и прихлопах, я вынес их в отдельный список под названием «Подпрограмма» и в итоге получил следующее:

«Основная функция (main
1. Включить печатающую машинку.
2. Стукнуть по машинке 3 раза.
3. Если сигнальная лампа включена, набрать букву «А». Иначе — перейти к строке 6.
4. Выполнить подпрограмму.
5. Перейти к строке 3
6. Набрать букву «Б».
7. Выполнить подпрограмму.
8. Перейти к строке 3.

«Подпрограмма»
1. Сделать два прихлопа.
2. Сделать три притопа.
3. Вернуться к основной функции и продолжить её.

Во время исполнения инструкций любого из списков могут произойти следующие события (назовём их «прерываниями»):
1. Звон колокола. При этом автоматически настройки печатающей машинки сбрасываются, а сама она — отключается.
2. Лай собаки.
3. Звонок в дверь.
4. Стук в окно.

ЦПУ, при наступлении любого из указанных событий, должен завершить исполняемую инструкцию, затем всё бросить и отреагировать на каждое событие соответствующим образом. Тут я решил несколько усложнить задачу и сделать реакцию на прерывания двухступенчатой, приложив к каждой ступени отдельный список. Первый список — «Вектор прерывания» — состоит всего лишь из одной инструкции, предписывающей перейти ко второму списку, называемому «Обработчик прерывания», причём для удара колокола обработчиком прерывания служит основная функция. Выглядеть всё это будет так:

«Вектор удара колокола»
1. Перейти к началу основной функции.

«Вектор лая собаки»
1. Перейти к обработчику лая собаки.
«Обработчик лая собаки»
1. Мяукнуть.
2. Вернуться к брошенному делу и продолжить его.

«Вектор звонка в дверь»
1. Перейти к обработчику звонка в дверь.
«Обработчик звонка в дверь»
1. Подпрыгнуть.
2. Вернуться к брошенному делу и продолжить его.

«Вектор стука в окно»
1. Перейти к обработчику стука в окно.
«Обработчик стука в окно»
1. Сделать 5 приседаний.
2. Вернуться к брошенному делу и продолжить его.

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

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

Опасаясь, что с такими способностями и памятью он рано или поздно натворит бед (перепутает списки, вернётся не к тому делу или просто заснёт на рабочем месте), я принимаю превентивные меры:
1. В помощь ЦПУ придаю бухгалтера-АЛУ для ведения счётных операций.
2. Говорю своему охраннику с тёмным прошлым и кличкой «ГТИ», чтобы он задавал исполнителю и бухгалтеру ритм работы, пиная их со строгой периодичностью.
3. Объединяю все списки в один большой («Программа»), в котором:
• присваиваю блокам названия прежних списков,
• применяю сквозную нумерацию строк (впредь, вместо термина «номер строки» будем употреблять «адрес» или «адрес инструкции»).
• меняю все инструкции перехода на однообразное «Перейти к адресу n».


Критический взгляд на Программу, даёт понимание того, что всё ещё осталась пара моментов, которые могут сбить с толку ЦПУ:
• Инструкция «Выполнить подпрограмму» не указывает, где последняя находится.
• Инструкция «Вернуться» стала короче, чем «Вернуться к брошенному делу и продолжить его», но от этого не стала менее загадочной.

Поэтому, исполнителю передаётся маленький прибор («Программный счётчик» или «ПС»), на дисплее которого отображается:
• на инструкции «Перейти к адресу n» — адрес перехода,
• на инструкции «Выполнить подпрограмму» — адрес первой инструкции подпрограммы (13),
• на инструкции «Вернуться» — адрес возврата,

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

Памятуя, что писать и читать вновь нанятый работник всё же умеет, я вручаю ему пару листков:
• Первый листок (пусть он называется «SRAM») разлинован на 12 строк с номерами или адресами от 10 по 21. При необходимости, ЦПУ может записывать данные (количество сделанных приседаний или промежуточные результаты расчётов бухгалтера, к примеру) по адресам 10-19 («куча»). Но, ему под страхом смерти запрещено использовать две последние строки, окрашенные в серый цвет («стек»).


• Во второй листок («Указатель стека» или «SP») исполнитель должен всего лишь один раз, перед включением печатной машинки, записать старший адрес SRAM (т.е. — 21), о чём в программу добавлена соответствующая инструкция (по адресу 4). Кстати, такая запись называется указанием на вершину стека.

Работа ПС, SP и SRAM определённым образом взаимосвязана:
• При каждом ударе колокола все они обнуляются, вместе с отключением печатной машинки.

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

• Каждый раз, когда в программе встречается инструкция «Выполнить подпрограмму», автоматически:
а) адрес следующей инструкции записывается в SRAM по адресу, указанному в SP,
б) значение SP уменьшается на единицу.
в) стартовый адрес подпрограммы (13) записывается в ПС.

• Каждый раз, когда в программе встречается инструкция «Вернуться», автоматически:
а) значение SP увеличивается на единицу.
б) значение в SRAM по адресу, указанному в SP, записывается в ПС.

Чтобы быть спокойным, я решаю промоделировать ту или иную рабочую ситуацию: сажаю ЦПУ, АЛУ и ГТИ в одну комнату, включаю сигнальную лампу и бью в колокол. Как вы помните, ПС, SP и SRAM при этом обнуляются, а печатающая машинка отключается со сбросом настроек.

Итак,
1. ЦПУ видит в ПС число 0 и переходит на этот адрес программы.
2. По адресу 0 — переход на адрес 4. ЦПУ убеждается, что в ПС указан тот же адрес и переходит.
3. Осуществляется запись в SP последнего адреса SRAM (21). ПС при этом увеличивается на 1 — до 5.
4. ЦПУ включает печатающую машинку и настраивает её (инструкции по адресам 5,6).
5. Поскольку сигнальная лампа включена, набирается буква «А».
6. На инструкции по адресу 8 («Выполнить подпрограмму»):
• Адрес следующей инструкции (9) записывается в SRAM адресу 21, поскольку именно это число записал ЦПУ в SP чуть ранее.
• Значение SP уменьшается (декрементируется) на 1, т. е. вершина стека теперь — 20.
• В ПС записывается стартовый адрес подпрограммы (13), куда и отправляется ЦПУ.

7. Сделав в требуемых количествах прихлопы и притопы (не забывайте, что ПС на этих инструкциях просто инкрементируется), ЦПУ подходит к адресу 15 (инструкция «Вернуться») и здесь:
• значение SP увеличивается на единицу — до 21.
• Значение из SRAM по адресу 21 (а там у нас — 9) записывается в ПС.
• ЦПУ выполняет инструкцию по указанному в ПС адресу 9, т. е. переходит к адресу 7. В этот момент я выключаю сигнальную лампу.

8. Поскольку лампа выключена, ЦПУ переходит по адресу 10.
9. Во время набора буквы «Б» раздаётся стук в окно. При этом:
• Адрес следующей инструкции (11) записывается в SRAM по адресу 21 (именно до этого значения увеличился SP в п.7).
• Значение SP декрементируется до 20.
• Адрес вектора стука в окно (3) записывается в ПС.
• ЦПУ завершает печатать букву «Б» и переходит по адресу в ПС — 3.

10. Перейдя по адресу 20 (об этом было указано в инструкции по адресу 3), ЦПУ прилежно приседает, а затем осуществляется возврат к адресу 11 в порядке, описанном п. 7.

Вдумчивый читатель может заинтересоваться, почему ЦПУ запрещено делать записи в строке 20 SRAM, если она в приведённых выше десяти пунктах ни разу не использовалась? Да и без указателя стека можно вполне обойтись: при инструкции «Выполнить подпрограмму» и прерываниях просто записать адрес инструкции, следующей за текущей, в 21-ю строку SRAM, а потом благополучно вернуть в ПС.
Давайте примем такой вариант и представим следующее.
1. ЦПУ набрал букву «А» и перешёл к подпрограмме. Адрес следующей инструкции (9) записывается в SRAM по адресу 21.
2. Во время второго прихлопа (адрес — 13) звонят в дверь и:
• При наличии стека и указателя на него адрес следующей инструкции (14) был бы записан в SRAM по адресу 20. Но, мы от них отказались, поэтому число 14 записывается по адресу 21 затирая предыдущую запись (9).
• Адрес вектора звонка в дверь (2) записывается в ПС.
3. ЦПУ переходит к адресу 2, оттуда — к адресу 18, подпрыгивает и возвращается к адресу, записанному в 21-й строке SRAM, т. е. 14.
4. В это время к шефу компании прибывает иностранная делегация и он решает похвастать перед ними тем, как замечательно работает новое управление.
5. Шумной толпой они вваливаются в комнату и видят, что бухгалтер и охранник, разинув рты, уставились на ЦПУ, который безостановочно притоптывает потому, что после каждых трёх притопов вновь возвращается к адресу 14, а адрес 9, куда он должен был в конце концов вернуться, затёрт.

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

Ещё несколько слов о стеке

1. Все мы имели дело с детской пирамидкой и помним, что кольцо, надетое последним, снимается первым. Стек, как вы наверняка заметили, работает по тому же методу LIFO (Last In First Out): значение, записанное последним, считывается первым.

2. В нашей модели, с целью упростить объяснение и облегчить его восприятие, размеры стека и кучи чётко определены, а ЦПУ в приказном порядке запрещены записи в стек. В реальных микроконтроллерах нет инструментов (специальной инструкции или аппаратного механизма) для разграничения стека и кучи. Кроме того, запись в стек возможна не только автоматически (при переходах в подпрограмму или обработчик прерывания): и в AVR, и в ARM имеется инструкция PUSH rn, которая сохраняет текущее значение n-го РОН в вершину стека. Таким образом, с ростом объёма сохраняемой информации куча и стек движутся навстречу друг другу и может, в конце концов, произойти их наложение: либо стек «продавит» кучу, либо куча «сорвёт» вершину стека.

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

Избежать их позволят несколько простых правил:
• старайтесь не увлекаться вложенными подпрограммами,
• в программах на ассемблере контролируйте использование инструкций PUSH и парной ей POP.
• в программах на С/С++ не злоупотребляйте глобальными переменными.
• при необходимости сохранения в куче постоянного потока данных применяйте циклический буфер.

Ну что же, вроде как всё работает. Я, как напоминание о моих трудах, вешаю на стену увеличенные копии схемы программы, SP и SRAM, рассаживаю троицу по рабочим местам и делаю фотографию для истории.


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

Реальные МК

Перед тем, как закончить с теорией, приведу некоторую информацию по реальным МК, рассматриваемых в данной работе.
Прежде всего — о документации, знакомиться с которой, рано или поздно, вам придётся в любом случае.
В случае с ATtiny85 и ATmega8 вполне достаточно внимательного изучения даташитов.
Для МК на базе ARM информация по ядру и периферии разнесена:

STM32F401

1. STM32F401 Datasheet.
2. STM32F401 Reference Manual.
3. STM32 Cortex-M4 Programming Manual.

NRF52832

1. nRF52832 Product Specification.

Cortex-M4

1. Cortex-M4 Generic User Guide.
2. Cortex-M4 Technical Reference Manual.
3. ARMv7-M Architecture Reference Manual
Указанные выше документы выложены в архив, и все последующие ссылки будут делаться именно на них.

О памяти и спецрегистрах микроконтроллеров


AVR-8


На рисунке 31. представлены карты памяти ATtiny85 и ATmega8.

Как видите, всё — очень близко к модели из Рисунка 27, за исключением количества регистров.

Память программ
Для обоих МК объём этой памяти составляет 8К или 8 * 1024 = 8192 байт. Организована она в виде массива из 16-битных или 2-байтных регистров в количестве 8192 / 2 = 4096 штук с адресами от 0 (0×0000) по 4095 (0×0FFF). Длина инструкций ATtiny85 и ATmega8, как и в модели, составляет 16 бит, т. е. каждый регистр может содержать лишь одну инструкцию. Набор инструкций, доступный программисту, представлен в Таблице «Instruction Set Summary» на страницах даташита за номером 202 (ATtiny85) и 311 (ATmega8).

Память данных
Регистры памяти данных обоих МК — 8 битные.
Количество регистров общего назначения — 32 с адресами от 0 по 31 (0×001F). Несмотря на то, что РОН имеют адреса, доступ к ним возможен и непосредственно по именам (r0, r1, r16 и т. д.), что обычно и делается.
Следующие 64 регистра памяти данных с адресами от 32 (0×0020) по 95 (0×005F) — так называемые регистры ввода-вывода (Input/Output Registers), включая регистры периферии и спецрегистры SREG и SP.
С адресацией регистров ввода-вывода AVR существует один нюанс. Адреса, указанные выше (0×0020 — 0×005F) — абсолютные. Сдвинув их на 32 позиции, можно получить относительные адреса — от 0 (0×0000) по 63 (0×003F). Соответственно, предусмотрены два набора инструкций для чтения/записи по абсолютным и относительным адресам регистров ввода-вывода.
Полный перечень регистров ввода-вывода и их адреса (абсолютные или относительные) можно узнать из Таблицы «Register Summary» на страницах 200 (ATtiny85) и 309 (ATmega8) даташита. Отмечу, что для ATtiny85 в указанной таблице приведены лишь относительные адреса регистров, а для ATmega8 — и абсолютные (в скобках), и относительные.
Объём SRAM составляет 512 и 1024 байт для ATtiny85 и ATmega8, соответственно.

Обратите внимание на несколько важных цифр, которые будут использоваться нами впоследствии:
1. Старшие адреса SRAM ATtiny85 и ATmega8 — 0×025F и 0×045F, соответственно. Именно эти значения нам предстоит в первых строках программы записывать в SP для указания вершины стека.
2. Младший адрес памяти программ обоих МК — 0×0000. Начиная с этого адреса будет загружаться в микроконтроллер написанная нами программа.

ARM (Cortex-M4)

Выдержки из карт памяти STM32F401 и nRF52832, наряду с Cortex M-4, приведены на Рисунке 32.

Компания ARM, как следует из рисунка, определяет границы блоков памяти (периферии, SRAM и программ), выделив на каждый по 0.5G байтов. Производители же МК на базе ядра ARM (в нашем случае — STMicroelectronics и Nordic Semiconductor), не выходя, обычно, за рамки этих ограничений, определяют стартовый адрес и объём каждого типа памяти, требуемый как для удовлетворения потребностей разработанной ими периферии, так и для эффективной работы всего микроконтроллера в целом.
Адресация регистров всех типов памяти — сквозная.
По аналогии с AVR определим наиболее важные для нас адреса карт памяти.
1. SRAM обоих МК имеет объём 64K байт и начинается с адреса 0×20000000.
2. Младший адрес памяти программ STM32F401 — 0×08000000, а nRF52832 — 0×00000000.


13 регистров общего назначения (r0 — r12), и 4 спецрегистра (указатель стека SP, регистр статуса программы PSR, программный счётчик PC и регистр связи LR, о назначении которого вы узнаете позже) в адресном пространстве не отражены и доступ к ним в программе осуществляется, как и в случае с РОН AVR, непосредственно по именам.


Адреса и наименования регистров периферии приводятся в конце раздела по каждому модулю периферии в «STM32F401 Reference Manual» и «nRF52832 Product Specification».
Набор инструкций для обоих МК можно найти в «Cortex-M4 Generic User Guide» (Раздел 3.1 «Instruction set summary»). Кроме того, для STM32F401 эта информация представлена в Разделе 3 «STM32 Cortex-M4 Programming Manual».
В последующих главах нам пригодится следующая информация касательно инструкций, рассматриваемых в данной работе МК.

Для 32-битных МК на базе ядра ARM предусмотрено два набора инструкций:
1. ARM, длина инструкций которого составляет 32 бита.
2. Thumb с инструкциями длиной 16 бит, призванный минимизировать размер программы после компиляции, а следовательно — объём flash-памяти, требуемой для её размещения.
Микроконтроллеры на базе ядра Cortex M-4, в том числе STM32F401 и nRF52832, используют второй набор — Thumb.

Дорожная карта

Теперь, когда мы располагаем всеми необходимыми инструментами и базовыми знаниями о работе МК, самое время определиться с маршрутом.
Схематично полный путь, который нам предстоит пройти, можно представить следующим образом.

Говоря «полный», я имею в виду случай, когда программа содержит участки, написанные как на языке Си, так и на ассемблере.

Давайте, не вникая пока в подробности, рассмотрим этапы процесса.

1. В файле с расширением .c пишется код на языке Си. Чаще всего вспомогательная информация при этом выносится в отдельный, так называемый хидер-файл с расширением .h. Отдельные участки программы, предъявляющие особые требования к скорости работы, плотности кода или таймингу, могут быть написаны на ассемблере (файл с расширением .S).
В реальном проекте файлов каждого типа может быть множество, но это никак не меняет структуру нашей схемы.

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

3. Этап ассемблирования. Все имеющиеся файлы с расширением .S преобразуются в объектные файлы с расширением .o.
Опять же, здесь включается Startup-файл, если он написан на ассемблере и предварительно не преобразован в объектный файл.

4. На этом этапе осуществляется компоновка (линковка) всех объектных файлов проекта в единый elf-файл. Помимо созданных в текущем проекте в компоновку могут включаться объектные файлы из других проектов, а также библиотеки (файлы с расширением .a).
Условия компоновки определяются в скрипте с расширением .ld.

5. Полученный в ходе предыдущего этапа elf-файл уже является исполняемым: его используют для отладки — пошаговой проверки работы программы на реальном МК или в симуляторе с целью поиска и устранения ошибок.

6. Происходит окончательное преобразование программы в файл с расширением .hex или .bin, который и загружается в МК (этап 7).

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


Файлы

🎁dokumentacija-mk.zip  24.26 Mb ⇣ 115
Программное обеспечение.zip (443.5 Мб) на облаке yandex.ru
В архивах вы найдёте:



Продолжение следует!
Благодарю за внимание.

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

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

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




 

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

Нравится

Статью одобрили 63 читателя.

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

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

 

 

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

 

Схема на Датагоре. Новая статья Микроконтроллеры AVR в радиолюбительской практике. А. В. Белов... А. В. Белов Микроконтроллеры AVR в радиолюбительской практике Данная книга представляет собой...
Схема на Датагоре. Новая статья Микроконтроллеры AVR семейств Tiny и Mega фирмы ATMEL, Евстифеев А.В.... Издательство: Додэка XXI [М.], 560 стр. 2005 г. Книга посвящена вопросам практического применения...
Схема на Датагоре. Новая статья Микроконтроллеры AVR семейства Classic фирмы ATMEL. Евстифеев А. В.... Книга представляет собой справочное издание по применению микроконтроллеров AVR семейства Classic...
Схема на Датагоре. Новая статья PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен... PIC микроконтроллеры. Все, что вам необходимо знать. Сид Катцен пер. с англ. Евстифеева А. В. — М.:...
Схема на Датагоре. Новая статья Разработка встроенных систем с помощью микроконтроллеров PIС. Уилмсхерст Т.... Разработка встроенных систем с помощью микроконтроллеров PIС. Уилмсхерст Т. Год издания: 2008...
Схема на Датагоре. Новая статья 10 практических устройств на AVR-микроконтроллерах. Книга 1. А.В. Кравченко... 10 практических устройств на AVR-микроконтроллерах. Книга 1. А.В. Кравченко Издательство: Москва,...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 1... Каждый человек, который только начинает осваивать программирование микроконтроллеров, да и вообще...
Схема на Датагоре. Новая статья Часы на ГРИ и К155ИД1. Нужна помощь... Доброго времени суток! Сделал часы на газоразрядных индикаторах с динамической индикацией на...
Схема на Датагоре. Новая статья Сопряжение компьютеров с внешними устройствами. Уроки реализации... Сопряжение компьютеров с внешними устройствами. Уроки реализации: Пер. с англ. -М.: Мир, 2000. —...
Схема на Датагоре. Новая статья Сборка и программирование мобильных роботов в домашних условиях. Жимарши Ф.... Сборка и программирование мобильных роботов в домашних условиях. Фредерик Жимарши. Издательство: НТ...
Схема на Датагоре. Новая статья Даташиты на микроконтроллеры AVR ATMega, ATTiny и AT90... Любому разработчику нужно знать специфику компонента, особенно если этот компонент -...
Схема на Датагоре. Новая статья Программирование микроконтроллеров в AtmelStudio 6. Часть 1. Первые шаги... Современное радиолюбительство невозможно представить без микроконтроллеров, и это очевидно. В...
 

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

 

Назад Вперед
<
Читатель Датагора

galrad

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

erbol

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

AlexD

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

erbol

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

galrad

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

EVA

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

erbol

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

GN

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

erbol

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

Yamazaki

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

Datagor

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

Yamazaki

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

Datagor

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

Borgal

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

erbol

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

mygor

Назад Вперед

Добавить комментарий, вопрос, отзыв 💬

Камрады, будьте дружелюбны, соблюдайте правила!

  • Смайлы и люди
    Животные и природа
    Еда и напитки
    Активность
    Путешествия и места
    Предметы
    Символы
    Флаги
 
 
В начало | Зарегистрироваться | Заказать наши киты почтой