Manual

do

Maker

.

com

RS485 USB - Comunicando PC com Arduino

RS485 USB - Comunicando PC com Arduino

RS485 USB

Já há muito tempo que não escrevo sobre RS485. Hoje resolvi escrever novamente, porque atualmente temos adaptadores RS485 USB para comunicar um computador com uma rede RS485, que nada mais é do que uma rede serial de longo alcance. Vou tentar ser bastante específico e detalhista nesse artigo, tenha paciência com a leitura.

RS-232

O RS-232 é um protocolo de comunicação serial de dados binários entre um DTE e um DCE. Durante muitos anos os computadores vinham com essa interface serial padrão, incluindo notebooks, mas com a evolução tecnologica, essa porta de comunicação foi retirada do circuito, o que para mim pelo menos, foi péssimo. Muitos roteadores e switches possuem interface de comunicação por serial e sempre que eu precisei configurar esses dispositivos, tive que utilizar um notebook mais antigo para não ficar carregando adaptadores.

O protocolo RS-232 foi padronizado em 1969; já é um velhinho, por assim dizer. Claro que como um padrão EIA, montes de características são definidas, mas não vamos entrar nesses detalhes aqui porque isso não é um TCC. Mas as informações importantes a se ter em mente são principalmente a velocidade e alcance. A comunicação serial dentro desse protocolo é de 115.200 bauds. Baud é a unidade de medida serial para a mudança de estado do canal de comunicação em 1 segundo. Ainda é comum utilizar a comunicação serial entre o computador e Arduino via USB-Serial em uma velocidade de 9600. Não é necessário utilizar velocidades maiores para fazer prints no terminal serial, mas se fosse troca de dados, haveria de se pesar a velocidade ideal, principalmente pelo fato de que quanto maior a velocidade, maior o ruído, o que pode gerar inconsistência dos dados transferidos.

Com a introdução do protocolo USB-Serial, é possível alcançar velocidades superiores a 115.200bps com menor índice de ruído, mas a questão aqui é outra; até que distância poderia ser feita essa comunicação serial? A resposta é simples, sendo uma distância máxima de 15 metros. Pensando em estabelecimento de comunicação de dispositivos em uma casa ou em uma empresa, é fácil passar dessa distância e hoje sequer considera-se a utilização de comunicação serial pois dispositivos ethernet estão disponíveis para embarcados.

Para ricos detalhes sobre a comunicação serial RS-232, recomendo esse artigo na wikipedia.

Agora um pouco de ethernet.

Padrões ethernet

Minha formação é em redes de computadores, que envolve qualquer tipo de rede de comunicação. E por muitos anos antes de me formar eu já atuava como administrador de redes. Passei por diversos padrões e acredite, não é uma coisa para se ter saudade. Hoje vivemos em um mundo que pode-se chamar de quase "estado da arte", tudo é fácil de configurar, os padrões estão muito bem definidos (apesar de que mesmo no protocolo IPv4 ainda existem deficiências e o IPv6 está em fase de implementação). Para não tornar o assunto tedioso, vou direto ao objeto em falar sobre ethernet. Também há limitação de distância.

Ainda é comum a utilização de dispositivos fast-ethernet, que são os dispositivos de 100Mbps (Megabits por segundo, equivalente a 12.5MegaBytes por segundo, incluindo não só os dados, mas toda a pilha TCP/IP). E nos equipamentos mais novos (e populares), temos as interfaces Gigabit. Em embarcados, temos algumas boards que já comportam essa interface Gigabit, como é o caso da Banana Pi M3. Claro que em um projeto que envolva uma board com Linux embarcado essa pode ser uma das melhores opções, mas quando se está utilizando esse nível de implementação, praticamente não há limites dos recursos a serem utilizados; você pode ter servidor web, comunicação ssh, sistemas de arquivos em rede (NFS ou SAMBA, por exemplo) e outras coisas mais. Já pensando em microcontroladores, a coisa fica bem mais limitada, apesar de ainda assim ser viável a utilização de redes ethernet, exceto casos específicos, onde a segurança é a prioridade máxima e onde a distância entre os dispositivos sejam maiores que, digamos, 300 metros para simplificar a limitação de distância da rede ethernet. Como exemplo, cito meu ilustríssimo amigo Marcelo Barros, que em uma das empresas em que trabalhou prestava serviço para a Petrobras e certa vez implementou e suportou a comunicação dos dispositivos que controlam as válvulas de pressão das plataformas. A comunicação entre esses dispositivos? Serial, claro. E talvez agora você questione: "Mas o alcance não é de apenas 15 metros"? Bem, é aí que entra o RS485.

Redes RS485

O RS485 (assim como praticamente todas as redes de comunicação bi-direcional, até onde eu consiga imaginar) é outro protocolo de comunicação serial. Mas o RS485 implementa outros padrões de comunicação, porque ele não é um protocolo entre duas máquinas apenas. Com RS485, vocẽ pode ter uma rede de dispositivos 2-wire. Com taxas de dados de 10Mbps, a distância suportada é de até 1.200 metros. Existem muitas implicações relacionadas, mas não vou entrar em detalhes. Apenas, se for necessário expandir ainda mais, mas ter um segundo controlador na ponta dessa rede e ele poderá mais uma vez repassar a comunicação adiante, de forma que não haja limitação quanto a distância, e isso não requer equipamentos especialistas. Com algumas moedas você já poderá implementar a expansão da rede e o melhor, por ser uma rede isolada sem interação humana, a segurança é muito maior.

Na definição do padrão, a rede funciona como master-slave, onde o master dispara o evento, todos os dispositivos recebem o evento, mas apenas o dispositivo  a qual a mensagem pertence responde a esse evento. É possível utilizar RS485 full-duplex, mas nunca fiz tal implementação, por isso não pretendo me arriscar a falar a respeito.

Eu escrevi um protocolo bastante satisfatório para uma empresa em que trabalhei e disponibilize o código (mas não havia um dungle RS485 USB nessa época) nesse outro artigo sobre RS485 com PIC16F690.

Meu primeiro teste de RS485 entre Arduino está nesse link. Um exemplo mais simples de comunicação feito também com PIC, pode ser visto nesse outro post.

Para concluir essa parte do artigo - o RS485 é amplamente utilizado em redes industriais e é uma boa opção para automação residencial no que diz respeito à segurança, pois não tem as fragilidades de uma rede WiFi nem os riscos de uma rede ethernet.

Comunicação RS485 entre computador e Arduino

Para o Arduino, utilizaremos esse pequeno módulo RS485 e para o Computador, utilizaremos esse adaptador USB RS485, ambos disponíveis na AutoCore Robótica. Vale salientar que do lado oposto ao Arduino, você pode utilizar um computador pessoal X86 ou qualquer placa ARM (Banana  Pi, Raspberry Pi, Orange Pi etc) ou MIPS (VoCore, Carambola, Onion Omega etc) que tenha uma USB disponível. Se precisar comunicar com outras arquiteturas como  Tensilica (ESP8266), também é fácl de implementar, porém já não mais com o adaptador RS485 USB.

Wiring RS485 com Arduino

modulo_rs485-arduino.webp

Nos artigos relacionados supracitados eu entrei em detalhes sobre cada um dos pinos do MAX487, se quiser entender mais a fundo essa comunicação, sugiro a leitura. Vou obrigatoriamente citar o pinout do MAX487 para que você tenha facilidade de identificar o wiring no módulo para Arduino.

Repare no modulo. Do lado oposto ao borne, estão dispostos os pinos a seguir:

RO - Receiver Out Conectar ao RX do Arduino
RE - Receiver Enable Colocar em LOW para receber dados
DE - Driver EnableHIGH para sinalizar envio de dados
DI - Driver InColocar ao TX do Arduino

Do lado do borne estão dispostos VCC, A, B e GND. Estamos trabalhando com o nível lógico de 5V, portanto alimente-o com 5V do Arduino. "A" deve ser conectado a "A" do do RS485 USB, assim como "B" deve ser conectado a "B" do RS485 USB.

Comunicação e regra

Existem duas possibilidades de controle dessa comunicação.

Regra

A regra é basicamente a seguinte; apenas 1 dispositivo por vez deve enviar dados na rede. Por isso que para controlar o fluxo da comunicação será fundamental que um dos dispositivos seja o mestre, que sempre iniciará a comunicação e os demais dispositivos da rede (até 32 dispositivos escravos) só enviarão dados quando solicitado pelo master. Somente o dispositivo solicitado pelo master se anunciará na rede.

Comunicação

Considerando que os slaves não terão autonomia para fazer a comunicação sem que essa seja requisitada pelo master, a comunicação necessitará ser averiguada constantemente. Uma das formas de fazer isso é com polling e a outra, com interrupção.

Polling

Em um loop constante, o Arduino verifica quando requisitado pelo master, validando o buffer na UART. Isto é, o master envia a mensagem na rede, todos os slaves recebem e fazem o parsing da mensagem recebida para validar se a mensagem lhe pertence. O ruim disso é que uma parte do tempo de processamento da MCU será dedicada a fazer esse polling entre as demais tarefas que lhe forem atribuidas, como por exemplo, analisar sensores.

Interrupção

Essa é sem dúvida a melhor opção, pois a MCU trabalhará livremente em sua rotina e quando o master enviar algum dado na rede, a MCU pára o que estiver fazendo para atender a requisição.

Repare que do lado interno a comunicação é UART, portanto a leitura do Arduino será transparente, sem nenhum código especial na leitura dos dados advindos por RS485; o MAX487 é apenas uma ponte transparente nessa rede. Do mesmo modo no computador, nada além de uma comunicação serial. O único inconveniente para você que usa Windows é em relação ao controlador UART, que dentro desse adaptador RS485 USB é o CH341, cujo driver precisa ser instalado. Já para Linux, isso é transparente, o suporte é nativo do kernel.

Código para o Arduino

Primeiro, vamos precisar chavear o estado do MAX487 entre enviar e receber. Para isso, utilizaremos um boolean que permitirá saber a condição atual. Esse boolean chamaremos de SEND. Quando true, coloca o MAX487 em modo de envio de dados. De outro modo, coloca o MAX487 em modo de recepção. Esse estado mudará conforme o resultado da leitura do buffer da UART, que podemos tratar com polling ou interrupção.

Para produzir esse exemplo, estou utilizando o Arduino Mega porque ele tem quatro seriais; a principal e a que se comunica com o computador e a outra (Serial1.begin() no código), é a que se comunicará com o módulo RS485 conectado a ele. Essa serial se refere aos pinos 19 e 18. Outro detalhe importante é que SerialEvent não funciona com o Arduino Leonardo, mas tem um jeito limpo de tratar interrupções nos pinos UART. Para isso, deixe-me discorrer um pouco sobre controle de interrupções no Arduino.

Interrupções em qualquer pino do Arduino

Todos os pinos do Arduino podem tratar interrupções, inclusive os pinos analógicos. Isso você pode conferir no artigo do dispenser de água, onde manipulei interrupções nos pinos analógicos para controlar o encoder rotativo. Mas no artigo do dispenser de água eu não expliquei esse controle de interrupções que vão além dos pinos 2 e 3 padrão.

As interrupções podem ser ativadas para cada pino individualmente. No caso do dispenser de água, ativei a interrupção nos pinos A0 e A1. Porém existem apenas 3 vetores de interrupção, de modo que conjuntos de pinos compartilharão da mesma rotina de tratamento e caberá a você gerenciar a origem da interrupção. Existem diversas formas de tratar isso, nenhuma é complicada, mas como estamos simplesmente fazendo uma prova de conceito com RS485 e o RS485 USB, não teremos problemas com interrupções indevidas.

As interrupções controladas pela ISR, onde você passa como parâmetro o vetor a ser tratado. No projeto de dispenser de água utilizei a ISR(PCINT1_vect) para habilitar as interrupções nos pinos analógicos e sem manipular mais nada, utilizei as interrupções dos pinos digitais 2 e 3, que são padrão. Para habilitar a interrupção nos pinos 0 e 1, devemos utilizar  a ISR(PCINT2_vect), que habilitará interrupção dos pinos D0 ao D7. Apenas para deixar como referência, as ISRs possíveis são:

ISR(PCINT0_vect) Habilita as interrupções dos pinos D8 ao D13
ISR(PCINT1_vect) Habilita as interrupções nos pinos analógicos
ISR(PCINT2_vect)Habilita as interrupções dos pinos D0 ao D7

Essas interrupções disparam na mudança de estado dos pinos. E já que estamos tratando de tantos detalhes de interrupção, vamos ver agora uma rotina de tratamento. A lógica é a seguinte; quando uma interrupção ocorre, a MCU para de fazer o que estiver fazendo no loop() e atende a interrupção. A primeira tarefa dentro da rotina de interrupção e desligar as interrupções para que não haja um novo evento que sobreponha a interrupção que está sendo tratada. Isto é, enquanto a interrupção atual não for tratada, outras não deverão surgir. O próximo passo dentro dessa rotina é tratar o evento, preferencialmente ajustando uma flag para que o processamento seja feito no loop e não dentro da ISR. Em seguida, a flag das interrupções deve ser limpa para que a ISR não entre em loop, considerando que um novo evento ocorreu. Por fim, coloca a MCU de volta à sua rotina, a partir da última instrução executada antes da ocorrência da interrupção. Escrevendo parece muita coisa, mas tudo isso que escrevi se resume nisso:

ISR(PCINT2_vect){
  noInterrupts(); //desabilita interrupções para fazer o tratamento
  SEND = true; //muda a flag para envio de dados
  cli(); //limpa flag de interrupções
  interrupts(); //reativa interrupções
  sei(); //volta ao ponto da última instrução antes da interrupção ter acontecido
}

Quando se utiliza ISR, não deve-se contar com o delay porque ele também usa interrupção e ele não funcionará durante o tratamento de uma interrupção; em suma, o ambiente do Arduino não é reentrante. A forma correta de utilizar ISR é manipulando variáveis globais, isto é, você tem que criar flags a serem manipuladas no programa principal, nunca trave uma ISR. As variáveis que serão utilizadas dentro de uma ISR compartilhadas com o programa principal devem ser declaradas como volatile:

volatile bool SEND = false;

Quando usando attachInterrupt, os estados monitorados podem ser CHANGE, FALLING, HIGH, LOW e RISING:

CHANGEDisparada quando um pino muda seu valor, não importando o estado atual.
FALLINGGera a interrupção quando o pino vai de HIGH para LOW.
HIGHInterrupção gerada quando o pino muda seu estado para HIGH.
LOWGatilho para quando o pino mudou para  LOW.
RISINGInterrompe quando o pino está muda para o estado de LOW para HIGH.

Porém, um passo mais deve ser incluido na ISR. Se você testar como disposto acima, funcionará, mas continuará interrompendo a cada bit recebido na interrupção que está sendo tratada. Para que isso não ocorra, disconecte a interrupção, depois reconecte-a no tratamento da flag dentro do código principal. O ISR() e o loop() ficariam assim:

ISR(PCINT2_vect){
  detachInterrupt(); //desconecta a interrupção
  noInterrupts(); //desabilita interrupções para fazer o tratamento
  SEND = true; //muda a flag para envio de dados
  cli(); //limpa flag de interrupções
  interrupts(); //reativa interrupções
  sei(); //volta ao ponto da última instrução antes da interrupção ter acontecido
}

E no loop:

void loop() {
  // put your main code here, to run repeatedly:
  if (SEND){
    Serial.println("Hello World");
    attachInterrupt(digitalPinToInterrupt(0),ISR,FALLING);
  }

}

Claro, quando o Arduino é iniciado, fundamentalmente deve-se habilitar essa mesma interrupção. Para isso (duas seriais porque estou utilizando o Arduino Mega):

void setup() {
  Serial.begin(9600); //velocidade da comunicação serial
  Serial1.begin(9600);
  attachInterrupt(digitalPinToInterrupt(0),ISR,FALLING);
}

Agora talvez você se aborreça comigo. O tratamento de dados na serial não precisa ser feita dessa forma, você pode simplesmente utilizar Serial1.available():

void loop() {
  // put your main code here, to run repeatedly:
  if (Serial.available()){
    Serial.println("Hello World");
  }

}

Mas de que outro modo eu poderia abordar tanto sobre interrupções sem um tema específico? Por isso, apesar de não ser necessário implementar o ISR nesse código, se não conhecia a ISR, passou a conhecer agora, hum?

Pra finalizar, você pode utilizar opcionalmente o Arduino UNO (ou outro) com softwareSerial, caso deseje ter duas seriais disponíveis. Já no Arduino Mega você tem 4 hardwareSerial disponíveis e como nunca fiz um artigo utilizando o Arduino Mega, escolhi ele dessa vez para estrelar o artigo. Nesse caso, podemos simplesmente utilizar o serialEvent(), que é automaticamente chamado dentro do loop, bastando fazer sua redeclaração com o respectivo tratamento.

Antes de colocar o código, gostaria de chamar sua atenção para alguns detalhes importantes. O primeiro é que você deve identificar a serial pela qual está se comunicando com o Arduino. A segunda, identificar qual a serial que seu computador identificou como sendo o RS485 USB. Se estiver utilizando o Arduino Mega também, fica fácil, pois a porta serial dele é a ACM0, enquanto do RS485 USB é a porta ttyUSB0. Por fim, você precisará abrir um programa de comunicação serial para abrir a porta referente ao RS485 USB; não adianta digitar nada na porta ACM0 abrindo o monitor serial porque não é lá que você passa os dados através do RS485, lembre-se que o RS485 USB está em outra porta USB que não a do Arduino Mega (óbvio?). No video isso fica claro, porque a lógica é a seguinte:

  • Abrir o terminal serial na IDE do Arduino para ver a resposta do que vem pelo RS485 USB.
  • Abrir um programa serial (CuteCom é ótimo) para enviar dados ao Arduino pelo RS485.
  • Digitar alguma coisa no programa serial (já falei que o CuteCom é ótimo?).
  • Ler a resposta no terminal serial da IDE do Arduino.

Estou detalhando isso porque eu já escrevi vários artigos sobre RS485, mas eu mesmo me confundi e comecei digitar no monitor serial e fiquei aguardando resposta, sendo que o monitor serial só servirá aqui para exibir o que veio pelo RS485 USB até o módulo RS485 conectado à serial1. Vamos ao código, mas só quero deixar mais uma informação importante; eu nem estou limpando a string após exibir o conteúdo. Desse modo, se a próxima mensagem for menor que a primeira, você verá alguns Bytes da mensagem anterior ao final da string. Além disso, fiz um buffer de 10 Bytes e não estou checando se a mensagem entrante cabe nessa reserva, logo, é mais uma razão para obter bugs. Em suma, esse código é só para prova de conceito mesmo.

#define MSG_LEN 10
#define RE_PIN 8
#define DE_PIN 9

char incoming[10] = {0};
String buf;
String parsed_message = "";
int start_byte = 0;
int end_byte   = 0;

void setup(){
  pinMode(RE_PIN,OUTPUT);
  pinMode(DE_PIN,OUTPUT);
  //apenas para teste, ja preparado para receber dados
  digitalWrite(RE_PIN,LOW);
  digitalWrite(DE_PIN,LOW);
  Serial.begin(9600);
  Serial1.begin(9600);
}

void serialEvent1(){
  Serial1.readBytes(incoming,MSG_LEN);
  buf = incoming;
  if (buf.length() > 1){
    Serial.println(incoming);
  }

  for (int i=0;i<buf.length();i++){
    if (incoming[i] == '['){
      start_byte = i+1;
    }
    if (incoming[i] = ']'){
      end_byte = i-1;
      break;
    }
  }
  //se a mensagem nao tem fim, esta mal formatada
  if (end_byte == 0){
    start_byte = 0;
    return;
  }
  parsed_message = buf.substring(start_byte,end_byte);
  Serial.println(parsed_message);
}

void loop(){

}

Como você pode notar, nada é chamado no loop. O brevíssimo video apenas para mostrar as coisas acontecendo:

https://youtu.be/o1MVtwyND5o

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.