I posted a series of FPGA blogs. They focus on the toolchains and steps to get a working design.
A common theme in those articles is the VHDL source. Each time, it's a PWM generator.
A specific kind of PWM block: it can generate complementary output signals, to drive a transistor half bridge.
In this post, I'm drilling into that VHDL part.
Why this PWM module with 2 outputs and dead time |
---|
I wanted to use a relevant exercise that solves a common task in electronics: drive a transistor half-bridge. Half-bridge circuits are all around. You'll find them in power designs such as motor control, buck converters, ... Virtually every digital output pin of a microcontroller has one.
It's a very common and well known design, and it's easy to translate the requirements into criteria for a digital driving circuit. And that's what we're making here: a digital circuit for an FPGA, with VHDL.
The PWM block is made to control this type of half-bridge circuits:
When you control such a design, there is always one watch-out: both transistors should never conduct at the same time. Because you would short the power supply directly to ground (shoot-trough). Because there is a little delay between closing the gate and switching off the source-drain channel, you can't drive the two transistors with a simple inverted signal. During that delay, both transistors would conduct and there would be a high current directly from power supply to ground. Ignoring to deal with this will destroy the transistors in seconds.
The remedy is to put a waiting time between switching off one transistor, and witching on the other one. This time is called dead time or dead band.
For efficient operation of the design, this dead time should be as short as possible. For reliability, it should be long enough to avoid the shoot-through.
This VHDL design can generate the control signals for the two transistors, and gives a very fine control over the dead time (resolution: one clock tick). You can precisely define it, and guarantee that it's always there. |
VHDL interface
The PWM block has these inputs and outputs:
pin | Function |
---|---|
clk_i | input: clock signal |
duty_i | input: 8 bit duty cycle. Range: 0 - 255 == 0 - 100% |
band_i | input: 4 bit dead time, 0 - 15 clock ticks time between falling edge of one output and rising edge of the other one |
pwmA_o | output: PWM signal, opposite to pwmB_o, will wait dead time before switching on after pwmB_o becomes low |
pwmB_o | output: PWM signal, opposite to pwmA_o, will wait dead time before switching on after pwmA_o becomes low |
You can control the block from any input that can set duty cycle and dead time.
In my Spartan blog, it's controlled by a rotary encoder (mouse scroll wheel) that's connected to the FPGA.
In the recent Vivado / Pynq blogs, these two inputs are controlled by the ARM processors embedded on the Zynq silicon.
It's the exact same VHDL though.
entity Pwm is port ( clk_i : in std_logic; -- Input clock. duty_i : in std_logic_vector (7 downto 0); -- Duty-cycle input. band_i : in std_logic_vector (3 downto 0); -- number of clock-ticks to keep both signals low before rising edge pwmA_o : out std_logic; -- PWM output. pwmB_o : out std_logic -- PWM output inverse. ); end entity;
VHDL implementation
The clock ticks are what brings this design to life.
A single full lifecycle of the block is 255 clock ticks.
This is because the duty_i input bus has 8 bit resolution.
We can do a single transition of output A and/or B per clock tick. So for 8 bits precision we'll consume 255 ticks.
Each tick, a counter is increased, and the values of output A or B are calculated.
If we ignore the dead time for a moment:
- all clock ticks from 0 to duty_i, A should be high, B should be low
- all clock ticks from duty_i to 255, A should be low and B should be high.
The dead time is inserted by adding its value to the desired point where either A or B is set to high.
if rising_edge(clk_i) then pwmA_o <= LO; timer_r <= timer_r + 1;
first part of the 255 step cycle: counter between 0 and duty_i - 1:
if timer_r < unsigned(duty_i) then pwmB_o <= LO; if timer_r >= unsigned(band_i) then pwmA_o <= HI; end if;
second part: counter from duty_i to 255
Here, I convert band_i and duty_i to integers, because their sum may not fit into 8 bits.
If I did not convert them, VHDL would try to fit the sum in a construct that has the same size as the left operand.
if timer_r >= to_integer(unsigned(band_i)) + to_integer(unsigned(duty_i)) then pwmB_o <= HI; end if;
The counter is defined in such a way that it automatically adapts to the bus width of duty_i, and wraps after 255 ticks:
signal timer_r : natural range 0 to 2**duty_i'length-1;
Full VHDL
this is the first version, at the end of this post you find a reworked approach after comments from community members)
Latest source on github: https://gist.github.com/jancumps/36f21e89bfb8e44f3dba7bf014ffd198
--********************************************************************* -- Module for generating repetitive pulses. --********************************************************************* library IEEE; use IEEE.std_logic_1164.all; package PulsePckg is constant HI : std_logic := '1'; constant LO : std_logic := '0'; constant ONE : std_logic := '1'; end package; --********************************************************************* -- PWM module. --********************************************************************* library IEEE; use IEEE.MATH_REAL.all; use IEEE.std_logic_1164.all; use IEEE.numeric_std.all; use WORK.PulsePckg.all; entity Pwm is port ( clk_i : in std_logic; -- Input clock. duty_i : in std_logic_vector (7 downto 0); -- Duty-cycle input. band_i : in std_logic_vector (3 downto 0); -- number of clock-ticks to keep both signals low before rising edge pwmA_o : out std_logic; -- PWM output. pwmB_o : out std_logic -- PWM output inverse. ); end entity; architecture arch of Pwm is signal timer_r : natural range 0 to 2**duty_i'length-1; begin process(clk_i) begin if rising_edge(clk_i) then pwmA_o <= LO; timer_r <= timer_r + 1; if timer_r >= to_integer(unsigned(band_i)) + to_integer(unsigned(duty_i)) then pwmB_o <= HI; end if; if timer_r < unsigned(duty_i) then pwmB_o <= LO; if timer_r >= unsigned(band_i) then pwmA_o <= HI; end if; end if; end if; end process; end architecture;
The design is not complex. And can be simplified further. I leave it as an exercise for the reader (edit: see at the end of the post).
Result
I've loaded the design on a Pynq-Z2, and set duty cycle and dead time:
pwm_register[8:12].write(12) pwm_register[0:8].write(200)
The oscilloscope shows signal A (yellow) and B (blue).
I've used the math function A + B to make the dead band visible (purple).
The two cursors show the start and end of a 255 clock tick cycle (white).
- start of the cycle. counter = 0
- counter = 12. pwmA_o waits 12 clock ticks before being set to 1
- counter = 200. pwmA_o is set to 0.
counter = 200 + 12: pwmB_o waits 12 clocks before being set to 1. - counter = 255. End of cycle. pwmB_o is set to 0.
Edit: VHDL Restructure
After reading other people's VHDL papers and checking how jc2048 and wolfgangfriedrich structure a design, I rewrote the architecture in a more organised way:
Latest source on github: https://gist.github.com/jancumps/36f21e89bfb8e44f3dba7bf014ffd198
library IEEE; use IEEE.std_logic_1164.all; package PulsePckg is constant HI : std_logic := '1'; constant LO : std_logic := '0'; constant ONE : std_logic := '1'; end package; --********************************************************************* -- PWM module. --********************************************************************* library IEEE; use IEEE.MATH_REAL.all; use IEEE.std_logic_1164.all; use IEEE.numeric_std.all; use WORK.PulsePckg.all; entity Pwm is port ( n_reset_i : in std_logic; -- async reset clk_i : in std_logic; -- Input clock. duty_i : in std_logic_vector (7 downto 0); -- Duty-cycle input. band_i : in std_logic_vector (3 downto 0); -- number of clock-ticks to keep both signals low before rising edge pwmA_o : out std_logic; -- PWM output. pwmB_o : out std_logic -- PWM output inverse. ); end entity; architecture arch of Pwm is signal timer_r : natural range 0 to 2**duty_i'length-1; begin clocked: process(clk_i, n_reset_i) begin pwmA_o <= LO; pwmB_o <= LO; -- async reset if n_reset_i = '0' then timer_r <= 0; elsif rising_edge(clk_i) then -- timer timer_r <= timer_r + 1; -- output a if timer_r < unsigned(duty_i) and timer_r >= unsigned(band_i) then pwmA_o <= HI; end if; -- output b if timer_r >= to_integer(unsigned(band_i)) + to_integer(unsigned(duty_i)) then pwmB_o <= HI; end if; end if; -- rising_edge end process clocked; end architecture;
This does not change the FPGA timings or use. The synthesis and implementation resulted into the exact same design and fabric consumption.
Critique is welcome ....
Top Comments