Дебажил я как-то сдвиговый регистр 74HC595 — та копеечная микросхема, через которую Arduino тремя пинами рулит восемью выходами. Скетч гонит в регистр байт, байт раскладывается по восьми пинам, на них у меня висели реле. Печатаю посланный байт в порт — а там 65. Что включено? Переводи в голове: 65 — это 0b01000001, значит, открыты биты 0 и 6. Каждый вывод — пауза на пересчёт, а байтов в логе — сотни. Вечер улетел на таблицу степеней двойки, хотя ответ лежал в самой функции печати: Serial.print умеет выводить числа сразу в двоичной записи, если попросить его вторым аргументом.
Вторая история — про ловлю одного бита. Устройство присылало байт состояния, я ждал 64: в DEC ровный, невинный номер. Приходило 65 — «почти то же», глаз не цепляется. А в BIN разница кричит: 0b01000000 против 0b01000001 — подожжён младший бит, кто-то затёр флаг. Заодно 65 — код буквы «A» в таблице ASCII: печатая байты-символы, вы видите букву, но никогда — что у неё внутри восемь переключателей. Байт в DEC или в виде буквы — как чек без расшифровки: сумма есть, позиций нет.
В этом уроке мы добавляем к печати одну букву — второй аргумент Serial.print — и байт перестаёт быть чёрным ящиком: BIN показывает биты, HEX сжимает его в две цифры, DEC остаётся для людей. Печатать в порт мы научились ещё в первом уроке про монитор, дальше только новый аргумент. Разберём, где какая запись живёт в реальной работе, посчитаем одно и то же число 202 во всех четырёх системах и пройдёмся по граблям: 0x10 не равно десяти, ведущие нули исчезают, а удобный String(x, BIN) снова наливает строки в оперативку — мы только что выгоняли их оттуда через F(). Внизу урока — симулятор: один и тот же байт с ползунка печатается четырьмя записями разом, гоняйте и сравнивайте.
Второй аргумент: BIN, HEX, DEC, OCT
У Serial.print есть вторая форма: первым аргументом — число, вторым — система счисления. Serial.print(202, BIN) печатает 11001010, Serial.println(202, HEX) — CA. Без второго аргумента печать идёт в DEC, десятичной. Никакой новой функции: у print и println есть несколько перегрузок, и та, что принимает два аргумента, переводит число в нужную запись сама. Имена BIN, HEX, DEC, OCT — обычные константы ядра со значениями 2, 16, 10 и 8: Serial.print(x, 16) легально и печатает то же, что Serial.print(x, HEX).
Одна оговорка про дробные, чтобы не путаться: у print есть перегрузка print(double, digits), где второй аргумент — число знаков после запятой, а вовсе не система счисления: Serial.print(23.4, 2) — это два знака, не HEX. И дробное в BIN не переведётся ни в какую: перегрузка выбирается по типу аргумента, а для double второй слот занят точностью. Нужно биты из float — умножайте на масштаб и округляйте сами, честный целочисленный путь.
Одно и то же число 202 во всех четырёх записях — в таблице ниже. Значение не меняется, меняется способ записи, как одно слово на разных языках: 202 = 0b11001010 = 0xCA = 312 в восьмеричной. Байт всегда влезает в 0–255, и у каждой записи характерная длина: BIN — до восьми цифр, HEX — ровно две, DEC — две-три.
Где какая запись встречается в работе. HEX — язык datasheet: в документе на ATmega328P адреса регистров даны в шестнадцатеричной — PINB на 0x23, DDRB на 0x24, PORTB на 0x25; там же дампы памяти и MAC-адреса. BIN — язык битов: маски, конфигурация регистров, состояния кнопок — всё, где важно видеть каждый разряд. DEC — язык человека: показания датчиков, счётчики, время. OCT досталась другая роль: ею записаны права файлов в Unix (chmod 644); в Arduino-делах она почти не нужна, но пусть лежит в копилке.
| Система | Запись 202 | Цифр на байт | Где встречается |
|---|---|---|---|
| DEC | 202 | 2–3 | показания датчиков, счётчики — числа для человека |
| BIN | 11001010 | до 8 | битовые маски, состояние регистров и кнопок — видно каждый бит |
| HEX | CA | ровно 2 | datasheet: адреса регистров ATmega328P (PINB — 0x23); дампы, MAC-адреса |
| OCT | 312 | до 3 | права файлов Unix (chmod 644); в скетчах — экзотика |
void setup() { Serial.begin(9600); Serial.println(202); // DEC по умолчанию: 202 Serial.println(202, BIN); // 11001010 Serial.println(202, OCT); // 312 Serial.println(202, HEX); // CA // BIN, OCT, HEX, DEC — просто числа 2, 8, 16, 10 Serial.println(202, 2); // 11001010 — то же, что с BIN} void loop() {}Грабли: 0x10 не равно 10, нули-невидимки и цена String
Первая грабля — наипростейшая и самая частая: 0x10 — это не десять. Префикс 0x означает «дальше цифры по основанию 16», и запись «10» в ней — шестнадцать, то есть 0b00010000. Я однажды полчаса смотрел на код, где маска была списана из datasheet как 0x10, а в голове жила как «десять»: проверка if (maska == 10) молчала, потому что маска на самом деле была 16. Правило перекрёстной проверки, которое меня спасает: любое HEX-число прогоняйте через печать в BIN — Serial.println(0x10, BIN) печатает 10000, и всё встаёт на места.
Вторая грабля: Serial.print(0b00001111, BIN) печатает 1111 — только значащие разряды. Для числа это корректно, а для регистра из восьми бит — беда: старшая половина исчезла, и маска выглядит четырёхбитной. У меня состояние восьми кнопок четыре дня выглядело в логе как «1111», пока не выяснилось, что это 00001111, — строка была короче, чем байт. Встроенного способа задать ширину поля у Print нет — printf на Arduino тоже нет из коробки. Честное лечение — самодельная функция pad8(): цикл по маскам от 128 вниз, на каждой печатаем «1» или «0», — и байт выходит со всеми восемью разрядами, нули на месте. Код — ниже, в практике.
Третья грабля — платная. String(202, BIN) даёт строку «11001010», которую удобно склеить с текстом, и соблазн велик. Но каждый вызов String заводит объект в куче — в оперативке: восемь символов плюс около шести байт служебных полей (указатель, длина, ёмкость), а после освобождения остаётся дырка. В прошлом уроке про F() мы только что выгнали литералы из SRAM — и я же тут зазываю их обратно, причём в loop() такие строки множатся каждую итерацию. На Uno с её 2048 байтами RAM плотный лог со String-склейкой наступает на те же грабли зависаний. Для печати хватает второго аргумента print; String — только когда строку нужно именно хранить или передавать.
Практика: кнопки в BIN, логи в HEX
Первый сценарий — сбор состояния кнопок в один байт. Восемь кнопок на пинах 2–9, каждая через INPUT_PULLUP (нажата — LOW, как в уроке про digitalRead): опрашиваем пины, на каждую нажатую поднимаем бит — knopki |= (1 << i). Печатаем байт в BIN — и не надо помнить, что 137 — это биты 0, 3 и 7: в записи 10001001 нажатые кнопки видны глазами. Каждую из восьми кнопок можно проверить и по отдельности — if ((knopki & 0b00000001) == 1) означает «нажата кнопка ноль»; побитовые операции отлично работают в паре с обычными условиями из урока про if/else.
Второй сценарий — маски. knopki & 0b00001111 оставляет младшие четыре бита — первая четвёрка кнопок; (knopki >> 4) & 0b00001111 — вторая. А чтобы в BIN-выводе не терялись старшие нули, в скетч добавлена pad8(): от маски 128 вниз, бит за битом, без единого String — печатаем «1» и «0» прямо в порт. На выходе всегда восемь разрядов: 00001111, а не 1111.
Третий — логи. В HEX байт укладывается ровно в две цифры, поэтому 32-битный millis() печатается максимум восемью знаками (переполнение — 0xFFFFFFFF), а в DEC — десятью. К исходу первых суток метка 86 400 000 пишется как 0x5265C00 — семь знаков против восьми, и чем длиннее лог, тем заметнее экономия: короткая метка дешевле глазами и по месту. В журнале, где каждая строка на счету — как в журнале показаний из урока про массивы, — HEX-метки читаются и сортируются так же, как DEC: порядок чисел сохраняется. А защищать millis() от переполнения через ~49,7 суток всё равно нужно приёмами из урока про millis() — сама по себе система счисления тут не спасает.
// Восемь кнопок на пинах 2–9 собираются в один байтconst int PERVYI_PIN = 2; // кнопки замыкают пины на GND void setup() { Serial.begin(9600); for (int p = PERVYI_PIN; p < PERVYI_PIN + 8; p++) { pinMode(p, INPUT_PULLUP); // нажата кнопка → LOW }} void pad8(int b) { // печать байта с ведущими нулями for (int maska = 128; maska > 0; maska = maska / 2) { if ((b & maska) == maska) Serial.print("1"); else Serial.print("0"); }} void loop() { static unsigned long last = 0; if (millis() - last >= 1000) { last = millis(); int knopki = 0; for (int i = 0; i < 8; i++) { if (digitalRead(PERVYI_PIN + i) == LOW) knopki |= (1 << i); } Serial.print("knopki="); Serial.print(knopki); // DEC: человеку Serial.print(" bin="); pad8(knopki); // 10001001 — видны все восемь разрядов Serial.print(" nizhnie="); Serial.println(knopki & 0b00001111); // маска: младшие четыре кнопки }}- Запустите симулятор, подвигайте ползунок A0 и поймайте глазами цепочку «dec=202 → bin=11001010 → hex=CA». Потом переведите сами 150: в BIN это 10010110, в HEX — 0x96; сверьтесь с монитором.
- Посчитайте на бумаге 137 в HEX по правилу «4 бита на цифру»: 1000 1001 → 8 и 9 → 0x89. Проверьте: Serial.println(137, HEX).
- Замените в pad8() стартовую маску 128 на 1 и шаг деления на умножение: биты напечатаются наоборот, младший первым. Разберите, что теперь означает строка 10000000.
- Добавьте в скетч с кнопками две строки: Serial.print(" starshie="); и Serial.println((knopki >> 4) & 0b00001111); — старшие четыре кнопки отдельным числом. Нажимайте кнопки и сверяйте половинки с полным байтом.
- Рядом с DEC-логом выведите Serial.println(millis(), HEX) и сравните длину строк за минуту работы; вспомните, что millis() переполняется через ~49,7 суток — 0xFFFFFFFF.
- Serial.print(x, BIN) печатает число в нужной системе: BIN — 2, OCT — 8, DEC — 10, HEX — 16; без второго аргумента — DEC
- BIN — язык битов: маски, состояния кнопок и регистров видны глазами; HEX — компактная запись байта в две цифры и язык datasheet: адреса регистров ATmega328P даны в HEX
- 0x10 — это 16, а не 10: цифры после 0x читаются по основанию 16; перекрёстная проверка — Serial.println(0x10, BIN) печатает 10000
- Ведущих нулей в Serial.print нет: 0b00001111 печатается как 1111; функция pad8() — цикл по маскам от 128 вниз — возвращает все восемь разрядов
- String(x, BIN) создаёт объект в куче — на Uno с её 2048 байтами RAM в loop() это фрагментация и зависания; для печати хватает второго аргумента print
- 1. Что напечатает Serial.print(0x10); — без второго аргумента?
- 2. Почему Serial.print(0b00001111, BIN) печатает 1111, а не 00001111?
- 3. Кнопки собрали байт knopki = 0b00001010. Что даст выражение knopki & 0b00001111?
- 4. Когда уместна String(x, BIN), а когда второй аргумент Serial.print?
Частые вопросы по теме урока
- Что напечатает Serial.print(0x10); — без второго аргумента?
- 0x10 — литерал в шестнадцатеричной записи, его значение — 16 (0b00010000). По умолчанию печать идёт в DEC, и монитору всё равно, как число было записано в исходнике: печатается значение. 16 — значение литерала в десятичной системе
- Почему Serial.print(0b00001111, BIN) печатает 1111, а не 00001111?
- print идёт от старшего значащего бита; механизма ширины поля, как printf %08d, у Print нет. Видеть все восемь разрядов помогает самодельная побитная печать pad8() из практики. Печатаются только значащие разряды: ширины поля у Print нет, ведущие нули не выводятся
- Кнопки собрали байт knopki = 0b00001010. Что даст выражение knopki & 0b00001111?
- Логическое И с 0b00001111 оставляет младшие четыре бита как есть: 0b00001010 & 0b00001111 = 0b00001010 = 10. Проверяйте себя через Serial.print(knopki & 0b00001111, BIN). 10 — младшие четыре бита прошли без изменений: 0b00001010 и есть десять
- Когда уместна String(x, BIN), а когда второй аргумент Serial.print?
- Каждый String — объект в куче: в loop() на Uno это фрагментация и риск молчаливой смерти скетча, о чём шла речь в уроке про F(). Печати хватает print(x, BIN) — без единой строки в RAM. Для печати в порт — второй аргумент print; String — только когда строку нужно хранить или передавать дальше