Print this page

Учебный курс. Как работать с битами. Макроопределения

19/09/2009 - 21:00
   При программировании микроконтроллеров постоянно приходится работать с битами. Устанавливать их, сбрасывать,  проверять их наличие в том или ином регистре. В AVR ассемблере для этих целей существует целый ряд команд. Во-первых, это группа команд операций с битами – они предназначены для установки или сброса битов в различных регистрах микроконтроллера, а во-вторых, группа команд передачи управления – они предназначены для организации ветвлений программ. В языке Си естественно нет подобных команд, поэтому у начинающих программистов часто возникает вопрос, а как в Си работать с битами. Эту тему мы сейчас и будем разбирать.

   В Си существуют 6 операторов для манипулирования битами. Их можно применять к любым целочисленным знаковым или беззнаковым типам переменных.

<<  - сдвиг влево
>>  - сдвиг вправо
~   -  поразрядная инверсия
|    - поразрядное ИЛИ
&  - поразрядное И
^   -  поразрядное исключающее ИЛИ

_______________ сдвиг влево << _______________

   Сдвигает число на n разрядов влево. Старшие n разрядов при этом исчезают, а младшие n  разрядов заполняются нулями.
        
        unsigned char tmp = 3;  //0b00000011    
        tmp = tmp << 1;   
        //теперь в переменной tmp число 6 или 0b00000110
        
        tmp = tmp << 3;
        //теперь в переменной tmp число 48  или 0b00110000

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

        tmp = 7;  //0b00000111    
        tmp <<= 2; //сокращенный вариант записи
        //теперь в переменной tmp число 28 или 0b00011100


Операция сдвига влево на n разрядов эквивалентна умножению переменной на 2n.

_______________ сдвиг вправо >> _______________

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

Пример для беззнаковой переменной

        unsigned char tmp = 255;  //0b11111111
        tmp = tmp >> 1;
        //теперь в переменной tmp число 127 или 0b01111111
        
        tmp >>=  3;  //сокращенный вариант записи
        //теперь в переменной tmp число 15 или 0b00001111

Пример для переменной знакового типа
        
        int tmp = 3400; //0b0000110101001000
        tmp >>= 2;
        //теперь в переменной число 850 или 0b0000001101010010

        tmp = -1200; //0b1111101101010000
        tmp >>= 2;
        //теперь в tmp число -300 или 0b1111111011010100
        //видите - два старших разряда заполнились единицами


   Операция сдвига вправо на n разрядов эквивалентна делению на 2n. При этом есть некоторые нюансы. Если потерянные младшие разряды содержали единицы, то результат подобного “деления” получается грубоватым.

Например    9/4 = 2,5         а 9>>2      (1001>>2)    равно 2
                  11/4 = 2,75     а 11>>2      (1011>>2)    равно 2    
                  28/4 = 7          а 28>>2    (11100>>2)    равно 7     
   
Во втором случае ошибка больше, потому что оба младших разряда единицы. В третьем случае ошибки нет, потому что потерянные разряды нулевые.

_______________поразрядная инверсия ~ _______________

   Поразрядно инвертирует число. Разряды, в которых были нули – заполняются единицами. Разряды, в которых были единицы – заполняются нулями. Оператор поразрядной инверсии являтся унарным оператором, то есть используется с одним операндом.

        unsigned char tmp =  94;     //0b01011110
        tmp = ~tmp;
        //теперь в переменной tmp число 161 или 0b10100001
        
        tmp = ~tmp;
        //теперь в tmp снова число 94 или 0b01011110

_______________ поразрядное ИЛИ | ______________

 
 
Оператор | осуществляет операцию логического ИЛИ между соответствующими битами  двух операндов. Результатом операции логического ИЛИ между двумя битами будет 0 только в случае, если оба бита равны 0. Во всех остальных случаях результат будет 1. Это проиллюстрировано в табице истинности.
 
 
Оператор | обычно используют для установки заданных битов переменной в единицу.
        
    tmp = 155
    tmp = tmp | 4; //устанавливаем в единицу второй бит переменной tmp

    155     0b10011011    
|
        4     0b00000100    
    159     0b10011111

   Использовать десятичные числа для установки битов довольно неудобно. Гораздо удобнее это делать с помощью операции сдвига влево <<.

    tmp = tmp | (1<<4); //устанавливаем в единицу четвертый бит переменной tmp

   Читаем справа налево – сдвинуть единицу на четыре разряда влево, выполнить операцию ИЛИ между полученным числом и значением переменной tmp, результат присвоить переменной tmp.

Установить несколько битов в единицу можно так

    tmp = tmp | (1<<7)|(1<<5)|(1<<0);
   //устанавливаем в единицу седьмой, пятый и нулевой биты переменной tmp
    
С помощью составного оператора присваивания  |= можно сделать запись компактней.

    tmp |= (1<<4);
    tmp |= (1<<7)|(1<<5)|(1<<0);

_______________ побитовое И & _______________


 
   Оператор & осуществляет операцию логического И между соответствующими битами двух операндов. Результатом операции логического И между двумя битами будет 1 только в том случае, если оба бита равны 1. Во всех других случаях результат будет 0. Это проиллюстрировано в таблице истинности.
 
 
Оператор & обычно применяют, чтобы обнулить один или несколько битов.
    
    tmp = 155;        
    tmp = tmp & 247; //обнуляем третий бит переменной tmp


    155        0b10011011    
&
    247        0b11110111    
    147        0b10010011

Видите, третий бит стал равен 0, а остальные биты не изменились.

Обнулять биты, используя десятичные цифры, неудобно. Но можно облегчить себе жизнь, воспользовавшись операторами << и ~

    tmp = 155;
    tmp = tmp & (~(1<<3)); //обнуляем третий бит

1<<3                          0b00001000
~(1<<3)                     0b11110111
tmp & (~(1<<3))        0b10011011 & 0b11110111
результат                    0b10010011

   Читаем справа налево – сдвинуть единицу на три разряда влево, выполнить инверсию полученного числа, выполнить операцию & между значением переменной tmp и проинвертированным числом, результат присвоить переменной tmp.

Обнулить несколько битов можно так

    tmp = tmp & (~((1<<3)|(1<<5)|(1<<6))); //обнуляем третий, пятый и шестой биты

   Здесь сначала выполняются операции сдвига, потом операции поразрядного ИЛИ, затем инверсия, поразрядное И, присвоение результата переменной tmp.

Используя составной оператор присваивания &= ,можно записать выражение более компактно

    tmp &= (~((1<<3)|(1<<5)|(1<<6)));

   Как проверить установлен ли бит в переменной?
Нужно обнулить все биты, кроме проверочного, а потом сравнить полученное значение с нулем
    
   if ((tmp & (1<<2)) != 0 ){
      // блок будет выполняться, только если установлен
      // второй бит переменной tmp

   }

    if ((tmp & (1<<2)) == 0 ){
      // блок будет выполняться, только если не установлен
      // второй бит переменной tmp

    }

    _______________побитовое исключающее ИЛИ ^ _______________  


   Оператор ^ осуществляет операцию логического исключающего ИЛИ между соответствующими битами двух операндов. Результатом операции логического исключающего ИЛИ будет 0 в случае равенства битов. Во всех остальных случаях результат будет 1. Это проиллюстрировано в табице истинности.

  
   Оператор ^ применяется не так часто как остальные битовые операторы, но и для него находится работенка. Например, с помощью него можно инвертировать один или несколько битов переменной.

    tmp = 155;
    tmp = tmp ^ 8; // инвертируем четвертый бит переменой tmp
    
    155         0b10011011    
^
    8             0b00001000    
    147         0b10010011

Четвертый бит изменил свое значение на противоположное, а остальные биты остались без изменений.

    tmp = tmp ^ 8; // опять инвертируем четвертый бит переменой tmp

    147         0b10010011    
^
    8             0b00001000    
    155         0b10011011

Видите, четвертый бит снова изменил свое значение на противоположное.  

Так записывать выражение намного удобнее

    tmp = tmp ^ (1<<3); // инвертируем третий бит переменой tmp
    
А так и удобно и компактно

    tmp ^= (1<<4);  //инвертируем четверый бит

Можно инвертировать несколько битов одновременно

    tmp ^= ((1<<4)|(1<<2)|(1<<1)); //инвертируем 4,2 и 1 биты   

    У поразрядного исключающего ИЛИ есть еще одно интересное свойство. Его можно использовать, для того чтобы поменять значения двух переменных местами. Обычно для этого требуется третья переменная.

tmp = var1;
var1 = var2;
var2 = tmp;

Но используя оператор ^  переставить значения можно так:

var1 ^= var 2;
var 2 ^= var 1;
var 1 ^= var 2;

Чистая магия, хотя, честно говоря,  я ни разу не пользовался таким приемом.

________________Директива #define__________________


   Теперь мы знаем, как устанавливать, обнулять и инвертировать биты, знаем, как проверять установлен ли бит или нет. Рассмотренные выше выражения довольно громоздки, но с помощью директивы препроцессора #define, им можно придать более приятный вид.
   Директива #define используется для присваивания символических имен константам и для макроопределений. Использование символических имен делают программу более модифицируемой и переносимой.
    Например, вы используете в тексте программы константу, и вдруг вам понадобилось изменить ее значение. Если она встречается всего в трех местах, то исправить ее можно и в ручную, а что делать, если она встречается в пятидесяти строчках? Мало того, что исправление займет много времени, так еще и ошибиться в этом случае проще простого. Здесь то, как раз и выручает директива #define. В начале программы задается символическое имя константы, которое  используется по ходу программы. Если нам нужно изменить это значение, это делается всего лишь в одном месте. А перед компиляцией препроцессор сам подставит во все выражения вместо имени константы ее значение.
    Программирование микроконтроллера неразрывно связано  с его аппаратной частью и чаще всего с внешней обвязкой. Взять хотя бы кнопки - опрашивая их в своей программе, мы обращаемся к реальным выводам микроконтроллера. А если нам вдруг понадобилось использовать программу опроса кнопок в другой схеме, где кнопки подключены к другим выводам? Придется исправлять программу. Опять таки, задав с помощью директивы #define символическое имя для соответствующих выводов, модифицировать программу будет проще простого

Пример:

#include "iom8535.h"

//порт, к которому подключены кнопки
#define PORT_BUTTON     PORTA
#define PIN_BUTTON        PINA
#define DDRX_BUTTON     DDRA

//выводы, к которым подключены кнопки
#define DOWN        3
#define CANCEL      4
#define UP              5
#define ENTER         6

int main()
{
    //конфигурируем порт на вход,
    //и включаем подтягивающие резисторы

    DDRX_BUTTON = 0;    
    PORT_BUTTON = 0xff;
    
…..
}


При задании символического имени можно использовать и выражения

#define MASK_BUTTONS       ((1<<DOWN)|(1<<CANCEL)|(1<<UP)|(1<<ENTER))

пример использования:
tmp = PORTB & MASK_BUTTONS;

Используя #define не жалейте скобок чтобы четко задать последовательность вычисления выражений!

Некоторые выражения можно замаскировать под «функции».

#define ADC_OFF()             ADCSRA = 0

пример использования:
ADC_OFF();

Можно использовать многострочные определения, используя в конце каждой строки символ \

#define INIT_Timer()        TIMSK = (1<<OCIE0);\
                                       TCCR0 = (1<<WGM01)|(0<<WGM00)|(1<<CS02);\
                                       TCNT0 = 0;\
                                       OCR0 = 0x7d

пример использования:
INIT_Timer();

 
Ну и самое мощное применение директивы #define – это задание макроопределений (или просто макросов). Вот как с помощью #define можно задать макросы для рассмотренных ранее операций с битами

#define   SetBit(reg, bit)          reg |= (1<<bit)           
#define   ClearBit(reg, bit)       reg &= (~(1<<bit))
#define   InvBit(reg, bit)          reg ^= (1<<bit)
#define   BitIsSet(reg, bit)       ((reg & (1<<bit)) != 0)
#define   BitIsClear(reg, bit)    ((reg & (1<<bit)) == 0)


пример использования:

SetBit(PORTB, 0);    //установить нулевой бит порта B    
InvBit(tmp,6);         //инвертировать шестой бит переменной tmp


if  (BitIsClear(PIND, 0)) {   //если очищен нулевой бит в регистре PIND                                    
…..                                     //выполнить блок
}

   Перед компиляцией препроцессор заменит эти строчки объявленными  ранее выражениями, подставив в них соответствующие аргументы.
   Макросы очень мощное средство, но использовать их нужно осторожно. Вот самые распространенные грабли, о которых написано во всех учебниках по программированию.
Определим макрос, вычисляющий квадрат числа:

#define SQUARE(x)    x*x

выражение    
tmp =  SQUARE(my_var);
даст корректный результат.

А что будет если в качестве аргумента макроопределения использовать выражение my_var+1

tmp =  SQUARE(my_var +1);

Препроцессор заменит эту строчку на

tmp = my_var + 1 * my_var +1;

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

Чтобы избежать таких ошибок не скупитесь на скобки при объявлении макросов!

Если объявить макрос так

#define SQUARE(x)    ((x)*(x))

выражение
tmp =  SQUARE(my_var +1);
даст корректный результат, потому что препроцессор заменит эту строчку на
tmp = ((my_var + 1) * (my_var +1));
 
макросы для работы с битами
записываем их в папку проекта, а в начале файла main.c  прописываем #include "bits_macros.h"

 

Related items