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. |
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.
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; library IEEE, UNISIM; use IEEE.MATH_REAL.all; 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; begin process(clk_i) begin 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 cycle | dead 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:
##PmodA #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 ol=Overlay("pwm_xess.bit")
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% bit_0.write(1) bit_1.write(1) bit_2.write(1) bit_3.write(1) bit_4.write(1) bit_5.write(1) bit_6.write(1) bit_7.write(0)
Setting a dead time of 8 clock ticks:
# dead time 8 clock ticks bit_8.write(0) bit_9.write(0) bit_10.write(0) bit_11.write(1)
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.
Top Comments