Para complementar o assunto do post sobre Modbus RTU, vamos ver como implementar o Modbus RTU no Arduino, sendo o Arduino o escravo da comunicação.

Informações básicas

A ideia é utilizar o módulo conversor de TTL para RS-485 mostrado na imagem abaixo.

Para facilitar os testes, também é possível utilizar este simulador ou este. E, vale lembrar que, o simulador comunica diretamente com o Arduino via cabo serial. Portanto, o conversor é totalmente desnecessário neste caso.

De qualquer modo, utilize da forma que você desejar. Você só não pode esquecer de configurar a comunicação dos dois dispositivos. Isso porque os dois precisam comunicar à mesma velocidade (9600 baud no caso dos exemplos). E as configurações de bit de parada/paridade também precisam estar iguais.

Circuito conversor

O circuito que utilizaremos para o caso de implementar com o padrão RS-485 (sem simulador) está mostrado abaixo.

circuito do conversor ttl para rs-485

Basta ligar o RO no Rx do Arduino e o DI no Tx. Os pinos RE e DE devem ser interligados e conectados ao pino 2 do Arduino. E eles serão os responsáveis por definir se o Arduino está recebendo ou transmitindo dados.

Por fim, é só ligar o Vcc no 5V e o GND no GND do Arduino. Os pinos A e B devem ser ligados nos barramentos A e B do RS-485.

Circuito conversor

Nos tópicos adiante, mostro e explico um código que aplica o Modbus. Entretanto, eu cheguei a desenvolver uma biblioteca simplificada e o vídeo abaixo mostra alguns testes que fiz usando ela. Se você for replicar algum código do post, sugiro usar a biblioteca, pois o código dela está mais organizado e contém outros detalhes adicionais.

Recebendo os dados

Observações

A lógica do código é armazenar todos os dados recebidos e reescrevê-los em um display LCD 16×2. Se você tiver alguma dúvida sobre o LCD, leia este post.

Como o conversor TTL para RS-485 está fazendo todo o trabalho complicado, basta ficar lendo o pino serial. Além da leitura, precisamos armazenar os dados recebidos e verificar se houve um “silêncio” de alguns milissegundos. Se sim, o código entra na parte de mostrar a mensagem recebida.

A parte de verificar o período de “silêncio” foi feita com o comando millis() do Arduino. Basicamente, criei uma variável “tempo” que recebe o valor de millis() toda vez que um dado é recebido via serial.

Se nenhum dado é recebido, a variável fica “desatualizada” e a diferença entre ela e o comando millis() aumenta. Portanto, é só verificar se a diferença dos dois é maior que 4 milissegundos (conforme a teoria).

Código completo

Leia o tópico acima e os comentários para entender o código e volte à teoria do Modbus RTU em caso de dúvida.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// Importa a bibilioteca do display
#include <LiquidCrystal.h>
// Cria uma instancia do display com os pinos
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

// Variaveis uteis para o programa
unsigned long tempo = 0; // "Timer" para saber se a msg acabou
String buf = ""; // String que recebe a mensagem
bool recebeu = false; // Booleana para saber se a msg foi iniciada

// Pino RE e DE do módulo RS485 (desnecessário no simulador)
#define RE_DE 2

void setup()
{
  pinMode(RE_DE, OUTPUT); // pino 8 configurado como saída
  digitalWrite(RE_DE, LOW); // Coloca os pinos RE_DE em LOW para receber os dados

  // Inicia a comunicação serial
  Serial.begin(9600);

  // Inicializa o LCD:
  lcd.begin(16, 2);
  lcd.clear();
}


void loop()
{
  // Se algum dado foi recebido
  if(Serial.available()>0){
    // Le o dado e adiciona à String 'buf'
    char recebido = Serial.read();
    buf += recebido;

    // Reseta  o "timer" e indica que a mensagem foi iniciada
    tempo = millis();
    recebeu = true;
  }

  // Se está a 4ms sem receber nada e a mensagem foi iniciada
  if((millis()-tempo > 4) && recebeu){
   
    // Escreve os dados da mensagem em hexadecimal
    lcd.setCursor(0,0);
    for(int i = 0; i<buf.length();i++){
      if(i==6){ // Pula a linha quando i = 6
        lcd.setCursor(0,1);
      }
      lcd.print(uint8_t(buf[i]), HEX);
     
      if(i != 2 && i != buf.length()-2){ // Só cria um espaço se não for o endereço do registrador ou o CRC
        lcd.print(" ");
      }
    }
   
    //// OS DADOS ESTÃO ARMAZENADOS EM:
    // Endereço do escravo: em buf[0]
    // Código da função: buf[1]
    // Endereço do registrador: buf[2] e buf[3]
    //  Para juntar os dois, podemos fazer o seguinte:
    int endereco = (uint8_t(buf[2]) << 8) | uint8_t(buf[3]);
    // Os dados da mensagem vem na sequência (buf[4]...)
    // O CRC está localizado nos ultimos dois bytes: buf[buf.length()-2] e buf[buf.length()-1]


    // Reseta os parâmetros
    buf = ""; // Limpa a mensagem
    recebeu = false; // Reseta a booleana
  }
}

Resultados

Após fazer upload do código, abri o simulador e configurei a mensagem. O resultado pode ser visto na imagem abaixo:

Arduino exibindo mensagem Modbus RTU

Portanto, de acordo com a imagem, temos (lembrando que está tudo em hexadecimal):

  • O endereço do escravo é 1.
  • O código da função é 1 (leitura de saída digital).
  • O endereço do registrador é 0001.
  • Os dados de leitura foram 0008 (lendo 8 saídas).
  • O CRC foi 06CC.

Tudo conforme a configuração que fiz no simulador.

Enviando resposta

Observações

Agora, o código também deverá enviar uma resposta. Entretanto, ele terá a mesma parte de recebimento mostrada acima.

A única modificação será dentro da parte do código de exibir a mensagem recebida. Pois, depois de receber os dados, o Arduino irá enviar a resposta.

Código da função 1

Para exemplificar este processo, escolhi responder à função 1, que é o mestre solicitando o estado das saídas digitais.

De acordo com o Simply Modbus, a solicitação vem no seguinte formato (os dados estão em hexadecimal):

11 01 0013 0025 0E84

Sendo que: 11 é o byte com o endereço do escravo; 01 é o byte com o código de função; 0013 são dois bytes com o endereço do registrador da primeira entrada digital; 0025 são dois bytes que informam a quantidade de entradas que devem ser lidas; 0E84 são dois bytes de CRC.

Desta forma, a resposta deve possuir a seguinte formatação (os dados estão em hexadecimal):

11 01 05 CD6BB20E1B 45E6

Sendo que: 11 é o byte com o endereço do escravo. 01 é o byte com o código de função. 05 é a quantidade de bytes que virão em seguida.

Os bytes que vem em seguida contêm o estado das entradas digitais. Como o requisição solicitou a leitura de 37 entradas (25 em hexadecimal), são necessários 5 bytes (37/8 ~= 5).

O estado da primeira entrada digital (endereço 0013 em hexadecimal) deve ser escrito no primeiro byte no bit menos significativo (CD) e o preenchimento é feito assim em diante. Como o último byte terá bits de sobra, basta preencher eles com 0.

Essa parte ficará mais clara no código.

Função geradora do CRC

A função para calcular o CRC que fiz foi adaptada dos sites que encontrei online. A função abaixo recebe como parâmetro um vetor de variáveis inteiras de 8 bits (uint8_t  msg[]) e também o tamanho do vetor (uint8_t len).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Calcula o CRC: Recebe a mensagem em formato de vetor e o tamanho da mensagem (quantos bytes)
unsigned int CRC16(uint8_t msg[], uint8_t len)
{  
  unsigned int crc = 0xFFFF;
  for (uint8_t pos = 0; pos < len; pos++)  {
    crc ^= msg[pos];    // Faz uma XOR entre o LSByte do CRC com o byte de dados atual
 
    for (uint8_t i = 8; i != 0; i--) {    // Itera sobre cada bit
      if ((crc & 0b1) != 0) {      // Se o LSB for 1:
        crc >>= 1;                  // Desloca para a direita
        crc ^= 0xA001;              // E faz XOR com o polinômio 0xA001 (1010 0000 0000 0001 ): x16 + x15 + x2 + 1
      }else{                      // Senão:
        crc >>= 1;                  // Desloca para a direita
      }
    }
  }

  // O formato retornado já sai invertido (LSByte primeiro que o MSByte)
  return crc;
}

Código completo

Leia os tópicos acima e os comentários para entender o código e volte à teoria do Modbus RTU em caso de dúvida.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Importa a bibilioteca do display
#include <LiquidCrystal.h>
// Configura os pinos do display
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

// Variaveis uteis para o recebimento de mensagem
unsigned long tempo = 0; // "Timer" para saber se a msg acabou
String buf = ""; // String que recebe a mensagem
bool recebeu = false; // Booleana para saber se a msg foi iniciada

// Pino RE e DE do módulo RS485 (desnecessário no simulador)
#define RE_DE 2

// Vetor booleano com o estado das lampadas
bool lampadas[8] = {0,0,0,0,1,1,1,1};

void setup()
{
  pinMode(RE_DE, OUTPUT); // pino 8 configurado como saída
  digitalWrite(RE_DE, LOW); // Coloca os pinos RE_DE em LOW para receber os dados

  // Inicia a comunicação serial
  Serial.begin(9600);

  // Inicializa o LCD:
  lcd.begin(16, 2);
  lcd.clear();
}


void loop()
{
  // Se algum dado foi recebido
  if(Serial.available()>0){
    // Le o dado e adiciona à String 'buf'
    char recebido = Serial.read();
    buf += recebido;

    // Reseta  o "timer" e indica que a mensagem foi iniciada
    tempo = millis();
    recebeu = true;
  }

  // Se está a 4ms sem receber nada e a mensagem foi iniciada
  if((millis()-tempo > 4) && recebeu){
    //--------------------------- RECEBIMENTO ---------------------------------------------
    // Escreve os dados da mensagem em hexadecimal
    lcd.setCursor(0,0);
    for(int i = 0; i<buf.length();i++){
      if(i==6){ // Pula a linha quando i = 6
        lcd.setCursor(0,1);
      }
      lcd.print(uint8_t(buf[i]), HEX);
     
      if(i != 2 && i != buf.length()-2){ // Só cria um espaço se não for o endereço do registrador ou o CRC
        lcd.print(" ");
      }
    }

    // --------------------------- ENVIO ---------------------------
    // Cria um vetor para guardar a mensagem de envio (o tamanho de 15 é arbitrário)
    uint8_t msg[15];
    // Cria uma variável para guardar o tamanho do vetor
    uint8_t tamanho = 3;
    msg[0] = uint8_t(buf[0]); // Endereço SLAVE
    msg[1] = uint8_t(buf[1]); // Codigo da função
    int registrador = (uint8_t(buf[2]) << 8) + uint8_t(buf[3]); // Endereço do registrador
   
    if(msg[1] == 1){ // Se o codigo for de leitura de saída digital

      // Calcula quantas saídas o master quer ler
      int n = (uint8_t(buf[4]) << 8) + uint8_t(buf[5]);
      // Coloca na mensagem quantos bytes de dados serão enviados (quantidade de saidas/8, já que cada saida é um bit e serão 8 saidas por byte)
      msg[2] = int(n/8);

      // Variavel para saber em qual posição do vetor da mensagem gravar os valores das lampadas
      int k = registrador;
      // Forma os bytes de estado das lampadas
      for(uint8_t i = 0; i<msg[2];i++){
        uint8_t dado=0;
        // Forma o byte atual
        for(uint8_t j=0;j<8;j++){
          dado += lampadas[k] << j;
          k++;
        }
        // Armazena o byte de dados na mensagem e soma 1 ao tamanho
        msg[tamanho++] = dado;
      }
      // Por fim, calcula o CRC com base na mensagem e no tamanho dela
      unsigned int crc = CRC16(msg, tamanho);

      digitalWrite(RE_DE, HIGH); // Coloca os pinos em HIGH para enviar os dados

      Serial.write(msg[0]); // Endereço SLAVE
      Serial.write(msg[1]); // Codigo da função
      Serial.write(msg[2]); // Quantidade de bytes de dados
      for(uint8_t i = 3; i< tamanho; i++){
        Serial.write(msg[i]); // Estado das lampadas
      }
      Serial.write(crc&0b11111111); // Envia os 8 LSbits do CRC primeiro
      Serial.write(crc>>8); // Envia os MSbits do CRC depois (são trocados mesmo)
      Serial.flush(); // Aguarda os dados serem enviados

      digitalWrite(RE_DE, LOW); // Coloca os pinos RE_DE em LOW novamente para receber os dados
    }


    // Reseta os parâmetros
    buf = "";
    recebeu = false;
  }
}


// Calcula o CRC: Recebe a mensagem em formato de vetor e o tamanho da mensagem (quantos bytes)
unsigned int CRC16(uint8_t msg[], uint8_t len)
{  
  unsigned int crc = 0xFFFF;
  for (uint8_t pos = 0; pos < len; pos++)  {
    crc ^= msg[pos];    // Faz uma XOR entre o LSByte do CRC com o byte de dados atual
 
    for (uint8_t i = 8; i != 0; i--) {    // Itera sobre cada bit
      if ((crc & 0b1) != 0) {      // Se o LSB for 1:
        crc >>= 1;                  // Desloca para a direita
        crc ^= 0xA001;              // E faz XOR com o polinômio 0xA001 (1010 0000 0000 0001 ): x16 + x15 + x2 + 1
      }else{                      // Senão:
        crc >>= 1;                  // Desloca para a direita
      }
    }
  }

  // O formato retornado já sai invertido (LSByte primeiro que o MSByte)
  return crc;
}

Resultados

Após fazer o upload do código, configurei, no simulador, a mesma mensagem do tópico do recebimento (leitura de 8 saídas digitais). A resposta recebida no simulador pode ser vista na imagem abaixo:

Resultado do simulador modbus

Como pode ser visto na imagem acima, o resultado recebido foi 11110000, sendo os MSbits os endereços mais altos.

Repare que, no caso acima, o endereço do registrador foi 0, sendo que na mensagem de recebimento tinha sido 1.

Considerações finais

A implementação apresentada neste post é apenas um exemplo de como ela pode ser feita. Portanto, utilize a teoria e os exemplos mostrados para criar o seus próprio código adaptado às suas necessidades.

E lembrando que, apesar de eu ter mostrado os resultados no simulador, o mesmo código deve funcionar sem problemas com a plaquinha conversora de TTL para RS-485.

Modbus RTU – O que é e como funciona