https://ph0en1x.net/81-howto-work-with-ports-register-bits-in-microcontroller.html
Работа с регистрами 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), достаточно выполнить следующую команду:
cat /usr/lib/avr/include/avr/iom8.h | grep PD
Bash
Увидим следующий результат:
#define SPDR _SFR_IO8(0x0F)
#define PD7 7
#define PD6 6
#define PD5 5
#define PD4 4
#define PD3 3
#define PD2 2
#define PD1 1
#define PD0 0
None
Теперь, при использовании константы PD0 в своей программе на Си, вы знаете что в ней содержится число 0, а в PD3 - 3 и т.д.
Операции битового сдвига
А сейчас, давайте подробно и с примерами разберемся с тем, как работают операторы битового сдвига.
Существует несколько разновидностей операций битового сдвига:
- логический (сдвинутые в направлении биты теряются, а освободившиеся позиции заполняются нулями);
- арифметический (сдвиг влево аналогичен логическому, а при сдвиге вправо - свободные позиции заполняются значениями крайнего левого бита, который еще называют знаковым);
- циклический (потерянные с одной стороны биты перемещаются на освободившиеся позиции с другой, как замкнутое кольцо).
Операторы битового сдвига в языке программирования Си обозначаются как ">>" и "<<" и выполняют логический сдвиг битов в используемой переменной в указанном направлении и на указанное число элементов.
Важный нюанс: при сдвиге вправо (">>") битов в числе с отрицательным знаком (signed) выполняется арифметический сдвиг - освободившиеся позиции слева заполняются единичками (перенос знака). Это важно помнить!
Сокращенные обозначения:
- Bin - от слова Binary, двичная система счисления;
- Dec - от слова Decimal, десятичная система счисления;
- Hex - от слова Hexadecimal, шестнадцатиричная система счисления.
Для примера выполним сдвиги битов в разных числах, предварительно представив их в двоичном виде.
Для числа 1 (Dec, в десятичной системе 1) - 00000001 (Bin, в двоичной системе 0b00000001):
- 1 << 0 = 1 (00000001);
- 1 << 1 = 2 (00000010);
- 1 << 2 = 4 (00000100)
- 1 << 5 = 32 (00100000);
- 1 >> 2 = 0 (00000000).
Сдвиг влево на один разряд выполняет умножение числа на 2, а сдвиг вправо - деление числа на 2.
Для числа 209 (Dec, в десятичной системе 209) - 11010001 (Bin, в двоичной системе 0b11010001):
- 209 = 11010001;
- 209 << 3 = 136 (10001000);
- 209 << 5 = 32 (00100000);
- 209 >> 5 = 6 (00000110)
Битовые операторы в языке Си
То как двигать биты в байте мы теперь знаем, дальше разберемся с битовыми операторами в Си:
- "&" (логическое И, AND) или умножение - бинарная операция, результат которой равен 1 только в том случае если оба операнда равны 1, в противном случае будем иметь 0;
- "|" (логическое ИЛИ, OR) или сложение - бинарная операция, результат которой равен 1 в том случае если хотя бы один из операндов равен 1;
- "~" (логическое НЕ) или инверсия - унарная операция, результат которой равен 0 если операнд равен 1, и наоборот - результат равен 1, если операнд равен 0;
- "^" (исключающее ИЛИ, XOR) - бинарная операция, результат которой равен 1 в том случае если только один из двух операндов равен 1.
Рассмотрим примеры битовых операций над числами 209, 7 и их битовыми представлениями:
1101 0001 (209) 0000 0111 (7) |
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-й бит (10001000) мы будем использовать битовую операцию логического ИЛИ в комплексе с битовой маской.
Что такое битовая маска? - по сути это специально подготовленное число, состоящее из требуемой конфигурации битов, которое в сочетании с некоторой операцией над битами другого числа позволяет установить или сбросить биты в последнем.
Для получения битовой маски, при помощи которой позже будет установлен один бит, мы выполним левосторонний сдвиг битов числа 1 (00000001) на 5 разрядов:
0000 0001 (1)
<< 5
----------------
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 << 5);
- PORTB = PORTB | (1 << PB5);
- PORTB |= (1 << PB5);
Наиболее удобно использовать последнюю краткую запись, где используется комбинирования операция логического ИЛИ и присвоения, в данном случае PB5. К примеру константа PB5 (канал 5 порта B, 6-й бит регистра) определена в файле /usr/lib/avr/include/avr/iom8.h для микроконтроллера ATmega8 и она равна числу 5.
Как установить несколько бит в регистре? - можно вызвать поочередно две конструкции с операторами, а можно все выполнить одной командой. Допустим нужно установить 2-й и 6-й биты в регистре порта PORTD, что соответствуют каналам PD1 и PD5:
- PORTD |= ( 1 << 1 ) | ( 1 << 5 );
- PORTD |= ( 1 << PD1 ) | ( 1 << PD5 );
Сброс битов в регистре порта
Для сброса разрядов в регистре порта мы будем использовать битовую операцию "&" (логическое "И"), которая применяется к двум битам (бинарная операция) и даёт единицу только в том случае если оба исходных бита имеют единичное значение, также нам пригодится битовая операция "~" (логическое "НЕ", инверсия).
Давайте выполним сброс 5-го бита в регистре порта PORTD, что в свою очередь выполнит установку низкого уровня на канале PD4. Допустим что сейчас в регистре PORTD содержится число 157, которое в битовом представлении выглядит как 10011101.
Для того чтобы сбросить 5-й бит (10011101) в регистре порта PORTD мы подготовим маску (как при установке битов), инвертируем ее биты "~", а потом выполним битовую операцию "&" над текущим значением регистра и полученной инвертированной маской.
Для подготовки маски выполним сдвиг битов на 4 разрядов в числе 1 (00000001).
0000 0001 (1)
<< 4
----------------
0001 0000 (16)
Маска готова, получили число 16 (00010000), 2 в 4-й степени. Выполним инверсию битов:
0001 0000 (16)
~
----------------
1110 1111 (239)
Готово, осталось применить маску к содержимому регистра порта PORTB используя битовую операцию "&":
1001 1101 (157)
&
1110 1111 (239)
----------------
1000 1101 (141)
Теперь в содержимом регистра PORTD значение 5-го бита установлено в 0 с сохранением положений остальных бит. В языке Си данные операции можно выполнить используя любую из приведенных ниже, идентичных по результату команд:
- PORTD = PORTD & ~ 16;
- PORTD = PORTD & 239;
- PORTD = PORTD & ~( 1 << 4 );
- PORTD = PORTD & ~( 1 << PD4 );
- PORTD &= ~( 1 << PD4 );
В данном случае наиболее удобной и информативной формой команды будет последний укороченный вариант. Для одновременного сброса нескольких битов регистра можно использовать вот такие конструкции из операторов:
- PORTD = PORTD & ~( ( 1 << PD4 ) | ( 1 << PD6 ) );
- PORTD &= ~( ( 1 << PD4 ) | ( 1 << PD6 ) );
Проверка разрядов регистра
Теперь разберемся каким образом можно проверить разряды регистра на наличие в них 1 или 0. Это может потребоваться если нужно получить значение битов в регистрах специального назначения (флагов) микропроцессора, а также для чтения состояния различных устройств и модулей, которые передают свое состояние используя битовую структуру.
Как проверить значение установленного бита в регистре на Си? - для этого нужно подобрать специальное выражение с использованием битовых операций, результатом работы которого будет значение: правда (True) или ложь (False). Имея булево (bool) значение выражения мы можем использовать для работы условные операторы языка Си.
Например нам нужно проверить есть ли единица (1) в 3-м бите регистра PORTD, тем самым мы проверим есть ли высокий уровень на канале PD2 порта PORTD. Примем что текущее значение регистра - 10010101 (149).
Для проверки используем выражение, которое состоит из битовой маски с установленным битом для проверки и проверяемого регистра, к которым применен битовый оператор "&" (логическое И).
Готовым маску, в которой только 3-й бит установлен в 1. Для этого выполним сдвиг битов числа 1 на 2 разряда влево:
0000 0001 (1)
<< 2
----------------
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 содержит единицу.
Вот как будет выглядеть данное выражение из двух логических операций на языке Си:
PORTD & (1 << 2)
None
Такое выражение можно использовать в условных операторах (if) и операторах циклов (while), например:
while( PORTD & (1 << 2) ) { ... }
if( PORTD & (1 << PD2) ) { ... }
C
Для проверки состояния бита в регистре на ноль (0) используем такую же конструкцию, только к результату выражения применим логическую операцию инверсии "!" (логическое НЕ).
Логическая операция "!" переворачивает значение с правды (True) на ложь (False), и наоборот. В отличие от битовой операции инверсии, которая переворачивает биты с 1 на 0 и наоборот, логическая операция инверсии оперирует с логическими значениями: правда (True) на ложь (False).
1 = True | 0 = False | 122 = True | (149 & (1 << 2)) = True |
!1 = False | !0 = True | !(5-1) = False | !(149 & (1 << 2)) = False |
Пример выражения на языке Си для проверки на ноль (0) 3-го бита в регистре порта PORTD (канал PD2):
while( !(PORTD & (1 << 2)) ) { ... }
if( !(PORTD & (1 << PD2)) ) { ... }
C
Здесь выражение "PORTD & (1 << 2)" берется в круглые дужки, что позволяет получить результат этого выражения, к которому потом и будет применен оператор логической инверсии.
Инверсия состояния бита в регистре
Иногда может понадобиться изменить состояние определенного бита в регистре на противоположное - выполнить инверсию состояния бита.
Для подобной операции отлично подходит битовый оператор "^" (исключающее ИЛИ, XOR). Чтобы выполнить инверсию определенного бита в регистре нужно создать маску, в которой этот бит установлен, а потом применить к содержимому регистра и полученной маске бинарный оператор "^", потом останется записать полученный результат в регистр и готово.
Возьмем, к примеру, что нужно погасить светодиод, который подключен к каналу PD5 порта PORTD. Если светодиод светится то это значит что в на канале PD5 присутствует высокий уровень, соответственно это значит что в регистре порта PORTD бит под номером 6 (PD5 = 5, 6-й бит в байте регистра) установлен в 1. Допустим что содержимое регистра порта PORTD сейчас - 10111010 (число 186, 1 байт, 8 разрядов, 6-й разряд = 1).
Подготовим маску, для установки 6-го бита нам необходимо сдвинуть все биты числа 1 на 5 разрядов:
0000 0001 (1)
<< 5
----------------
0010 0000 (32)
Применим маску к содержимому регистра порта PORTD:
1011 1010 (186)
^
0010 0000 (32)
----------------
1001 1010 (154)
Как видите, 6-й бит в байте регистра, который раньше был 1, сейчас установлен в 0 (1001 1010). Теперь осталось записать число в регистр порта и задачу можно считать выполненной. Примеры использования такой конструкции на языке Си:
- PORTD = PORTD ^ 32;
- PORTD = PORTD ^ (1<< 5);
- PORTD = PORTD ^ (1<< PD5);
- PORTD ^=(1<< PD5);
Как и в предыдущих примерах по установке и сбросу битов, последняя краткая конструкция является наиболее простой и понятной для использования.