VLSI Implementation Guide
VLSI Implementation Guide
# Windows path:
C:\Xilinx\Vivado\2025.1\data\boards\board_files\
# Linux path:
/opt/Xilinx/Vivado/2025.1/data/boards/board_files/
board_files/
pynq-z2/
C.0/
[Link]
part0_pins.xml
[Link]
NOT If board files still don't appear, go to Tools -> Settings -> Board Repository in Vivado and add the
E path to your board_files directory manually.
IMPO Always select the BOARD, not the PART. Selecting the board automatically configures PS
RTA settings, DDR memory, and pin assignments for PYNQ-Z2.
NT
PHASE
VERILOG HDL SOURCE FILES
2 Create all RTL modules - MAC unit, controller, AXI interface
Files to Create
1. mac_unit.v - Reconfigurable 4/8/16-bit MAC processing element
2. conv_layer_ctrl.v - Convolution layer FSM controller (drives MAC array)
3. axi_ctrl_regs.v - AXI-Lite slave for precision mode & start control
4. top_accel.v - Top-level wrapper connecting all modules
// ============================================================
// mac_unit.v - Reconfigurable Mixed-Precision MAC Unit
// Supports: 4-bit (INT4), 8-bit (INT8), 16-bit (INT16)
// Target: PYNQ-Z2 (Zynq-7020) @ 100 MHz
// ============================================================
module mac_unit #(
parameter ACCU_WIDTH = 32 // Accumulator always 32-bit
)( input wire clk,
input wire rst_n, // Active-low sync reset
input wire [1:0] mode, // 00=INT4 01=INT8 10=INT16
input wire [15:0] weight_in, // Weight (upper bits ignored in lower modes)
input wire [15:0] act_in, // Activation input
input wire valid_in, // Data valid strobe
input wire clear_acc, // Reset accumulator for new output pixel
output reg [ACCU_WIDTH-1:0] acc_out,
output reg valid_out
);
reg signed [3:0] w4, a4;
reg signed [7:0] w8, a8;
reg signed [15:0] w16, a16;
reg signed [31:0] product;
DSP On Zynq-7020, each DSP48E1 block is 25x18 (18-bit signed multiplier). The 16-bit mode maps 1:1.
MAP The 8-bit mode packs two MACs per DSP. The 4-bit mode packs four MACs per DSP - this is the
PING key novelty vs. prior work.
// ============================================================
// conv_layer_ctrl.v - Convolution Layer FSM Controller
// Computes: Output[r][c] = ReLU( sum_{kr,kc} W[kr][kc]*A[r+kr][c+kc] )
// ============================================================
module conv_layer_ctrl #(
parameter DATA_W = 16,
parameter ACCU_W = 32,
parameter IMG_SIZE = 28,
parameter K_SIZE = 5
)( input wire clk,
input wire rst_n,
input wire [1:0] precision_mode,
input wire start,
output reg [9:0] weight_addr,
input wire [DATA_W-1:0] weight_data,
output reg [9:0] act_addr,
input wire [DATA_W-1:0] act_data,
output reg [ACCU_W-1:0] out_data,
output reg [9:0] out_addr,
output reg out_valid,
output reg done
);
localparam IDLE=3'd0, LOAD=3'd1, WAIT=3'd2, COMP=3'd3, STORE=3'd4, FINISH=3'd5;
reg [2:0] state;
reg [4:0] row_cnt, col_cnt;
reg [2:0] k_row, k_col;
reg [9:0] out_pixel_cnt;
reg mac_valid, mac_clear;
localparam OUT_SIZE = IMG_SIZE - K_SIZE;
Register Map
Address 0x00 (Write): [1:0] = precision_mode [2] = start pulse
Address 0x04 (Read): [0] = done flag
// ============================================================
// axi_ctrl_regs.v - AXI-Lite Slave Control Registers
// Base Address (assigned in block design): 0x43C00000
// ============================================================
module axi_ctrl_regs (
input wire s_axi_aclk,
input wire s_axi_aresetn,
input wire [31:0] s_axi_awaddr,
input wire s_axi_awvalid,
output reg s_axi_awready,
input wire [31:0] s_axi_wdata,
input wire [3:0] s_axi_wstrb,
input wire s_axi_wvalid,
output reg s_axi_wready,
output reg [1:0] s_axi_bresp,
output reg s_axi_bvalid,
input wire s_axi_bready,
input wire [31:0] s_axi_araddr,
input wire s_axi_arvalid,
output reg s_axi_arready,
output reg [31:0] s_axi_rdata,
output reg [1:0] s_axi_rresp,
output reg s_axi_rvalid,
input wire s_axi_rready,
output reg [1:0] precision_mode,
output reg start,
input wire done
);
reg [1:0] aw_addr_latch;
reg aw_valid_latch;
always @(posedge s_axi_aclk) begin
if (!s_axi_aresetn) begin
precision_mode<=2'b01; start<=1'b0;
s_axi_awready<=0; s_axi_wready<=0; s_axi_bvalid<=0;
s_axi_arready<=0; s_axi_rvalid<=0; aw_valid_latch<=0;
end else begin
start <= 1'b0;
if (s_axi_awvalid && !s_axi_awready) begin
s_axi_awready<=1; aw_addr_latch<=s_axi_awaddr[3:2]; aw_valid_latch<=1;
end else s_axi_awready<=0;
if (s_axi_wvalid && !s_axi_wready) begin
s_axi_wready<=1;
if (aw_valid_latch) begin
if (aw_addr_latch==2'b00) begin
precision_mode<=s_axi_wdata[1:0];
start<=s_axi_wdata[2];
end
aw_valid_latch<=0; s_axi_bvalid<=1; s_axi_bresp<=2'b00;
end
end else s_axi_wready<=0;
if (s_axi_bvalid && s_axi_bready) s_axi_bvalid<=0;
if (s_axi_arvalid && !s_axi_arready) begin
s_axi_arready<=1; s_axi_rvalid<=1; s_axi_rresp<=2'b00;
case (s_axi_araddr[3:2])
2'b00: s_axi_rdata<={29'b0,start,precision_mode};
2'b01: s_axi_rdata<={31'b0,done};
default: s_axi_rdata<=32'hDEADBEEF;
endcase
end else begin
s_axi_arready<=0;
if (s_axi_rvalid && s_axi_rready) s_axi_rvalid<=0;
end
end
end
endmodule
// ============================================================
// top_accel.v - Top-Level Accelerator Wrapper
// ============================================================
module top_accel #(
parameter DATA_W=16, ACCU_W=32, IMG_SIZE=28, K_SIZE=5
)( input wire s_axi_aclk, s_axi_aresetn,
// AXI-Lite ports (abbreviated - see full version in project)
input wire [31:0] s_axi_awaddr, s_axi_wdata, s_axi_araddr,
input wire [3:0] s_axi_wstrb,
input wire s_axi_awvalid, s_axi_wvalid, s_axi_bready,
input wire s_axi_arvalid, s_axi_rready,
output wire s_axi_awready, s_axi_wready, s_axi_bvalid,
output wire s_axi_arready, s_axi_rvalid,
output wire [31:0] s_axi_rdata,
output wire [1:0] s_axi_bresp, s_axi_rresp,
// BRAM ports
output wire [9:0] weight_addr, act_addr, out_addr,
input wire [DATA_W-1:0] weight_data, act_data,
output wire [ACCU_W-1:0] out_data,
output wire out_valid,
output wire [3:0] status_led
);
wire [1:0] precision_mode;
wire start, done;
axi_ctrl_regs u_ctrl (
.s_axi_aclk(s_axi_aclk), .s_axi_aresetn(s_axi_aresetn),
// ... (connect all AXI ports)
.precision_mode(precision_mode), .start(start), .done(done)
);
conv_layer_ctrl #(.DATA_W(DATA_W),.ACCU_W(ACCU_W),
.IMG_SIZE(IMG_SIZE),.K_SIZE(K_SIZE)) u_conv (
.clk(s_axi_aclk), .rst_n(s_axi_aresetn),
.precision_mode(precision_mode), .start(start),
.weight_addr(weight_addr), .weight_data(weight_data),
.act_addr(act_addr), .act_data(act_data),
.out_data(out_data), .out_addr(out_addr),
.out_valid(out_valid), .done(done)
);
`timescale 1ns/1ps
module tb_mac_unit;
reg clk, rst_n, valid_in, clear_acc;
reg [1:0] mode;
reg [15:0] weight_in, act_in;
wire [31:0] acc_out;
wire valid_out;
integer pass_count, fail_count;
initial clk = 0;
always #5 clk = ~clk; // 100 MHz
task do_reset;
begin
rst_n=0; valid_in=0; clear_acc=0;
mode=2'b01; weight_in=0; act_in=0;
repeat(4) @(posedge clk); #1;
rst_n=1; @(posedge clk); #1;
end
endtask
initial begin
pass_count=0; fail_count=0;
do_reset;
// TC1: INT4 3*2 + 1*(-1) = 5
feed(2'b00,16'h0003,16'h0002); feed(2'b00,16'h0001,16'hFFFF);
check(32'd5, 8'd1);
// TC2: INT8 50*3 + 10*5 = 200
feed(2'b01,16'h0032,16'h0003); feed(2'b01,16'h000A,16'h0005);
check(32'd200, 8'd2);
// TC3: INT16 1000*2 + 500*4 = 4000
feed(2'b10,16'h03E8,16'h0002); feed(2'b10,16'h01F4,16'h0004);
check(32'd4000, 8'd3);
// TC4: INT8 negative weight -128*2 = -256
feed(2'b01,16'hFF80,16'h0002); check(-32'd256, 8'd4);
// TC5-TC7: (accumulator clear, mode switch, all-negative INT4)
// ... (see full version in source)
$display("Results: %0d PASSED %0d FAILED",pass_count,fail_count);
#100 $finish;
end
endmodule
Expected output:
[PASS] TC1: acc=5
[PASS] TC2: acc=200
[PASS] TC3: acc=4000
[PASS] TC4: acc=-256
[PASS] TC5: acc=9
[PASS] TC6: acc=19
[PASS] TC7: acc=6
Results: 7 PASSED, 0 FAILED
ALL TESTS PASSED - Safe to proceed to synthesis
CRIT Never proceed to synthesis or block design IP packaging if any testbench test cases fail. Fix the
ICAL RTL first. Synthesis errors after block design integration are extremely hard to debug.
PHASE
PACKAGE CUSTOM IP
4 Convert top_accel.v into a reusable Vivado IP block
IP location: C:/Projects/mixed_prec_cnn_accel/ip_repo/
[ ] Copy sources into IP directory <- CHECK THIS
Click: Next > -> Finish
TIP If you get clock domain crossing warnings, ensure all blocks share the same FCLK_CLK0 signal.
All AXI DMA aclk inputs must connect to the same clock source.
PHASE
CONSTRAINTS FILE (XDC)
6 Pin assignments, timing constraints, and bitstream settings for PYNQ-Z2
# ============================================================
# pynq_z2_constraints.xdc - PYNQ-Z2 (Zynq-7020) Constraints
# Mixed-Precision CNN Accelerator - Vivado 2025.1
# ============================================================
# Bitstream Configuration
set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property [Link] TRUE [current_design]
set_property [Link].SPI_BUSWIDTH 4 [current_design]
NOT The LED port names (status_led_0) must exactly match what Vivado generates when you make
E the port external in Block Design. After generating the wrapper, check design_1_wrapper.v and
update these port names if different.
PHASE
SYNTHESIS, IMPLEMENTATION & BITSTREAM
7 Generate the hardware configuration file for PYNQ-Z2
Bitstream: <project>/runs/impl_1/design_1_wrapper.bit
HWH file: <project>/gen/sources_1/bd/design_1/hw_handoff/design_1.hwh
PHASE
PYNQ-Z2 PYTHON DRIVER & INFERENCE
8 Load overlay and run precision-selectable CNN inference from Python
ol = Overlay('/home/xilinx/design_1_wrapper.bit')
ctrl = ol.top_accel_0
def set_precision(mode):
[Link](REG_CTRL, int(mode) & 0x3)
def start_accel(mode):
[Link](REG_CTRL, (int(mode) & 0x3) | (1 << 2))
def wait_done(timeout_ms=5000):
t0 = [Link]()
while ([Link](REG_STATUS) & 0x1) == 0:
if ([Link]()-t0)*1000 > timeout_ms:
raise TimeoutError('Accelerator timed out!')
latency_us = (t1-t0)*1e6
return [Link](np.float32), latency_us
if __name__ == '__main__':
[Link](42)
test_image = [Link](28, 28).astype(np.float32)
test_weights = [Link](5, 5).astype(np.float32)
results = benchmark_all_modes(test_image, test_weights)
PHASE
EXPERIMENTS & RESULTS COLLECTION
9 Data to collect for your IEEE paper tables and figures
Metric INT4 Only INT8 Only INT16 Only Mixed (Proposed) Prior Work [3]
LUT (%) - - - - -
DSP48E1 (%) - - - - -
BRAM (%) - - - - -
FF (%) - - - - -
Power (W) - - - - -
Throughput (GOP/s) - - - - -
Energy (GOP/s/W) - - - - -
Precision Accuracy (%) Latency (us) Power (mW) SNR vs FP32 (dB)
INT4 (proposed) - - - -
INT8 (proposed) - - - -
INT16 (proposed) - - - -
Board not found in Vivado Install board files to Xilinx/Vivado/2025.1/data/boards/board_files/ and restart
Vivado
AXI-Lite not auto-detected Port names must start with s_axi_ exactly. Check prefix in axi_ctrl_regs.v
Critical Warning: Clock Domain Ensure all AXI slave aclk ports connect to the same FCLK_CLK0 in block
Crossing design
Timing failure WNS < 0 Reduce FCLK_CLK0 from 100MHz to 50MHz in Zynq PS config and re-run
implementation
Overlay() fails on PYNQ Ensure both .bit AND .hwh files are in the same directory with matching names
[Link]() - No attribute error Check IP instance name in block design. Try ol.<Tab> in Jupyter to see
available names
DMA transfer hangs Check HP0 is enabled in Zynq PS. Ensure DMA MM2S and S2MM connect to
same HP port
Synthesis: reset_rtl_0 undriven Run Connection Automation in block design to connect all processor resets
automatically
LED port not found in XDC Check exact port name in design_1_wrapper.v after generation and update
XDC accordingly
All testbench outputs are X (unknown) rst_n is not deasserted long enough. Extend reset to 5+ cycles in testbench
do_reset task
Final Implementation Checklist
[] Phase 1 Install PYNQ-Z2 board files, create Vivado project with correct part
[] Phase 5 Create block design: Zynq PS + AXI IC + DMA + BRAM + custom IP + connections
[] Phase 6 Add XDC constraints file with LED pins and timing settings
[] Phase 7 Run implementation, verify WNS >= 0, generate bitstream, export XSA
[] Phase 8 Copy .bit and .hwh to PYNQ-Z2, run Python driver, verify output
[] Paper Write 5-page IEEE paper with results tables from Phases 7 & 9