Регистры, байты, биты


В этом уроке мы научимся работать напрямую с регистрами микроконтроллера. Зачем? Я думаю в первую очередь это нужно для того, чтобы понимать чужой код и переделывать его под себя, потому что прямая работа с регистрами в скетчах из Интернета встречается довольно часто.

Начнём с того, что вспомним, где мы пишем код: Arduino IDE. Несмотря на большое количество не всегда оправданного негатива в сторону этой программы, она очень крутая. Помимо кучи встроенных инструментов и поддержки “репозиториев” от сторонних разработчиков, Arduino IDE позволяет нам программировать плату на разных языках программирования. Это некая условность, но по сути получается три языка:

  • C++, который мы изучили в рамках уроков на сайте;
  • Ассемблер (ассемблерные вставки) – прямая работа с микроконтроллером, очень сложный язык;
  • Язык регистров микроконтроллера. Условно назовём его отдельным языком.

Что такое регистр? Тут всё весьма просто: регистры это грубо говоря глобальные переменные, информацию из которых мы можем как считать, так и изменить, это сверхбыстрые блоки оперативной памяти объёмом 1 байт, находящиеся рядом с ядром МК и периферией. В регистрах микроконтроллера хранятся “настройки” для различной его периферии: таймеры-счётчики, порты с пинами, АЦП, шина UART, I2C, SPI и прочее железо, встроенное в МК. Меняя регистр, мы даём практически прямую команду микроконтроллеру, что и как нужно сделать. Запись в регистр занимает 1 такт, то есть 0.0625 микросекунды. Это ОЧЕНЬ быстро. Названия (имена) регистров фиксированные, все их описания можно найти в даташите на микроконтроллер. Работа с регистрами очень непростая, если вы не выучили их все наизусть. А названия у них обычно нечитаемые, аббревиатуры. Так называемые “Ардуиновские” функции собственно и занимаются тем, что работают с регистрами, оставляя нам удобную, понятную и читаемую функцию.

Итак, с точки зрения кода, регистр – это переменная, которую мы можем читать и писать. В микроконтроллерах серии ATmega регистры 8 битные (вроде бы поэтому сам микроконтроллер и считается 8-ми битным), то есть регистр это (грубо) переменная типа byte, меняя которую, можно на низком уровне конфигурировать работу микроконтроллера. Насколько мы знаем, байт это число от 0 до 255, получается каждый регистр имеет 255 настроек? Нет, логика здесь совсем другая: байт это 8 бит, принимающих значение 0 и 1, то есть один регистр хранит 8 настроек, которые можно включить/выключить. Именно так и происходит конфигурация микроконтроллера на низком уровне. Давайте для примера рассмотрим один из регистров таймера 1, под названием TCCR1B. Картинка из даташита на ATmega328p:

Регистр TCCR1B, как и положено здоровому байту, состоит из 8 бит. Почти каждый его бит имеет имя (кроме 5-го, в этом МК он не занят). Что значит каждый бит и регистр – самым подробным образом расписано в даташите на микроконтроллер. Названия битов являются чем-то вроде констант, все их имена “заняты”, и создать переменную, совпадающую с названием бита, нельзя, ровно как попытаться изменить значение бита, обратившись к его имени.

int WGM12;  // приведёт к ошибке
CS11 = 5;   // приведёт к ошибке

Так что если вы случайно назовёте переменную так, как называется регистр или бит – вы получите ошибку. Но это вряд ли случится, названия у регистров и битов не очень вменяемые. Но считать значение бита по его названию – можно! Причём его значение будет равно его номеру в регистре, считая справа. CS11 равен 1, WGM13 равен 4, ICNC1 равен 7, также эти значения указаны в таблице даташита (над таблицей). Думаю здесь всё понятно: есть регистр (байт), имеющий уникальное имя и состоящий из 8 бит, каждый бит тоже имеет уникальное имя, по которому можно получить номер этого бита в байте его регистра. Осталось понять, как этим всем пользоваться.

Запись/чтение регистра


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

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 разными способами:

// для использования sbi и cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

void setup() {
  TCCR1B = 0;             // обнулили регистр
  bitSet(TCCR1B, CS11);   // включили бит №1
  cbi(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) Ардуиновская макро-функция bitRead

Два рассмотренных способа возвращают 0 или 1 в зависимости от состояния бита. Пример:

void setup() {
  TCCR1B = 0;             // обнулили регистр
  bitSet(TCCR1B, CS12);   // включили бит №2
  Serial.begin(9600);     // открыли порт
  Serial.println(bitRead(TCCR1B, 2)); // получили 1
}

Теперь вы готовы к любой встрече с регистрами!

16-бит регистры


У Ардуинок (AVR) встречаются также 16-битные регистры, которые на деле разделены на два 8-битных, например регистры АЦП ADCH и ADCL. АЦП у нас 10 битный, но регистры – 8 битные, поэтому часть (8  бит) хранится в одном регистре (ADCL), а остальное (2 бита) – в другом (ADCH). Смотрите, как это выглядит в виде таблицы:

Вопрос: как нам принять это самое 10 битное число, если оно разбито на два разных регистра? Очень просто, используя сдвиг:

int val = 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, и последующая запись старшего будет проигнорирована.

ВАЖНО: запись в 16 бит регистры производится начиная со СТАРШЕГО байта, чтение – с МЛАДШЕГО

Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Последнее обновление Август 08, 2019
2019-08-08T08:27:56+03:00