Битовые операции в 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 |
|---|---|---|
| установить бит n | x |= (1 << n) | PORTB |= (1 << PB5); |
| сбросить бит n | x &= ~(1 << n) | PORTB &= ~(1 << PB5); |
| переключить бит n | x ^= (1 << n) | PORTB ^= (1 << PB5); |
| проверить бит n | x & (1 << n) | if (PINB & (1 << PB0)) |
| установить несколько бит | x |= (1 << a) | (1 << b) | TCCR1B |= (1 << CS11) | (1 << CS10); |
| записать поле из k бит со сдвигом s | x = (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
| Запись | arm-none-eabi-gcc: int — 32 бита | avr-gcc: int — 16 бит |
|---|---|---|
| 1 << 31 | int, 32 бита: результат не помещается — неопределённое поведение | int, 16 бит: сдвиг на 31 — неопределённое поведение |
| 1U << 31 | unsigned int: 0x80000000 | unsigned int, 16 бит: сдвиг на 31 — неопределённое поведение |
| 1UL << 31 | unsigned long: 0x80000000рекомендуется | unsigned long: 0x80000000рекомендуется |
| 1ULL << 31 | unsigned long long: 0x80000000 | unsigned long long: 0x80000000 |
| Запись | arm-none-eabi-gcc: int — 32 бита | avr-gcc: int — 16 бит |
|---|---|---|
| 0x3 << 20 | int: 0x300000 | int, 16 бит: сдвиг на 20 — неопределённое поведение |
| 0x3U << 20 | unsigned int: 0x300000 | unsigned int, 16 бит: сдвиг на 20 — неопределённое поведение |
| 0x3UL << 20 | unsigned long: 0x300000рекомендуется | unsigned long: 0x300000рекомендуется |
| 0x3ULL << 20 | unsigned long long: 0x300000 | unsigned 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^k | x & ((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, который устанавливает и сбрасывает биты одной записью без чтения.
Источники
- ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — 6.2.5p9 — беззнаковая арифметика по модулю; 6.3.1.1 — целочисленное продвижение; 6.5.7 — сдвиги: неопределённое поведение при отрицательном или слишком большом сдвиге и при сдвиге влево отрицательного числа, сдвиг вправо отрицательного — на усмотрение реализации; 6.7.2.1 — битовые поля
- GCC Manual: Integers implementation; Structures, Unions, Enumerations, and Bit-Fields implementation — GCC: только дополнительный код, >> над отрицательным числом — с расширением знака; порядок и размещение битовых полей определяет ABI
- GCC Wiki: avr-gcc — ABI and implementation defined behaviour — у avr-gcc int 16-битный, long — 32-битный, порядок байтов little-endian
- AVR-LibC: <avr/sfr_defs.h> (github.com/avrdudes/avr-libc) — макросы _BV(bit), bit_is_set, loop_until_bit_is_set
Нашли ошибку?
Смотрите также
- Программирование МКC для МК, битовые операции, фиксированная точка, регистры
- C для микроконтроллеровТипы, stdint, volatile, атомарность, регистры, константы во Flash
- Система команд AVRВсе инструкции с флагами и тактами, адресация, примеры кода
- GPIO STM32Режимы выводов, регистры F1 и F4, BSRR, AF, скорость, токи, 5 В, код
- Калькулятор CRCCRC-8, CRC-16, CRC-32, Modbus, блок CRC STM32, код на C
- Битовые маскиПоле регистра: маска, сдвиг, чтение и запись, код на C