The goal of this blog series is to master the Xilinx Zynq.

I'll try to build a PWM controller for a half bridge power design.

I've made a PWM with dead time design for the Xilinx Spartan 6 FPGA in 2017.
I'm now learning to design for Zynq (I got a Pynq-Z2 board from our balearicdynamics!). I use
the 2017 Spartan 6 design as a starting point.



Goals for this post


I want to have that PWM VHDL design running on the Zynq, and be able to change the dead time and duty cycle from the ARM processors inside the Zynq.

The output signals have to be available for probing.

To set duty cycle and dead time, I'll use the simplest solution for now: the GPIO port between ARM and FPGA fabric.


The VHDL PWM with Dead Time


Very close to the version of the original post. Only edited to deal with obsolete constructs.


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;

library IEEE, UNISIM;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;
use UNISIM.vcomponents.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
  constant MAX_DUTY_C : std_logic_vector(duty_i'range) := (duty_i'range => ONE);
  signal timer_r      : natural range 0 to 2**duty_i'length-1;

    if rising_edge(clk_i) then
       pwmA_o   <= LO;
       timer_r <= timer_r + 1;
      if timer_r >= unsigned(band_i) + 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;


It has 3 inputs:

  • the clock. It will come directly from the ARM side
  • duty cycle. Also coming from the ARM. Via 8 bits of the GPIO connector.
    If you need a faster output frequency, you'll have to compromise on the bit width. The less precision, the faster the outpu.
  • dead time. Also coming from the ARM. Via 8 bits of the GPIO connector. The number of clock ticks that both output signals should stay off, to prevent overshoot in the power half bridge you'll drive with this design.

and 2 outputs: the complementary driving signals for the half bridge, with dead time injected.

image: preview of what the final output will be. The cursors show the dead time before output A switches high.


Interaction between the VHDL PWM and the Zynq ARM realms.


In my original Spartan 6 post, everything was happening inside the FPGA. There was a rotary encoder setting the duty cycle, and the PWM module reacting on that.

In my Zynq design here, there is no rotary encoder. The settings come from the Arm, running Linux.

The interface in this blog is the 64 bit GPIO pass-through.

image: the GPIO bus between ARM and FPGA is enabled


The interface will give duty cycle (8 bits) and dead time (4 bits).

I'll use 2 slice modules to cut these 2 values out of the 64 bit wide GPIO output bus.


image: interconnections. in this picture, the ARM part and the slices that provide duty cycle and dead time are highlighted.


The table below shows how the 64 bit port is "sliced" into different sections:


duty cycledead time

image: the slices that cut the 8 bit duty cycle and 4 bit dead time out of the ARM 64 bit GPIO output bus.

in a next post, I'll replace this mechanism by memory mapped interaction between ARM and FPGA fabric.


The duty cycle and dead time are inputs for the PWM VHDL module. The 3rd input is Zynq's clock signal.


This is a good time to verify the design and run a synthesis.

If successful, the output pins can be assigned. I'm going to route them to two pins on the PMODA connector:


Output Pins: Constraints


The output of the PWM block is routed to two pins on the PMODA connector:


image source: online published schematics

image: tapping the signals



The template XDC file for the Pynq-ZQ shows what balls on the Zynq package they relate to:


#set_property -dict { PACKAGE_PIN Y18   IOSTANDARD LVCMOS33 } [get_ports { ja[0] }]; #IO_L17P_T2_34 Sch=ja_p[1]
#set_property -dict { PACKAGE_PIN Y19   IOSTANDARD LVCMOS33 } [get_ports { ja[1] }]; #IO_L17N_T2_34 Sch=ja_n[1]


I could copy over that code, but used the GUI to configure them instead:

Before doing that, take care to generate a wrapper for the diagram, and put that as the top module.

That takes care that the inputs and outputs defined on that diagram will be the one that are accessible and can be constrained.


If all is good, let Vivado generate the bit stream.


Deploy the Design to the Pynq-Z2


After generating the bitstream, there are two files that you'll need to copy over to the Zynq.

Both can be found in your project subdirectories (mine is named pwm_xess)

  • the .bit file (bitstream, I found it in pwm_xess/pwm_xess.runs/impl_1)
  • the .hwh file (description of the bitstream access points, I found it in pwm_xess/pwm_xess.gen/sources_1/bd/pwm_xess/hw_handoff)


Coy them over to a subdirectory on the Pynq, in the Jupyter structure.

I made a /home/xilinx/jupyter_notebooks/pwm directory and put them there.

They both have to have the same filename, to make to overlay on the Pynq work.



Use the Design on the Pynq-Z2


Now that the bitsream and wrapper are deployed, I can test from a Jupyter notebook.

The notebook is available on the network by surfing to http://pynq.

I copied my files to the pwm subdirectory of the notebooks. In my browser, I navigate to that same subfolder:

image: my Jupyter pwm test folder contains other designs too. I highlighted the ones for this post


I find the .bit and .hwh file listed.

To execute the bitstream, I generate a new Python3 notebook called pwm_xess.


imaqe: my notebook after doing all the tests


imaqe: the design in action with 50% duty cycle


First the code to load the bitstream:


from pynq import Overlay


Then the definition of the two variables that define duty cycle and dead time:


from pynq import GPIO

# duty
bit_0 = GPIO(GPIO.get_gpio_pin(0), 'out')
bit_1 = GPIO(GPIO.get_gpio_pin(1), 'out')
bit_2 = GPIO(GPIO.get_gpio_pin(2), 'out')
bit_3 = GPIO(GPIO.get_gpio_pin(3), 'out')
bit_4 = GPIO(GPIO.get_gpio_pin(4), 'out')
bit_5 = GPIO(GPIO.get_gpio_pin(5), 'out')
bit_6 = GPIO(GPIO.get_gpio_pin(6), 'out')
bit_7 = GPIO(GPIO.get_gpio_pin(7), 'out')

# dead time
bit_8 = GPIO(GPIO.get_gpio_pin(8), 'out')
bit_9 = GPIO(GPIO.get_gpio_pin(9), 'out')
bit_10 = GPIO(GPIO.get_gpio_pin(10), 'out')
bit_11 = GPIO(GPIO.get_gpio_pin(11), 'out')


All is ready now, to play with the inputs.


Setting the duty cycle to 50%:


# 50%


Setting a dead time of 8 clock ticks:


# dead time 8 clock ticks


Changing the value has real time impact on the generated signal

image: PWMA and PWMB, with generated deadband


In essence, I now have a half bridge controller that can be controlled from Linux.

A simple starting point, but a real one.


image: the complimentary signals are available on PMODA, pins 1 and 2


The Vivado project, with generated .bit and .hwh files is attached.