3а. Битовые операции
Всё программирование микроконтроллера сводится к одному действию: поставить, снять или прочитать биты в регистре. Раздел 3 разобрал язык в целом; этот — про ту его часть, которой пользуются в каждой второй строке, и про ловушки, которые в ней спрятаны. Каждая формула здесь проверена компилятором.
Как это называется в документации
Оригинал Перевод bit mask битовая маска to set / clear / toggle a bit установить / сбросить / инвертировать бит bit field битовое поле; поле из нескольких бит position / mask ( _Pos/_Msk)позиция / маска поля в заголовках shift left / right сдвиг влево / вправо logical / arithmetic shift логический / арифметический сдвиг integer promotion целочисленное расширение read-modify-write чтение-изменение-запись population count число единиц в слове leading zeros ведущие нули alignment выравнивание endianness порядок байтов
3а.1 Четыре действия над битом
Всё начинается с маски — слова, в котором стоит ровно нужный бит:
#define BIT(n) (1u << (n)) /* BIT(13) == 0x2000 */
| Действие | Запись | Результат для x = 0x20, бит 5 |
|---|---|---|
| установить | x \| BIT(5) |
0x20 |
| сбросить | x & ~BIT(5) |
0x00 |
| инвертировать | x ^ BIT(5) |
0x00 (был — снят) |
| проверить | (x & BIT(5)) != 0 |
истина |
Три операции — три оператора: | ставит, & ~ снимает, ^ переворачивает.
Четвёртая — & — читает, и здесь первая ловушка.
⚠️ Результат
x & BIT(n)— маска, а не 0/1. Для бита 5 это0x20или0. Сравнение(x & BIT(5)) == 1ложно всегда, кроме нулевого бита, — и код «работает» ровно до того момента, когда бит переносят. Сравнивать с нулём:!= 0. То же относится кREAD_BITиз заголовков (приложение B).
Суффикс u в 1u — не украшение, о нём в 3а.3.
3а.2 Поле из нескольких бит
Режим вывода в GPIOC->CRH занимает два бита, делитель в RCC->CFGR —
четыре. Для поля шириной w на позиции p — три формулы:
#define FIELD_MSK(p, w) (((1u << (w)) - 1u) << (p))
#define FIELD_GET(x, p, w) (((x) >> (p)) & ((1u << (w)) - 1u))
#define FIELD_SET(x, p, w, v) (((x) & ~FIELD_MSK(p, w)) | (((v) << (p)) & FIELD_MSK(p, w)))
Для трёх бит с позиции 4: маска 0x70; из 0x65 извлекается 6;
запись 2 даёт 0x25.
Заголовки CMSIS избавляют от арифметики — у каждого поля есть готовые
имена: GPIO_CRH_MODE13 — маска 0x00300000, GPIO_CRH_MODE13_0 —
её младший бит. У F4 ещё и _Pos: GPIO_MODER_MODER13_Pos равен 26.
Запись поля через них:
GPIOC->CRH = (GPIOC->CRH & ~GPIO_CRH_MODE13) | GPIO_CRH_MODE13_1; /* режим 2: выход 2 МГц */
Или тем же макросом из заголовка: MODIFY_REG(GPIOC->CRH, GPIO_CRH_MODE13, GPIO_CRH_MODE13_1).
В окне регистров отладчика эта строка выглядит так — от значения после сброса:
⚠️ Поле вывода у F1 — четыре бита,
MODEиCNF. Строка выше меняет толькоMODE13, аCNF13остаётся01, как после сброса: «стало» на рисунке —0x44644444, тетрада вывода0110, выход с открытым стоком. Светодиоду на PC13, который зажигается нулём, это не мешает, но обычный двухтактный выход получится, только если очистить оба поля (раздел 8.4):
c GPIOC->CRH = (GPIOC->CRH & ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13)) | GPIO_CRH_MODE13_1; /* выход 2 МГц, двухтактный: CNF13 = 00 */Тогда «стало» —
0x44244444.⚠️ Поле меняют одним выражением, а не двумя. Записать
0b10туда, где стояло0b01, двумя шагами — «поставить старший, снять младший» — значит на мгновение получить0b11: чужое значение, которое периферия успеет увидеть. Для режима вывода это короткий импульс не туда, для делителя тактов — скачок частоты.MODIFY_REGочищает поле и записывает новое за одну операцию над регистром.
3а.3 Сдвиги: знак и ширина
Три вещи о << и >>, каждая из которых стоила кому-то дня.
Литерал без суффикса — знаковый int. 1 << 31 переполняет int
и по стандарту — неопределённое поведение. Компилятор вправе сделать
что угодно, и с оптимизацией делает. 1u << 31 — беззнаковое,
определено, равно 0x80000000. Правило: маски — только с u.
Сдвиг вправо знакового отрицательного определяет компилятор. GCC и
armclang для ARM делают его арифметическим: знак размножается, -16 >> 2 равен -4, а не 0x3FFFFFFC. Для регистра,
где старший бит — флаг, а не знак, это мусор. Правило: сдвигать
только беззнаковые.
Сдвиг на ширину типа и больше — неопределён. x << 32 для
32-разрядного x на Cortex‑M даст ноль, но компилятор вправе считать
иначе и, например, выбросить проверку. Переменная величина сдвига
обязана быть меньше ширины.
Сдвиг вправо беззнакового — логический, слева входят нули:
0x80000000u >> 31 равен 1.
3а.4 Ловушка расширения: ~ на узких типах
Перед любой арифметикой uint8_t и uint16_t превращаются в int.
Обычно это незаметно. С инверсией — нет:
uint8_t x = 0x0F;
if (~x == 0xF0) { /* НИКОГДА не истинно */ }
~x вычисляется над int: ~0x0000000F = 0xFFFFFFF0, а не 0xF0.
Присвоение обратно в uint8_t обрежет до 0xF0 — но сравнение до
присваивания сравнивает с 0xFFFFFFF0. Лечится маской или приведением:
(~x & 0xFFu) либо (uint8_t)~x.
То же расширение делает uint8_t a = 200, b = 100; a + b равным 300,
а не 44, — что обычно и нужно; но при сравнении a + b > 255 даёт
не то, что ждут те, кто помнит про переполнение байта.
3а.5 Приёмы в одну строку
Формулы, которые кажутся фокусами, пока не увидишь их разложенными по битам:
| Задача | Запись | Почему работает |
|---|---|---|
| степень двойки | x && !(x & (x - 1)) |
у степени двойки один бит; x − 1 гасит его и зажигает всё ниже |
| младший установленный бит | x & -x |
дополнительный код: −x инвертирует всё выше младшей единицы |
| сбросить младший установленный | x & (x - 1) |
то же вычитание без & с исходным |
выровнять вверх до a (степени двойки) |
(x + a - 1) & ~(a - 1) |
добить до границы, обнулить хвост |
| номер старшего бита | 31 - __CLZ(x) |
ведущие нули считает одна инструкция |
| число единиц | цикл x &= x - 1 |
каждая итерация гасит одну; итераций столько, сколько единиц |
| обмен байтов | __REV(x) |
одна инструкция; 0x12345678 → 0x78563412 |
| чётность | младший бит числа единиц |
Выравнивание — то самое, что делает куча ядра (раздел 25): запрос 92 байта
даёт ALIGN_UP(92, 8) = 96. Число единиц пригодится для подсчёта
активных каналов по маске; старший бит — для приоритетов: планировщик
FreeRTOS находит готовую задачу через __CLZ.
3а.6 Регистры: чтение-изменение-запись и как его избежать
GPIOC->ODR |= BIT(13) — три инструкции: прочитать, изменить, записать
(раздел 30). Прерывание между первой и третьей, поменявшее в том же
регистре другой бит, теряется: запись вернёт старое значение бита.
У STM32 есть два способа обойтись одной записью.
BSRR: установить и сбросить, ничего не читая. Младшие 16 бит —
что установить, старшие — что сбросить; нули не трогают ничего:
GPIOC->BSRR = GPIO_BSRR_BS13 | GPIO_BSRR_BR5; /* 0x00202000: 13 в 1, 5 в 0, остальное как было */
Это атомарно, быстрее на две инструкции и не зависит от того, что
происходит в прерываниях. Для выводов — всегда BSRR, ODR только
для чтения состояния выходов.
Bit-banding: у каждого бита — свой адрес. На Cortex‑M3/M4 биту 13
регистра ODR порта C соответствует слово 0x422201B4 в области
псевдонимов; запись туда атомарна и не трогает соседей (раздел 1,
приложение B). Работает для первого мегабайта периферии и первого мегабайта
SRAM — там, где у блока нет своего BSRR, — кроме регистров флагов,
которые сбрасываются записью: шина сама читает регистр целиком (раздел 1.5).
Псевдонимы есть только у адресов 0x4000 0000–0x400F FFFF: у F4 туда
попадают APB1, APB2 и AHB1, а блоки на AHB2 (USB OTG FS с 0x5000 0000,
DCMI, RNG), регистры FSMC и ядра — нет.
⚠️ Регистры флагов bit-band портит. Запись в псевдоним шина выполняет как чтение всего слова, замену одного бита и запись слова обратно. У
EXTI->PR, где флаг сбрасывают записью единицы, вернувшиеся единицы соседних флагов сбросят и их; уTIMx->SR, где сбрасывают нулём, пропадёт флаг, вставший между чтением и записью. Флаги сбрасывают прямой записью маски:EXTI->PR = EXTI_PR_PR0,TIM2->SR = ~TIM_SR_UIF.
Третий способ — исключительный доступ __LDREXW/__STREXW — для
переменных в памяти, а не регистров (приложение B).
3а.7 Битовые поля C: не для регистров
Язык позволяет описать регистр структурой с полями заданной ширины:
typedef struct {
uint32_t mode : 2;
uint32_t cnf : 2;
uint32_t : 28;
} pin_cfg_t;
Читается красиво — и для регистров периферии не годится. Стандарт
не определяет, в каких битах лежит mode — в младших или старших,
какой шириной компилятор обратится к памяти и сделает ли он это одной
операцией. Для ARM порядок закреплён двоичным соглашением (первое поле —
в младших битах), но регистры это не спасает. Каждое присваивание полю —
отдельное чтение-изменение-запись всего регистра: две строки дают две
записи и промежуточное значение, а флаги, которые сбрасывают записью
единицы, стираются. Ширина обращения зависит от ключей компилятора:
оптимизация может прочитать байт вместо слова, а для регистров это
разные обращения с разным смыслом.
Для упаковки своих флагов в структуре битовые поля уместны: порядок не важен, память своя. Для регистров — только маски и сдвиги: они означают ровно то, что написано.
3а.8 Порядок байтов
Cortex‑M хранит многобайтовые числа младшим байтом вперёд:
0x12345678 лежит в памяти как 78 56 34 12. Проявляется в трёх местах:
при разборе пакетов из сети, где принят обратный порядок (раздел 37);
при чтении датчиков, отдающих старший байт первым (раздел 17);
при просмотре памяти в отладчике, где число «выглядит перевёрнутым».
Лечение — __REV для слова и __REV16 для полуслова, либо сборка
числа из байтов вручную: (hi << 8) | lo. Второе переносимо, первое —
одна инструкция.
3а.9 Грабли
Проверка бита «работает», пока бит нулевой. x & BIT(n) даёт маску, а не 1; сравнивать с
нулём, не с единицей (3а.1).
Периферия на миг получает чужое значение поля. Поле меняют одной записью (MODIFY_REG), а не двумя шагами
«поставить, потом снять» (3а.2).
3а.10 Итоги
- Маска —
1u << n; ставит|, снимает& ~, переворачивает^, читает&— и результат чтения есть маска, а не 0/1. - Поле меняют одним выражением: очистить маску, записать значение; два шага дают чужое промежуточное значение.
- Маски только с
u:1 << 31— неопределённое поведение; сдвигать только беззнаковые; величина сдвига меньше ширины типа. ~надuint8_tдаётintс единицами в старших битах; маскировать или приводить.x & (x − 1)гасит младшую единицу — отсюда проверка степени двойки и подсчёт единиц;(x + a − 1) & ~(a − 1)выравнивает.- Для выводов —
BSRR, а неODR |=: одна запись вместо трёх инструкций и без гонки. - Битовые поля C — для своих структур, не для регистров.
- Cortex‑M — младшим байтом вперёд;
__REVпереворачивает за такт.
Дальше: раздел 4 «Инструментарий» — где всё это компилировать, прошивать и отлаживать.