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


KEYES L298 Motor Driver


Descargas


Descarga el código Verilog de los ejemplos vistos en este tutorial aquí abajo.