Embedded HandbookПриложения
Все разделы › I. Основы
STM32F103, STM32F407 · уровень базовый · 10 мин чтения
Открыть в интерактивном справочнике

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).

В окне регистров отладчика эта строка выглядит так — от значения после сброса:

Окно регистра GPIOC->CRH в отладчике: было, маска поля MODE13, стало — одной записью

⚠️ Поле вывода у 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.

Приёмы в одну строку на числе 0x58, разложенном по битам: x минус 1 гасит младшую единицу и зажигает всё ниже, x и x минус 1 сбрасывают младший установленный бит до 0x50, минус x инвертирует всё выше младшей единицы, x и минус x оставляют только её — 0x08; старший бит 31 минус 25 равно 6, три единицы, выравнивание 92 до 96


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 Итоги

Дальше: раздел 4 «Инструментарий» — где всё это компилировать, прошивать и отлаживать.

Открыть в интерактивном справочнике