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.
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:
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:
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
Estou com um sensor medidor de tensão que comunica através do RS 485. Pretendo usar esse sensor para enviar o sinal de tensão lido para o arduino, com a utilização do módulo conversor C25B (sensor->conversor->arduino). E, preciso exibir essa leitura no LCD ligado ao arduino. Isso é possível? Consigo um código base para a implementação?
Olá, Luciano. Não sei te dizer, porque não sei como esse sensor que você comentou funciona. Mas o código do site serve apenas para o Arduino atuando como escravo (respondendo aos comandos).
Olá Fábio. Tens algum documento do Arduino como mestre de rede?
Olá, Cleiton. Infelizmente não.
Testei aqui e funciona perfeitamente. Muito obrigado por sua contribuicao. Agora é entender para implementar as outras funções.
Show de bola, Marcelo! Eu que agradeço pelo comentário.