NodeMCU (Lua) — GPIO

Работа с внешними устройствами происходит через порты ввода-вывода (GPIO). С их помощью можно как считывать сигналы, например с различных датчиков, так и управления другими устройствами. ESP8266 не может похвастаться большим количеством свободных портов. В зависимости от исполнения модуля (см. ниже), доступно разное число портов. Например, в ревизии ESP01, если не считать UART, имеется только два порта. Но даже в версиях где все порты выведены (ESP12E), без ограничений доступны только часть их, так как остальные совмещают системные функции (выбор режима загрузки, SPI флешь, вывод из сна).

1

Для работа с портами ввода-вывода в NodeMCU используется модуль GPIO. Если открыть документацию, первое что бросается в глаза это нумерацией портов GPIO (см. ниже), разработчики решили использовать свою нумерацию, поэтому нумерации портов NodeMCU и официальным обозначением портов ESP8266 отличается. Разрабатывая какое-то устройство при использовании плат NodeMCU или WeMos D1, где порты "правильно" подписаны, нумерация в коде и на самой плате одинаковая, но если разрабатывать на "голом" ESP8266 и руководствуясь официальным обозначением GPOI, то довольно часто приходиться заглядывать в таблицу.

2

При проектировании устройств на NodeMCU, для себя сделал такую пометку, свободных портов которыми можно пользоваться без каких либо ограничений (вход, выход, прерывания) всего 5 - это GPIO4, GPIO5, GPIO12, GPIO13, GPIO14. При использовании остальных необходимо учитывать их особенности. Поэтому внимательно читайте документацию во избежание проблем.

Для "нормальной" загрузки ESP8266 необходимо чтобы в момент старта (подачи питания) на портах GPIO0, GPIO2 присутствовал высокий, а GPIO15 низкий уровень.

GPIO16 можно настроить только как вход или выход, а если используем режим сна, "будить" его через определенный интервал времени, должен быть соединен с RESET.

Инициализация GPIO

Перед тем как начать работать с портом необходимо его про инициализировать, выбрав один из четырех режимов работы:

  1. Вход (gpio.INPUT)
  2. Выход (gpio.OUTPUT)
  3. Прерывания (gpio.INT)
  4. Открытый сток (gpio.OPENDRAIN)

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

С помощью функции gpio.mode(pin, mode [, pullup]) устанавливается режим работы. Функция принимает два  обязательных параметра (номер порта и режим работы) и один не обязательный (будет ли порт подтянут к питанию, по умолчанию не подтянут)

\-- настроить порт GPIO4 на выход 
gpio.mode(2, gpio.OUTPUT)

-- настроить порт GPIO5 на вход и включить подтяжку к питанию
gpio.mode(1, gpio.INPUT, gpio.PULLUP) 

Вход

Чтобы узнать какой логический уровень присутствует на порту, можно с помощью функции gpio.read(pin). Функции принимает один параметр номер порта который необходимо считать, а возвращает его состояние, 0 или 1 в зависимости от логического уровня на порту.

Считывать состояние порта можно как в режиме вход, так и выход.

pin = 1                       -- GPIO5 (см. в таблицу выше)
mode = gpio.INPUT             -- как вход
pullup = gpio.PULLUP          -- вкл. подтяжку порта к питанию

gpio.mode(pin, mode, pullup)  -- инициализируем порт
result = gpio.read(pin)       -- получаем состояние порта
print(result)                 -- выводим состояние в консоль

Выход

Состояние логического уровня на выходе порта меняется с помощью функции gpio.write(pin, level). В отличии от функции чтения, функция записи ничего не возвращает, а на вход поступает уже два параметра, это номер порта и необходимый логический уровень на его выходе.

pin = 2                       -- GPIO4 (см. в таблицу выше)
mode = gpio.OUTPUT            -- как выход

gpio.mode(pin, mode)          -- инициализируем порт

gpio.write(pin, gpio.HIGH)    -- установить высокий уровень 1
print(gpio.read(pin))         -- выводим состояние порта в консоль
gpio.write(pin, gpio.LOW)     -- установить низкий уровень 0
print(gpio.read(pin))         -- выводим состояние порта в консоль

Прерывания

Разобрав работу функций чтения мы можем проверить текущий уровень порта, но что если нам необходимо отслеживать изменения уровня и как-то на него реагировать. Для этого нам нужно подписатся на какое-то событие, в этом нам поможет функция gpio.trig(pin, [type [, callback_function]]). Функция принимает два параметра, номер порта и типа события на который хотим подписаться, а также callback функцию, которая будет вызвана если произойдет данное событие.

Типы событий.

  • up - отслеживание по переднему фронту, то есть будет вызвана в момент перехода от логического низкого уровня на высокий.
  • down - отслеживание по заднему фронту, то есть будет вызвана в момент перехода от логического высокого уровня на низкий.
  • both - отслеживание обоих фронтов, как по переднему так и заднему фронту
  • low - отслеживание низкого логического уровня, то есть будет срабатывать пока присутствует низкий логический уровень
  • high - отслеживание высокого логического уровня, то есть будет срабатывать пока присутствует высокий логический уровень
  • none - отписаться от всех прерываний

Например, чтобы отследить нажатие кнопки достаточно нескольких строк кода. Для этого нужно создать событие и функцию обработки события. Когда кнопка будет нажата, будет запущена наша функция.

pin = 1                           -- GPIO5 (см. в таблицу выше)
mode = gpio.INT                   -- режим прерывания

gpio.mode(pin, mode, gpio.PULLUP) -- инициализируем порт

function callback(level, time)    -- callback функция
    print(level, time)            -- выводим в консоль уровень и время
end

gpio.trig(pin, "down", callback)  -- подписываемся на событие 

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

pin = 1                                    -- GPIO5 (см. в таблицу выше)
mode = gpio.INT                            -- режим прерывания

function debounce(pin)                     -- callback функция
  local last, delay = 0, 200000            -- устанавливаем задержку в 0,2 сек 
  return function (level, time)
    if time - last < delay then return end
    if gpio.read(pin) == 0 then            -- проверяем текущий уровень 
      last = time
      print("OK")
    end
  end
end

gpio.mode(pin, mode, gpio.PULLUP)          -- инициализируем порт
gpio.trig(pin, 'down', debounce(pin))      -- подписываемся на событие

В случае если необходимо отписаться от всех событий, сделать это можно с помощью той же функции gpio.trig(pin, type), но указав в качестве второго параметра none.

pin = 1

gpio.trig(pin, "none")

Как видно модуль GPIO и асинхронность LUA упрощает работу с портами ввода-вывода, достаточно пары строк кода чтобы узнать, изменить состояние порта или повесить на него событие, этого более чем достаточно для простых задач. Но что касается генерации или замера цифрового сигнала, то тут не все так радужно. Если для генерацией сигналов с последними обновлениями появились новые функции (gpio.serout и gpio.pulse), которые позволяют формировать различные импульсы с минимальными интервалами по времени, то вот с подсчетом их дела обстоят немного хуже. При максимально возможной частоте, подсчет занимает все ресурсное время не оставляя его для других операций. Но и даже при подсчете коротких импульсов одного и того же сигнала можно наблюдать разный результат, особенно это заметно в нагруженных проектах. Эта расплата за асинхронность

При изучении модулей NodeMCU в первую очередь руководствуйтесь официальной документацией, т.к. постоянно вносятся изменения и возможно что некоторая информация потеряет свою актуальность.

ESP8266 NodeMCU
(0.0) / 0
Прежде чем оставить комментарий, пожалуйста, ознакомьтесь с правилами комментирования. Оставляя комментарий, вы подтверждаете ваше согласие с данными правилами и осознаете возможную ответственность за их нарушение. Все комментарии премодерируются.
0