PYNQ: PYTHON PRODUCTIVITY

Tutorial: Creating a Verilog Overlay with Bidirectional Pins

Building a Verilog overlay with Bidirectional pins

Background

This tutorial is targeted at users who wish to interface the PL of a Zynq 7000 and use pins as inputs, outputs or Bidirectional pins. This tutorial was developed on a TUL-2 Board… Other development boards may require modifications. Sample sources are linked. Use caution when selecting pins or applying drive values to pins as contending drivers (either from your board or you) can damage the part or the board. Also be sure to select pins which can handle any external drive voltages you apply (don’t put 3.3V input on a 1.8V pin).

Approach

Since this tutorial was built based on the TUL-2, the PL Overlay will contain an AXI4 peripheral containing the following:

  1. Four inputs (tied to the board buttons)
  2. Four outputs (tied to the board monocolor lights)
  3. Four BIDIs (tied to the Arduino interface)

There is a primer on developing an AXI overlay available to help understand that process.

Build AXI Peripheral

Based on the TUL-2 board assets, five 4-pin ports are added at the top level of the peripheral:

Pins Purpose
inPins[3:0] Tied to the buttons
outPins[3:0] Tied to the lights
bidiPins_i[3:0] Inputs from the bidi pins
bidiPins_o[3:0] Outputs from the bidi pins
bidiPins_oe[3:0] Output Enables from the bidi pins

Registers are mapped as follows:

Offset Register Purpose
0x00 slv_reg0 Outputs (lights)
0x04 slv_reg1 Inputs (buttons)
0x08 slv_reg2 BIDI Enable
0x0C slv_reg3 BIDI Output Drive
0x10 slv_reg4 BIDI Input Read
0x14-18 slv_reg5-6 Reserved
0x1C slv_reg7 0xdecade90 (read-only to verify hookup)

Steps

  • Create a new Vivado project and name it “SimplePinControlAXIProject”.
    • Select RTL Project, don’t include any RTL, and select your board.
  • Create a new AXI4 Peripheral. See my prior tutorial if you are not familar with this process.
    • Name: “SimplePinControlAXIIP”
    • Interface Type: Lite
    • Interface Mode: Slave
    • Data Width: 32
    • Number of Registers: 8
  • Select “Verify Peripheral IP using AXI4 VIP”

  • Notice a schematic containing a ‘master’ and then the target peripheral.

The ‘master’ VIP (Verification IP) allows AXI transactions through API calls in Verilog tasks.

Go to the Sources window and expand out the Design Sources and Simulation Sources.
Right Click on ‘SimplePinControlAXIIP’ and select “Edit IP In Packager”. It will allow edits to the IP project.

There should now have two Vivado Windows open:

  • A window for SimplePinControlAXIIP (containing the VIP Testbench)
  • A window for main peripheral project.

Expand out the hierarchy for the SimplePinControlAXIIP Project. The expansion should look like:

Make the following edits to the files (sample files in links):

SimplePinControlAXIIP_v1_0:

Add the following ports in the top-level user-ports area:
input inPins,
output outPins,
input bidiPins_i,
output bidiPins_o,
output bidiPins_oe,
And add those ports to the lower-module instantiation:

  .inPins(inPins),
  .outPins(outPins),
  .bidiPins_i(bidiPins_i),
  .bidiPins_o(bidiPins_o),
  .bidiPins_oe(bidiPins_oe),

SimplePinControlAXIIP_v1_0_S00_AXI_inst

Add ports to the user ports area:

  input inPins,
  output outPins,
  input bidiPins_i,
  output bidiPins_o,
  output bidiPins_oe,

Add logic to update slv_reg1,4 and 7:

            end
          else begin
         slv_reg1 <= {28'h0000000, inPins[3:0]};
         slv_reg4 <= bidiPins_i;
         slv_reg7 <= 32'hdecade90;
            if (slv_reg_wren)
              begin
                case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
                  3'h0:
                    for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
                      if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                        // Respective byte enables are asserted as per write strobes
                        // Slave register 0

Also remove the 3’h1, 3’h4 and 3’h7 cases from the case statement (and the slv_reg1,4 and 7 entries from the default statement).

And inout/output drive logic:

// Add user logic here
// slv_reg0 is the value to be driven to the output pins.

assign outPins = slv_reg0;

// slv_reg2[3:0] is an output enable.
// slv_reg3[3:0] is desired drive value of output pins.

assign bidiPins_oe[3:0] = slv_reg2[3:0];
assign bidiPins_o[3:0]  = slv_reg3[3:0];

// User logic ends

Run a quick simulation (just to check for syntax errors) and close the project.

Implementing a full testbench is beyond the scope of this tutorial, however this is a reference that I used.

Build the overlay

Start a new project and click “Create Block Design”. Add the SimplePinControl Module and the Zynq 7000 processing.

Inside the block design window, use the right-click menu to select “Create Port”.

Create the following ports (port names must match the pin names in the constraints file):

Name Direction Type
btn input Data Bus from 3 to 0
led output Data Bus from 3 to 0
bidiPins_i input Data Bus from 3 to 0
bidiPins_o output Data Bus from 3 to 0
bidiPins_oe output Data Bus from 3 to 0

Tie btn to SimplePinControl.inPins, led to SimplePinControl.outPins, and the bidi Pins as named.

And generate a top-level wrapper.

Inside the TOP LEVEL wrapper, remove the bidiPins entries and replace with ar[3:0]

design_1_wrapper:

//  input [3:0]bidiPins_i;
//  output [3:0]bidiPins_o;
//  output [3:0]bidiPins_oe;
  inout [3:0] ar;

And add IO Buffers (this is one of them- add one for each of ar0-3 or use a generate statement):

 IOBUF #(
      .DRIVE(12), // Specify the output drive strength
      .IBUF_LOW_PWR("TRUE"),  // Low Power - "TRUE", High Performance = "FALSE"
      .IOSTANDARD("DEFAULT"), // Specify the I/O standard
      .SLEW("SLOW") // Specify the output slew rate
   ) BidiPins0_inst (
      .O(bidiPins_i[0]),     // Buffer output
      .IO(ar[0]),            // Buffer inout port (connect directly to top-level port)
      .I(bidiPins_o[0]),     // Buffer input
      .T(bidiPins_oe[0])     // 3-state enable input, high=input, low=output
   );

Finally, take the constraints file supplied with the board and uncomment the pins necessary for inputs, outputs and bidis.

This is my example for Tul-2 SimplePinControl Tul2 constraints based on the TUL supplied file.

And execute ‘Generate Bitstream’.

Testing

Copy the .bit and .hwh files from the FPGAImage project to the TUL-2 board and save them in
/home/xilinx/pynq/overlays/SimplePinControl/ as SimplePinControl.bit and SimplePinControl.hwh

As root, start python3 and execute:

from pynq import Overlay
overlay = Overlay(“SimplePinControl.bit”)
simplePinControl = overlay.SimplePinControl_0

Check the checksum

"{0:X}".format(simplePinControl.read(0x1c))

‘decade90’

Hold down buttons on your board and execute this- watch it change:

simplePinControl.read(0x4)

‘1’

Toggle the lights:

simplePinControl.write(0x0,0x7)
simplePinControl.write(0x0,0x8)

Drive the BIDIs (you will need to use a voltmeter or some sort of light to scope these.

simplePinControl.write(0x8,0x0)
simplePinControl.write(0xC,0xF)
simplePinControl.read(0x10)

‘15’

Turn off the BIDIs

simplePinControl.write(0x8,0xF)

You can use a voltmeter to measure the drive out voltage… if you are daring you can try applying a pullup/pulldown with a resistor to toggle the bidi pins when not otherwise driven. BE CAREFUL as having multiple drivers on a pin can damage your board. I always use a pull resistor (at least 10K Ohm) between my voltage source and the pin.

Pins can be read with

simplePinControl.read(0x10)

References:

[Ref1] Tutorial: Creating a new Verilog Module Overlay for a primer tutorial on this.