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:
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 <= (others => '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 <= (others => '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;
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:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; use ieee.math_real.all; 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;
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.

A zoom in on one cycle of the PWM:

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, can be downloaded at Github.
Proposed exercises:
- Identify on the simulation waveforms the maximum output value
- Change the testbench so it will output exactly five cycles for each possible duty value
- Change the source so the output can have a value of ‘allways high’ when duty = max. value