diff --git a/Makefile b/Makefile index a99abe2..685d447 100644 --- a/Makefile +++ b/Makefile @@ -2,17 +2,23 @@ MAKEFILE_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) RUN_TAG = $(shell ls librelane/runs/ | tail -n 1) TOP = chip_top +FINAL_DIR ?= $(if $(wildcard $(MAKEFILE_DIR)/final/pnl/$(TOP).pnl.v),$(MAKEFILE_DIR)/final,$(MAKEFILE_DIR)/librelane/runs/$(RUN_TAG)/final) PDK_ROOT ?= $(MAKEFILE_DIR)/gf180mcu PDK ?= gf180mcuD PDK_TAG ?= 1.8.0 VENV_DIR ?= $(MAKEFILE_DIR)/.venv VENV_PYTHON := $(VENV_DIR)/bin/python +COCOTB_PYENV_PYTHON := $(HOME)/.pyenv/versions/cocotb-env/bin/python ifeq ($(wildcard $(VENV_PYTHON)),$(VENV_PYTHON)) PYTHON ?= $(VENV_PYTHON) else +ifeq ($(wildcard $(COCOTB_PYENV_PYTHON)),$(COCOTB_PYENV_PYTHON)) +PYTHON ?= $(COCOTB_PYENV_PYTHON) +else PYTHON ?= python3 endif +endif AVAILABLE_SLOTS = 1x1 0p5x1 1x0p5 0p5x0p5 DEFAULT_SLOT = 1x1 @@ -119,9 +125,21 @@ build-riscv-toolchain: ## Build bare-metal RISC-V GCC from third_party/riscv-gnu .PHONY: build-riscv-toolchain sim-gl: ## Run gate-level simulation with cocotb (after copy-final) - cd cocotb; GL=1 PDK_ROOT=${PDK_ROOT} PDK=${PDK} SLOT=${SLOT} python3 chip_top_tb.py + cd cocotb; GL=1 FINAL_DIR=$(FINAL_DIR) PDK_ROOT=${PDK_ROOT} PDK=${PDK} SLOT=${SLOT} $(PYTHON) chip_top_tb.py .PHONY: sim-gl +sim-gl-debug-modes: ## Run gate-level chip_top debug-mode pad smoke test + cd cocotb; PYTHONPATH=$(MAKEFILE_DIR)/cocotb/sim/tb GL=1 FINAL_DIR=$(FINAL_DIR) PDK_ROOT=${PDK_ROOT} PDK=${PDK} SLOT=${SLOT} COCOTB_TEST_MODULE=test_chip_core_debug_modes COCOTB_TEST_FILTER=test_chip_top_debug_modes_force_inputs_reach_debug_bus $(PYTHON) chip_top_tb.py +.PHONY: sim-gl-debug-modes + +sim-gl-sensor-bridge: ## Run full gate-level sensor-model pad bridge feature test + cd cocotb; PYTHONPATH=$(MAKEFILE_DIR)/cocotb/sim/tb GL=1 CHIP_TOPLEVEL=sim_chip_top_gl_sensor_bridge_env 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 + +sim-gl-sensor-bridge-smoke: ## Run gate-level sensor bridge connectivity smoke test + cd cocotb; PYTHONPATH=$(MAKEFILE_DIR)/cocotb/sim/tb GL=1 CHIP_TOPLEVEL=sim_chip_top_gl_sensor_bridge_env 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_reaches_models $(PYTHON) chip_top_tb.py +.PHONY: sim-gl-sensor-bridge-smoke + sim-view: ## View simulation waveforms in GTKWave gtkwave cocotb/sim_build/chip_top.fst .PHONY: sim-view diff --git a/cocotb/chip_top_tb.py b/cocotb/chip_top_tb.py index 803cff7..33faaf6 100644 --- a/cocotb/chip_top_tb.py +++ b/cocotb/chip_top_tb.py @@ -296,6 +296,8 @@ async def test_chip_top_feature_inject(dut): def chip_top_runner(): proj_path = Path(__file__).resolve().parent + final_path = Path(final_dir).expanduser() if final_dir else proj_path / "../final" + gl_netlist = final_path / "pnl" / f"{netlist_toplevel}.pnl.v" sources = [] # Compile with SIM define so the sim bus and sensor models are active. @@ -307,6 +309,8 @@ def chip_top_runner(): sources.append(Path(pdk_root) / pdk / "libs.ref" / scl / "verilog" / "primitives.v") sources.append(proj_path / f"../final/pnl/{hdl_toplevel}.pnl.v") defines = {"FUNCTIONAL": True, "USE_POWER_PINS": True} + if hdl_toplevel == "sim_chip_top_gl_sensor_bridge_env": + defines["SENSOR_SIM_PAD_BRIDGE"] = True else: src_dir = proj_path / "../src" pad_level = hdl_toplevel in {"chip_top", "chip_top_sim_wrap"} @@ -366,6 +370,7 @@ def chip_top_runner(): runner.test( hdl_toplevel=hdl_toplevel, test_module=test_module, + test_filter=test_filter, plusargs=plusargs, waves=True, ) diff --git a/cocotb/sensors/i2c_slave_adpd144ri.sv b/cocotb/sensors/i2c_slave_adpd144ri.sv index 88cad5c..2e5bf71 100644 --- a/cocotb/sensors/i2c_slave_adpd144ri.sv +++ b/cocotb/sensors/i2c_slave_adpd144ri.sv @@ -58,9 +58,11 @@ module i2c_slave_adpd144ri #( RSP_WRITE = 3'd4 } rsp_state_t; - rsp_state_t rsp_state; - reg [1:0] byte_cnt; - reg [7:0] bytes_left; + rsp_state_t rsp_state; + reg [1:0] byte_cnt; + reg [7:0] bytes_left; + reg sim_req_q; + wire sim_req_rise = sim_req && !sim_req_q; initial begin if (!$value$plusargs("DATA_DIR=%s", data_dir)) @@ -94,20 +96,22 @@ module i2c_slave_adpd144ri #( sim_rdata <= 8'h00; sim_rvalid <= 1'b0; sim_rlast <= 1'b0; - sim_err <= 1'b0; - rsp_state <= RSP_IDLE; - byte_cnt <= 2'd0; - bytes_left <= 8'd0; - end else begin - sim_ack <= 1'b0; - sim_rvalid <= 1'b0; - sim_rlast <= 1'b0; + sim_err <= 1'b0; + rsp_state <= RSP_IDLE; + byte_cnt <= 2'd0; + bytes_left <= 8'd0; + sim_req_q <= 1'b0; + end else begin + sim_req_q <= sim_req; + sim_ack <= 1'b0; + sim_rvalid <= 1'b0; + sim_rlast <= 1'b0; sim_err <= 1'b0; - case (rsp_state) - RSP_IDLE: begin - if (sim_req && (sim_addr == I2C_ADDR)) begin - sim_ack <= 1'b1; + case (rsp_state) + RSP_IDLE: begin + if (sim_req_rise && (sim_addr == I2C_ADDR)) begin + sim_ack <= 1'b1; byte_cnt <= 2'd0; if (sim_write) begin rsp_state <= RSP_WRITE; diff --git a/cocotb/sensors/i2c_slave_lis2dw12.sv b/cocotb/sensors/i2c_slave_lis2dw12.sv index 2fb9830..f8f29d0 100644 --- a/cocotb/sensors/i2c_slave_lis2dw12.sv +++ b/cocotb/sensors/i2c_slave_lis2dw12.sv @@ -50,8 +50,10 @@ module i2c_slave_lis2dw12 #( RSP_DATA = 2'd2 } rsp_state_t; - rsp_state_t rsp_state; - reg [2:0] byte_cnt; + rsp_state_t rsp_state; + reg [2:0] byte_cnt; + reg sim_req_q; + wire sim_req_rise = sim_req && !sim_req_q; initial begin if (!$value$plusargs("DATA_DIR=%s", data_dir)) @@ -84,19 +86,21 @@ module i2c_slave_lis2dw12 #( sim_rdata <= 8'h00; sim_rvalid <= 1'b0; sim_rlast <= 1'b0; - sim_err <= 1'b0; - rsp_state <= RSP_IDLE; - byte_cnt <= '0; - end else begin - sim_ack <= 1'b0; - sim_rvalid <= 1'b0; - sim_rlast <= 1'b0; + sim_err <= 1'b0; + rsp_state <= RSP_IDLE; + byte_cnt <= '0; + sim_req_q <= 1'b0; + end else begin + sim_req_q <= sim_req; + sim_ack <= 1'b0; + sim_rvalid <= 1'b0; + sim_rlast <= 1'b0; sim_err <= 1'b0; - case (rsp_state) - RSP_IDLE: begin - if (sim_req && (sim_addr == I2C_ADDR)) begin - sim_ack <= 1'b1; + case (rsp_state) + RSP_IDLE: begin + if (sim_req_rise && (sim_addr == I2C_ADDR)) begin + sim_ack <= 1'b1; if (sim_reg == REG_STATUS) begin rsp_state <= RSP_STATUS; end else if (sim_reg == REG_DATA) begin diff --git a/cocotb/sim/tb/sim_chip_top_gl_sensor_bridge_env.sv b/cocotb/sim/tb/sim_chip_top_gl_sensor_bridge_env.sv new file mode 100644 index 0000000..2862d37 --- /dev/null +++ b/cocotb/sim/tb/sim_chip_top_gl_sensor_bridge_env.sv @@ -0,0 +1,165 @@ +`timescale 1ns/1ps + +module sim_chip_top_gl_sensor_bridge_env; + localparam [6:0] ACC_ADDR = 7'h19; + localparam [6:0] PPG_ADDR = 7'h64; + + logic clk_drv = 1'b0; + logic rst_n_drv = 1'b0; + logic [11:0] input_drv = 12'h000; + wire clk_PAD; + wire rst_n_PAD; + wire [11:0] input_PAD; + logic [39:0] bidir_drv = 40'h0; + logic [39:0] bidir_oe = 40'h0; + wire [39:0] bidir_PAD; + wire [39:0] bidir_sample; + wire [1:0] analog_PAD; + + wire VDD = 1'b1; + wire VSS = 1'b0; + + wire sensor_bridge_en = input_drv[11]; + + wire sim_req = bidir_PAD[8]; + wire sim_write = bidir_PAD[9]; + wire [6:0] sim_addr = bidir_PAD[16:10]; + wire [7:0] sim_reg = bidir_PAD[24:17]; + wire [7:0] sim_len = bidir_PAD[32:25]; + wire [7:0] sim_wdata = bidir_PAD[7:0]; + wire sensor_req = sensor_bridge_en ? sim_req : 1'b0; + wire sensor_write = sensor_bridge_en ? sim_write : 1'b0; + wire [6:0] sensor_addr = sensor_bridge_en ? sim_addr : 7'h00; + wire [7:0] sensor_reg = sensor_bridge_en ? sim_reg : 8'h00; + wire [7:0] sensor_len = sensor_bridge_en ? sim_len : 8'h00; + wire [7:0] sensor_wdata = sensor_bridge_en ? sim_wdata : 8'h00; + + wire accel_sim_ack; + wire [7:0] accel_sim_rdata; + wire accel_sim_rvalid; + wire accel_sim_rlast; + wire accel_sim_err; + + wire ppg_sim_ack; + wire [7:0] ppg_sim_rdata; + wire ppg_sim_rvalid; + wire ppg_sim_rlast; + wire ppg_sim_err; + + wire sensor_sim_ack = (sensor_addr == ACC_ADDR) ? accel_sim_ack : + (sensor_addr == PPG_ADDR) ? ppg_sim_ack : 1'b0; + wire [7:0] sensor_sim_rdata = (sensor_addr == ACC_ADDR) ? accel_sim_rdata : + (sensor_addr == PPG_ADDR) ? ppg_sim_rdata : 8'h00; + wire sensor_sim_rvalid = (sensor_addr == ACC_ADDR) ? accel_sim_rvalid : + (sensor_addr == PPG_ADDR) ? ppg_sim_rvalid : 1'b0; + wire sensor_sim_rlast = (sensor_addr == ACC_ADDR) ? accel_sim_rlast : + (sensor_addr == PPG_ADDR) ? ppg_sim_rlast : 1'b0; + wire sensor_sim_err = (sensor_addr == ACC_ADDR) ? accel_sim_err : + (sensor_addr == PPG_ADDR) ? ppg_sim_err : 1'b1; + + logic sensor_sim_ack_pad_q; + logic sensor_sim_rvalid_pad_q; + logic [7:0] sensor_sim_rdata_pad_q; + logic sensor_sim_rlast_pad_q; + logic sensor_sim_err_pad_q; + + // Sensor models update on the rising edge, like the GL netlist. Retiming the + // pad response on the falling edge makes the external pads stable before the + // chip samples them on the next rising edge. + always_ff @(negedge clk_PAD or negedge rst_n_PAD) begin + if (!rst_n_PAD) begin + sensor_sim_ack_pad_q <= 1'b0; + sensor_sim_rvalid_pad_q <= 1'b0; + sensor_sim_rdata_pad_q <= 8'h00; + sensor_sim_rlast_pad_q <= 1'b0; + sensor_sim_err_pad_q <= 1'b0; + end else if (!sensor_bridge_en) begin + sensor_sim_ack_pad_q <= 1'b0; + sensor_sim_rvalid_pad_q <= 1'b0; + sensor_sim_rdata_pad_q <= 8'h00; + sensor_sim_rlast_pad_q <= 1'b0; + sensor_sim_err_pad_q <= 1'b0; + end else begin + sensor_sim_ack_pad_q <= sensor_sim_ack; + sensor_sim_rvalid_pad_q <= sensor_sim_rvalid; + sensor_sim_rdata_pad_q <= sensor_sim_rdata; + sensor_sim_rlast_pad_q <= sensor_sim_rlast; + sensor_sim_err_pad_q <= sensor_sim_err; + end + end + + assign bidir_sample = bidir_PAD; + assign clk_PAD = clk_drv; + assign rst_n_PAD = rst_n_drv; + assign input_PAD = input_drv; + + genvar i; + generate + for (i = 0; i < 40; i = i + 1) begin : tb_bidir_drive + if (i < 8) begin : sensor_data_drive + assign bidir_PAD[i] = sensor_bridge_en ? (sensor_sim_rvalid_pad_q ? sensor_sim_rdata_pad_q[i] : 1'bz) : + (bidir_oe[i] ? bidir_drv[i] : 1'bz); + end else if (i == 33) begin : sensor_ack_drive + assign bidir_PAD[i] = sensor_bridge_en ? sensor_sim_ack_pad_q : + (bidir_oe[i] ? bidir_drv[i] : 1'bz); + end else if (i == 34) begin : sensor_rvalid_drive + assign bidir_PAD[i] = sensor_bridge_en ? sensor_sim_rvalid_pad_q : + (bidir_oe[i] ? bidir_drv[i] : 1'bz); + end else if (i == 35) begin : sensor_rlast_drive + assign bidir_PAD[i] = sensor_bridge_en ? sensor_sim_rlast_pad_q : + (bidir_oe[i] ? bidir_drv[i] : 1'bz); + end else if (i == 36) begin : sensor_err_drive + assign bidir_PAD[i] = sensor_bridge_en ? sensor_sim_err_pad_q : + (bidir_oe[i] ? bidir_drv[i] : 1'bz); + end else begin : external_drive + assign bidir_PAD[i] = bidir_oe[i] ? bidir_drv[i] : 1'bz; + end + end + endgenerate + + pullup host_sda_pullup (bidir_PAD[6]); + + i2c_slave_lis2dw12 #( + .I2C_ADDR(ACC_ADDR) + ) u_accel_slave ( + .clk(clk_PAD), + .resetn(rst_n_PAD), + .sim_req(sensor_req), + .sim_addr(sensor_addr), + .sim_reg(sensor_reg), + .sim_len(sensor_len), + .sim_ack(accel_sim_ack), + .sim_rdata(accel_sim_rdata), + .sim_rvalid(accel_sim_rvalid), + .sim_rlast(accel_sim_rlast), + .sim_err(accel_sim_err) + ); + + i2c_slave_adpd144ri #( + .I2C_ADDR(PPG_ADDR) + ) u_ppg_slave ( + .clk(clk_PAD), + .resetn(rst_n_PAD), + .sim_req(sensor_req), + .sim_addr(sensor_addr), + .sim_reg(sensor_reg), + .sim_len(sensor_len), + .sim_write(sensor_write), + .sim_wdata(sensor_wdata), + .sim_ack(ppg_sim_ack), + .sim_rdata(ppg_sim_rdata), + .sim_rvalid(ppg_sim_rvalid), + .sim_rlast(ppg_sim_rlast), + .sim_err(ppg_sim_err) + ); + + chip_top u_chip ( + .VDD(VDD), + .VSS(VSS), + .clk_PAD(clk_PAD), + .rst_n_PAD(rst_n_PAD), + .input_PAD(input_PAD), + .bidir_PAD(bidir_PAD), + .analog_PAD(analog_PAD) + ); +endmodule diff --git a/cocotb/sim/tb/test_chip_top_gl_sensor_bridge.py b/cocotb/sim/tb/test_chip_top_gl_sensor_bridge.py new file mode 100644 index 0000000..608ba15 --- /dev/null +++ b/cocotb/sim/tb/test_chip_top_gl_sensor_bridge.py @@ -0,0 +1,673 @@ +import logging +import os +from collections import defaultdict +from pathlib import Path + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ReadOnly, RisingEdge, Timer +from cocotb.utils import get_sim_time + +from ref_pipeline.pipeline_ref import PipelineReference, PipelineStepInputs, TopPipelineConfig +from ref_pipeline.trace_loader import load_accel_trace, load_ppg_trace +from ref_pipeline.utils import to_signed + + +PIN_SIM_REQ = 8 +PIN_SIM_ACK = 33 +PIN_SIM_RVALID = 34 +PIN_SIM_RLAST = 35 +PIN_FEATURE_SEEN = 33 +PIN_FEATURE_VALID = 34 +PIN_EPOCH_END = 35 +PIN_ML_GATE = 36 + +TEST_MODE_MSSD = 0x1 +TEST_MODE_DELTA_HR = 0x2 +TEST_MODE_TIME = 0x3 +TEST_MODE_MOTION = 0x4 +TEST_MODE_STATUS = 0x6 +DEBUG_BUS_LO = 7 +DEBUG_BUS_HI = 22 +SENSOR_STATUS_CLEAR = 1 << 10 +SENSOR_BRIDGE_EN = 1 << 11 + +ACC_ADDR = 0x19 +PPG_ADDR = 0x64 +ACC_REG_OUT_X_L = 0x28 +PPG_REG_FIFO_ACCESS = 0x60 + +CLK_HZ = 1000 +GT_EPOCH_HZ = 100 +GT_EPOCH_COUNT_MAX = 300 +SAMPLE_PERIOD_TICKS = 10 +PAD_MODE_SETTLE_NS = 20 +CLOCK_PERIOD_PS = 40_000 + +ROOT = Path(__file__).resolve().parents[3] +SIM_DATA = ROOT / "cocotb" / "sim" / "data" + + +def _pad_bit(dut, bit): + return str(dut.bidir_sample.value[bit]) + + +def _pad_slice_int(dut, high, low): + return dut.bidir_sample.value[high:low].to_unsigned() + + +def _pad_slice_resolved_int(dut, high, low): + value = dut.bidir_sample.value[high:low] + bits = str(value) + unknowns = sum(bit not in "01" for bit in bits) + resolved = "".join(bit if bit in "01" else "0" for bit in bits) + return int(resolved, 2), unknowns + + +def _input_value(mode=TEST_MODE_MSSD, *, bridge=True, clear=False): + value = mode & 0x1F + if bridge: + value |= SENSOR_BRIDGE_EN + if clear: + value |= SENSOR_STATUS_CLEAR + return value + + +def _u16_to_s16(value): + return to_signed(value, 16) + + +async def _startup(dut, post_reset_ns=400): + dut.input_drv.value = _input_value(TEST_MODE_MSSD, bridge=True) + dut.bidir_drv.value = 0 + dut.bidir_oe.value = 0 + dut.rst_n_drv.value = 0 + cocotb.start_soon(Clock(dut.clk_drv, 40, "ns").start()) + await Timer(400, "ns") + dut.rst_n_drv.value = 1 + if post_reset_ns: + await Timer(post_reset_ns, "ns") + + +async def _timer_step_ref(duration_ns, ref=None): + start_ps = int(get_sim_time("ps")) + await Timer(duration_ns, "ns") + if ref is not None: + end_ps = int(get_sim_time("ps")) + for _ in range((end_ps // CLOCK_PERIOD_PS) - (start_ps // CLOCK_PERIOD_PS)): + ref.step() + + +@cocotb.test() +async def test_chip_top_gl_sensor_bridge_reaches_models(dut): + """Check that the special GL netlist drives sensor sim transactions through pads.""" + + log = logging.getLogger("gl_sensor_bridge") + await _startup(dut) + + saw_req = False + saw_ack = False + saw_rvalid = False + seen_addrs = set() + + for _ in range(200_000): + await RisingEdge(dut.clk_drv) + + if _pad_bit(dut, PIN_SIM_REQ) == "1": + saw_req = True + seen_addrs.add(_pad_slice_int(dut, 16, 10)) + + saw_ack = saw_ack or (_pad_bit(dut, PIN_SIM_ACK) == "1") + saw_rvalid = saw_rvalid or (_pad_bit(dut, PIN_SIM_RVALID) == "1") + + if saw_req and saw_ack and saw_rvalid: + break + + log.info("sensor bridge addrs seen: %s", [f"0x{addr:02x}" for addr in sorted(seen_addrs)]) + + assert saw_req, "chip did not drive a sensor sim_req onto bidir[8]" + assert seen_addrs & {0x19, 0x64}, "sensor bridge did not expose accel/PPG target addresses" + assert saw_ack, "sensor models did not return sim_ack through bidir[33]" + assert saw_rvalid, "sensor models did not return sim_rvalid through bidir[34]" + + +class PadPipelineReference: + """Reference model fed only from pad-visible bridge traffic and cycle count.""" + + def __init__(self): + self.ref = PipelineReference(config=TopPipelineConfig()) + self.cycle = 0 + self.prev_epoch_end = False + self.pending = defaultdict(list) + self.prev_motion = None + self.prev_beat = None + self.prev_mssd = None + self.prev_quality = None + self.last_feature = None + self.feature_count = 0 + self.ppg_last_sample_time = 0 + self.ppg_last_observed_sample_time = 0 + self.last_rr_cycle = None + self.last_rr_interval = 0 + self.last_delta_hr = 0 + self.rr_count = 0 + self.motion_epoch_history = [] + + def schedule_accel(self, due_cycle, ax, ay, az): + self.pending[due_cycle].append(("accel", ax, ay, az)) + + def schedule_ppg(self, due_cycle, sample, sample_time): + self.pending[due_cycle].append(("ppg", sample, sample_time)) + + def step(self): + self.cycle += 1 + events = self.pending.pop(self.cycle, []) + + accel_valid = False + ax = ay = az = 0 + ppg_valid = False + ppg_sample = 0 + ppg_sample_time = 0 + + for event in events: + if event[0] == "accel": + _, ax, ay, az = event + accel_valid = True + elif event[0] == "ppg": + _, ppg_sample, ppg_sample_time = event + ppg_valid = True + + cycles_per_epoch = CLK_HZ // GT_EPOCH_HZ + epoch_end = (self.cycle % (cycles_per_epoch * GT_EPOCH_COUNT_MAX)) == 0 + seconds = self.cycle // CLK_HZ + + inputs = PipelineStepInputs( + rst=False, + accel_valid=accel_valid, + ax=ax, + ay=ay, + az=az, + ppg_valid=ppg_valid, + ppg_sample=ppg_sample, + ppg_sample_time=ppg_sample_time, + beat_pulse=bool(self.prev_beat and self.prev_beat.beat_pulse), + beat_quality=self.prev_beat.beat_quality if self.prev_beat else 0, + double_beat=bool(self.prev_beat and self.prev_beat.double_beat), + missed_beat=bool(self.prev_beat and self.prev_beat.missed_beat), + rr_valid=bool(self.prev_beat and self.prev_beat.rr_valid), + rr_accepted=bool(self.prev_beat and self.prev_beat.rr_accepted), + rr_interval=self.prev_beat.rr_interval if self.prev_beat else 0, + delta_hr=self.prev_beat.delta_hr_bpm if self.prev_beat else 0, + motion_epoch=bool(self.prev_motion and self.prev_motion.epoch_done), + motion_energy=self.prev_motion.motion_energy_epoch if self.prev_motion else 0, + mssd_valid=bool(self.prev_mssd and self.prev_mssd.mssd_valid), + mssd_epoch=self.prev_mssd.mssd_epoch if self.prev_mssd else 0, + fifo_overflow_event=False, + ppg_i2c_err_event=False, + epoch_end=epoch_end, + epoch_end_d=self.prev_epoch_end, + time_value=seconds, + ml_update_gate=bool(self.prev_quality and self.prev_quality.ml_update_gate), + ) + + outputs = self.ref.step(inputs) + self.prev_motion = outputs.motion + self.prev_beat = outputs.beat + self.prev_mssd = outputs.mssd + self.prev_quality = outputs.quality + self.prev_epoch_end = epoch_end + + if outputs.beat.rr_valid: + self.last_rr_cycle = self.cycle + self.last_rr_interval = outputs.beat.rr_interval + self.last_delta_hr = outputs.beat.delta_hr_bpm + self.rr_count += 1 + + if outputs.motion.epoch_done: + self.motion_epoch_history.append((self.cycle, outputs.motion.motion_energy_epoch & 0xFFFF)) + + if outputs.feature.feat_valid: + self.last_feature = outputs.feature + self.feature_count += 1 + + +class BridgeMonitor: + def __init__(self, dut, ref): + self.dut = dut + self.ref = ref + self.current = None + self.accel_index = 0 + self.ppg_index = 0 + self.accel_alignments = 0 + self.ppg_alignments = 0 + self.accel_trace = load_accel_trace(SIM_DATA / "accel_digital.csv") + self.ppg_trace = load_ppg_trace(SIM_DATA / "ppg_digital.csv") + self.ppg_fifo_bytes_avail = 0 + self.ppg_fifo_thresh_words = 0 + self.ppg_next_time = None + self.accel_events = [] + + @staticmethod + def _find_trace_match(trace, start, observed, getter, window=32): + for index in range(start, min(start + window, len(trace))): + if getter(trace[index]) == observed: + return index + return None + + def safe_to_pause(self): + return self.current is None and _pad_bit(self.dut, PIN_SIM_REQ) != "1" and _pad_bit(self.dut, PIN_SIM_RVALID) != "1" + + def observe(self): + if self.current is None and _pad_bit(self.dut, PIN_SIM_REQ) == "1": + self.current = { + "addr": _pad_slice_int(self.dut, 16, 10), + "reg": _pad_slice_int(self.dut, 24, 17), + "len": _pad_slice_int(self.dut, 32, 25), + "write": _pad_slice_int(self.dut, 9, 9), + "bytes": [], + } + + if self.current and self.current["write"]: + if _pad_bit(self.dut, PIN_SIM_ACK) == "1": + self.current = None + return + + if not self.current or _pad_bit(self.dut, PIN_SIM_RVALID) != "1": + return + + byte = _pad_slice_int(self.dut, 7, 0) + self.current["bytes"].append(byte) + + is_ppg_read = self.current["addr"] == PPG_ADDR and not self.current["write"] + + if is_ppg_read and self.current["reg"] == PPG_REG_FIFO_ACCESS and len(self.current["bytes"]) % 2 == 0: + lo = self.current["bytes"][-2] + hi = self.current["bytes"][-1] + sample = lo | (hi << 8) + if self.ppg_index >= len(self.ppg_trace): + raise AssertionError("PPG trace exhausted while monitoring GL bridge") + if self.ppg_next_time is None: + samples_in_burst = max(1, self.current["len"] // 2) + backfill = (samples_in_burst - 1) * SAMPLE_PERIOD_TICKS + start_time = max(0, self.ref.cycle - backfill) + if self.ref.ppg_last_sample_time: + start_time = max(start_time, self.ref.ppg_last_sample_time + SAMPLE_PERIOD_TICKS) + self.ppg_next_time = start_time + expected = self.ppg_trace[self.ppg_index].value + if sample != expected: + matched_index = self._find_trace_match( + self.ppg_trace, self.ppg_index + 1, sample, lambda entry: entry.value + ) + assert matched_index is not None, ( + f"PPG sample[{self.ppg_index}] bridge data mismatch: got={sample} exp={expected}" + ) + if self.ppg_alignments < 3: + self.dut._log.info( + "aligning PPG trace index from %d to %d after prior sensor-model reads", + self.ppg_index, + matched_index, + ) + self.ppg_alignments += 1 + self.ppg_index = matched_index + self.ppg_index += 1 + sample_time = self.ppg_next_time + self.ppg_next_time += SAMPLE_PERIOD_TICKS + self.ref.ppg_last_sample_time = sample_time + self.ref.ppg_last_observed_sample_time = sample_time + self.ref.schedule_ppg(self.ref.cycle + 2, sample, sample_time) + + if _pad_bit(self.dut, PIN_SIM_RLAST) != "1": + return + + if is_ppg_read and self.current["reg"] == 0x00 and len(self.current["bytes"]) >= 2: + status = self.current["bytes"][0] | (self.current["bytes"][1] << 8) + self.ppg_fifo_bytes_avail = (status >> 8) & 0xFF + + if is_ppg_read and self.current["reg"] == 0x06 and len(self.current["bytes"]) >= 2: + threshold = self.current["bytes"][0] | (self.current["bytes"][1] << 8) + self.ppg_fifo_thresh_words = (threshold >> 8) & 0x3F + max_burst_bytes = 32 * 2 + read_bytes_pre = min(self.ppg_fifo_bytes_avail, max_burst_bytes) + read_bytes = (read_bytes_pre // 2) * 2 + read_samples = read_bytes // 2 + threshold_words = self.ppg_fifo_thresh_words or 8 + threshold_bytes = threshold_words * 2 + should_read = self.ppg_fifo_bytes_avail >= threshold_bytes and read_bytes >= 2 + if should_read: + backfill = max(0, read_samples - 1) * SAMPLE_PERIOD_TICKS + decide_cycle = self.ref.cycle + 1 + start_time = max(0, decide_cycle - backfill) + if self.ref.ppg_last_sample_time: + start_time = max(start_time, self.ref.ppg_last_sample_time + SAMPLE_PERIOD_TICKS) + self.ppg_next_time = start_time + + if ( + self.current["addr"] == ACC_ADDR + and self.current["reg"] == ACC_REG_OUT_X_L + and not self.current["write"] + ): + data = self.current["bytes"] + if len(data) != 6: + raise AssertionError(f"accelerometer read returned {len(data)} bytes, expected 6") + ax = _u16_to_s16(data[0] | (data[1] << 8)) + ay = _u16_to_s16(data[2] | (data[3] << 8)) + az = _u16_to_s16(data[4] | (data[5] << 8)) + if self.accel_index >= len(self.accel_trace): + raise AssertionError("accelerometer trace exhausted while monitoring GL bridge") + expected = self.accel_trace[self.accel_index] + got_triplet = (ax, ay, az) + exp_triplet = (expected.ax, expected.ay, expected.az) + if got_triplet != exp_triplet: + matched_index = self._find_trace_match( + self.accel_trace, + self.accel_index + 1, + got_triplet, + lambda entry: (entry.ax, entry.ay, entry.az), + ) + assert matched_index is not None, ( + f"accelerometer sample[{self.accel_index}] bridge data mismatch: got={got_triplet} exp={exp_triplet}" + ) + if self.accel_alignments < 3: + self.dut._log.info( + "aligning accelerometer trace index from %d to %d after prior sensor-model reads", + self.accel_index, + matched_index, + ) + self.accel_alignments += 1 + self.accel_index = matched_index + self.accel_index += 1 + mag = abs(ax) + abs(ay) + abs(az) + self.accel_events.append((self.ref.cycle, self.ref.cycle + 1, self.accel_index - 1, ax, ay, az, mag)) + self.ref.schedule_accel(self.ref.cycle + 1, ax, ay, az) + + self.current = None + + +async def _read_debug_bus(dut, mode, ref=None): + dut.input_drv.value = _input_value(mode, bridge=False) + await _timer_step_ref(PAD_MODE_SETTLE_NS, ref) + value = dut.bidir_sample.value[DEBUG_BUS_HI:DEBUG_BUS_LO] + try: + return value.to_unsigned() + except ValueError as exc: + extra = f"; gl_motion={_gl_motion_debug(dut)}" if mode == TEST_MODE_MOTION else "" + raise AssertionError(f"debug mode 0x{mode:x} has unknown pad bits: {value}{extra}") from exc + + +def _logic_to_int(value): + bits = str(value) + if any(bit not in "01" for bit in bits): + return None + return int(bits, 2) + + +def _to_signed(value, width): + if value is None: + return None + sign = 1 << (width - 1) + mask = (1 << width) - 1 + value &= mask + return value - (1 << width) if value & sign else value + + +def _gl_signed_vector(dut, escaped_base, width): + return _to_signed(_gl_vector(dut, escaped_base, width), width) + + +def _gl_scalar(dut, escaped_name): + try: + return _logic_to_int(dut.u_chip._id(f"\\{escaped_name} ", extended=False).value) + except (AttributeError, KeyError, ValueError): + return None + + +def _gl_vector(dut, escaped_base, width): + value = 0 + for bit in range(width): + bit_value = _gl_scalar(dut, f"{escaped_base}[{bit}]") + if bit_value is None: + return None + value |= bit_value << bit + return value + + +def _gl_delta_debug(dut): + return { + "rr_valid_w": _gl_scalar(dut, "i_chip_core.u_top.rr_valid_w"), + "rr_interval_w": _gl_vector(dut, "i_chip_core.u_top.rr_interval_w", 16), + "delta_hr_w": _to_signed(_gl_vector(dut, "i_chip_core.u_top.delta_hr_w", 16), 16), + "delta_hr_feat_top_w": _to_signed(_gl_vector(dut, "i_chip_core.delta_hr_feat_top_w", 16), 16), + "have_prev_hr_r": _gl_scalar(dut, "i_chip_core.u_top.u_beat_detect.have_prev_hr_r"), + "prev_hr_bpm_r": _gl_vector(dut, "i_chip_core.u_top.u_beat_detect.prev_hr_bpm_r", 16), + "ppg_sample_time_w": _gl_vector(dut, "i_chip_core.u_top.ppg_sample_time_w", 32), + } + + +def _gl_motion_debug(dut): + return { + "motion_epoch_w": _gl_scalar(dut, "i_chip_core.u_top.motion_epoch_w"), + "motion_energy_w": _gl_vector(dut, "i_chip_core.u_top.motion_energy_w", 16), + "motion_feat_top_w": _gl_vector(dut, "i_chip_core.motion_feat_top_w", 16), + "motion_accum_lo": _gl_vector(dut, "i_chip_core.u_top.u_motion_process.motion_energy_accum_r", 16), + "epoch_end_w": _gl_scalar(dut, "i_chip_core.u_top.epoch_end_w"), + "epoch_end_d": _gl_scalar(dut, "i_chip_core.u_top.epoch_end_d"), + "accel_valid_w": _gl_scalar(dut, "i_chip_core.u_top.accel_valid_w"), + } + + +def _recent_accel_debug(ref): + monitor = getattr(ref, "monitor", None) + if monitor is None: + return "" + recent = monitor.accel_events[-8:] + motion_epochs = getattr(ref, "motion_epoch_history", [])[-4:] + epoch_sums = [] + for start, stop in ((0, 3000), (3000, 6000), (6000, 9000)): + total = sum(event[6] for event in monitor.accel_events if start < event[1] <= stop) + count = sum(1 for event in monitor.accel_events if start < event[1] <= stop) + epoch_sums.append((start, stop, count, total & 0xFFFF)) + return ( + " motion_epochs=" + repr(motion_epochs) + + " epoch_sums=" + repr(epoch_sums) + + " recent_accel=" + repr(recent) + ) + + +async def _clear_feature_seen(dut, ref=None): + dut.input_drv.value = _input_value(TEST_MODE_MSSD, bridge=False, clear=True) + await RisingEdge(dut.clk_drv) + await ReadOnly() + if ref is not None: + ref.step() + await Timer(1, "ps") + dut.input_drv.value = _input_value(TEST_MODE_MSSD, bridge=False, clear=False) + await _timer_step_ref(PAD_MODE_SETTLE_NS, ref) + + +async def _check_latest_feature_on_debug_pads(dut, expected, ref=None): + expected_by_mode = { + TEST_MODE_MSSD: expected.mssd_feat, + TEST_MODE_DELTA_HR: expected.delta_hr_feat, + TEST_MODE_TIME: expected.time_feat, + TEST_MODE_MOTION: expected.motion_feat, + } + + for mode, expected_value in expected_by_mode.items(): + got = await _read_debug_bus(dut, mode, ref) + assert got == (expected_value & 0xFFFF), ( + f"debug mode 0x{mode:x} mismatch: got=0x{got:04x} expected=0x{expected_value & 0xFFFF:04x}" + + ( + f"; ref_motion={expected.motion_feat & 0xFFFF:04x} gl_motion={_gl_motion_debug(dut)}" + f"{_recent_accel_debug(ref)}" + if ref is not None and mode == TEST_MODE_MOTION + else "" + ) + + ( + f"; ref_rr_count={ref.rr_count} ref_last_rr_cycle={ref.last_rr_cycle} " + f"ref_last_rr_interval={ref.last_rr_interval} ref_last_delta_hr={ref.last_delta_hr} " + f"ref_last_ppg_time={ref.ppg_last_observed_sample_time} " + f"gl_delta={_gl_delta_debug(dut)}" + if ref is not None and mode == TEST_MODE_DELTA_HR + else "" + ) + ) + + +def _maybe_internal_debug(dut): + signals = {} + try: + chip = dut.u_chip + except AttributeError: + return signals + + for name in ( + "sensor_feature_valid_seen_q", + "feat_valid_w", + "epoch_end_w", + "ml_update_gate_w", + "invalid_reason_w", + "feat_en", + "test_mode_w", + ): + try: + handle = chip._id(f"\\i_chip_core.{name} ", extended=False) + signals[name] = str(handle.value) + except (AttributeError, KeyError, ValueError): + try: + signals[name] = str(getattr(chip, name).value) + except AttributeError: + pass + return signals + + +@cocotb.test() +async def test_chip_top_gl_sensor_bridge_debug_features_match_python_reference(dut): + """Run raw sensor data through GL and compare pad debug features to Python refs.""" + + log = logging.getLogger("gl_sensor_bridge_features") + await _startup(dut, post_reset_ns=0) + + ref = PadPipelineReference() + monitor = BridgeMonitor(dut, ref) + ref.monitor = monitor + + checked_features = 0 + target_features = int(os.getenv("GL_SENSOR_BRIDGE_FEATURES", "4")) + max_cycles = int(os.getenv("GL_SENSOR_BRIDGE_MAX_CYCLES", "30000")) + warmup_features = int(os.getenv("GL_SENSOR_BRIDGE_WARMUP_FEATURES", "1")) + debug_internals = os.getenv("GL_SENSOR_BRIDGE_DEBUG_INTERNALS") == "1" + status_polls = 0 + feature_seen_polls = 0 + feature_valid_polls = 0 + epoch_seen_polls = 0 + ml_gate_seen_polls = 0 + status_debug_epoch_polls = 0 + status_debug_ml_gate_polls = 0 + status_debug_invalid_or = 0 + status_debug_unknown_polls = 0 + last_feature_poll_cycle = -1_000_000 + next_debug_poll_cycle = 1500 + consumed_ref_features = 0 + + for _ in range(max_cycles): + await RisingEdge(dut.clk_drv) + await ReadOnly() + + ref.step() + monitor.observe() + + feature_poll_due = ( + ref.feature_count > consumed_ref_features and (_ - last_feature_poll_cycle) >= 100 + ) + debug_poll_due = debug_internals and _ >= next_debug_poll_cycle + if not (feature_poll_due or debug_poll_due): + continue + + if not monitor.safe_to_pause(): + continue + + await Timer(1, "ps") + dut.input_drv.value = _input_value(TEST_MODE_MSSD, bridge=False) + await _timer_step_ref(PAD_MODE_SETTLE_NS, ref) + status_polls += 1 + feature_seen = _pad_bit(dut, PIN_FEATURE_SEEN) == "1" + feature_valid = _pad_bit(dut, PIN_FEATURE_VALID) == "1" + epoch_seen = _pad_bit(dut, PIN_EPOCH_END) == "1" + ml_gate_seen = _pad_bit(dut, PIN_ML_GATE) == "1" + feature_seen_polls += int(feature_seen) + feature_valid_polls += int(feature_valid) + epoch_seen_polls += int(epoch_seen) + ml_gate_seen_polls += int(ml_gate_seen) + dut.input_drv.value = _input_value(TEST_MODE_STATUS, bridge=False) + await _timer_step_ref(PAD_MODE_SETTLE_NS, ref) + status_debug, status_unknowns = _pad_slice_resolved_int(dut, DEBUG_BUS_HI, DEBUG_BUS_LO) + status_debug_unknown_polls += int(status_unknowns != 0) + status_debug_ml_gate_polls += int(bool(status_debug & (1 << 15))) + status_debug_epoch_polls += int(bool(status_debug & (1 << 14))) + status_debug_invalid_or |= (status_debug >> 6) & 0xFF + + if feature_poll_due: + last_feature_poll_cycle = _ + if debug_poll_due: + next_debug_poll_cycle += 1500 + + if debug_internals and (debug_poll_due or feature_seen or ml_gate_seen): + dut._log.info( + "cycle=%d status pads: seen=%d valid=%d epoch=%d ml_gate=%d status_debug=0x%04x status_unknowns=%d internals=%s", + _, + feature_seen, + feature_valid, + epoch_seen, + ml_gate_seen, + status_debug, + status_unknowns, + _maybe_internal_debug(dut), + ) + + if not feature_seen: + dut.input_drv.value = _input_value(TEST_MODE_MSSD, bridge=True) + continue + + assert ref.last_feature is not None, "GL reported feature_valid before Python reference did" + if warmup_features > 0: + warmup_features -= 1 + consumed_ref_features += 1 + log.info("skipping warm-up feature vector before strict debug-pad comparison") + await _clear_feature_seen(dut, ref) + dut.input_drv.value = _input_value(TEST_MODE_MSSD, bridge=True) + continue + + await _check_latest_feature_on_debug_pads(dut, ref.last_feature, ref) + checked_features += 1 + consumed_ref_features += 1 + log.info( + "feature[%d] matched: time=%d motion=%d delta_hr=%d mssd=%d", + checked_features, + ref.last_feature.time_feat, + ref.last_feature.motion_feat, + ref.last_feature.delta_hr_feat, + ref.last_feature.mssd_feat, + ) + + await _clear_feature_seen(dut, ref) + dut.input_drv.value = _input_value(TEST_MODE_MSSD, bridge=True) + + if checked_features >= target_features: + break + else: + raise AssertionError( + f"timed out after {max_cycles} cycles; checked {checked_features}/{target_features} feature vectors; " + f"reference_features={ref.feature_count}, accel_samples={monitor.accel_index}, " + f"ppg_samples={monitor.ppg_index}, status_polls={status_polls}, " + f"feature_seen_polls={feature_seen_polls}, feature_valid_polls={feature_valid_polls}, " + f"epoch_seen_polls={epoch_seen_polls}, ml_gate_seen_polls={ml_gate_seen_polls}, " + f"status_debug_epoch_polls={status_debug_epoch_polls}, " + f"status_debug_ml_gate_polls={status_debug_ml_gate_polls}, " + f"status_debug_invalid_or=0x{status_debug_invalid_or:02x}, " + f"status_debug_unknown_polls={status_debug_unknown_polls}, " + f"accel_alignments={monitor.accel_alignments}, ppg_alignments={monitor.ppg_alignments}." + ) + + assert monitor.accel_index > 0, "no accelerometer samples were observed through the bridge" + assert monitor.ppg_index > 0, "no PPG samples were observed through the bridge" + assert ref.feature_count >= checked_features, "reference did not produce enough feature vectors" diff --git a/src/chip_core.sv b/src/chip_core.sv index e38af11..d49a864 100644 --- a/src/chip_core.sv +++ b/src/chip_core.sv @@ -3,6 +3,8 @@ // Current pinout: // input_in[4:0] : test mode selector + // input_in[10] : clear sticky feature-valid status when SENSOR_SIM_PAD_BRIDGE is defined + // input_in[11] : sensor sim-bus pad bridge enable when SENSOR_SIM_PAD_BRIDGE is defined // bidir[0] : alarm output // bidir[1] : SPI flash clock output // bidir[2] : SPI flash MOSI output @@ -17,6 +19,24 @@ // bidir[38] : force wake source input, only used by test modes = 01011 / 11011 // bidir[39] : external test clock, used by the 1xxxx test-mode bank // + // SENSOR_SIM_PAD_BRIDGE mode, enabled by input_in[11], repurposes bidir pads: + // bidir[7:0] : sim_wdata from chip on writes, sim_rdata from TB on reads + // bidir[8] : sim_req output from chip + // bidir[9] : sim_write output from chip + // bidir[16:10] : sim_addr output from chip + // bidir[24:17] : sim_reg output from chip + // bidir[32:25] : sim_len output from chip + // bidir[33] : sim_ack input to chip + // bidir[34] : sim_rvalid input to chip + // bidir[35] : sim_rlast input to chip + // bidir[36] : sim_err input to chip + // + // When SENSOR_SIM_PAD_BRIDGE is defined and input_in[11] is low: + // bidir[33] : sticky feature-valid-seen status, cleared by input_in[10] + // bidir[34] : live feat_valid pulse + // bidir[35] : live epoch_end pulse + // bidir[36] : live ml_update_gate status + // // Test mode map: // 00000 : normal mode, PLL clock, debug bus disabled // 00001 : MSSD feature @@ -39,6 +59,13 @@ `default_nettype none +`ifdef SIM +`define CHIP_CORE_HAS_SENSOR_SIM_BUS +`endif +`ifdef SENSOR_SIM_PAD_BRIDGE +`define CHIP_CORE_HAS_SENSOR_SIM_BUS +`endif + module chip_core #( parameter NUM_INPUT_PADS = 12, parameter NUM_BIDIR_PADS = 40, @@ -49,21 +76,61 @@ module chip_core #( parameter CLK_HZ = 10_000_000, parameter GT_CLK_HZ = 10_000_000, parameter GT_EPOCH_HZ = 100, - parameter GT_EPOCH_COUNT_MAX = 1000, - parameter ACC_POLL_PERIOD_TICKS = 50_000, - parameter PPG_POLL_PERIOD_TICKS = 100, + parameter GT_EPOCH_COUNT_MAX = + `ifdef SENSOR_SIM_PAD_BRIDGE + 300, + `else + 1000, + `endif + parameter ACC_POLL_PERIOD_TICKS = + `ifdef SENSOR_SIM_PAD_BRIDGE + 8, + `else + 50_000, + `endif + parameter PPG_POLL_PERIOD_TICKS = + `ifdef SENSOR_SIM_PAD_BRIDGE + 2, + `else + 100, + `endif parameter PPG_WATERMARK = 8, parameter PPG_MAX_BURST_SAMPLES = 32, parameter CFG_REFRACT_MS = 32'd250, parameter CFG_RR_MIN_MS = 32'd300, parameter CFG_RR_MAX_MS = 32'd2000, - parameter CFG_Q_MIN_ACCEPT = 8'd10, - parameter CFG_BEAT_Q_MIN = 8'd16, - parameter CFG_MIN_VALID_FRAC = 8'd96, + parameter CFG_Q_MIN_ACCEPT = + `ifdef SENSOR_SIM_PAD_BRIDGE + 8'd0, + `else + 8'd10, + `endif + parameter CFG_BEAT_Q_MIN = + `ifdef SENSOR_SIM_PAD_BRIDGE + 8'd0, + `else + 8'd16, + `endif + parameter CFG_MIN_VALID_FRAC = + `ifdef SENSOR_SIM_PAD_BRIDGE + 8'd0, + `else + 8'd96, + `endif parameter CFG_MAX_DOUBLE = 8'd4, parameter CFG_MAX_MISSED = 8'd3, - parameter CFG_MOTION_HI_TH = 16'd2000, - parameter CFG_MAX_MOTION_HI = 16'd3, + parameter CFG_MOTION_HI_TH = + `ifdef SENSOR_SIM_PAD_BRIDGE + 16'hFFFF, + `else + 16'd2000, + `endif + parameter CFG_MAX_MOTION_HI = + `ifdef SENSOR_SIM_PAD_BRIDGE + 16'hFFFF, + `else + 16'd3, + `endif parameter MSSD_MIN_RR_COUNT = 1 )( `ifdef USE_POWER_PINS @@ -118,6 +185,26 @@ module chip_core #( localparam int TEST_FORCE_IRQ_PAD = 37; localparam int TEST_FORCE_WAKE_PAD = 38; localparam int TEST_CLK_PAD = 39; + localparam int SENSOR_STATUS_CLEAR_PAD = 10; + localparam int SENSOR_BRIDGE_EN_PAD = 11; + localparam int SENSOR_DATA_LO = 0; + localparam int SENSOR_DATA_HI = 7; + localparam int SENSOR_REQ_PAD = 8; + localparam int SENSOR_WRITE_PAD = 9; + localparam int SENSOR_ADDR_LO = 10; + localparam int SENSOR_ADDR_HI = 16; + localparam int SENSOR_REG_LO = 17; + localparam int SENSOR_REG_HI = 24; + localparam int SENSOR_LEN_LO = 25; + localparam int SENSOR_LEN_HI = 32; + localparam int SENSOR_ACK_PAD = 33; + localparam int SENSOR_RVALID_PAD = 34; + localparam int SENSOR_RLAST_PAD = 35; + localparam int SENSOR_ERR_PAD = 36; + localparam int SENSOR_STATUS_FEAT_SEEN_PAD = 33; + localparam int SENSOR_STATUS_FEAT_VALID_PAD = 34; + localparam int SENSOR_STATUS_EPOCH_END_PAD = 35; + localparam int SENSOR_STATUS_ML_GATE_PAD = 36; logic [4:0] test_mode_w; logic core_clk_w; @@ -207,6 +294,18 @@ module chip_core #( assign test_mode_w = input_in[4:0]; assign test_force_irq_w = bidir_in[TEST_FORCE_IRQ_PAD]; assign test_force_wake_w = bidir_in[TEST_FORCE_WAKE_PAD]; + `ifdef SENSOR_SIM_PAD_BRIDGE + assign sensor_bridge_en_w = input_in[SENSOR_BRIDGE_EN_PAD]; + assign sensor_status_clear_w = input_in[SENSOR_STATUS_CLEAR_PAD]; + `else + assign sensor_bridge_en_w = 1'b0; + assign sensor_status_clear_w = 1'b0; + `endif + assign sensor_sim_ack_w = sensor_bridge_en_w ? bidir_in[SENSOR_ACK_PAD] : sim_ack_i; + assign sensor_sim_rdata_w = sensor_bridge_en_w ? bidir_in[SENSOR_DATA_HI:SENSOR_DATA_LO] : sim_rdata_i; + assign sensor_sim_rvalid_w = sensor_bridge_en_w ? bidir_in[SENSOR_RVALID_PAD] : sim_rvalid_i; + assign sensor_sim_rlast_w = sensor_bridge_en_w ? bidir_in[SENSOR_RLAST_PAD] : sim_rlast_i; + assign sensor_sim_err_w = sensor_bridge_en_w ? bidir_in[SENSOR_ERR_PAD] : sim_err_i; // Upper-half test modes run from the external test clock. assign core_clk_w = test_mode_w[4] ? bidir_in[TEST_CLK_PAD] : clk; @@ -216,6 +315,20 @@ module chip_core #( assign delta_hr_feat_w = (DEBUG_STIM_EN && debug_stim_override_en_i) ? $signed(debug_stim_delta_hr_i) : delta_hr_feat_top_w; assign mssd_feat_w = (DEBUG_STIM_EN && debug_stim_override_en_i) ? $signed(debug_stim_mssd_i) : mssd_feat_top_w; + `ifdef SENSOR_SIM_PAD_BRIDGE + always_ff @(posedge core_clk_w or negedge rst_n) begin + if (!rst_n) begin + sensor_feature_valid_seen_q <= 1'b0; + end else if (sensor_status_clear_w) begin + sensor_feature_valid_seen_q <= 1'b0; + end else if (feat_valid_w) begin + sensor_feature_valid_seen_q <= 1'b1; + end + end + `else + assign sensor_feature_valid_seen_q = 1'b0; + `endif + always_comb begin debug_bus_w = '0; unique case (test_mode_w) @@ -472,11 +585,11 @@ module chip_core #( .sim_len_o (sim_len_w), .sim_write_o (sim_write_w), .sim_wdata_o (sim_wdata_w), - .sim_ack_i (sim_ack_i), - .sim_rdata_i (sim_rdata_i), - .sim_rvalid_i (sim_rvalid_i), - .sim_rlast_i (sim_rlast_i), - .sim_err_i (sim_err_i), + .sim_ack_i (sensor_sim_ack_w), + .sim_rdata_i (sensor_sim_rdata_w), + .sim_rvalid_i (sensor_sim_rvalid_w), + .sim_rlast_i (sensor_sim_rlast_w), + .sim_err_i (sensor_sim_err_w), `endif .feat_valid_o (feat_valid_w), .time_feat_o (time_feat_top_w), diff --git a/src/top.sv b/src/top.sv index a6324f5..6ab4169 100644 --- a/src/top.sv +++ b/src/top.sv @@ -107,26 +107,26 @@ module top #( output logic ml_update_gate_o, // gate: only update ML when signal-quality checks pass output logic [7:0] invalid_reason_o, // reason code when ML update is gated off - // Shared flash SPI bus: spi_boot_ctrl owns before boot_done, weight_flash_axi owns after - output logic flash_spi_clk_o, - output logic flash_spi_mosi_o, - input logic flash_spi_miso_i, - output logic flash_spi_cs_n_o, - - // Compatibility/debug views of the two internal flash owners. - output logic boot_spi_clk_o, - output logic boot_spi_mosi_o, - input logic boot_spi_miso_i, - output logic boot_spi_cs_n_o, - output logic weight_spi_clk_o, - output logic weight_spi_mosi_o, - input logic weight_spi_miso_i, - output logic weight_spi_cs_n_o, - - // CPU SPI master (spi_master_mmio) kept idle in this configuration - output logic spi_clk_o, - output logic spi_mosi_o, - input logic spi_miso_i, + // Shared flash SPI bus: spi_boot_ctrl owns before boot_done, weight_flash_axi owns after + output logic flash_spi_clk_o, + output logic flash_spi_mosi_o, + input logic flash_spi_miso_i, + output logic flash_spi_cs_n_o, + + // Compatibility/debug views of the two internal flash owners. + output logic boot_spi_clk_o, + output logic boot_spi_mosi_o, + input logic boot_spi_miso_i, + output logic boot_spi_cs_n_o, + output logic weight_spi_clk_o, + output logic weight_spi_mosi_o, + input logic weight_spi_miso_i, + output logic weight_spi_cs_n_o, + + // CPU SPI master (spi_master_mmio) kept idle in this configuration + output logic spi_clk_o, + output logic spi_mosi_o, + input logic spi_miso_i, output logic spi_cs_n_o, @@ -437,22 +437,22 @@ module top #( wire init_done; wire boot_spi_mosi_w; wire weight_spi_mosi_w; - wire boot_spi_clk_w; - wire weight_spi_clk_w; - wire boot_spi_cs_n_w; - wire weight_spi_cs_n_w; - wire boot_spi_miso_mux_w; - wire weight_spi_miso_mux_w; - -`ifdef SIM - assign boot_spi_miso_mux_w = (flash_spi_miso_i === 1'bz) ? boot_spi_miso_i : flash_spi_miso_i; - assign weight_spi_miso_mux_w = (flash_spi_miso_i === 1'bz) ? weight_spi_miso_i : flash_spi_miso_i; -`else - assign boot_spi_miso_mux_w = flash_spi_miso_i; - assign weight_spi_miso_mux_w = flash_spi_miso_i; -`endif - - // Lightweight feature register bank exposed to firmware. This is the + wire boot_spi_clk_w; + wire weight_spi_clk_w; + wire boot_spi_cs_n_w; + wire weight_spi_cs_n_w; + wire boot_spi_miso_mux_w; + wire weight_spi_miso_mux_w; + +`ifdef SIM + assign boot_spi_miso_mux_w = (flash_spi_miso_i === 1'bz) ? boot_spi_miso_i : flash_spi_miso_i; + assign weight_spi_miso_mux_w = (flash_spi_miso_i === 1'bz) ? weight_spi_miso_i : flash_spi_miso_i; +`else + assign boot_spi_miso_mux_w = flash_spi_miso_i; + assign weight_spi_miso_mux_w = flash_spi_miso_i; +`endif + + // Lightweight feature register bank exposed to firmware. This is the // current handoff point between the sensor pipeline and the CPU-owned ML // path; firmware reads these latched values and copies them into WEIGHT_BASE. wire feat_sel = mmio_sel && (mem_addr[31:12] == FEATURE_BASE[31:12]); @@ -999,7 +999,7 @@ module top #( weight_flash_axi #( .BASE_ADDR (WEIGHT_BASE), .CLK_DIV (8'd2), - .FLASH_BASE(24'h00_1000) + .FLASH_BASE(24'h00_1000) ) u_weight_ram ( .clk (clk_i), .resetn (~reset_i), @@ -1013,7 +1013,7 @@ module top #( .spi_cs_n (weight_spi_cs_n_w), .spi_clk (weight_spi_clk_w), .spi_mosi (weight_spi_mosi_w), - .spi_miso (boot_done ? weight_spi_miso_mux_w : 1'b0), + .spi_miso (boot_done ? weight_spi_miso_mux_w : 1'b0), // AXI slave — write channel (accept-and-discard) .saxi_awid (wram_awid), .saxi_awaddr (wram_awaddr), @@ -1068,7 +1068,7 @@ module top #( .resetn (~reset_i), .spi_clk_o (boot_spi_clk_w), .spi_mosi_o (boot_spi_mosi_w), - .spi_miso_i (boot_done ? 1'b0 : boot_spi_miso_mux_w), + .spi_miso_i (boot_done ? 1'b0 : boot_spi_miso_mux_w), .spi_cs_n_o (boot_spi_cs_n_w), .sram_valid_o(boot_sram_valid_w), .sram_wstrb_o(boot_sram_wstrb_w), @@ -1230,22 +1230,22 @@ module top #( assign i2c_scl_o = 1'b1; // SCL idle high assign i2c_sda_drive_low_o = 1'b0; // not driving SDA low - // Shared flash bus: spi_boot_ctrl drives until boot_done, weight_flash_axi drives after - assign flash_spi_clk_o = boot_done ? weight_spi_clk_w : boot_spi_clk_w; - assign flash_spi_mosi_o = boot_done ? weight_spi_mosi_w : boot_spi_mosi_w; - assign flash_spi_cs_n_o = boot_done ? weight_spi_cs_n_w : boot_spi_cs_n_w; - - // Compatibility/debug views for older benches that model boot and weight - // flash as separate devices. - assign boot_spi_clk_o = boot_spi_clk_w; - assign boot_spi_mosi_o = boot_spi_mosi_w; - assign boot_spi_cs_n_o = boot_spi_cs_n_w; - assign weight_spi_clk_o = weight_spi_clk_w; - assign weight_spi_mosi_o = weight_spi_mosi_w; - assign weight_spi_cs_n_o = weight_spi_cs_n_w; - - // CPU SPI master not instantiated; keep pins idle. - assign spi_clk_o = 1'b0; - assign spi_mosi_o = 1'b0; - assign spi_cs_n_o = 1'b1; -endmodule + // Shared flash bus: spi_boot_ctrl drives until boot_done, weight_flash_axi drives after + assign flash_spi_clk_o = boot_done ? weight_spi_clk_w : boot_spi_clk_w; + assign flash_spi_mosi_o = boot_done ? weight_spi_mosi_w : boot_spi_mosi_w; + assign flash_spi_cs_n_o = boot_done ? weight_spi_cs_n_w : boot_spi_cs_n_w; + + // Compatibility/debug views for older benches that model boot and weight + // flash as separate devices. + assign boot_spi_clk_o = boot_spi_clk_w; + assign boot_spi_mosi_o = boot_spi_mosi_w; + assign boot_spi_cs_n_o = boot_spi_cs_n_w; + assign weight_spi_clk_o = weight_spi_clk_w; + assign weight_spi_mosi_o = weight_spi_mosi_w; + assign weight_spi_cs_n_o = weight_spi_cs_n_w; + + // CPU SPI master not instantiated; keep pins idle. + assign spi_clk_o = 1'b0; + assign spi_mosi_o = 1'b0; + assign spi_cs_n_o = 1'b1; +endmodule