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:

 

 

pinFunction
clk_iinput: clock signal
duty_iinput: 8 bit duty cycle. Range: 0 - 255 == 0 - 100%
band_iinput: 4 bit dead time, 0 - 15 clock ticks time between falling edge of one output and rising edge of the other one
pwmA_ooutput: PWM signal, opposite to pwmB_o, will wait dead time before switching on after pwmB_o becomes low
pwmB_ooutput: 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).

 

  1. start of the cycle. counter = 0
  2. counter = 12. pwmA_o waits 12 clock ticks before being set to 1
  3. counter = 200. pwmA_o is set to 0.
    counter = 200 + 12: pwmB_o waits 12 clocks before being set to 1.
  4. 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 ....

 

Pynq - Zync - Vivado series
Add Pynq-Z2 board to Vivado
Learning Xilinx Zynq: port a Spartan 6 PWM example to Pynq
Learning Xilinx Zynq: use AXI with a VHDL example in Pynq
VHDL PWM generator with dead time: the design
Learning Xilinx Zynq: use AXI and MMIO with a VHDL example in Pynq
Learning Xilinx Zynq: port Rotary Decoder from Spartan 6 to Vivado and PYNQ
Learning Xilinx Zynq: FPGA based PWM generator with scroll wheel control
Learning Xilinx Zynq: use RAM design for Altera Cyclone on Vivado and PYNQ
Learning Xilinx Zynq: a Quadrature Oscillator - 2 implementations
Learning Xilinx Zynq: a Quadrature Oscillator - variable frequency
Learning Xilinx Zynq: Hardware Accelerated Software
Automate Repeatable Steps in Vivado
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 1: Vitis HLS
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 2: Vivado Block Design
Learning Xilinx Zynq: Logic Gates in Vivado
Learning Xilinx Zynq: Interrupt ARM from FPGA fabric
Learning Xilinx Zynq: reuse and combine components to build a multiplexer