File size: 9,191 Bytes
2166d7e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | """
Cocotb Testbench for Systolic Array (8x8)
Validates:
1. Matrix multiplication: C = A @ B^T (8x8 fixed-point)
2. Dataflow: A row-broadcast, B column-broadcast, C bottom-propagate
3. Pipeline: 13-cycle latency
4. Numerical accuracy: Fixed-point Q4.4 arithmetic
Test Method:
- Generate random 8x8 Q4.4 matrices (A, B)
- Compute golden reference using NumPy
- Feed data to RTL via AXI-like handshake
- Collect output, compare with tolerance
"""
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, FallingEdge, Timer
import numpy as np
import random
def matrix_to_fixed_point(matrix, int_bits=4, frac_bits=4, dtype=np.int8):
"""
Convert floating-point matrix to fixed-point (Q4.4) representation.
Args:
matrix: (8, 8) float array
int_bits, frac_bits: Fixed-point format
dtype: output data type (int8 for Q4.4)
Returns:
Fixed-point matrix (int8)
"""
scale = 2 ** frac_bits
fixed = np.round(matrix * scale).astype(dtype)
return fixed
def fixed_point_to_float(value, frac_bits=4, dtype=np.float32):
"""Convert fixed-point back to float."""
return np.float32(value) / (2 ** frac_bits)
def golden_matmul_fixed_point(A_fp, B_fp, int_bits=4, frac_bits=4):
"""
Golden model: Matrix multiplication in fixed-point arithmetic.
Args:
A_fp, B_fp: (8, 8) fixed-point matrices (Q4.4)
Returns:
C_fp: (8, 8) fixed-point result (16-bit accumulator range)
"""
A = fixed_point_to_float(A_fp, frac_bits)
B = fixed_point_to_float(B_fp, frac_bits)
# Standard matrix multiply: C = A @ B^T
C_float = A @ B.T
# Convert back to fixed-point (16-bit range for safety)
scale = 2 ** frac_bits
C_fp = np.round(C_float * scale).astype(np.int16)
return C_fp
class SystemVerilogInterface:
"""Wrapper for SystemVerilog signal access."""
def __init__(self, dut):
self.dut = dut
async def write_input_block(self, A_block, B_block):
"""
Write an 8x8 block to inputs.
Args:
A_block: (8,) uint8 array (feeds left side, row 0-7)
B_block: (8,) uint8 array (feeds top side, col 0-7)
"""
# Set valid_in = 1
self.dut.valid_in.value = 1
# Broadcast A to rows
for i in range(8):
self.dut.matrix_A_in[i].value = int(A_block[i])
# Broadcast B to columns
for j in range(8):
self.dut.matrix_B_in[j].value = int(B_block[j])
# Wait for clock edge
await RisingEdge(self.dut.clk)
# De-assert valid
self.dut.valid_in.value = 0
async def read_output_block(self, latency_cycles=14):
"""
Wait for output valid and read C block.
Args:
latency_cycles: Cycles to wait for output validity
Returns:
C_block: (8, 8) array of accumulated results
"""
# Wait for valid_out to pulse
for _ in range(latency_cycles + 5):
await RisingEdge(self.dut.clk)
if self.dut.valid_out.value == 1:
break
# Read output on valid_out assertion
C_block = np.zeros((8, 8), dtype=np.int16)
for i in range(8):
for j in range(8):
C_block[i][j] = int(self.dut.matrix_C_out[i][j].value)
return C_block
@cocotb.test()
async def test_systolic_array_basic(dut):
"""Basic functionality test: single matrix multiplication."""
cocotb.log.info("=" * 70)
cocotb.log.info("TEST: Systolic Array Basic Functionality")
cocotb.log.info("=" * 70)
# Create interface wrapper
iface = SystemVerilogInterface(dut)
# Start clock
cocotb.log.info("Starting 10ns clock...")
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
# Reset
cocotb.log.info("Asserting reset...")
dut.rst_n.value = 0
await Timer(30, "ns")
dut.rst_n.value = 1
dut.valid_in.value = 0
await Timer(30, "ns")
# ===================================================================
# Test Case 1: Identity-like matrices
# ===================================================================
cocotb.log.info("\nTest 1: Small values (easy validation)")
# A: simple pattern
A_fp = np.array([
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 1],
], dtype=np.int8)
# B: simple pattern
B_fp = np.array([
[1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 1],
], dtype=np.int8)
# Compute golden reference
C_fp_golden = golden_matmul_fixed_point(A_fp, B_fp)
cocotb.log.info(f"Golden C (identity x identity):\n{C_fp_golden}")
# Write input block (A and B are transmitted row/column-wise)
# For this simple implementation, we'll simulate row-by-row transmission
# Actually, let's simplify: feed entire block at once
# The testbench interface sends A_in[0:8] and B_in[0:8] in parallel
# Wait a bit
await Timer(100, "ns")
# Send data
cocotb.log.info("Sending input block...")
await iface.write_input_block(A_fp[0], B_fp[0])
cocotb.log.info("Waiting for output...")
C_block_rtl = await iface.read_output_block(latency_cycles=13)
cocotb.log.info(f"RTL output:\n{C_block_rtl}")
# Compare (only check [0,0] for identity case - should be 1)
cocotb.log.info(f"\nResult C[0,0]: golden={C_fp_golden[0,0]}, rtl={C_block_rtl[0,0]}")
# ===================================================================
# Test Case 2: Random small values
# ===================================================================
cocotb.log.info("\n" + "=" * 70)
cocotb.log.info("Test 2: Random Q4.4 values")
# Generate random Q4.4 data
A_fp_rand = np.random.randint(-8, 8, (8, 8), dtype=np.int8)
B_fp_rand = np.random.randint(-8, 8, (8, 8), dtype=np.int8)
cocotb.log.info(f"Random A_fp:\n{A_fp_rand}")
cocotb.log.info(f"Random B_fp:\n{B_fp_rand}")
# Golden
C_fp_golden_rand = golden_matmul_fixed_point(A_fp_rand, B_fp_rand)
cocotb.log.info(f"Golden C_fp:\n{C_fp_golden_rand}")
# Reset DUT
await Timer(100, "ns")
# Send
cocotb.log.info("Sending random block...")
await iface.write_input_block(A_fp_rand[0], B_fp_rand[0])
cocotb.log.info("Waiting for RTL output...")
C_block_rtl_rand = await iface.read_output_block(latency_cycles=13)
cocotb.log.info(f"RTL output:\n{C_block_rtl_rand}")
# Compare a few elements
cocotb.log.info("\nComparison (golden vs RTL):")
errors = 0
for i in range(min(4, 8)):
for j in range(min(4, 8)):
diff = abs(int(C_fp_golden_rand[i, j]) - int(C_block_rtl_rand[i, j]))
status = "✓" if diff == 0 else "✗"
cocotb.log.info(f" C[{i},{j}]: golden={C_fp_golden_rand[i,j]:6d}, "
f"rtl={C_block_rtl_rand[i,j]:6d}, diff={diff:6d} {status}")
if diff > 1: # Allow small tolerance for fixed-point
errors += 1
if errors == 0:
cocotb.log.info("\n✓ TEST PASSED")
else:
cocotb.log.info(f"\n✗ TEST FAILED ({errors} mismatches)")
# ===================================================================
# Test Case 3: Pipeline latency check
# ===================================================================
cocotb.log.info("\n" + "=" * 70)
cocotb.log.info("Test 3: Pipeline latency measurement")
A_test = np.ones((8, 8), dtype=np.int8)
B_test = np.ones((8, 8), dtype=np.int8)
await Timer(100, "ns")
# Send with precise timing
dut.valid_in.value = 1
for i in range(8):
dut.matrix_A_in[i].value = 1
dut.matrix_B_in[i].value = 1
cycle_sent = 0
await RisingEdge(dut.clk)
cycle_sent = 0
dut.valid_in.value = 0
# Wait for valid_out
cycle_count = 0
while cycle_count < 30:
await RisingEdge(dut.clk)
cycle_count += 1
if dut.valid_out.value == 1:
cocotb.log.info(f"✓ Output valid after {cycle_count} cycles")
break
cocotb.log.info("=" * 70)
@cocotb.test()
async def test_systolic_array_reset(dut):
"""Test reset functionality."""
cocotb.log.info("TEST: Reset functionality")
clock = Clock(dut.clk, 10, units="ns")
cocotb.start_soon(clock.start())
# Apply reset
dut.rst_n.value = 0
await Timer(50, "ns")
dut.rst_n.value = 1
cocotb.log.info("✓ Reset test passed")
|