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 que recorre a um relé:

Uma outra solução passa por usar um TRIAC e um acoplador ótico como os presentes na 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

Blink

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

from time 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)

De notar a presença da função sleep do módulo time, que coloca o processador em pausa durante um determinado número de segundos.

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

from time 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)

Follow

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())

De notar a presença do parâmetro Pin.PULL_UP na especificação das entradas onde os botões estão ligados e que nos permitiria prescindir do uso das resistências de pull-up junto aos mesmos.

Blink + Follow (KO)

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 time 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.

Blink + Follow (OK)

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

from time 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. Para isso, o controlo do tempo entretanto decorrido pode ser feito pela função ticks_ms.

Classes

Seria contudo interessante “esconder” esta e outras funcionalidades dentro de classes que integrem as funções esperadas num botão e num LED, respetivamente os ficheiros button.py e led.py que a seguir se apresentam:

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 main.py 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 Button e Led, seguem-se respetivamente os ficheiros button.py:

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)

E led.py:

from time 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()

uasyncio

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 módulo uasyncio que desde a versão 1.13 do MicroPython já vem incluído no firmware. Um excelente tutorial sobre o assunto pode ser visto aqui.

Uma solução com esta abordagem seria por exemplo a seguinte:

from machine import Pin
import uasyncio

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)

async def blink():
    while True:
        led_red.value(not led_red.value())
        await uasyncio.sleep_ms(1000)

async def follow():
    while True:
        led_green.value(not button_left.value())
        await uasyncio.sleep_ms(5)
        
async def main():
    uasyncio.create_task(blink())
    uasyncio.create_task(follow())
    while button_right.value():
        await uasyncio.sleep_ms(5)
 
uasyncio.run(main())

Aula laboratorial

Na aula deverá testar todos os programa acima indicados, consolidando assim os conhecimentos transmitidos na aula teórica.

No caso do programa Follow deverá adicionalmente observar no osciloscópio os sinais nos pinos 23 e 19 (respetivamente o sinal gerado pelo botão da esquerda e o sinal que controla o LED verde), medindo concretamente o atraso introduzido pelo programa Micropython.

Por último deverá desenvolver um programa que implemente um pequeno sistema de controlo dum semáforo que a seguir se descreve.

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).