Una señal PWM es una onda digital periódica cuyo tiempo en nivel alto podemos variar para controlar la energía entregada a una carga.
Hemos visto como hacer señales binarias (digitales). Pero, ¿y si queremos que el LED brille al 50%? ¿O si queremos mover un brazo robótico a una posición específica de 45 grados?
Para regular un LED o un servo no necesitamos generar un voltaje analógico continuo. Podemos utilizar PWM (Pulse Width Modulation o modulación por ancho de pulso) desde una salida digital.
Hoy vamos a diseñar nuestro propio generador PWM desde cero en Verilog.
¿Qué es el PWM?
El concepto es engañar al ojo (o al motor) mediante la velocidad. Si encendemos y apagamos una señal muy rápido, el mundo físico percibe el promedio de energía.
- Periodo (
T): El tiempo total de un ciclo (encendido + apagado). - Frecuencia (
f = 1/T): Cuántos ciclos ocurren por segundo. - Duty Cycle (Ciclo de Trabajo): El porcentaje de tiempo que la señal está en ALTO (1).
- Si el Duty Cycle es 100%, tenemos 3.3V constantes (Brillo máximo).
- Si el duty cycle es 50%, la salida pasa la mitad del tiempo a 3,3 V y la otra mitad a 0 V. Una carga lenta o un filtro perciben un valor medio de aproximadamente 1,65 V.
- Si el Duty Cycle es 0%, tenemos 0V (Apagado).
En Arduino usas analogWrite(pin, 128).
En FPGA, nosotros construimos el circuito que hace ese analogWrite. Podemos generar muchos canales en paralelo, limitados por los pines, la lógica y el timing disponibles.
Implementación en hardware
Generar un PWM en una FPGA es increíblemente sencillo. Solo necesitamos dos piezas que ya conocemos:
- Un Contador que sube continuamente (diente de sierra).
- Un Comparador que decide si la salida es 0 o 1.
La lógica es:
“Mientras el valor del contador sea MENOR que mi valor deseado (Duty), mantén la salida en 1. Si lo supera, ponla a 0”.
Generador PWM genérico
Vamos a hacer un módulo parametrizable para poder reusarlo.
module generador_pwm #(
parameter N_BITS = 8 // Resolución de 8 bits (0-255)
)(
input wire clk,
input wire rst,
input wire [N_BITS-1 : 0] duty, // Valor de "potencia" deseada
output reg pwm_out
);
reg [N_BITS-1 : 0] contador = 0;
always @(posedge clk) begin
if (rst) begin
contador <= 0;
pwm_out <= 0;
end else begin
// 1. Incrementamos el contador (Overflow automático al llegar a 255)
contador <= contador + 1;
// 2. Comparador
if (contador < duty)
pwm_out <= 1'b1;
else
pwm_out <= 1'b0;
end
end
endmoduleEfecto breathing para un LED
Vamos a probar nuestro módulo. Haremos que un LED suba y baje de intensidad suavemente, como la luz de reposo de un Mac.
Para ello, necesitamos instanciar nuestro generador_pwm y alimentar su entrada duty con otro contador más lento que suba y baje (una onda triangular).
module breathing_led (
input wire clk, // 12 MHz
output wire led
);
// 1. Contador lento para variar la intensidad
// Necesitamos que sea lento para que el ojo vea el cambio.
// Usaremos un contador grande y cogeremos los bits superiores.
reg [23:0] contador_lento = 0;
always @(posedge clk) begin
contador_lento <= contador_lento + 1;
end
// Truco: Usamos los bits altos del contador lento como valor de duty.
// Para hacer efecto sube-baja (triangular) se requiere un poco más de lógica,
// pero para efecto sierra (sube y apaga de golpe) nos vale esto directo:
wire [7:0] nivel_brillo = contador_lento[23:16];
// 2. Instancia del PWM
generador_pwm #( .N_BITS(8) ) mi_pwm (
.clk(clk),
.rst(1'b0),
.duty(nivel_brillo),
.pwm_out(led)
);
endmoduleFrecuencia del PWM:
Con un reloj de 12 MHz y 8 bits (256 pasos), la frecuencia es 12.000.000 / 256 = 46.875 Hz. Es mucho más rápido de lo que el ojo puede distinguir y resulta adecuado para atenuar LED.
Control de servomotores
Los servomotores de modelismo (esos motorcitos azules SG90) se controlan por PWM, pero son muy exigentes con los tiempos. No les vale cualquier frecuencia.
Muchos servos de modelismo utilizan aproximadamente:
- Periodo: Alrededor de 20 ms (50 Hz).
- Pulso alto (
t_on): Suele durar entre 1 ms y 2 ms.
- 1,0 ms: un extremo.
- 1,5 ms: posición central.
- 2,0 ms: el otro extremo.
El reto matemático
Nuestro reloj va a 12 MHz. Un periodo de 20 ms contiene 12.000.000 × 0,020 = 240.000 ciclos, así que necesitamos un contador que vaya de 0 a 239.999.
Y necesitamos poner la salida a 1 solo durante los primeros X ciclos:
- Para el centro (1,5 ms):
12.000.000 × 0,0015 = 18.000ciclos.
Los tiempos y el ángulo real dependen de cada servo. Consulta su hoja de datos antes de acercarte a los extremos mecánicos.
Driver de servo en Verilog
Vamos a crear un módulo específico, porque el generador_pwm simple que hicimos antes (basado en desbordamiento natural ) no nos da el control exacto de la frecuencia de 50 Hz.
module servo_driver (
input wire clk, // 12 MHz
input wire [7:0] posicion, // Entrada de 0 a 255 (0 = 1ms, 255 = 2ms)
output reg servo_pin
);
// Parámetros físicos
localparam CLK_HZ = 12000000;
localparam PRESCALER_MAX = 240000; // 20ms a 12MHz
// Límites de pulso
localparam PULSO_MIN = 12000; // 1ms (12000 ciclos)
localparam PULSO_MAX = 24000; // 2ms (24000 ciclos)
// Contador principal del periodo (0 a 239.999)
reg [17:0] contador_periodo = 0;
// Cálculo del ancho de pulso deseado
// Mapeamos (0-255) al rango (12000-24000)
// Formula: ancho = MIN + (posicion * (MAX-MIN) / 255)
wire [17:0] ancho_pulso;
assign ancho_pulso = PULSO_MIN + (posicion * (PULSO_MAX - PULSO_MIN) / 255);
always @(posedge clk) begin
// 1. Control del Periodo (50Hz)
if (contador_periodo == PRESCALER_MAX - 1) begin
contador_periodo <= 0;
end else begin
contador_periodo <= contador_periodo + 1;
end
// 2. Generación del Pulso
if (contador_periodo < ancho_pulso) begin
servo_pin <= 1'b1;
end else begin
servo_pin <= 1'b0;
end
end
endmoduleAcabamos de crear un driver de hardware cuyo periodo no depende de un bucle de software. Su precisión queda limitada por el reloj, el redondeo entero y el timing de salida, pero otros módulos del diseño no retrasan el pulso mientras se cumplan las restricciones temporales.