Добрый день, уважаемые читатели! В данной статье я хотел бы обсудить с вами, как же все-таки правильнее всего работать с расширителями портов ввода-вывода (IO expanders) в многопоточной среде FreeRTOS в общем и в ESP-IDF в частности. Именно обсудить - а вдруг вы мне предложите лучшее решение, чем придумал я. Поэтому надеюсь на ваши комментарии...
Поводом к написанию данной статьи послужила небольшое сообщение об ошибке "Failed to post event: ESP_ERR_TIMEOUT", проскользнувшее в логах во время разработки одного из устройств, где использовался расширитель портов PCF8574 (или MCP23017, не важно).
Это означает, что цикл событий заполнен полностью и задача, которая пыталась добавить еще одно событие в цикл, не смогла этого сделать за отведенное время (в моем случае это было 1000 мс). Это не катастрофа, но свидетельствует о том, что программа имеет системные проблемы.
Если расширитель (любой) используется только "на выход", то сложностей с ним никаких - запись в шину I2C обычно осуществляется из контекста основной "прикладной" задачи и не требует каких-либо мер для дополнительной синхронизации.
Все становится интересней, когда расширитель GPIO (любой) требуется задействовать не только "на выход", но и на "вход", причем с использованием выхода генерации прерываний, например как на схеме ниже.
Как я обычно обрабатываю прерывания по "родным" GPIO?
Так как обработчик прерываний должен выполняться как можно меньше по времени, то я генерирую событие типа REVT_GPIO и "кидаю" его в общий цикл событий.
В данных события содержится вся нужная информация о том, какой именно вывод сгенерировал событий. Прикладная задача подписана на этот тип событий и обработчик выполняет нужный мне код. Например так:
Точно так же до последнего времени, не особо задумываясь, я поступал и с входом прерываний. По сигналу прерывания → генерируется событие → читаются данные с расширителя портов.
Но тут проблема в том, что обработчик события будет вызван не из контекста основной прикладной задачи, а из контекста задачи - цикла событий. А прикладная задача в этот самый момент сама может писать в расширитель портов и ничего хорошего из этого не выйдет. Поэтому вызов ioExp.update(), да и все остальные обращения к ioExp приходится "оборачивать" защитным семафором:
Библиотека расширителя портов, в свою очередь, из метода update() генерирует свои собственные события того же типа, и их можно обработать в то же обработчике.
Точно так же защищены обращения к шине I2C, но другим семафором, так как там тоже возможны обращения к ней из разных задач.
Этот подход в принципе более-менее успешно работал несколько лет. То есть это вполне себе рабочий подход, но не без недостатков.
Что здесь не так?
Здесь есть достаточно большая потенциальная проблема. Когда прикладная задача обращается к расширителю (или даже просто к шине I2C) она блокирует его вызовом ioExpTake() (а внутри в конечном итоге есть ещё один семафор для доступа к шине). И если в данный момент произойдет изменение уровня на входе, то произойдет следующее:
- будет сгенерировано прерывание (низкий сигнал на выходе INT PCF и на входе GPIO19 для примера выше)
- обработчик прерывания создаст событие REVT_GPIO::bus_0::gpio_19 и отправит его в цикл событий
- обработчик события начнет его выполнение и ...., если в этот момент расширитель или шина были заблокированы другим потоком, упрется в ioExpTake() или аналогичный семафор шины!
На этом абсолютно все события в системе перестанут выполняться, пока шина и расширитель не будут освобождены и обработчик не завершиться.
При этом события в цикл поначалу добавляются как положено, но спустя некоторое время очередь цикла заполняется полностью и все "встает колом". Попа!!!
До последнего времени меня спасало то, что ioExp.update() выполняется очень мало по времени, и заметных блокировок не возникало. И всё более-менее стабильно работало. Все изменилось после добавления в устройство экрана LCD2004 - эта хреновина обращается к шине много и надолго (так как каждый символ отдельно и на каждый требуется 2 байта, а с русскими символами вообще беда), и в моменты вывода текста стали возникать "затыки".
Что же делать, как же быть?
Я пока не придумал ничего лучше, чем создать для расширителей портов отдельную задачу. Идея такова:
- Создаем спецзадачу, которая полностью будет отвечать за работу с расширителем портов в режиме "на вход", да и "на выход" тоже.
- Для записи в расширитель "на выход" предусмотрим очередь задачи, в которую будем кидать необходимые данные.
- Далее обработчик прерывания на входе может выполнить два варианта кода: либо установить бит в группе событий "прочитай расширитель" или кинуть событие "прочитай расширитель" в очередь задачи. Что по вашему мнению лучше?
- Получив такой сигнал, спецзадача выходит из спячки, обращается к расширителю (возможно с ожиданием разблокировки шины), обменивается с ним данными и генерирует новое событие REVT_GPIO в общий цикл событий. После чего вновь уходит в спячку и не тратит ресурсы процессора.
Таким образом мы исключаем прерывания по входу из расширителя из общего цикла событий и полностью исключаем блокировки общего цикла событий по ожиданию. А если вдруг будет заблокирована спецзадача расширителя - то это её проблемы и на всю систему не влияет.
Разумеется, за это придется заплатить лишним расходом памяти и некоторым усложнением кода.
Код ещё не написан, так как это пока "размышления на тему" в попытках найти оптимальное решение.
Если вы знаете лучший способ - добро пожаловать в комментарии! Интересно было бы найти более простое и элегантное решение.
Если вам интересно продолжение, что из этого вышло - так же прошу в комментарии. Если тема окажется не интересной (так как это в принципе частное решение проблемы) - не буду тратить на неё время.
_______________
На этом пока всё, до встречи на сайте и на dzen-канале!
👍 Понравилась статья? Поддержите канал лайком или комментарием! Каналы на Дзене "живут" только за счет ваших лайков.
📌Подпишитесь на канал и вы всегда будете в курсе новых статей.