GPIO

Nesta página iremos apresentar a funcionalidade GPIO presente nos microcontroladores (MCUs) em geral, e no ESP32 em particular.

GPIO são as iniciais de General Purpose Input/Output e como o nome indica permite usar os pinos do ESP32 como entradas ou saídas digitais.

Como se pode ver nesta página a placa ESP32 Pico Kit disponibiliza muitos dos pinos do seu MCU (ESP32-PICO-D4) ao longo da sua periferia. Isto pode ser também comprovado vendo o esquemático desta placa.

Correntes

A datasheet do ESP32-PICO-D4 (ou a do ESP32 em geral) indica que os valores máximos de corrente em cada pino são os seguintes:

  • caso a corrente seja fornecida (source): 40mA
  • caso a corrente seja absorvida (sync): 28mA

No nosso caso para simular um sinal de entrada ou saída iremos recorrer respetivamente a um botão e a um LED. Existem contudo diferentes formas de ligar estes componentes ao ESP32. Como a figura seguinte ilustra, tudo depende se queremos ter sinais ativos ao nível alto ou baixo:

As resistências em série com os LEDs destinam-se a limitar a corrente que os atravessa, a mesma fornecida/absorvida pelo pino GPIO. Com a resistência de 1k, e supondo uma queda de tensão no LED de cerca de 2,3V, a corrente seria de cerca de 1mA (muito abaixo do valor máximo permitido e mesmo assim suficiente para que a emissão de luz seja visível).

Quanto às resistência junto dos botões, destinam-se a definir a tensão quando o botão não está premido. Na sua ausência, quando não se prime o botão, a entrada GPIO ficaria ligada a… nada! A presença duma resistência de pull-up (no primeiro circuito) ou pull-down (no segundo) resolve este problema. Iremos ver mais tarde que é possível prescindir destas resistências desde que se dê indicações para o ESP32 as introduza internamente.

De notar ainda a presença dos condensadores em paralelo com os botões, destinados a minorar os problemas do bouncing dos botões. Com efeito, por serem elementos mecânicos, quando do momento do contacto, este nem sempre é perfeito pelo que por breves milissegundos pode-se verificar uma oscilação entre valores lógicos (como se vê na figura seguinte) que poderá motivar comportamentos indesejáveis no nosso programa.

Tensões

Para além da problemática das correntes nos pinos também é necessário ter atenção às tensões a aplicar neles. O ESP32 trabalha com uma tensão de 3.3V não admitindo nos seus pinos tensões muito superiores a esse valor (no máximo podemos ir até aos 3.3V + 0.3V).

Caso se pretenda por exemplo receber sinais provenientes de lógica que trabalhe a 5V, ou devemos baixar essa tensão através dum divisor de tensão ou usar um circuito adaptador de níveis como o da figura:

Se os níveis de tensão e/ou corrente forem muito diferentes será necessário usar um circuito de interface mais complexo como o da seguinte figura:

MicroPython

Nesta página iremos apresentar a class Pin do MicroPython que nos vai permitir definir os pinos da placa ESP32 Pico Kit (e consequentemente do MCU ESP32) como saídas ou entradas digitais.

Para realizar os testes iremos usar uma placa com LEDs e botões cujo esquemático se apresenta de seguida:

Na placa ESP32 Pico Kit os pinos estão numerados sendo essa a forma de os identificar nos nossos programas:

Para os restantes exemplos iremos efetuar as seguintes ligações:

A class Pin está disponível dentro do módulo Python machine. e daí que todos estes exemplos incluam a linha:

from machine import Pin

Para fazer piscar o LED vermelho, bastaria criar o seguinte programa:

from utime import sleep
from machine import Pin

led_red = Pin(21, Pin.OUT)

while True:
  led_red.value(True)
  sleep(1)
  led_red.value(False)
  sleep(1)

Uma outra forma de fazer piscar o LED vermelho, seria:

from utime import sleep
from machine import Pin

led_red = Pin(21, Pin.OUT)
led_red.value(False)

while True:
  led_red.value(not led_red.value())
  sleep(1)

Vamos agora controlar o estado do LED verde com o botão esquerdo (premindo o botão direito termina o programa):

from machine import Pin

led_green = Pin(19, Pin.OUT)
button_left = Pin(23, Pin.IN, Pin.PULL_UP)
button_right = Pin(18, Pin.IN, Pin.PULL_UP)

while button_right.value():
  led_green.value(not button_left.value())

Será que caso se pretenda agora desenvolver um programa que inclui as duas últimas funcionalidade, bastará juntar os dois programas da forma como a seguir se mostra?

from utime import sleep
from machine import Pin

led_red = Pin(21, Pin.OUT)
led_red.value(False)
led_green = Pin(19, Pin.OUT)
button_left = Pin(23, Pin.IN, Pin.PULL_UP)
button_right = Pin(18, Pin.IN, Pin.PULL_UP)

while button_right.value():
  led_red.value(not led_red.value())
  sleep(1)

  led_green.value(not button_left.value())

Embora à primeira vista, o funcionamento pareça o pretendido, rapidamente nos apercebemos que as ações sobre os botões nem sempre são respeitadas pois o programa fica “bloqueado” pela instrução sleep durante 1 segundo.

A resolução do problema passa por se evitar o uso da função sleep, por exemplo assim:

from utime import ticks_ms
from machine import Pin

led_red = Pin(21, Pin.OUT)
led_red.value(False)
led_green = Pin(19, Pin.OUT)
button_left = Pin(23, Pin.IN, Pin.PULL_UP)
button_right = Pin(18, Pin.IN, Pin.PULL_UP)

last = ticks_ms()
while button_right.value():
  now = ticks_ms()
  if now - last >= 1000:
    last = now
    led_red.value(not led_red.value())

  led_green.value(not button_left.value())

Esta solução é designada de “event loop” pois dentro do loop implementado pelo ciclo while são detetados os eventos: neste caso a passagem de 1 segundo (1000 milissegundos) desde a última troca no estado do LED.

Seria contudo interessante “esconder” esta e outras funcionalidades dentro de classes que integrem as funções esperadas num LED e num botão:

class Button:
  def __init__(self, pin, callback=None, active_high=False, pull_resistor=True):
    pass

  def state(self):
    pass

  def proc(self):
    pass
class Led:
  def __init__(self, pin, active_high=True):
    pass

  def state(self, value=None):
    pass

  def on(self):
    pass

  def off(self):
    pass

  def blink(self, period):
    pass

  def proc(self):
    pass

A existirem tais classes o nosso programa ficaria simplesmente assim:

from led import Led
from button import Button

led_red = Led(21)
led_red.blink(1000)
led_green = Led(19)

def onBut(state):
  led_green.state(state)

button_left = Button(23, onBut)
button_right = Button(18)

while not button_right.state():
  led_red.proc()
  button_left.proc()

Quanto às classes, uma implementação possível seria a seguinte:

from machine import Pin

class Button:
  def __init__(self, pin, callback=None, active_high=False, pull_resistor=True):
    self.active_high = active_high
    self.callback = callback
    if not pull_resistor:
      self.but = Pin(pin, Pin.IN)
    else:
      self.but = Pin(pin, Pin.IN, Pin.PULL_UP if not active_high else Pin.PULL_DOWN)
    self.last = self.state()

  def logic(self, value):
    return value if self.active_high else not value

  def state(self):
    return self.logic(self.but.value())

  def proc(self):
    state = self.state()
    if self.last != state:
      self.last = state
      if self.callback is not None:
        self.callback(state)
from utime import ticks_ms
from machine import Pin

class Led:
  def __init__(self, pin, active_high=True):
    self.active_high = active_high
    self.led = Pin(pin, Pin.OUT)
    self.state(False)
    self.period = 0
    self.last = 0

  def logic(self, value):
    return value if self.active_high else not value

  def state(self, value=None):
    if value is not None:
      self.led.value(self.logic(value))
    return self.logic(self.led.value())

  def on(self):
    self.period = 0
    self.state(True)

  def off(self):
    self.period = 0
    self.state(False)

  def blink(self, period):
    self.period = period
    self.last = ticks_ms()

  def proc(self):
    if self.period != 0:
      if ticks_ms() - self.last >= self.period:
        self.state(not self.state())
        self.last = ticks_ms()

Mesmo usando Classes esta abordagem recorrendo ao “event loop” é considerada pouco “pythonic” pelo que existem outras formas de resolver este tipo de problemas, como o uso do package uasyncio. Um excelente tutorial sobre o assunto pode ser visto aqui.

TPC (Projeto Semáforo)

Pretende-se desenvolver um programa em MicroPython que coloque a nossa placa ESP32 Pico Kit a controlar um semáforo para automóveis situado junto a uma passadeira para peões. Note que os tempos referidos de seguida, embora irrealistas, visam tornar o processo de simulação mais rápido!

O ciclo normal de funcionamento do semáforo será de 9 segundos em verde (LED Lg), 1 segundo em amarelo (Ly) e 5 segundos em vermelho (Lr).

Caso seja premido o botão dos peões (Bp) o tempo de verde deverá ser imediatamente terminado desde que já tenham decorrido 4 segundos. Se esse tempo ainda não decorreu a passagem para amarelo deverá ocorrer só após esses 4 segundos.

O botão Bi servirá para que na central de controlo do tráfego se inicie/termine o modo “intermitente” no semáforo. Neste modo o semáforo deverá ter o amarelo a piscar ciclicamente (1 segundo ligado, 1 segundo desligado).