Меню Рубрики

Установка битов в регистр

Установка битов в регистр

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

Начнём с того, что вспомним, где мы пишем код: 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-го, в этом МК он не занят). Что значит каждый бит и регистр – самым подробным образом расписано в даташите на микроконтроллер. Названия битов являются чем-то вроде констант, все их имена “заняты”, и создать переменную, совпадающую с названием бита, нельзя, ровно как попытаться изменить значение бита, обратившись к его имени.

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

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

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

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

Читайте также:  Установка кроссовера в багажнике

Таким образом мы включили и выключили нужные биты сразу, одним махом. Как вы помните из урока о типах данных и чисел, микроконтроллеру всё равно, в какой системе исчисления вы с ним работаете, то есть число 0b01010101 у нас в двоичной системе, в десятичной это будет 85, а в шестнадцатеричной – 0x55. И вот эти три варианта абсолютно одинаковы с точки зрения результата:

Только на первый можно посмотреть и сразу понять, что где стоит. Чего не скажешь про остальные два. Очень часто в чужих скетчах встречается такая запись, и это не очень комфортно.

Гораздо чаще бывает нужно “прицельно” изменить один бит в байте, и тут на помощь приходят логические (битовые) функции и макросы. Рассмотрим все варианты, во всех из них BYTE это байт-регистр, и BIT это номер бита, считая с правого края. То есть BIT это цифра от 0 до 7, либо название бита из даташита.

Установка бита в 1
Установка бита в 0 Описание
BYTE |= (1 (пример не рабочий!)
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 – то для их использования нужно в самом начале документа (среди остальных дефайнов) создать макро для этих функций:

И после этого можно пользоваться sbi и cbi.

Давайте рассмотрим пример, где просто подёргаем TCCR1B разными способами:

Можно ещё добавить вариант, где в одной строчке можно “прицельно” установить несколько битов:

Я думаю тут всё понятно, давайте теперь попробуем “прицельно” прочитать бит из регистра:

Чтение бита Описание
(BYTE >> BIT) & 1 Вручную через сдвиг
bitRead(BYTE, BIT) Ардуиновская макро-функция bitRead

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

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

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

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

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

Читать нужно с младшего регистра. Как только мы читаем младший регистр (первый), у МК полностью блокируется доступ к всему регистру, пока не будет прочитан старший. Если прочитать сначала старший – значение младшего может быть утеряно.

Обратная задача: есть опять же мнимый 16-битный регистр (состоящий из двух 8-битных), в который нам нужно записать значение. Например сдвоенный регистр ICR1H и ICR1L, вот таблица:

Микроконтроллер может работать только с одним байтом, а как нам записать двухбайтное число? А вот так: разбить число на два байта (при помощи ардуиновских функций highByte() и lowByte() ), и эти байты записать в соответствующие регистры. Пример:

Записывать нужно со старшего байта. Как только мы записываем младший байт (последний) – МК “защелкивает” оба регистра в память, соответственно если сначала записать младший – в старшем будет 0, и последующая запись старшего будет проигнорирована.

источник

Работа с регистрами AVR микроконтроллера на Си, битовые операции

Показаны принципы работы с отдельными битами регистра порта в AVR микроконтроллере. Подробно рассмотрены битовые операции и операции сдвига битов в языке Си. Приведены примеры установки и сброса битов в регистре порта, чтение состояния битов и их инверсии.

Для записи и чтения отдельных битов в портах микроконтроллера необходимо научиться выполнять битовые операции, уметь использовать битовые маски и выполнять запись в порт.

Структура байта

Мы знаем что один байт представляет собою 8 бит, а каждый бит это — 1 или 0, биты в байте считаются справа налево. Бит 1 является младшим, а бит 8 — старшим.

1 Байт
8 (старший бит) 7 6 5 4 3 2 1 (младший бит)

Порты, байты и биты

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

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

Названия каналов в порте микроконтроллера отсчитываются с нуля (0). Ниже приведен пример битовой структуры порта PORTD:

Регистр порта PORTD, 1 байт
Номер бита в регистре 8 7 6 5 4 3 2 1
Канал порта PD7 PD6 PD5 PD4 PD3 PD2 PD1 PD0

Аналогично по структуре выглядят и другие порты — PORTA, PORTB, DDRD, PINA.

Предустановленные значения констант PD0, PD1, PB5, PA4 (и многих других) для каждого типа AVR-микроконтроллера указаны в специальном заголовочном файле библиотеки avr-libc.

Например, чтобы вывести на экран значения всех констант из IO-файла для микроконтроллера ATmega8, содержащих сочетание символов «PD» (ищем константы для порта D, PORTD), достаточно выполнить следующую команду:

Увидим следующий результат:

Теперь, при использовании константы PD0 в своей программе на Си, вы знаете что в ней содержится число 0, а в PD3 — 3 и т.д.

Операции битового сдвига

А сейчас, давайте подробно и с примерами разберемся с тем, как работают операторы битового сдвига.

Существует несколько разновидностей операций битового сдвига:

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

Операторы битового сдвига в языке программирования Си обозначаются как «>>» и » >») битов в числе с отрицательным знаком (signed) выполняется арифметический сдвиг — освободившиеся позиции слева заполняются единичками (перенос знака). Это важно помнить!

  • Bin — от слова Binary, двичная система счисления;
  • Dec — от слова Decimal, десятичная система счисления;
  • Hex — от слова Hexadecimal, шестнадцатиричная система счисления.

Для примера выполним сдвиги битов в разных числах, предварительно представив их в двоичном виде.

Для числа 1 (Dec, в десятичной системе 1) — 00000001 (Bin, в двоичной системе 0b00000001):

Сдвиг влево на один разряд выполняет умножение числа на 2, а сдвиг вправо — деление числа на 2.

Для числа 209 (Dec, в десятичной системе 209) — 11010001 (Bin, в двоичной системе 0b11010001):

  • 209 = 1110001;
  • 209 > 5 = 6 (00000110)

Для пересчета из одной системы счисления в другую в Linux удобно использовать калькулятор, об этом я уже раньше писал.

Битовые операторы в языке Си

То как двигать биты в байте мы теперь знаем, дальше разберемся с битовыми операторами в Си:

» (логическое НЕ) или инверсия — унарная операция, результат которой равен 0 если операнд равен 1, и наоборот — результат равен 1, если операнд равен 0;

  • «^» (исключающее ИЛИ, XOR) — бинарная операция, результат которой равен 1 в том случае если только один из двух операндов равен 1.
  • Рассмотрим примеры битовых операций над числами 209, 7 и их битовыми представлениями:

    1101 0001 (209)
    &

    0000 0111 (7)
    —————-
    0000 0001 (1)

    1101 0001 (209)
    |
    0000 0111 (7)
    —————-
    1101 0111 (215) 1101 0001 (209)

    —————-
    0010 1110 (46) 1101 0001 (209)
    ^
    0000 0111 (7)
    —————-
    1101 0110 (214)

    Как видите, битовые операции позволяют установить или сбросить отдельные биты числа.

    Установка битов в регистре порта

    А теперь немного практики, давайте сделаем установку 6-го бита в регистре порта PORTB что в свою очередь установит высокий уровень для канала PB5 (6-й бит в регистре). Допустим что сейчас в регистре PORTB содержится число 136, которое в битовом представлении выглядит как 10001000 (высокий уровень на каналах PB7 и PB3).

    Чтобы установить 6-й бит (1001000) мы будем использовать битовую операцию логического ИЛИ в комплексе с битовой маской.

    Что такое битовая маска? — по сути это специально подготовленное число, состоящее из требуемой конфигурации битов, которое в сочетании с некоторой операцией над битами другого числа позволяет установить или сбросить биты в последнем.

    Для получения битовой маски, при помощи которой позже будет установлен один бит, мы выполним левосторонний сдвиг битов числа 1 (00000001) на 5 разрядов:

    0000 0001 (1)
    —————-
    0010 0000 (32)

    В результате битовой операции получим число 32 (00100000), это и есть наша битовая маска. Хочу заметить что это число равняется числу 2 в 5-й степени, каждый сдвиг разряда влево умножает результат на 2.

    Теперь нам останется выполнить битовую операцию ИЛИ над текущим числом в регистре и получившимся числом-маской:

    1000 1000 (136)
    |
    0010 0000 (32)
    —————-
    1010 1000 (168)

    А теперь сравните состояние регистра перед операцией и после — все состояния битов сохранены и дополнительно установлен 6-й бит.

    Для установки 6-го бита и последующей записи числа в регистр порта PORTB (установка высокого уровня для канала PB5) в нашем примере можно использовать любую из следующих конструкций операторов, они все выполняют идентичную задачу:

    • PORTB = PORTB | 32;
    • PORTB = PORTB | (1 0000 0001 (1)
      —————-
      0001 0000 (16)

    Маска готова, получили число 16 (00010000), 2 в 4-й степени. Выполним инверсию битов:

    0001 0000 (16)

    Готово, осталось применить маску к содержимому регистра порта PORTB используя битовую операцию «&»:

    1001 1101 (157)
    &
    1110 1111 (239)
    —————-
    1000 1101 (141)

    Теперь в содержимом регистра PORTD значение 5-го бита установлено в 0 с сохранением положений остальных бит. В языке Си данные операции можно выполнить используя любую из приведенных ниже, идентичных по результату команд:

    16;

  • PORTD = PORTD & 239;
  • PORTD = PORTD &

    ( 1 0000 0001 (1)
    —————-
    0000 0100 (4)

    Теперь применим битовую операцию «&» (логическое И) к содержимому регистра PORTD и получившейся маске:

    1001 0101 (149)
    &
    0000 0100 (4)
    —————-
    0000 0100 (4)

    В результате выражения получим число 4 (0000 0100).

    В языке Си все числа которые НЕ равны «нулю» (-100, -5, 1, 500) являются логической истиной (True), а 0 — логической ложью (False).

    Результат нашего выражения — число 4, которое является логической истиной (True), а это значит что 3-й разряд регистра PORTD содержит единицу.

    Вот как будет выглядеть данное выражение из двух логических операций на языке Си:

    Такое выражение можно использовать в условных операторах (if) и операторах циклов (while), например:

    Для проверки состояния бита в регистре на ноль (0) используем такую же конструкцию, только к результату выражения применим логическую операцию инверсии «!» (логическое НЕ).

    Логическая операция «!» переворачивает значение с правды (True) на ложь (False), и наоборот. В отличие от битовой операции инверсии, которая переворачивает биты с 1 на 0 и наоборот, логическая операция инверсии оперирует с логическими значениями: правда (True) на ложь (False).

    1 = True 0 = False 122 = True (149 & (1 0000 0001 (1)
    —————-
    0010 0000 (32)

    Применим маску к содержимому регистра порта PORTD:

    1011 1010 (186)
    ^
    0010 0000 (32)
    —————-
    1001 1010 (154)

    Как видите, 6-й бит в байте регистра, который раньше был 1, сейчас установлен в 0 (101 1010). Теперь осталось записать число в регистр порта и задачу можно считать выполненной. Примеры использования такой конструкции на языке Си:

    • PORTD = PORTD ^ 32;
    • PORTD = PORTD ^ (1 >» и других, но один раз хорошо разобравшись и попробовав на практике вы всегда будете иметь понятие что и как работает, откуда берется и что содержит.

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

    источник

    Добавить комментарий