Работа с регистрами
Регистры, байты, биты
В прошлом уроке мы освоили битовые операции, а в этом - научимся работать напрямую с регистрами микроконтроллера. Зачем? Я думаю в первую очередь это нужно для того, чтобы понимать чужой код и переделывать его под себя, потому что прямая работа с регистрами в скетчах из Интернета встречается довольно часто.
Что такое регистр? Тут всё весьма просто: это сверхбыстрые блоки оперативной памяти объёмом 1 Байт, находящиеся рядом с ядром МК и периферией. На стороне программы это обычные глобальные переменные, которые можно читать и менять. В регистрах микроконтроллера хранятся "настройки" для различной его периферии: таймеры-счётчики, порты с пинами, АЦП, шина UART, I2C, SPI и прочее железо, встроенное в МК. Меняя регистр, мы даём практически прямую команду микроконтроллеру, что и как нужно сделать. Запись в регистр занимает около 1 такта МК (0.0625 мкс при частоте тактирования 16 МГц) - это ОЧЕНЬ быстро. Имена регистров фиксированные, полный перечень с подробным описанием можно найти в даташите на микроконтроллер (официальный даташит на ATmega328 - Arduino Nano/UNO/Mini).
Работа с регистрами очень непростая, если вы не выучили их все наизусть, потому что названия у них обычно нечитаемые, аббревиатуры. Так называемые "Ардуиновские" функции собственно и занимаются тем, что работают с регистрами, оставляя нам удобную, понятную и читаемую функцию, ничего сверхъестественного в этом нет. Зачем вообще работать с регистрами напрямую? Есть несколько больших преимуществ:
- Скорость работы: чтение/запись регистра выполняется максимально быстро, что позволяет ускорить работу с МК (например дёргать пины вручную вместо
digitalWrite()
). Максимальная скорость работы с периферией МК нужна далеко не всегда, поэтому если вам не нужно сэкономить несколько микросекунд - с регистрами можно даже не связываться. - Объём памяти: прямая работа с регистрами позволяет максимально компактно работать с периферией МК, читая и записывая только нужные биты, поэтому написанный под свои задачи конкретный код займёт меньше места, чем чьи-то готовые универсальные функции и библиотеки (например тот же digitalWrite или работа с Serial). При работе с Ардуино можно не заморачиваться по этому поводу практически никогда, а вот если делать проект на ATTiny - там придётся ужимать код вплоть до байта.
- Гибкость настройки: работа с микроконтроллером напрямую при помощи регистров позволяет очень гибко настраивать периферию под свои задачи. Дело в том, что все существующие Ардуино-библиотеки охватывают чуть больше половины всех возможностей микроконтроллера! Огромное количество настроек и полезных трюков не описано нигде, кроме даташита, и для их использования нужно уметь читать этот самый даташит и работать с регистрами.
В микроконтроллерах серии ATmega/ATtiny регистры 8 битные, то есть регистр - это переменная типа byte
. Насколько мы знаем, байт это число от 0 до 255, получается каждый регистр имеет 255 настроек? Нет, логика здесь совсем другая: байт это 8 бит, то есть один регистр хранит 8 настроек, которые можно включить/выключить. Давайте для примера рассмотрим один из регистров таймера 1, под названием TCCR1B
. Картинка из даташита на ATmega328p:
Регистр TCCR1B
, как и положено здоровому байту, состоит из 8 бит. Почти каждый его бит имеет имя (кроме 5-го, в этом МК он не используется). Что делает каждый бит и регистр - самым подробным образом расписано в даташите. Имена всех битов и регистров заданы в компиляторе, то есть создавать переменные с такими же именами нельзя. Изменять значения битов также нельзя, они являются константами:
int WGM12; // приведёт к ошибке CS11 = 5; // приведёт к ошибке
Но считать значение бита по его названию можно! Причём значение будет равно его номеру в регистре, считая справа. CS11
равен 1, WGM13
равен 4, ICNC1
равен 7 (см. таблицу выше).
Думаю здесь всё понятно: есть регистр (байт), имеющий уникальное имя и состоящий из 8 бит, каждый бит также имеет уникальное имя, по которому можно получить номер этого бита в байте его регистра. Осталось понять, как этим всем пользоваться.
Запись/чтение регистра
Существует несколько способов установки битов в регистрах. Мы рассмотрим их все, чтобы столкнувшись с одним из них вы знали, что это вообще такое и как работает данная строчка кода. В предыдущем уроке по битовым операциям мы подробно разобрали всё, что касается манипуляций с битами, поэтому если вы его прочитали и поняли - следующая информация не будет для вас новой.
Давайте вернёмся к регистру таймера, который я показывал выше, и попробуем его сконфигурировать. Первый способ, это явное задание всего байта сразу, со всеми единицами и нулями. Сделать это можно так:
TCCR1B = 0b01010101;
Таким образом мы включили и выключили нужные биты сразу, одним махом. Как вы помните из урока о типах данных и чисел, микроконтроллеру всё равно, в какой системе исчисления вы с ним работаете, то есть число 0b01010101
у нас в двоичной системе, в десятичной это будет 85
, а в шестнадцатеричной - 0x55
. И вот эти три варианта абсолютно одинаковы с точки зрения результата:
TCCR1B = 0b01010101; TCCR1B = 85; TCCR1B = 0x55;
Только на первый можно посмотреть и сразу понять, что где стоит, чего не скажешь про остальные два. Очень часто в чужих скетчах встречается такая запись, и это не очень комфортно.
Гораздо чаще бывает нужно "прицельно" изменить один бит в байте, и тут на помощь приходят логические (битовые) функции и макросы. Рассмотрим все варианты, во всех из них BYTE
это байт-регистр, и BIT
это номер бита, считая с правого края. То есть BIT это цифра от 0 до 7, либо название бита из даташита.
Установка бита в 1 | Установка бита в 0 | Описание |
BYTE |= (1 << BIT); |
BYTE &= ~(1 << BIT); |
Битовый сдвиг << |
BYTE |= (2^BIT); |
BYTE &= ~(2^BIT); |
2 в степени номер бита |
BYTE |= bit(BIT); |
BYTE &= ~bit(BIT); |
Ардуиновский макрос bit(), заменяющий сдвиг |
BYTE |= _BV(BIT); |
BYTE &= ~_BV(BIT); |
Встроенная функция _BV(), опять же сдвиг |
sbi(BYTE, BIT); |
cbi(BYTE, BIT); |
Используем ассемблерные макросы sbi и cbi |
bitSet(BYTE, BIT); |
bitClear(BYTE, BIT); |
Используем Ардуиновские функции bitSet() и bitClear() |
Что хочу сказать по перечисленным вариантам: они все по сути являются одним и тем же, а именно - первым, просто обёрнуты в другие функции и макросы. Время выполнения всех вариантов одинаково, т.к. макро-функции не делают лишних действий, а приводят все способы к первому, со сдвигом и |=
и &=
. Все эти способы вы можете встретить в скетчах из интернета, это факт. Лично мне больше всего нравится ардуиновский bitSet и bitClear, потому что они имеют читаемое название и заранее сидят в библиотеке.
Что касается sbi()
и cbi()
- то для их использования где-то должны быть созданы макросы:
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
И после этого можно пользоваться sbi()
и cbi()
. Давайте рассмотрим пример, где просто подёргаем TCCR1B
разными способами:
void setup() { TCCR1B = 0; // обнулили регистр bitSet(TCCR1B, CS11); // включили бит №1 TCCR1B |= _BV(4); // включили бит №4 TCCR1B |= (1 << WGM12); // включили бит №3 TCCR1B &= ~_BV(WGM13); // вЫключили бит №4 bitClear(TCCR1B, 3); // вЫключили бит №3 }
Можно ещё добавить вариант, где в одной строчке можно "прицельно" установить несколько битов:
void setup() { TCCR1B = 0; // обнулили регистр // ставим бит 1, 3 и 4(WGM13) TCCR1B |= _BV(1) | _BV(3) | _BV(WGM13); }
Я думаю тут всё понятно, давайте теперь попробуем "прицельно" прочитать бит из регистра:
Чтение бита | Описание |
(BYTE >> BIT) & 1 |
Вручную через сдвиг |
bitRead(BYTE, BIT) |
Ардуиновская макро-функция |
Два рассмотренных способа возвращают 0 или 1 в зависимости от состояния бита. Пример:
void setup() { TCCR1B = 0; // обнулили регистр bitSet(TCCR1B, CS12); // включили бит №2 Serial.begin(9600); // открыли порт Serial.println(bitRead(TCCR1B, 2)); // получили 1 }
Ещё больше примеров работы с битами смотри в уроке по битовым операциям.
16-бит регистры
У Ардуинок (AVR) встречаются также сдвоенные 16-битные регистры, состоят из двух 8-битных, например "сдвоенный регистр" АЦП ADC
состоит из ADCH
и ADCL
, или регистр таймера ICR1
состоит из ICR1H
и ICR1L
. АЦП у нас 10 битный, но регистры - 8 битные, поэтому часть (8 бит) хранится в одном регистре (ADCL
), а остальное (2 бита) - в другом (ADCH
). Смотрите, как это выглядит в виде таблицы:
Вопрос: как нам принять или изменить это самое 10 битное число, если оно разбито на два разных регистра? Очень просто: работа с такими сдвоенными регистрами встроена в компилятор и можно просто работать с ними напрямую как с обычной переменной, например:
int val = ADC; // читаем значение ICR1 = 1234; // записываем значение
Такую запись почему-то используют редко, возможно для совместимости с другими компиляторами. Чаще всего вы встретите вот такой вариант, в котором значения "склеиваются" через сдвиг:
int val = ADCL + (ADCH << 8); // возможен вариант ADCL | (ADCH << 8) , см. урок про битовые операции
Читать нужно с младшего регистра. Как только мы читаем младший регистр (первый), у МК полностью блокируется доступ к всему регистру, пока не будет прочитан старший. Если прочитать сначала старший - значение младшего может быть утеряно.
Обратная задача: есть опять же мнимый 16-битный регистр (состоящий из двух 8-битных), в который нам нужно записать значение. Например сдвоенный регистр ICR1H
и ICR1L
, вот таблица:
Микроконтроллер может работать только с одним байтом, а как нам записать двухбайтное число? А вот так: разбить число на два байта при помощи ардуиновских функций highByte()
и lowByte()
, и уже эти байты записать в соответствующие регистры. Пример:
uint16_t val = 1500; // просто число типа int ICR1H = highByte(val); // пишем старший байт ICR1L = lowByte(val); // пишем младший байт // читаем байты обратно и "склеиваем" в int byte val_1 = ICR1L; byte val_2 = ICR1H; uint16_t value = val_1 + (val_2 << 8);
Записывать нужно со старшего байта. Как только мы записываем младший байт (последний) - МК "защелкивает" оба регистра в память, соответственно если сначала записать младший - в старшем будет 0, и последующая запись старшего будет проигнорирована.
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])