Site menu IoT: Controle remoto 433MHz (parte 5: RX com RMT)

IoT: Controle remoto 433MHz (parte 5: RX com RMT)

Este texto é uma continuação desta série.

Em produção

Nosso receptorzinho de keyfobs 433MHz chegou no estágio de "produção" e ganhou até uma caixinha e uma antena externa.

Figura 1: Receptor 433MHz - forma final (assim esperamos).

Ando meio parado nas atividades de radioamadorismo, então a antena extra do HT ganhou um emprego novo. Não tenho como saber se está casando impedância, etc. mas o alcance parece essencialmente o mesmo, e ficou mais estético que um pedaço de fio.

Como dito no capítulo anterior, o esquema é bem simples: a caixinha apenas capta sinal de keyfobs e publica um tópico MQTT com códigos recebidos, sem filtros. Outro programa (que poderia ser um Home Assistant da vida) dispara algum evento de automação quando recebe determinado código.

Melhorias no alcance

O portão eletrônico ainda tem sensibilidade um pouco melhor que nossa caixa, o que não é um problema para a funcionalidade que extraímos do sistema. Porém, não podemos deixar barato!

Descobri um módulo no AliExpress (RXB-8) que alega ter sensibilidade 7db melhor que o SYN480R, o que significa o dobro do alcance, em tese. Também adquiri uma antena específica para controle remoto 433MHz.

Os primeiros resultados com o novo setup foram animadores. (O ideal seria mudar um componente de cada vez e testar o ganho, mas quem tem tempo e saco pra isso?) O alcance realmente dobrou, talvez mais que dobrou: atinge uns 100m com obstáculos consideráveis (a antena está dentro de uma casa de alvenaria), enquanto antes eram 30m típicos/50m máximos.

Além disso, a caixa está rotineiramente pegando o sinal de um controle que não é meu, o único vizinho que parece ter portão eletrônico está 3 casas distante.

ESP32 RMT em recepção?

Também já foi dito que a implementação do receptor é um busy loop (classe OOKReceiverBusy), o que é feio. Funciona melhor do que eu esperava, mas é feio. O jitter (variação da latência) do tratador de interrupções do MicroPython é muito alto no ESP32 quando WiFi está ativado.

A rigor, não é pecado usar busy loop em microcontroladores, porque no nível do metal é assim que eles trabalham o tempo todo. Mesmo as implementações de chamadas de sistema análogas a sleep e select/poll são busy loops internamente. Mas no meu frameworkzinho tentamos fazer tudo à moda do UNIX, então busy loops vão de encontro aos nossos princípios, e criam problemas para meu framework.

Por que nosso framework é assim? a) Por uma questão de princípios; b) Por termos a esperança que um dia o MicroPython coloque o ESP32 em "sono leve" por inatividade (é mais fácil que volte Dom Sebastião, mas o suporte de hardware existe, então teoricamente é possível); c) Porque outro microcontrolador com essa capacidade pode popularizar-se no futuro, e aí já estamos prontos.

Para abolir o busy loop, a solução mais elegante é usar um recurso do ESP32 chamado RMT, que permite transmitir e receber trens de pulsos sem ocupar a CPU.

O RMT foi concebido para interfacear com controles remotos infravermelho, mas serve para muitas outras tarefas de "bitbanging" — inclusive os módulos machine.bitstream e neopixel do MicroPython empregam o RMT no porte ESP32. (Na verdade, os keyfobs 433MHz são semelhantes aos controles infravermelho em muitos aspectos; os protocolos são parecidos, e alguns chips desse nicho servem para ambos.)

O MicroPython dá acesso ao RMT, mas apenas para transmissão, inclusive já fizemos uso dele no segundo capítulo. Se quisermos usar em recepção, precisamos arregaçar as mangas e adicionar este recurso ao módulo MicroPython.

Recursos do RMT para RX

O esquema básico é o seguinte: o software inicia a transação. O RMT vai gravando a largura e a polaridade dos pulsos recebidos até encontrar um pulso com largura maior que max, que sinaliza o fim da transação (tudo isso em hardware). Os dados são repassados ao software. Abrir nova transação depende de iniciativa do software (o RMT não tem modo contínuo de RX).

O trem de pulsos dos keyfobs 433MHz começam e terminam com um longo silêncio (5000µs no mínimo), então o RMT consegue isolar um código de keyfob numa transação, sem nenhum pulso demais ou de menos. (A largura do pulso final que fecha a transação não é incluída na transação.)

Pulsos mais curtos que min são considerados glitches e ignorados pelo RMT. Isto permite excluir pulsos impossivelmente curtos que não nos interessam (e que encheriam rapidamente a escassa memória do RMT). Note que pulsos curtos não resetam a transação; eles são simplesmente ignorados. Ainda vamos receber transações com ruído, porém o ruído será filtrado em freqüência.

Nosso receptor considera válidos os pulsos entre 150µs e 1500µs. Se o teorema de Nyquist vale alguma coisa, temos de fazer min <= 75µs para que seja possível distinguir sinal de ruído. (O ideal para nós seria que o RMT descartasse a transação ao receber um pulso curto, mas não é assim que acontece.)

Em nosso caso, pulsos entre 1500µs e 5000µs estão numa faixa proibida: invalidam uma transação, e também não servem como marcadores de fim de transação. Infelizmente o RMT não possui um filtro para largura máxima de pulso válido, então temos de filtrar em software.

Até aqui, o RMT parece absolutamente perfeito para nossa aplicação. Agora, vêm as limitações.

As larguras de pulso têm 15 bits (sem sinal), o que limita sua resolução e valor máximo. O "tick" ou unidade de medida é configurável, na forma de um divisor do clock de 80MHz (e.g. um divisor 80 produz um "tick" de 1µs, e a largura máxima mensurável seria de 32767µs).

O valor de min tem apenas 8 bits, portanto ele possui um teto relativamente baixo (255µs para um "tick" de 1µs). O valor de max possui mais bits (não sei quantos, acredito que sejam 24) mas também tem um teto.

O RMT possui vários canais simultâneos (o número exato depende da variante de ESP32) para que se possa trabalhar diversos pinos de GPIO simultaneamente. Porém, a memória total do RMT é equivalente a 1024 pulsos, compartilhada entre todos os canais.

Essas limitações (e mais outras impostas pelo ESP-IDF, que serão abordadas logo abaixo) não nos impedem de usar o RMT para decodificar keyfobs. Porém está claro que o RMT está longe de ser uma panaceia, e é muito menos poderoso que o PIO do Raspberry Pi Pico (RP2040).

Problemas

Em primeiro lugar, há uma escassez de exemplos de uso do RMT para RX. A documentação da Espressif é boa, mas é um tópico complexo e fora da minha zona de conforto. Mesmo a Espressif oferece um punhado de exemplos de TX e apenas um de RX. Um exemplo didático e "top-down" é o que ajudaria. Acabei achando este exemplo para framework Arduino que confirmei funciona, e que foi nossa baliza daqui por diante.

Outro ponto é que a API RMT do ESP-IDF (framework da Espressif para seus chips) mudou bastante entre a versão 4.x e 5.x. A API de legado (no estilo 4.x) ainda está disponível no ESP-IDF 5.x e é utilizada pelo módulo RMT do MicroPython (cujo suporte a ESP-IDF 5 é relativamente recente). Para estender o módulo RMT do MicroPython, teria de codificar a parte de RX usando a API de legado, para a qual não tinha um exemplo didático.

Decidi então usar a API RMT atual, e implementar um módulo MicroPython separado, naturalmente copiando o máximo possível de boilerplate do módulo preexistente. Assim poderia me concentrar em "fazer funcionar", sem ter de esquentar a cabeça com questões de compatibilidade, convivência com código TX, etc.

Tive de desligar completamente o módulo RMT preexistente, bem como o módulo bitstream, pois o ESP-IDF não aceita que as APIs velha e nova do RMT sejam referenciadas ao mesmo tempo. (A imagem emite uma mensagem de erro e reseta assim que inicia.)

No desenvolvimento em si, o que deu mais trabalho foi lidar com a "burocracia" de um módulo MicroPython escrito em C. Muito embora eu considere que o MicroPython é mais simples que o Python convencional neste ponto (impressão que vem desde o CircuitPython, um fork do MicroPython). Nem sequer existe Py_INCREF e cia. A coleta de lixo faz todo o serviço.

Adicionamos filtros opcionais "soft" para largura mínima e máxima de pulso, bem como para contagem mínima e máxima de pulsos. Esses filtros são aplicados por software, porém na linguagem C, são muito mais rápidos do que se a filtragem fosse feita em Python.

No caso, só queremos transações com pulsos entre 150µs e 1500µs (pulsos fora dessa faixa invalidam a transação) e contagem entre 49 e 57 (na verdade, exatamente 49 para o keyfob HT1527 ou exatamente 57 para o keyfob HT6B20). Com esses filtros implementados, a camada MicroPython recebe praticamente zero transações de ruído.

Finalmente, tornei o objeto RMT "pollável", ou seja, pode ser tratado como um arquivo e ser monitorado por select() ou poll(). Isto evita o uso de callbacks e integra perfeitamente com nosso framework, que já usa poll() para eventos de rede.

O resultado dos nossos esforços pode ser visto aqui, e o cliente do novo módulo pode ser visto aqui (classe OOKReceiverRMT).. A única desvantagem é ter de usar um build customizado do MicroPython, mas não é a primeira vez que tenho de fazer isso.

Devolvendo para a komunidade

Do jeito que está, nem vou tentar kolaborar esse módulo para o MicroPython, porque não tem TX e é incompatível com o módulo preexistente, pois como dito cada um usa uma versão diferente da API RMT do ESP-IDF. Publiquei meu branch como comentário no GitHub do MicroPython, para o caso de alguém se interessar.

Uma alternativa é portar a parte TX do módulo preexistente para a API nova, e adicionar o RX. É uma quantidade respeitável de trabalho, e a API nova é bastante diferente da velha, aí teria de ver se é possível manter a API MicroPython compatível.

A API nova é indiscutivelmente melhor, principalmente em RX. Porém ainda tem arestas, que estão sendo resolvidas (threads relevantes aqui e aqui). Esses problemas podem ser contornados mexendo direto no código do ESP-IDF, o que é viável para um fabricante de dispositivos, mas o MicroPython tem de ser baseado num ESP-IDF padrão.

Teria de analisar primeiro se essas arestas afetam o RMT TX. Se não afetarem, não perdemos funcionalidade preexistente. Basta documentar que o RX possui limitações por culpa da Espressif.

Outra alternativa é reescrever o RMT RX usando a API de legado, para integrá-lo com o módulo preexistente. É um caminho seguro, mas por outro lado parece trabalho jogado fora escrever algo novo baseado numa API do ESP-IDF 4.x (que o MicroPython nem sequer suporta mais). Também há uma carência de bons exemplos de RMT RX usando essa API de legado — muito embora o exemplo da Espressif está um pouco mais fácil de entender, agora que estou tarimbado.

De um jeito ou de outro, é um bocado de trabalho. Decidi ir pelo primeiro caminho, portando o módulo RMT atual (apenas TX) para a API nova. Aqui está o PR e vamos ver quando (ou se) ele vai ser aceito.

Se estiver apreciando a história, siga para a parte 6.