iiOTESP32 Brasil
Modo de contingencia: las páginas públicas y las descargas siguen disponibles; el inicio de sesión, la comunidad y los datos del servidor pueden no estar disponibles.
Volver al catálogo de tutoriales
Ilustração: um técnico de colete refletivo e óculos de proteção confere, com um tablet, o quadro de comando onde uma placa ESP32 está ligada ao sensor que lê com um feixe vermelho as caixas passando na esteira de uma fábrica.
Imagem ilustrativa gerada por IA
IntermedioIIoTLectura: 14 min

Contador de producción con ESP32 sin perder pulsos

Debounce de un tutorial de botones se vuelve límite de producción: 50 ms limitan la línea a 1.200 piezas/min. Interrupción en IRAM y entrada aislada.

Tecnologías:#ESP32#Interrupções#PC817#Sensor Indutivo#Debounce#MQTT

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

El contador que cuenta menos de lo que produjo la línea

Un contador de producción es el proyecto que todos consideran sencillo: un sensor, una interrupción y un número que sube. Es sencillo hasta el día en que el supervisor compara el número del panel con el conteo de expedición y encuentra una diferencia de cientos de piezas; siempre menos y siempre en los turnos de mayor ritmo.

La causa casi nunca es el sensor. Es una línea copiada de un tutorial de botones: un debounce de 50 ms, colocado para resolver un problema que el sensor inductivo no tiene y que impone un límite de producción que nadie calculó.

Este tutorial trata los cuatro puntos que determinan si el número es confiable: por qué un sensor de estado sólido no rebota, cómo el debounce se convierte en límite, cómo aislar 24 V del ESP32 sin estropear el pulso y por qué el contador debe ser acumulado en vez de incremental.

El sensor inductivo no rebota

El rebote es un fenómeno de contacto mecánico. Un interruptor, un relé o un botón cierran físicamente dos metales y el contacto rebota durante algunos milisegundos antes de estabilizarse. Por eso todo tutorial de botones comienza con debounce.

Un sensor inductivo PNP M12 no tiene contacto. Es un oscilador con un transistor de salida: cuando el objetivo metálico entra en el campo, la salida satura. No hay rebote ni microsegundos de indecisión mecánica. La señal que entrega ya es limpia.

Eso no significa que la señal llegue limpia al ESP32. En una línea de producción hay un variador de frecuencia conmutando decenas de amperios a pocos centímetros del cable, un contactor abriendo una carga inductiva y una puesta a tierra que nadie revisó. El ruido de la entrada es acoplamiento eléctrico, no rebote, y la diferencia importa porque el remedio es otro: cable blindado, par trenzado, separación física de los conductores de potencia y aislamiento galvánico. El software no resuelve el acoplamiento; solo oculta el síntoma hasta el día en que ocultarlo también cuesta piezas.

El debounce es un límite de producción

Un debounce por tiempo ignora cualquier pulso que llegue antes de que termine la ventana. Eso es exactamente lo que quieres contra el rebote y exactamente lo que no quieres contra una producción rápida: también se ignora el pulso legítimo de la pieza siguiente.

La cuenta es directa. El límite en piezas por minuto es 60 dividido por la ventana en segundos:

code.cpp
cpp
1ventana pulsos/s limite de produccion
2 50 ms 20 1.200 piezas/min
3 20 ms 50 3.000 piezas/min
4 12 ms 83 5.000 piezas/min
5 10 ms 100 6.000 piezas/min
6 5 ms 200 12.000 piezas/min
7 2 ms 500 30.000 piezas/min

Este proyecto requiere 5.000 piezas por minuto, es decir, una pieza cada 12 ms. Un debounce de 50 ms no rechaza la mitad de las piezas: rechaza cuatro de cada cinco, y el panel muestra 1.200 con toda la confianza del mundo. No aparece ningún error en el registro porque, desde el punto de vista del firmware, hizo exactamente lo que le pidieron.

La regla práctica es que la ventana debe ser mucho menor que el intervalo mínimo entre piezas, con margen para variar el ritmo. Para un intervalo de 12 ms, una ventana de 2 ms es cómoda: filtra un transitorio de conmutación que dura microsegundos y todavía deja un margen de seis veces.

También existe un límite que el código no puede cambiar: la frecuencia de conmutación del propio sensor, indicada en su hoja de datos. Un M12 común está entre 500 Hz y 1 kHz. Si tu sensor conmuta a 500 Hz, el límite físico es 30.000 piezas por minuto, independientemente del firmware. Comprueba ese número antes de prometer nada.

Aislamiento: los 24 V no entran en el ESP32

La salida del sensor PNP entrega los 24 V de la alimentación de control cuando se activa. El GPIO del ESP32 tolera 3,3 V. Conectar uno con el otro destruye la entrada y, en muchos casos, también el regulador.

El optoacoplador resuelve dos problemas al mismo tiempo: adapta el nivel y separa las tierras. Lo segundo importa más de lo que parece. La tierra de 24 V del panel y la tierra del ESP32 suelen estar en puntos eléctricos distintos; conectarlas crea un camino de corriente por donde el ruido del variador pasa directamente a la electrónica.

El PC817 tiene un LED en un lado y un fototransistor en el otro, sin conexión eléctrica entre ellos. El lado de 24 V necesita una resistencia en serie para limitar la corriente del LED.

Cálculo de la resistencia y por qué 1 kΩ se quema

La corriente directa recomendada del PC817 en funcionamiento continuo es de unos 10 mA y la caída del LED es aproximadamente 1,2 V. La resistencia debe absorber el resto:

code.cpp
cpp
1R = (24 V - 1,2 V) / 10 mA = 2.280 ohmios -> valor comercial 2,2 k
2P = I^2 x R = (0,0104)^2 x 2200 = 0,24 W -> resistencia de 1/2 W

Muchos ejemplos de Internet usan 1 kΩ, que era el valor correcto para 5 V. A 24 V entrega:

code.cpp
cpp
1I = (24 - 1,2) / 1000 = 22,8 mA
2P = (0,0228)^2 x 1000 = 0,52 W

Veintidós miliamperios no queman el LED de inmediato, pero aceleran la degradación de la ganancia del optoacoplador. El componente pierde sensibilidad durante meses hasta comenzar a perder pulsos de forma intermitente. La resistencia de 1/4 W, que casi todos tienen en el cajón, disipa el doble de lo que soporta y se quema. Usa 2,2 kΩ de 1/2 W.

Del lado del ESP32, el emisor del fototransistor va a GND y el colector al GPIO con una resistencia pull-up. Cuando el sensor se activa, el fototransistor conduce y lleva el pin a nivel bajo. La lógica se invierte a propósito: pieza presente es nivel bajo y la interrupción se dispara en el flanco descendente.

El pull-up interno del ESP32 tiene unos 45 kΩ. Funciona, pero hace lenta la subida. Para el ritmo de este proyecto no es un problema. Si vas a acercar la tasa al límite del sensor, coloca un pull-up externo de 10 kΩ: el flanco queda más definido y el fototransistor deja de conducir más rápido.

Interrupción: qué puede hacer y qué no

La rutina de interrupción debe ser corta. Se ejecuta con el resto del sistema detenido y todo lo que hagas dentro es tiempo durante el cual esperan la red, el watchdog y el propio conteo.

Tres reglas evitan la mayoría de los bloqueos:

La función necesita el atributo IRAM_ATTR para permanecer en la RAM interna. Sin él, la rutina puede estar en una región de flash no disponible durante una escritura, y una interrupción que llegue en ese instante puede hacer caer la placa.

Toda variable compartida entre la interrupción y el bucle principal debe ser volatile. De lo contrario, el compilador guarda el valor en un registro y el bucle lee un número que nunca cambia.

Nada de Serial.print, asignación de memoria, delay o publicación MQTT dentro de la interrupción. Guarda el dato y sal; el bucle principal imprime y publica.

El debounce vive dentro de la interrupción y compara el instante actual con el del último pulso aceptado. Usa el reloj de microsegundos, que tiene resolución suficiente para una ventana de 2 ms:

code.cpp
cpp
1constexpr int PIN_SENSOR = 4;
2constexpr int64_t VENTANA_US = 2000;
3volatile uint32_t conteo = 0;
4volatile int64_t ultimoPulsoUs = 0;
5volatile uint32_t rechazados = 0;
6portMUX_TYPE bloqueo = portMUX_INITIALIZER_UNLOCKED;
7// La interrupcion hace tres cosas y sale: lee el reloj, decide si el pulso
8// es valido e incrementa. Cualquier otra cosa roba tiempo a la pieza siguiente.
9void IRAM_ATTR alDetectarPieza() {
10 const int64_t ahora = esp_timer_get_time();
11 portENTER_CRITICAL_ISR(&bloqueo);
12 if (ahora - ultimoPulsoUs >= VENTANA_US) {
13 ultimoPulsoUs = ahora;
14 conteo++;
15 } else {
16 // No es solo descartarlo: un pulso rechazado es diagnostico. Si este
17 // contador sube con la produccion, la ventana es demasiado grande.
18 rechazados++;
19 }
20 portEXIT_CRITICAL_ISR(&bloqueo);
21}
22void configurarContador() {
23 pinMode(PIN_SENSOR, INPUT_PULLUP);
24 attachInterrupt(digitalPinToInterrupt(PIN_SENSOR), alDetectarPieza, FALLING);
25}

Observa el contador de rechazados. Convierte un límite silencioso en un defecto visible: si la línea acelera y este número empieza a subir con ella, la ventana es demasiado grande. Sin él, el firmware pierde piezas sin decir nunca que las perdió.

Cuenta acumulada, nunca incremental

La tentación es publicar cuántas piezas pasaron desde el último envío. Está mal, y el error solo aparece el día en que la red se vuelve inestable.

Si se pierde el mensaje que decía “12 piezas”, esas 12 piezas desaparecen para siempre. El sistema del otro lado no puede saber que falta algo: recibió una secuencia de números plausibles sin ningún hueco visible.

Publicar el total acumulado desde el arranque lo resuelve por sí solo. Si se pierde un mensaje, el siguiente ya contiene el total correcto y el sistema de gestión calcula la diferencia entre dos lecturas consecutivas. El dato se corrige sin intervención.

El otro lado debe tratar dos situaciones. La primera es el reinicio del contador: cuando el ESP32 se reinicia, el total vuelve a cero y una diferencia negativa debe interpretarse como reinicio, no como producción negativa. Publica un identificador de sesión de arranque para que la distinción sea explícita.

La segunda es no aceptar nunca un comando de poner a cero con retención. Un comando retenido se entrega de nuevo en cada reconexión del cliente y puede poner a cero la producción del turno en mitad de la tarde. Se aplica la misma regla del artículo de integración con SCADA: se retiene el estado, nunca los comandos.

Piezas por minuto sin ruido

Calcular el ritmo con el intervalo entre dos piezas consecutivas produce un número que salta constantemente: cada variación de milisegundos se convierte en decenas de piezas por minuto hacia arriba o hacia abajo, y el gráfico queda ilegible.

Usa una ventana de tiempo fija. En cada intervalo de publicación, toma la diferencia entre el conteo actual y el conteo publicado anteriormente y conviértela:

code.cpp
cpp
1piezas por minuto = (conteo - conteoAnterior) * 60000 / intervaloMs

Con un intervalo de 10 segundos, el resultado es suficientemente estable para una alarma y todavía responde rápido a una parada de línea. Una ventana muy corta devuelve ruido; una muy larga oculta la parada demasiado tiempo.

Contrato MQTT

Publica el acumulado, el ritmo, la sesión de arranque y la calidad. Usa un tópico estable con el valor en la carga:

code.cpp
cpp
1{
2 "ts": 1755990000,
3 "count_total": 184523,
4 "rate_ppm": 4870,
5 "boot_id": 7,
6 "rejected": 0,
7 "quality": 192
8}

El campo rejected viaja a propósito: es la prueba de que el debounce no está comiéndose la producción. En operación normal queda en cero y nadie lo mira. El día en que la línea acelera más allá de lo previsto, sube antes de que la diferencia aparezca en el inventario; eso convierte un error silencioso en una alarma.

Retén la telemetría y el estado del gateway para que el panel aparezca rellenado después de un reinicio. Marca el instante de la lectura, con el reloj sincronizado por NTP; sin NTP sincronizado, publica una calidad incierta en vez de inventar la hora.

Montaje y validación

Paso 1: comprobar el sensor antes del ESP32

Alimenta el sensor inductivo con los 24 V del panel y mide su salida con un multímetro, con el objetivo dentro y fuera del campo. Debes ver que la salida pasa de cerca de cero a cerca de 24 V. Solo después conecta cualquier cosa al ESP32. La mitad de los problemas de conteo termina aquí, en un sensor mal colocado o una distancia de detección fuera de rango.

Paso 2: montar la entrada aislada

El positivo de la salida del sensor va a la resistencia de 2,2 kΩ de 1/2 W y de ahí al ánodo del PC817. El cátodo va a 0 V del lado de 24 V. En el otro lado, emisor a GND del ESP32 y colector al GPIO 4. Confirma que las dos tierras no se tocan en ningún punto: si están conectadas, el aislamiento es solo decoración.

Paso 3: validar el pulso antes de contar

Antes de activar la interrupción, lee el pin en un bucle e imprime el nivel. Mueve el objetivo despacio con la mano y confirma que el valor alterna. Si no alterna, el problema es eléctrico y ningún ajuste de firmware lo corregirá.

Paso 4: contar despacio y comprobar a mano

Pasa exactamente 20 piezas lentamente y compara con el monitor serie. Debe coincidir exactamente. Si cuenta de más, entra ruido o el debounce es demasiado corto para tu entorno. Si cuenta de menos, el pulso es demasiado corto para detectarlo o la ventana ya es demasiado grande.

Paso 5: la prueba que casi nadie hace

Reproduce el ritmo máximo previsto. Si el banco no puede hacerlo, simúlalo: genera pulsos con un segundo ESP32 o un generador de señales en la frecuencia objetivo, usando un número conocido de pulsos, y comprueba que el conteo final coincida.

Para 5.000 piezas por minuto, genera 83 pulsos por segundo durante un minuto y confirma que el total es 5.000, con rejected en cero. Esta es la prueba que separa un contador que funciona de uno que parece funcionar. También es la única forma honesta de sostener un número de la hoja de datos.

Paso 6: comprobar el reinicio

Reinicia el ESP32 mientras cuenta. El total vuelve a cero y el identificador de sesión debe cambiar. Confirma que el otro sistema interpreta esto como reinicio, no como producción negativa ni como un salto de cientos de miles de piezas.

Solución de problemas

Cuenta menos de lo que produce la línea. La ventana de debounce es demasiado grande para el ritmo. Mira el contador de rechazados: si acompaña a la producción, esa es la causa. Reduce la ventana según la tabla inicial.

Cuenta de más, especialmente cuando arranca el variador. Hay ruido acoplado en el cable. No aumentes el debounce como primera reacción, porque cambias un error visible por un límite invisible. Trata la causa: cable blindado con la malla conectada a tierra en un solo extremo, par trenzado, separación de los conductores de potencia y revisión de la tierra del panel.

El conteo se congela después de horas. Busca trabajo pesado dentro de la interrupción o la ausencia de IRAM_ATTR. Comprueba también que el bucle principal no tenga un bloqueo largo que impida la publicación.

El total vuelve a cero solo. Se publicó un comando de reset con retención o no se trató el reinicio. Revisa el identificador de sesión y la política de retención del tópico de comandos.

La producción medida no coincide con la expedida, pero rechazados está en cero. El problema probablemente está antes del ESP32: el objetivo pasa fuera del alcance del sensor, la pieza no activa por su posición o el punto de conteo no corresponde al punto de expedición.

Sobre la validación de este material

El firmware del paquete fue escrito a partir del funcionamiento documentado del ESP32, del PC817 y de sensores inductivos PNP, y revisado línea por línea. Los cálculos de resistencia, disipación y límite de producción están en el texto para que puedas comprobarlos en vez de creerlos.

No se montó en banco con un sensor y una línea reales. Las cifras de límite proceden de la aritmética, no de la medición, y la prueba del Paso 5 existe para que produzcas tu propia evidencia antes de confiar el número a alguien. Donde no medí, digo que no medí.

Ver más tutoriales técnicosIIoT ESP32 Brasil — Documentación práctica