Continuation of Learning AMD Zynq: a project to generate a set of PWM signals. 1 - problem statement and possible approach .
yepe has a goal to create a set of signals for an ultrasone pulse generator.
Status after Post 1
In Post 1, we got the PWM and PWMN signals, and a first stab at the PULSE signal. Let's start at that stable point here, and try to add
- the initial delay between PRF falling edge and the start of the pulse train
- a PULSE signal that starts and ends exactly when the pulse train starts and ends.
Here's the design from the 1st blog:
The clocking wizard generates a 320 MHz clock. High enough to give the PWM signal a decent granularity for the dead times (2.5 ns steps).
The PWM generator generates 2 complementary signals, PWM and PWMN, with configurable deadband.
The PWM generator has an enable pin. When that pin is low, it will set PWM low and PWMN high, else it outputs the train of pulses
Goal of this post:
- Build a control block that will tell the PWM when to generate pulses (control its enable)
- let that block also control the PULSE signal
- establish the configurable? delay at the start
- maybe handle PRF?
What this session will not do: handle GATE
Same as in post 1, the action will be in the comments. It's a collaborative exercise.
Update 4/5/2023:
Possible signal controller design (partly - not all states and signals implemented)
library IEEE; use IEEE.STD_LOGIC_1164.ALL; package PulsePckg is component signal_controller is generic ( start_delay_resolution : integer := 6; -- bit width of the counter used delaying pulse train start train_length : integer := 7; -- bit width of the PWM burst train counter gate_delay_resolution : integer := 6; -- bit width of the counter used delaying the gate signal start counter_resolution : integer := 7 -- big enought o hold the largest of above ); port ( n_reset_i : in std_logic; -- async reset clk_i : in std_logic; -- Input clock. start_delay_i : in std_logic_vector (start_delay_resolution - 1 downto 0); -- Duty-cycle input. train_length_i: in std_logic_vector (train_length - 1 downto 0); -- how many PWMs in a burst train. prime_i : in std_logic; -- allow cycle start fire_i : in std_logic; -- start a cycle prf_o : out std_logic; -- PRF enable_o : out std_logic; -- enable pwm pulse_o : out std_logic; -- PULSE gate_o : out std_logic; -- GATE leds_o : out std_logic_vector (3 downto 0) ); end component; end package; library IEEE; use IEEE.STD_LOGIC_1164.ALL; use ieee.numeric_std.all; entity signal_controller is generic ( train_length : integer := 7; -- bit width of the PWM burst train counter start_delay_resolution : integer := 6; -- bit width of the counter used delaying pulse train start gate_delay_resolution : integer := 6; -- bit width of the counter used delaying the gate signal start counter_resolution : integer := 7 -- big enought o hold the largest of above ); port ( n_reset_i : in std_logic; -- async reset clk_i : in std_logic; -- Input clock. start_delay_i : in std_logic_vector (start_delay_resolution - 1 downto 0); -- Duty-cycle input. train_length_i: in std_logic_vector (train_length - 1 downto 0); -- how many PWMs in a burst train. prime_i : in std_logic; -- allow cycle start fire_i : in std_logic; -- start a cycle prf_o : out std_logic; -- PRF enable_o : out std_logic; -- enable pwm pulse_o : out std_logic; -- PULSE gate_o : out std_logic; -- GATE leds_o : out std_logic_vector (3 downto 0) ); end signal_controller; architecture arch of signal_controller is type states is (ready, delay, pulse, gate_delay, gate, done ); signal counter : integer range 0 to 2**counter_resolution-1; signal state : states; begin clocked: process(clk_i) begin if rising_edge(clk_i) then -- sync reset if n_reset_i = '0' then counter <= 0; state <= ready; -- reset output states prf_o <= '1'; enable_o <= '0'; pulse_o <= '0'; gate_o <= '0'; leds_o <= (others => '0'); else counter <= counter + 1; -- default output states prf_o <= '1'; enable_o <= '0'; pulse_o <= '0'; gate_o <= '0'; leds_o <= (others => '0'); case State is when ready => -- start a pulse group as soon as the fire bit is set leds_o <= (0 => '1', others => '0'); -- check if the caller has reset the prime bit if prime_i = '0' then -- if yes, start if the fire bit is set. if fire_i = '1' then counter <= 0; state <= delay; end if; end if; when delay => leds_o <= (0 => '0', 1 => '1', others => '0'); prf_o <= '0'; if counter >= to_integer(unsigned(start_delay_i)) - 1 then counter <= 0; enable_o <= '1'; state <= pulse; end if; when pulse => leds_o <= (0 => '1', 1 => '1', others => '0'); enable_o <= '1'; prf_o <= '0'; if counter < to_integer(unsigned(train_length_i)) - 1 then -- let the pulse change sync with last edge of PWM pulse_o <= '1'; elsif counter = to_integer(unsigned(train_length_i)) - 1 then pulse_o <= '1'; counter <= 0; state <= gate_delay; end if; when gate_delay => leds_o <= (0 => '0', 1 => '0', 2 => '1', others => '0'); state <= gate; -- todo implement when gate => leds_o <= (0 => '1', 1 => '0', 2 => '1', others => '0'); state <= done; -- todo implement when done => leds_o <= (0 => '1', 1 => '1', 2 => '1', others => '0'); -- check if the caller has reset the fire bit if fire_i = '0' then -- if yes, prime if the prime_bit is set. if prime_i = '1' then counter <= 0; state <= ready; end if; end if; end case; end if; -- sync reset end if; -- rising_edge end process clocked; end architecture;
warning: the train length should always result in full PWM cycles. If you disable the PWM module before it had the chance to generate a full cycle, both PWM and PWMN will switch at the same time and not respect the deadband.
Block diagram:
Capture:
Jupyter:
Jupyter source:
{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# PWM with generic dead band and duty cycle resolution\n", "\n", "https://community.element14.com/technologies/fpga-group/b/blog/posts/learning-xilinx-zynq-a-school-project-to-generate-a-set-of-pwm-signals-1---problem-statement-and-possible-approach\n", "\n", "pwm_ultrasound_pulser.xpr" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [], "source": [ "from pynq import Overlay\n", "ol=Overlay(\"pwm_ultrasound_pulser.bit\")" ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [], "source": [ "from pynq import MMIO\n", "RANGE = 8 # Number of bytes; 8/4 = 2x 32-bit locations which is all we need for this example" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [], "source": [ "duty_address = ol.ip_dict['axi_gpio_duty']['phys_addr']\n", "duty_register = MMIO(duty_address, RANGE) \n", "# Write 0x00 to the tri-state register at offset 0x4 to configure the IO as outputs.\n", "duty_register.write(0x4, 0x0) # Write 0x0 to location 0x4; Set tri-state to output" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [], "source": [ "band_address = ol.ip_dict['axi_gpio_band']['phys_addr']\n", "band_register = MMIO(band_address, RANGE) \n", "# Write 0x00 to the tri-state register at offset 0x4 to configure the IO as outputs.\n", "band_register.write(0x4, 0x0) # Write 0x0 to location 0x4; Set tri-state to output" ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [], "source": [ "flags_address = ol.ip_dict['axi_gpio_flags']['phys_addr']\n", "flags_register = MMIO(flags_address, RANGE) \n", "# Write 0x00 to the tri-state register at offset 0x4 to configure the IO as outputs.\n", "flags_register.write(0x4, 0x0) # Write 0x0 to location 0x4; Set tri-state to output" ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [], "source": [ "start_delay_address = ol.ip_dict['axi_gpio_start_delay']['phys_addr']\n", "start_delay_register = MMIO(start_delay_address, RANGE) \n", "# Write 0x00 to the tri-state register at offset 0x4 to configure the IO as outputs.\n", "start_delay_register.write(0x4, 0x0) # Write 0x0 to location 0x4; Set tri-state to output" ] }, { "cell_type": "code", "execution_count": 76, "metadata": {}, "outputs": [], "source": [ "train_length_address = ol.ip_dict['axi_gpio_train_length']['phys_addr']\n", "train_length_register = MMIO(train_length_address, RANGE) \n", "# Write 0x00 to the tri-state register at offset 0x4 to configure the IO as outputs.\n", "train_length_register.write(0x4, 0x0) # Write 0x0 to location 0x4; Set tri-state to output" ] }, { "cell_type": "code", "execution_count": 77, "metadata": {}, "outputs": [], "source": [ "def duty(duty):\n", " duty_register.write(0x00, duty)\n", " \n", "def band(band):\n", " band_register.write(0x00, band)\n", " \n", "def dutypct(duty):\n", " duty_register.write(0x00, round((0x1F*2)/(100/duty)))\n", " \n", "def fire():\n", " flags_register.write(0x00, 1) # bit 0\n", " flags_register.write(0x00, 0)\n", "\n", "def prime():\n", " flags_register.write(0x00, 2) # bit 1\n", " flags_register.write(0x00, 0)\n", " \n", "def startdelay(startdelay):\n", " start_delay_register.write(0x0, startdelay);\n", "\n", "def trainlength(trainlength):\n", " train_length_register.write(0x0, trainlength);\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "duty(0x1F)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "dutypct(50)" ] }, { "cell_type": "code", "execution_count": 110, "metadata": {}, "outputs": [], "source": [ "band(2)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "startdelay(20)" ] }, { "cell_type": "code", "execution_count": 87, "metadata": {}, "outputs": [], "source": [ "trainlength(125)" ] }, { "cell_type": "code", "execution_count": 90, "metadata": {}, "outputs": [], "source": [ "prime()" ] }, { "cell_type": "code", "execution_count": 91, "metadata": {}, "outputs": [], "source": [ "fire()" ] }, { "cell_type": "code", "execution_count": 115, "metadata": {}, "outputs": [], "source": [ "trainlength(125)\n", "prime()\n", "fire()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.4" } }, "nbformat": 4, "nbformat_minor": 2 }
Scope capture (PWM, PWMN, PULSE, PRF)
0 dead time:
2 dead time:
Compare to requirement:
Example with longer initial delay and a longer pulse train, 5 pulses of dead band. The FPGA design is the same. This is done by writing different values to the registers:
Study of the edges. You see the PWM and PULSE signal, 0 dead band.
A few example settings that generate complete pulses. The first two generate 2 pulses, with a short start delay. The second 2 have a longer start delay and generate 5 pulses. The last one is retriggered in a software loop. I've also played with the deadbands.
Vivado project and Jupyter notebook are attached. They are for version 2022.1 and PYNQ-Z2 board
pwm_ultrasound_pulser_and_jupyter_20230506.zip
Top Comments