From 1071a97332091a29549e5c1d71590b573bc04f1f Mon Sep 17 00:00:00 2001 From: Michal Czyz Date: Wed, 13 Sep 2023 18:03:00 +0200 Subject: [PATCH 1/5] Add PIC tests to noxfile Signed-off-by: Maciej Kurc --- verification/block/noxfile.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/verification/block/noxfile.py b/verification/block/noxfile.py index 11f77f8715d..46e84a10101 100644 --- a/verification/block/noxfile.py +++ b/verification/block/noxfile.py @@ -123,6 +123,31 @@ def verify_block(session, blockName, testName, coverage=""): raise Exception("SimFailure: cocotb failed. See test logs for more information.") +@nox.session(tags=["tests"]) +@nox.parametrize("blockName", ["pic"]) +@nox.parametrize( + "testName", + [ + "test_reset", + "test_clken", + "test_config", + "test_prioritization", + "test_servicing", + ], +) +@nox.parametrize("coverage", coverageTypes) +def pic_verify(session, blockName, testName, coverage): + verify_block(session, blockName, testName, coverage) + + +@nox.session(tags=["tests"]) +@nox.parametrize("blockName", ["pic-gw"]) +@nox.parametrize("testName", ["test_gateway"]) +@nox.parametrize("coverage", coverageTypes) +def pic_gw_verify(session, blockName, testName, coverage): + verify_block(session, blockName, testName, coverage) + + @nox.session() def isort(session: nox.Session) -> None: """Options are defined in pyproject.toml file""" From 1f66dc36aabfdcde9af67c6097d65b75469394e6 Mon Sep 17 00:00:00 2001 From: Michal Czyz Date: Wed, 13 Sep 2023 17:50:47 +0200 Subject: [PATCH 2/5] PIC Gateway: implement tests Signed-off-by: Maciej Kurc --- verification/block/pic-gw/Makefile | 16 ++ verification/block/pic-gw/test_gateway.py | 244 ++++++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 verification/block/pic-gw/Makefile create mode 100644 verification/block/pic-gw/test_gateway.py diff --git a/verification/block/pic-gw/Makefile b/verification/block/pic-gw/Makefile new file mode 100644 index 00000000000..190cc64d176 --- /dev/null +++ b/verification/block/pic-gw/Makefile @@ -0,0 +1,16 @@ +null := +space := $(null) # +comma := , + +CURDIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +SRCDIR := $(abspath $(CURDIR)../../../../design) + +TEST_FILES = $(sort $(wildcard test_*.py)) + +MODULE ?= $(subst $(space),$(comma),$(subst .py,,$(TEST_FILES))) +TOPLEVEL = el2_configurable_gw + +VERILOG_SOURCES = \ + $(SRCDIR)/el2_pic_ctrl.sv + +include $(CURDIR)/../common.mk diff --git a/verification/block/pic-gw/test_gateway.py b/verification/block/pic-gw/test_gateway.py new file mode 100644 index 00000000000..b1d7b9e055d --- /dev/null +++ b/verification/block/pic-gw/test_gateway.py @@ -0,0 +1,244 @@ +# +# Copyright (c) 2023 Antmicro +# SPDX-License-Identifier: Apache-2.0 + +import os + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, FallingEdge + +# ============================================================================== + + +async def start_clocks(dut): + """ + Starts DUT clocks + """ + + # When VeeR is built in FPGA-optimized mode rawclk is used by rvdffs inside + # the gateway. Otherwise the gw_clk is used. For testing start both of them. + gw_clk = Clock(dut.gw_clk, 1, units="ns") + rawclk = Clock(dut.rawclk, 1, units="ns") + + cocotb.start_soon(gw_clk.start(start_high=False)) + cocotb.start_soon(rawclk.start(start_high=False)) + + # Enable + cocotb.top.clken.value = 1 + + +async def do_reset(dut): + """ + Resets the DUT + """ + + await ClockCycles(dut.gw_clk, 2) + + dut.rst_l.value = 0 + + await ClockCycles(dut.gw_clk, 1) + await FallingEdge(dut.gw_clk) + + dut.rst_l.value = 1 + + +async def clear_pending(dut): + """ + Clears the pending interrupt flag of the tested module + """ + + dut.meigwclr.value = 1 + await ClockCycles(dut.gw_clk, 1) + dut.meigwclr.value = 0 + + await ClockCycles(dut.gw_clk, 3) + + +# ============================================================================== + + +async def test_level(dut, pol): + """ + Tests level-sensitive interrupt + """ + + # Default state + dut.extintsrc_req.value = not pol + + # Level-sensitive + dut.meigwctrl_type.value = 0 + dut.meigwctrl_polarity.value = not pol + + # Reset + await do_reset(dut) + + # Wait + await ClockCycles(dut.gw_clk, 3) + + # The request should not be pending + assert dut.extintsrc_req_config.value == 0 + + # Request an interrupt + dut.extintsrc_req.value = pol + await ClockCycles(dut.gw_clk, 3) + + # The request should be pending now + assert dut.extintsrc_req_config.value == 1 + + # Clear the pending bit + await clear_pending(dut) + + # The request should be still pending (level sensitive) + assert dut.extintsrc_req_config.value == 1 + + # Cancel the request + dut.extintsrc_req.value = not pol + await ClockCycles(dut.gw_clk, 3) + + # The request should be still pending (latched state) + # assert dut.extintsrc_req_config.value == 1 + + # FIXME: It appears that the gateway does not latch the trigger state + # in level-sensitive mode but rather passes through the interrupt request + # signal. + assert dut.extintsrc_req_config.value == 0 + + # Clear the pending bit again + await clear_pending(dut) + + # The request should not be pending now + assert dut.extintsrc_req_config.value == 0 + + # Wait + await ClockCycles(dut.gw_clk, 3) + + +@cocotb.test() +async def test_level_hi(dut): + await start_clocks(dut) + await test_level(dut, True) + + +@cocotb.test() +async def test_level_lo(dut): + await start_clocks(dut) + await test_level(dut, False) + + +async def test_edge(dut, pol): + """ + Tests edge-sensitive interrupt + """ + + # Default state + dut.extintsrc_req.value = not pol + + # Edge-sensitive + dut.meigwctrl_type.value = 1 + dut.meigwctrl_polarity.value = not pol + + # Reset + await do_reset(dut) + + # Wait + await ClockCycles(dut.gw_clk, 3) + + # The request should not be pending + assert dut.extintsrc_req_config.value == 0 + + # Request an interrupt + dut.extintsrc_req.value = pol + await ClockCycles(dut.gw_clk, 1) + dut.extintsrc_req.value = not pol + + await ClockCycles(dut.gw_clk, 3) + + # The request should be pending now + assert dut.extintsrc_req_config.value == 1 + + # Wait + await ClockCycles(dut.gw_clk, 10) + + # The request should still be pending now + assert dut.extintsrc_req_config.value == 1 + + # Clear the pending bit + await clear_pending(dut) + + # The request should not be pending now + assert dut.extintsrc_req_config.value == 0 + + # Wait + await ClockCycles(dut.gw_clk, 3) + + +@cocotb.test() +async def test_edge_rising(dut): + await start_clocks(dut) + await test_edge(dut, True) + + +# @cocotb.test() # TODO: Falling edge case is failing. Re-enable upon RTL fix +async def test_edge_falling(dut): + await start_clocks(dut) + await test_edge(dut, False) + + +async def test_edge_reset(dut, pol): + """ + Tests edge-sensitive interrupt + """ + + # Default state + dut.extintsrc_req.value = not pol + + # Edge-sensitive + dut.meigwctrl_type.value = 1 + dut.meigwctrl_polarity.value = not pol + + # Reset + await do_reset(dut) + + # Wait + await ClockCycles(dut.gw_clk, 3) + + # The request should not be pending + assert dut.extintsrc_req_config.value == 0 + + # Request an interrupt + dut.extintsrc_req.value = pol + await ClockCycles(dut.gw_clk, 1) + dut.extintsrc_req.value = not pol + + await ClockCycles(dut.gw_clk, 3) + + # The request should be pending now + assert dut.extintsrc_req_config.value == 1 + + # Wait + await ClockCycles(dut.gw_clk, 10) + + # Reset + await do_reset(dut) + + # Wait + await ClockCycles(dut.gw_clk, 3) + + # The request should not be pending now + assert dut.extintsrc_req_config.value == 0 + + # Wait + await ClockCycles(dut.gw_clk, 3) + + +@cocotb.test() +async def test_edge_rising_reset(dut): + await start_clocks(dut) + await test_edge_reset(dut, True) + + +# @cocotb.test() # TODO: Falling edge case is failing. Re-enable upon RTL fix +async def test_edge_falling_reset(dut): + await start_clocks(dut) + await test_edge_reset(dut, False) From 41d36129253ea19b3d4de2ec4323e32c487f5637 Mon Sep 17 00:00:00 2001 From: Michal Czyz Date: Wed, 13 Sep 2023 17:50:54 +0200 Subject: [PATCH 3/5] PIC: implement tests Signed-off-by: Maciej Kurc --- verification/block/pic/Makefile | 16 + verification/block/pic/test_clken.py | 358 ++++++++++ verification/block/pic/test_config.py | 163 +++++ verification/block/pic/test_prioritization.py | 315 +++++++++ verification/block/pic/test_reset.py | 28 + verification/block/pic/test_servicing.py | 297 ++++++++ verification/block/pic/testbench.py | 656 ++++++++++++++++++ 7 files changed, 1833 insertions(+) create mode 100644 verification/block/pic/Makefile create mode 100644 verification/block/pic/test_clken.py create mode 100644 verification/block/pic/test_config.py create mode 100644 verification/block/pic/test_prioritization.py create mode 100644 verification/block/pic/test_reset.py create mode 100644 verification/block/pic/test_servicing.py create mode 100644 verification/block/pic/testbench.py diff --git a/verification/block/pic/Makefile b/verification/block/pic/Makefile new file mode 100644 index 00000000000..9b9ab29bbe0 --- /dev/null +++ b/verification/block/pic/Makefile @@ -0,0 +1,16 @@ +null := +space := $(null) # +comma := , + +CURDIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +SRCDIR := $(abspath $(CURDIR)../../../../design) + +TEST_FILES = $(sort $(wildcard test_*.py)) + +MODULE ?= $(subst $(space),$(comma),$(subst .py,,$(TEST_FILES))) +TOPLEVEL = el2_pic_ctrl + +VERILOG_SOURCES = \ + $(SRCDIR)/el2_pic_ctrl.sv + +include $(CURDIR)/../common.mk diff --git a/verification/block/pic/test_clken.py b/verification/block/pic/test_clken.py new file mode 100644 index 00000000000..ce931eb1bb0 --- /dev/null +++ b/verification/block/pic/test_clken.py @@ -0,0 +1,358 @@ +# Copyright (c) 2023 Antmicro +# SPDX-License-Identifier: Apache-2.0 + +from copy import deepcopy + +import cocotb +import pyuvm +from cocotb.result import SimTimeoutError +from cocotb.triggers import Edge, Lock, RisingEdge, Timer, with_timeout +from pyuvm import * +from testbench import BaseEnv, BaseTest, collect_signals + +# ============================================================================= + + +class ClockEnableItem(uvm_sequence_item): + def __init__(self, clk_en, io_clk_en): + super().__init__("ClockEnableItem") + self.clk_en = clk_en + self.io_clk_en = io_clk_en + + +class ClockStateItem(uvm_sequence_item): + def __init__(self, state): + super().__init__("ClockStateItem") + self.state = deepcopy(state) + + +# ============================================================================= + + +class ClkenDriver(uvm_driver): + """ + A driver for clock gating override signals + """ + + SIGNALS = [ + "clk_override", + "io_clk_override", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + async def run_phase(self): + while True: + it = await self.seq_item_port.get_next_item() + + if isinstance(it, ClockEnableItem): + self.clk_override.value = it.clk_en + self.io_clk_override.value = it.io_clk_en + else: + raise RuntimeError("Unknown item '{}'".format(type(it))) + + self.seq_item_port.item_done() + + +class ClkenMonitor(uvm_component): + """ + A monitor for clock gating override signals + """ + + SIGNALS = [ + "clk", + "clk_override", + "io_clk_override", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + self.prev_clk_override = 0 + self.prev_io_clk_override = 0 + + def build_phase(self): + self.ap = uvm_analysis_port("ap", self) + + async def run_phase(self): + while True: + # Sample control signals + await RisingEdge(self.clk) + clk_override = int(self.clk_override.value) + io_clk_override = int(self.io_clk_override.value) + + if ( + self.prev_clk_override != clk_override + or self.prev_io_clk_override != io_clk_override + ): + self.ap.write(ClockEnableItem(clk_override, io_clk_override)) + + self.prev_clk_override = clk_override + self.prev_io_clk_override = io_clk_override + + +# ============================================================================= + + +class ClockMonitor(uvm_component): + """ + A monitor for clock signal activity. + + The monitor spawns one task per clock signal. Each task waits either for a + signal transition or a fixed time equal to twice the expected clock period + (its actually important that the time is greater than half-period) If, + during the waiting time, the task detects any signal transition + (1->0, 0->1), then it marks the signal as an active clock. Otherwise, the + signal is marked as inactive. + + The main task of the monitor periodically samples the state vector reported + by monitoring tasks and sends a message through its analysis port. This is + scheduled to happen periodically every 5 * the expected clock period. The + scheduling is chosen arbitrarily. + """ + + SIGNALS = [ + "pic_raddr_c1_clk", + "pic_data_c1_clk", + "pic_pri_c1_clk", + "pic_int_c1_clk", + "gw_config_c1_clk", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + self.lock = Lock() + self.state = {sig: False for sig in self.SIGNALS} + + def build_phase(self): + self.ap = uvm_analysis_port("ap", self) + + async def run_phase(self): + period = ConfigDB().get(None, "", "TEST_CLK_PERIOD") + + # Start monitoring tasks + for name in self.SIGNALS: + cocotb.start_soon(self.monitor_clock(name)) + + # Periodically sample clock state and send messages + while True: + # Wait + await Timer(period * 5, "ns") + + # Sample state and send item + async with self.lock: + self.ap.write(ClockStateItem(self.state)) + + async def monitor_clock(self, name): + period = ConfigDB().get(None, "", "TEST_CLK_PERIOD") + signal = getattr(self, name) + + # Monitor the clock signal + while True: + # Wait for clock edges with timeout + try: + await with_timeout(Edge(signal), 2.0 * period, "ns") + toggling = True + except SimTimeoutError: + toggling = False + + # Update the state + async with self.lock: + self.state[name] = toggling + + +# ============================================================================= + + +class Scoreboard(uvm_component): + """ + Clock activity scoreboard. + """ + + CLOCKS = [ + "pic_raddr_c1_clk", + "pic_data_c1_clk", + "pic_pri_c1_clk", + "pic_int_c1_clk", + ] + + IO_CLOCKS = [ + # FIXME: "IO" clock gates are instanced along with gateway modules + # inside a generate block. It appears that cocotb does not have access + # to them. + ] + + def __init__(self, name, parent): + super().__init__(name, parent) + + self.passed = None + + def build_phase(self): + self.fifo = uvm_tlm_analysis_fifo("fifo", self) + self.port = uvm_get_port("port", self) + + def connect_phase(self): + self.port.connect(self.fifo.get_export) + + def check_phase(self): + while self.port.can_get(): + # Get an item + got_item, item = self.port.try_get() + assert got_item + + # Got a change in clock override control + if isinstance(item, ClockEnableItem): + # Initially pass + if self.passed is None: + self.passed = True + + # Reject next clock state item + got_it, it = self.port.try_get() + assert got_it and isinstance(it, ClockStateItem) + + # Get next clock state item and process it + got_it, it = self.port.try_get() + assert got_it and isinstance(it, ClockStateItem) + + # Check clocks + for clk in self.CLOCKS: + if it.state[clk] != item.clk_en: + self.passed = False + self.logger.error( + "Clock '{}' is {}toggling".format( + clk, + "not " if item.clk_en else "", + ) + ) + + for clk in self.IO_CLOCKS: + if it.state[clk] != item.io_clk_en: + self.passed = False + self.logger.error( + "IO clock '{}' is {}toggling".format( + clk, + "not " if item.io_clk_en else "", + ) + ) + + def final_phase(self): + if not self.passed: + self.logger.critical("{} reports a failure".format(type(self))) + assert False + + +# ============================================================================= + + +class TestSequence(uvm_sequence): + """ + A sequence which instructs a driver to enable/disable clock gating override + """ + + async def body(self): + period = ConfigDB().get(None, "", "TEST_CLK_PERIOD") + + # Disable overrides + item = ClockEnableItem(0, 0) + await self.start_item(item) + await self.finish_item(item) + + # Wait + await Timer(20 * period, "ns") + + # Enable clock override + item = ClockEnableItem(1, 0) + await self.start_item(item) + await self.finish_item(item) + + # Wait + await Timer(20 * period, "ns") + + # Disable overrides + item = ClockEnableItem(0, 0) + await self.start_item(item) + await self.finish_item(item) + + # Wait + await Timer(20 * period, "ns") + + # Enable clock override + item = ClockEnableItem(0, 1) + await self.start_item(item) + await self.finish_item(item) + + # Wait + await Timer(20 * period, "ns") + + # Disable overrides + item = ClockEnableItem(0, 0) + await self.start_item(item) + await self.finish_item(item) + + # Wait + await Timer(20 * period, "ns") + + +# ============================================================================= + + +class TestEnv(BaseEnv): + """ + Test environment + """ + + def build_phase(self): + super().build_phase() + + # Sequencers + self.clken_seqr = uvm_sequencer("clken_seqr", self) + + # Clock enable driver and monitor + self.clken_drv = ClkenDriver("clken_drv", self, uut=cocotb.top) + self.clken_mon = ClkenMonitor("clken_mon", self, uut=cocotb.top) + + # Clock monitor + self.clk_mon = ClockMonitor("clk_mon", self, uut=cocotb.top) + + # Add scoreboard + self.scoreboard = Scoreboard("scoreboard", self) + + def connect_phase(self): + super().connect_phase() + self.clken_drv.seq_item_port.connect(self.clken_seqr.seq_item_export) + self.clken_mon.ap.connect(self.scoreboard.fifo.analysis_export) + self.clk_mon.ap.connect(self.scoreboard.fifo.analysis_export) + + +@pyuvm.test() +class TestClockEnable(BaseTest): + """ + A test that checks forcing clock gates open + """ + + def __init__(self, name, parent): + super().__init__(name, parent, TestEnv) + + def end_of_elaboration_phase(self): + super().end_of_elaboration_phase() + self.seq = TestSequence.create("stimulus") + + async def run(self): + await self.seq.start(self.env.clken_seqr) diff --git a/verification/block/pic/test_config.py b/verification/block/pic/test_config.py new file mode 100644 index 00000000000..4c763eb4847 --- /dev/null +++ b/verification/block/pic/test_config.py @@ -0,0 +1,163 @@ +# Copyright (c) 2023 Antmicro +# SPDX-License-Identifier: Apache-2.0 +import random + +import pyuvm +from pyuvm import * +from testbench import BaseEnv, BaseTest, BusReadItem, BusWriteItem, RegisterMap + +# ============================================================================= + + +class TestSequence(uvm_sequence): + """ + A sequence of randomized register and content writes followed by randomized + reads of them. + """ + + def __init__(self, name): + super().__init__(name) + self.reg_map = RegisterMap() + + async def body(self): + num_itr = ConfigDB().get(None, "", "TEST_ITERATIONS") + max_pri = ConfigDB().get(None, "", "PIC_NUM_PRIORITIES") + + for i in range(num_itr): + # Issue register writes + names = list(self.reg_map.reg.keys()) + random.shuffle(names) + + written = [] + + for name in names: + reg = self.reg_map.reg[name] + val = None + + if name.startswith("meipl"): + val = random.randint(0, max_pri) + elif name.startswith("meigwctrl"): + val = random.randint(0, 3) # 2-bit combinations + elif name.startswith("meie"): + val = random.randint(0, 1) # 1-bit combinations + + if val is None: + continue + + item = BusWriteItem(reg, val) + await self.start_item(item) + await self.finish_item(item) + + written.append(reg) + + # Issue register reads for the written ones + random.shuffle(written) + + for reg in written: + item = BusReadItem(reg) + await self.start_item(item) + await self.finish_item(item) + + +class Scoreboard(uvm_component): + """ + A scoreboard that keeps track of data written to registers and compares + it with data read afterwards. + """ + + def __init__(self, name, parent): + super().__init__(name, parent) + + self.passed = None + self.reg_map = RegisterMap() + self.reg_content = dict() + + def build_phase(self): + self.fifo = uvm_tlm_analysis_fifo("fifo", self) + self.port = uvm_get_port("port", self) + + def connect_phase(self): + self.port.connect(self.fifo.get_export) + + def check_phase(self): + while self.port.can_get(): + # Get an item + got_item, item = self.port.try_get() + assert got_item + + # Initially pass + if self.passed is None: + self.passed = True + + # Bus write + if isinstance(item, BusWriteItem): + self.reg_content[item.addr] = item.data + + # Bus read + elif isinstance(item, BusReadItem): + # Get register name + reg_name = "0x{:08X}".format(item.addr) + name = self.reg_map.adr.get(item.addr) + if name is not None: + reg_name += " '{}'".format(name) + + # No entry + golden = self.reg_content.get(item.addr) + if golden is None: + self.logger.error("Register {} was not written".format(reg_name)) + self.passed = False + + # Mismatch + elif golden != item.data: + self.logger.error( + "Register {} content mismatch, is 0x{:08X} should be 0x{:08X}".format( + reg_name, item.data, golden + ) + ) + self.passed = False + else: + self.logger.debug( + "Register {} ok, 0x{:08X}".format( + reg_name, + item.data, + ) + ) + + def final_phase(self): + if not self.passed: + self.logger.critical("{} reports a failure".format(type(self))) + assert False + + +# ============================================================================== + + +class TestEnv(BaseEnv): + def build_phase(self): + super().build_phase() + + # Add scoreboard + self.scoreboard = Scoreboard("scoreboard", self) + + def connect_phase(self): + super().connect_phase() + + # Connect monitors + self.reg_mon.ap.connect(self.scoreboard.fifo.analysis_export) + + +@pyuvm.test() +class TestConfig(BaseTest): + """ + A test for PIC configuration register access + """ + + def __init__(self, name, parent): + super().__init__(name, parent, TestEnv) + + def end_of_elaboration_phase(self): + super().end_of_elaboration_phase() + self.seq = TestSequence.create("stimulus") + + async def run(self): + await self.seq.start(self.env.reg_seqr) diff --git a/verification/block/pic/test_prioritization.py b/verification/block/pic/test_prioritization.py new file mode 100644 index 00000000000..f4e3699a6a2 --- /dev/null +++ b/verification/block/pic/test_prioritization.py @@ -0,0 +1,315 @@ +# Copyright (c) 2023 Antmicro +# SPDX-License-Identifier: Apache-2.0 +import random + +import cocotb +import pyuvm +from cocotb.triggers import ClockCycles +from pyuvm import * +from testbench import ( + BaseEnv, + BaseTest, + BusReadItem, + BusWriteItem, + ClaimItem, + IrqItem, + PrioLvlItem, + PriorityPredictor, + PrioThrItem, + RegisterMap, +) + +# ============================================================================= + + +class TestSequence(uvm_sequence): + def __init__(self, name): + super().__init__(name) + + self.reg_seqr = ConfigDB().get(None, "", "REG_SEQR") + self.pri_seqr = ConfigDB().get(None, "", "PRI_SEQR") + self.irq_seqr = ConfigDB().get(None, "", "IRQ_SEQR") + + self.regs = RegisterMap() + + async def body(self): + num_irq = ConfigDB().get(None, "", "PIC_NUM_INTERRUPTS") + max_pri = ConfigDB().get(None, "", "PIC_NUM_PRIORITIES") + num_itr = ConfigDB().get(None, "", "TEST_ITERATIONS") + ena_prob = ConfigDB().get(None, "", "TEST_IRQ_ENA_PROB") + irq_prob = ConfigDB().get(None, "", "TEST_IRQ_REQ_PROB") + + # Basic PIC config + item = BusWriteItem(self.regs.reg["mpiccfg"], random.randint(0, 1)) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + for i in range(num_itr): + # Randomize priorities + for i in range(1, num_irq): + reg = self.regs.reg["meipl{}".format(i)] + val = random.randint(0, max_pri) + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + # Randomize enables + for i in range(1, num_irq): + reg = self.regs.reg["meie{}".format(i)] + val = int(random.random() < ena_prob) + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + # Configure gateways + for i in range(1, num_irq): + reg = self.regs.reg["meigwctrl{}".format(i)] + val = 0x2 # Edge, active-high + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + # Randomize current priority and threshold + lvl = random.randint(0, max_pri) + thr = random.randint(0, max_pri) + + item = PrioLvlItem(lvl) + await self.pri_seqr.start_item(item) + await self.pri_seqr.finish_item(item) + + item = PrioThrItem(thr) + await self.pri_seqr.start_item(item) + await self.pri_seqr.finish_item(item) + + # Wait + await ClockCycles(cocotb.top.clk, 4) + + # Randomize IRQ + irqs = 0 + while irqs == 0: + for i in range(1, num_irq): + if random.random() > irq_prob: + irqs |= 1 << i + + item = IrqItem(irqs) + await self.irq_seqr.start_item(item) + await self.irq_seqr.finish_item(item) + + # Wait + await ClockCycles(cocotb.top.clk, 2) + + # Clear IRQ + item = IrqItem(0) + await self.irq_seqr.start_item(item) + await self.irq_seqr.finish_item(item) + + # Wait + await ClockCycles(cocotb.top.clk, 4) + + # Clear pending gateways + for i in range(1, num_irq): + if irqs & (1 << i): + reg = self.regs.reg["meigwclr{}".format(i)] + val = 0 + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + # Wait + await ClockCycles(cocotb.top.clk, 5) + + +# ============================================================================== + + +class Scoreboard(uvm_component): + def __init__(self, name, parent): + super().__init__(name, parent) + + self.passed = None + self.predictor = PriorityPredictor(self.logger) + self.regs = RegisterMap() + + def build_phase(self): + self.fifo = uvm_tlm_analysis_fifo("fifo", self) + self.port = uvm_get_port("port", self) + + def connect_phase(self): + self.port.connect(self.fifo.get_export) + + def check_phase(self): + num_irq = ConfigDB().get(None, "", "PIC_NUM_INTERRUPTS") + max_pri = ConfigDB().get(None, "", "PIC_NUM_PRIORITIES") + + pri_lvl = 0 + pri_thr = 0 + irqs = 0 + + claimid = 0 + mexintpend = 0 + mhwakeup = 0 + + while self.port.can_get(): + _, item = self.port.try_get() + + # Register write + if isinstance(item, BusWriteItem): + # Get the reg name + reg = self.regs.adr.get(item.addr) + if not reg: + self.logger.error("Unknown register address 0x{:08X}".format(item.addr)) + self.passed = False + continue + + if reg.startswith("meipl"): + s = int(reg[5:]) + self.predictor.irqs[s].priority = item.data + + if reg.startswith("meie"): + s = int(reg[4:]) + self.predictor.irqs[s].enabled = bool(item.data) + + if reg == "mpiccfg": + self.predictor.inv_order = bool(item.data) + + # Priority level + elif isinstance(item, PrioLvlItem): + pri_lvl = item.prio + + # Priority threshold + elif isinstance(item, PrioThrItem): + pri_thr = item.prio + + # IRQ + elif isinstance(item, IrqItem): + # Mark triggered interrupts + for i in range(1, num_irq): + if item.irqs & (1 << i): + self.predictor.irqs[i].triggered = True + + # Store requested irqs + if item.irqs != 0: + irqs = item.irqs + + # Interrupt claim + elif isinstance(item, ClaimItem): + claimid = item.claimid + mexintpend = item.mexintpend + mhwakeup = item.mhwakeup + + # Check only if IRQs were requested + if not irqs: + continue + irqs = 0 + + # Initially pass + if self.passed is None: + self.passed = True + + # Predict the claim + pred = self.predictor.predict() + + # Check + if claimid != pred.id: + self.logger.error( + "Interrupt mismatch, is {} should be {}".format(claimid, pred.id) + ) + self.passed = False + + # Check if the interrupt is above the current priority level + # and threshold. Check if it is signaled correctly. + else: + # Predict mexintpend + if self.predictor.inv_order: + pred_mexintpend = ( + pred.id != 0 + and pred.priority != max_pri + and pred.priority < pri_thr + and pred.priority < pri_lvl + ) + else: + pred_mexintpend = ( + pred.id != 0 + and pred.priority != 0 + and pred.priority > pri_thr + and pred.priority > pri_lvl + ) + + # Predict mhwakeup + if self.predictor.inv_order: + pred_mhwakeup = pred.id != 0 and pred.priority == 0 + else: + pred_mhwakeup = pred.id != 0 and pred.priority == max_pri + + # Check + if pred_mexintpend != mexintpend: + self.logger.error( + "Signaling mismatch, mexintpend is {} should be {}. irq {}, meicurpl={}, meipt={}".format( + bool(mexintpend), + bool(pred_mexintpend), + pred.id, + pri_lvl, + pri_thr, + ) + ) + self.passed = False + + if pred_mhwakeup != mhwakeup: + self.logger.error( + "Signaling mismatch, mhwakeup is {} should be {}. irq {}, meicurpl={}, meipt={}".format( + bool(mhwakeup), + bool(pred_mhwakeup), + pred.id, + pri_lvl, + pri_thr, + ) + ) + self.passed = False + + # Clear triggers + for i in range(1, num_irq): + self.predictor.irqs[i].triggered = False + + def final_phase(self): + if not self.passed: + self.logger.critical("{} reports a failure".format(type(self))) + assert False + + +# ============================================================================== + + +class TestEnv(BaseEnv): + def build_phase(self): + super().build_phase() + + # Add scoreboard + self.scoreboard = Scoreboard("scoreboard", self) + + def connect_phase(self): + super().connect_phase() + + # Connect monitors + self.reg_mon.ap.connect(self.scoreboard.fifo.analysis_export) + self.pri_mon.ap.connect(self.scoreboard.fifo.analysis_export) + self.irq_mon.ap.connect(self.scoreboard.fifo.analysis_export) + self.claim_mon.ap.connect(self.scoreboard.fifo.analysis_export) + + +@pyuvm.test() +class TestPrioritization(BaseTest): + """ """ + + def __init__(self, name, parent): + super().__init__(name, parent, TestEnv) + + def end_of_elaboration_phase(self): + super().end_of_elaboration_phase() + self.seq = TestSequence.create("stimulus") + + async def run(self): + await self.seq.start() diff --git a/verification/block/pic/test_reset.py b/verification/block/pic/test_reset.py new file mode 100644 index 00000000000..6a791f3ca90 --- /dev/null +++ b/verification/block/pic/test_reset.py @@ -0,0 +1,28 @@ +# Copyright (c) 2023 Antmicro +# SPDX-License-Identifier: Apache-2.0 + +import cocotb +import pyuvm +from testbench import BaseTest + +# ============================================================================= + + +@pyuvm.test() +class TestReset(BaseTest): + """ + A basic test that resets the DUT + """ + + async def run(self): + # Check state of DUT signals after reset + state = { + "mexintpend": 0, + "mhwakeup": 0, + "pl": 0, + "claimid": 0, + } + + for name, value in state.items(): + signal = getattr(cocotb.top, name) + assert signal.value == value, "{}={}, should be {}".format(name, signal.value, value) diff --git a/verification/block/pic/test_servicing.py b/verification/block/pic/test_servicing.py new file mode 100644 index 00000000000..cc8cb403017 --- /dev/null +++ b/verification/block/pic/test_servicing.py @@ -0,0 +1,297 @@ +# Copyright (c) 2023 Antmicro +# SPDX-License-Identifier: Apache-2.0 +import random + +import cocotb +import pyuvm +from cocotb.triggers import ClockCycles +from pyuvm import * +from testbench import ( + BaseEnv, + BaseTest, + BusReadItem, + BusWriteItem, + ClaimItem, + IrqItem, + PrioLvlItem, + PriorityPredictor, + PrioThrItem, + RegisterMap, +) + +# ============================================================================= + + +class TestSequence(uvm_sequence): + def __init__(self, name): + super().__init__(name) + + self.reg_seqr = ConfigDB().get(None, "", "REG_SEQR") + self.pri_seqr = ConfigDB().get(None, "", "PRI_SEQR") + self.irq_seqr = ConfigDB().get(None, "", "IRQ_SEQR") + + self.regs = RegisterMap() + + async def body(self): + num_irq = ConfigDB().get(None, "", "PIC_NUM_INTERRUPTS") + max_pri = ConfigDB().get(None, "", "PIC_NUM_PRIORITIES") + num_itr = ConfigDB().get(None, "", "TEST_ITERATIONS") + ena_prob = ConfigDB().get(None, "", "TEST_IRQ_ENA_PROB") + + # Basic PIC config + item = BusWriteItem(self.regs.reg["mpiccfg"], 0) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + predictor = PriorityPredictor() + enabled_irqs = list() + + for i in range(num_itr): + # Randomize priorities + for i in range(1, num_irq): + reg = self.regs.reg["meipl{}".format(i)] + val = random.randint(0, max_pri) + + predictor.irqs[i].priority = val + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + # Randomize enables + for i in range(1, num_irq): + reg = self.regs.reg["meie{}".format(i)] + val = int(random.random() < ena_prob) + + predictor.irqs[i].enabled = val + + if val: + enabled_irqs.append(i) + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + # Configure gateways + for i in range(1, num_irq): + reg = self.regs.reg["meigwctrl{}".format(i)] + val = 0x2 # Edge, active-high + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + # Set interrupt threshold + item = PrioThrItem(random.randint(0, max_pri)) + await self.pri_seqr.start_item(item) + await self.pri_seqr.finish_item(item) + + # Wait + await ClockCycles(cocotb.top.clk, 4) + + # Request IRQs + irqs = 0 + for i in enabled_irqs: + predictor.irqs[i].triggered = val + irqs |= 1 << i + + item = IrqItem(irqs) + await self.irq_seqr.start_item(item) + await self.irq_seqr.finish_item(item) + + # Wait + await ClockCycles(cocotb.top.clk, 2) + + # Clear IRQ + item = IrqItem(0) + await self.irq_seqr.start_item(item) + await self.irq_seqr.finish_item(item) + + # Wait + await ClockCycles(cocotb.top.clk, 4) + + # Mimic interrupt servicing + for i in range(50): # Limit iterations + # Predict the IRQ to be serviced + irq = predictor.predict() + if irq.id == 0: + break + + # Begin servicing, set meicurpl + item = PrioLvlItem(irq.priority) + await self.pri_seqr.start_item(item) + await self.pri_seqr.finish_item(item) + + # Servicing period + await ClockCycles(cocotb.top.clk, 5) + + # Finish servicing, set meicurpl to 0 + item = PrioLvlItem(0) + await self.pri_seqr.start_item(item) + await self.pri_seqr.finish_item(item) + + # Clear pending + reg = self.regs.reg["meigwclr{}".format(irq.id)] + val = 0 + + item = BusWriteItem(reg, val) + await self.reg_seqr.start_item(item) + await self.reg_seqr.finish_item(item) + + predictor.irqs[irq.id].triggered = False + + await ClockCycles(cocotb.top.clk, 4) + + +# ============================================================================== + + +class Scoreboard(uvm_component): + def __init__(self, name, parent): + super().__init__(name, parent) + + self.passed = None + self.predictor = PriorityPredictor(self.logger) + self.regs = RegisterMap() + + def build_phase(self): + self.fifo = uvm_tlm_analysis_fifo("fifo", self) + self.port = uvm_get_port("port", self) + + def connect_phase(self): + self.port.connect(self.fifo.get_export) + + def check_phase(self): + num_irq = ConfigDB().get(None, "", "PIC_NUM_INTERRUPTS") + + pri_thr = 0 + + irq_order = [] + + while self.port.can_get(): + _, item = self.port.try_get() + + # Register write + if isinstance(item, BusWriteItem): + # Get the reg name + reg = self.regs.adr.get(item.addr) + if not reg: + self.logger.error("Unknown register address 0x{:08X}".format(item.addr)) + self.passed = False + continue + + if reg.startswith("meipl"): + s = int(reg[5:]) + self.predictor.irqs[s].priority = item.data + + if reg.startswith("meie"): + s = int(reg[4:]) + self.predictor.irqs[s].enabled = bool(item.data) + + # Priority threshold + elif isinstance(item, PrioThrItem): + pri_thr = item.prio + + # IRQ + elif isinstance(item, IrqItem): + # Nothing triggered + if not item.irqs: + continue + + # Mark triggered interrupts + for i in range(1, num_irq): + if item.irqs & (1 << i): + self.predictor.irqs[i].triggered = True + + # Predict the order of interrupt servicing + for i in range(50): # Limit iterations + # Predict the IRQ to be serviced + irq = self.predictor.predict() + if irq.id == 0: + break + + irq_order.append(irq.id) + + # Clear pending + self.predictor.irqs[irq.id].triggered = False + + self.logger.debug("Interrupt order: {}".format(irq_order)) + + # Interrupt claim + elif isinstance(item, ClaimItem): + # Not waiting for any interrupt + if not irq_order: + continue + + # Initially pass + if self.passed is None: + self.passed = True + + self.logger.debug( + "Servicing {}, mexintpend={}".format( + item.claimid, + item.mexintpend, + ) + ) + + # check id + if item.claimid != irq_order[0]: + self.logger.error( + "Incorrect interrupt servicing order, claimed {} should be {}".format( + item.claimid, irq_order[0] + ) + ) + self.passed = False + + # mexintpend must be set + if not item.mexintpend and item.claimpl > pri_thr: + self.logger.error("Interrupt not reported to the core") + self.passed = False + + # Remove the serviced id + irq_order = irq_order[1:] + + # Check if all interrupts were services + if irq_order: + self.logger.error("Interrupts {} were not serviced".format(irq_order)) + self.passed = False + + def final_phase(self): + if not self.passed: + self.logger.critical("{} reports a failure".format(type(self))) + assert False + + +# ============================================================================== + + +class TestEnv(BaseEnv): + def build_phase(self): + super().build_phase() + + # Add scoreboard + self.scoreboard = Scoreboard("scoreboard", self) + + def connect_phase(self): + super().connect_phase() + + # Connect monitors + self.reg_mon.ap.connect(self.scoreboard.fifo.analysis_export) + self.pri_mon.ap.connect(self.scoreboard.fifo.analysis_export) + self.irq_mon.ap.connect(self.scoreboard.fifo.analysis_export) + self.claim_mon.ap.connect(self.scoreboard.fifo.analysis_export) + + +@pyuvm.test() +class TestServicing(BaseTest): + """ """ + + def __init__(self, name, parent): + super().__init__(name, parent, TestEnv) + + def end_of_elaboration_phase(self): + super().end_of_elaboration_phase() + self.seq = TestSequence.create("stimulus") + + async def run(self): + await self.seq.start() diff --git a/verification/block/pic/testbench.py b/verification/block/pic/testbench.py new file mode 100644 index 00000000000..e92feb83448 --- /dev/null +++ b/verification/block/pic/testbench.py @@ -0,0 +1,656 @@ +# +# Copyright (c) 2023 Antmicro +# SPDX-License-Identifier: Apache-2.0 + +import os + +import pyuvm +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, FallingEdge, RisingEdge +from pyuvm import * + +# ============================================================================== + + +class RegisterMap: + """ + Map of PIC memory-mapped registers + """ + + def __init__(self, max_irqs=32, base_addr=0xF00C0000): + self.reg = dict() + self.adr = dict() + + self.add_reg("mpiccfg", base_addr + 0x3000) + + for s in range(1, max_irqs): + name = "meipl{}".format(s) + addr = base_addr + 4 * s + self.add_reg(name, addr) + + for x in range(0, max_irqs // 32): + name = "meip{}".format(x) + addr = base_addr + 0x1000 + 4 * x + self.add_reg(name, addr) + + for s in range(1, max_irqs): + name = "meie{}".format(s) + addr = base_addr + 0x2000 + 4 * s + self.add_reg(name, addr) + + for s in range(1, max_irqs): + name = "meigwctrl{}".format(s) + addr = base_addr + 0x4000 + 4 * s + self.add_reg(name, addr) + + for s in range(1, max_irqs): + name = "meigwclr{}".format(s) + addr = base_addr + 0x5000 + 4 * s + self.add_reg(name, addr) + + def add_reg(self, name, addr): + self.reg[name] = addr + self.adr[addr] = name + + +# ============================================================================== + + +class BusWriteItem(uvm_sequence_item): + """ + A generic data bus write request / response + """ + + def __init__(self, addr, data): + super().__init__("BusWriteItem") + self.addr = addr + self.data = data + + def randomize(self): + pass + + +class BusReadItem(uvm_sequence_item): + """ + A generic data bus read request / response + """ + + def __init__(self, addr, data=None): + super().__init__("BusReadItem") + self.addr = addr + self.data = data + + def randomize(self): + pass + + +class PrioLvlItem(uvm_sequence_item): + def __init__(self, prio): + super().__init__("PrioLvlItem") + self.prio = prio + + +class PrioThrItem(uvm_sequence_item): + def __init__(self, prio): + super().__init__("PrioThrItem") + self.prio = prio + + +class IrqItem(uvm_sequence_item): + def __init__(self, irqs): + super().__init__("IrqItem") + self.irqs = irqs + + +class ClaimItem(uvm_sequence_item): + def __init__(self, claimid, claimpl, mexintpend, mhwakeup): + super().__init__("ClaimItem") + self.claimid = claimid + self.claimpl = claimpl + + self.mexintpend = mexintpend + self.mhwakeup = mhwakeup + + +class WaitItem(uvm_sequence_item): + """ + A generic wait item. Used to instruct a driver to wait N cycles + """ + + def __init__(self, cycles): + super().__init__("WaitItem") + self.cycles = cycles + + def randomize(self): + pass + + +# ============================================================================== + + +def collect_signals(signals, uut, obj): + """ + Collects signal objects from UUT and attaches them to the given object + """ + + for sig in signals: + if hasattr(uut, sig): + s = getattr(uut, sig) + + else: + s = None + logging.error("Module {} does not have a signal '{}'".format(str(uut), sig)) + + setattr(obj, sig, s) + + +# ============================================================================== + + +class RegisterBfm: + """ + A BFM for the PIC configuration (registers) interface. + """ + + SIGNALS = [ + "picm_rden", + "picm_rdaddr", + "picm_rd_data", + "picm_wren", + "picm_wraddr", + "picm_wr_data", + "picm_mken", + ] + + def __init__(self, uut, clk): + # Collect signals + collect_signals(self.SIGNALS, uut, self) + + # Get the clock + obj = getattr(uut, clk) + setattr(self, "picm_clk", obj) + + async def read(self, addr): + """ + Reads a register + """ + + await RisingEdge(self.picm_clk) + + self.picm_rdaddr.value = addr + self.picm_rden.value = 1 + self.picm_mken.value = 0 + + await RisingEdge(self.picm_clk) + + self.picm_rden.value = 0 + + await FallingEdge(self.picm_clk) + + data = self.picm_rd_data.value + + return data + + async def write(self, addr, data): + """ + Writes a register + """ + + await RisingEdge(self.picm_clk) + + self.picm_wraddr.value = addr + self.picm_wr_data.value = data + self.picm_wren.value = 1 + self.picm_mken.value = 0 + + await RisingEdge(self.picm_clk) + + self.picm_wren.value = 0 + + +class RegisterDriver(uvm_driver): + """ + Configuration (register) interface driver + """ + + def __init__(self, *args, **kwargs): + self.bfm = kwargs["bfm"] + del kwargs["bfm"] + super().__init__(*args, **kwargs) + + async def run_phase(self): + while True: + it = await self.seq_item_port.get_next_item() + + if isinstance(it, BusWriteItem): + await self.bfm.write(it.addr, it.data) + elif isinstance(it, BusReadItem): + it.data = await self.bfm.read(it.addr) + elif isinstance(it, WaitItem): + await ClockCycles(self.bfm.picm_clk) + else: + raise RuntimeError("Unknown item '{}'".format(type(it))) + + self.seq_item_port.item_done() + + +class RegisterMonitor(uvm_component): + """ + Configuration (register) interface monitor + """ + + def __init__(self, *args, **kwargs): + self.bfm = kwargs["bfm"] + del kwargs["bfm"] + super().__init__(*args, **kwargs) + + def build_phase(self): + self.ap = uvm_analysis_port("ap", self) + + async def run_phase(self): + while True: + await RisingEdge(self.bfm.picm_clk) + + # Read + if self.bfm.picm_rden.value: + addr = int(self.bfm.picm_rdaddr.value) + + await FallingEdge(self.bfm.picm_clk) + + data = int(self.bfm.picm_rd_data.value) + self.logger.debug("read 0x{:08X} -> 0x{:08X}".format(addr, data)) + self.ap.write(BusReadItem(addr, data)) + + # Write + if self.bfm.picm_wren.value: + addr = int(self.bfm.picm_wraddr.value) + data = int(self.bfm.picm_wr_data.value) + self.logger.debug("write 0x{:08X} <- 0x{:08X}".format(addr, data)) + self.ap.write(BusWriteItem(addr, data)) + + +# ============================================================================== + + +class PrioDriver(uvm_driver): + """ + A driver for priority and priority threshold inputs of the PIC + """ + + SIGNALS = [ + "meicurpl", + "meipt", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + async def run_phase(self): + while True: + it = await self.seq_item_port.get_next_item() + + if isinstance(it, PrioLvlItem): + self.meicurpl.value = it.prio + elif isinstance(it, PrioThrItem): + self.meipt.value = it.prio + else: + raise RuntimeError("Unknown item '{}'".format(type(it))) + + self.seq_item_port.item_done() + + +class PrioMonitor(uvm_component): + """ + A monitor for priority and priority threshold of the PIC + """ + + SIGNALS = [ + "clk", + "meicurpl", + "meipt", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + self.prev_meicurpl = None + self.prev_meipt = None + + def build_phase(self): + self.ap = uvm_analysis_port("ap", self) + + async def run_phase(self): + while True: + # Even though the signals are not registered sample them on + # rising clock edge + await RisingEdge(self.clk) + + # Sample signals + curr_meicurpl = int(self.meicurpl.value) + curr_meipt = int(self.meipt.value) + + # Send an item in case of a change + if self.prev_meicurpl != curr_meicurpl: + self.ap.write(PrioLvlItem(curr_meicurpl)) + if self.prev_meipt != curr_meipt: + self.ap.write(PrioThrItem(curr_meipt)) + + self.prev_meicurpl = curr_meicurpl + self.prev_meipt = curr_meipt + + +# ============================================================================== + + +class IrqDriver(uvm_driver): + """ + A driver for interrupt requests + """ + + SIGNALS = [ + "extintsrc_req", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + async def run_phase(self): + while True: + it = await self.seq_item_port.get_next_item() + + if isinstance(it, IrqItem): + self.extintsrc_req.value = it.irqs + else: + raise RuntimeError("Unknown item '{}'".format(type(it))) + + self.seq_item_port.item_done() + + +class IrqMonitor(uvm_component): + """ + A monitor for interrupt requests + """ + + SIGNALS = [ + "clk", + "extintsrc_req", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + self.prev_irqs = None + + def build_phase(self): + self.ap = uvm_analysis_port("ap", self) + + async def run_phase(self): + while True: + # Sample signals + await RisingEdge(self.clk) + curr_irqs = int(self.extintsrc_req.value) + + # Send an item in case of a change + if self.prev_irqs != curr_irqs: + self.ap.write(IrqItem(curr_irqs)) + + self.prev_irqs = curr_irqs + + +# ============================================================================== + + +class ClaimMonitor(uvm_component): + SIGNALS = [ + "clk", + "extintsrc_req", + "picm_wren", + "claimid", + "pl", + "mexintpend", + "mhwakeup", + ] + + def __init__(self, *args, **kwargs): + uut = kwargs["uut"] + del kwargs["uut"] + + super().__init__(*args, **kwargs) + + collect_signals(self.SIGNALS, uut, self) + + self.prev_irqs = 0 + self.prev_wren = 0 + + def build_phase(self): + self.ap = uvm_analysis_port("ap", self) + + async def run_phase(self): + while True: + # Sample control signals + await RisingEdge(self.clk) + irqs = int(self.extintsrc_req.value) + wren = int(self.picm_wren.value) + + is_wren = wren and not self.prev_wren # Rising edge of wren + is_irqs = irqs & ~self.prev_irqs # Rising edge of any IRQ + + if is_wren or is_irqs: + # Sample signals after a delay to give PIC time to react. + # It was observed that in the simulation that the wait must + # be at least 3 clock cycles long. + await ClockCycles(self.clk, 3) + + claimid = int(self.claimid.value) + claimpl = int(self.pl.value) + mexintpend = int(self.mexintpend.value) + mhwakeup = int(self.mhwakeup.value) + + self.ap.write(ClaimItem(claimid, claimpl, mexintpend, mhwakeup)) + + self.prev_irqs = irqs + self.prev_wren = wren + + +# ============================================================================== + + +class PriorityPredictor: + class Irq: + """ + Interrupt request state + """ + + def __init__(self, n): + self.id = n + self.priority = 0 + self.enabled = False + self.triggered = False + + def __str__(self): + return "id={:3d} en={} pri={:2d} trg={}".format( + self.id, + int(self.enabled), + self.priority, + int(self.triggered), + ) + + def __repr__(self): + return str(self) + + def __init__(self, logger=None): + self.inv_order = False + self.irqs = {i: self.Irq(i) for i in range(1, 32)} + self.logger = logger + + if self.logger is None: + self.logger = uvm_root().logger + + def predict(self): + # Dump IRQs + self.logger.debug("IRQs:") + keys = sorted(list(self.irqs)) + for k in keys: + self.logger.debug(" " + str(self.irqs[k])) + + # Filter only enabled and triggered + irqs = {k: v for k, v in self.irqs.items() if v.enabled and v.triggered} + + # Get the highest priority + pred = None + for irq in irqs.values(): + # Skip priority 0 or 15 + if self.inv_order: + if irq.priority == 15: + continue + else: + if irq.priority == 0: + continue + + # Find max priority and min id + if pred is None: + pred = irq + else: + if self.inv_order: + if irq.priority < pred.priority: + pred = irq + else: + if irq.priority > pred.priority: + pred = irq + + if irq.priority == pred.priority: + if irq.id < pred.id: + pred = irq + + if pred is None: + return self.Irq(0) + + self.logger.debug("pred:") + self.logger.debug(" " + str(pred)) + + return pred + + +# ============================================================================== + + +class BaseEnv(uvm_env): + """ + Base PyUVM test environment + """ + + def build_phase(self): + # Config + ConfigDB().set(None, "*", "PIC_NUM_INTERRUPTS", 32) + ConfigDB().set(None, "*", "PIC_NUM_PRIORITIES", 15) + + ConfigDB().set(None, "*", "TEST_CLK_PERIOD", 1) + ConfigDB().set(None, "*", "TEST_ITERATIONS", 50) + ConfigDB().set(None, "*", "TEST_IRQ_ENA_PROB", 0.75) + ConfigDB().set(None, "*", "TEST_IRQ_REQ_PROB", 0.90) + + # Sequencers + self.reg_seqr = uvm_sequencer("reg_seqr", self) + self.pri_seqr = uvm_sequencer("pri_seqr", self) + self.irq_seqr = uvm_sequencer("irq_seqr", self) + + # Register interface + bfm = RegisterBfm(cocotb.top, "clk") + self.reg_drv = RegisterDriver("reg_drv", self, bfm=bfm) + self.reg_mon = RegisterMonitor("reg_mon", self, bfm=bfm) + + # Current priority and priority threshold interface + self.pri_drv = PrioDriver("pri_drv", self, uut=cocotb.top) + self.pri_mon = PrioMonitor("pri_mon", self, uut=cocotb.top) + + # Interrupt request + self.irq_drv = IrqDriver("irq_drv", self, uut=cocotb.top) + self.irq_mon = IrqMonitor("irq_mon", self, uut=cocotb.top) + + # Interrupt claim monitor + self.claim_mon = ClaimMonitor("claim_mon", self, uut=cocotb.top) + + ConfigDB().set(None, "*", "REG_SEQR", self.reg_seqr) + ConfigDB().set(None, "*", "PRI_SEQR", self.pri_seqr) + ConfigDB().set(None, "*", "IRQ_SEQR", self.irq_seqr) + + def connect_phase(self): + self.reg_drv.seq_item_port.connect(self.reg_seqr.seq_item_export) + self.pri_drv.seq_item_port.connect(self.pri_seqr.seq_item_export) + self.irq_drv.seq_item_port.connect(self.irq_seqr.seq_item_export) + + +# ============================================================================== + + +class BaseTest(uvm_test): + """ + Base test for the module + """ + + def __init__(self, name, parent, env_class=BaseEnv): + super().__init__(name, parent) + self.env_class = env_class + + # Synchronize pyuvm logging level with cocotb logging level. + level = logging.getLevelName(os.environ.get("COCOTB_LOG_LEVEL", "INFO")) + uvm_report_object.set_default_logging_level(level) + + def build_phase(self): + self.env = self.env_class("env", self) + + def start_clock(self, name): + period = ConfigDB().get(None, "", "TEST_CLK_PERIOD") + sig = getattr(cocotb.top, name) + clock = Clock(sig, period, units="ns") + cocotb.start_soon(clock.start(start_high=False)) + + async def do_reset(self): + cocotb.top.rst_l.value = 0 + await ClockCycles(cocotb.top.clk, 2) + await FallingEdge(cocotb.top.clk) + cocotb.top.rst_l.value = 1 + + async def run_phase(self): + self.raise_objection() + + # Start clocks + self.start_clock("clk") + self.start_clock("free_clk") + + # Issue reset + await self.do_reset() + + # Wait some cycles + await ClockCycles(cocotb.top.clk, 2) + + # Run the actual test + await self.run() + + # Wait some cycles + await ClockCycles(cocotb.top.clk, 10) + + self.drop_objection() + + async def run(self): + raise NotImplementedError() From 0ae72aec5d2dc51a26a39e566d1c8d4a1baba27c Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Tue, 19 Sep 2023 16:39:01 +0200 Subject: [PATCH 4/5] Moved pic-gw to pic_gw Signed-off-by: Maciej Kurc --- verification/block/noxfile.py | 2 +- verification/block/{pic-gw => pic_gw}/Makefile | 0 verification/block/{pic-gw => pic_gw}/test_gateway.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename verification/block/{pic-gw => pic_gw}/Makefile (100%) rename verification/block/{pic-gw => pic_gw}/test_gateway.py (100%) diff --git a/verification/block/noxfile.py b/verification/block/noxfile.py index 46e84a10101..d146beed982 100644 --- a/verification/block/noxfile.py +++ b/verification/block/noxfile.py @@ -141,7 +141,7 @@ def pic_verify(session, blockName, testName, coverage): @nox.session(tags=["tests"]) -@nox.parametrize("blockName", ["pic-gw"]) +@nox.parametrize("blockName", ["pic_gw"]) @nox.parametrize("testName", ["test_gateway"]) @nox.parametrize("coverage", coverageTypes) def pic_gw_verify(session, blockName, testName, coverage): diff --git a/verification/block/pic-gw/Makefile b/verification/block/pic_gw/Makefile similarity index 100% rename from verification/block/pic-gw/Makefile rename to verification/block/pic_gw/Makefile diff --git a/verification/block/pic-gw/test_gateway.py b/verification/block/pic_gw/test_gateway.py similarity index 100% rename from verification/block/pic-gw/test_gateway.py rename to verification/block/pic_gw/test_gateway.py From de87df8c51f5017fe7445ea0e081b0da29252e84 Mon Sep 17 00:00:00 2001 From: Maciej Kurc Date: Tue, 19 Sep 2023 15:32:53 +0200 Subject: [PATCH 5/5] Adjust GitHub actions CI for microarchitectural tests Signed-off-by: Maciej Kurc --- .github/scripts/convert_coverage_data.sh | 57 +++++------ .github/workflows/ci.yml | 5 + .github/workflows/report-coverage.yml | 6 ++ .github/workflows/test-regression.yml | 14 ++- .github/workflows/test-riscof.yml | 9 +- .github/workflows/test-riscv-dv.yml | 11 +- .github/workflows/test-uarch.yml | 125 +++++++++++++++++++++++ .github/workflows/test-verification.yml | 14 +-- 8 files changed, 183 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/test-uarch.yml diff --git a/.github/scripts/convert_coverage_data.sh b/.github/scripts/convert_coverage_data.sh index c19b853e5d2..bcedde0cd9e 100755 --- a/.github/scripts/convert_coverage_data.sh +++ b/.github/scripts/convert_coverage_data.sh @@ -4,46 +4,41 @@ SELF_DIR="$(dirname $(readlink -f ${BASH_SOURCE[0]}))" . ${SELF_DIR}/common.inc.sh convert_coverage_data(){ - # This function uses verilator_coverage module to convert a coverage .dat file: - # ${DAT_DIR}/coverage.dat - # into an .info file: - # ${RESULTS_DIR}/${FILE_PREFIX}_${COVERAGE}.info + # This function uses verilator_coverage module to convert a coverage .dat + # file(s) into an .info file(s) for further processing. # Args: - # COVERAGE : type of coverage - # DAT_DIR: path to dir containing coverage.dat file - # RESULTS_DIR: path to dir, where .info file will be placed - # FILE_PREFIX: prefix used in the name of coverage_.info - check_args_count $# 4 - COVERAGE=$1 - DAT_DIR=$2 - RESULTS_DIR=$3 - FILE_PREFIX=$4 - echo -e "${COLOR_WHITE}======= convert_coverage_data =======${COLOR_CLEAR}" - echo -e "${COLOR_WHITE}COVERAGE = ${COVERAGE}" - echo -e "${COLOR_WHITE}DAT_DIR = ${DAT_DIR}" - echo -e "${COLOR_WHITE}RESULTS_DIR = ${RESULTS_DIR}" - echo -e "${COLOR_WHITE}FILE_PREFIX = ${FILE_PREFIX}" - echo -e "${COLOR_WHITE}========== ${COVERAGE} coverage ==========${COLOR_CLEAR}" + # DAT_DIR: path to dir containing coverage.dat file(s) + DAT_DIR="${1:-results_verification}" + echo -e "${COLOR_WHITE}======= Parse arguments =======${COLOR_CLEAR}" + echo -e "${COLOR_WHITE}DAT_DIR = ${DAT_DIR}" + echo -e "${COLOR_WHITE}===============================${COLOR_CLEAR}" # Function body - if ! [ -f "${DAT_DIR}/coverage.dat" ]; then - echo -e "${COLOR_WHITE}coverage.dat not found in dir=${DAT_DIR} ${COLOR_RED}FAIL${COLOR_CLEAR}" + FILES=`find ${DAT_DIR} -name "coverage*.dat"` + if [ -z "$FILES" ]; then + echo -e "${COLOR_RED}ERROR: No coverage data files were found${COLOR_CLEAR}" + echo -e "${COLOR_RED}ERROR: Searched directory: `realpath ${DAT_DIR}`${COLOR_CLEAR}" + echo -e "${COLOR_RED}ERROR: convert_coverage_data ended with errors${COLOR_CLEAR}" exit -1 else - mkdir -p ${RESULTS_DIR} - cp ${DAT_DIR}/coverage.dat ${RESULTS_DIR}/${FILE_PREFIX}_${COVERAGE}.dat - verilator_coverage --write-info ${RESULTS_DIR}/${FILE_PREFIX}_${COVERAGE}.info ${RESULTS_DIR}/${FILE_PREFIX}_${COVERAGE}.dat - echo -e "${COLOR_WHITE}Conversion: ${DAT_DIR}/coverage.dat -> ${RESULTS_DIR}/${FILE_PREFIX}_${COVERAGE}.info ${COLOR_GREEN}SUCCEEDED${COLOR_CLEAR}" + for dat_file in ${FILES}; do + info_file=`basename -s .dat ${dat_file}`.info + info_realpath=`realpath \`dirname ${dat_file}\`` + info_file=${info_realpath}/${info_file} + verilator_coverage --write-info ${info_file} ${dat_file} + echo -e "${COLOR_WHITE}Conversion: ${dat_file} -> ${info_file} ${COLOR_GREEN}SUCCEEDED${COLOR_CLEAR}" + done fi } # Example usage -# RESULTS_DIR="results" -# COVERAGE="branch" -# DAT_DIR="." -# FILE_PREFIX="coverage_test" +# DAT_DIR="results_verification" # -# convert_coverage_data $COVERAGE $DAT_DIR $RESULTS_DIR $FILE_PREFIX +# convert_coverage_data $DAT_DIR + +echo -e "${COLOR_WHITE}========== convert_coverage_data ==============${COLOR_CLEAR}" -check_args_count $# 4 convert_coverage_data "$@" + +echo -e "${COLOR_WHITE}convert_coverage_data ${COLOR_GREEN}SUCCEEDED${COLOR_CLEAR}" +echo -e "${COLOR_WHITE}========== convert_coverage_data ==============${COLOR_CLEAR}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ba0f81ba90..b1d8b7c40a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,11 @@ jobs: needs: [Build-Verilator] uses: ./.github/workflows/test-verification.yml + Test-Microarchitectural: + name: Test-Microarchitectural + needs: [Build-Verilator] + uses: ./.github/workflows/test-uarch.yml + Test-RISCV-DV: name: Test-RISCV-DV needs: [Build-Verilator, Build-Spike] diff --git a/.github/workflows/report-coverage.yml b/.github/workflows/report-coverage.yml index e10e07d3d36..a3ceded6b00 100644 --- a/.github/workflows/report-coverage.yml +++ b/.github/workflows/report-coverage.yml @@ -48,6 +48,12 @@ jobs: name: verification_tests_coverage_data path: ./ + - name: Download coverage reports + uses: actions/download-artifact@v3 + with: + name: uarch_tests_coverage_data + path: ./ + - name: Download coverage reports uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/test-regression.yml b/.github/workflows/test-regression.yml index 7692715ced7..a4d22851df6 100644 --- a/.github/workflows/test-regression.yml +++ b/.github/workflows/test-regression.yml @@ -86,17 +86,15 @@ jobs: - name: Prepare coverage data run: | - pushd ${{ github.workspace }} - mkdir -p coverage_${{ matrix.test }} - mv ${TEST_PATH}/coverage.dat coverage_${{ matrix.test }}/ - echo "Prepared coverage data" - .github/scripts/convert_coverage_data.sh ${{ matrix.coverage }} ${{ github.workspace }}/coverage_${{ matrix.test }} ${{ github.workspace }}/results coverage_${{ matrix.test }} - echo "convert_coverage_data.sh exited with RET_CODE = "$? - popd + .github/scripts/convert_coverage_data.sh ${TEST_PATH}/ + echo "convert_coverage_data.sh exited with RET_CODE = "$? + mkdir -p results + mv ${TEST_PATH}/coverage.info \ + results/coverage_${{ matrix.test }}_${{ matrix.coverage }}.info - name: Pack artifacts if: always() uses: actions/upload-artifact@v3 with: name: regression_tests_coverage_data - path: ./results/*.info + path: results/*.info diff --git a/.github/workflows/test-riscof.yml b/.github/workflows/test-riscof.yml index 6ec0b446b14..d9545ea5554 100644 --- a/.github/workflows/test-riscof.yml +++ b/.github/workflows/test-riscof.yml @@ -141,11 +141,10 @@ jobs: - name: Prepare coverage data run: | export PATH=/opt/verilator/bin:$PATH - .github/scripts/convert_coverage_data.sh \ - ${{ matrix.coverage }} \ - riscof/coverage \ - riscof/coverage \ - coverage_riscof + .github/scripts/convert_coverage_data.sh riscof/coverage/ + echo "convert_coverage_data.sh exited with RET_CODE = "$? + mv riscof/coverage/coverage.info \ + riscof/coverage/coverage_riscof_${{ matrix.coverage }}.info - name: Pack artifacts if: always() diff --git a/.github/workflows/test-riscv-dv.yml b/.github/workflows/test-riscv-dv.yml index 0210066cc87..4c3c67982e4 100644 --- a/.github/workflows/test-riscv-dv.yml +++ b/.github/workflows/test-riscv-dv.yml @@ -246,14 +246,11 @@ jobs: - name: Prepare coverage data run: | - mkdir -p coverage_riscv-dv_${{ matrix.test }} - mv ${RV_ROOT}/tools/riscv-dv/work/coverage.dat coverage_riscv-dv_${{ matrix.test }}/ - echo "Prepared coverage data" - .github/scripts/convert_coverage_data.sh \ - ${{ matrix.coverage }} \ - coverage_riscv-dv_${{ matrix.test }} \ - results coverage_riscv-dv_${{ matrix.test }} + .github/scripts/convert_coverage_data.sh ${RV_ROOT}/tools/riscv-dv/work/ echo "convert_coverage_data.sh exited with RET_CODE = "$? + mkdir -p results + mv ${RV_ROOT}/tools/riscv-dv/work/coverage.info \ + results/coverage_riscv-dv_${{ matrix.test }}_${{ matrix.coverage }}.info - name: Pack artifacts if: always() diff --git a/.github/workflows/test-uarch.yml b/.github/workflows/test-uarch.yml new file mode 100644 index 00000000000..b6c9fa4ad98 --- /dev/null +++ b/.github/workflows/test-uarch.yml @@ -0,0 +1,125 @@ +name: VeeR-EL2 Microarchitectural tests + +on: + workflow_call: + +env: + VERILATOR_VERSION: v5.010 + +jobs: + tests: + name: Microarchitectural tests + runs-on: ubuntu-latest + strategy: + matrix: + test: ["block/pic", "block/pic_gw"] + env: + CCACHE_DIR: "/opt/verification/.cache/" + VERILATOR_VERSION: v5.010 + DEBIAN_FRONTEND: "noninteractive" + steps: + - name: Setup repository + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Setup Cache Metadata + id: cache_metadata + run: | + date=$(date +"%Y_%m_%d") + time=$(date +"%Y%m%d_%H%M%S_%N") + cache_verilator_restore_key=cache_verilator_ + cache_verilator_key=${cache_verilator_restore_key}${{ env.VERILATOR_VERSION }} + cache_test_restore_key=uarch_${{ matrix.test }}_${{ matrix.coverage }}_ + cache_test_key=${cache_test_restore_key}${time} + + echo "date=$date" | tee -a "$GITHUB_ENV" + echo "time=$time" | tee -a "$GITHUB_ENV" + echo "cache_verilator_restore_key=$cache_verilator_restore_key" | tee -a "$GITHUB_ENV" + echo "cache_verilator_key=$cache_verilator_key" | tee -a "$GITHUB_ENV" + echo "cache_test_restore_key=$cache_test_restore_key" | tee -a "$GITHUB_ENV" + echo "cache_test_key=$cache_test_key" | tee -a "$GITHUB_ENV" + + - name: Restore verilator cache + id: cache-verilator-restore + uses: actions/cache/restore@v3 + with: + path: | + /opt/verilator + /opt/verilator/.cache + key: ${{ env.cache_verilator_key }} + restore-keys: ${{ env.cache_verilator_restore_key }} + + - name: Setup tests cache + uses: actions/cache@v3 + id: cache-test-setup + with: + path: | + ${{ env.CCACHE_DIR }} + key: ${{ env.cache_test_key }} + restore-keys: ${{ env.cache_test_restore_key }} + + - name: Install prerequisities + run: | + sudo apt -qqy update && sudo apt -qqy --no-install-recommends install \ + autoconf automake autotools-dev \ + bc bison build-essential \ + ccache cpanminus curl \ + flex \ + gawk gcc-riscv64-unknown-elf git gperf \ + help2man \ + libexpat-dev libfl-dev libfl2 libgmp-dev \ + libmpc-dev libmpfr-dev libpython3-all-dev libtool \ + ninja-build \ + patchutils python3 python3-dev python3-pip \ + texinfo \ + zlib1g zlib1g-dev + sudo cpanm Bit::Vector + + - name: Setup environment + run: | + echo "/opt/verilator/bin" >> $GITHUB_PATH + RV_ROOT=`pwd` + echo "RV_ROOT=$RV_ROOT" >> $GITHUB_ENV + PYTHONUNBUFFERED=1 + echo "PYTHONUNBUFFERED=$PYTHONUNBUFFERED" >> $GITHUB_ENV + + TEST_TYPE=`echo ${{ matrix.test }} | cut -d'/' -f1` + TEST_NAME=`echo ${{ matrix.test }} | cut -d'/' -f2` + TEST_PATH=$RV_ROOT/verification/${TEST_TYPE} + + echo "TEST_TYPE=$TEST_TYPE" >> $GITHUB_ENV + echo "TEST_NAME=$TEST_NAME" >> $GITHUB_ENV + echo "TEST_PATH=$TEST_PATH" >> $GITHUB_ENV + + pip3 install meson nox + + - name: Run ${{ matrix.test }} + run: | + pushd ${TEST_PATH} + nox -s ${TEST_NAME}_verify + popd + + - name: Prepare coverage data + run: | + export PATH=/opt/verilator/bin:$PATH + .github/scripts/convert_coverage_data.sh ${TEST_PATH}/${TEST_NAME}/ + echo "convert_coverage_data.sh exited with RET_CODE = "$? + mkdir -p results + mv ${TEST_PATH}/${TEST_NAME}/*.info results/ + + # Prefix coverage results + pushd results + for OLD_NAME in *.info; do + NEW_NAME=${OLD_NAME/coverage_/coverage_${TEST_NAME}_} + echo "renaming '${OLD_NAME}' to '${NEW_NAME}'" + mv ${OLD_NAME} ${NEW_NAME} + done + popd + + - name: Upload coverage data artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: uarch_tests_coverage_data + path: ./results/*.info diff --git a/.github/workflows/test-verification.yml b/.github/workflows/test-verification.yml index 2059b41490e..b54a0c22ded 100644 --- a/.github/workflows/test-verification.yml +++ b/.github/workflows/test-verification.yml @@ -113,12 +113,12 @@ jobs: - name: Prepare coverage data run: | - pushd ${{ github.workspace }} - mkdir -p coverage_${{ matrix.test }} - mv ${TEST_PATH}/coverage.dat coverage_${{ matrix.test }}/ - .github/scripts/convert_coverage_data.sh ${{ matrix.COVERAGE }} ${{ github.workspace }}/coverage_${{ matrix.test }} ${{ github.workspace }}/results coverage_${{ matrix.test }} - echo "convert_coverage_data.sh exited with RET_CODE = "$? - popd + export PATH=/opt/verilator/bin:$PATH + .github/scripts/convert_coverage_data.sh ${TEST_PATH}/coverage.dat + echo "convert_coverage_data.sh exited with RET_CODE = "$? + mkdir -p results + mv ${TEST_PATH}/coverage.info \ + results/coverage_${{ matrix.test }}_${{ matrix.coverage }}.info - name: Upload pytest-html artifacts if: always() @@ -133,4 +133,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: verification_tests_coverage_data - path: ./results/*.info + path: results/*.info