Программирование, радиоэлектроника,
саморазвитие и частичка из моей жизни здесь...

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

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

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

Содержание:

  1. Структура байта
  2. Порты, байты и биты
  3. Операции битового сдвига
  4. Битовые операторы в языке Си
  5. Установка битов в регистре порта
  6. Сброс битов в регистре порта
  7. Проверка разрядов регистра
  8. Инверсия состояния бита в регистре
  9. Заключение

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

Мы знаем что один байт представляет собою 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

В файле констант и определений в библиотеке avr-libc для каждого типа микроконтроллера указаны значения для констант PD0, PD1, PB5, PC4, и другие. Например для вывода значений всех констант из IO-файла для микроконтроллера ATmega8 и где встречается сочетание "PD" (ищем константы для порта D), достаточно выполнить команду:

cat /usr/lib/avr/include/avr/iom8.h | grep PD

Получим вот такой результат:

#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

Теперь при использовании константы PD0 вы знаете что в ней содержится число 0, а в PD1 - 1 и т.д.

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

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

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

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

Операторы битового сдвига ">>" и "<<" в языке программирования Си выполняют сдвиг битов в переменной вправо и влево на указанное число элементов. Биты которые были сдвинуты теряются, а с другой стороны появляются нули - выполняется логический сдвиг.

Важный нюанс: при сдвиге вправо (">>") числа в переменной с отрицательным знаком (signed) выполняется арифметический сдвиг - освободившиеся позиции слева заполняются единичками (перенос знака). Это важно помнить!

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

Для числа 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)

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

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

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

  • "&" (логическое И, AND) или умножение - бинарная операция, результат которой равен 1 только в том случае если оба операнда равны 1, в противном случае будем иметь 0;
  • "|" (логическое ИЛИ, OR) или сложение - бинарная операция, результат которой равен 1 в том случае если хотя бы один из операндов равен 1;
  • "~" (логическое НЕ) или инверсия - унарная операция, результат которой равен 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-й бит (10001000) мы будем использовать битовую операцию логического ИЛИ в комплексе с битовой маской. Для получения битовой маски, при помощи которой позже будет установлен один бит, мы выполним левосторонний сдвиг битов числа 1 (00000001) на 5 разрядов:

0000 0001 (1)
<< 5
----------------
0010 0000 (32)

В результате битовой операции получим число 32 (00100000), двойка в 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:

  • PORTB |= ( 1 << 2 ) | ( 1 << 6 );
  • PORTB |= ( 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-й степени. Выполним инверсию битов:

0010 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-й бит установлен в 0. Для этого выполним сдвиг числа 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)

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

while( PORTD & (1 << 2) ) { ... }
if( PORTD & (1 << PD2) ) { ... }

Для проверки содержимого бита в регистре на ноль (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-го бита (канал PD2) в регистре порта PORTD:

while( !(PORTD & (1 << 2)) ) { ... } 
if( !(PORTD & (1 << PD2)) ) { ... }

Здесь мы выражение "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);

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

Заключение

С первого взгляда очень просто запутаться в операторах и значениях таких как "&", "!", "PD1", ">>" и других, но один раз хорошо разобравшись и попробовав на практике вы всегда будете иметь понятие что и как работает, откуда берется и что содержит.

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

Начало цикла статей: Программирование AVR микроконтроллеров в Linux на языках Asembler и C.

Комментарии к публикации (8):
папа #1папа
31 Октябрь 2016 20:28

Как установить несколько бит в регистре? - можно вызвать поочередно две конструкции с операторами, а можно все выполнить одной командой. Допустим нужно установить 2-й и 6-й биты в регистре порта PORTD, что соответствуют каналам PD1 и PD5:

PORTB |= ( 1 << 2 ) | ( 1 << 6 );

+1
Дмитрий #2Дмитрий
16 Февраль 2017 10:57

В таблице раздела Битовые операторы в языке Си в последнем столбце ошибочка, или как?

0
ph0en1x #3ph0en1x
16 Февраль 2017 16:02

Дмитрий, спасибо за внимательность. Ошибка исправлена.

0
ph0en1x #4ph0en1x
27 Февраль 2017 14:26

Недавно получил такое письмо:

Прочёл вашу статью. Но на других сайтах встретил такие записи:
MCUCR=(0<<ISC00)|(1<<ISC01);
DDRB |= ((1<<PB2)|(1<<PB0));                  
TCCR0A=(1<<COM0A1) | (1<<WGM00);
 SREG|= (1<<7);
DDRD  = (0 << PD2);
Так всё таки ноль можно писать? И чем отличается выражение имеющий
знак "|" от выражения не имеющего знак "|"?

Знак "|" - это логическая операция ИЛИ (OR).
К примеру, записи ниже эквивалентны между собою:
"SREG |= (1<<7);" - то же самое что и "SREG = SREG | (1<<7);"
Допустим что SREG содержит значение 01110001, вот что получится:
1 = 00000001 (в двоичном представлении)
00000001 << 7 = 10000000 (число 128)
01110001 | 10000000 = 11110001
Таким образом, мы выполнили установку восьмого(старшего) бита в регистре SREG (регистр состояния микроконтроллера) чем разрешили использование перерываний в МК.

Теперь рассмотрим вот эту запись:
DDRD  = (0 << PD2);
Здесь выполняется логический сдвиг числа 0 влево на 2 позиции (PD2=2, смотрим значение для своего МК).
00000000 << 2 = 00000000
Приведенную выше запись можно использовать для наглядности, хотя по сути она эквивалентна:
DDRD  = 0

Следующая конструкция нужна для установки бит ISCxx в регистре MCUCR (настройка прерываний):
MCUCR = (0<<ISC00) | (1<<ISC01);
Она эквивалентна вот этим конструкциям:
MCUCR = 0 | (1<<ISC01);
MCUCR = (1<<ISC01);
В первом варианте выражение "0<<ISC00" используется для наглядности, так более понятно что и как установлено в регистре.

0
VB99 #5VB99
04 Март 2017 09:40

Хочу выразить Вам огромную благодарность!

Как для новичка, то все очень доступно и понятно изложено.

Спасибо за Ваш труд!

+1
Ivan #6Ivan
14 Март 2017 19:06

«В языке Си все числа что равны или больше 1 являются логической истиной (True), а 0 - логической ложью (False).»
Исправьте , так как в Си, все числа что не равны 0 (-1,-5,1,5,150…) являются логической истиной (True), а 0 — логической ложью (False)

+1
ph0en1x #7ph0en1x
14 Март 2017 22:28

Ivan, благодарю за хорошее замечание! Исправил.

+1
Schneider #8Schneider
04 Май 2017 18:27

Спасибо,
Переехал с другого языка и никак не мог понять, что же там внутри происходит, что значат такие записи. Истолковано все отлично. Про битовые сдвиги инфы достаточно. Но здесь истолкована сама логика записи. Что важно для запоминания такой нотации. Третий день вникаю. Помогли! Спасибо!

+1
captcha