Практическое применение RISC-V при программировании микроконтроллеров
Сначала пару слов о терминологии:
Битовая маска - число, задающее с какими битами идет работа. Обычно это двоичное число, состоящее из нулей и с единственной единицей на месте интересующего бита. Бывает наоборот - все единицы и единственный ноль. Реже вместо одной единицы идет последовательность из двух, трех и т.д. единиц. Совсем редко единицы идут вразнобой, но это используется либо для какой-то совсем странной периферии, либо если несколько масок накладываются одновременно.
Обозначение числа как abcdefgh - каждой буквой обозначается отдельный бит. Хотя контроллер у нас 32-битный, писать 32 буквы было бы долго и ненаглядно, поэтому здесь переменные 8-битные.
3.1. AND (побитовое умножение)
A | B | A & B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
Среди битов результата 1 будет только на тех позициях, на которых в обоих аргументах были 1. Если хотя бы в одном аргументе был 0, в результате тоже будет 0.
A abcdefgh
B 00001000 &
A & B 0000e000
Стирает (выставляет в 0) все биты аргумента за исключением тех, которые в маске выставлены в 1.
3.2. OR (побитовое сложение)
A | B | A & B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
Среди битов результата 0 будет только на тех позициях, на которых в обоих аргументах был 0. Если хотя бы в одном аргументе была 1, в результате тоже будет 1.
A abcdefgh
B 00001000 |
A | B abcd1fgh
Выставляет в 1 все биты, в которых в маске была 1.
3.3. Сдвиги
Аргумент сдвигается на нужное количество битов вправо или влево. Освободившиеся места дополняются нулями.
A abcdefgh
A << 2 cdefgh00
A >> 2 00abcdef
Простейшая битовая маска
A=1 00000001
A << 2 00000100
Число (битовая маска), содержащее единственный бит на нужной позиции
A 00000abc
A << 2 000abc00
Число (битовая маска), содержащее последовательность битов начиная с нужной позиции
3.4. Побитовая инверсия
Меняет каждый бит на противоположный: 0->1, 1->0
A 10011110
~A 01100001
3.5. Запись значения начиная с нужного бита
Мы с этим уже сталкивались при настройке порта. Допустим, надо записать комбинацию из четырех битов WXYZ в 16-битный регистр начиная с 4-й позиции
0b1111 00000000 00001111
(0b1111<<4) 00000000 11110000
~(0b1111<<4) 11111111 00001111
Далее полученная маска накладывается на исходную переменную:
A abcdefgh ijklmnpq
~(0b1111<<4) 11111111 00001111 &
B abcdefgh 0000mnpq
Потом формируется значение для присваивания:
C 00000000 0000WXYZ
(C<<4) 00000000 WXYZ0000
Которое накладывается на обрезанную по маске переменную
B abcdefgh 0000mnpq
(C<<4) 00000000 WXYZ0000 |
D abcdefgh WXYZmnpq
Таким образом путем наложения двух битовых масок можно определенным битам переменной присвоить любое значение.
3.6. XOR (Исключающее или)
A | B | A & B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Инвертирует те биты аргумента, в которых в маске были 1.
A 10011110
B 00100100 ^
A ^ B 10111010
3.7. Специальный регистр BOP
Рассмотренная ранее работа непосредственно с OCTL не является безопасной. Если между чтением и записью возникнет прерывание, пытающееся также поменять значение OCTL, после выхода результат его работы будет перезаписан основным кодом. Чтобы этого избежать, в GD32VF103 есть специальный регистр BOP, старшие 16 битов которого отвечают за очистку соответствующих битов OCTL, а младшие - за выставление в 1. Значение имеет только запись единицы, запись нуля ни на что не влияет
li t0, PORTB_BOP
li t1, (1<<5)<<16
sw t1, 0(t0)
# PB5 = 0
li t1, (1<<6)
sw t1, 0(t0)
# PB6 = 1
В данном случае код, реализующий мигалку через чтение OCTL и запись BOP, приводить не обязательно, поскольку задача абсолютно не ответственная, и можно обойтись xor’ом. К тому же в реальности куда чаще встречается задача сброса в 0 или выставления в 1 независимо от текущего состояния.
3.8. Логические операции
Большая путаница возникает у изучающих язык Си при знакомстве с побитовыми (|
, &
, ~
)и логическими (||
, &&
, !
) операциями. Разница между ними в том, что побитовые работают с переменной как с массивом отдельных битов, никак не связанных друг с другом, а логические - как с единым целым, где лог.0 это ноль и только ноль, а лог.1 - все остальное. Причем результат логической операции строго 0 или 1. Например:
uint8_t x = 0b00001111
uint8_t y = 0b01010101
uint8_t z;
z = x | y; // 0b01011111
z = x || y; // 0b00000001
z = x & y; // 0b00000101
z = x && y; // 0b00000001
z = ~x; // 0b11110000
z = !x; // 0b00000000
3.9. Частые ошибки
-
Использование побитовой операции в условии:
if( (x >= '0') & ( x <= '9') )
- неправильно -
Использование логической операции для работы с битами
-
Использование арифметических операций вместо логических или побитовых
-
Использование конструкций вида
x = x | (0<<3);
в надежде обнулить бит.
Д/З:
-
Задано число, надо определить является ли оно степенью двойки или нет. Подсказка: в двоичной записи степень двойки содержит единственную единицу.
-
Реальная задача, которую мне когда-то пришлось решать. В заголовочных файлах от производителей часто встречаются битовые маски, покрывающие сразу все поле регистра, например 0b00111000. Биты 3, 4, 5 вместе отвечают за режим работы чего-нибудь. Одна из практических задач - выставить эти биты в нужное значение, например, в 0b001. На языке Си это может выглядеть как
.
reg = (reg &~(0b111<<3)) | (0b001<<3)
Но здесь мне пришлось указать и размер битового поля, и смещение. Как при помощи битовой магии добиться того же, зная только маску, покрывающую все поле?