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:
- Four inputs (tied to the board buttons)
- Four outputs (tied to the board monocolor lights)
- 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):
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]
// 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 - #2 by cathalmccabe for a primer tutorial on this.