I²C · GPIO · Arduino
PCF8574 без магии: как работают квазидвунаправленные входы-выходы и что должна правильно делать Arduino-библиотека
У этой микросхемы нет привычного регистра направления и нет симметричного push-pull HIGH. Поэтому самое важное слово в её API — не «включить», а «отпустить».
PCF8574 часто появляется в мастерской как маленькая плата, обещающая «ещё восемь GPIO всего двумя проводами». Формально — да. Практически — эти GPIO ведут себя так, будто в договоре мелким шрифтом написано: «HIGH предоставляется на особых условиях». Если их не прочитать, кнопка вдруг перестаёт быть входом, байт 0xFF превращается в «ошибку», а невинный старт библиотеки включает все active-low нагрузки.
Содержание статьи
- Один latch-бит вместо INPUT и push-pull HIGH
- Какое состояние должна хранить библиотека
- Адреса и правильная проверка ACK
- Что реально делает jm_PCF8574 v2.0.0
- Нагрузка и конфликт выходов
- LCD-backpack без универсального pinout
- Почему 4,7 кОм не универсально
- Чек-лист правильной библиотеки
- Технические источники
1. Один latch-бит вместо INPUT и push-pull HIGH
У обычного GPIO микроконтроллера мы ожидаем отдельный выбор направления и выходной latch. PCF8574 устроена иначе: каждая P-линия квазидвунаправленная, а доступного программе регистра направления нет. После power-on I/O находятся в HIGH/released состоянии; записанный бит непосредственно определяет, активно ли притягивание LOW.[1][2]
| Latch-бит | Физическое действие PCF8574 | Что может прочитать MCU | Практическая трактовка |
|---|---|---|---|
0 |
Выходной транзистор активно притягивает Pn к LOW. | Физический LOW, пока нагрузка не выведет узел за нормированные условия. | Активный LOW-выход. |
1 |
Сильный LOW-драйвер отключён; остаётся слабый источник тока к питанию. | HIGH без внешнего притягивания или LOW, если внешняя схема притянула Pn. | Released HIGH или вход. |
0 активно тянет вниз, 1 отпускает сильный LOW-драйвер и позволяет внешнему сигналу определить уровень.HIGH здесь не push-pull
Когда в Pn записан 1, PCF8574 не включает обычный сильный high-side выход. Статический HIGH поддерживается слабым внутренним источником тока. NXP описывает его как источник примерно 100 мкА, а TI на функциональной схеме также обозначает 100 мкА, но в таблице электрических характеристик задаёт диапазон тока при конкретных условиях. Поэтому превращать этот узел в «внутренний резистор 100 кОм» не следует: даташит нормирует источник тока, а не постоянное линейное сопротивление.[1][2]
Во время записи HIGH дополнительно включается более сильное кратковременное подтягивание. Оно помогает быстрее зарядить ёмкость линии и поднять фронт до HIGH, после чего отключается по фазе SCL. Это стартовый толчок, а не постоянный push-pull HIGH и точно не ускоритель перехода к LOW.[2]
digitalWrite(Pn, HIGH) для PCF8574 означает прежде всего «прекрати активно притягивать Pn». Вывод становится пригодным как вход, но не превращается в полностью отключённый CMOS-вход.
Рабочие параметры — не то же самое, что Absolute Maximum
PCF8574 значительно сильнее поглощает ток в LOW, чем отдаёт его в HIGH. Но цифры из даташита живут в разных «ящиках». Electrical Characteristics описывают гарантированные или типовые точки при заданных условиях. Recommended Operating Conditions задают область штатной работы. Absolute Maximum Ratings — это стрессовые пределы, превышение которых может повредить микросхему; они не являются рецептом проектирования.[2]
Например, у TI Rev. K 50 мА continuous output-low current приведены именно среди Absolute Maximum Ratings, тогда как рекомендуемый предел IOL достигает 25 мА. И это ещё не обещание «по 25 мА с каждого из восьми выводов»: надо одновременно проверять VOL при нужном токе, суммарный ток корпуса, питание и температуру.[2]
2. Какое состояние должна хранить библиотека
Ключевое правило даташита сформулировано без мистики: перед использованием Pn как входа этот I/O должен быть HIGH. В терминах latch это означает: сначала записать 1, потом читать. Если бит остался 0, микросхема сама удерживает линию LOW, и внешний переключатель не получит права голоса.[1][2]
Отсюда вытекает требование к библиотеке: она не должна смешивать три разные сущности.
output_latch— байт, который библиотека хочет записать в PCF8574;input_sample— байт физических уровней, только что прочитанный с P0–P7;status— результат I²C-транзакции.
Почему read-modify-write может испортить вход
Представим, что P0 отпущен записанной единицей, а кнопка снаружи притянула его к LOW. Чтение справедливо возвращает 0 для P0. Если библиотека сохранит этот sample как «тень выхода», затем изменит только P7 и запишет весь байт назад, P0 получит latch=0. Кнопку уже можно отпустить, но PCF8574 продолжит сама держать P0 в LOW. Это не прихоть I²C — это следствие потери разницы между командой и наблюдением.[1][2]
Эскиз корректной семантики API
Это не код конкретной библиотеки, а контракт, который не противоречит электрике:
bool releaseInput(uint8_t pin); // записывает 1 в latch
bool writePin(uint8_t pin, bool low); // изменяет output_latch
bool readPort(uint8_t& sample); // data отдельно, success отдельно
bool probeAddress(); // address-only ACK probe
// Перед каждой полной записью:
byte safe = output_latch | input_mask;
Название метода может быть другим. Важна не косметика, а гарантия: переход к входу физически записывает 1, а ошибка шины не маскируется под один из 256 вполне законных байтов.
3. Адреса и правильная проверка ACK
PCF8574 и PCF8574A имеют разные диапазоны
Arduino Wire принимает 7-битный адрес устройства. Для PCF8574 три входа A2:A0 дают диапазон 0x20–0x27; для PCF8574A — 0x38–0x3F. Следовательно, 0x27 не означает «A-версия»: это PCF8574 со всеми тремя адресными входами HIGH. Маркировка микросхемы и реальные перемычки платы надёжнее, чем «типовой адрес» из чужого скетча.[1]
| Вариант | A2:A0 = 000 | A2:A0 = 111 | Полный диапазон |
|---|---|---|---|
| PCF8574 | 0x20 |
0x27 |
0x20–0x27 |
| PCF8574A | 0x38 |
0x3F |
0x38–0x3F |
Для практики можно перейти к проверенному в каталоге модулю расширения портов PCF8574. Это внутренняя товарная ссылка, а не доказательство адреса или токовых пределов конкретного экземпляра.
beginTransmission() не отвечает на вопрос «устройство есть?»
Wire.beginTransmission(address) лишь подготавливает локальный TX-буфер и адрес. Официальный Arduino API возвращает nothing, а в AVR core реальная сигнатура — void TwoWire::beginTransmission(uint8_t address). Поэтому конструкция if (Wire.beginTransmission(...)) не проверяет ACK и вообще не является корректным использованием API.[3][5]
Результат передачи возвращает Wire.endTransmission(): код 0 означает успех, 2 — NACK адреса, 3 — NACK данных; другие коды описывают другие ошибки реализации. Для проверки только ответа по адресу не добавляем ни одного data byte.[4][5]
bool i2cAddressAck(TwoWire& bus, uint8_t address)
{
bus.beginTransmission(address);
return bus.endTransmission() == 0;
}
Почему инициализационный 0x00 опасен
Запись 0x00 — не нейтральный ping. Он приказывает всем восьми P-линиям активно перейти в LOW. На неизвестной плате это может включить active-low нагрузки, переключить управляющие сигналы LCD или создать конфликт с внешним драйвером. Если нужен только ACK, TX-буфер должен оставаться пустым.[1][2][5]
На противоположном конце байтовой шкалы 0xFF тоже нельзя резервировать как универсальную «ошибку чтения». Это вполне корректное физическое состояние: все восемь линий прочитаны HIGH. Данные и статус должны идти отдельными каналами.[1][8]
4. Что реально делает jm_PCF8574 v2.0.0
Рассмотрим не воображаемую «правильную библиотеку», а конкретный tagged release. jm_PCF8574 v2.0.0 опубликован 4 июля 2025 года; release описывает переработанный API и выбор I²C bus.[6] Ниже разбираем фактический код этого релиза.
Реальные публичные сигнатуры
В tagged header v2.0.0 объявлены именно такие основные конструкторы и методы:[7]
jm_PCF8574();
jm_PCF8574(byte i2c_address);
jm_PCF8574(byte i2c_address, TwoWire & wire);
operator bool();
byte i2c_address();
bool connected();
bool begin();
bool begin(byte port_mode, byte port_shadow);
bool begin(byte i2c_address);
bool begin(byte i2c_address, byte port_mode, byte port_shadow);
bool end();
byte port_input();
void port_output(byte value);
void port_output(const byte *data, size_t quantity);
void pinMode(byte pin, byte mode);
int digitalRead(byte pin);
void digitalWrite(byte pin, byte value);
Header также содержит wire(), port_mode(), port_shadow(), pin-level helper-ы и wait(). Важнее другое: overload-а begin(TwoWire*) здесь нет.
Произвольный TwoWire передают в конструктор
Выбор Wire, Wire1 или другого объекта, унаследованного/совместимого с TwoWire, реализован ссылкой в конструкторе: jm_PCF8574(byte, TwoWire&). Выбранный объект должен существовать в конкретном Arduino core, а пользователь должен сам инициализировать шину способом, предусмотренным этим core. Библиотека хранит TwoWire& и выполняет транзакции через него.[7][8]
// После инициализации Wire1 по правилам вашего Arduino core:
jm_PCF8574 io(0x20, Wire1);
const bool started = io.begin();
begin() изменяет порт, а не только проверяет адрес
Типовой begin() стартует с режимом all-input/released и тенью 0xFF. Внутренний _begin() формирует байт порта, записывает его устройству и устанавливает успех по endTransmission()==0. Это конфигурационная запись, хотя и более безопасная, чем 0x00; чистым address-only probe он не является. Если _connected уже true, публичный begin(...) возвращает false, а end() только сбрасывает внутренний флаг и не вызывает Wire.end().[8]
connected() и operator bool() — кеш, не live probe
connected() просто возвращает _connected; operator bool() делегирует этому же методу. Следовательно, обе формы сообщают последнее известное библиотеке состояние. Если кабель сняли после успешной операции, флаг не узнает об этом телепатически — нужна следующая I²C-транзакция. И наоборот, повторный begin() не является удобным health check, потому что при _connected=true он возвращает false.[7][8]
Где v2.0.0 теряет грань между latch и sample
При успешном port_input() реализация читает P-порт и записывает полученный байт в _port_shadow. При ошибке или disconnected она возвращает текущий _port_shadow, а не гарантированный 0xFF. Поэтому byte port_input() не имеет отдельного канала ошибки; любой возвращённый байт может быть и допустимым значением состояния.[8]
Ещё острее: pinMode(INPUT) в v2.0.0 изменяет только _port_mode в RAM и не выполняет I²C-записи, которая физически отпустила бы pin единицей. А после чтения внешний LOW уже может оказаться в _port_shadow; следующая бит-операция строит запись на этой тени. Именно так подтверждённая структура source создаёт риск опасного read-modify-write для released input bits.[8]
5. Нагрузка и конфликт выходов
Кнопка: released input, а не «плавающий pin»
Типичная кнопка между Pn и GND работает естественно: библиотека записывает в Pn единицу, слабый источник поддерживает HIGH, нажатие притягивает линию к LOW. Задержка может понадобиться для debounce механических контактов, но универсальная delay(1) не вытекает из даташита и не «лечит floating input» — released вход уже имеет слабый источник тока.[1][2]
Светодиод: active-low часто естественнее
Из-за асимметрии токов светодиод с резистором часто подключают так, чтобы PCF8574 поглощала ток в состоянии LOW. Но номинал резистора, допустимый VOL, суммарный ток и условия даташита всё равно надо рассчитать. Релейную катушку не следует подключать непосредственно: нужны соответствующий транзистор или драйвер, flyback path и проверка тока и тепла.[2]
MCU GPIO и P7 нельзя просто соединить «для PWM»
Аппаратный PWM/LEDC микроконтроллера возникает на GPIO самого MCU, а не внутри PCF8574. Чтобы он повлиял на сеть подсветки, нужен физический путь. Если просто соединить push-pull GPIO MCU с P7 и P7 когда-то получит latch=0, MCU может подать HIGH навстречу активному LOW-драйверу расширителя. Это конфликт выходов с потенциально чрезмерным током, а не способ «передать PWM через I²C». Электрическая структура P-порта не даёт оснований считать такое прямое объединение безопасным.[1][2]
6. LCD-backpack без универсального pinout
PCF8574 знает только P0–P7. Она не знает, что рядом припаян HD44780-compatible дисплей. Соответствие P-битов сигналам RS, R/W, E, D4–D7 и подсветке определяет плата адаптера, а не даташит расширителя.
Конфигурируемый New-LiquidCrystal прямо принимает номера En, Rw, Rs, D4–D7, pin подсветки и его полярность; в репозитории есть пример с одной конкретной раскладкой. Это демонстрирует гибкость software-модели, но не доказывает pinout неизвестной физической платы.[10][11]
R/W: управляемый или зафиксированный в режиме записи
Контроллер HD44780U имеет сигнал R/W, циклы чтения и busy flag.[9] Конкретный backpack может провести R/W к одному из P-битов или сделать его недоступным программе, зафиксировав режим записи. Во втором случае библиотека не читает busy flag и должна выдерживать документированные задержки. Какой вариант перед нами, показывают схема, трассировка или continuity test, а не адрес 0x27.
Подсветка: pin и полярность зависят от платы
Каскад подсветки может управляться отдельным P-битом через транзистор, иметь другую инверсию или вообще не предоставлять программного управления. То, что библиотека просит параметры backlight pin и polarity, — полезное напоминание: «active LOW всегда» здесь не существует.[10]
В каталоге MyProject есть адаптер I²C для LCD1602/LCD2004 и LCD1602 с I²C-модулем. Эти страницы полезны для выбора форм-фактора, но без схемы и точной ревизии не устанавливают P0–P7 map, R/W или полярность подсветки.
Тайминги HD44780 зависят от условий
Один магический набор задержек для всех HD44780-compatible контроллеров — плохая инженерная привычка. В оригинальном HD44780U минимальная ширина E HIGH составляет 450 нс при VCC=2,7–4,5 В и 230 нс при 4,5–5,5 В; минимальный enable cycle — соответственно 1000 нс и 500 нс. Стартовая последовательность и время выполнения команд также зависят от питания и частоты внутреннего генератора; clear display и return home значительно дольше обычных операций.[9]
| Параметр | VCC 2,7–4,5 В | VCC 4,5–5,5 В | Практический вывод |
|---|---|---|---|
| E HIGH, минимум | 450 нс | 230 нс | Не отрывать число от диапазона питания. |
| Enable cycle, минимум | 1000 нс | 500 нс | Каждый nibble требует корректного E-strobe. |
| Завершение команды | Зависит от команды и oscillator | Зависит от команды и oscillator | delayMicroseconds(40) не является универсальным для clear/home. |
7. Почему 4,7 кОм не универсальное I²C-подтягивание
В шине I²C линии SDA и SCL формируют HIGH через pull-up, а устройства тянут их к LOW открытым стоком/коллектором. Значение резистора должно попасть в окно между двумя ограничениями: слишком малое сопротивление создаёт слишком большой sink current и мешает гарантированному LOW; слишком большое вместе с ёмкостью шины делает фронт HIGH запаздывающим.[13]
TI SLVA689 выводит нижнюю границу из VCC, допустимого VOL(max) и sink current, а верхнюю — из разрешённого rise time tr и ёмкости шины Cb.[12] Поэтому 4,7 кОм может быть хорошим значением в конкретной схеме, но не является универсальной константой природы.
Отдельная ловушка модульных макетов — несколько готовых плат с собственными pull-up. Параллельные 4,7 кОм дают меньшее эквивалентное сопротивление, так что считать надо всю шину, её длину, ёмкость, напряжение и выбранный режим.[12] Если нужно введение в ACK, адреса и физики самой шины, пригодится материал «I2C, SPI, UART — что это такое и как работают интерфейсы микросхем».
8. Чек-лист правильной Arduino-библиотеки
- Не выдумывает direction register. API объясняет released input и не обещает обычный push-pull HIGH.
- Физически отпускает вход. Переход к input записывает
1в соответствующий latch-бит. - Разделяет состояния. Output latch, input sample, input mask и I²C status не живут в одном байте.
- Не резервирует
0xFFкак ошибку. Данные возвращаются отдельно от success/error. - Проверяет ACK через
endTransmission().beginTransmission()не используется как bool. - Имеет безопасный probe. Address-only проверка не добавляет
Wire.write(0x00)и честно говорит, что ACK не идентифицирует модель. - Не называет кеш живой проверкой.
connected()или аналог документирует момент обновления состояния. - Принимает нужный
TwoWireреальным API. Пользователь видит, кто и когда инициализирует шину. - Не привязывает LCD к одному pinout. Карта сигналов и полярность подсветки конфигурируются или подтверждаются схемой конкретной платы.
- Документирует стартовый байт. Никакого «безопасного»
0x00без анализа нагрузок. - Различает рабочие и аварийные пределы. Absolute Maximum не рекламируется как штатный ток.
- Не подменяет PWM. Частые I²C-записи не называются hardware PWM, а общая сеть MCU/PCF не допускается без изоляции.
Краткие ответы на типовые вопросы
- Можно ли прочитать LOW, который сама PCF8574 выставила?
- Да. Чтение возвращает физический уровень P-порта. Просто отдельного читаемого регистра latch, который объяснил бы причину LOW, нет.[1][2]
- Достаточно ли записать
0xFFодин раз? - Для всех released inputs — это правильный старт. Но каждая следующая полная запись должна сохранять единицы в input-битах; иначе библиотека снова превратит их в активные LOW.
- Можно ли делать PWM частыми записями по I²C?
- Программное переключение возможно, но это не timer-backed PWM: частота, jitter и загруженность шины зависят от I²C-транзакций и Arduino core. Без измерений конкретной системы не следует обещать отсутствие мерцания.[1][2] Для основ PWM смотрите отдельный материал о ШИМ.
- Где почитать про
VIH,VIL,VOHиVOL? - В статье «Тайны логических уровней» — она объясняет, почему логическое слово HIGH не заменяет электрические условия даташита.
Технические источники
- NXP Semiconductors — “PCF8574; PCF8574A Remote 8-bit I/O expander for I²C-bus with interrupt” — Rev. 5, 27 May 2013.Квазидвунаправленный порт, слабый источник тока, power-on state, чтение/запись и адреса обоих вариантов.
- Texas Instruments — “PCF8574 Remote 8-Bit I/O Expander for I2C Bus” — SCPS068K, Rev. K, September 2024.Функциональная схема P-порта, статическое и transient подтягивание, I/O currents и классы электрических пределов.
- Arduino — “Wire.beginTransmission()” — revision 24.01.2026.Синтаксис, 7-битный адрес и отсутствие return value.
- Arduino — “Wire.endTransmission()” — revision 24.01.2026.Завершение транзакции и коды success/NACK/error.
- Arduino — “ArduinoCore-avr / Wire.cpp” — master.Сигнатура
void beginTransmission, TX-буфер и address-only передача в AVR Wire. - Jean-Marc Paratte — “jm_PCF8574 v2.0.0” release — 4 July 2025, commit 53cfd20.Существование релиза, API refactor и selectable I²C bus.
- Jean-Marc Paratte — “src/jm_PCF8574.h” — v2.0.0, commit 53cfd20.Реальные constructors,
TwoWire&,operator bool,connected, begin/end и port/pin API. - Jean-Marc Paratte — “src/jm_PCF8574.cpp” — v2.0.0, commit 53cfd20.Реальное поведение begin/end,
_connected, port input/output,_port_shadowиpinMode. - Hitachi — “HD44780U (LCD-II) Dot Matrix Liquid Crystal Display Controller/Driver” — доступное зеркало SparkFun.RS/RW/E, busy flag, 4-bit initialization, execution times и зависящие от VCC bus timings.
- Francisco Malpartida — “New-LiquidCrystal / LiquidCrystal_I2C.h” — master.Конфигурируемые En/Rw/Rs/D4–D7, backlight pin и polarity; это software API, не схема конкретной платы.
- Francisco Malpartida / contributor — “HelloWorld_byVac.ino” — repository master.Пример конкретно настроенной LCD map; не идентифицирует все физические ревизии backpack.
- Texas Instruments, Rajan Arora — “I2C Bus Pullup Resistor Calculation” — SLVA689, February 2015.Формулы
Rp(min),Rp(max)и зависимость от тока, rise time и bus capacitance. - NXP Semiconductors — “UM10204 I²C-bus specification and user manual” — Rev. 7.0, 1 October 2021.Open-drain структура шины, pull-up, rise-time и bus-capacitance requirements.
Материал опирается на документацию производителей, официальную Arduino-документацию и tagged source библиотеки.
© 2026 Мій Проект. Автор: Jazzzman. Використання матеріалів дозволено лише з посиланням на джерело.
Написать комментарий