Как увеличить количество пинов на esp32?

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Картинка BRGFX, Freepik

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

Решил я это всё своеобразным образом, который и описан ниже. Возможно, кому-то ещё будет интересно и полезно…

Дело в том, что этот проект я строю на базе esp32 ввиду того, что данные микроконтроллеры у меня валяются по дому коробками — и это не фигура речи, а буквально так.

Однако проект требует достаточно большого количества подключаемых устройств — восемь цифровых датчиков Холла (KY-003), один аналоговый датчик расстояния (тоже Холла, KY-035), три шаговых двигателя Nema 17, один коллекторный двигатель.

Дело с двигателями осложняется также тем, что каждый из шаговых двигателей требует по три контакта (pulse, direction, enable), так как мне пришлось вынужденно использовать драйвер ТВ6600. Хотел изначально использовать также имеющиеся в наличии L298N, однако они не потянули шаговые двигатели Nema 17 — работали на пределе и раскалялись буквально за 6 секунд работы с последующим температурным отказом.

Да, многие хают ТВ6600, и советуют выбирать более профессиональные версии, тем не менее его цена в моём случае решает: 500 руб. против 2500 руб. Да и задача у меня не такая уж сложная и не требующая круглосуточной работы, наподобие фрезерования 3D-картин, так что мне сойдёт…

Итак, у нас проблема — не хватает пинов (весьма наглядное описание пинов есть здесь). Как её решать? Наверное, можно поискать какую-либо плату расширения, допускающую подключение большого количества периферии — например, у меня с незапамятных времён лежит такая для Arduino:


Но что-то не захотелось мне с этим возиться и разбираться, и пришла в голову мысль сделать всё гораздо более простым способом: с помощью соединения микроконтроллеров через шину I2C.

Природная лень заставила меня поискать готовые решения, однако здесь пришлось натолкнуться на практически полное их отсутствие (связь двух и более esp32 между собой), таким образом, «беглый гуглинг» практически ничего не дал (что, однако, не исключает их наличия где-либо — это говорит только о том, что я их не нашёл). Ну ок, начинаем вникать…

Шина I2C требует для реализации связи всего двух проводов: SCL (Serial Clock) — сигнал тактирования и SDA (Serial Data) — передаваемые данные (но, вообще говоря, в реальности больше, так как требуется ещё как минимум заземление, а ещё желательно и питание).

Эта шина относительно низкоскоростная (стандартно — 100 кбит/сек, на современных устройствах — до 400 кбит/сек), тем не менее она полностью удовлетворяет моим требованиям, так как рамки моего проекта не требуют передачи каких-то тяжёлых данных.

Работа в рамках I2C подразумевает, что любое подключённое к этим линиям (SCL, SDA) устройство может занимать одну из двух позиций: ведущую (master) или ведомую (slave). Обычно, говоря об этой шине, подразумевают работу в ней одного master-a и ряда slave-ов, хотя не запрещается и работа нескольких master-ов (насколько я понял, реализовано это через использующуюся master-ом функцию endTransmission(), которая, если вызывается с параметром true (endTransmission(true)), после передачи сообщения посылает стоп-сообщение, освобождая линию I2C, и она по сути свободна для использования каким угодно ещё master-ом).

Общение между устройствами в рамках этой шины всегда инициирует master, вызывая соответствующее ведомое устройство, обращаясь по его адресу, который является семибитным, и в сети одновременно могут присутствовать 128 устройств с адресами, обозначенными числами от 0 до 127, где 0 согласно спецификации — адрес общего вызова, а ведомым можно назначать адреса от 1 до 127 (это последнее уже не по спецификации, а «по факту», если использовать библиотеку wire). Хотя на самом деле всё несколько сложнее, и если строго следовать спецификации (ссылка на которую выше), то это оставляет нам только 112 адресов:


Но если вы сами проектируете систему и вообще «правила не для вас», то 127. И, вроде как при сильном желании даже 128 :-) Но тут сделаю ремарку: используя библиотеку wire, я пытался обращаться на нулевой адрес. С «нулевым» же результатом. Видимо, если только самому библиотеку писать, так как люди вроде как используют и его… Но тут «дальше мои полномочия всё»… :-0)

И кстати говоря, почему стандарт такой странный — семибитный? А вот почему, всё оттуда же, из спецификации:


А почему 127 устройств? Для этого нам всего лишь нужно открыть стандартный калькулятор Windows и переключить его в режим «программист»:


Затем мы переключимся в десятичный режим (показано ниже стрелкой слева) и забьём число 127. После чего мы увидим в битовом выражении наверху справа, что число 127 для компьютера кодируется семью знаками:


Теперь, если мы попробуем стереть это число и забить туда 128, то увидим наверху справа, что в битовом выражении это число кодируется уже восемью знаками. А наша шина — семибитовая (т. е. можно сказать семизнаковая), и этот адрес просто не поместится в рамках её стандарта (тут следует сделать оговорку — возможно, это не шина семибитовая, а библиотека wire, но я глубже не копал, честно признаюсь):


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

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

Делается это крайне просто:

  1. Запускаете калькулятор Windows.
  2. Переключаетесь в десятичный режим (Dec).
  3. Вбиваете любое число от 1 до 127.
  4. Переключаетесь в шестнадцатеричный режим (Hex).
  5. Копируете получившееся число из окошка наверху и вставляете с припиской «0х...» в свой код (например, 0х7F).


И это всё! Теперь, когда у вашего ведомого устройства есть адрес, к нему легко может обратиться ведущее…

Кстати говоря, это (программное назначение адреса) касается только моего случая, так как, насколько мне известно, имеющиеся в продаже устройства (I2C ЖК-экраны и т. д.) уже содержат жёстко прошитые I2C-адреса (у некоторых их можно менять перемычкой).

Что ещё нам нужно знать о шине I2C? Один из самых важных моментов состоит в том, что она требует для своей работы подтягивающие к питанию (pull-up) резисторы:

Картинка: randomnerdtutorials.com

Об этом даже упоминает и производитель esp32:


Хорошая статья на тему, что такое подтягивающие резисторы, и зачем они нужны для esp32, есть вот здесь.

Тем не менее, насколько я знаю, ряд пинов esp32 уже содержит подтягивающие резисторы. Обратимся к документации производителя esp32:


И мы увидим, что если явным образом программно не определено, пины, имеющие подтягивающие резисторы (45 кОм при подтягивании к питанию, и такой же на 45 кОм при подтягивании к земле — это я уже в даташите глянул), находятся в высокоимпедансном состоянии. Разбираемся, что это за состояние… Вот что нам говорит Вики по этому поводу:

Высокоимпедансное состояние, высокоомное состояние, Z-состояние или состояние «Выключено» — состояние выхода цифровой микросхемы, при котором он «отключается» от сигнальной шины, к которой подключены несколько передатчиков сигналов. Таким образом, сопротивление между её внутренней схемой, формирующей выходной сигнал, и внешней схемой, очень большое. Вывод микросхемы, переведённый в состояние «Выключено», ведёт себя как не подключённый к ней. Внешние устройства (микросхемы), подключённые к этому выводу, могут изменять напряжение на нём по своему усмотрению (в некоторых рамках), не влияя на работу микросхемы. И наоборот — схема не мешает внешним устройствам менять напряжение на выводе микросхемы.

И ещё: Состояние «Выключено» применяется, когда устройству приходится временно отключаться от шины — например, в программаторах, мультиплексорах, многоточечных интерфейсах передачи данных наподобие JTAG, I2C или USB и т. д.

То есть получается, что такая «подтяжка» важна для работы в рамках шины I2C. А как её реализовать, учитывая, что у нас вроде как уже есть резисторы на борту платы?

Реализуется это с помощью функции pinMode. Например, так:

pinMode (21, INPUT_PULLUP)

pinMode (22, INPUT_PULLUP)

Хорошая статья про функции подтягивания есть здесь. В ходе экспериментов пробовал вешать эти функции и на master, и на slave (несмотря на то, что это не совсем входы, а двунаправленное общение как бы). Разницы не увидел никакой.

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

По крайней мере, из того, что мне удалось выяснить:

  1. Если расстояния между устройствами малы, то особого смысла в подтягивающих резисторах нет, и все устройства используют один и тот же источник питания.
  2. Внутренние встроенные в плату pull-up резисторы слишком большого номинала. Да, их можно использовать, но это не позволит достаточно быстро осуществлять подтягивание: «Передача/Приём сигналов осуществляется прижиманием линии в 0, в единичку устанавливается сама за счёт подтягивающих резисторов. Их ставить обязательно всегда! Стандарт! Резисторы на 10к оптимальны. Чем больше резистор, тем дольше линия восстанавливается в единицу (идёт перезаряд паразитной ёмкости между проводами), и тем сильней заваливаются фронты импульсов, а значит скорость передачи падает. Именно поэтому у I2C скорость передачи намного ниже, чем у SPI. Обычно IIC работает либо на скорости 10кбит/с — в медленном режиме, либо на 100кбит/с в быстром. Но в реальности можно плавно менять скорость вплоть до нуля.»

В любом случае, было бы интересно прочитать в комментах, если вы знаете что-то больше по этому вопросу…

Мы же рискнём соединить платы просто напрямую. Теперь — куда нам необходимо подключаться?

Если мы посмотрим на распиновку, то увидим, что интерфейс расположен на 22 (SCL) и 21 (SDA) пинах:

Картинка: circuits4you.com

И именно сюда же и предлагает подключаться производитель:

Картинка: docs.espressif.com

Тем не менее, плата поддерживает два таких интерфейса, они могут быть конфигурированы на любом из универсальных портов ввода-вывода.

Для работы с этой шиной будем использовать стандартную библиотеку wire.h.

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


И даже на сайте espressif упоминается о возможности назначения любых пинов, только для master-устройств:


Приходилось видеть и другой вариант, но также для master-устройств, для назначения на любые пины, например 18-19:

Wire.begin(18,19)

Тем не менее, существует и ещё один вариант, который гораздо более универсален и подходит как для master-a, так и для slave-a — мне он кажется самым оптимальным и нравится больше всего:


Итак, попробуем соединить платы, как было описано выше, кроме того, соединим у обеих плат GND и Vin (пробовал и без этого, тоже работает, но идёт сильная потеря пакетов).

Загрузим на каждую из esp32 свою часть примера библиотеки wire.h: на левую — сканер адресов подключённых устройств, на правую — slave. И мы видим, что slave-устройство отлично обнаруживается:


А теперь попробуем установить на ведущее устройство код master-части. Всё работает ОК, потерь пакетов не наблюдается:


Теперь переключим монитор порта на slave-устройство и посмотрим, что пишет оно. Тоже потерь пакетов не наблюдается:


UPD: после долгого тестирования обнаружил, что в некоторых ситуациях всё-таки наблюдаются потери пакетов, однако от этого удалось избавиться очень простым способом: провода I2C и провода питания (Vin, GND) смотал друг с другом, наподобие витой пары. После этого потери пакетов прекратились. Видно, как отчитывается функция endTransmission(), где 0 означает успешную передачу:


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

В качестве такого датчика возьмём уже упомянутый цифровой (то есть который выдаёт только значения LOW и HIGH) датчик Холла KY-003, а в качестве шагового двигателя также упомянутый выше Nema 17 и драйвер двигателя ТВ6600:

Всё работает как и должно! Таким вот нехитрым образом вы можете увеличить количество пинов esp32, соединив между собой n-плат. В показанном выше видео код опроса датчика помещён прямо в цикле loop, что есть не совсем правильно и сделано это для ускорения демонстрации. В реальном проекте, чтобы не занимать процессорное время, датчик надо подключать через функцию attachInterrupt(interrupt, function, mode). В своём проекте собираюсь сделать именно так. Мало того — чтобы постоянно не занимать линию опросами датчиков, все датчики повешу на master, а все двигатели (ну или почти все) — на slave.

Здесь я выложил упрощённый пример показанного в видео, где код управления шаговым двигателем заменён на код светодиода (срабатывание датчика на master-e зажигает встроенный светодиод на slave-e).

Я не ставил целью строить распределённую систему (одна esp32 — в одной комнате, другая — в другой и т. д.), поэтому для моих целей (простое расширение количества пинов) всё работает хорошо. Если вам нужна будет именно распределённая система, возможно, вам придётся углубиться в изучение этой темы дальше. Этот пост не претендует на идеальное знание темы и «учебника» по ней. Скорее это история про то, как у меня возникла проблема и как я её решил.

Соединение между платами по беспроводным каналам (wifi, bluetooth) не рассматривал, так как в моём случае это будет в высшей мере странно — «забивание гвоздей микроскопом» :-)

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх
Источник: https://habr.com/ru/companies/ruvds/articles/755700/


Интересные статьи

Интересные статьи

Изменили в лендинге шаблонные фразы, клише и воду на цифры, примеры, доказательства. Конверсия лендинга выросла на 518%.Привет! Меня зовут Евгения Корюкова. Я руководитель студии MuscleLend. Мы провел...
Приходилось ли вам когда-нибудь, попадая в сложную ситуацию, думать: «Это ведь всё результат моих собственных действий». Эта обычная фраза раскрывает то, как на самом деле мы, люди, понимаем время...
Налоговая отдаёт данные ЕГРЮЛ  по организации в виде PDF. Посредники за автоматический доступ по API хотят денег. На многих сайтах часть данных закрыто, часть функций недоступны бесплатно, и полн...
DDoS-атаки посылают волны в океане Интернета, создаваемые творениями разных размеров - ботнетами. Некоторые из них питаются ближе к поверхности, но существует категория огромных глубоководных чудовищ,...
Два года я делился статистикой только по Российской Федерации и решил, что это не справедливо. Решил, потому что моим планам не суждено было сбыться, так как обновление с...