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

Битовые операции в 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.

Ловушка 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

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