La modulación por ancho de pulso, conocida por su sigla en inglés PWM, es una de las técnicas más rentables de la electrónica digital: permite generar cualquier voltaje promedio entre 0 y la tensión de alimentación usando nada más que una salida digital común. Espressif publicó en su blog para desarrolladores una guía que recorre el tema de punta a punta, desde la teoría de la señal hasta el filtro que la convierte en un valor continuo utilizable.

Los dispositivos de Espressif incluyen dos periféricos distintos para generar PWM, y elegir el correcto es el primer paso.

PeriféricoUso previstoCapacidades
ledcControl de LED e iluminaciónPWM simple, hasta 8 canales, 4 temporizadores
mcpwmControl de motoresSalidas complementarias, tiempos muertos programables, detección de fallas, salida trifásica

La guía se concentra en el primero, ledc, que es la opción más habitual en aplicaciones de iluminación y la más simple para partir.

¿Qué es exactamente una señal PWM?

Una señal PWM es una onda rectangular con dos niveles de voltaje: 0 y Von. La señal permanece en Von durante un tiempo Ton y en 0 durante un tiempo Toff. El intervalo entre dos flancos de subida consecutivos define el periodo de conmutación Ts, que es la suma de ambos.

Partes de una señal PWM
Partes de una señal PWM

El parámetro más importante es el ciclo de trabajo (D), que se calcula como Ton dividido por Ts. Como Ton no puede superar a Ts, el ciclo de trabajo va de 0 a 1 y normalmente se expresa en porcentaje. El segundo parámetro clave es la frecuencia de conmutación (fs), que es simplemente el inverso del periodo.

Al integrar la señal sobre un periodo completo, el voltaje promedio resulta ser D multiplicado por Von, es decir, directamente proporcional al ciclo de trabajo. Esa relación lineal es la razón de ser del PWM: variando D entre 0 y 1 se obtiene cualquier voltaje promedio del rango con una sola salida digital.

¿Cómo se genera la señal de forma analógica y digital?

En el mundo analógico, lo clásico es alimentar un comparador con una onda triangular o de diente de sierra y compararla contra una señal continua de control.

Generación analógica de PWM con comparador
Generación analógica de PWM con comparador

Cuando el voltaje de control iguala el valor peak de la triangular, la salida queda alta todo el periodo. Cuando es cero, nunca sube. En el rango intermedio, el ancho del pulso escala linealmente con el voltaje de control, de modo que el ciclo de trabajo termina siendo la razón entre el voltaje de control y el voltaje peak.

Generación digital de PWM con contador y comparador
Generación digital de PWM con contador y comparador

La versión digital reemplaza todo eso por un contador alimentado con un reloj y un comparador interno. La resolución del ciclo de trabajo depende del tamaño del contador: con 12 bits se obtienen 4.096 pasos, equivalentes a una resolución de 0,024%. El costo es que la frecuencia de conmutación cae en la misma proporción, según la relación fs = fclock / 2 elevado a N bits.

Esa tensión entre resolución y frecuencia es el compromiso central del diseño. Tomando el valor de reloj de 4 MHz que menciona la propia guía, la aritmética queda así:

ResoluciónPasos de dutyResolución de dutyFrecuencia máxima con reloj de 4 MHz
8 bits2560,39%15.625 Hz
10 bits1.0240,098%3.906 Hz
12 bits4.0960,024%977 Hz
13 bits8.1920,012%488 Hz

Frecuencias más bajas obligan a filtros más grandes para eliminar los armónicos, un punto que la guía retoma al final.

¿Cómo se configura el periférico LEDC?

El periférico LEDC tiene dos partes: el temporizador y el canal. Un mismo temporizador puede alimentar varios canales, con la salvedad de que todos los canales que lo compartan quedan atados a la misma resolución y a la misma frecuencia.

Diagrama del periférico LEDC de Espressif
Diagrama del periférico LEDC de Espressif

El temporizador se configura llenando la estructura ledc_timer_config_t y pasándola a ledc_timer_config():

C
ledc_timer_config_t ledc_timer = {
    .speed_mode      = LEDC_MODE,
    .duty_resolution = LEDC_DUTY_RES,
    .timer_num       = LEDC_TIMER,
    .freq_hz         = LEDC_FREQUENCY,
    .clk_cfg         = LEDC_AUTO_CLK,
};

ledc_timer_config(&ledc_timer);

Los campos significan lo siguiente:

  • speed_mode: selecciona el modo de velocidad del grupo de temporizadores. En todos los chips salvo el ESP32 original, la única opción es LEDC_LOW_SPEED_MODE.
  • duty_resolution: fija la resolución en bits. Por ejemplo, LEDC_TIMER_13_BIT entrega un rango de duty de 0 a 8.191.
  • timer_num: elige cuál de los cuatro temporizadores por hardware se configura, entre LEDC_TIMER_0 y LEDC_TIMER_3.
  • freq_hz: la frecuencia de salida deseada en hertz, por ejemplo 5.000 para 5 kHz.
  • clk_cfg: la fuente de reloj. Con LEDC_AUTO_CLK el propio driver elige la fuente adecuada según la resolución y la frecuencia pedidas al inicializar.

El canal usa una estructura análoga:

C
ledc_channel_config_t ledc_channel = {
    .speed_mode = LEDC_MODE,
    .channel    = LEDC_CHANNEL,
    .timer_sel  = LEDC_TIMER,
    .gpio_num   = LEDC_OUTPUT_IO,
    .duty       = 0,   // Parte con 0% de ciclo de trabajo
    .hpoint     = 0,
};

ledc_channel_config(&ledc_channel);

Acá channel selecciona cuál de los canales disponibles se usa (los dispositivos de Espressif tienen típicamente entre 6 y 8), gpio_num define el pin de salida y duty fija el ciclo de trabajo inicial, cuyo valor máximo equivale a 2 elevado a la cantidad de bits de resolución.

El campo hpoint merece atención aparte: define en qué valor del contador la salida se pone en alto dentro del ciclo. Dejarlo en 0 es el comportamiento estándar, pero si dos canales comparten temporizador, ese parámetro permite generar una versión desfasada de la señal, algo muy útil en varios escenarios de conversión de potencia.

¿Cómo se cambia el ciclo de trabajo en ejecución?

La función base es ledc_set_duty(), que espera un entero entre 0 y el valor de fondo de escala. Como en la práctica uno prefiere pensar en una fracción entre 0 y 1, la guía propone envolverla:

C
#define LEDC_FS (1 << LEDC_DUTY_RES)   // Fondo de escala: 2^13

esp_err_t ledc_set_duty_cycle(float duty) {
    esp_err_t ret = ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, (int)(duty * LEDC_FS));
    if (ret != ESP_OK) {
        return ret;
    }
    ret = ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
    return ret;
}

El detalle que suele morder a quien recién parte: no basta con ledc_set_duty(). El nuevo valor recién toma efecto cuando se llama a ledc_update_duty(), por eso el envoltorio encadena ambas llamadas y propaga el código de error.

Salida PWM medida en el osciloscopio
Salida PWM medida en el osciloscopio

Con el ejemplo armado sobre el GPIO 25 y un osciloscopio conectado, la medición muestra una frecuencia de 4 kHz y un ciclo de trabajo de 75% exacto.

¿Cómo se convierte el PWM en voltaje continuo?

Que el promedio de la señal sea D por Von no significa que la salida sea continua. Ese promedio es apenas la componente DC de la onda: el resto son componentes alternas que hay que eliminar si lo que se busca es un voltaje limpio.

Al ser periódica, la señal PWM se puede descomponer en serie de Fourier. Lo relevante para el diseño es que los armónicos aparecen en la frecuencia de conmutación y en sus múltiplos enteros, es decir en 2fs, 3fs y así sucesivamente.

Espectro de una señal PWM con ciclo de trabajo de 0,25
Espectro de una señal PWM con ciclo de trabajo de 0,25

La amplitud del armónico j tiene forma cerrada y depende del ciclo de trabajo, lo que explica por qué el espectro cambia de aspecto según dónde se ubique D. El gráfico anterior corresponde a un ciclo de trabajo de 0,25.

Topología del filtro RC
Topología del filtro RC

Para quedarse solo con la componente continua hay que atenuar todo lo que esté en fs y por encima, dejando el valor DC intacto. El filtro más simple que hace ese trabajo es una red resistor-condensador, que se comporta como un divisor de voltaje dependiente de la frecuencia. Ahí aparece el compromiso final del que advierte la guía: menos rizado exige una constante de tiempo más grande, y una constante de tiempo más grande significa una respuesta más lenta ante cambios del ciclo de trabajo.

Esa es la decisión de diseño que separa un atenuador de LED que funciona de uno que parpadea o que reacciona con retardo visible.