Перейти к содержанию

Битовые операции в C

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

Обновлено Редакция GAW

Почти вся работа с периферией микроконтроллера — это работа с отдельными битами регистров: включить тактирование модуля, настроить вывод, проверить флаг готовности. В C для этого нет специальных команд — используются побитовые операторы и маски. Ниже — приёмы, которые встречаются в любой прошивке, и ловушки, которые описаны в стандарте C11 (черновик N1570) и в документации GCC.

Коротко

  • Операторы: & (И), | (ИЛИ), ^ (исключающее ИЛИ), ~ (инверсия), << и >> (сдвиги).
  • Четыре действия с битом n: установить x |= 1 << n, сбросить x &= ~(1 << n), переключить x ^= 1 << n, проверить x & (1 << n).
  • Маски шире int записывают с суффиксом: 1UL << 20. У avr-gcc int 16-битный, у GCC для ARM Cortex-M — 32-битный.
  • Неопределённое поведение: сдвиг на отрицательное число или на ширину типа и больше, сдвиг влево отрицательного числа или с переполнением знакового типа (C11, 6.5.7).
  • Маленькие типы продвигаются до int: ~ над uint8_t даёт отрицательный int, а не байт.

Операторы и маски

ЗадачаВыражениеПример для AVR
установить бит nx |= (1 << n)PORTB |= (1 << PB5);
сбросить бит nx &= ~(1 << n)PORTB &= ~(1 << PB5);
переключить бит nx ^= (1 << n)PORTB ^= (1 << PB5);
проверить бит nx & (1 << n)if (PINB & (1 << PB0))
установить несколько битx |= (1 << a) | (1 << b)TCCR1B |= (1 << CS11) | (1 << CS10);
записать поле из k бит со сдвигом sx = (x & ~(mask << s)) | ((v & mask) << s)см. ниже
прочитать поле(x >> s) & mask(ADMUX >> MUX0) & 0x0F

Маска — число, в котором единицами отмечены нужные биты. 1 << n даёт маску одного бита, (1 << k) - 1 — маску из k младших бит. Для AVR в avr-libc есть макрос _BV(n), определённый как (1 << (n)), и макросы проверки bit_is_set() и bit_is_clear(); имена бит вроде PB5 и CS11 в заголовках — это их номера.

Запись многобитного поля без порчи соседних бит — классический приём «очистить, затем установить»:

/* STM32F1: set the MODE/CNF field of PA5 (bits 23:20 of GPIOA->CRL) to 0b0010 */
uint32_t crl = GPIOA->CRL;
crl &= ~(0xFUL << 20);      /* clear the 4-bit field */
crl |= (0x2UL << 20);       /* output push-pull, 2 MHz */
GPIOA->CRL = crl;

Чтение в локальную переменную и одна запись обратно короче и безопаснее, чем две операции &= и |= над самим регистром: регистр не проходит через промежуточное состояние. Как устроены поля регистров GPIO STM32, разобрано в статье о GPIO STM32.

Маску и сдвиг поля любого регистра, его чтение и запись, суффикс констант для AVR и ARM и готовый код на C считает калькулятор ниже — отдельно он открывается на странице калькулятора битовых масок. В нём сразу открыт тот же GPIOA->CRL после сброса, но выбрано только двухбитное поле MODE5, биты 21:20. Запись 2 в него оставляет CNF5 = 01 и даёт 0x4464 4444 — выход с открытым стоком. Чтобы повторить пример выше, задайте поле 23:20: получится 0x4424 4444.

Исходные данные

Разрядность регистра
Компилятор

0x4444_4444, 0b1010 0101 или десятичное

Биты — нажмите, чтобы переключить; рамка — поле

он же сдвиг

0–3, можно 0x… и 0b…

установить, сбросить, проверить

NAME_Pos и NAME_Msk

Результат

Маска поля 21:20 со сдвигом
0x00300000
2 бита, сдвиг 20, маска без сдвига 0x3 = 0b11
Значение поля
0 = 0x00b00: (x >> 20) & 0x3
После записи 2 в поле
0x44644444(x & ~0x00300000) | (2 << 20)
Регистр
0x444444441145324612 в десятичной
Двоичный вид
0100 0100 0100 0100 0100 0100 0100 0100
Бит 31
0маска 0x80000000
Установить бит 31
0xC4444444
Сбросить бит 31
0x44444444
Переключить бит 31
0xC4444444
Бит 31: какой суффикс нужен
Записьarm-none-eabi-gcc: int — 32 битаavr-gcc: int — 16 бит
1 << 31int, 32 бита: результат не помещается — неопределённое поведениеint, 16 бит: сдвиг на 31 — неопределённое поведение
1U << 31unsigned int: 0x80000000unsigned int, 16 бит: сдвиг на 31 — неопределённое поведение
1UL << 31unsigned long: 0x80000000рекомендуетсяunsigned long: 0x80000000рекомендуется
1ULL << 31unsigned long long: 0x80000000unsigned long long: 0x80000000
Маска поля 21:20
Записьarm-none-eabi-gcc: int — 32 битаavr-gcc: int — 16 бит
0x3 << 20int: 0x300000int, 16 бит: сдвиг на 20 — неопределённое поведение
0x3U << 20unsigned int: 0x300000unsigned int, 16 бит: сдвиг на 20 — неопределённое поведение
0x3UL << 20unsigned long: 0x300000рекомендуетсяunsigned long: 0x300000рекомендуется
0x3ULL << 20unsigned long long: 0x300000unsigned long long: 0x300000

Тип константы — по таблице C11 6.4.4.1, сдвиг — по 6.5.7, инверсия ~ — по 6.5.3.3. Константа уже регистра опасна в выражении x &= ~маска: старшие биты регистра тоже обнулятся.

Код на C

/* GPIOA->CRL: 32-bit register, arm-none-eabi-gcc (int is 32 bits): suffix UL */

/* Bit 31 */
GPIOA->CRL |= (1UL << 31);    /* set */
GPIOA->CRL &= ~(1UL << 31);   /* clear */
GPIOA->CRL ^= (1UL << 31);    /* toggle */
if (GPIOA->CRL & (1UL << 31)) {    /* test */
    /* bit 31 is 1 */
}

/* Field GPIO_CRL_MODE5: bits 21:20, 2 bits */
/* If a device header already defines these names, the lines must match it token for token */
#define GPIO_CRL_MODE5_Pos  (20U)
#define GPIO_CRL_MODE5_Msk  (0x3UL << GPIO_CRL_MODE5_Pos)  /*!< 0x00300000 */

uint32_t mode5 = (GPIOA->CRL & GPIO_CRL_MODE5_Msk) >> GPIO_CRL_MODE5_Pos;    /* read */
GPIOA->CRL = (GPIOA->CRL & ~GPIO_CRL_MODE5_Msk)
    | ((0x2UL << GPIO_CRL_MODE5_Pos) & GPIO_CRL_MODE5_Msk);    /* write 2 */

/* The same with the CMSIS-Core macros from core_cm*.h */
mode5 = _FLD2VAL(GPIO_CRL_MODE5, GPIOA->CRL);
GPIOA->CRL = (GPIOA->CRL & ~GPIO_CRL_MODE5_Msk) | _VAL2FLD(GPIO_CRL_MODE5, 0x2);

Ловушка 1: продвижение маленьких типов до int

Перед арифметическими и побитовыми операциями значения типов уже int — uint8_t, int8_t, uint16_t на 32-битных платформах — продвигаются до int (C11, 6.3.1.1). Поэтому:

uint8_t flags = 0x0F;
if (~flags == 0xF0) {
    /* never executed: ~flags is the int 0xFFFFFFF0 (-16), not 0xF0 */
}
if ((uint8_t)~flags == 0xF0) {
    /* executed */
}

Так же продвигаются операнды сдвига: uint8_t b = 0x80; b << 1 даёт int со значением 256, а не 0, и обрезается до байта только при присваивании в uint8_t. Это часто полезно — при сборке 16-битного значения из двух байтов (hi << 8) | lo старший байт не теряется, — но при сравнениях и инверсии результат приходится явно приводить к нужному типу.

Ловушка 2: сдвиги

Стандарт C11 (6.5.7) задаёт для сдвигов несколько случаев неопределённого поведения — компилятор вправе считать, что их не бывает, и оптимизировать код исходя из этого:

  • сдвиг на отрицательное число бит или на число, большее или равное ширине продвинутого левого операнда: x << 32 для 32-битного x не определён, и рассчитывать на результат 0 нельзя;
  • сдвиг влево отрицательного знакового числа или такой сдвиг положительного, результат которого не помещается в тип.

Сдвиг вправо отрицательного знакового числа стандарт оставляет на усмотрение реализации; GCC выполняет его с расширением знака, то есть как арифметический сдвиг. Беззнаковые типы ведут себя предсказуемо: результат берётся по модулю 2^N (C11, 6.2.5p9), поэтому для масок и регистров используют только беззнаковые типы.

Ширина int зависит от платформы: у avr-gcc он 16-битный, поэтому выражение 1 << 15 уже выходит за пределы знакового int, а 1 << 16 не определено. Для масок старше 15-го бита на AVR и старше 31-го на ARM нужна константа подходящего беззнакового типа:

uint32_t mask = 1UL << 20;     /* OK on AVR and ARM */
uint32_t bad  = 1 << 20;       /* undefined on AVR: int is 16 bits */
uint64_t big  = 1ULL << 40;

Ловушка 3: битовые поля структур

Структура с битовыми полями выглядит удобным описанием регистра, но порядок полей внутри слова, их размещение и выравнивание стандарт относит к поведению реализации (C11, 6.7.2.1), а GCC — к ABI конкретной платформы. Обычный int в битовом поле у GCC знаковый, если не указан ключ -funsigned-bitfields. Наконец, запись в поле — это чтение всего слова, изменение и запись, что для регистров с битами «сброс записью единицы» может случайно сбросить чужие флаги. Поэтому заголовки CMSIS и avr-libc описывают регистры масками и номерами бит, а не битовыми полями.

Атомарность

Операция REG |= mask на большинстве архитектур — три действия: чтение, изменение, запись. Если между ними сработает прерывание, которое меняет тот же регистр, его изменение потеряется. Решения зависят от архитектуры:

  • AVR: для регистров ввода-вывода с адресами 0x00–0x1F запись одного бита компилируется в одну команду SBI или CBI, она атомарна; для остальных — запрет прерываний на время операции (подробности в системе команд AVR).
  • STM32: регистр BSRR устанавливает и сбрасывает биты порта одной записью без чтения, а у Cortex-M3 и M4 есть ещё область bit-band — отдельный адрес для каждого бита.
  • Общий случай: запретить прерывания на время чтения-изменения-записи или изменять регистр только в одном месте программы.

Полезные приёмы

ЗадачаВыражение
x — степень двойки (x ≠ 0)(x & (x - 1)) == 0
сбросить младшую единицуx & (x - 1)
выделить младшую единицуx & (~x + 1) для беззнакового x
старший и младший байт 16-битного числа(uint8_t)(v >> 8), (uint8_t)v
собрать 16 бит из двух байтов(uint16_t)((hi << 8) | lo)
остаток от деления на 2^kx & ((1U << k) - 1) для беззнакового x
проверить, установлены ли все биты маски(x & m) == m
проверить, установлен ли хотя бы один(x & m) != 0

Деление беззнакового числа на 2^k сдвигом x >> k эквивалентно делению с отбрасыванием остатка; для отрицательных знаковых чисел — нет: GCC сдвигает с расширением знака и округляет вниз (−3 >> 1 = −2), а деление в C округляет к нулю (−3 / 2 = −1). Подсчёт контрольных сумм на сдвигах и исключающем ИЛИ — например, CRC — можно проверить в калькуляторе CRC, который генерирует такой код на C.

Частые вопросы

Как установить и сбросить бит в регистре на C?

Установить — REG |= (1 << n), сбросить — REG &= ~(1 << n), переключить — REG ^= (1 << n), проверить — if (REG & (1 << n)). Для AVR в avr-libc есть макрос _BV(n), равный (1 << n): PORTB |= _BV(PB5). Если номер бита 15 и больше, единицу записывают как беззнаковую нужной ширины — 1UL << 20, — иначе на 16-битном int AVR сдвиг выйдет за разрядность.

Почему 1 << 15 на AVR даёт отрицательное число?

У avr-gcc int 16-битный, а константа 1 имеет тип int. Сдвиг 1 << 15 даёт значение 32 768, которое в 16-битный знаковый int не помещается, — по стандарту C11 (6.5.7) это неопределённое поведение, а сдвиг на 16 и больше не определён в любом случае. Пишите 1U << 15 для 16-битных масок и 1UL << n для 32-битных.

Можно ли использовать битовые поля структур для регистров?

Лучше не надо. Стандарт оставляет порядок битовых полей внутри слова и их выравнивание на усмотрение реализации, GCC перекладывает это на ABI платформы, а запись в поле компилятор выполняет как чтение-изменение-запись всего слова. Для регистров надёжнее маски и сдвиги: так написаны заголовки CMSIS и avr-libc.

Атомарна ли операция REG |= mask?

В общем случае нет: это чтение, изменение и запись, и прерывание между ними может потерять изменения, сделанные в обработчике. У AVR запись одного бита в регистр ввода-вывода с адресом 0x00–0x1F компилятор превращает в одну команду SBI или CBI, и она атомарна. У STM32 для портов есть регистр BSRR, который устанавливает и сбрасывает биты одной записью без чтения.

Источники

  1. ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — 6.2.5p9 — беззнаковая арифметика по модулю; 6.3.1.1 — целочисленное продвижение; 6.5.7 — сдвиги: неопределённое поведение при отрицательном или слишком большом сдвиге и при сдвиге влево отрицательного числа, сдвиг вправо отрицательного — на усмотрение реализации; 6.7.2.1 — битовые поля
  2. GCC Manual: Integers implementation; Structures, Unions, Enumerations, and Bit-Fields implementation — GCC: только дополнительный код, >> над отрицательным числом — с расширением знака; порядок и размещение битовых полей определяет ABI
  3. GCC Wiki: avr-gcc — ABI and implementation defined behaviour — у avr-gcc int 16-битный, long — 32-битный, порядок байтов little-endian
  4. AVR-LibC: <avr/sfr_defs.h> (github.com/avrdudes/avr-libc) — макросы _BV(bit), bit_is_set, loop_until_bit_is_set

Нашли ошибку?