Перейти к содержимому

Добрый день, уважаемый читатель! Проблема нехватки свободных портов ввода-вывода ( GPIO ) на микроконтроллерах знакома, думаю, многим разработчикам устройств. Особенно эта проблема актуальна для весьма популярного микроконтроллера в народе ESP8266 (он же Гога ESP-01, он же Жора NodeMCU, и прочих модификаций на том же чипе) – там свободных GPIO вообще “кот наплакал”. На ESP32 ситуация попроще, но всё равно иногда приходится упираться в аппаратные ограничения.

Но есть довольно простое решение – расширители портов ввода вывода, которые подключаются к основному микроконтроллеру через I2C или SPI шину. Существуют самые разные расширители – как для дискретных (цифровых) GPIO, так и для аналоговых входов. Применение таких микросхем просто и изящно решает проблему с выводами, как на вход, так и на вывод.

Примечание: подробнее про шину I2C можно прочитать тут:

 

В данной статье поговорим о самом простом и популярном представителе семейства – квази-двунаправленном расширителе цифровых портов PCF8574 (8 портов) / PCF8575 (16 портов). Почему “квази”? Потому что не такой он уж и двунаправленный, но об этом чуть ниже…

Главный герой собственной персоной в DIP-корпуск. Источник картинки: AliExpress

Главный герой собственной персоной в DIP-корпусе.

Не обязательно использовать PCF8574 только в “чистом” виде, готовых плат-шилдов на основе PCF8574 изготавливается и продается видимо-невидимо, самых разных форм, расцветок и назначений, в том числе и для подключения LCD-экранов:

Расширитель GPIO PCF8574

 

Давайте вначале разберемся, что это такое и с чем его едят. Открываем даташит, например этот, читаем… (все примерно тоже относится и к PCF8575)


Общие сведения

PCF8574/74A обеспечивает расширение портов ввода/вывода общего назначения с помощью двухпроводной двухнаправленной I2C-шины. Микросхема имеет:

  • восемь квази-двунаправленных портов (16 для PCF8575) с открытым стоком (open drain), при этом ток порта при низком уровне может достигать 10мА, а при высоком – всего 0,1мА (100мкА)
  • интерфейс I2C-bus со стандартной скоростью 100 кГц с возможностью выбора 8 различных адресов с помощью выводов установки адреса
  • выход для генерации прерывания при изменении уровня на входах

Напряжение питания микросхемы может составлять от 2,5 В до 6 В, что позволяет безопасно подключать её как к пятивольтовым, так и к трехвольтовым микроконтроллерам без согласования логических уровней, напрямую. Чип достаточно экономичный, собственный ток потребления около 2.5 мкA.

Микросхема выпускается в трех разных корпусах:

Схема расположения выводов. Источник: datasheet

Схема расположения выводов. Источник: datasheet

  • DIP-16 – с маркировкой P на конце
  • SO16 – с маркировкой Т на конце
  • SSOP20 – с маркировкой ТS на конце или без
Источник: datasheet

Источник: datasheet

Буква A в маркировке обозначает другое пространство адресов на шине:

  • Без буквы A – адреса от 0x20h до 0x27h включительно
  • С буквой A – адреса от 0x38h до 0x3Fh включительно

Три вывода для установки аппаратного адреса на шине позволяет выбрать один из восьми адресов на шине I2C, что позволяет одновременно подключить к одной шине 8 микросхем, что в сумме дает 64 порта для PCF8574 (или 128 портов для PCF8575). А если комбинировать микросхемы разных серий с разными адресами, то можно увеличить количество портов до 128 (или до 256) одновременно! Внушительные цифры.

Источник: datasheet

Источник: datasheet

Расширитель GPIO PCF8574

Каждая микросхема имеет всего один единственный регистр, в котором хранится текущее состояние всех портов, (как входов, так и выходов), поэтому указывать номер регистра при обмене информацией с микросхемой не требуется – просто отправляем адрес и режим – чтение или запись. Все! Предельно просто. Как же тогда изменить режим работы порта? Об этом ниже.

 


Режимы работы портов

Квази-двунаправленный ввод/вывод – это входной или выходной порт без использования регистра управления режимом его работы. Всякий раз, когда микроконтроллер читает регистр расширителя, состояние регистра обновляется и возвращаемое MCU значение зависит от текущего фактического напряжения на всех выводах расширителя.

Каждый из портов микросхемы может быть работать либо в качестве выхода, либо в качестве входа, не зависимо от других. То есть можно например, 3 порта использовать как выходы, остальные – как входы. Или наоборот. Или в любых других сочетаниях. Но микросхема не имеет отдельного регистра (команды) для изменения режима работы порта – как же быть?

Очень просто – как только мы записываем в порт логическую единицу, порт становится входным. И наоборот, когда в регистр для соответствующего порта записан логический “0”, то порт способен работать только на выход, так как по сути является выходом с открытым стоком.

Давайте посмотрим на схему порта, для понимания сути важна только правая часть с выходными транзисторами и источником тока Iон:

Расширитель GPIO PCF8574

В упрощенном виде эту схему можно перерисовать так:

Расширитель GPIO PCF8574

Ключевым фактором здесь является источник тока с ограничением всего в 100мкА, который помечен как Iон (это определяет ток вывода для высокого уровня). Этот источник устанавливает высокий уровень на выходе или входе, когда вывод “свободен” и в регистре записана логическая “1”. Транзистор параллельно ему, помеченный как Itrt(pu), на самом деле открывается только на очень короткое время во время переключения логического уровня (иначе переключение с 0 до 1 могло бы происходить очень долго), и в дальнейшем в работе схемы не участвует, поэтому называется accelerator pull-up.

При записи в регистр логического “0” верхние транзисторы закрываются, а нижний открывается, что подтягивает вывод к “земле”. В этом случае вывод больше не может использоваться как входной, так как если если вы подадите на вывод источник с логической единицей, то просто зашунтируете его (либо спалите выходной транзистор микросхемы).

Режим ввода

При включении все выводы подтянуты к питанию через источник тока 100 мкА (0,1 мА), то есть фактически работают в режиме ввода. Либо микроконтроллер должен записать “1” в регистр, чтобы установить порт в режим ввода, если устройство не находится в состоянии по умолчанию.

  • Входной высокий уровень. Если внешний источник подтягивает вывод порта до Vdd (логическая единица на входе) или вывод висит в воздухе тогда при чтении регистра в него попадет 1.
  • Входной низкий уровень. Если внешний источник подтягивает вывод порта к земле (логический ноль на входе), это шунтирует внутренний слабый источник тока 100 мкA, и в регистр попадет 0. Таким образом внешний источник должен быть способен полностью “просадить” ток 100 мкА (то есть внешний источник сопротивлением 100кОм вряд ли переведет входной порт в состояние 0).

 

Использование прерываний

При изменении физического состояния входа состояние внутреннего регистра может и не соответствовать текущему состоянию входов – состояние регистра обновляется только во время обмена данными с мастером. Но если напряжение на входе отличается от состояния внутреннего регистра, генерируется сигнал прерывания для микроконтроллера.

Вывод прерывания INT может быть подключен к GPIO микроконтроллера для уведомления микроконтроллера о том, что состояние входных портов расширителя изменилось. Получив этот сигнал прерывания, ваш микроконтроллер должен заново считать состояние регистра PCF8574 и самостоятельно определить, на каких входах изменился уровень. Во время обмена данными по шине в регистр будет записано текущее состояние входов и всё придет в соответствие до следующего изменения. Разумеется, для того, чтобы определить на каких выводах произошли изменения после последнего чтения, придется где-то хранить последнее считанное состояние на стороне MCU, как как чтение регистра вернет состояние всех выводов, а не только тех, которые были изменены.

Некоторую сложность при работе с прерываниями вызывает тот факт, что читать шину I2C непосредственно в обработчике прерывания GPIO – очень не желательно. По простой причине – обмен с шиной I2C штука медленная, и такой фокус зачастую вызывает срабатывание сторожевого таймера WDT для прерываний. Как же быть?

На ESP32 и ESP-IDF задача решается достаточно просто. После срабатывания прерывания отправляем уведомление в системную очередь сообщений и создаем обработчик сообщения, который в другом потоке, в нормальном режиме, прочитает регистр микросхемы. Для Arduino придется придумывать какой-то другой способ.

Режим выхода

  • Выходной низкий уровень. Мастер записывает 0 в регистр порта. Нижний по схеме транзистор подтягивает выходную схему к Vss “земле” в режиме “открытый сток“. Классика. Следует только учитывать, что слишком мощная нагрузка, подключенная напрямую к порту (например катушка реле), может сжечь транзистор, не стоит превышать “паспортный” ток 10мА на вывод. Если вам нужен больший ток для управления нагрузкой – используйте несколько выводов, включенных параллельно, таким образом можно получить до 80мА.
  • Выходной высокий уровень. Мастер записывает 1 в регистр порта. К выводу подключается источник тока 100мкА, который и подтягивает нагрузку к Vdd. Но сила тока через вывод при этом не может превысить это ограничение, поэтому если у вас мощная нагрузка – вы не сможете ей управлять “по высокому уровню”, она просто зашунтирует его.

Здесь есть одна проблема – емкость нагрузки. Если емкость нагрузки высока, её зарядка через стабильный источник тока 100мкА будет происходить крайне медленно. Для решения этой проблемки существует дополнительный «ускоритель» Itrt(pu), который включается на очень короткое время, когда мастер устанавливает высокий уровень. Ток акселератора помогает источнику тока 100 мкA порта быстро поднять напряжение на нагруженном выходе, а затем отключается. После этого остается только источник тока 100 мкА, чтобы удерживать высокий уровень на выходе.

 


Выводы

Другими словами, порты микросхемы могут работать “на выход“, но только в режиме “открытый сток”. И могут работать “на вход“, но тоже только подтягиванием вывода порта к земле. Поэтому и называются квази-двунаправленными. То есть по-русски “ну почти” двунаправленными.

Управление нагрузкой

  • Если вы хотите подключить к к выходам PCF8574 светодиоды, то следует выбирать подключения “катодом к выводу, анодом к питанию“:

Расширитель GPIO PCF8574

  • Соответственно, если вы планируете управлять с помощью PCF8574 семисегментным индикатором, то нужно выбирать модели с общим анодом, это позволит подключить сегменты непосредственно к выходам микросхемы через резисторы.

Расширитель GPIO PCF8574

 

  • Если вы планируете подключать к выходам PCF8574 модули реле, то следует выбирать такие модули, которые включают реле низким уровнем на входе, например такие:
Реле с управлением по низкому уровню
Реле с управлением по низкому уровню
Реле с оптронами на входе

Есть и готовые платы с реле на этой микросхеме, я уже писал про одну из них на этом канале. Стоит не очень дорого, а пользоваться весьма удобно.

Готовая плата расширителя GPIO на PCF8574

 

Работа на вход

Допустим вы хотите подключить ко входам обычную кнопку. В этом случае кнопка должна быть подключена по следующей схеме:

Правильное подключение кнопки к PCF

Правильное подключение кнопки к PCF

В принципе, резистор подтяжки даже ставить не обязательно, так как как только вы запишите в регистр порта 1, подтяжка к питанию будет обеспечена встроенным источником тока.

При нажатии на такую кнопку вы замыкаете источник тока на землю, чем устанавливаете уровень 0 на входе. А если в регистре записан “0” и вход уже подтянут к земле? Ничего не произойдет, порт просто настроен на выход…

А что будет, если включить кнопку наоборот, то есть подтягивать вход кнопкой к питанию? Если порт настроен на вход (подтянут к питанию встроенным источником тока), также ничего не произойдёт – вход просто не будет реагировать на нажатие кнопки, так как на нем уже по умолчанию высокий уровень.

Гораздо хуже, если вы случайно настроите вход в режим выхода, записав в регистр порта “0” – в этом случае встроенный транзистор порта вызовет короткое замыкание питания на “землю” через кнопку, и ничем хорошим это не закончится – “сгорит” либо порт, либо источник питания, либо подгорят контакты кнопки. А может и все вместе.

То же самое следует учитывать при подключении и других источников сигнала.

 


Осторожно – помехи на шине!

При разработке устройств с PCF cледует учитывать, что данная микросхема довольно чувствительна к длине проводников шины и различным помехам. Происходит это потому, что она не имеет собственного микроконтроллера, а вся логика работы с шиной I2С реализована на конечном автомате и “простой” логике, которая работает по фронтам и спадам управляющих импульсов на линицяхSCL и SDA. Если паразитная емкость проводников шины I2C слишком большая, то импульсы могут быть сильно искажены, что в рано или поздно приведет к нарушениям в логике работы конечного автомата.

Самое плохое в этой ситуации то, что программного сброса микросхемы в исходное состояние не предусмотрено. Потому при входе в “запрещенное” состояние каждый новый сеанс связи с управляющим микроконтроллером будет начинаться с “неправильного” состояния конечного автомата, что может только усугублять ситуацию. В итоге микросхема становится полностью неуправляемой, причем программный сброс микроконтроллера и перезапуск управляющий программы может и не помочь – может потребоваться кратковременное отключение питания для возврата микросхемы в нормальное состояние.

Поэтому, при разработке устройств с PCF857x (и всех аналогичных по принципу работы) следует избегать подключение к шине I2C датчиков и сенсоров с длинными проводами, по возможности их стоит перенести на другую шину (например на ESP32 их две).


Библиотека для работы с PCF8574

Если вы будете программировать PCF8574 из Arduino, то без особого труда найдете в каталоге готовые библиотеки для работы с ней.

Для ESP-IDF ситуация несколько хуже, но учитывая простоту обмена данными с этой микросхемой, написать свою библиотеку не составит никакого труда. Пример такой библиотеки вы можете скачать с моего GitHub-а:

GitHub – kotyara12/rePCF8574

Как с ней работать? Для работы с реле – очень просто:

Шаг 1. Объявляем переменную – экземпляр класса rePCF8574

static rePCF8574 ioexRelays1(0, 0x21, nullptr);

Где 0 – это номер шины I2C, а 0x21 – адрес расширителя на шине. Третьим параметром можно передать указатель на функцию обратного вызова, которая будет вызвана при изменении уровня на входах.

Шаг 2. Записываем начальное состояние в микросхему:

ioexRelays1.set(0x00);

Записав 0x00, мы устанавливаем низкий уровень на всех выходах микросхемы. Моя плата работает таким образом, что при низком уровне на выходах реле выключены. Если ваши реле такой командой включаются, можно записать 0xFF.

Шаг 3. Включаем или выключаем отдельное реле

Для этого достаточно вызвать команду:

ioexRelays1.write(pin, 1) для установки высокого уровня на выходе pin или ioexRelays1.write(pin, 0) для установки низкого уровня на отдельно выбранном выходе pin.

А как быть с чтением в режиме входов?

Для этого используйте метод update() и функцию обратного вызова. Любое обращение к микросхеме сохраняет копию состояния регистра микросхемы во внутреннем буфере. Поэтому, прочитав в очередной раз состояние регистра микросхемы, мы можем легко определить, состояние каких выводов было изменено:

uint8_t rePCF8574::update(bool reset)
{
  // Read new data from i2c ( reset INT!!! )
  uint8_t new_data = 0;
  if (!read8(&new_data)) {
    return 0;
  };
  
  // Compare previous and new data
  uint8_t changes = _data ^ new_data;
  // rlog_d(logTAG, "Update: prev data: 0x%02X, new data: 0x%02X, changed: 0x%02X", _data, new_data, changes);
  if (reset) {
    write8(_data);
  } else {
    _data = new_data;
  };

  if (changes != 0) {
    // Prepare data for events
    gpio_data_t data;
    data.bus = (uint8_t)_numI2C + 1;
    data.address = _addrI2C;

    // Send events only for pins that have changed
    for (uint8_t i = 0; i < 8; i++) {
      if ((changes & (1 << i)) > 0) {
        data.pin = i;
        data.value = (uint8_t)((new_data & (1 << i)) > 0);
        eventLoopPost(RE_GPIO_EVENTS, RE_GPIO_CHANGE, &data, sizeof(data), portMAX_DELAY);
        if (_callback) {
          _callback((void*)this, data, 0);
        };
      };
    };
  };

  return changes;
}

Мой код отправляет уведомление об изменении состояния GPIO в системный цикл сообщений, но вы можете удалить это в своей версии кода, оставив только вызов callback-а.

 

Методы класса rePCF8574

Библиотечка предельно проста и содержит минимум публичных методов

1. Создание экземпляра класса и его инициализация

rePCF8574(i2c_port_t numI2C, uint8_t addrI2C, cb_gpio_change_t callback);

Здесь нужно указать номер шины, адрес микросхемы и callback при необходимости.

2. Чтение и запись всего регистра сразу

uint8_t get(bool read_i2c = true);
bool set(uint8_t data);

Если в get передать read_i2c = false, то реального чтения данных по шине не произойдет, функция вернет текущее состояние внутреннего буфера.

3. Чтение и запись отдельных GPIO

uint8_t read(uint8_t pin, bool read_i2c = true);
bool write(uint8_t pin, const uint8_t value);

value может принимать только два значения – 0 или 1

4. Обновление состояния при работе в режиме входа

uint8_t update();

Этот метод я описывал выше.

На этом пока всё, в следующий раз постараюсь рассказать, как работать с более продвинутой версией расширителя портов на микросхеме MCP23017

 


Ссылки

  1. PCF8574 datasheet
  2. Библиотека rePCF8574 для ESP-IDF

 

Если вам понравилась статья и вы желаете отблагодарить автора – просто кликните по любому рекламному объявлению.

Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 1 оценок ]

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *