Verilog Práctico
Capítulo 9. Pulse Width Modulation - 1ª Parte
4 de abril de 2021
En esta primera parte, explicaremos conceptos generales; cómo generar pulsos PWM con la Alhambra FPGA, cómo customizar la frecuencia y el ciclo de trabajo -duty cycle- y como ejemplo de aplicación veremos cómo controlar la velocidad y sentido de giro de un motor de CC.
Ejemplo 1
Generar pulsos PWM con la Alhambra es muy sencillo; tan sólo necesitamos un contador y un comparador. Ambos dispositivos los podemos describir con apenas ocho líneas de código ¡alucinante!
El principio de funcionamiento de un generador PWM se basa -como hemos dicho anteriormente- en un contador y un comparador. El contador (en color azul) va incrementando su valor con cada ciclo de reloj hasta un máximo, luego se resetea y pasa a cero. El comparador compara el valor del contador con una constante fija (en color rojo) y como resultado obtenemos una señal binaria (0 o 1, alto o bajo) que se repite a intervalos regulares.
La siguiente imagen ilustra lo que acabamos de explicar. Mientras el contador sea menor que la constante, la salida es 1 y en caso contrario 0. Modificando el valor de la constante obtenemos diferentes anchos de pulso, es decir, una señal PWM.
Vamos a ver lo que acabamos de explicar con un ejemplo práctico.
Esquema
Conectamos un led externo para visualizar la forma de onda con un osciloscopio.
Código Verilog
Declaramos la señal de reloj y una salida que mostraremos con un led externo.
module pwm (input clk, output led);
Utilizaremos un contador hasta 100 -para que sean número redondos- luego con 7 bits será suficiente.
reg [6:0] counter = 0;
Mientras el contador sea menor de 100, lo incrementamos una unidad.
if (counter < 100) counter <= counter + 1;
Cuando llegue a 100 lo reseteamos.
else counter <= 0;
Utilizaremos una estructura condicional para implementar el comparador.
Mientras el contador sea menor que 20 → led = 1; si no → led = 0
assign led = (counter < 20) ? 1:0;
Lo ponemos todo junto y queda así:
module pwm (input clk, output led);
reg [6:0] counter = 0;
always @ ( posedge clk) begin
if (counter < 100) counter <= counter + 1;
else counter <= 0;
end
assign led = (counter < 20) ? 1:0;
endmodule
Archivo .pcf
set_io clk 21
set_io led 119
Lo subimos a la Alhambra y esto es lo que vemos: una señal con un periodo de 8.4 μs y una frecuencia de 119 KHz...
...con un ancho de pulso de 1.6 μs de duración; que curiosamente es el 20 % de 8.4 μs.
Ejemplo 2
Toda señal PWM se caracteriza por dos parámetros: frecuencia y ciclo de trabajo o duty cycle.
Podemos customizar la señal PWM en frecuencia, modificando el tamaño del contador; a mayor número de bits, menor frecuencia. El duty cycle se modifica con la constante del comparador.
En el siguiente ejemplo vamos a declarar un registro de 15 bits, un contador hasta 24.000 y una constante de 4.800 (el 20% de 24.000). Estos datos no son aleatorios, seguid leyendo y veréis el resultado.
module pwm (input clk, output led);
reg [14:0] counter = 0;
always @ ( posedge clk) begin
if (counter < 24000) counter <= counter + 1;
else counter <= 0;
end
assign led = (counter < 4800) ? 1:0;
endmodule
El archivo .pcf no cambia.
set_io clk 21
set_io led 119
Lo subimos a la Alhambra y este es el resultado: una señal PWM de 500 Hz con un duty cycle de 20 %.
Nos ha salido una frecuencia de 500 Hz exactos ¿casualidad? no creo. Veamos un poco de matemáticas.
Si la Alhambra tiene un reloj de 12 MHz, en contar hasta 24000 tarda: 24000/12·106 = 2 ms
Como la frecuencia es la inversa del periodo f = 1 / 2 ms = 500 Hz
Como hemos explicado antes, la frecuencia de la señal la definimos con el tamaño del contador:
if (counter < 24000) counter <= counter + 1;
Y el duty cycle lo definimos con la constante 4800
assign led = (counter < 4800) ? 1:0;
Hay que tener en cuenta que el número de bits del registro contador reg [14:0] counter = 0 permita contar hasta la cantidad deseada.
Entendiendo estos sencillos cálculos podemos modelar señales PWM de acuerdo con las necesidades de nuestro diseño.
Ejemplo 3
En este ejemplo vamos a modelar cuatro señales PWM a la vez, cada una con un ciclo de trabajo diferente. Sacaremos cada una de las señales por un led externo.
Esquema
Código Verilog
Declaramos un registro de 4 bits como salida output [3:0] led
Asignamos a cada led una constante, de acuerdo al ciclo de trabajo que queramos.
assign led[0] = (counter < 4800) ? 1:0; // 20 %
assign led[1] = (counter < 9600) ? 1:0; // 40 %
assign led[2] = (counter < 14400) ? 1:0; // 60 %
assign led[3] = (counter < 19200) ? 1:0; // 80 %
El código Verilog queda así:
module pwm (input clk, output [3:0] led);
reg [14:0] counter = 0;
always @ ( posedge clk) begin
if (counter < 24000) counter <= counter + 1;
else counter <= 0;
end
assign led[0] = (counter < 4800) ? 1:0; // 20 %
assign led[1] = (counter < 9600) ? 1:0; // 40 %
assign led[2] = (counter < 14400) ? 1:0; // 60 %
assign led[3] = (counter < 19200) ? 1:0; // 80 %
endmodule
Archivo .pcf
set_io clk 21
set_io led[0] 119
set_io led[1] 117
set_io led[2] 115
set_io led[3] 113
Lo subimos a la Alhambra y vemos cuatro señales PWM de 500 Hz, pero cada una con un duty cycle diferente.
Ejemplo 4
A continuación vamos a diseñar una señal PWM con un duty cycle variable. Utilizaremos los pulsadores de la Alhambra SW1 y SW2 para aumentar o disminuir el ciclo de trabajo y sustituiremos el led de salida por un motor de CC.
Esquema
Aplicamos los pulsos PWM a un MOSFET mediante un driver MIC4427.
El driver se coloca por seguridad, para que en caso de cortocircuito no se dañe la Alhambra. Si no lo tenemos, se puede conectar la puerta del MOSFET directamente en la salida de la Alhambra, manteniendo la resistencia de 10K entre la puerta y GND.
Código Verilog
En los ejemplos anteriores el ciclo de trabajo era fijo y lo configurábamos con una constante. En este ejemplo utilizaremos una variable duty_cycle cuyo valor aumentará o disminuirá cada vez que pulsemos los switches SW1 y SW2 en la Alhambra.
Declaramos las entradas y salidas que vamos a utilizar.
module pwm (input clk, sw1, sw2, output motor_pwm);
reg [14:0] counter = 0;
reg [14:0] duty_cycle = 12000;
Para mejorar el funcionamiento del circuito, cada vez que accionemos los pulsadores de la Alhambra, generaremos un pulso único (tic) con el código visto en el Capítulo 8.
Declaramos dos variables intermedias duty_up y duty_dwn, donde almacenamos los pulsos tic.
wire duty_up, duty_dwn;
tic switch1 (clk, sw1, duty_up);
tic switch2 (clk, sw2, duty_dwn);
Este bloque aumenta o disminuye el ciclo de trabajo en un 10% cada vez que se pulsa un botón.
always @(posedge clk)
begin
if (duty_up == 1 && duty_cycle <= 21600)
duty_cycle <= duty_cycle + 2400;
else if (duty_dwn == 1 && duty_cycle >= 2400)
duty_cycle <= duty_cycle - 2400;
end
Generador de PWM variable de 500 Hz
always @(posedge clk) begin
if (counter < 24000) counter <= counter + 1;
else counter <= 0;
end
assign motor_pwm = (counter < duty_cycle) ? 1:0;
Lo ponemos todo junto y queda así.
module pwm (input clk, sw1, sw2, output motor_pwm);
reg [14:0] counter = 0;
reg [14:0] duty_cycle = 12000;
wire duty_up, duty_dwn;
tic switch1 (clk, sw1, duty_up);
tic switch2 (clk, sw2, duty_dwn);
always @(posedge clk)
begin
if (duty_up == 1 && duty_cycle <= 21600)
duty_cycle <= duty_cycle + 2400;
else if (duty_dwn == 1 && duty_cycle >= 2400)
duty_cycle <= duty_cycle - 2400;
end
// PWM generator 500 Hz
always @(posedge clk) begin
if (counter < 24000) counter <= counter + 1;
else counter <= 0;
end
assign motor_pwm = (counter < duty_cycle) ? 1:0;
endmodule
// Modulo generador tic para los pulsadores sw1 y sw2
module tic (input clk, btn_in, output out);
reg d2;
reg r_in;
always @(posedge clk)
d2 <= btn_in;
always @(posedge clk)
r_in <= d2;
reg btn_prev = 0;
reg btn_out = 0;
reg [16:0] counter = 0;
always @(posedge clk) begin
if (btn_prev ^ r_in == 1'b1) begin
counter <= 0;
btn_prev <= r_in;
end
else if (counter[16] == 1'b0)
counter <= counter + 1;
else
btn_out <= btn_prev;
end
//-- Generar tic en flanco de subida del boton
reg old;
always @(posedge clk)
old <= btn_out;
assign out = !old & btn_out;
endmodule
Archivo .pcf
set_io sw1 10
set_io sw2 11
set_io clk 21
set_io motor_pwm 112
Lo subimos a la Alhambra y lo que vemos es una señal PWM que aumenta cuando pulsamos SW1 y disminuye cuando pulsamos SW2. Esta señal PWM variable aplicada en la puerta del MOSFET, se traduce en un aumento / disminución de la velocidad del motor.
Si colocamos las sondas del osciloscopio en los puntos indicados, vemos como la señal aplicada en la puerta del MOSFET es igual a la señal PWM que sale de la Alhambra, pero amplificada. EL valor de salida depende de la tensión de alimentación V+ aplicada en el pin 6; el rango admitido es 4.5 a 18V. Normalmente con 5V es suficiente para disparar la puerta de un MOSFET.
Ejemplo 5
Para terminar vamos a aplicar lo aprendido en este tutorial para diseñar un circuito que controle la velocidad y el sentido de giro de un motor de CC.
Esquema
Para controlar la velocidad y el sentido de giro del motor, voy a echar mano del módulo L298. Este circuito integrado nos permite controlar dos motores de CC de manera independiente.
La velocidad se controla con pulsos PWM aplicados en las entradas ENABLE A y ENABLE B, para cada motor conectado en Motor A y Motor B. El sentido de giro se controla con las entradas IN1, IN2 para el Motor A e IN3, IN4 para el Motor B. No voy a extenderme más sobre el funcionamiento del L298 porque este módulo es ampliamente usado y conocido en el mundillo maker. Más info aquí.
Como en el ejemplo anterior, la velocidad la controlamos con los pulsadores sw1 y sw2 y el sentido de giro lo seleccionamos con un interruptor externo conectado en D0.
Recomiendo intercalar un line-driver o un optoacoplador, entre la Alhambra y el L298 para eliminar el ruido y mejorar la calidad de las señales.
Código Verilog
Partiendo del código del ejemplo 4, vamos a añadir la entrada dir_sw y un registro reg [1:0] dir donde almacenamos las señales para controlar la dirección, donde dir[0] es IN1 y dir[1] es IN2.
Dependiendo de si el switch dir_sw está o no accionado, asignamos a las entradas IN1 e IN2 los valores 10 o 01, mediante los bits 0 y 1 del registro dir.
always @(posedge clk)
begin
if (direction == 1)
dir <= 2'b10;
else
dir <= 2'b01;
end
El switch conectado en D0 lo vamos a pasar por un circuito debounce. Consulta el Capítulo 8 para más información.
debouncer db (clk, dir_sw, direction);
A grandes rasgos esos son los añadidos necesarios. Lo ponemos todo junto y el código Verilog para la Alhambra queda así:
module pwm (input clk, sw1, sw2, dir_sw, output motor_pwm, output reg [1:0] dir = 0);
reg [14:0] counter = 0;
reg [14:0] duty_cycle = 12000;
wire duty_up, duty_dwn, direction;
tic switch1 (clk, sw1, duty_up);
tic switch2 (clk, sw2, duty_dwn);
debouncer db (clk, dir_sw, direction);
always @(posedge clk)
begin
if (direction == 1)
dir <= 2'b10;
else
dir <= 2'b01;
end
always @(posedge clk)
begin
if (duty_up == 1 && duty_cycle <= 21600)
duty_cycle <= duty_cycle + 2400;
else if (duty_dwn == 1 && duty_cycle >= 2400)
duty_cycle <= duty_cycle - 2400;
end
// PWM generator 500 Hz
always @(posedge clk) begin
if (counter < 24000) counter <= counter + 1;
else counter <= 0;
end
assign motor_pwm = (counter < duty_cycle) ? 1:0;
endmodule
// Modulo tic para los pulsadores sw1 y sw2
module tic (input clk, btn_in, output out);
reg d2;
reg r_in;
always @(posedge clk)
d2 <= btn_in;
always @(posedge clk)
r_in <= d2;
reg btn_prev = 0;
reg btn_out = 0;
reg [16:0] counter = 0;
always @(posedge clk) begin
if (btn_prev ^ r_in == 1'b1) begin
counter <= 0;
btn_prev <= r_in;
end
else if (counter[16] == 1'b0)
counter <= counter + 1;
else
btn_out <= btn_prev;
end
//-- Generar tic en flanco de subida del boton
reg old;
always @(posedge clk)
old <= btn_out;
assign out = !old & btn_out;
endmodule
//-- Circuito Debouncer para dir_switch
module debouncer(input wire btn_in, clk, output wire out);
reg btn_prev = 0;
reg btn_out = 0;
reg [16:0] counter = 0;
always @(posedge clk) begin
if (btn_prev ^ btn_in == 1'b1)
begin
counter <= 0;
btn_prev <= btn_in;
end
else if (counter[16] == 1'b0)
counter <= counter + 1;
else
btn_out <= btn_prev;
end
assign out = btn_out;
endmodule
Archivo .pcf
set_io clk 21
set_io sw1 10
set_io sw2 11
set_io dir_sw 119
set_io motor_pwm 112
set_io dir[0] 113
set_io dir[1] 114
Eso es todo ¡hasta pronto!
Links
Simply Embedded How to Create PWM in Verilog on FPGA
FPGA4student Verilog code for PWM generator
Descargas
Descarga el código Verilog de los ejemplos vistos en este tutorial aquí abajo.