Na disciplina de Fundamentos de Sistemas Embarcados, o primeiro trabalho do semestre é um simulador de elevadores. Uma ESP32 faz o papel do prédio inteiro — três cabines, uma fila de chamadas, um barramento de energia — e a Raspberry Pi tem de controlá-lo. Na segunda entrega, o desafio era fazer as duas conversarem pela porta serial: primeiro com um protocolo simples, depois com MODBUS sobre UART, lendo e escrevendo os registradores reais do simulador.

O código estava pronto e com todos os testes unitários passando. Mesmo assim, foi na bancada, com a placa de verdade e o dashboard aberto ao lado, que a maior parte do aprendizado aconteceu. Este artigo junta o que eu precisei entender para explicar (e gravar) essa entrega: o que cada byte de uma trama faz, por que o CRC não era o dos tutoriais, o que é um watchdog e como o I2C fecha a cadeia.
Um fio, dois protocolos
A primeira confusão é de vocabulário. UART não é um protocolo de mensagens: é o canal. É a porta serial (/dev/serial0, a 115200 bps, 8 bits, sem paridade, 1 bit de parada) que só sabe transportar bytes de um lado para o outro.
Em cima desse mesmo fio rodam dois protocolos de mensagem, e a ESP32 entende os dois ao mesmo tempo:
Raspberry Pi ──── UART (fio serial) ──── ESP32
│
├─ Protocolo simplificado → comando + dado + matrícula
└─ MODBUS modificado → endereço + função + dados + matrícula + CRC
Raspberry Pi ──── I2C ──── sensor BMP280 (outro fio, outro dispositivo)
Além disso, existe um terceiro barramento, o I2C, que não leva à ESP32: ele liga a Raspberry a um sensor de temperatura e pressão. Vamos chegar nele no fim.
Do protocolo simplificado ao MODBUS sobre UART

O protocolo simplificado é o mais enxuto possível. Para pedir um inteiro, a Raspberry manda o byte de comando seguido dos seis últimos dígitos da matrícula do aluno — cada dígito como byte cru (o dígito 6 vira 0x06, e não o caractere ASCII 0x36). Com a matrícula de exemplo do enunciado, 654321:
Simplificado: A1 │ 06 05 04 03 02 01 (7 bytes)
cmd │ matrícula
MODBUS: 01 │ 23 │ A1 │ 06 05 04 03 02 01 │ CRC CRC (11 bytes)
end. │ fun│ sub│ matrícula │ verificação
O MODBUS pega a mesma mensagem e acrescenta três respostas que o simplificado não dá: para quem é, o que fazer e chegou inteiro?
Endereço, função e sub-código
O endereço diz qual dispositivo deve responder. No simplificado não existe endereço: quem estiver ouvindo responde. No MODBUS, vários dispositivos dividem o mesmo fio, e só o endereçado fala. A resposta volta com o mesmo endereço no primeiro byte, o que confirma quem respondeu.
Curiosamente, todos esses "dispositivos" moram dentro da mesma ESP32, que finge ser vários aparelhos:
| Endereço | O que é | Para que serve |
|---|---|---|
0x01 |
dispositivo didático | devolve valores fixos; existe só para validar o formato das mensagens |
0x11, 0x12, 0x13 |
cabines 1, 2 e 3 | andar, nivelamento, porta, corrente, carga, posição e falhas |
0x20 |
controlador do prédio | fila de chamadas, barramento de potência, watchdog, métricas |
A função diz o tipo de operação. No dispositivo didático, 0x23 significa "me dê um dado" e 0x16, "receba este dado". O sub-código que vem depois (A1, A2, A3, B1, B2, B3) é exatamente o comando do protocolo simplificado: diz se o dado é inteiro, float ou string. Ou seja, o byte único do simplificado foi dividido em dois.
Por isso a parte de MODBUS da entrega só conversa com o 0x01: ela valida o envelope (endereço, função, CRC) com respostas previsíveis antes de tocar no simulador de verdade. Um detalhe bonito: nos comandos de envio, a ESP32 devolve o valor recebido multiplicado pelo último dígito da matrícula. Se a resposta bate, você sabe que ela leu a sua matrícula como byte cru — em ASCII, ela teria multiplicado por um número completamente diferente.
CRC-16: a pegadinha do valor inicial
O CRC-16 são os dois últimos bytes da trama: uma soma de verificação calculada sobre todos os bytes anteriores. Quem recebe recalcula; se não bater, algum byte se corrompeu no caminho e a mensagem é descartada. Do lado da Raspberry, uma resposta com CRC errado vira uma nova tentativa (até três). O protocolo simplificado não tem nada disso — um byte trocado passa despercebido.
A pegadinha: o CRC deste trabalho não é o CRC-16/MODBUS que aparece em todo tutorial. O polinômio é o mesmo (0xA001, na forma refletida), mas o valor inicial é zero, e não 0xFFFF. Isso só ficou claro conferindo o cálculo contra os exemplos resolvidos do enunciado — e vale a lição: quando a especificação traz exemplos com os bytes de verificação prontos, eles são o gabarito.
/* CRC-16 com polinômio 0xA001 refletido e valor inicial 0x0000.
* O CRC-16/MODBUS dos tutoriais começa em 0xFFFF e NÃO fecha com os exemplos. */
uint16_t crc16(const uint8_t *dados, size_t n)
{
uint16_t crc = 0x0000;
for (size_t i = 0; i < n; i++) {
crc ^= dados[i];
for (int bit = 0; bit < 8; bit++)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
return crc; /* vai na trama em little-endian: byte baixo primeiro */
}
Com esse código, a leitura dos 9 registradores da cabine 1 do enunciado (11 03 00 00 09 00 06 05 04 03 02 01) dá 0xE40E, que viaja como 0E E4 — exatamente o que o exemplo mostra.
Registradores: onde o simulador guarda o estado
Com o envelope validado, a terceira parte da entrega fala com os dispositivos reais usando duas funções padrão do MODBUS: 0x03 (ler registradores) e 0x10 (escrever registradores). Cada registrador tem 16 bits e é identificado por um offset. A cabine tem 9 (do 0 ao 8); o prédio, 16.
Ler o estado da cabine 1 numa bancada devolveu isto:
11 03 12 │ 00 00 │ 00 00 │ 00 00 │ 00 00 │ 01 2C │ 00 00 │ 00 00 │ FF 98 │ 00 01 │ 16 37
end fun n│ andar │ nivel │ porta │ cmd │ 300mA │ carga │ pass. │ posição│ falha │ CRC
Repare no FF 98: é a posição, o único registrador com sinal. Lido como inteiro de 16 bits com sinal, vale −104 mm. A cabine não estava "no térreo", e sim 10 cm abaixo dele, no poço — e, como a porta só opera com a cabine nivelada (dentro de ±10 mm do andar), foi preciso reposicioná-la antes de seguir.
A ordem dos bytes não é uniforme
Esse é o tipo de detalhe que faz uma bancada devolver "lixo que parece ruído". No pedido, o número do registrador e a quantidade vão em little-endian (byte baixo primeiro). Na resposta, os valores vêm em big-endian (byte alto primeiro). O 01 2C acima é 300, e não 11265, justamente por isso. Errar a ordem num único campo desloca tudo, e o resultado parece um problema de comunicação quando é só decodificação.
Exceções não se repetem
Quando o pedido é inválido, o dispositivo não fica em silêncio: responde com a função "marcada" (o bit 0x80 ligado) e um código. Tentar escrever na posição da cabine, que é somente leitura, devolve 0x90 com o código 0x02; ler a partir do registrador 50, fora do mapa, devolve 0x83 com 0x02:
11 83 02 B0 F4 → cabine 0x11, leitura (0x03 + 0x80), código 0x02 = endereço inválido
A regra de projeto que vale guardar: exceção não é falha de comunicação. É uma resposta válida, com CRC correto, dizendo "não". Repetir o pedido daria o mesmo "não" três vezes. Só timeout e CRC inválido justificam nova tentativa.
Watchdog e Condição de Contorno: o fiscal e o dado

Esta foi a parte que mais gerou perguntas — inclusive minhas.
O prédio tem um barramento de potência, a fonte que alimenta os motores das cabines. Quanta corrente ele entrega depende da temperatura da casa de máquinas: quanto mais quente, menos potência. O enunciado define a regra (o derating térmico):
barramento = 12000 − 200 × max(0, temperatura − 25) mA
25,0 °C → 12000 mA 25,3 °C → 11940 mA 35,0 °C → 10000 mA
Só que o simulador não sabe a temperatura sozinho. Alguém precisa informar — e essa informação (temperatura e pressão do ambiente) é a Condição de Contorno. O nome vem da simulação física: é o dado do mundo externo que o modelo precisa receber para conseguir calcular.
O watchdog é o fiscal desse dado. Se a Raspberry ficar mais de 30 segundos sem reescrever a Condição de Contorno, o simulador conclui que o sensor (ou o programa) falhou, deixa de confiar na última temperatura e assume o pior: trava o barramento num piso de 3000 mA, e as cabines passam a andar na velocidade mínima. Funciona como o modo de segurança de um carro quando o sensor de temperatura do motor para de responder.
Dois mal-entendidos comuns, que eu tive:
- O watchdog não bloqueia nada. Com ele expirado, todas as leituras e escritas continuam funcionando. A porta abriu e fechou normalmente com o watchdog em 1. A única consequência é a potência limitada.
- Expirado é o estado normal em repouso. Sem ninguém alimentando, ele fica em 1. Só aparece como "OK" nos 30 segundos depois de uma escrita.
Na prática, a demonstração é uma sequência de três passos: ler o prédio (watchdog em 1, 3000 mA), escrever 253 (25,3 °C, em décimos de grau) e 1013 hPa nos registradores 5 e 6 numa única escrita 0x10, e ler de novo (watchdog em 0, 11940 mA). A trama da escrita, ainda com a matrícula de exemplo:
20 10 05 00 02 00 04 FD 00 F5 03 06 05 04 03 02 01 56 C3
fun reg qtd bc 253 1013 matrícula CRC
Se a ordem dos bytes estivesse errada, 253 chegaria como 64768 e o barramento jamais bateria com a fórmula. O número certo na leitura é, ao mesmo tempo, prova do protocolo e da física do simulador.
Fechando a cadeia com I2C

Escrever 25,3 °C à mão prova que o caminho MODBUS funciona, mas não é o sistema real. O enunciado é direto: um sistema que não implemente a cadeia I2C → MODBUS não move o edifício.
A metade que falta é o I2C: um segundo barramento, outro par de fios, ligando a Raspberry ao sensor BMP280. O programa abre o barramento, identifica o chip pelo registrador de identificação, lê os valores brutos de temperatura e pressão e os converte com os coeficientes de calibração gravados de fábrica no próprio sensor. Só então usa a mesma escrita MODBUS de antes.
BMP280 ──I2C──► Raspberry Pi ──UART/MODBUS──► ESP32 (simulador)
mede converte e decide aplica o derating
A diferença para o passo anterior não está no destino, e sim na origem do dado: antes era o teclado, agora é o sensor. A pressão lida em Brasília fica perto de 880 hPa, bem abaixo dos 1013 do nível do mar, por causa da altitude — um bom teste de sanidade de que o valor é real. E um laço que repete leitura e escrita a cada 10 segundos, com folga dentro dos 30 do watchdog, é exatamente o que o servidor central vai fazer sozinho na entrega final.
Um aprendizado à parte: em algumas placas do laboratório o barramento I2C não vinha habilitado na configuração do sistema, e não tínhamos permissão de administrador para mudar isso. Em vez de deixar o programa quebrar com um erro cru, o driver detecta a ausência do dispositivo e explica a causa. Mensagem de erro que diz o que fazer economiza a sessão de quem vem depois.
O que a bancada ensinou
Algumas lições só aparecem com hardware de verdade e com o tempo contado — cada sessão na placa durava 15 minutos.
- Prepare a placa antes de gravar. A placa vinha com chamadas deixadas na fila por quem a usou antes e com a cabine fora de nível. Um script que confere o ambiente (porta serial, permissões, compilação, testes) num único comando poupa a sessão inteira.
- O simulador tem comportamento próprio. Mandei abrir a porta e só comecei a observar seis segundos depois: ela já estava fechando sozinha, como um elevador real. A leitura contínua precisa estar rodando antes do comando — no meu caso, com o terminal dividido em duas metades, uma observando e outra comandando.
- Contadores diferentes contam coisas diferentes. Depois de atribuir uma chamada à cabine e retirá-la da fila, o dashboard mostrava "na fila: 0" mas "em atendimento: 1". Não era bug: a chamada saiu da fila de espera, mas só termina quando a cabine fizer a viagem — o que, nesta entrega, nada fazia acontecer.
- O dashboard é o outro lado da conversa. Cada comando que a ESP32 recebia aparecia no widget, com o protocolo de origem (MODBUS ou "ALT", de alternativo, para o simplificado) e a matrícula. Ver os dois lados ao mesmo tempo transforma "acho que funcionou" em "funcionou".
No fim, a lição mais útil foi a mais simples: testes unitários garantem que você monta a trama certa; a bancada garante que o outro lado concorda com você. Os dois são necessários, e nenhum substitui o outro.


