Мой самый быстрый учитель двоичной системы стоил копейки: восемь светодиодов в ряд на макетке. Ни одна таблица не объяснила мне байт так наглядно, как эта линейка: 255 — горит всё, 127 — горит ровно половина справа, 128 — одинокий огонёк в дальнем конце. Показания потенциометра перестали быть отвлечённым числом: крутишь ручку — и видишь, как единицы прибегают по одной, как бит за битом растёт число. Перевод в голове, за которым я раньше лез в калькулятор, случается сам, прямо перед глазами.
В этом уроке соберём такой светящийся байт: потенциометр на A0 задаёт число 0–255 через map, шкала из восьми светодиодов показывает его в двоичном виде, а Serial печатает обе записи рядом. Разберём степени двойки и честный диапазон байта, познакомимся с функцией bitRead() и её ручным эквивалентом через сдвиг и маску. Шкала — та же железка, что в уроке про бегущие огни: восемь светодиодов через резисторы 220 Ом на цифровых пинах, каждый со своей ножкой. Если светодиод вы подключаете впервые — начните с первого урока, там вся электрика.
Байт: восемь тумблеров и 256 состояний
Байт — восемь бит, и каждый бит — тумблер с весом-степенью двойки: 1, 2, 4, 8, 16, 32, 64, 128. Бит 0 с весом 1 зовут младшим, бит 7 с весом 128 — старшим; складывая веса включённых тумблеров, получаете число. Младший бит прыгает чаще всех: любое нечётное число держит его включённым. Максимальная сумма — 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 — даёт 255, поэтому у байта диапазон 0–255, а всего значений 256: на единицу больше, чем максимум, из-за нуля.
Диапазоны соседних типов масштабируются той же логикой. int на Uno занимает два байта — 16 бит — и умеет −32768…32767, потому что старший бит отдан знаку; unsigned int берёт те же 16 бит целиком: 0–65535. Четырёхбайтовый unsigned long добирается до 4 294 967 295 — на таких числах тикает millis(), и его переполнение наступает на 49,7 сутки. Разбор этих типов по байтам я делал в уроке про переменные — сейчас важно одно: выбирая тип, вы выбираете ширину линейки, и у каждой ширины свой потолок.
| DEC | BIN | Что видно на шкале |
|---|---|---|
| 0 | 00000000 | всё погашено; ноль — тоже законное значение, всего их 256 |
| 5 | 00000101 | 4 + 1: два огонька через один; соседние числа меняют картинку точечно |
| 127 | 01111111 | 64+32+16+8+4+2+1 — максимум без старшего бита, горит правая половина |
| 255 | 11111111 | весь байт: 8 бит по 13-15 мА — около 110-120 мА на линейку, USB это тянет |
bitRead(): достать один бит
Чтобы зажечь светодиод по биту, бит надо достать. У Arduino есть готовое семейство функций: bitRead(x, n) возвращает 0 или 1 — состояние бита n; рядом живут bitSet(x, n), bitClear(x, n), bitWrite(x, n, b) и bit(n), возвращающая единицу, сдвинутую на n разрядов. Нумерация с нуля и справа: bitRead(0b101, 0) вернёт 1, bitRead(0b101, 2) тоже 1, а bitRead(0b101, 1) — 0.
Под капотом bitRead — одна строчка арифметики: сдвиг вправо на n разрядов и маска И с единицей — (x >> n) & 1. Сдвиг выталкивает нужный бит в младшую позицию, И с 1 обнуляет всё, что тянулось за ним следом. Я советую в скетчах писать bitRead — имя читается, — но формула нужна вам на каждом шагу, поэтому ниже в симуляторе она стоит открыто, а в скетче для платы та же строка завёрнута в bitRead.
Печатать байт в монитор удобнее всего через Serial.print(x, BIN) — второй аргумент мы разбирали в уроке про BIN, HEX и DEC. Там же лежит грабля, которая больно щипнула и меня: ведущие нули BIN не рисует, и 5 печатается как 101, а не 00000101. На шкале проблемы нет — там ровно восемь лампочек и пустота между ними — а в журнале я дорисовываю нули циклом от старшего бита к младшему, иначе двоичные строки разной длины сливаются в кашу.
const int POT = A0;const int BITS[8] = {5, 6, 7, 8, 9, 10, 11, 12}; // бит 0 -> пин 5 ... бит 7 -> пин 12int proshloe = -1; void setup() { for (int i = 0; i < 8; i = i + 1) { pinMode(BITS[i], OUTPUT); } Serial.begin(9600); Serial.println("Krutite ruchku: chislo 0-255 na shkale");} void loop() { int chislo = map(analogRead(POT), 0, 1023, 0, 255); if (chislo != proshloe) { // рисуем только при изменении proshloe = chislo; for (int i = 0; i < 8; i = i + 1) { digitalWrite(BITS[i], bitRead(chislo, i)); // то же, что (chislo >> i) & 1 } Serial.print("dec="); Serial.print(chislo); Serial.print(" bin="); for (int i = 7; i >= 0; i = i - 1) { // от старшего к младшему: нули на месте Serial.print(bitRead(chislo, i)); } Serial.println(); } delay(50);}Три места в этом скетче, где новичков обычно ждёт сюрприз. Первое — map(analogRead(POT), 0, 1023, 0, 255): АЦП Uno десятибитный, его родной диапазон 0–1023, и map сжимает его в байт. Сжимает с усечением дробной части: показание 511 превращается в 127, а 514 — уже в 128, поэтому граница старшего бита у ручки проходит чуть дальше середины хода. Механику analogRead с шагом около 4,9 мВ на деление я раскладывал в уроке про потенциометр.
Второе — строка if (chislo != proshloe). Без неё скетч печатал бы в монитор пять строк в секунду даже на неподвижной ручке, а заодно перерисовывал шкалу на каждом проходе. Сравнение с прошлым значением — дешёвый способ сделать лог читаемым: строка появляется ровно тогда, когда число изменилось. Третье — ток: при 220 Ом на сегмент линейка из восьми горящих ест 110-120 мА, USB-порт с его 500 мА это переваривает, но добавлять на ту же макетку серво уже стоит с отдельным питанием, о чём я предупреждал ещё в уроке про шкалу.
Биты, байты и переполнение — видно глазами, а не в голове.
Подгружаем интерактив…
- Байт — восемь бит с весами 1-2-4-8-16-32-64-128; диапазон 0-255, всего 256 значений
- int на Uno — два байта (−32768…32767), unsigned long — четыре (до 4 294 967 295, millis переполняется на 49,7 сутки)
- bitRead(x, n) возвращает бит n; её механика — сдвиг и маска: (x >> n) & 1
- map(analogRead, 0, 1023, 0, 255) сжимает десятибитный АЦП в байт, дробная часть усекается: 514 даёт 128
- Serial.print(x, BIN) не рисует ведущие нули — в журнале их дорисовывают циклом от старшего бита
- 1. Сколько разных значений помещается в один байт?
- 2. Что вернёт bitRead(0b00000101, 2)?
- 3. АЦП выдал 514. Что напечатает map(514, 0, 1023, 0, 255)?
- 4. Почему у числа 127 на шкале не горит старший светодиод?
- 5. Что напечатает Serial.print(5, BIN)?
Покрутите шкалу ещё раз с блокнотом: поставьте ручку на 85 и попробуйте предсказать картинку до взгляда — 85 это 64 + 16 + 4 + 1, значит, горят биты 6, 4, 2 и 0. Когда предсказание начнёт сходиться с первого раза, двоичную систему можно считать посаженной. Следующий шаг — арифметика над этими битами: как включить один бит, не тронув остальные, как переключить и как проверить. Это побитовые операции, и на той же шкале они превращаются в эффекты, которые вы уже умеете читать глазами.
Частые вопросы по теме урока
- Сколько разных значений помещается в один байт?
- Восемь бит дают 2 в восьмой степени сочетаний — 256. Максимум 255, но ноль тоже считается, поэтому значений именно 256. 256
- Что вернёт bitRead(0b00000101, 2)?
- Нумерация битов с нуля справа: в 0b101 установлены биты 0 и 2. Значит bitRead с номером 2 вернёт 1. 1 — второй бит установлен
- АЦП выдал 514. Что напечатает map(514, 0, 1023, 0, 255)?
- 514 × 255 / 1023 ≈ 128,1, и map отбрасывает дробную часть: получается 128. Соседние показания 511-513 остаются на 127 — граница старшего бита съезжает за середину хода. 128 — дробная часть усекается
- Почему у числа 127 на шкале не горит старший светодиод?
- 127 складывается из весов 64+32+16+8+4+2+1 — это все биты, кроме старшего. Чтобы зажечь бит 7, нужно число от 128 до 255. потому что 127 = 0b01111111: сумма всех весов без старшего бита 128
- Что напечатает Serial.print(5, BIN)?
- BIN печатает значащие цифры: 101. Нули слева исчезают — на байт они намекают, но их ширина плавает, поэтому в журналах нули дорисовывают циклом. 101 — ведущие нули не выводятся