Найти в Дзене
K12 :: О ESP32 и не только

Рассуждения на тему работы c IO expanders в FreeRTOS

Добрый день, уважаемые читатели! В данной статье я хотел бы обсудить с вами, как же все-таки правильнее всего работать с расширителями портов ввода-вывода (IO expanders) в многопоточной среде FreeRTOS в общем и в ESP-IDF в частности. Именно обсудить - а вдруг вы мне предложите лучшее решение, чем придумал я. Поэтому надеюсь на ваши комментарии...

Поводом к написанию данной статьи послужила небольшое сообщение об ошибке "Failed to post event: ESP_ERR_TIMEOUT", проскользнувшее в логах во время разработки одного из устройств, где использовался расширитель портов PCF8574 (или MCP23017, не важно).

Это означает, что цикл событий заполнен полностью и задача, которая пыталась добавить еще одно событие в цикл, не смогла этого сделать за отведенное время (в моем случае это было 1000 мс). Это не катастрофа, но свидетельствует о том, что программа имеет системные проблемы.

Если расширитель (любой) используется только "на выход", то сложностей с ним никаких - запись в шину I2C обычно осуществляется из контекста основной "прикладной" задачи и не требует каких-либо мер для дополнительной синхронизации.

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

Пример минимальной схемы подключения PCF8574 с использованием выхода прерываний
Пример минимальной схемы подключения PCF8574 с использованием выхода прерываний

Как я обычно обрабатываю прерывания по "родным" GPIO?

Так как обработчик прерываний должен выполняться как можно меньше по времени, то я генерирую событие типа REVT_GPIO и "кидаю" его в общий цикл событий.

В данных события содержится вся нужная информация о том, какой именно вывод сгенерировал событий. Прикладная задача подписана на этот тип событий и обработчик выполняет нужный мне код. Например так:

На самом деле тут и выделенный блок не совсем корректен...
На самом деле тут и выделенный блок не совсем корректен...

Точно так же до последнего времени, не особо задумываясь, я поступал и с входом прерываний. По сигналу прерывания → генерируется событие → читаются данные с расширителя портов.

-3

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

-4

Библиотека расширителя портов, в свою очередь, из метода update() генерирует свои собственные события того же типа, и их можно обработать в то же обработчике.

Точно так же защищены обращения к шине I2C, но другим семафором, так как там тоже возможны обращения к ней из разных задач.

Этот подход в принципе более-менее успешно работал несколько лет. То есть это вполне себе рабочий подход, но не без недостатков.

Что здесь не так?

Здесь есть достаточно большая потенциальная проблема. Когда прикладная задача обращается к расширителю (или даже просто к шине I2C) она блокирует его вызовом ioExpTake() (а внутри в конечном итоге есть ещё один семафор для доступа к шине). И если в данный момент произойдет изменение уровня на входе, то произойдет следующее:

  • будет сгенерировано прерывание (низкий сигнал на выходе INT PCF и на входе GPIO19 для примера выше)
  • обработчик прерывания создаст событие REVT_GPIO::bus_0::gpio_19 и отправит его в цикл событий
  • обработчик события начнет его выполнение и ...., если в этот момент расширитель или шина были заблокированы другим потоком, упрется в ioExpTake() или аналогичный семафор шины!

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

При этом события в цикл поначалу добавляются как положено, но спустя некоторое время очередь цикла заполняется полностью и все "встает колом". Попа!!!

До последнего времени меня спасало то, что ioExp.update() выполняется очень мало по времени, и заметных блокировок не возникало. И всё более-менее стабильно работало. Все изменилось после добавления в устройство экрана LCD2004 - эта хреновина обращается к шине много и надолго (так как каждый символ отдельно и на каждый требуется 2 байта, а с русскими символами вообще беда), и в моменты вывода текста стали возникать "затыки".

Что же делать, как же быть?

Я пока не придумал ничего лучше, чем создать для расширителей портов отдельную задачу. Идея такова:

  • Создаем спецзадачу, которая полностью будет отвечать за работу с расширителем портов в режиме "на вход", да и "на выход" тоже.
  • Для записи в расширитель "на выход" предусмотрим очередь задачи, в которую будем кидать необходимые данные.
  • Далее обработчик прерывания на входе может выполнить два варианта кода: либо установить бит в группе событий "прочитай расширитель" или кинуть событие "прочитай расширитель" в очередь задачи. Что по вашему мнению лучше?
  • Получив такой сигнал, спецзадача выходит из спячки, обращается к расширителю (возможно с ожиданием разблокировки шины), обменивается с ним данными и генерирует новое событие REVT_GPIO в общий цикл событий. После чего вновь уходит в спячку и не тратит ресурсы процессора.

Таким образом мы исключаем прерывания по входу из расширителя из общего цикла событий и полностью исключаем блокировки общего цикла событий по ожиданию. А если вдруг будет заблокирована спецзадача расширителя - то это её проблемы и на всю систему не влияет.

Разумеется, за это придется заплатить лишним расходом памяти и некоторым усложнением кода.

Код ещё не написан, так как это пока "размышления на тему" в попытках найти оптимальное решение.

Если вы знаете лучший способ - добро пожаловать в комментарии! Интересно было бы найти более простое и элегантное решение.

Если вам интересно продолжение, что из этого вышло - так же прошу в комментарии. Если тема окажется не интересной (так как это в принципе частное решение проблемы) - не буду тратить на неё время.

_______________

На этом пока всё, до встречи на сайте и на dzen-канале!

👍 Понравилась статья? Поддержите канал лайком или комментарием! Каналы на Дзене "живут" только за счет ваших лайков.

📌Подпишитесь на канал и вы всегда будете в курсе новых статей.

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