Камера таймлапса на балконе отняла у меня два вечера из-за собственной самоуверенности. Экран и часы занимали A4 и A5, понадобился ещё один датчик — и я пересадил его на D7 и D8 с мыслью «ну это же те же два провода». Тишина. Сканер шины видел только старых жильцов. Причина простая и неприятная: Wire держит шину не кодом, а железом — TWI-периферия физически пришита к конкретным ногам, и никакой pinMode её оттуда не утащит. Пересаживать датчик было некуда, а вторую шину хотелось.
Второй сюрприз ждал меня раньше первого: сканер шины на пересаженном датчике молчал не потому, что датчик плох, а потому, что сканер читал аппаратную шину A4/A5 — на D7 и D8 он смотреть не умел в принципе. Железная периферия одна, и все библиотеки поверх Wire разговаривают только с ней. Это полезный урок вообще: если устройство молчит на «перенесённых» пинах, первым делом проверяйте, а существует ли вообще шина на этих пинах.
Выход — нарисовать шину самому: бит-бэнг, он же SoftI2C. Любые два пина, пара функций, и каждый такт выставляет ваш код. В этом уроке разберём механику: открытый сток на digitalWrite и INPUT, обязательные подтяжки, полубит в пять микросекунд, условия START и STOP и девятый такт под ACK. Соберём минимальный передатчик, посмотрим на трассу в замедленной съёмке и честно сравним с аппаратной шиной — где софт выигрывает, а где платит нагрузкой на процессор.
Шину рисуют фронты, а не регистры
Всё, что видит слейв, — это уровни на двух проводах и их порядок, а железо TWI лишь аккуратно эти уровни выдаёт. Значит, справится и код. Первое правило — открытый сток: линия прижимается к земле парой pinMode(OUTPUT) и digitalWrite(LOW), а «отпускается» переводом пина в INPUT — пин перестаёт давить, и уровень поднимает резистор. Дёргать линию HIGH напрямую выходом нельзя: два устройства, толкающие в разные стороны, греют выходные каскады сквозным током; протокол на этом стоит, о чем я рассказывал в уроке про устройство шины. Второе правило — подтяжки обязательны: отпущенная линия без резистора 4,7 кОм висит в воздухе и читает погоду; раскладка номиналов была в уроке про подключение и подтяжки.
Теперь тайминг. Такт 100 кГц — это десять микросекунд, по пять на полутакт; байт стоит девять тактов — восемь бит плюс подтверждение, около девяноста микросекунд. START — SDA падает, пока SCL высокий; STOP — SDA растёт, пока SCL высокий; между ними по восемь тактов на байт, и в девятом такте мастер отпускает SDA и слушает: прижатая слейвом линия — ACK, согласие принять байт. Это весь протокол; дальше только аккуратность.
Три правила порядка, на которых я сам спотыкался. Первое: тактирует всегда мастер — SCL дёргает только он, слейв не имеет права двигать часы; SDA меняется только при низком SCL, иначе слейв прочитает START или STOP там, где их не было. Второе: биты уходят от старшего к младшему, и первый байт — это семь бит адреса плюс бит направления: адрес 0x3C в режиме записи превращается в 0x78, потому что к семи битам приписан слева ноль. Третье: чтение — те же полубиты наоборот, мастер отпускает SDA на восемь тактов и сам решает судьбу девятого: ACK для «читай дальше», NACK для «хватит».
| Параметр | Аппаратный Wire | SoftI2C (бит-бэнг) |
|---|---|---|
| Пины | только A4/A5 или выделенные ноги платы | любые цифровые: хоть D7 и D8 |
| Скорость | 100-400 кГц держит стабильно | на Uno реалистично 50-100 кГц; на ESP32 бит-бэнг выжимал и больше |
| Нагрузка CPU | байты складываются в буфер, шину ведёт железо | все полубиты выставляет код: на время передачи процессор занят целиком |
| Надёжность таймингов | периферия держит такты сама | длинное прерывание растягивает такт: Servo.h рядом — уже риск |
Писать полубиты руками ради production — редко нужная экзотика: в менеджере библиотек живут готовые решения SoftWire и SoftwareWire, чьи вызовы повторяют привычный Wire — begin, beginTransmission, requestFrom, — а внутри прячутся те же полубиты и открытый сток. Пины задаются в конструкторе. Вызовы повторяют привычный Wire — begin, beginTransmission, requestFrom из урока про анатомию библиотеки Wire — пины задаются в конструкторе, а паузу полубита можно подстроить под линию: длинной шине дам помедленнее, короткой — на максимум. Я беру софт-шину в трёх случаях: нужна вторая независимая шина (два одинаковых экрана 0x3C по разным мирам — альтернатива мультиплексору из урока про TCA9548A); линия длинная и медленная, где сотня килогерц не нужна, а ёмкость проводов душит фронты — тема разбиралась в уроке про длину проводов; и пины A4/A5 просто заняты. Граница та же, что с мультиплексором: свободны A4/A5 — беру аппаратный Wire, он дешевле по процессору и не боится прерываний.
Минимальный бит-бэнг своими руками
#include <Wire.h> // не обязателен: шину рисуем сами // Мини-SoftI2C: пины D4 (SDA) и D5 (SCL), полубит 5 мкс -> около 100 кГц.// linHi переводит пин в INPUT и включает подтяжку AVR — так «отпускают» линию. const byte SDA = 4;const byte SCL = 5; void linLo(byte pin) { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); }void linHi(byte pin) { pinMode(pin, INPUT); digitalWrite(pin, HIGH); } void i2cStart() { linHi(SCL); linHi(SDA); delayMicroseconds(5); linLo(SDA); // START: SDA падает при высоком SCL delayMicroseconds(5); linLo(SCL); // шина «занята»: SCL прижат} void i2cStop() { linLo(SDA); delayMicroseconds(5); linHi(SCL); // SCL высок delayMicroseconds(5); linHi(SDA); // STOP: SDA растёт при высоком SCL} bool i2cWriteByte(byte b) { bool ack = false; for (int i = 7; i >= 0; i--) { if ((b >> i) & 1) linHi(SDA); else linLo(SDA); delayMicroseconds(2); linHi(SCL); // слейв читает бит на высоком полутакте delayMicroseconds(5); linLo(SCL); delayMicroseconds(2); } linHi(SDA); // девятый такт: отпускаем и слушаем delayMicroseconds(2); linHi(SCL); delayMicroseconds(5); ack = (digitalRead(SDA) == LOW); // слейв прижал линию — согласен linLo(SCL); linHi(SDA); return ack;} void setup() { linHi(SDA); linHi(SCL); // обе линии отпущены — шина свободна Serial.begin(9600);} void loop() { i2cStart(); bool ack = i2cWriteByte(0x78); // адрес 0x3C в режиме записи: 0x3C << 1 i2cStop(); Serial.print("ack="); Serial.println(ack); delay(2000);}Про честность симуляции скажу прямо: движок не тактирует линии в реальном времени, поэтому полубит в пять микросекунд он показать не может — отсюда замедленная съёмка, где паузу полубита задаёт ползунок. Слейва на шине тоже нет, и девятый такт изображает сам скетч — я отмечаю это в трассе отдельной строкой. Зато последовательность фронтов подлинная: свободная шина, START, биты от старшего к младшему, ACK, STOP — тот же порядок, который покажет логический анализатор за пару сотен рублей, подключённый к настоящим D4 и D5. Трасса заодно подсвечивает главное правило протокола: в каждой строке SDA меняется только при прижатом SCL, а единственные исключения — START и STOP, ради которых такт и разрывается.
Цифры для живой платы прилагаются бесплатно. Тысяча байтов при 100 кГц — около девяноста миллисекунд чистой шины, и всё это время процессор занят выставлением полубитов: аппаратный Wire за тот же кадр отдал бы шину периферии и спокойно считал millis(). При полубите в пять микросекунд на один полутакт у ядра 16 МГц приходится всего восемьдесят тактов — отсюда и потолок софт-шины на Uno, до четырёхсот килогерц кодом не дотянуться без держащихся на честном слове задержек; потолки аппаратной скорости разбирались в уроке про частоту шины.
Эксперименты для закрепления
Первое: поднимите скорость — уменьшите полубит до трёх микросекунд в скетче для платы и проверьте на коротких проводах, что слейв всё ещё отвечает ACK; на длинной линии та же скорость потребует более сильной подтяжки. Второе: допишите приём байта — мастер отпускает SDA на все восемь тактов, а в девятом сам прижимает линию как ACK; на чтении стороны меняются местами. Третье: заведите вторую шину на D6 и D7 и посадите на каждую по одинаковому экрану 0x3C — два независимых мира без мультиплексора; переключение шин теперь одна строка, но и следить придётся за двумя подтяжками.
Четвёртое: измерьте в симуляторе время передачи при паузе 300 мс и пересчитайте его в байты за секунду — потом сравните с расчётом для настоящих 100 кГц, разница вас позабавит примерно в тридцать тысяч раз. Пятое, если полубиты наскучили: поставьте библиотеку SoftwareWire, подставьте D4 и D5 в конструктор и убедитесь, что знакомые вызовы Wire работают на тех же проводах — ручной бит-бэнг останется вам как учебник, а библиотека — как рабочий инструмент. Шестое, контрольное: прервите передачу посреди байта долгим обработчиком кнопки на пине 2 и посмотрите, как растянется такт в трассе — вот наглядная цена соседства софт-шины и прерываний.
- Открытый сток: LOW — pinMode OUTPUT плюс digitalWrite LOW, отпускание — INPUT; подтяжки 4,7 кОм обязательны
- Полубит 5 мкс — такт 10 мкс — около 100 кГц; байт стоит девять тактов, около 90 мкс
- START — SDA падает при высоком SCL, STOP — растёт; девятый такт отдан под ACK слейва
- SoftWire и SoftwareWire дают привычный API Wire на любых пинах — ручной бит-бэнг для понимания, библиотека для работы
- Ниша SoftI2C: вторая шина, занятые A4/A5, медленные длинные линии; цена — процессор, занятый на всю передачу
- 1. Как «отпустить» линию в открытом стоке?
- 2. Полубит длится 5 микросекунд. Какая частота шины?
- 3. Условие START — это...
- 4. Почему аппаратный Wire надёжнее на 400 кГц, чем самописный бит-бэнг?
- 5. Что покажет панель I2C симулятора при софт-шине на D4/D5?
Частые вопросы по теме урока
- Как «отпустить» линию в открытом стоке?
- Прижимаем OUTPUT плюс LOW, отпускаем INPUT. HIGH толкающим выходом выдавать нельзя — открытый сток на этом стоит. pinMode(pin, INPUT) — пин перестаёт давить, уровень даёт подтяжка
- Полубит длится 5 микросекунд. Какая частота шины?
- Такт складывается из двух полубитов: 5 плюс 5 мкс = 10 мкс, то есть сто тысяч тактов в секунду. около 100 кГц: такт — 10 мкс
- Условие START — это...
- Старт — падение SDA, пока SCL высокий; стоп — рост SDA при высоком SCL. Всё остальное происходит при низком SCL. SDA падает при высоком SCL
- Почему аппаратный Wire надёжнее на 400 кГц, чем самописный бит-бэнг?
- Железо генерирует фронты независимо от кода. В бит-бэнге любое прерывание растягивает такт, и привередливый слейв ошибётся. периферия TWI держит такты сама, а софту мешают прерывания
- Что покажет панель I2C симулятора при софт-шине на D4/D5?
- Панель рисует только вызовы Wire на аппаратной шине. Софт-шина выставляет уровни пинами — движок видит лишь digitalWrite. ничего: софт-шина не на A4/A5 и не состоит из вызовов Wire