Saltar al contenido
IDE libre

PWM

PWM en la práctica con la ESP32: LED, zumbador y servo

PWM en la ESP32 con MicroPython: frecuencia, duty, por qué 1000 Hz se vuelve 998, brillo de LED, notas en el zumbador y ángulo del servo SG90.

Por Prof. Rodrigo Faustino · Actualizado el · 12 min de lectura

Métricas de este artículo

Palabras
2377
Caracteres
12.643
Tiempo de lectura
~12 min

Un pin digital solo sabe dos cosas: 0 V o 3,3 V. Aun así, la ESP32 deja un LED a media luz, toca una melodía en un zumbador y coloca el brazo de un servo en 37 grados. El truco detrás de las tres cosas es el mismo: encender y apagar el pin muy rápido, controlando cuánto tiempo está en cada nivel. Esto se llama PWM, pulse width modulation, o modulación por ancho de pulso.

En este artículo vamos a entender los dos números que describen un PWM, ver cómo genera la ESP32 esas señales por dentro (incluido el motivo por el que freq() devuelve 998 cuando pediste 1000) y aplicar el PWM en las tres piezas más comunes de laboratorio: el LED, el zumbador pasivo y el servo SG90. Cada una usa el PWM de una manera distinta, y entender esa diferencia es lo que separa a quien copia ejemplos de quien consigue adaptarlos.

Los dos números del PWM

Una señal PWM es una onda cuadrada que se repite. Se describe con dos números:

  • la frecuencia: cuántas veces por segundo se repite el ciclo. A 1000 Hz son mil ciclos por segundo, y cada ciclo dura 1 ms. El tiempo de un ciclo es el período: período = 1 / frecuencia;
  • el duty (ciclo de trabajo): la fracción del período en que el pin está en nivel alto. Con un duty de 50 %, el pin pasa la mitad del ciclo en 3,3 V y la mitad en 0 V; con 10 %, solo una décima parte del ciclo en 3,3 V.

Si el ciclo es lo bastante rápido, quien recibe la señal no percibe los parpadeos, solo la media. Para un LED, la media es el brillo: un duty de 50 % da una tensión media de 1,65 V y un LED más débil. Para un motor, la media es la fuerza. Para un zumbador, en cambio, lo que importa es justamente la vibración, y ahí manda la frecuencia. Y para un servo, lo que cuenta es el ancho exacto de cada pulso, en microsegundos.

Tres ondas cuadradas con el mismo período y duty de 10, 50 y 90 por ciento. Junto a cada una, una barra muestra la tensión media resultante: cerca de 0,33 V, 1,65 V y 2,97 V.
Mismo período, anchos distintos: el duty decide la fracción del ciclo en 3,3 V, y la media es lo que siente el LED (y el ojo).
FastTeacherPara profesores

Del programa al semestre planificado

  • Plan de enseñanza y calendario montados solos
  • Diario de clases listo para pegar en el SIGA
  • Diapositivas y listas de ejercicios con solucionario
Conocer FastTeacher

Versión Community gratis · Windows

PWM en el MicroPython de la ESP32

El PWM es la clase PWM del módulo machine. Recibe un Pin y, opcionalmente, la frecuencia y el duty:

from machine import Pin, PWM
led = PWM(Pin(18), freq=1000, duty_u16=32768)  # 1000 Hz, 50 %
led.duty_u16(16384)   # 25 %
led.duty(256)         # lo mismo en 10 bits (0 a 1023)
led.duty_ns(250000)   # lo mismo en tiempo: 250 µs en alto
led.freq(2000)        # cambia la frecuencia
led.deinit()          # apaga el PWM; el pin queda en 0

Hay tres maneras de decir el duty, cada una útil en un caso:

  • duty_u16(x), de 0 a 65535: la manera portátil, igual en todas las placas con MicroPython. 65535 es siempre encendido;
  • duty(x), de 0 a 1023: la escala antigua de 10 bits, todavía muy usada en tutoriales. En la ESP32 es solo duty_u16 >> 6;
  • duty_ns(x): el tiempo en nivel alto, en nanosegundos. Es el más natural para servos, que entienden el ancho de pulso.

Si no dices nada, el PWM nace con 5000 Hz y un duty de 50 % (duty_u16 32768). Ese valor por defecto es razonable para LEDs y desastroso para servos, como veremos.

Los límites de la ESP32

El periférico que genera PWM en la ESP32 se llama LEDC (el nombre viene de LED control, pero sirve para todo). Sus límites, que el simulador reproduce con los mensajes de la placa, son:

  • frecuencia de 1 Hz a 40 MHz. Fuera de eso: ValueError: frequency must be from 1Hz to 40MHz;
  • 16 canales: hasta 16 pines con PWM al mismo tiempo;
  • como máximo 8 frecuencias distintas al mismo tiempo, porque los canales comparten 8 temporizadores (2 modos con 4 timers cada uno). Pedir una novena frecuencia en un pin nuevo da RuntimeError: out of PWM channels:16;
  • los GPIO 34 a 39 son solo de entrada y no hacen PWM: la placa responde OSError: (-258, 'ESP_ERR_INVALID_ARG').

La regla de las 8 frecuencias rara vez molesta en proyectos de clase, pero aparece cuando alguien intenta tocar varias notas distintas en varios zumbadores al mismo tiempo. Los pines con la misma frecuencia comparten un temporizador, así que diez LEDs a 1000 Hz caben sin problema.

Por qué 1000 Hz se vuelve 998 Hz

Ejecuta este ejemplo y fíjate en lo que imprime freq():

from machine import Pin, PWM
import time

led = PWM(Pin(18), freq=1000, duty_u16=0)
print('pedí 1000, la placa usa', led.freq())

for f in (5000, 2000, 700):
    led.freq(f)
    print('pedí', f, '-> la placa usa', led.freq())

for duty in (6553, 32768, 65535):
    led.duty_u16(duty)
    time.sleep(1)
led.duty_u16(0)

La salida empieza con 998, y no con 1000. La ESP32 no "inventa" la frecuencia: el LEDC cuenta pulsos de un reloj interno de 80 MHz y divide ese reloj por un número. Ese divisor tiene una parte fraccionaria limitada, así que no toda frecuencia sale exacta. Para 1000 Hz no existe un divisor perfecto, y la placa usa el más cercano, que da 998 Hz. Pidiendo 2000 Hz, la placa usa 1997; pidiendo 700, usa 701; 5000 sale exacto. freq() siempre devuelve la frecuencia que la placa realmente genera, no la que pediste.

El mismo cálculo define la resolución del duty, es decir, en cuántos escalones se divide el período. Cuanto más alta la frecuencia, menos cuentas del reloj caben en un período, y menos escalones quedan: a 50 Hz y a 1000 Hz, el duty tiene 16 bits; a 5000 Hz, 13 bits; a 20 kHz, 11 bits; y en el extremo de 40 MHz, solo 1 bit: casi no quedan escalones para el duty. Sigues escribiendo duty_u16 de 0 a 65535 en todos los casos (MicroPython lo convierte), pero en frecuencias muy altas varios valores vecinos se convierten en el mismo escalón.

El simulador hace estas cuentas con las mismas fórmulas del driver de ESP-IDF que usa MicroPython v1.29.0. Por eso el 998 aparece igual en la pantalla y en la placa.

Señal invertida y el pin después del deinit

El constructor también acepta invert=True, que invierte la salida: el duty pasa a medir el tiempo en nivel bajo. Es útil con cargas conectadas "al revés", como el LED RGB de ánodo común, en el que cada color se enciende con el pin en 0. Sin invert, el mismo efecto exige hacer 65535 - duty en todas partes del programa.

Cuando el PWM se apaga con deinit(), el pin sigue siendo una salida, parada en el nivel de reposo: en 0 con la señal normal. Y llamar a Pin(18, Pin.OUT) saca el pin del control del PWM y devuelve el mando al Pin, lo que permite alternar entre los dos usos en el mismo programa.

LED: brillo y fade

Para un LED, el duty es el brillo. El circuito es el mismo LED con resistencia de 220 Ω del primer artículo, ahora en el GPIO18. La frecuencia tiene que ser lo bastante alta para que el ojo no vea los parpadeos: por debajo de unos 30 Hz el LED parpadea visiblemente en lugar de parecer más débil. Cientos o miles de hercios son la elección segura.

El fade (encender y apagar despacio) es solo un bucle que cambia el duty poco a poco. Pensar en porcentaje facilita la cuenta:

from machine import Pin, PWM
import time

led = PWM(Pin(18), freq=1000, duty_u16=0)

for vez in range(3):
    for p in range(0, 101, 5):          # 0, 5, ..., 100 %
        led.duty_u16(p * 65535 // 100)
        time.sleep_ms(30)
    for p in range(100, -1, -5):        # 100, 95, ..., 0 %
        led.duty_u16(p * 65535 // 100)
        time.sleep_ms(30)
led.deinit()

Dos detalles de este código sorprenden a los principiantes. El primero es la división entera //: duty_u16 solo acepta enteros, y p * 65535 / 100 daría un float y un TypeError. El segundo es la duración del fade: son 20 escalones de 30 ms, 600 ms para encender. Un paso menor deja la transición más suave, una espera menor la deja más rápida.

Un tercer detalle es de percepción: el ojo no ve el brillo en línea recta. La diferencia entre 0 % y 10 % parece mucho mayor que la diferencia entre 90 % y 100 %. Los fades que parecen "profesionales" usan escalones pequeños al comienzo y mayores al final (una curva exponencial o cuadrática). Para aprender, los escalones iguales bastan, y dejan clara la matemática.

Colores con el LED RGB

El LED RGB de cátodo común son tres LEDs en un solo cuerpo, rojo, verde y azul, con el negativo compartido. Con un PWM en cada pata, el ojo mezcla los colores: rojo con verde da amarillo, verde con azul da cian, los tres juntos dan blanco. En los colores de pantalla, cada canal va de 0 a 255; multiplicando por 257, 255 se convierte exactamente en 65535.

Hay un detalle físico que la cuenta revela: el rojo tiene una tensión directa de cerca de 2,0 V, y el verde y el azul quedan cerca de 3,0 V. Con 3,3 V en el pin y 220 Ω, el rojo recibe unos 6 mA y los otros dos poco más de 1 mA, y por eso el "blanco" sale rosado. Para equilibrar, o se baja el duty del rojo, o se usan resistencias menores en el verde y en el azul.

FastMindAsistente de IA local

La IA que recuerda tu vida y vive en tu ordenador

  • Memoria a largo plazo con la fuente de cada respuesta
  • Funciona sin conexión: tus datos se quedan en tu PC
  • Lee PDF, diapositivas, fotos y grabaciones de reuniones
Descargar gratis para Windows

Plan Esencial gratis para siempre

Zumbador pasivo: la frecuencia se vuelve nota

El sonido es aire que vibra. El zumbador pasivo tiene una placa pequeña que se mueve con cada cambio de tensión: si el pin se enciende y se apaga 440 veces por segundo, la placa vibra 440 veces por segundo y oyes la nota la. Aquí la frecuencia deja de ser un detalle técnico y se vuelve la nota; el duty de 50 % da la onda más "llena", y el duty 0 hace silencio.

from machine import Pin, PWM
import time

buzzer = PWM(Pin(23), freq=440, duty_u16=0)

def tocar(freq, ms):
    buzzer.freq(freq)
    buzzer.duty_u16(32768)   # onda cuadrada: mitad arriba, mitad abajo
    time.sleep_ms(ms)
    buzzer.duty_u16(0)       # silencio entre las notas
    time.sleep_ms(50)

# do, re, mi, fa, sol, la, si, do
for nota in (262, 294, 330, 349, 392, 440, 494, 523):
    tocar(nota, 250)
buzzer.deinit()

En el simulador, activa el botón Sonido del banco de trabajo antes de ejecutar (el navegador exige un clic antes del primer sonido). Las frecuencias de la escala de do mayor, redondeadas y con el la en 440 Hz, son: do 262, re 294, mi 330, fa 349, sol 392, la 440, si 494 y el do siguiente 523. Una octava más arriba es siempre el doble de la frecuencia.

Fíjate en que tocar() cambia la frecuencia en cada nota. Como solo hay un zumbador, solo hay una frecuencia en uso a la vez, y el límite de 8 frecuencias ni se acerca.

El zumbador activo es otra pieza: tiene un oscilador dentro y toca un tono fijo (cerca de 2300 Hz, según la hoja de datos típica) con tensión continua. Basta con Pin(23, Pin.OUT).on(). Es más simple, pero toca una sola nota. El pasivo, en cambio, queda mudo con tensión parada y necesita el PWM; a cambio, toca cualquier melodía.

Servo SG90: el ángulo viene del ancho del pulso

El servo SG90 es un motorcito con engranajes y un circuito de control que sostiene el brazo en un ángulo entre 0° y 180°. No entiende "porcentaje de brillo". Entiende un pulso que llega 50 veces por segundo (uno cada 20 ms) y cuyo ancho dice el ángulo:

  • pulso de cerca de 0,5 ms (500 µs): 0°;
  • pulso de cerca de 2,4 ms (2400 µs): 180°;
  • en el medio, proporcional: cerca de 1,45 ms, unos 90°.
Línea de tiempo de 20 milisegundos de una señal de servo a 50 Hz, con un pulso de 0,5 ms que corresponde a 0 grados, uno de cerca de 1,45 ms a 90 grados y uno de 2,4 ms a 180 grados, y el brazo del servo dibujado en cada ángulo.
En el servo SG90 lo que importa es el ancho del pulso, no el porcentaje: cada 20 ms, de ~0,5 ms (0°) a ~2,4 ms (180°).

Por eso, con un servo, la manera más clara de definir el duty es duty_ns(), que recibe exactamente el ancho del pulso en nanosegundos (1 µs = 1000 ns). La cuenta de grados a microsegundos queda en una función:

from machine import Pin, PWM
import time

servo = PWM(Pin(18), freq=50, duty_u16=0)   # 50 Hz, todavía sin pulso

def angulo(graus):
    pulso_us = 500 + graus * 1900 // 180     # 0° = 500 µs ... 180° = 2400 µs
    servo.duty_ns(pulso_us * 1000)

for g in (0, 45, 90, 135, 180, 90):
    angulo(g)
    time.sleep(1)

Tres cuidados marcan la diferencia entre un servo que obedece y un servo que tiembla.

Empieza con duty_u16=0. Sin él, el PWM nace con un duty de 50 %: a 50 Hz eso es un pulso de 10 ms, muy fuera del rango, y el servo da un tirón hacia uno de los extremos en cuanto el programa se enciende. Con duty 0, no sale ningún pulso y el brazo se queda donde está hasta el primer angulo().

Dale tiempo al brazo para llegar. El motor gira con velocidad de verdad: cerca de 0,1 s cada 60°, o 0,6° por milisegundo. De 0° a 180° tarda unos 300 ms. Si el programa manda otro ángulo antes, el brazo cambia de rumbo a mitad de camino. El simulador reproduce esa velocidad: el brazo no se "teletransporta".

Los extremos varían. Los 500 y 2400 µs son valores típicos; cada servo tiene los suyos. Si el SG90 del banco de trabajo fuerza y vibra en el 0° o en el 180°, reduce un poco el rango hasta que deje de forzar.

Quien prefiera la escala de 10 bits puede usar duty(): a 50 Hz, el rango útil del SG90 queda aproximadamente entre 26 y 123. Funciona, pero esconde la idea de "ancho del pulso" detrás de un número sin unidad.

Captura de pantalla de PyPlaca con la lección del servo: el código con la función angulo en el editor, el circuito con la ESP32 conectada al servo SG90 por jumpers y el brazo girado.
La lección del servo en el simulador: el brazo gira con velocidad de verdad, cerca de 0,1 s cada 60°.

Alimentación: 5 V, no 3,3 V

El motor del servo consume bastante más corriente que un LED, sobre todo en el arranque. Conectado al 3V3 de la placa, puede hundir la tensión hasta reiniciar la ESP32 en medio del movimiento, un síntoma confuso, porque parece un error del programa. El simulador muestra el aviso servo-em-3v3 en ese caso. La conexión correcta es:

  • cable rojo al VIN (los 5 V que vienen del USB);
  • cable marrón al GND, el mismo GND de la placa;
  • cable naranja al pin del PWM.

El GND común es indispensable: el pulso solo tiene significado si el servo y la ESP32 miden la tensión desde la misma referencia. Con varios servos u otras cargas fuertes, el USB deja de dar abasto (un puerto USB 2.0 entrega hasta 500 mA), y el camino es una fuente externa de 5 V, con el GND conectado al de la placa.

Cuando el PWM no hace lo que esperas

Una guía para los problemas más comunes:

  • El LED parpadea en lugar de quedar débil: frecuencia demasiado baja. Sube a cientos de hercios;
  • TypeError en el duty: el valor es un float. Usa // o int();
  • El servo da un tirón al encender: faltó duty_u16=0 en el constructor;
  • El servo tiembla parado: pulso fuera del rango de tu servo, o alimentación débil;
  • out of PWM channels: demasiadas frecuencias distintas al mismo tiempo, o más de 16 pines con PWM;
  • El pin queda en 0 después del programa: es el deinit() haciendo su trabajo: el pin sigue siendo salida, en nivel bajo.

La pestaña Pines del simulador ayuda en los diagnósticos: muestra cada pin con PWM, la frecuencia real y el duty, y la pestaña Avisos señala los problemas eléctricos.

Adónde ir después

El PWM es el puente entre el mundo digital del programa y las magnitudes continuas: brillo, sonido, posición. El camino inverso (medir una magnitud continua y convertirla en número) es el trabajo del conversor analógico-digital, tema del próximo artículo, El ADC de la ESP32. Juntando los dos, un potenciómetro se convierte en un dimmer de LED o en el control de un servo en pocas líneas.

La ruta de PWM rehace este artículo en lecciones cortas, con el LED a media luz, el fade, los colores del RGB, la escala en el zumbador y el barrido del servo, y termina con un desafío en el que montas tú mismo el servo en el banco de trabajo.

Para practicar en PyPlaca

Rutas

Lecciones

Fichas de componentes

FastMindAsistente de IA local

La IA que recuerda tu vida y vive en tu ordenador

  • Memoria a largo plazo con la fuente de cada respuesta
  • Funciona sin conexión: tus datos se quedan en tu PC
  • Lee PDF, diapositivas, fotos y grabaciones de reuniones
Descargar gratis para Windows

Plan Esencial gratis para siempre

FastTeacherPara profesores

Del programa al semestre planificado

  • Plan de enseñanza y calendario montados solos
  • Diario de clases listo para pegar en el SIGA
  • Diapositivas y listas de ejercicios con solucionario
Conocer FastTeacher

Versión Community gratis · Windows

FastMindAsistente de IA local

La IA que recuerda tu vida y vive en tu ordenador

  • Memoria a largo plazo con la fuente de cada respuesta
  • Funciona sin conexión: tus datos se quedan en tu PC
  • Lee PDF, diapositivas, fotos y grabaciones de reuniones
Descargar gratis para Windows

Plan Esencial gratis para siempre