summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Soto <alejandro@34project.org>2024-05-18 09:13:11 -0600
committerAlejandro Soto <alejandro@34project.org>2024-05-24 05:58:41 -0600
commit4cf5ed94a8767efd13265abcc20a5082acc02824 (patch)
tree0a98aaac24245a8daf76d25e8161cd3c03acd2fb
parenta3559d9e53100cd2d9c1e481ec81eeb90ca0ba5b (diff)
tb/gfx_shader_bind: initial commit
-rw-r--r--tb/gfx_shader_bind/dut.sv139
-rw-r--r--tb/gfx_shader_bind/mod.mk10
-rw-r--r--tb/gfx_shader_bind/testbench/__init__.py0
-rw-r--r--tb/gfx_shader_bind/testbench/axi.py190
-rw-r--r--tb/gfx_shader_bind/testbench/checkers.py68
-rw-r--r--tb/gfx_shader_bind/testbench/common.py6
-rw-r--r--tb/gfx_shader_bind/testbench/data.py23
-rw-r--r--tb/gfx_shader_bind/testbench/drivers.py85
-rw-r--r--tb/gfx_shader_bind/testbench/main.py91
-rw-r--r--tb/gfx_shader_bind/testbench/models.py140
-rw-r--r--tb/gfx_shader_bind/testbench/monitors.py30
-rw-r--r--tb/mod.mk2
12 files changed, 783 insertions, 1 deletions
diff --git a/tb/gfx_shader_bind/dut.sv b/tb/gfx_shader_bind/dut.sv
new file mode 100644
index 0000000..e1d1228
--- /dev/null
+++ b/tb/gfx_shader_bind/dut.sv
@@ -0,0 +1,139 @@
+module dut
+import gfx::*;
+(
+ input logic clk,
+ rst_n,
+
+ output logic mem_AWVALID,
+ input logic mem_AWREADY,
+ output logic[7:0] mem_AWID,
+ output logic[7:0] mem_AWLEN,
+ output logic[2:0] mem_AWSIZE,
+ output logic[2:0] mem_AWPROT,
+ output logic[1:0] mem_AWBURST,
+ output word mem_AWADDR,
+
+ output logic mem_WVALID,
+ input logic mem_WREADY,
+ output word mem_WDATA,
+ output logic mem_WLAST,
+ output logic[3:0] mem_WSTRB,
+
+ input logic mem_BVALID,
+ output logic mem_BREADY,
+ input logic[7:0] mem_BID,
+ input logic[1:0] mem_BRESP,
+
+ output logic mem_ARVALID,
+ input logic mem_ARREADY,
+ output logic[7:0] mem_ARID,
+ output logic[7:0] mem_ARLEN,
+ output logic[2:0] mem_ARSIZE,
+ output logic[2:0] mem_ARPROT,
+ output logic[1:0] mem_ARBURST,
+ output word mem_ARADDR,
+
+ input logic mem_RVALID,
+ output logic mem_RREADY,
+ input logic[7:0] mem_RID,
+ input word mem_RDATA,
+ input logic[1:0] mem_RRESP,
+ input logic mem_RLAST,
+
+ input logic icache_flush,
+
+ input logic loop_valid,
+ input group_id loop_group,
+
+ input word_ptr pc_front_pc,
+ output group_id pc_front_group,
+
+ output logic wave_valid,
+ wave_retry,
+ output group_id wave_group,
+ output word wave_insn,
+
+ output logic runnable_in_ready,
+ runnable_out_valid,
+ output group_id runnable_out_data
+);
+
+ if_axib mem();
+ gfx_front_back front_back();
+ gfx_regfile_io regfile();
+
+ assign mem_AWID = mem.s.awid;
+ assign mem_AWLEN = mem.s.awlen;
+ assign mem_AWADDR = mem.s.awaddr;
+ assign mem_AWSIZE = mem.s.awsize;
+ assign mem_AWBURST = mem.s.awburst;
+ assign mem_AWVALID = mem.s.awvalid;
+ assign mem.s.awready = mem_AWREADY;
+
+ assign mem_WDATA = mem.s.wdata;
+ assign mem_WLAST = mem.s.wlast;
+ assign mem_WSTRB = mem.s.wstrb;
+ assign mem_WVALID = mem.s.wvalid;
+ assign mem.s.wready = mem_WREADY;
+
+ assign mem_BREADY = mem.s.bready;
+ assign mem.s.bid = mem_BID;
+ assign mem.s.bresp = mem_BRESP;
+ assign mem.s.bvalid = mem_BVALID;
+
+ assign mem_ARID = mem.s.arid;
+ assign mem_ARLEN = mem.s.arlen;
+ assign mem_ARADDR = mem.s.araddr;
+ assign mem_ARSIZE = mem.s.arsize;
+ assign mem_ARBURST = mem.s.arburst;
+ assign mem_ARVALID = mem.s.arvalid;
+ assign mem.s.arready = mem_ARREADY;
+
+ assign mem_RREADY = mem.s.rready;
+ assign mem.s.rid = mem_RID;
+ assign mem.s.rdata = mem_RDATA;
+ assign mem.s.rresp = mem_RRESP;
+ assign mem.s.rlast = mem_RLAST;
+ assign mem.s.rvalid = mem_RVALID;
+
+ assign mem_AWID = mem.s.awid;
+ assign mem_AWLEN = mem.s.awlen;
+ assign mem_AWADDR = mem.s.awaddr;
+ assign mem_AWSIZE = mem.s.awsize;
+ assign mem_AWBURST = mem.s.awburst;
+ assign mem_AWVALID = mem.s.awvalid;
+ assign mem.s.awready = mem_AWREADY;
+
+ assign mem_WDATA = mem.s.wdata;
+ assign mem_WLAST = mem.s.wlast;
+ assign mem_WSTRB = mem.s.wstrb;
+ assign mem_WVALID = mem.s.wvalid;
+ assign mem.s.wready = mem_WREADY;
+
+ assign pc_front_group = regfile.regs.pc_front_group;
+ assign regfile.regs.pc_front = pc_front_pc;
+
+ assign front_back.back.loop.group = loop_group;
+ assign front_back.back.loop.valid = loop_valid;
+
+ assign wave_insn = front.bind_.wave.insn;
+ assign wave_group = front.bind_.wave.group;
+ assign wave_retry = front.bind_.wave.retry;
+ assign wave_valid = front.bind_.wave.valid;
+
+ assign runnable_in_ready = front.bind_.runnable_in.tx.ready;
+ assign runnable_out_data = front.bind_.runnable_out.rx.data;
+ assign runnable_out_valid = front.bind_.runnable_out.rx.valid;
+
+ gfx_shader_front front
+ (
+ .clk,
+ .rst_n,
+ .front(front_back.front),
+ .reg_read(regfile.read), // Únicamente para que verilator esté feliz
+ .reg_bind(regfile.bind_),
+ .fetch_mem(mem.m),
+ .icache_flush
+ );
+
+endmodule
diff --git a/tb/gfx_shader_bind/mod.mk b/tb/gfx_shader_bind/mod.mk
new file mode 100644
index 0000000..e031f52
--- /dev/null
+++ b/tb/gfx_shader_bind/mod.mk
@@ -0,0 +1,10 @@
+define core
+ $(this)/deps := gfx
+ $(this)/targets := test
+
+ $(this)/rtl_top := dut
+ $(this)/rtl_files := dut.sv
+
+ $(this)/cocotb_paths := .
+ $(this)/cocotb_modules := testbench.main
+endef
diff --git a/tb/gfx_shader_bind/testbench/__init__.py b/tb/gfx_shader_bind/testbench/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/__init__.py
diff --git a/tb/gfx_shader_bind/testbench/axi.py b/tb/gfx_shader_bind/testbench/axi.py
new file mode 100644
index 0000000..2f58531
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/axi.py
@@ -0,0 +1,190 @@
+import enum
+
+import cocotb
+from cocotb.binary import BinaryValue
+from cocotb.triggers import Lock, RisingEdge, ReadOnly
+
+from cocotb_bus.drivers import BusDriver
+
+class AXIBurst(enum.IntEnum):
+ FIXED = 0b00
+ INCR = 0b01
+ WRAP = 0b10
+
+
+class AXIxRESP(enum.IntEnum):
+ OKAY = 0b00
+ EXOKAY = 0b01
+ SLVERR = 0b10
+ DECERR = 0b11
+
+
+class AXIProtocolError(Exception):
+ def __init__(self, message: str, xresp: AXIxRESP):
+ super().__init__(message)
+ self.xresp = xresp
+
+
+class AXIReadBurstLengthMismatch(Exception):
+ pass
+
+
+class AXI4Agent(BusDriver):
+ '''
+ AXI4 Agent
+
+ Monitors an internal memory and handles read and write requests.
+ '''
+ _signals = [
+ "ARREADY", "ARVALID", "ARADDR", # Read address channel
+ "ARLEN", "ARSIZE", "ARBURST", "ARPROT",
+
+ "RREADY", "RVALID", "RDATA", "RLAST", # Read response channel
+
+ "AWREADY", "AWADDR", "AWVALID", # Write address channel
+ "AWPROT", "AWSIZE", "AWBURST", "AWLEN",
+
+ "WREADY", "WVALID", "WDATA",
+
+ ]
+
+ # Not currently supported by this driver
+ _optional_signals = [
+ "WLAST", "WSTRB",
+ "BVALID", "BREADY", "BRESP", "RRESP",
+ "RCOUNT", "WCOUNT", "RACOUNT", "WACOUNT",
+ "ARLOCK", "AWLOCK", "ARCACHE", "AWCACHE",
+ "ARQOS", "AWQOS", "ARID", "AWID",
+ "BID", "RID", "WID"
+ ]
+
+ def __init__(self, entity, name, clock, memory, callback=None, event=None,
+ big_endian=False, **kwargs):
+
+ BusDriver.__init__(self, entity, name, clock, **kwargs)
+ self.clock = clock
+
+ self.big_endian = big_endian
+ self.bus.ARREADY.setimmediatevalue(0)
+ self.bus.RVALID.setimmediatevalue(0)
+ self.bus.RLAST.setimmediatevalue(0)
+ self.bus.AWREADY.setimmediatevalue(0)
+ self._memory = memory
+
+ self.write_address_busy = Lock("%s_wabusy" % name)
+ self.read_address_busy = Lock("%s_rabusy" % name)
+ self.write_data_busy = Lock("%s_wbusy" % name)
+
+ cocotb.start_soon(self._read_data())
+ cocotb.start_soon(self._write_data())
+
+ def _size_to_bytes_in_beat(self, AxSIZE):
+ if AxSIZE < 7:
+ return 2 ** AxSIZE
+ return None
+
+ async def _write_data(self):
+ clock_re = RisingEdge(self.clock)
+
+ while True:
+ while True:
+ self.bus.WREADY.value = 0
+ await ReadOnly()
+ if self.bus.AWVALID.value:
+ self.bus.WREADY.value = 1
+ break
+ await clock_re
+
+ await ReadOnly()
+ _awaddr = int(self.bus.AWADDR)
+ _awlen = int(self.bus.AWLEN)
+ _awsize = int(self.bus.AWSIZE)
+ _awburst = int(self.bus.AWBURST)
+ _awprot = int(self.bus.AWPROT)
+
+ burst_length = _awlen + 1
+ bytes_in_beat = self._size_to_bytes_in_beat(_awsize)
+
+ if __debug__:
+ self.log.debug(
+ "AWADDR %d\n" % _awaddr +
+ "AWLEN %d\n" % _awlen +
+ "AWSIZE %d\n" % _awsize +
+ "AWBURST %d\n" % _awburst +
+ "AWPROT %d\n" % _awprot +
+ "BURST_LENGTH %d\n" % burst_length +
+ "Bytes in beat %d\n" % bytes_in_beat)
+
+ burst_count = burst_length
+
+ await clock_re
+
+ while True:
+ if self.bus.WVALID.value:
+ word = self.bus.WDATA.value
+ word.big_endian = self.big_endian
+ _burst_diff = burst_length - burst_count
+ _st = _awaddr + (_burst_diff * bytes_in_beat) # start
+ _end = _awaddr + ((_burst_diff + 1) * bytes_in_beat) # end
+ self._memory[_st:_end] = array.array('B', word.buff)
+ burst_count -= 1
+ if burst_count == 0:
+ break
+ await clock_re
+
+ async def _read_data(self):
+ clock_re = RisingEdge(self.clock)
+
+ while True:
+ self.bus.ARREADY.value = 1
+ while True:
+ await ReadOnly()
+ if self.bus.ARVALID.value:
+ break
+ await clock_re
+
+ await ReadOnly()
+ _araddr = int(self.bus.ARADDR)
+ _arlen = int(self.bus.ARLEN)
+ _arsize = int(self.bus.ARSIZE)
+ _arburst = int(self.bus.ARBURST)
+ _arprot = int(self.bus.ARPROT)
+
+ burst_length = _arlen + 1
+ bytes_in_beat = self._size_to_bytes_in_beat(_arsize)
+
+ word = BinaryValue(n_bits=bytes_in_beat*8, bigEndian=self.big_endian)
+
+ if __debug__:
+ self.log.debug(
+ "ARADDR %d\n" % _araddr +
+ "ARLEN %d\n" % _arlen +
+ "ARSIZE %d\n" % _arsize +
+ "ARBURST %d\n" % _arburst +
+ "ARPROT %d\n" % _arprot +
+ "BURST_LENGTH %d\n" % burst_length +
+ "Bytes in beat %d\n" % bytes_in_beat)
+
+ burst_count = burst_length
+
+ await clock_re
+ self.bus.ARREADY.value = 0
+
+ self.bus.RVALID.value = 1
+ while burst_count > 0:
+ _burst_diff = burst_length - burst_count
+ _st = _araddr + (_burst_diff * bytes_in_beat)
+ _end = _araddr + ((_burst_diff + 1) * bytes_in_beat)
+ word.buff = self._memory[_st:_end].tobytes()
+ self.bus.RDATA.value = word
+ self.bus.RLAST.value = int(burst_count == 1)
+
+ await ReadOnly()
+
+ if self.bus.RREADY.value:
+ burst_count -= 1
+
+ await clock_re
+ self.bus.RLAST.value = 0
+
+ self.bus.RVALID.value = 0
diff --git a/tb/gfx_shader_bind/testbench/checkers.py b/tb/gfx_shader_bind/testbench/checkers.py
new file mode 100644
index 0000000..0109249
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/checkers.py
@@ -0,0 +1,68 @@
+import cocotb
+from cocotb.triggers import RisingEdge, ReadOnly
+
+from cocotb_coverage.coverage import CoverCheck
+
+class PipelineIntegrityChecker:
+ def __init__(self, dut, name, clk):
+ self._clk, self._dut = clk, dut
+ self._queue = [None] * dut.front.bind_.BIND_STAGES.value
+ self._ready_sticky = False
+
+ @CoverCheck(
+ f'{name}.runnable_tx_ready',
+ f_pass = lambda ready: not self._ready_sticky and ready,
+ f_fail = lambda ready: self._ready_sticky and not ready,
+ )
+ def sample_ready(ready):
+ self._ready_sticky = self._ready_sticky or ready
+
+ @CoverCheck(
+ f'{name}.in_to_out_integrity',
+ f_pass = lambda group: group == self._queue[0] and group is not None,
+ f_fail = lambda group: group != self._queue[0],
+ )
+ def sample_wave_group(group):
+ pass
+
+ self._sample_ready = sample_ready
+ self._sample_wave_group = sample_wave_group
+
+ cocotb.start_soon(self._run())
+
+ async def _run(self):
+ while True:
+ await RisingEdge(self._clk)
+ await ReadOnly()
+
+ self._sample_ready(self._dut.runnable_in_ready.value)
+
+ if self._dut.wave_valid.value:
+ self._sample_wave_group(self._dut.wave_group.value)
+ else:
+ self._sample_wave_group(None)
+
+ new_group = None
+ if self._dut.runnable_out_valid.value:
+ new_group = self._dut.runnable_out_data.value
+
+ self._queue[:-1] = self._queue[1:]
+ self._queue[-1] = new_group
+
+class PcChecker:
+ def __init__(self, dut, name, clk, *, mem, pc_table):
+ self._clk, self._dut = clk, dut
+ self._mem, self._pc_table = mem, pc_table
+
+ @CoverCheck(
+ f'{name}.pc_ok',
+ f_pass = lambda wave: wave.insn and self._pc_ok(wave),
+ f_fail = lambda wave: wave.insn and not self._pc_ok(wave),
+ )
+ def sample_wave(wave):
+ pass
+
+ self.sample_wave = sample_wave
+
+ def _pc_ok(self, wave):
+ return wave.insn == self._mem.read(self._pc_table[wave.group] * 4)
diff --git a/tb/gfx_shader_bind/testbench/common.py b/tb/gfx_shader_bind/testbench/common.py
new file mode 100644
index 0000000..0e9a97b
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/common.py
@@ -0,0 +1,6 @@
+import cocotb
+
+MAX_GROUPS = 64
+MEM_WORDS = 64 << (20 - 2)
+
+log = cocotb.logging.getLogger("cocotb.test")
diff --git a/tb/gfx_shader_bind/testbench/data.py b/tb/gfx_shader_bind/testbench/data.py
new file mode 100644
index 0000000..4aa3630
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/data.py
@@ -0,0 +1,23 @@
+BAD_PC = 0xffff_ffff >> 2
+
+class FrontWave:
+ def __init__(self, *, group, insn, soft=False):
+ self.group, self.insn = group, insn
+ self.retry = insn is None
+ self._soft = soft
+
+ def __eq__(self, other):
+ if self._soft and not other._soft:
+ return other.__eq__(self)
+
+ if other._soft:
+ return self.group == other.group and \
+ ((other.insn is None and self.insn is None) or \
+ (other.insn is not None and (not self.insn or self.insn == other.insn)))
+
+ return self.group == other.group and self.insn == other.insn
+
+ def __repr__(self):
+ insn = f', insn=0x{self.insn:08x}' if not self.retry else ''
+ soft = f', soft' if self._soft else ''
+ return f'FrontWave(group={self.group}, retry={int(self.retry)}{insn}{soft})'
diff --git a/tb/gfx_shader_bind/testbench/drivers.py b/tb/gfx_shader_bind/testbench/drivers.py
new file mode 100644
index 0000000..c069d97
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/drivers.py
@@ -0,0 +1,85 @@
+import cocotb
+from cocotb.clock import Clock
+from cocotb.queue import Queue
+from cocotb.triggers import Event, ReadOnly, RisingEdge
+
+from cocotb_bus.drivers import BusDriver
+
+from .data import BAD_PC
+
+class ClockResetDriver:
+ def __init__(self, dut):
+ self._reset_event = Event('reset_done')
+
+ dut.clk.setimmediatevalue(0)
+ dut.rst_n.setimmediatevalue(0)
+
+ self._dut = dut
+ self._clock_gen = Clock(dut.clk, 2, 'step')
+
+ def start(self):
+ cocotb.start_soon(self._clock_gen.start())
+ cocotb.start_soon(self.reset())
+
+ async def reset(self):
+ self._reset_event.clear()
+
+ self._dut.rst_n.value = 0
+ await RisingEdge(self._dut.clk)
+ self._dut.rst_n.value = 1
+ await ReadOnly()
+
+ self._reset_event.set()
+
+ async def wait_for_reset(self):
+ await self._reset_event.wait()
+
+class PcDriver(BusDriver):
+ _signals = ['pc', 'group']
+
+ def __init__(self, *args, table, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self._table = table
+ self._delay1 = BAD_PC
+ self._delay2 = BAD_PC
+ self._delay3 = BAD_PC
+
+ cocotb.start_soon(self._run())
+
+ async def _run(self):
+ while True:
+ self.bus.pc.value = self._delay3
+
+ await RisingEdge(self.clock)
+
+ self._delay3 = self._delay2
+ self._delay2 = self._delay1
+ self._delay1 = self._table[self.bus.group.value]
+
+class LoopDriver(BusDriver):
+ _signals = ['group', 'valid']
+
+ def __init__(self, *args, maxsize=8, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self._queue = Queue(maxsize=maxsize)
+ self.bus.valid.setimmediatevalue(0)
+
+ cocotb.start_soon(self._run())
+
+ async def put(self, group):
+ await self._queue.put(group)
+
+ async def _run(self):
+ while True:
+ await RisingEdge(self.clock)
+
+ valid = not self._queue.empty()
+ if valid:
+ group = self._queue.get_nowait()
+ valid = group is not None
+
+ self.bus.valid.value = int(valid)
+ if valid:
+ self.bus.group.value = group
diff --git a/tb/gfx_shader_bind/testbench/main.py b/tb/gfx_shader_bind/testbench/main.py
new file mode 100644
index 0000000..f31e198
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/main.py
@@ -0,0 +1,91 @@
+import random
+
+import cocotb
+from cocotb.triggers import ClockCycles, FallingEdge, ReadOnly, RisingEdge
+
+from cocotb_bus.scoreboard import Scoreboard
+
+from cocotb_coverage.coverage import coverage_db
+
+from .axi import AXI4Agent
+from .data import FrontWave
+from .common import MAX_GROUPS, MEM_WORDS, log
+from .models import Memory, PcTable
+from .drivers import ClockResetDriver, LoopDriver, PcDriver
+from .checkers import PcChecker, PipelineIntegrityChecker
+from .monitors import FrontWaveMonitor
+
+@cocotb.test()
+async def test(dut):
+ mem = Memory('insn_mem', word_size=4, words=MEM_WORDS)
+ mem_agent = AXI4Agent(dut, 'mem', dut.clk, mem, case_insensitive=False)
+
+ out_groups = set()
+ out_groups_retry = set()
+ out_monitor = FrontWaveMonitor(dut, 'wave', dut.clk, case_insensitive=False)
+
+ def out_callback(wave):
+ nonlocal out_groups, out_groups_retry
+
+ out_groups.add(wave.group)
+ if wave.insn is None:
+ out_groups_retry.add(wave.group)
+
+ out_monitor.add_callback(out_callback)
+
+ out_expected = []
+ out_scoreboard = Scoreboard(dut, fail_immediately=False)
+ out_scoreboard.add_interface(out_monitor, out_expected)
+
+ clock_reset = ClockResetDriver(dut)
+ clock_reset.start()
+
+ pc_table = PcTable()
+ pc_driver = PcDriver(dut, 'pc_front', dut.clk, table=pc_table, case_insensitive=False)
+ loop_driver = LoopDriver(dut, 'loop', dut.clk, case_insensitive=False)
+
+ pc_checker = PcChecker(dut, 'bind', dut.clk, mem=mem, pc_table=pc_table)
+ pipeline_checker = PipelineIntegrityChecker(dut, 'bind', dut.clk)
+
+ out_monitor.add_callback(pc_checker.sample_wave)
+
+ await clock_reset.wait_for_reset()
+
+ await FallingEdge(dut.front.bind_.icache.in_flush) #FIXME
+ await RisingEdge(dut.clk)
+
+ for iteration in range(1000):
+ all_groups = list(range(MAX_GROUPS))
+ random.shuffle(all_groups)
+
+ for group in all_groups:
+ pc = random.randint(0, MEM_WORDS) * 4
+ pc_table[group] = pc
+ mem.randomize_line(pc)
+
+ out_expected.append(FrontWave(group=group, insn=mem.read(pc), soft=True))
+ await loop_driver.put(group)
+
+ #while out_groups_retry or len(out_groups) < MAX_GROUPS:
+ for _ in range(100):
+ if out_groups_retry:
+ group = out_groups_retry.pop()
+ out_groups.remove(group)
+
+ out_expected.append(FrontWave(group=group, insn=mem.read(pc_table[group] * 4), soft=True))
+
+ await loop_driver.put(group)
+
+ await RisingEdge(dut.clk)
+
+ for _ in range(100):
+ await RisingEdge(dut.clk)
+
+ out_groups.clear()
+ out_groups_retry.clear()
+
+ coverage_db.report_coverage(log.info, bins=True)
+ coverage_db.export_to_xml(filename="coverage.xml")
+ coverage_db.export_to_yaml(filename='coverage.yml')
+
+ raise out_scoreboard.result
diff --git a/tb/gfx_shader_bind/testbench/models.py b/tb/gfx_shader_bind/testbench/models.py
new file mode 100644
index 0000000..928f07b
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/models.py
@@ -0,0 +1,140 @@
+import random
+
+from cocotb.binary import BinaryValue
+
+from cocotb_coverage.coverage import CoverCross, CoverPoint
+
+from .data import BAD_PC
+from .common import log
+
+class PcTable:
+ def __init__(self):
+ self._pcs = {}
+
+ def __getitem__(self, group):
+ if isinstance(group, BinaryValue):
+ group = group.integer
+
+ return self._pcs.get(group, BAD_PC)
+
+ def __setitem__(self, group, pc):
+ assert (pc & 3) == 0
+ if isinstance(group, BinaryValue):
+ group = group.integer
+
+ self._pcs[group] = pc >> 2
+
+class Memory:
+ def __init__(self, name, *, word_size, words, start=0):
+ word_mask = word_size - 1
+ assert word_size > 0 and (word_mask & word_size) == 0, \
+ f'{word_size} is not a power of two'
+
+ self._data = {}
+ self._dirty = set()
+ self._observed = set()
+
+ self._start = start
+ self._words = words
+ self._word_size = word_size
+ self._subword_mask = word_mask
+
+ self._all_ones = (1 << (8 * word_size)) - 1
+
+ @CoverPoint(
+ f'{name}.read_dirty',
+ bins = [True, False],
+ bins_labels = ['dirty', 'clean'],
+ rel = lambda word_num, dirty: self._test_bin(word_num, self._dirty, dirty),
+ )
+ @CoverPoint(
+ f'{name}.read_observed',
+ bins = [True, False],
+ bins_labels = ['multiple_reads', 'first_read'],
+ rel = lambda word_num, observed: self._test_bin(word_num, self._observed, observed),
+ )
+ @CoverCross(
+ f'{name}.read_dirty_observed',
+ items = [
+ f'{name}.read_dirty',
+ f'{name}.read_observed',
+ ],
+ ign_bins = [
+ ('dirty', 'first_read'),
+ ],
+ )
+ def _read_word(word_num):
+ if self._expect_in_range(word_num):
+ self._observed.add(word_num)
+
+ word = self._data.get(word_num)
+ if word is None:
+ log.warning(f'Uninitialized memory read: {self._addr_repr(word_num)}')
+ word = self._all_ones
+
+ return word
+
+ @CoverPoint(
+ f'{name}.write_dirty',
+ bins = [True, False],
+ bins_labels = ['dirty_write', 'clean_write'],
+ xf = lambda word_num, data: word_num,
+ rel = lambda word_num, dirty: self._test_bin(word_num, self._observed, dirty),
+ )
+ def _write_word(word_num, data):
+ if self._expect_in_range(word_num):
+ self._data[word_num] = data
+ if word_num in self._observed:
+ self._dirty.add(word_num)
+
+ self._read_word = _read_word
+ self._write_word = _write_word
+
+ def read(self, addr):
+ return self._data[addr >> 2]
+
+ def randomize_line(self, addr):
+ first_word = (addr >> 2) & ~15
+ for word_num in range(first_word, first_word + 16):
+ self._write_word(word_num, random.randint(0, self._all_ones))
+
+ def __getitem__(self, index):
+ if not isinstance(index, slice):
+ return super()[index]
+
+ assert index.stop >= index.start
+ assert (index.stop & self._subword_mask) == 0
+ assert (index.start & self._subword_mask) == 0
+
+ return MemoryRead(self, index.start // self._word_size, index.stop // self._word_size)
+
+ def _expect_in_range(self, word_num):
+ delta = word_num - self._start
+
+ in_range = delta >= 0 and delta < self._words
+ if not in_range:
+ log.error(f'Bad memory address: {self._addr_repr(word_num)}')
+
+ return in_range
+
+ def _test_bin(self, word_num, flag_set, flag_bin):
+ if not self._expect_in_range(word_num):
+ return False
+
+ return (word_num in flag_set) == flag_bin
+
+ def _addr_repr(self, word_num):
+ addr = word_num * self._word_size
+ return f'0x{addr:08x}'
+
+class MemoryRead:
+ def __init__(self, mem, start, stop):
+ self._mem, self._start, self._stop = mem, start, stop
+
+ def tobytes(self):
+ array = bytearray()
+ for word_num in range(self._start, self._stop):
+ word = self._mem._read_word(word_num)
+ array.extend(word.to_bytes(self._mem._word_size, 'little'))
+
+ return array
diff --git a/tb/gfx_shader_bind/testbench/monitors.py b/tb/gfx_shader_bind/testbench/monitors.py
new file mode 100644
index 0000000..6ecea0f
--- /dev/null
+++ b/tb/gfx_shader_bind/testbench/monitors.py
@@ -0,0 +1,30 @@
+import cocotb
+from cocotb.triggers import ReadOnly, RisingEdge
+
+from cocotb_bus.monitors import BusMonitor
+
+from .data import FrontWave
+
+class FrontWaveMonitor(BusMonitor):
+ _signals = ['insn', 'group', 'retry', 'valid']
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ async def _monitor_recv(self):
+ pkt_receiving = False
+ received_data = []
+
+ while True:
+ await RisingEdge(self.clock)
+ await ReadOnly()
+
+ if not self.bus.valid.value:
+ continue
+
+ wave = FrontWave(
+ group=self.bus.group.value.integer,
+ insn=(self.bus.insn.value.integer if not self.bus.retry.value else None),
+ )
+
+ self._recv(wave)
diff --git a/tb/mod.mk b/tb/mod.mk
index ff445be..c45f56e 100644
--- a/tb/mod.mk
+++ b/tb/mod.mk
@@ -1,5 +1,5 @@
cores := ip_mul interconnect
-subdirs := top/conspiracion
+subdirs := gfx_shader_bind top/conspiracion
define core/ip_mul
$(this)/rtl_files := dsp_mul.sv