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. However, one rather simple implementation 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 that fast. In that case, the clock frequency is first passed through a divider, and the divider output is used to generate the PWM.
The code for the PWM controller presented below is based in this idea, first it instantiates a clock divider and then PWM generation:
[CODE]library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.pwm_reg_pack.ALL;
entity pwm is
port (
clk : in std_logic;
rst : in std_logic;
— signals from top module to registers sub-module
en : in std_logic;
duty : in std_logic_vector(DUTY_CYCLE_W-1 downto 0);
pwm_out : out std_logic
);
end entity pwm;
architecture rtl of pwm is
signal clk_en : std_logic;
signal cnt : unsigned(PERIOD_W-1 downto 0);
signal cnt_duty : unsigned(DUTY_CYCLE_W-1 downto 0);
begin
cnt_pr : process(clk, rst)
begin
if (rst = ‘1’) then
cnt ‘0’);
clk_en <= '0';
elsif (rising_edge(clk)) then
— default
clk_en <= '0';
if (en = ‘1’) then
if (cnt = 0) then
cnt <= to_unsigned(PERIOD-1, cnt'length);
clk_en <= '1';
else
cnt <= cnt – 1;
end if;
end if;
end if;
end process cnt_pr;
cnt_duty_pr : process(clk, rst)
begin
if (rst = ‘1’) then
cnt_duty ‘0’);
pwm_out <= '0';
elsif (rising_edge(clk)) then
if (clk_en = ‘1’) then
cnt_duty <= cnt_duty + 1;
end if;
if (cnt_duty < unsigned(duty)) then
pwm_out <= '1';
else
pwm_out <= '0';
end if;
end if;
end process cnt_duty_pr;
end rtl;
[/CODE]
The are two control signals among the ports of the entity:
- en: enables the PWM output
- duty: is the duty cycle of the PWM output
The first counter cntis the frequency divider, which originates the clk_ensignal. 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:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
[CODE]
package pwm_reg_pack is
————————————————————————-
— Data size definitions
————————————————————————-
constant SYS_CLK : natural := 100_000; — System clock in kHz
constant PWM_CLK : natural := 500; — PWM clock in kHz
constant DUTY_CYCLE_W : natural := 5; — PWM resolution in bits
constant PERIOD : natural := SYS_CLK / (PWM_CLK * 2**DUTY_CYCLE_W);
constant PERIOD_W : natural := integer(ceil(log2(real(PERIOD+1))));
end pwm_reg_pack;
[/CODE]
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.
The waveform below is a ‘zoom-in’:
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):
The module sources, including simulation testbench, are attached below. Replace the .txt extension with .vhd extension
My blog:FPGA Site
Proposed exercises:
[LIST=1]
Podcast EP267: The Broad Impact Weebit Nano’s ReRAM is having with Coby Hanoch