Parameterized PWM controller

Digital outputs can either go ON or OFF. Analog signals, on the other side, can smoothly assume multiple values in a range.

There is a technique that emulates analog behavior with a digital output. That technique is PWM, namely, Pulse Width Modulation. It can be implemented as pulses with varying ‘high’ and ‘low’ duration. A rather simple implementation of PWM is to take a fixed output frequency and vary only the duty cycle. If the pulses are fast enough compared to the response time of the system, a PWM is equivalent to a varying analog signal, whose amplitude is proportional to the duty cycle.

The maximum output frequency of the PWM output depends on the clock signal used to generate the PWM, and on the resolution desired.

For example, if our clock frequency fc=100MHz, and we want our signal to have 64 different ‘analog’ values, the max. PWM frequency output will be 100MHz/64 ~1.5MHz.

In many applications we don’t need a PWM which is so fast. In that case, the clock frequency is first passed through a divider, and the divider output is used to generate the PWM.

This is what the code below does:

  1. library ieee;
  2. use ieee.std_logic_1164.all;
  3. use ieee.numeric_std.all;
  4. use work.pwm_reg_pack.ALL;
  5.  
  6. entity pwm is
  7. port (
  8. clk : in std_logic;
  9. rst : in std_logic;
  10.  
  11. -- signals from top module to registers sub-module
  12. en : in std_logic;
  13. duty : in std_logic_vector(DUTY_CYCLE_W-1 downto 0);
  14. pwm_out : out std_logic
  15. );
  16. end entity pwm;
  17.  
  18. architecture rtl of pwm is
  19. signal clk_en : std_logic;
  20. signal cnt : unsigned(PERIOD_W-1 downto 0);
  21. signal cnt_duty : unsigned(DUTY_CYCLE_W-1 downto 0);
  22.  
  23. begin
  24. cnt_pr : process(clk, rst)
  25. begin
  26. if (rst = '1') then
  27. cnt <= (others => '0');
  28. clk_en <= '0';
  29. elsif (rising_edge(clk)) then
  30. -- default
  31. clk_en <= '0';
  32.  
  33. if (en = '1') then
  34. if (cnt = 0) then
  35. cnt <= to_unsigned(PERIOD-1, cnt'length);
  36. clk_en <= '1';
  37. else
  38. cnt <= cnt - 1;
  39. end if;
  40. end if;
  41. end if;
  42. end process cnt_pr;
  43.  
  44. cnt_duty_pr : process(clk, rst)
  45. begin
  46. if (rst = '1') then
  47. cnt_duty <= (others => '0');
  48. pwm_out <= '0';
  49. elsif (rising_edge(clk)) then
  50. if (clk_en = '1') then
  51. cnt_duty <= cnt_duty + 1;
  52. end if;
  53. if (cnt_duty < unsigned(duty)) then
  54. pwm_out <= '1';
  55. else
  56. pwm_out <= '0';
  57. end if;
  58. end if;
  59. end process cnt_duty_pr;
  60.  
  61. end rtl;

The first counter cnt is the frequency divider, which originates the clk_en signal. The clk_en signal is used to increment the duty cycle counter cnt_duty. The value of cnt_duty cycles from 0 to max on each cycle. During the cycle, while the counter is less than the programmed duty value, the output value pwm_out is high, otherwise, it is low.

The values for the constants used are included in a package file:

  1. library ieee;
  2. use ieee.std_logic_1164.all;
  3. use ieee.numeric_std.all;
  4. use ieee.math_real.all;
  5.  
  6. package pwm_reg_pack is
  7.  
  8. -------------------------------------------------------------------------
  9. -- Data size definitions
  10. -------------------------------------------------------------------------
  11. constant SYS_CLK : natural := 100_000; -- System clock in kHz
  12. constant PWM_CLK : natural := 500; -- PWM clock in kHz
  13. constant DUTY_CYCLE_W : natural := 5; -- PWM resolution in bits
  14. constant PERIOD : natural := SYS_CLK / (PWM_CLK * 2**DUTY_CYCLE_W);
  15. constant PERIOD_W : natural := integer(ceil(log2(real(PERIOD+1))));
  16.  
  17. end pwm_reg_pack;

On these simulation waveforms we can see how the PWM works. Notice that the implementation is such that the output can be zero all the time, but it cannot be ‘high’ all the time.

pwm_sim

A zoom in on one cycle of the PWM:

pwm_sim1

A further zoom showing when the cnt_duty value becomes greater than duty and the output goes low (in this graph, cnt_duty is shown with values and not as ‘analog graph’, for clarity):

pwm_sim2

The module sources, including simulation testbench, can be downloaded at Github.


Proposed exercises:

  1. Identify on the simulation waveforms the maximum output value
  2. Change the testbench so it will output exactly five cycles for each possible duty value
  3. Change the source so the output can have a value of ‘allways high’ when duty = max. value
Advertisements