Serial

Introdução

O uso de portas de entrada/saída de uso genérico (GPIOs) como as referidas numa outra página permite a troca de bits isolados de informação. Com efeito para se ligar ou desligar um LED basta usar um pino de saída, enquanto que para receber o estado dum botão um pino é igualmente suficiente. Mas o que fazer quando se tem que enviar ou receber dados mais complexos?

De facto, em certos projetos pode ser necessário trocar texto ou valores numéricos com dispositivos eletrónicos ligados à placa ESP32 Pico Kit, como LCDs, sensores diversos, etc. Em geral toda essa informação é decomposta em bytes que são depois enviados/recebidos por um protocolo série.

Série vs Paralelo

Num protocolo série os bits que compõem os bytes de informação não são enviados todos em simultâneo (como no caso dum protocolo paralelo) mas sim um após outro ao longo do tempo. Desta forma são necessários menos pinos do interface GPIO da placa ESP32 Pico Kit, e o número de fios a interligar a placa com o dispositivo é também menor.

NOTE: Na imagem é o bit mais significativo (MSB) o que é enviado primeiro mas como iremos ver, dependendo do protocolo a ordem pode variar!

Protocolos série

Os protocolos série disponíveis no microcontrolador ESP32, e que serão descritos ao longo desta página, são os seguintes:

Nesta página para além de se apresentarem as principais ideias sobre cada um deles, serão também descritas as funções MicroPython que possibilitam o uso destes protocolos.


Protocolo UART

O protocolo UART (Universal Asynchronous Receiver/Transmitter) é o mais antigo e como o nome indica é assíncrono. Isto porque não existe qualquer sinal de relógio que marque a cadência a que os bits são enviados/recebidos. Neste caso os dois intervenientes nesta comunicação (a placa e o dispositivo) acordam previamente qual o número de bits por segundo a enviar/receber. Este parâmetro é vulgarmente designado de baudrate e pode ser escolhido de entre um conjunto de valores standard: 2400, 4800, 9600, … 115200, etc.

UART

Na ausência de dados (Bus Idle) estas duas linhas apresentam o nível lógico 1. Os bits de dados são antecedidos por um Start Bit igual a 0, e seguidos por um Stop Bit igual a 1. Os bits de dados (em geral 8) são enviados do menos para o mais significativo*. Antes do envio do Stop Bit (que pode ter uma duração de 1 ou 2 bits) pode ainda ser enviado um bit de paridade**: par (Even) ou ímpar (Odd), destinado à deteção de eventuais erros de transmissão.

* em certos casos esta ordem pode também ser configurada!

** o bit de paridade terá o valor lógico de forma a que o número de bits iguais a 1 fique par ou ímpar.

UART_bits

Classe MicroPython UART

O MicroPython possui uma classe UART que integra métodos quer para configurar a comunicação, quer para enviar ou receber dados através dela. Mais informação específica da utilização desta classe para o caso particular do ESP32 pode ser vista aqui.

Script ledremote.py

Neste exemplo vamos controlar o LED vermelho presente na breadboard presente na bancada do lado por intermédio dos botões presentes na nossa breadboard.

Neste exemplo iremos usar para o protocolo série UART as seguintes características:

  • 9600 bits por segundo de baudrate
  • 8 bits de dados
  • paridade par
  • 1 stop bit

O seguinte script (ledremote.py) depois de configurar a comunicação série com estas características e definindo igualmente os pinos a usar (tx=2, rx=4), configura o pino 21 como uma saída de controlo do LED, e os pinos 23 e 18 como entradas provenientes dos botões esquerdo e direito que irão respetivamente ligar e desligar o LED. De notar que ambos irão dar origem a interrupções pela chamada da rotina send_msg() onde se procede ao envio de mensagens em modo texto pela porta uart configurada indicado o estado do LED pretendido (‘L1’ para ligar e ‘L0’ para desligar o LED).

No ciclo principal do programa (rotina loop()) far-se-á a chamada cíclica da rotina check_msg() para verificar da receção de mensagens uart, e se for o caso, proceder ao controlo do LED em conformidade.

from machine import UART, Pin
from utime import sleep_ms

uart = UART(1)
uart.init(baudrate=9600, bits=8, parity=0, stop=1, tx=2, rx=4)

def send_msg(p):
    if p == bon:
        uart.write('L1')
    if p == boff:
        uart.write('L0')

def check_msg():
    if uart.any():
        msg = uart.read().decode()
        if msg == 'L1':
            led.value(True)
        if msg == 'L0':
            led.value(False)

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

bon = Pin(23, Pin.IN, Pin.PULL_UP)
bon.irq(trigger=Pin.IRQ_FALLING, handler=send_msg)

boff = Pin(18, Pin.IN, Pin.PULL_UP)
boff.irq(trigger=Pin.IRQ_FALLING, handler=send_msg)

def loop():
    while True:
        check_msg()
        sleep_ms(10)

try:
    loop()
except KeyboardInterrupt:
    print('Got Ctrl-C')
finally:
    uart.deinit()
    bon.irq(handler=None)
    boff.irq(handler=None)
    print("Finishing...")

De notar que as mensagens trocadas estão definidas no Python como sendo do tipo bytes (ou bytearray), pelo que caso se queira convertê-las para strings teremos que “descodificá-las” em conformidade atravez do método .decode().

O parâmetro parity no método de inicialização deverá tomar o valor 0, 1 ou None caso se pretenda respetivamente paridade par, ímpar ou “sem paridade”. Neste script pretendendo-se paridade par o seu valor foi definido como 0.

Caso se teste o script fornecido, nada acontece pois os pinos de transmissão (2) e receção (4) não estão interligados, mas se o fizermos com um simples fio condutor, o controlo do LED inicia-se de imediato:

Sugere-se que na aula laboratorial se proceda também à visualização no osciloscópio do sinal série que deverá ter uma evolução próxima da indicada na figura seguinte para o caso da mensagem que desliga o LED: L0

De notar a presença dos códigos ASCII dos dois caracteres (L = 0x4C e 0 = 0x30) na forma de 8 bits da dados (começando pelo LSB), precedidos cada um deles por um start bit e seguidos do bit de paridade e do stop bit.

Para terminar diga-se que para que o objetivo inicial seja atingido (os botões deveriam sim controlar o LED da bancada vizinha), será necessário ligar os dois ESP32 envolvidos da seguinte forma:

Desta forma as mensagens uart transmitidas por um ESP32 serão recebidas pelo outro e vice-versa!

Comunicação série Assíncrona vs Síncrona

O facto de no protocolo UART não existir linha de relógio faz com que as velocidades de transmissão não possam ser demasiado elevadas (em geral o valor máximo não vai além de 115200 bps). Pelo contrário os protocolos a apresentar seguidamente são síncronos pois enviam os dados de forma sincronizada com uma linha de relógio, permitindo assim maiores velocidades.

Adicionalmente os próximos protocolos seguem uma filosofia mestre-escravo (master-slave), onde a placa ESP32 Pico Kit toma o papel de “master” e o dispositivo a ela ligado, o de “slave“. Ou seja, é a placa que gera o sinal de relógio e comanda o processo de leitura e escrita dos dados.


Protocolo SPI

No protocolo SPI (Serial Peripheral Interface), quer o master quer o slave possuem internamente um registo de deslocamento que serão ligados entre si por duas linhas de dados:

  • MOSI – Master Output Slave Input
  • MISO – Master Input Slave Output
400px-SPI_8-bit_circular_transfer.svg

Os bits são deslocados ao ritmo do sinal de relógio (SCLK) gerado pelo master.

Para se poder ligar diferentes slaves, o master controla o valor presente em determinadas linhas de seleção (designadas na figura seguinte por SSx). O slave que tiver a sua linha ativa, saberá que os dados na linha MOSI são para si, e/ou que pode gerar dados na linha MISO. Deste modo não há qualquer conflito ao nível das linhas MISO pois só uma estará ativa (as restantes estarão em “alta impedância”).

350px-SPI_three_slaves.svg

Classe MicroPython SPI

O MicroPython possui uma classe SPI que integra métodos quer para configurar a comunicação, quer para enviar ou receber dados através dela. Mais informação específica da utilização desta classe para o caso particular do ESP32 pode ser vista aqui.

Quando o protocolo SPI é iniciado, são definidos o seu identificador (pode ser 1 ou 2), a frequência do relógio a usar, os pinos para os sinais SCK, MOSI e MISO, e ainda o modo SPI através da definição dos parâmetros polarity e phase. Tal como a figura seguinte mostra, a polaridade tem a ver com o estado de repouso do sinal de relógio SCK, enquanto que a fase relaciona-se com a ordem da transição usada para ler os sinais de dados:

spi-modes

De notar que enquanto que os sinais SCK, MOSI e MISO são tratados automaticamente pelo hardware do ESP32, o sinal SS (também designado de CS: chip select) deverá ser gerado pelo próprio programa num dos seus GPIOs.

Script mcp42010.py

Neste script vamos controlar um potenciómetro digital presente no circuito integrado MCP42010, através do protocolo SPI que ele possui. O potenciómetro apresenta uma resistência de 10kΩ entre os pontos A e B sendo que a posição do seu ponto médio W pode ser programado numa de 256 posições possíveis enviando através do barramento SPI um valor de 8 bits (entre 0 e 255).

De notar que, para um mais fácil teste, implementou-se com o potenciómetro um divisor de tensão (ligando os seus pontos A e B a 3V3 e GND respetivamente) sendo o seu ponto médio ligado a uma entradas analógica do PIC32. Desta forma o potenciómetro digital transforma-se numa espécie de DAC (Digital to Analog Converter).

O circuito resultante é pois o seguinte:

O programa começa por pedir um valor entre 0 e 255 que será enviado para o MCP42010 pelo barramento SPI. Esse valor define a posição do ponto médio do potenciómetro dando nele origem a uma tensão analógica que é depois convertida pelo ADC do ESP32. O resultado desta conversão é escalado para 8 bits (de 0 a 255) de forma a ser confrontado com o valor inicial. Embora possa ocorrer alguma discrepância entre o valor inicial e final devido aos erros introduzidos nas medidas, é de esperar que esses valores sejam contudo próximos e fortemente correlacionados.

from machine import Pin, ADC, SPI

spi = SPI(1)
spi.init(baudrate=2000000, firstbit=SPI.MSB, polarity=0, phase=0,
    sck=Pin(14), mosi=Pin(12), miso=Pin(13))

cs = Pin(27, Pin.OUT)
cs.value(1)

adc = ADC(Pin(33))
adc.atten(ADC.ATTN_11DB)  # 3.6V
adc.width(ADC.WIDTH_9BIT) # 0..511

cmd_byte = 0b00_01_00_10

def loop():
    while True:
        data_byte = int(input("Enter pot position (0..255): "))

        cs.value(0)
        spi.write(bytes([cmd_byte, data_byte]))
        cs.value(1)

        print('ADC value: ', int(adc.read() / 2))

try:
    loop()
except KeyboardInterrupt:
    print('Got Ctrl-C')
finally:
    spi.deinit()
    print("Finishing...")

Relativamente aos valores a enviar para definição do potenciómetro presente no MCP42010, tudo está explicado na sua datasheet, resumida na seguinte figura:

spi_waves

A seguinte imagem mostra os sinais envolvidos no barramento SPI quando da escolha da posição 19 (0x13).

spi_wr

É possível ver o envio dos referidos 16 bits, formados por um byte com o comando (0x12 – escrita no potenciómetro 1), seguido dum byte com o dado propriamente dito (0x13 neste exemplo).

No caso do MCP42010, não existe sinal MISO (o pino SO destina-se apenas a ligar vários circuitos integrados semelhantes numa cadeia tipo daisy chain), não sendo pois realizadas quaisquer leituras. Sugere-se pois a leitura da página que descreve a classe SPI para ver esses outros métodos.


Protocolo I2C

O protocolo I2C (Inter-Integrated Circuit) é implementado apenas com duas linhas:

  • SDA – Serial DAta
  • SCL – Serial CLock

A existência duma só linha de dados impede que, tal como ocorre no protocolo SPI, a comunicação seja full-duplex, ou seja que se possa realizar em simultâneo o envio e a receção de dados.

Como a figura seguinte mostra, todos os dispositivos ficam “pendurados” nesse barramento. Não existem quaisquer linhas de seleção pelo que o mecanismo de escolha de qual o dispositivo slave com que o master pretende comunicar. Isso é feito na forma dum endereçamento por software, i.e. cada slave terá um endereço de 7 bits a ser usado quando do estabelecimento da comunicação.

I2C

De notar que todas as ligações ao barramento são realizadas por saídas em dreno-aberto (e daí a necessidade das resistências de pull-up). Dessa forma nunca existirão curto-circuitos nas linhas mesmo que hajam acessos simultâneos.

Classe MicroPython I2C

O MicroPython possui uma classe I2C que integra métodos quer para configurar a comunicação, quer para enviar ou receber dados através dela. Mais informação específica da utilização desta classe para o caso particular do ESP32 pode ser vista aqui.

Script ad5241.py

Neste projeto vamos controlar um outro potenciómetro digital desta vez situado no circuito integrado AD5241 através do seu barramento I2C.

O circuito de ligações é agora o seguinte (não esquecer as resistências de pull-up nas linhas I2C com por exemplo 2k2):

O programa é semelhante ao anterior sendo que neste caso os valores a introduzir para a posição do potenciómetro têm o seguinte significado:

  • 0..255: definir a posição do potenciómetro (e sua estimativa via ADC)
  • -1: neste caso é feita uma listagem dos endereços dos dispositivo I2C encontrados no barramento
  • -2: aqui é feita a leitura da posição do potenciómetro diretamente dele
from machine import Pin, ADC, I2C

i2c = I2C(1, freq=100000, scl=Pin(25), sda=Pin(26))

adc = ADC(Pin(32))
adc.atten(ADC.ATTN_11DB)  # 3.6V
adc.width(ADC.WIDTH_9BIT) # 0..511

I2C_ADDR = 0x2C
inst_byte = 0b00000_000

def printI2CDevices():
    print('Scan i2c bus...')
    devices = i2c.scan()
    print('Found {0} I2C device(s): '.format(len(devices)), end='')
    for device in devices:  
        print('{0} '. format(hex(device)), end='')
    print()

def loop():
    while True:
        data_byte = int(input("Enter position (0..255): "))

        if data_byte == -1:
            printI2CDevices()
        elif data_byte == -2:
            buf = i2c.readfrom(I2C_ADDR, 1)
            print('Position:', buf[0])
        else:
            i2c.writeto(I2C_ADDR, bytes([inst_byte, data_byte]))
            print('ADC value: ', int(adc.read() / 2))

try:
    loop()
except KeyboardInterrupt:
    print('Got Ctrl-C')
finally:
    print("Finishing...")

Na definição do protocolo I2C, será necessário fornecer a sua identificação (pode ser 0 ou 1), a frequência do sinal de relógio (os valores standard são 100kHz ou 400kHz), e os pinos onde serão gerados os sinal SCL e SDA (neste exemplo os pinos 25 e 26).

Relativamente aos valores a enviar para definição do potenciómetro presente no AD5241, tudo está explicado na sua datasheet, resumida na seguinte figura:

i2c_waves

As seguinte imagens mostram os sinais envolvidos no barramento I2C quando da escrita da posição 19 (0x13) no potenciómetro, e a subsequente leitura dessa mesma posição:

i2c_wr
i2c_rd

Como as imagens mostram, em ambos os processos (escrita e leitura) é enviado um primeiro byte com o endereço do dispositivo. Esse byte é formado juntando ao endereço de 7 bits, um 0 ou um 1, no caso duma escrita ou de uma leitura respetivamente. Assim o endereço de 7 bits 0x2C transforma-se respetivamente nos endereços de 8 bits: 0x58 ou 0x59.

De notar que este dispositivo admite 3 outros endereços de 7 bits, num total de 4 endereços portanto. Tudo depende da forma como os pinos AD1 e AD0 estão ligados. Desta forma é possível coexistir no mesmo circuito até 4 dispositivos iguais.

Todas as comunicações iniciam-se com uma “start condition” (descida da linha de dados com o sinal de relógio ao nível 1) e terminam com uma “stop condition” (subida da linha de dados com o sinal de relógio ao nível 1). Todas as outras transições da linha de dados ocorre pois quando a linha de relógio está já ao nível 0).

Por último, note-se que todos os 8 bits que compõem os bytes enviados/recebidos são sempre seguidos dum bit de “acknowledgement“. Este bit, por exemplo após o byte de endereço, serve para o slave sinalizar que está presente no barramento (ao colocar a linha de dados a 0).

Últimas considerações

Como se pôde constatar, para o uso deste género de protocolos, tanto ou mais importante que saber quais as classes MicroPython e respetivos métodos existem, é saber que mensagens os dispositivos a que nos vamos ligar enviam e esperam receber, algo que só uma leitura atenta das respetivas datasheets pode permitir.

Aula laboratorial

Implemente e confirme as funcionalidades dos 3 projetos descritos ao longo desta página:

Em cada um deles visualize no osciloscópio os sinais de dados (respetivamente TX, MOSI e SDA) juntamente com os eventuais sinais de relógio (*, SCLK, SCL), e interprete a evolução temporal e lógica desses sinais face às datasheets dos componentes envolvidos.

* No caso do protocolo UART não existe sinal de relógio!

Responda ainda às seguintes questões:

  • Qual seria a forma de onda TX quando se carrega no botão Bon, caso o programa ledremote.py apresente antes a linha: uart.init(baudrate=50000, bits=8, parity=1, stop=1, tx=2, rx=4)?
  • Caso o pino 33 do ESP32 tenha que estar ligado ao pino PW0 do integrado MCP42010, que alterações de hardware e software teria que realizar para manter a funcionalidade do programa?
  • Caso o pino AD1 do integrado AD5241 estiver ligado a 3V3 (em vez de ao GND), o que mudaria no programa para manter a sua funcionalidade?

Vídeo

O vídeo desta aula pode ser obtido aqui.