
Contador de produção com ESP32 sem perder pulso
O debounce copiado de tutorial de botão vira teto de produção: 50 ms limitam a linha a 1.200 peças por minuto. Interrupção em IRAM e entrada isolada.
Requisitos de hardware:
- Placa ESP32 DevKit
- Sensor indutivo PNP M12, alimentado em 24 V
- Optoacoplador PC817 com resistor de 2,2 kΩ de 1/2 W
- Fonte de 24 V do painel e fonte separada para o ESP32
- Multímetro para conferir a saída do sensor antes de ligar a placa
Baixe o material deste tutorial:
- Projeto completo (ZIP)148 KBplatformio.ini, firmware com interrupção em IRAM, debounce por microssegundos, contador de rejeitados, contagem acumulada com sessão de boot, README e o manual em PDF.
- Manual em PDF (6 páginas)176 KBTabela de teto por janela de debounce, cálculo do resistor do PC817, regras da rotina de interrupção, contrato MQTT e o teste de ritmo máximo.
Quer receber novos manuais e esquemas industriais? Entre no nosso canal do Telegram / WhatsApp
O contador que conta menos do que a linha produziu
Um contador de produção é o projeto que todo mundo acha simples: um sensor, uma interrupção, um número que sobe. Ele é simples até o dia em que o supervisor compara o número do painel com a contagem do expedidor e encontra uma diferença de centenas de peças — sempre para menos, sempre nos turnos de maior ritmo.
O motivo quase nunca é o sensor. É uma linha de código copiada de um tutorial de botão: um debounce de 50 ms, colocado para tratar um problema que o sensor indutivo não tem, e que impõe um teto de produção que ninguém calculou.
Este tutorial trata dos quatro pontos que decidem se o número é confiável: por que o sensor de estado sólido não trepica, como o debounce vira teto, como isolar 24 V do ESP32 sem estragar o pulso e por que o contador precisa ser acumulado em vez de incremental.
O sensor indutivo não trepica
Trepidação é fenômeno de contato mecânico. Uma chave, um relé ou um botão fecham fisicamente dois metais, e o contato quica por alguns milissegundos antes de estabilizar. Por isso todo tutorial de botão começa com debounce.
Um sensor indutivo PNP M12 não tem contato. Ele é um oscilador com um transistor de saída: quando o alvo metálico entra no campo, a saída satura. Não há quique, não há microssegundos de indecisão mecânica. O sinal que ele entrega já é limpo.
Isso não significa que o sinal chegue limpo no ESP32. Numa linha de produção existe inversor de frequência chaveando dezenas de ampères a poucos centímetros do seu cabo, contator abrindo carga indutiva e aterramento que ninguém revisou. O ruído que aparece na entrada é acoplamento elétrico, não trepidação, e a diferença importa porque o remédio é outro: cabo blindado, par trançado, separação física dos condutores de potência e isolação galvânica. Software não resolve acoplamento; ele só esconde o sintoma até o dia em que esconder também custa peças.
O debounce é um teto de produção
Um debounce por tempo ignora qualquer pulso que chegue antes de a janela expirar. Isso é exatamente o que você quer contra trepidação e exatamente o que você não quer contra produção rápida: o pulso legítimo da peça seguinte também é ignorado.
A conta é direta. O teto em peças por minuto é 60 dividido pela janela em segundos:
| 1 | janela pulsos/s teto de producao |
| 2 | 50 ms 20 1.200 pecas/min |
| 3 | 20 ms 50 3.000 pecas/min |
| 4 | 12 ms 83 5.000 pecas/min |
| 5 | 10 ms 100 6.000 pecas/min |
| 6 | 5 ms 200 12.000 pecas/min |
| 7 | 2 ms 500 30.000 pecas/min |
O requisito deste projeto é 5.000 peças por minuto, ou seja, uma peça a cada 12 ms. Um debounce de 50 ms não recusa metade das peças: ele recusa quatro de cada cinco, e o painel mostra 1.200 com toda a confiança do mundo. Nenhum erro aparece no log, porque do ponto de vista do firmware ele fez exatamente o que mandaram.
A regra prática: a janela precisa ser bem menor que o intervalo mínimo entre peças, com folga para variação de ritmo. Para 12 ms de intervalo, 2 ms de janela é uma escolha confortável — filtra transiente de chaveamento, que dura microssegundos, e ainda deixa margem de seis vezes.
Existe ainda um teto que o seu código não muda: a frequência de comutação do próprio sensor, que vem na folha de dados. Um M12 comum fica entre 500 Hz e 1 kHz. Se o seu sensor comuta a 500 Hz, o limite físico é 30.000 peças por minuto, independentemente do firmware. Confira esse número antes de prometer qualquer coisa.
Isolação: 24 V não entra no ESP32
A saída do sensor PNP entrega os 24 V da alimentação de comando quando ativa. O GPIO do ESP32 tolera 3,3 V. Ligar um no outro destrói a entrada e, em muitos casos, leva o regulador junto.
O optoacoplador resolve duas coisas ao mesmo tempo: adapta o nível e separa os terras. A segunda importa mais do que parece. O terra de 24 V do painel e o terra do ESP32 costumam estar em pontos elétricos diferentes; ligá-los cria um caminho de corrente por onde o ruído do inversor passa direto para a sua eletrônica.
O PC817 tem um LED de um lado e um fototransistor do outro, sem conexão elétrica entre eles. O lado de 24 V precisa de um resistor em série para limitar a corrente do LED.
O cálculo do resistor, e por que 1 kΩ queima
A corrente direta recomendada do PC817 em operação contínua fica em torno de 10 mA, e a queda no LED é de aproximadamente 1,2 V. O resistor precisa absorver o resto:
| 1 | R = (24 V - 1,2 V) / 10 mA = 2.280 ohms -> valor comercial 2,2 k |
| 2 | P = I^2 x R = (0,0104)^2 x 2200 = 0,24 W -> resistor de 1/2 W |
Muitos exemplos na internet usam 1 kΩ, que era o valor certo para 5 V. Em 24 V ele entrega:
| 1 | I = (24 - 1,2) / 1000 = 22,8 mA |
| 2 | P = (0,0228)^2 x 1000 = 0,52 W |
Vinte e dois miliampères não queimam o LED de imediato, mas aceleram a degradação do ganho do optoacoplador — o componente vai perdendo sensibilidade ao longo de meses, até começar a falhar pulso de forma intermitente. Já o resistor de 1/4 W, que é o que quase todo mundo tem na gaveta, está dissipando o dobro do que aguenta e queima. Use 2,2 kΩ de 1/2 W.
Do lado do ESP32, o emissor do fototransistor vai ao GND e o coletor ao GPIO com resistor de pull-up. Quando o sensor ativa, o fototransistor conduz e puxa o pino para baixo. A lógica fica invertida de propósito: peça presente é nível baixo e a interrupção dispara na borda de descida.
O pull-up interno do ESP32 tem cerca de 45 kΩ, o que funciona, mas deixa a borda de subida lenta. Para o ritmo deste projeto isso não atrapalha. Se você for empurrar a taxa para perto do limite do sensor, coloque um pull-up externo de 10 kΩ: a borda fica mais definida e o fototransistor sai da condução mais rápido.
Interrupção: o que pode e o que não pode
A rotina de interrupção precisa ser curta. Ela roda com o resto do sistema parado, e tudo o que você fizer dentro dela é tempo em que a rede, o watchdog e a própria contagem ficam esperando.
Três regras evitam a maioria dos travamentos:
A função precisa do atributo IRAM_ATTR, para ficar na RAM interna. Sem ele, a rotina pode estar em uma região de flash que fica indisponível durante uma escrita, e a interrupção que chegar nesse instante derruba a placa.
Toda variável compartilhada entre a interrupção e o laço principal precisa ser volatile, senão o compilador guarda o valor em registrador e o laço lê um número que nunca muda.
Nada de Serial.print, alocação de memória, delay ou publicação MQTT dentro da interrupção. Guarde o dado e saia; quem imprime e publica é o laço principal.
O debounce mora dentro da interrupção, comparando o instante atual com o do último pulso aceito. Use o relógio de microssegundos, que tem resolução suficiente para uma janela de 2 ms:
| 1 | constexpr int PINO_SENSOR = 4; |
| 2 | constexpr int64_t JANELA_US = 2000; |
| 3 | volatile uint32_t contagem = 0; |
| 4 | volatile int64_t ultimoPulsoUs = 0; |
| 5 | volatile uint32_t rejeitados = 0; |
| 6 | portMUX_TYPE trava = portMUX_INITIALIZER_UNLOCKED; |
| 7 | // A interrupcao faz tres coisas e sai: le o relogio, decide se o pulso e |
| 8 | // valido e incrementa. Qualquer coisa alem disso e tempo roubado da |
| 9 | // proxima peca. |
| 10 | void IRAM_ATTR aoDetectarPeca() { |
| 11 | const int64_t agora = esp_timer_get_time(); |
| 12 | portENTER_CRITICAL_ISR(&trava); |
| 13 | if (agora - ultimoPulsoUs >= JANELA_US) { |
| 14 | ultimoPulsoUs = agora; |
| 15 | contagem++; |
| 16 | } else { |
| 17 | // Nao e so descartar: pulso rejeitado e diagnostico. Se este contador |
| 18 | // sobe junto com a producao, a janela esta grande demais. |
| 19 | rejeitados++; |
| 20 | } |
| 21 | portEXIT_CRITICAL_ISR(&trava); |
| 22 | } |
| 23 | void configurarContador() { |
| 24 | pinMode(PINO_SENSOR, INPUT_PULLUP); |
| 25 | attachInterrupt(digitalPinToInterrupt(PINO_SENSOR), aoDetectarPeca, FALLING); |
| 26 | } |
Repare no contador de rejeitados. Ele transforma um teto silencioso em um defeito visível: se a linha acelera e esse número começa a subir junto, a janela está grande demais. Sem ele, o firmware perde peças sem nunca dizer que perdeu.
Conte acumulado, nunca incremento
A tentação é publicar quantas peças passaram desde o último envio. É errado, e o erro só aparece no dia em que a rede oscila.
Se a mensagem que dizia “12 peças” se perde, essas 12 peças desaparecem para sempre. Não há como o sistema do outro lado saber que faltou alguma coisa: ele recebeu uma sequência de números plausíveis, sem buraco visível.
Publicar o total acumulado desde o boot resolve sozinho. Se uma mensagem se perde, a seguinte já traz o total correto, e o sistema de gestão calcula a diferença entre duas leituras consecutivas. O dado se cura sem intervenção.
Isso exige duas coisas do outro lado. A primeira é saber tratar o reinício do contador: quando o ESP32 reinicia, o total volta a zero, e uma diferença negativa deve ser lida como reinício, não como produção negativa. Publique junto um identificador de sessão de boot para que essa distinção seja explícita.
A segunda é nunca aceitar comando de zerar contador com retenção. Um comando retido é reentregue a cada reconexão do cliente e zera a produção do turno sozinho, no meio da tarde. Vale a mesma regra do artigo de integração com SCADA: estado se retém, comando nunca.
Peças por minuto sem ruído
Calcular o ritmo pelo intervalo entre duas peças consecutivas dá um número que pula o tempo todo: qualquer variação de milissegundos vira dezenas de peças por minuto para cima ou para baixo, e o gráfico fica ilegível.
Use uma janela de tempo fixa. A cada intervalo de publicação, tome a diferença entre a contagem atual e a da publicação anterior e converta:
| 1 | pecas por minuto = (contagem - contagemAnterior) * 60000 / intervaloMs |
Com um intervalo de 10 segundos, o número resultante é estável o bastante para alarme e ainda responde rápido a uma parada de linha. Janela muito curta devolve ruído; muito longa esconde a parada por tempo demais.
Contrato MQTT
Publique o acumulado, o ritmo, a sessão de boot e a qualidade. Um tópico estável, com o valor na carga:
| 1 | { |
| 2 | "ts": 1755990000, |
| 3 | "count_total": 184523, |
| 4 | "rate_ppm": 4870, |
| 5 | "boot_id": 7, |
| 6 | "rejected": 0, |
| 7 | "quality": 192 |
| 8 | } |
O campo rejected viaja junto de propósito: ele é a prova de que o debounce não está comendo produção. Em operação normal fica em zero e ninguém olha para ele. No dia em que a linha acelera além do previsto, ele sobe antes de a diferença aparecer no inventário — e é isso que transforma um erro silencioso em um alarme.
Retenha a telemetria e o status do gateway, para que o painel suba já preenchido depois de um reinício. E carimbe o instante da leitura, com o relógio sincronizado por NTP; sem NTP sincronizado, publique qualidade incerta em vez de inventar horário.
Montagem e validação
Passo 1: conferir o sensor antes do ESP32
Alimente o sensor indutivo com os 24 V do painel e meça a saída com um multímetro, com o alvo dentro e fora do campo. Você precisa ver a saída indo de perto de zero para perto de 24 V. Só depois disso ligue qualquer coisa no ESP32. Metade dos problemas de contagem termina aqui, em sensor mal posicionado ou distância de detecção fora da faixa.
Passo 2: montar a entrada isolada
O positivo da saída do sensor vai ao resistor de 2,2 kΩ de 1/2 W e daí ao ânodo do PC817. O cátodo vai ao 0 V do lado de 24 V. Do outro lado, emissor no GND do ESP32 e coletor no GPIO 4. Confirme que os dois terras não se tocam em nenhum ponto: se estiverem interligados, a isolação virou enfeite.
Passo 3: validar o pulso antes de contar
Antes de ligar a interrupção, leia o pino em laço e imprima o nível. Passe o alvo pelo sensor devagar, com a mão, e confirme que o valor alterna. Se não alternar, o problema é elétrico e nenhum ajuste de firmware corrige.
Passo 4: contar devagar e conferir na mão
Passe exatamente 20 peças, devagar, e compare com o número no monitor serial. Precisa bater exatamente. Se contar a mais, há ruído entrando ou o debounce está curto demais para o seu ambiente. Se contar a menos, o pulso é curto demais para ser detectado ou a janela já está grande demais.
Passo 5: o teste que quase ninguém faz
Reproduza o ritmo máximo previsto. Se a bancada não permite, simule: gere pulsos com um segundo ESP32 ou um gerador de sinal na frequência alvo, com um número conhecido de pulsos, e confira se a contagem final bate.
Para 5.000 peças por minuto, gere 83 pulsos por segundo durante um minuto e confirme que o total é 5.000, com rejected em zero. Esse é o teste que separa um contador que funciona de um contador que parece funcionar. É também o único jeito honesto de sustentar um número de folha de dados.
Passo 6: conferir o reinício
Com a contagem em andamento, reinicie o ESP32. O total volta a zero e o identificador de sessão precisa mudar. Confirme que o sistema do outro lado interpreta isso como reinício, e não como produção negativa ou como um salto de centenas de milhares de peças.
Solução de problemas
Conta menos do que a linha produz. Janela de debounce grande demais para o ritmo. Olhe o contador de rejeitados: se ele acompanha a produção, é isso. Reduza a janela conforme a tabela do início.
Conta a mais, e mais ainda quando o inversor liga. Ruído acoplado no cabo. Não aumente o debounce como primeira reação, porque isso troca um erro visível por um teto invisível. Trate a causa: cabo blindado com a malha aterrada em uma ponta só, par trançado, afastamento dos condutores de potência e conferência do aterramento do painel.
Contagem congela depois de horas. Procure trabalho pesado dentro da interrupção ou ausência do IRAM_ATTR. Verifique também se o laço principal não tem bloqueio longo que impeça a publicação.
O total volta a zero sozinho. Comando de reset publicado com retenção ou reinício não tratado. Confira o identificador de sessão e a política de retenção do tópico de comando.
A produção medida não bate com o expedido, mas o rejeitado está em zero. O problema provavelmente está antes do ESP32: alvo passando fora do alcance do sensor, peça que não aciona por posição ou ponto de contagem que não corresponde ao ponto de expedição.
Sobre a validação deste material
O firmware do pacote foi escrito a partir do funcionamento documentado do ESP32, do PC817 e de sensores indutivos PNP, e revisado linha a linha. As contas de resistor, dissipação e teto de produção estão no texto justamente para você conferir em vez de acreditar.
Ele não foi montado em bancada com sensor e linha reais. Os números de teto vêm de aritmética, não de medição, e o teste do Passo 5 existe para que você produza a sua própria evidência antes de confiar o número a alguém. Onde eu não medi, eu digo que não medi.
