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.

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, 1.5 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.

Script myuart.py

Neste exemplo vamos ligar a placa ESP32 Pico Kit a um recetor GPS EM406A que usa um protocolo de comunicação UART no qual serão trocadas mensagens de dados segundo o standard NMEA.

De notar que os sinais RX/TX deste dispositivo em particular possuem níveis de tensão entre 0 e 5V pelo que não podem ser ligados diretamente ao ESP32. Daí a presença no circuito de um adaptador de níveis que passa os sinais referidos para a gama de tensão 0..3.3V

Neste recetor, o protocolo série UART por ele usado possui por omissão as seguintes características:

  • 4800 bits por segundo de baudrate
  • 8 bits de dados
  • sem bit de paridade
  • 1 stop bit

O seguinte script (myuart.py) depois de configurar a comunicação série com estas características e definindo igualmente os pinos a usar, fica a aguardar que o botão ligado ao pino 23 seja premido para enviar uma mensagem SiRF $PSRF103 a solicitar a receção duma mensagem NMEA $GPGGA que traz as coordenadas GPS, imprimindo-as de seguida:

from utime import ticks_ms
import esp
from machine import UART, Pin

led = Pin(21, Pin.OUT)

esp.osdebug(None)

uart = UART(1, 4800)
uart.init(baudrate=4800, bits=8, parity=None, stop=1, tx=15, rx=2)

def queryGGA(p):
  if not p.value():
    uart.write(b'$PSRF103,00,01,00,01*25\r\n')

but = Pin(23, Pin.IN, Pin.PULL_UP)
but.irq(trigger=Pin.IRQ_FALLING, handler=queryGGA)

# print GGA message hiding part of the coordinates
def printGGA(m):
  s = m.decode("utf-8") # bytes to str
  s = s[:-2] # remove \r\n
  if s[18] == ',':
    print(s)
  else:
    print('{0}__{1}__{2}'.format(s[:20], s[22:33], s[35:]))

def loop():
  last = 0
  msg = b''
  while 1:
    now = ticks_ms()
    if now - last >= 100:
      last = now
      led.value(not led.value())

    n = uart.any()
    if n:
      msg += uart.read(n)
    v = msg.find(b'\n')
    if v >= 0:
      printGGA(msg[:v+1])
      msg = msg[v+1:]

try:
  loop()
except KeyboardInterrupt:
  print('Got Ctrl-C')
finally:
  uart.deinit()
  led.value(0)

De notar que as mensagens trocadas estão definidas no Python como sendo do tipo bytes.

No standard NMEA todas as mensagens iniciam com um caracter ‘$’ e terminam com dois caracteres (carriage-return e new-line, respetivamente ‘\r’ e ‘\n’).

O facto das mensagens terminarem com o caracter ‘\n’ permitiria usar o método readline(), contudo isso bloquearia o programa até à chegada desse caracter (ou o timeout definido fosse esgotado). Como neste programa se pretende colocar em paralelo um LED a piscar, foi adotada outra estratégia usando métodos como any() e read() que não bloqueiam o programa.

De seguida são apresentadas 4 mensagens $GPGGA, sendo que nas primeiras duas as coordenadas GPS ainda estão ausentes (por falta de cobertura do sinal GPS), enquanto que nas duas seguintes as coordenadas já são visíveis (em torno dos 41º N, 8º W):

$GPGGA,212321.000,,,,,0,00,,,M,0.0,M,,0000*57
$GPGGA,212325.000,,,,,0,00,,,M,0.0,M,,0000*53
$GPGGA,212348.000,41__.5186,N,008__.2958,W,1,03,5.6,75.1,M,51.4,M,,0000*7C
$GPGGA,212350.000,41__.5181,N,008__.2935,W,1,03,5.6,73.8,M,51.4,M,,0000*76

A imagem seguinte mostra a onda do sinal RX do ESP32 (pino 2) quando da chegada dos primeiros caracteres duma mensagem $GPGGA:

De notar a presença para cada caracter dum start bit, 8 bits de dados (o código ASCII respetivo começando pelo LSB) e um stop bit.

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.


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.

Script myspi.py

Neste exemplo vamos ligar a placa ESP32 Pico Kit a um acelerómetro ADXL345 cuja datasheet pode ser vista aqui.

Trata-se dum sensor capaz de medir as acelerações por ele sofridas segundo os três eixos x, y e z. O sensor ADXL345 está presente num módulo GY-291 cujo esquemático se mostra de seguida:

Este sensor tem a possibilidade de ser ligado por I2C ou SPI sendo que neste script pretende-se usar este último, pelo que as ligações a realizar entre a placa ESP32 Pico Kit e o módulo são as seguintes:

De notar que embora o módulo seja alimentado a 5V, o seu ADXL345 é alimentado a 3.3V (existindo no módulo uma regulador de tensão para o efeito) pelo que os níveis de tensão de todos os sinais de dados são compatíveis com o ESP32 não sendo desta vez necessário qualquer adaptador de níveis.

O seguinte programa mede ciclicamente (a cada 100ms) os valores de aceleração fazendo a respetiva escrita:

from utime import sleep_ms
from machine import Pin, SPI

# pylint: disable=bad-whitespace
# Registers
DEVID       = 0x00
POWER_CTL   = 0x2D
DATA_FORMAT = 0x31
DATAX0      = 0x32
# Bits
READ_BIT      = 0x80
MULTIBYTE_BIT = 0x40

spi = SPI(baudrate=100000, polarity=1, phase=1, sck=Pin(32), mosi=Pin(33), miso=Pin(27))

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

def toDec(high, low):
  value = low + (high<<8)
  if value > 2**15:
    value -= 2**16
  return value

def writeReg(reg, value):
  cs.value(0)
  buf = bytearray([reg, value])
  spi.write(buf)
  cs.value(1)

def readReg(reg):
  cs.value(0)
  spi.write(bytearray([reg | READ_BIT]))
  buf = spi.read(1)
  cs.value(1)
  return buf

def checkID():
  return readReg(DEVID)[0] == 0xE5

def readData():
  cs.value(0)
  spi.write(bytearray([DATAX0 | READ_BIT | MULTIBYTE_BIT]))
  b = spi.read(6)
  x = toDec(b[1], b[0])
  y = toDec(b[3], b[2])
  z = toDec(b[5], b[4])
  cs.value(1)
  return [x, y, z]

def initADXL():
  writeReg(DATA_FORMAT, 0x01)
  writeReg(POWER_CTL, 0x08)

if checkID():
  initADXL()
  while 1:
    print(readData())
    sleep_ms(100)

Quando a classe SPI é instanciada, são definidos a frequência do relógio a usar (100kHz neste caso embora pudesse ir até aos 5MHz), os pinos para os sinais SCK, MOSI e MISO (respetivamente 32, 33 e 27), 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 CS deverá ser gerado pelo próprio programa num dos seus GPIOs (o pino 14 neste caso).

A datasheet do sensor indica que todos os seus dados internos estão organizados no seguinte conjunto de registos:

Um desses registos é o DEVID (situado no endereço 0x00) e que destina-se apenas à leitura dum código identificador do dispositivo (0xE5 neste caso). A verificação deste código (realizado na função checkID()) permite confirmar que a comunicação com o dispositivo está a ser realizada com sucesso.

Seguem-se as escritas na função initADXL() dos registos DATA_FORMAT e POWER_CTL (situados nos endereços 0x31 e 0x2D) com os valores 0x01 e 0x08 respetivamente, e que permitem escolher a gama de medida +-4 g e ligar o processo de medida.

Resta efetuar ciclicamente (a cada 100ms) a medida das acelerações que se encontram em 3 pares de registos consecutivos a partir do endereço DATAX0 (0x32). Porque se tratam de valores em complemento para dois e repartidos por um byte menos significativo seguido de outro mais significativo, faz-se a respetiva conversão para decimal na rotina toDec().

As escritas e leituras via SPI devem respeitar as indicações dadas na datasheet e que a seguir se representam:

Assim, ainda antes de se efetuar qualquer escrita ou leitura de dados é necessário enviar um byte contendo a seguinte informação:

  • bit 7 (READ_BIT) – 0 para escrita de dados ou 1 para leitura
  • bit 6 (MULTIBYTE_BIT) – ativo para aceder a vários bytes de dados em endereços consecutivos
  • bit 5 a 0 (A5..A0) – endereço do (primeiro) registo a aceder

Na figura seguinte é possível ver a evolução temporal das linhas CS, SCK, MOSI e MISO quando da leitura do registo DEVID (endereço 0x00) que tal como já foi referido retornou o valor fixo 0xE5:

De notar que neste caso, e ao contrário do que aconteceu com o exemplo UART, o primeiro bit de cada dado é o mais significativo!


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 “dependurados” 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 myi2c.py

Neste exemplo vamos ligar a placa ESP32 Pico Kit ao mesmo módulo que contem o acelerómetro ADXL345 mas desta vez usando o protocolo I2C, pelo que as ligações a realizar entre a placa e o módulo são agora as seguintes:.

Nestas ligações só não foi necessário incluir as resistências de pull-up nas linhas SDA e SCL porque elas já se encontram presentes no próprio módulo (resistências R3 e R4 do esquemático anteriormente apresentado).

De notar que o pino SDO, segundo a datasheet, pode ser usado para definir um de dois endereços (de 7 bits) I2C possíveis. Caso ele seja ligado a 0V ou fique em vazio (uma vez que o módulo tem aí uma resistência de pull-down) o endereço é o 0x53, mas se se efetuar uma ligação aos 3.3V o endereço I2C passará a ser antes o 0x1D.

Tal como no exemplo anterior, o seguinte programa mede ciclicamente (a cada 100ms) os valores de aceleração fazendo a respetiva escrita, usando desta vez o protocolo I2C:

from utime import sleep_ms
from machine import Pin, I2C

I2C_ADDR = 0x1D

# Registers
# pylint: disable=bad-whitespace
DEVID       = 0x00
POWER_CTL   = 0x2D
DATA_FORMAT = 0x31
DATAX0      = 0x32

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

def toDec(high, low):
  value = low + (high<<8)
  if value > 2**15:
    value -= 2**16
  return value

def printDevices():
  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 writeReg(reg, value):
  return i2c.writeto(I2C_ADDR, bytearray([reg, value]))

def readReg(reg):
  # return i2c.readfrom_mem(I2C_ADDR, DEVID, 1)
  i2c.writeto(I2C_ADDR, bytearray([reg]))
  return i2c.readfrom(I2C_ADDR, 1)

def checkID():
  return readReg(DEVID)[0] == 0xE5

def readData():
  b = i2c.readfrom_mem(I2C_ADDR, DATAX0, 6)
  x = toDec(b[1], b[0])
  y = toDec(b[3], b[2])
  z = toDec(b[5], b[4])
  return [x, y, z]

def initADXL():
  writeReg(DATA_FORMAT, 0x01)
  writeReg(POWER_CTL, 0x08)

printDevices()
if checkID():
  initADXL()
  while 1:
    print(readData())
    sleep_ms(100)

Mais uma vez a troca de dados deverá respeitar as indicações dadas na seguinte figura da datasheet:

Aqui é possível ver que após o envio do endereço I2C, terá que ser enviado o endereço do registo a aceder e só depois se seguem os dados.

A classe MicroPython I2C possui várias funções para escrita e leitura de dados, sendo que em todas elas o primeiro argumento é o endereço de 7 bits I2C do slave. No nosso exemplo foram usadas as seguintes funções:

  • writeto(i2c_addr, data)
  • readfrom(i2c_addr, size)
  • readfrom_mem(i2c_addr, reg_addr, size)

De notar que ainda antes do acesso ao ADXL345, este script chama a função printDevices() que como o nome indica vai imprimir a lista de todos os dispositivos I2C presentes no barramento. Esta função recorre à função scan() que tenta aceder a todos os endereços de 7 bits possíveis criando uma lista com aqueles que fazem o acknowledge a esse acesso.

Na figura seguinte é possível ver a evolução temporal das linhas SCL e SDA quando da leitura do registo DEVID (endereço 0x00) que segundo a datasheet deverá retornar o valor fixo 0xE5:

Tendo em conta que o primeiro bit é o MSB, pode-se verificar em primeiro lugar a realização uma escrita (endereço 0x3A*) do número do registo que se pretende ler (0x00), e seguidamente faz-se uma leitura (endereço 0x3B*) do conteúdo desse registo (0xE5 neste caso).

* Estes endereços de 8 bits resultam do de 7 bits (0x1D) acrescentado à direita (LSB) de um 0 (no caso duma escrita) e de um 1 no caso duma leitura).

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