To create a custom hardware overlay (.bit and .hwh) that can be loaded onto a running PYNQ system without rebuilding the Linux OS.
We will design a system containing:
- Standard IP: An AXI GPIO block to control the board's LEDs.
- Custom IP: A "Hardware Counter" that counts clock cycles. We will control its start/stop state and read its value from Python.
- Vivado Installed: You have a working installation (2024.1 or similar).
- Board Files: PYNQ-Z2 board files are installed in Vivado.
- PYNQ Board: Your board is running the standard PYNQ image (Phase 1 setup), connected to the network.
We will create a logic block that counts ticks. This demonstrates internal state retention, which simple combinational logic (like an adder) does not have.
-
Create IP Project:
- Open Vivado. Click Create Project.
- Name:
ip_counter_temp. Click Next. - Project Type: Select RTL Project (Do not specify sources). Click Next.
- Project Part: Switch to the Boards tab.
- Search for:
pynq. - Select: pynq-z2.
- Search for:
- Click Next, then Finish.
- Start IP Wizard: Go to Tools > Create and Package New IP. Click Next.
- Task: Select Create a new AXI4 peripheral. Click Next.
- Details:
- Name:
axi_dyn_counter. - Description:
My new AXI IP(optional). - Click Next.
- Name:
- Interfaces: Keep defaults (Lite, Slave, 32-bit, 4 Registers). Click Next.
- Summary:
- Select Edit IP (Important!).
- Click Finish.
- Observation: A new Vivado window will open specifically for editing this IP.
-
Modify Verilog Source:
In the Sources panel, you will see two files:-
axi_dyn_counter_v1_0.v: The top-level wrapper (instantiates the AXI logic). -
axi_dyn_counter_v1_0_S00_AXI.v: The actual AXI Lite implementation. Edit this one. -
Why not the wrapper?
- The Wrapper (
_v1_0.v): This file is just the shell. Its job is to group multiple interfaces together. For example, if your IP had an AXI-Lite port plus an AXI-Stream video port plus an Interrupt line, the wrapper connects them all into one black box. - The Logic (
_S00_AXI.v): This file contains the AXI Protocol Engine. Vivado has pre-written the complex code that handles the valid/ready handshakes and address decoding. It gives you simple variables (slv_reg0) to work with. If you wrote code in the wrapper, you would have to write the AXI state machine yourself!
- The Wrapper (
-
Open
axi_dyn_counter_v1_0_S00_AXI.v. -
1. Add Logic (The Counter):
Scroll to the very bottom of the file (around line 400). You will see a placeholder:// Add user logic here // User logic ends
Paste the following code between those lines:
// -- Custom Signals -- reg [31:0] internal_count; wire enable_signal; wire reset_signal; // -- Mappings -- // slv_reg0[0] = Enable // slv_reg0[1] = Reset assign enable_signal = slv_reg0[0]; assign reset_signal = slv_reg0[1]; // -- Counter Logic -- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 || reset_signal == 1'b1 ) begin internal_count <= 0; end else if ( enable_signal == 1'b1 ) begin internal_count <= internal_count + 1; end end
-
2. Map Output (The Read):
Scroll up slightly (around line 303). Find the long line that handles reading data (assign S_AXI_RDATA = ...).Change:
... ? slv_reg1 : ...To:... ? internal_count : ...Old Line (Simplified):
assign S_AXI_RDATA = (... == 2'h0) ? slv_reg0 : (... == 2'h1) ? slv_reg1 : ...New Line:
assign S_AXI_RDATA = (... == 2'h0) ? slv_reg0 : (... == 2'h1) ? internal_count : ...Effect: When the processor reads Register 1 (Offset 0x4), it now sees our live
internal_countinstead of the staticslv_reg1value.
-
-
Package:
- Go to the Package IP... tab.
- Click Review and Package > Re-Package IP.
- Close the temporary IP project.
Now we build the system that PYNQ will load.
Why a new project?
- Separation of Concerns:
ip_counter_tempis like a "Library Project" (specifically for creating the component).pynq_overlay_demois the "Application Project" (where we wire components together).- Reusability: By packaging the IP separately, you can now use
axi_dyn_counterin any future Vivado project, just like you use the standard GPIO block.
- Create Project:
- Create a new RTL Project:
pynq_overlay_demo. - Board: PYNQ-Z2.
- Create a new RTL Project:
- Add IP Repository:
(Crucial for finding your custom IP)- In the Flow Navigator (left), click Settings.
- Go to IP > Repository.
- Click + (Add Repository).
- Navigate to your
ip_repofolder (usually insideip_counter_temp/ip_repo). - Select it and click Select. Click OK.
- Setup Block Design:
- Create Block Design named design_1.
- Add Zynq PS: Add ZYNQ7 Processing System and run Block Automation (Apply Board Preset).
- Add GPIO: Add AXI GPIO. Run Connection Automation.
- Select: GPIO interface -> leds_4bits (Board Interface).
- Note: This maps the software "GPIO" directly to the physical LED pins.
- Add Custom IP: Add axi_dyn_counter. Run Connection Automation.
- Connects S00_AXI to the Zynq GP Port.
- Connects S00_AXI to the Zynq GP Port.
- Validate & Build:
- Validate Design (F6).
- Create HDL Wrapper (Right-click design_1 in Sources->Design Sources).
- Generate Bitstream.
- Wait for it to finish with the message "Bitstream generated successfully". You can click Cancel in the dialog box.
This is the secret sauce. PYNQ needs two files:
- .bit: The binary configuration for the FPGA.
- .hwh: A metadata file describing what is in the bitstream (addresses, interrupts, GPIO names). PYNQ uses this to auto-generate the Python drivers.
- Locate Bitstream:
<project_dir>/pynq_overlay_demo.runs/impl_1/design_1_wrapper.bit - Locate Handoff:
<project_dir>/pynq_overlay_demo.gen/sources_1/bd/design_1/hw_handoff/design_1.hwh
(Note: In older Vivado versions, this might be .tcl, but .hwh is preferred for PYNQ v2.6+). - Rename & Gather:
Copy both files to a single folder and rename them to match exactly:- my_counter.bit
- my_counter.hwh
Now we switch to the Jupyter Notebook interface.
-
Upload:
- Open Jupyter (http://pynq:9090).
- Upload my_counter.bit and my_counter.hwh to the pynq home folder.
-
Create Notebook:
Create a new Python 3 notebook and create then run the following cells:
Cell 1: Load Overlay & Create Driversfrom pynq import Overlay import time # 1. Load the Overlay ol = Overlay("my_counter.bit") # 2. Check IP Dictionary print("Available IPs:", ol.ip_dict.keys()) # 3. Create Drivers # Standard PYNQ driver for LEDs (High-Level) leds = ol.axi_gpio_0.channel1 # Custom IP access via MMIO (Low-Level) counter = ol.axi_dyn_counter_0
Cell 2: Test LEDs (Standard GPIO)
# Configure Direction (TRI Register) # The High-Level driver defaults to Input. We must force Output. # Offset 0x4 = Channel 1 Tri-State Control (0 = Output, 1 = Input) ol.axi_gpio_0.mmio.write(0x4, 0x0) print("Direction set to Output via MMIO.") print("Blinking LEDs...") for i in range(4): leds.write(0xF, 0xF) # All ON time.sleep(0.2) leds.write(0x0, 0xF) # All OFF time.sleep(0.2) print("Done.")
Cell 3: Test Custom Counter (MMIO)
print("Testing Custom Counter...") # Reset (Write 2 to Reg0) counter.write(0x0, 2) # Enable (Write 1 to Reg0) counter.write(0x0, 1) print("Counting for 1 second...") time.sleep(1.0) # Stop (Write 0 to Reg0) counter.write(0x0, 0) # Read Result (Read Reg1 @ Offset 0x4) count_val = counter.read(0x4) # Math check: Clock is 100MHz. 1 second ~ 100M ticks. print(f"Counter Value: {count_val}") print(f"Frequency: {count_val / 1_000_000:.2f} MHz")
-
Expected Result:
- LEDs on the board blink.
- Output shows a count value close to 100,000,000 (roughly 100MHz), confirming your custom logic ran at hardware speeds.
- Overlay Philosophy: You created hardware that acts like a software library. You loaded it dynamically without touching the bootloader.
- Handoff Files: You learned that the .hwh file is the bridge that allows Python to understand your Vivado Block Design structure automatically.
- Hybrid Driver Model: You used a high-level PYNQ driver for the standard GPIO IP, but fell back to MMIO for your custom Counter IP within the same design.
Next Steps: You now have the skills to build the hardware for any custom accelerator.
The hardware is done. Now, let's look at where you go from here.