Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ sim-gl-sensor-bridge-full: ## Run long gate-level sensor I2C debug-feature refer
cd cocotb; PYTHONPATH=$(MAKEFILE_DIR)/cocotb/sim/tb GL=1 GL_SENSOR_I2C_FULL_FEATURES=1 CHIP_TOPLEVEL=sim_chip_top_gl_sensor_bridge_env CHIP_NETLIST_TOP=$(TOP) FINAL_DIR=$(FINAL_DIR) PDK_ROOT=${PDK_ROOT} PDK=${PDK} SLOT=${SLOT} COCOTB_TEST_MODULE=test_chip_top_gl_sensor_bridge COCOTB_TEST_FILTER=test_chip_top_gl_sensor_bridge_debug_features_match_python_reference $(PYTHON) chip_top_tb.py
.PHONY: sim-gl-sensor-bridge-full

sim-rtl-sensor-i2c-pads: ## Run RTL chip_top sensor I2C pad test with cocotbext-i2c
cd cocotb; PYTHONPATH=$(MAKEFILE_DIR)/cocotb/sim/tb CHIP_TOP_PAD_I2C=1 CHIP_TOPLEVEL=sim_chip_top_gl_sensor_bridge_env PDK_ROOT=${PDK_ROOT} PDK=${PDK} SLOT=${SLOT} COCOTB_TEST_MODULE=test_chip_top_i2c_pads COCOTB_TEST_FILTER=test_chip_top_i2c_pads_reach_cocotbext_sensor_models $(PYTHON) chip_top_tb.py
.PHONY: sim-rtl-sensor-i2c-pads


sim-view: ## View simulation waveforms in GTKWave
gtkwave cocotb/sim_build/chip_top.fst
Expand Down
2 changes: 2 additions & 0 deletions cocotb/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ FW_C_test_ml_weights := $(FW_TESTS)/test_ml_weights.c
FW_C_test_ml_cpu_wake_i2c := $(FW_TESTS)/test_ml_cpu_wake_i2c.c
FW_C_test_top_feature_ml_cpu:= $(FW_TESTS)/test_top_feature_ml_cpu.c
FW_C_test_top_feature_ml_cpu_spi_flash:= $(FW_TESTS)/test_top_feature_ml_cpu_spi_flash.c
FW_C_test_top_feature_ml_30logits := $(FW_TESTS)/test_top_feature_ml_30logits.c
FW_C_test_top_ml_control_unified := $(FW_TESTS)/test_top_ml_control_unified.c
FW_C_test_top_sleep_wake_unified := $(FW_TESTS)/test_top_sleep_wake_unified.c
FW_C_test_top_ml_golden_vector_unified := $(FW_TESTS)/test_top_ml_golden_vector_unified.c
Expand All @@ -283,6 +284,7 @@ FW_ARCH_test_ml_cpu_wake_i2c := rv32im
FW_ARCH_prod_main := rv32im
FW_ARCH_test_top_feature_ml_cpu := rv32im
FW_ARCH_test_top_feature_ml_cpu_spi_flash := rv32im
FW_ARCH_test_top_feature_ml_30logits := rv32im
FW_ARCH_test_top_ml_control_unified := rv32im
FW_ARCH_test_top_sleep_wake_unified := rv32im
FW_ARCH_test_top_ml_golden_vector_unified := rv32im
Expand Down
2 changes: 2 additions & 0 deletions cocotb/chip_top_sim_wrap.sv
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ module chip_top_sim_wrap #(
.PPG_POLL_PERIOD_TICKS (2),
.PPG_WATERMARK (8),
.PPG_MAX_BURST_SAMPLES (32),
.CFG_MOTION_HI_TH (16'hFFFF),
.CFG_MAX_MOTION_HI (16'hFFFF)
.TIMER_RELOAD_DEFAULT (1000)
)
`endif
Expand Down
208 changes: 106 additions & 102 deletions cocotb/chip_top_tb.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@
gl = os.getenv("GL", False)
slot = os.getenv("SLOT", "1x1")
test_module = os.getenv("COCOTB_TEST_MODULE", "chip_top_tb")
pad_i2c_rtl = os.getenv("CHIP_TOP_PAD_I2C", "0") == "1"


hdl_toplevel = os.getenv("CHIP_TOPLEVEL", "chip_top_sim_wrap")
chip_netlist_top = os.getenv("CHIP_NETLIST_TOP", "chip_top")


_PROJ = Path(__file__).resolve().parent
_FIRMWARE_NAME = os.getenv("FIRMWARE_NAME", "test_top_feature_ml_cpu_spi_flash")
_FIRMWARE_NAME = os.getenv("FIRMWARE_NAME", "test_top_feature_ml_30logits")
_FIRMWARE_HEX = str(_PROJ / "firmware" / "build" / _FIRMWARE_NAME / "firmware.hex")
_WEIGHT_HEX = str(_PROJ / "firmware" / "build" / "generated" / "taketwo_params.hex")

Expand Down Expand Up @@ -178,22 +179,56 @@ async def test_chip_top_boot(dut):

#full pipeline test

_N_LOGITS = 30 # number of inferences firmware runs before writing CAFEBABE


async def _feat_monitor(u_top):
"""Background coroutine: prints features when they change, logits when they change."""
prev_feat = None
prev_logit = None
while True:
await RisingEdge(u_top.feat_valid_o)

mot = _s16_or_none(u_top.feat_motion_latched_r)
tim = _s16_or_none(u_top.feat_time_latched_r)
dhr = _s16_or_none(u_top.feat_delta_hr_latched_r)
msd = _s16_or_none(u_top.feat_mssd_latched_r)
curr_feat = (mot, tim, dhr, msd)
if curr_feat != prev_feat:
print(
f"[feat] mot={mot!s:>7} tim={tim!s:>6} "
f"dhr={dhr!s:>7} msd={msd!s:>7}",
flush=True,
)
prev_feat = curr_feat

try:
lr = int(u_top.u_weight_ram.logit_reg_0.value)
log0 = _s16(lr & 0xFFFF)
log1 = _s16((lr >> 16) & 0xFFFF)
curr_logit = (log0, log1)
except Exception:
curr_logit = None
if curr_logit is not None and curr_logit != prev_logit:
pred = "non-N3" if log1 > log0 else "N3"
print(f"[logit] log0={log0:6d} log1={log1:6d} → {pred}", flush=True)
prev_logit = curr_logit


@cocotb.test(skip=(hdl_toplevel != "chip_top_sim_wrap"))
async def test_chip_top_feature_inject(dut):
"""Full pipeline: sensor → features → ML inferencealarm output."""
"""Full pipeline: sensor → features → 30 ML inferencesprint logits live."""
logger = logging.getLogger("chip_top_feature_inject")

# ALL mode: features + ML + CPU all active; bypasses SLEEP state in sim.
await set_defaults(dut)
dut.input_PAD.value = 0b00000101
await start_clock(dut.clk_PAD)
await reset(dut.rst_n_PAD)

core = _core(dut)
u_top = _top(dut)

BOOT_TIMEOUT = 500_000
RUNTIME_TIMEOUT = 10_000_000
RUNTIME_TIMEOUT = 3_000_000

# --- Phase 1: wait for boot ---
logger.info("Waiting for boot_done...")
Expand All @@ -205,119 +240,82 @@ async def test_chip_top_feature_inject(dut):
raise AssertionError("Timeout waiting for boot_done")

assert core.pico_trap_w.value == 0, "CPU trapped during boot"
logger.info("Boot done. Waiting for firmware to signal pass/fail...")
logger.info("Boot done. Waiting for 30 logits...")

# Start background monitor — prints features + stale logits on every feat_valid_o
cocotb.start_soon(_feat_monitor(u_top))

# --- Phase 2: collect 30 logits as firmware writes them ---
# Firmware writes TEST_STATUS = 1..30 (one per inference), then 0xCAFEBABE.
# TEST_CODE = packed logits: bits[15:0]=log0, bits[31:16]=log1.
last_status = 0
logit_count = 0

# --- Phase 2: wait for firmware test_status (CAFE_BABE = pass, DEAD_BEEF = fail) ---
for cycle in range(RUNTIME_TIMEOUT):
await RisingEdge(dut.clk_PAD)

if cycle % 100_000 == 0:
try:
ts = u_top.test_status.value.to_unsigned()
tc_raw = u_top.test_code.value
tc_str = str(tc_raw)
alarm = dut.alarm_o.value
try:
tc_int = tc_raw.to_unsigned()
tc_display = f"0x{tc_int:08X}"
except ValueError:
tc_display = f"X:{tc_str}"
logger.info(
f" cycle {cycle}: test_status=0x{ts:08X} "
f"test_code={tc_display} alarm={alarm}"
)
except Exception:
pass

if core.pico_trap_w.value == 1:
raise AssertionError("CPU trapped during runtime")
raise AssertionError(f"CPU trapped at cycle {cycle}")

try:
status = u_top.test_status.value.to_unsigned()
except Exception:
continue

if status == 0xDEAD_BEEF:
tc_raw = u_top.test_code.value
try:
code = tc_raw.to_unsigned()
code_str = f"0x{code:08X}"
except ValueError:
code_str = f"X:{tc_raw!s}"
code_str = f"0x{u_top.test_code.value.to_unsigned():08X}"
except Exception:
code_str = str(u_top.test_code.value)
raise AssertionError(f"Firmware reported FAIL: test_code={code_str}")

if status == 0xCAFE_BABE:
tc_raw = u_top.test_code.value
try:
code = tc_raw.to_unsigned()
code_str = f"0x{code:08X}"
except ValueError:
code = None
code_str = f"X:{tc_raw!s}"
logger.info(
f"Firmware reported PASS: test_code={code_str} "
f"alarm={dut.alarm_o.value}"
)

# Check bits 30 and 29 by position in binary string (MSB first).
# This handles X bits in other positions (e.g., the confidence field).
binstr = str(tc_raw) # 32-char string: '0'/'1'/'X'/'Z', MSB at [0]
bit30 = binstr[1] # bit 30 = outputs_mutated
bit29 = binstr[2] # bit 29 = saw_busy
assert bit30 == "1", \
f"Firmware: ML output mutation not observed (bit30={bit30}, test_code={code_str})"
assert bit29 == "1", \
f"Firmware: ML BUSY high not observed (bit29={bit29}, test_code={code_str})"

if code is not None:
logger.info(
f" predicted_class={code >> 31} "
f"outputs_mutated={(code >> 30) & 1} "
f"saw_busy={(code >> 29) & 1} "
f"confidence={code & 0xFFFF}"
)
else:
logger.warning(
f"test_code has X bits — confidence field indeterminate. "
f"Raw: {code_str}. Check logit WRAM region for X propagation."
)
await ClockCycles(dut.clk_PAD, 4)
await ReadOnly()

dbg_log0 = _s16_or_none(u_top.logit0)
dbg_log1 = _s16_or_none(u_top.logit1)
logit_word0 = _u32_or_none(u_top.u_weight_ram.logit_reg_0)
logit_word1 = _u32_or_none(u_top.u_weight_ram.logit_reg_1)

if dbg_log0 is not None and dbg_log1 is not None:
logger.info(f" logits_dbg=({dbg_log0}, {dbg_log1})")
else:
logger.warning(
f"top-level dbg logits unresolved: "
f"logit0={u_top.logit0.value} logit1={u_top.logit1.value}"
)
if status != last_status:
last_status = status

if logit_word0 is not None:
reg_log0 = _s16(logit_word0)
reg_log1 = _s16(logit_word0 >> 16)
aux_word = "X" if logit_word1 is None else f"0x{logit_word1:08X}"
logger.info(
f" logits_reg=({reg_log0}, {reg_log1}) "
f"word0=0x{logit_word0:08X} word1={aux_word}"
)
else:
logger.warning(
f"logit register window unresolved: "
f"word0={u_top.u_weight_ram.logit_reg_0.value} "
f"word1={u_top.u_weight_ram.logit_reg_1.value}"
# Each value 1..N_LOGITS signals a completed inference
if 1 <= status <= _N_LOGITS:
try:
tc = u_top.test_code.value.to_unsigned()
log0 = _s16(tc & 0xFFFF)
log1 = _s16((tc >> 16) & 0xFFFF)
pred = "N3" if log1 > log0 else "non-N3"
# Read latched feature values directly from RTL for diagnostics
try:
mot = _s16(int(u_top.feat_motion_latched_r.value))
tim = _s16(int(u_top.feat_time_latched_r.value))
dhr = _s16(int(u_top.feat_delta_hr_latched_r.value))
msd = _s16(int(u_top.feat_mssd_latched_r.value))
feat_str = f" feat=[mot={mot} tim={tim} dhr={dhr} msd={msd}]"
except Exception:
feat_str = ""
logger.info(
f" logit {status:2d}/{_N_LOGITS}: "
f"log0={log0:6d} log1={log1:6d} → {pred}{feat_str}"
)
logit_count += 1
except Exception as e:
logger.warning(f" logit {status}: could not read TEST_CODE ({e})")

if status == 0xCAFE_BABE:
assert logit_count == _N_LOGITS, (
f"Expected {_N_LOGITS} logits before PASS, got {logit_count}"
)

logger.info("Full pipeline test passed.")
return
try:
tc = u_top.test_code.value.to_unsigned()
log0 = _s16(tc & 0xFFFF)
log1 = _s16((tc >> 16) & 0xFFFF)
pred_class = (tc >> 31) & 1
logger.info(
f"Full pipeline PASS — 30 logits collected. "
f"Final: log0={log0} log1={log1} class={pred_class}"
)
except Exception:
logger.info(f"Full pipeline PASS — 30 logits collected.")
return

raise AssertionError(
f"Timeout after {RUNTIME_TIMEOUT} cycles — "
f"firmware never reached pass/fail state"
f"only {logit_count}/{_N_LOGITS} logits collected"
)


Expand Down Expand Up @@ -353,7 +351,7 @@ async def test_chip_top_normal_mode(dut):
u_top = _top(dut)

BOOT_TIMEOUT = 500_000
RUNTIME_TIMEOUT = 10_000_000
RUNTIME_TIMEOUT = 1#10_000_000

logger.info("Normal mode test started. Monitoring for boot completion...")

Expand Down Expand Up @@ -438,8 +436,11 @@ def chip_top_runner():
proj_path = Path(__file__).resolve().parent

sources = []
# RTL builds use the SIM-only sensor bus; GL builds below intentionally do not.
defines = {f"SLOT_{slot.upper().replace('.', 'P')}": True, "SIM": True}
# Most RTL builds use the SIM-only sensor bus. Pad-level I2C RTL tests
# intentionally compile without SIM so i2c_master drives bidir_PAD[23:24].
defines = {f"SLOT_{slot.upper().replace('.', 'P')}": True}
if not pad_i2c_rtl:
defines["SIM"] = True
includes = [proj_path / "../src/"]

if gl:
Expand Down Expand Up @@ -477,7 +478,7 @@ def chip_top_runner():
defines = {"FUNCTIONAL": True, "functional": True, "USE_POWER_PINS": True}
else:
src_dir = proj_path / "../src"
pad_level = hdl_toplevel in {"chip_top", "chip_top_sim_wrap"}
pad_level = hdl_toplevel in {"chip_top", "chip_top_sim_wrap", "sim_chip_top_gl_sensor_bridge_env"}
skip = {"dummy_top.sv", "soc_top.v"}
if pad_level:
skip.add("gf180mcu_fd_ip_sram__sram512x8m8wm1.v")
Expand All @@ -493,9 +494,12 @@ def chip_top_runner():
# Sim wrapper is only needed when it is the selected HDL toplevel.
if hdl_toplevel == "chip_top_sim_wrap":
sources.append(proj_path / "chip_top_sim_wrap.sv")
elif hdl_toplevel == "sim_chip_top_gl_sensor_bridge_env":
sources.append(proj_path / "sim/tb/sim_chip_top_gl_sensor_bridge_env.sv")

# Pad-level builds need GF180 IO models. Direct chip_core RTL DFT does not.
if pad_level:
defines["USE_POWER_PINS"] = True
sources += [
Path(pdk_root) / pdk / "libs.ref/gf180mcu_fd_io/verilog/gf180mcu_fd_io.v",
Path(pdk_root) / pdk / "libs.ref/gf180mcu_fd_io/verilog/gf180mcu_ws_io.v",
Expand Down
Loading
Loading