UART

Comunicação série

Vamos agora apresentar as funções da biblioteca Arduino que permitirão tirar partido das funcionalidades de comunicação série com o microcontrolador Pic32 presente nas placas ChipKit Uno32. Concretamente nesta página iremos descrever o protocolo UART, sendo que outros protocolos série como o I2C e SPI serão descritos noutra página.

A comunicação série permite trocar dados (usualmente bytes) enviando os seus bits não de forma simultânea (paralela), o que obrigaria a ter várias linhas, mas sim bit após bit ao longo do tempo (de forma série portanto). Desta forma minimiza-se o números de linhas (e portanto pinos) necessários à sua implementação.

par_ser

UART

Como indicia o nome do protocolo UART (Universal Asynchronous Receiver/Transmitter) a comunicação é assíncrona, i.e. sem qualquer sinal de relógio. Os bits são enviados/recebidos a uma dada cadência acordada entre emissor e recetor a que se chama baudrate (medida em bits por segundo).

Os bits circulam por duas linhas (TX e RX, respetivamente de transmissão e receção) o que permite uma comunicação simultânea nos dois sentidos (full duplex portanto).

rx_tx

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 ao nível lógico 0, e terminados por um ou mais Stop Bits ao nível lógico 1. Os bits de dados (em geral 8) são enviados do menos para o mais significativo. Antes do envio dos Stop Bits (usualmente um) pode ainda ser enviado um bit de paridade: par (Even) ou ímpar (Odd), destinado à deteção de eventuais erros na comunicação.

A imagem seguinte mostra o exemplo do envio dum byte igual a 0x4E (0b01001110). Cada bit demora um pouco mais de 1 divisão (100μs) resultante do baudrate igual a 9600 bit/s usados na comunicação. Os 8 bits de dados são enviados do LSB para o MSB, sendo precedidos pelo Start bit:

uart_wave

No caso da placa ChipKit Uno32, os pinos da interface necessários à implementação do protocolo UART são os pinos 0 (RX) e 1 (TX) sendo que estes já se encontram conetados ao circuito integrado FT232 da FTDI que os liga ao porto USB da placa, permitindo assim a comunicação da placa com o computador a que ela está ligada via USB.

Como se pode ver no esquemático da placa ChipKIT UNO32, a presença das resistências R1 e R2 permite a ligação aos pinos 0 e 1 um outros dispositivo série sem qualquer conflito elétrico.

rxtx

Projeto serialtest

No projeto serialtest vamos apresentar alguns dos métodos do objecto Serial, que implementa a comunicação série com a placa ChipKit Uno32:

#include <Arduino.h>

int n = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.print("Hello World number ");
  Serial.println(++n);
  delay(1000);
}

Para que os pinos 0 e 1 sejam usados para implementar a comunicação UART, e não como simples portas digitais I/O, será necessário usar o método begin para iniciar a configuração indicando o respetivo baudrate (neste exemplo 9600 bit/s).

A transmissão de dados pode ser feita pelos métodos print ou println. Neste último caso, a transmissão é terminada com o envio dos caracteres “carriage return” (‘\r’, ASCII = 13) e “newline” (‘\n’, ASCII 10).

Serial monitor

Uma vez programada a placa é possível receber as mensagens do lado do computador ao qual ela está ligada, abrindo um terminal série como o PuTTY.

O resultado será o seguinte:

Hello World number 1
Hello World number 2
Hello World number 3
Hello World number 4
Hello World number 5

IMPORTANTE:

  • De notar que devido à forma como a placa ChipKIT foi implementada, sempre que se estabelece uma comunicação série com ela é feito um Reset ao PIC32 o que leva a que o seu programa recomece a sua execução. Isso é contudo precedido pelo arranque do bootloader (sinalizado pelo piscar do LED4 durante cerca de 5 segundos)!
  • O uso do PuTTY impede a utilização da porta série por outras aplicações (nomeadamente o PlatformIO quando duma operação de Upload). É pois necessário fechar o PuTTY sempre que se pretende reprogramar o PIC32!

write

No próximo exemplo são apresentadas outras formas de envio, nomeadamente a que usa o método write.

#include <Arduino.h>

int n = 65; // ASCII of character A
const char arr[5] = {'X', 13, 10, 'Y', 'Z'};

void setup() {
  Serial.begin(9600);
  Serial.println("Serial test");
}

void loop() {
  Serial.println(n);
  Serial.println(n, BIN);
  Serial.println(n, OCT);
  Serial.println(n, HEX);
  Serial.write(n);
  Serial.write(arr+1, 2);
  Serial.write("------\r\n");
  n++;
  delay(3000);
}

O resultado será o seguinte:

65
1000001
101
41
A
------
66
1000010
102
42
B
------

printf

Para enviar dados pela porta série há ainda a possibilidade de se usar o método printf já conhecido da linguagem C:

#include <Arduino.h>

int n = 65; // ASCII of character A
const char arr[5] = {'X', 13, 10, 'Y', 'Z'};

void setup() {
  Serial.begin(9600);
  Serial.println("Serial test");
}

void loop() {
  Serial.println(n);
  Serial.println(n, BIN);
  Serial.println(n, OCT);
  Serial.println(n, HEX);
  Serial.write(n);
  Serial.write(arr+1, 2);
  Serial.printf("O caracter '%c' tem o codigo ASCII %d (%02Xh)\r\n", n, n, n);
  Serial.write("------\r\n");
  n++;
  delay(3000);
}

O resultado seria agora este:

65
1000001
101
41
A
O caracter 'A' tem o codigo ASCII 65 (41h)
------
66
1000010
102
42
B
O caracter 'B' tem o codigo ASCII 66 (42h)
------

De notar que o método printf, embora bastante versátil, tem um peso significativo na memória de programa necessária. Por exemplo as duas últimas versões do programa, apesar de só diferirem numa linha de código onde é usado o método printf, apresentam dimensões bastante diferentes (14056 e 30620 bytes respetivamente)!

Memory Usage -> http://bit.ly/pio-memory-usage
DATA:    [====      ]  38.2% (used 6260 bytes from 16384 bytes)
PROGRAM: [=         ]  11.1% (used 14056 bytes from 126976 bytes)

Memory Usage -> http://bit.ly/pio-memory-usage
DATA:    [====      ]  38.2% (used 6260 bytes from 16384 bytes)
PROGRAM: [==        ]  24.1% (used 30620 bytes from 126976 bytes)

É claro que este acréscimo de mais de 100% na memória de programa acontece apenas no primeiro uso da função printf (devido à inclusão de todo o código por ela requerido). O uso de posteriores printfs originariam acréscimos adicionais diminutos.

Comportamento (não) bloqueante

É importante ter em conta que o envio duma mensagem pela porta série pode demorar um tempo significativo especialmente se o número de caracteres for elevado e/ou o baudrate for baixo. Por exemplo, caso o baudrate seja 9600, o envio de 50 caracteres demora cerca de 48ms.

Segundo a documentação da biblioteca Arduino, a implementação dos métodos print não bloqueia o programa, i.e. o processo de transmissão é iniciado e imediatamente começa-se a executar a instrução seguinte, a menos que se usasse o método flush que faria esperar o fim da transmissão. Com efeito nesta página é possível ler:

Programming Tips
As of version 1.0, serial transmission is asynchronous; Serial.print() will return before any characters are transmitted.

Se assim fosse o seguinte programa por cada ciclo loop, geraria dois impulsos no LED da placa: um rápido seguido dum mais demorado em virtude do uso do flush:

#include <Arduino.h>

#define LED 13

const char* str = "This is a string with a total of <50> characters";

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  digitalWrite(LED, 0);
}

void loop() {
  digitalWrite(LED, 1);
  Serial.println(str);
  digitalWrite(LED, 0);
  delay(1000);

  digitalWrite(LED, 1);
  Serial.println(str);
  Serial.flush();
  digitalWrite(LED, 0);
  delay(1000);
}

Contudo no caso da biblioteca para as placas ChipKit Uno32 tal não acontece (os impulsos são ambos longos), o que mostra que a implementação dos métodos print para os PIC32 são de facto bloqueantes. Com efeito a onda gerada no sinal do pino 13 é a seguinte:

blocking

Serve este exemplo para alertar que quem portou a biblioteca Arduino para as placas ChipKit não o fez exatamente da forma como essa biblioteca está descrita, situação que poderá acontecer noutros casos!!!

Para minimizar o tempo de bloqueio podemos usar baudrates mais elevados como 115200.

Outras portas série

As placas ChipKIT possuem uma segunda porta série situada nos pinos 39 (RX) e 40 (TX), bastando para o seu uso invocar antes o objeto Serial1 em vez do Serial que temos usado nestes exemplos.

Existe ainda a possibilidade de ser usar outras portas série, mas que são implementadas em software ao contrário das atrás descritas que são implementadas em hardware. Para isso deve-se incluir no nosso projeto a biblioteca SoftwareSerial. Nestes casos o baudrate é em geral limitado a um valor mais baixo.

Receção de dados

A leitura de dados pela porta série é feita pelo método read, sendo contudo preciso primeiro verificar que já chegaram dados através do método available. O seguinte programa imprime todos os caracteres digitados na janela “Serial monitor” no computador, e seus códigos ASCII.

#include <Arduino.h>

void setup() {
  Serial.begin(115200);
  Serial.println("Prima uma tecla...");
}

void loop() {
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    if (ch >= ' ')
      Serial.print(ch);
    Serial.print(' ');
    Serial.println(int(ch));
    if (ch == 8)
      Serial.end();
  }
}

De notar que os caracteres com código ASCII abaixo de 32 (espaço) não são imprimíveis pelo que nesse caso só se imprime o respetivo código ASCII. É o caso por exemplo do carácter backspace (ASCII = 8), que neste programa tem a função adicional de terminar o uso da comunicação série (voltando a definir os pinos RX e TX como simples portas I/O). Para isso é usado o método end.

Projeto butledserial

O seguinte código implementa um programa capaz de controlar um LED e detetar o estado dum botão, ambos exteriores à placa segundo o seguinte esquemático:

butled

Para controlar o LED bastará a partir da janela “Serial Monitor” digitar os comandos:

  • l 1<enter> – liga o LED
  • l 0<enter> – desliga o LED

Para receber o estado do botão (b 1 para premido e b 0 para não premido) bastará enviar o comando b<enter>. Estas mensagens de estado são igualmente enviadas pela placa sempre que o estado do botão se alterar, sendo acompanhadas pelo instante (em milissegundos) em que ocorreram:

#include <Arduino.h>

#define LED 7
#define BUT 6
bool lastbut = false;

#define BUFSIZE 10
char cmd[BUFSIZE];
int pos = 0;

void setup() {
  Serial.begin(115200);
  Serial.println("Introduza um comando seguido de <enter>");
  pinMode(LED, OUTPUT);
  pinMode(BUT, INPUT);
}

void loop() {
  char ch;
  if (Serial.available()) {
    ch = Serial.read();
    Serial.write(ch);
    if (ch == '\r') {
      Serial.write('\n');
      switch (cmd[0]) {
        case 'l':
          digitalWrite(LED, cmd[2] != '0');
          break;
        case 'b':
          Serial.print("b ");
          Serial.println(!digitalRead(BUT));
          break;
      }
      pos = 0;
    } else
      if (pos < BUFSIZE)
        cmd[pos++] = ch;
  }

  bool but = digitalRead(BUT);
  if (but != lastbut) {
    lastbut = but;
    Serial.print(millis());
    Serial.print(" b ");
    Serial.println(!but);
  }
}

ToDo

Tendo por base o projeto butledserial:

  1. analise o seu código fonte que implementa as funcionalidades do programa
  2. adicione um novo comando v<enter> que imprime uma mensagem identificadora do projeto
  3. altere o programa de forma a permitir o uso da tecla backspace (ASCII 8) na escrita dos comandos
  4. adicione o comando nn mm<enter> que faz piscar o LED4 (pino 13 da placa ChipKit) nn vezes com um período de mm milissegundos

Biblioteca SerialCommand

Como se constatou no projeto anterior o parsing (análise) das mensagens série recebidas nem sempre é trivial pelo que será preferível usar uma biblioteca com esse fim como é o caso da SerialCommand.

Para instalar esta biblioteca, a exemplo do que aconteceu com outras, basta executar o seguinte comando num janela de terminal (onde 173 é o identificador da biblioteca):

pio lib install 173

Com esta biblioteca o projeto butledserial original ficaria simplesmente assim:

#include <Arduino.h>
#include <SerialCommand.h>

SerialCommand sCmd;

#define LED 7
#define BUT 6
bool lastbut = false;

void led() {
  char *arg;
  arg = sCmd.next();
  digitalWrite(LED, atoi(arg));
}

void button() {
  Serial.print("b ");
  Serial.println(!digitalRead(BUT));
}

void unrecognized(const char *command) {
  Serial.println("???");
}

void setup() {
  Serial.begin(115200);
  Serial.println("Introduza um comando seguido de <enter>");
  pinMode(LED, OUTPUT);
  pinMode(BUT, INPUT);

  sCmd.addCommand("l", led);
  sCmd.addCommand("b", button);
  sCmd.setDefaultHandler(unrecognized);
}

void loop() {
  sCmd.readSerial();

  bool but = digitalRead(BUT);
  if (but != lastbut) {
    lastbut = but;
    Serial.print(millis());
    Serial.print(" b ");
    Serial.println(!but);
  }
}

Há contudo que fazer algumas alterações:

  • Para que o carácter que termina um comando seja o \r (enter) em vez do \n, alterar a linha 33 do ficheiro SerialCommand.cpp para:
      term('\r');
  • Para que os caracteres enviados sejam devolvidos de volta de forma a serem impressos, adicionar após a linha 75:
      Serial.write(inChar);
      if (inChar == '\r')
        Serial.write('\n');

ToDo

  1. Tire partido da biblioteca TaskScheduler (#id 721) , para implementar o comando p descrito na secção ToDo anterior.
  2. Confirme se a biblioteca SerialCommand admite o uso da tecla backspace (ASCII = 8) e se não for o caso altere o seu código para que tal ocorra.