diff --git a/tb/driver.py b/tb/driver.py index 4dff21f..c5dc0ba 100644 --- a/tb/driver.py +++ b/tb/driver.py @@ -7,7 +7,6 @@ from cocotb.handle import SimHandleBase from interface.bus.master import Master -from interface.utils import BLOCK_ADDR, CTRL_ADDR, DIGEST_ADDR, align class Driver: @@ -25,6 +24,9 @@ def __init__( byte_align: int, block_width: int, digest_width: int, + block_addrs: List[int], + digest_addrs: List[int], + ctrl_addr: int, bus_mapping: Optional[Dict[str, str]] = None, ): self.entity = entity @@ -38,14 +40,9 @@ def __init__( self.block_width = block_width self.digest_width = digest_width - self.block_addrs: List[int] = [ - align(addr, byte_align) + BLOCK_ADDR - for addr in range(0, block_width, data_width) - ] - self.digest_addrs: List[int] = [ - align(addr, byte_align) + DIGEST_ADDR - for addr in range(0, digest_width, data_width) - ] + self.block_addrs: List[int] = block_addrs + self.digest_addrs: List[int] = digest_addrs + self.ctrl_addr: int = ctrl_addr async def write_block(self, block: int) -> None: mask = 2**self.data_width - 1 @@ -75,18 +72,18 @@ async def read_digest(self) -> int: async def enable(self, last_block=False) -> None: # write enable or last_block + enable value = 0x1 if not last_block else 0x21 - await self.bus.write(value=value, address=CTRL_ADDR) + await self.bus.write(value=value, address=self.ctrl_addr) async def disable(self) -> None: - await self.bus.write(value=0x0, address=CTRL_ADDR) + await self.bus.write(value=0x0, address=self.ctrl_addr) async def reset(self) -> None: - await self.bus.write(value=0x2, address=CTRL_ADDR) + await self.bus.write(value=0x2, address=self.ctrl_addr) async def read_hold(self) -> int: - ctrlreg = await self.bus.read(address=CTRL_ADDR) + ctrlreg = await self.bus.read(address=self.ctrl_addr) return ctrlreg & 0x8 async def read_valid(self) -> int: - ctrlreg = await self.bus.read(address=CTRL_ADDR) + ctrlreg = await self.bus.read(address=self.ctrl_addr) return ctrlreg & 0x10 diff --git a/tb/interface/bus/master.py b/tb/interface/bus/master.py index 3b4e12d..a8e1c11 100644 --- a/tb/interface/bus/master.py +++ b/tb/interface/bus/master.py @@ -122,7 +122,7 @@ async def read(self, address: int) -> BinaryValue: self.bus.reqstrobe.value = int("0" * len(self.bus.reqstrobe), 2) while True: - if self.bus.rspvalid.value: + if self.bus.rspvalid.value or self.bus.rsperror.value: break await RisingEdge(self.clock) diff --git a/tb/interface/makefile b/tb/interface/makefile index dfff3b8..4796288 100644 --- a/tb/interface/makefile +++ b/tb/interface/makefile @@ -30,3 +30,6 @@ EXTRA_ARGS += --trace --trace-structs include $(shell cocotb-config --makefiles)/Makefile.sim +clean:: + rm -f *.xml + diff --git a/tb/interface/test_simple_interface.py b/tb/interface/test_simple_interface.py index 1f25e1e..5cdd60b 100644 --- a/tb/interface/test_simple_interface.py +++ b/tb/interface/test_simple_interface.py @@ -3,23 +3,33 @@ # SPDX-License-Identifier: Apache-2.0 import os -from secrets import choice, randbits +from secrets import randbits from typing import Dict, List import cocotb import pytest +import vsc from bus.master import Master from cocotb.clock import Clock from cocotb.regression import TestFactory -from cocotb.result import TestSuccess from cocotb.runner import Simulator, get_runner from cocotb.triggers import ClockCycles, RisingEdge, Timer -from utils import BLOCK_ADDR, CTRL_ADDR, DIGEST_ADDR, MAPPING, align +from utils import ( + BLOCK_ADDR, + CTRL_ADDR, + DIGEST_ADDR, + MAPPING, + Request, + RequestCovergroup, + align, +) ITERATIONS = int(os.getenv("ITERATIONS", 10)) SIM = os.getenv("SIM", "verilator") SIM_BUILD = os.getenv("SIM_BUILD", "sim_build") WAVES = os.getenv("WAVES", "0") +VSC = os.getenv("VSC", "0") +VSC_FILE = os.getenv("VSC_FILE", "vsc.xml") if cocotb.simulator.is_running(): DATA_WIDTH = int(cocotb.top.DataWidth) @@ -28,21 +38,34 @@ BYTE_ALIGN = int(cocotb.top.ByteAlign) DIGEST_WIDTH = int(cocotb.top.DigestWidth) + ADDR_STEP: int = 8 if BYTE_ALIGN else 32 -@cocotb.coroutine -async def init(dut): - """Initialize input signals value""" + DIGEST_REGS_ADDR: List[int] = [ + align(addr, ADDR_STEP) + DIGEST_ADDR + for addr in range(0, DIGEST_WIDTH, DATA_WIDTH) + ] - """ + BLOCK_REGS_ADDR: List[int] = [ + align(addr, ADDR_STEP) + BLOCK_ADDR + for addr in range(0, BLOCK_WIDTH, DATA_WIDTH) + ] - This would be the correct way to do it. - But verilator does not support it so the list must be maintained by hand. + VALID_REGS_ADDR: List[int] = DIGEST_REGS_ADDR + BLOCK_REGS_ADDR + [CTRL_ADDR] - for signal in dir(sha): - if signal.endswith('_i') and signal != "clk_i": - dut._id(signal, extended=False).value = 0 - - """ + request: Request = Request(ADDR_WIDTH, DATA_WIDTH, ADDR_STEP) + request_covergroup: RequestCovergroup = RequestCovergroup( + ADDR_WIDTH, + DATA_WIDTH, + ADDR_STEP, + BLOCK_REGS_ADDR, + DIGEST_REGS_ADDR, + VALID_REGS_ADDR, + ) + + +@cocotb.coroutine +async def init(dut): + """Initialize input signals value""" dut.reqdata_i.value = 0 dut.reqaddr_i.value = 0 @@ -59,356 +82,157 @@ async def init(dut): await Timer(1, units="ns") -def sim_strobe_write(prevval: int, wrval: int, strobe: int, databytes: int) -> int: - regval: int = 0 - data: int = 0 - - for b in range(0, databytes): - data = wrval if strobe & (1 << b) else prevval - regval |= data & (0xFF << b * 8) - - return regval +def overwwrite(prev_value: int, write_value: int, strobe: int) -> int: + """Overwrite a register value with a new value""" + exp_value: int = 0 + data: int = 0 -async def block_registers_access(dut, test_id) -> None: - """ - - Write operation is performed with random data and valid address. - The address is read back and it is expected to find the previously - written data. - - """ - - await init(dut) + for b in range(0, DATA_WIDTH >> 3): + data = write_value if strobe & (1 << b) else prev_value + exp_value |= data & (0xFF << b * 8) - REGS_ADDR: List[int] = [ - align(addr, BYTE_ALIGN) + BLOCK_ADDR - for addr in range(0, BLOCK_WIDTH, DATA_WIDTH) - ] + return exp_value - data: int = randbits(DATA_WIDTH) - regaddr: int = choice(REGS_ADDR) - cocotb.start_soon(Clock(dut.clk_i, period=10, units="ns").start()) +def pseudowrite( + prev_value: int, write_value: int, addr: int, strobe: int, err: bool +) -> int: + """Simulate a write operation in the register interface based on the register previous value and request parameters""" - master: Master = Master(dut, name=None, clock=dut.clk_i, mapping=MAPPING) + # Return early if we know it's an invalid address + if err: + return 0 - await Timer(35, units="ns") + # Function is split by address ranges + if addr in DIGEST_REGS_ADDR: + return prev_value - # Turn off reset - dut.rst_ni.value = 1 + elif addr in BLOCK_REGS_ADDR: + return overwwrite(prev_value, write_value, strobe) - await ClockCycles(dut.clk_i, 5) - - assert dut.rst_ni.value == 1, f"{dut.name} is still under reset" + elif addr == CTRL_ADDR: + # If strobe[0] is not set, nothing is written + if not (strobe & 0x1): + return prev_value - dut._log.info(f"Register access with data = {data:#x} at address = {regaddr:#x}.") + # Mask for control register + ctrl_mask: int = 0b100001 - await master.write(address=regaddr, value=data) - dut._log.debug(f"Write: {data:#x} at address {regaddr:#x}") + # If reset bit is written, register is cleared + if (write_value >> 1) & 0x1: + return 0 - await ClockCycles(dut.clk_i, 5) + return overwwrite(prev_value, write_value, strobe) & ctrl_mask - regval = await master.read(address=regaddr) - regval = int(regval.value) - dut._log.debug(f"Read: {regval:#x} at address {regaddr:#x}") - assert regval == data, ( - f"Test {test_id}:", - f"Expected {data:#x} at address {regaddr:#x}, " f"read {regval:#x}", - ) - - -async def invalid_block_registers_access(dut, test_id) -> None: - """Error response from slave interface - - Write operation is performed with random data and invalid address. - It is checked whether an error response is sent back. - - """ +@cocotb.test() +async def toggle_reset(dut) -> None: + """Toggle reset signal (sanity check)""" await init(dut) - REGS_ADDR: List[int] = [ - align(addr, BYTE_ALIGN) + BLOCK_ADDR - for addr in range(0, BLOCK_WIDTH, DATA_WIDTH) - ] - - maxaddr: int = BLOCK_WIDTH >> 3 if BYTE_ALIGN == 1 else BLOCK_WIDTH >> 5 - INVALID_REGS_ADDR: List[int] = [ - addr for addr in range(maxaddr, maxaddr + BLOCK_ADDR) if addr not in REGS_ADDR - ] - - if not INVALID_REGS_ADDR: - raise TestSuccess( - f"No invalid addresses for BlockWidth = {BLOCK_WIDTH}, ", - f"DataWidth = {DATA_WIDTH} and ByteAlign = {BYTE_ALIGN}", - ) - - data: int = 0x55 - regaddr: int = choice(INVALID_REGS_ADDR) - - cocotb.start_soon(Clock(dut.clk_i, period=10, units="ns").start()) - - master: Master = Master(dut, name=None, clock=dut.clk_i, mapping=MAPPING) - - await Timer(35, units="ns") - # Turn off reset dut.rst_ni.value = 1 - await ClockCycles(dut.clk_i, 5) - - assert dut.rst_ni.value == 1, f"{dut.name} is still under reset" - - dut._log.info( - f"Invalid register access with data = {data:#x} at address = {regaddr:#x}." - ) - - await master.write(address=regaddr, value=data) - dut._log.debug(f"Write: {data:#x} at address {regaddr:#x}") - - # Wait next cycle for response - await RisingEdge(dut.clk_i) - - error: int = int(dut.rsperror_o.value) - - assert ( - error == 1 - ), f"Test {test_id}: Expected an error response at address {regaddr:#x}." - - await ClockCycles(dut.clk_i, 5) - - -async def strobe_block_registers_accesses(dut, test_id) -> None: - """Access the block registers with random strobe value - - Write operations are performed with random data and valid addresses. - This tests that only valid bytes are written. - - """ - - await init(dut) - - REGS_ADDR: List[int] = [ - align(addr, BYTE_ALIGN) + BLOCK_ADDR - for addr in range(0, BLOCK_WIDTH, DATA_WIDTH) - ] - - data: int = randbits(DATA_WIDTH) - validbytes: int = randbits(DATA_WIDTH >> 3) - regaddr: int = choice(REGS_ADDR) - - cocotb.start_soon(Clock(dut.clk_i, period=10, units="ns").start()) - - master: Master = Master(dut, name=None, clock=dut.clk_i, mapping=MAPPING) - await Timer(35, units="ns") - - # Turn off reset - dut.rst_ni.value = 1 - - await ClockCycles(dut.clk_i, 5) - - assert dut.rst_ni.value == 1, f"{dut.name} is still under reset" - - # Read register before write operation - prev_regval = await master.read(address=regaddr) - - expval: int = sim_strobe_write(prev_regval.value, data, validbytes, DATA_WIDTH >> 3) - - dut._log.info( - f"Register access with data = {data:#x} at address = {regaddr:#x}, with strobe = {validbytes:#x}." - ) - - # Write to the register - await master.write(address=regaddr, value=data, strobe=validbytes) - - await ClockCycles(dut.clk_i, 5) - - regval = await master.read(address=regaddr) - regval = int(regval.value) - - dut._log.debug(f"Read: {regval:#x} at address {regaddr:#x}") - - assert regval == expval, ( - f"Test {test_id}:", - f"Expected {expval:#x} at address {regaddr:#x}, read {regval:#x}", - ) - - -async def digest_register(dut, test_id) -> None: - """Digest register access - - Read operations are performed on the digest register. - - """ - - await init(dut) - - # Give a random value to the digest input - digest = randbits(DIGEST_WIDTH) - - REGS_ADDR: List[int] = [ - align(addr, BYTE_ALIGN) + DIGEST_ADDR - for addr in range(0, DIGEST_WIDTH, DATA_WIDTH) - ] - - SHL = 3 if BYTE_ALIGN else 5 - - regaddr: int = choice(REGS_ADDR) - - nbits = (regaddr & 0xFF) << SHL - mask = ((1 << DATA_WIDTH) - 1) << nbits - expval = (digest & mask) >> nbits - - cocotb.start_soon(Clock(dut.clk_i, period=10, units="ns").start()) - - master: Master = Master(dut, name=None, clock=dut.clk_i, mapping=MAPPING) - - await Timer(35, units="ns") - - # Turn off reset - dut.rst_ni.value = 1 - - await ClockCycles(dut.clk_i, 5) - assert dut.rst_ni.value == 1, f"{dut.name} is still under reset" - dut.digest_i.value = digest - - await ClockCycles(dut.clk_i, 5) - - # Read operations on the digest register - - regval = await master.read(address=regaddr) - regval = int(regval.value) - dut._log.debug(f"Digest: {digest:#x} with mask {mask:#x}") - dut._log.info(f"Register read with address = {regaddr:#x}.") +async def register_accesses(dut, id) -> None: + """Register accesses with randomly contrained data""" - assert regval == expval + # Randomize and sample the request + request.randomize() + request_covergroup.sample(request) - assert regval == expval, ( - f"Test {test_id}:", - f"Expected {expval:#x} at address {regaddr:#x}, " f"read {regval:#x}", - ) - - -# Automatic tests generation depending on requested number of iterations - -cocotb_tests = [ - block_registers_access, - invalid_block_registers_access, - strobe_block_registers_accesses, - digest_register, -] - -for func_test in cocotb_tests: - factory = TestFactory(func_test) - factory.add_option(name="test_id", optionlist=range(ITERATIONS)) - factory.generate_tests() + # Gather request parameters + data: int = request.data + be: int = request.be + addr: int = request.addr + # Define expected results based on request parameters + err: bool = addr not in VALID_REGS_ADDR + valid: bool = addr in VALID_REGS_ADDR -# This test only needs one iteration - - -@cocotb.test() -async def control_register(dut) -> None: - """Control register accesses - - Write and read operations are performed on the control register. - - """ - - await init(dut) + # Apply a random value to the read only digest register + dut.digest_i.value = randbits(DIGEST_WIDTH) + # Start clock and create Master interface cocotb.start_soon(Clock(dut.clk_i, period=10, units="ns").start()) - master: Master = Master(dut, name=None, clock=dut.clk_i, mapping=MAPPING) - await Timer(35, units="ns") - - # Turn off reset - dut.rst_ni.value = 1 - - await ClockCycles(dut.clk_i, 5) - - assert dut.rst_ni.value == 1, f"{dut.name} is still under reset" - - # Enable computation - - await master.write(address=CTRL_ADDR, value=0x1) - - await ClockCycles(dut.clk_i, 5) - - regval = await master.read(address=CTRL_ADDR) - regval = int(regval.value) - - assert regval == 0x1 - assert dut.enable_hash_o.value == 0x1 - - dut._log.debug(">> Hash enabled") - - # Last block signal - - await master.write(address=CTRL_ADDR, value=0x21) - - await ClockCycles(dut.clk_i, 5) - - regval = await master.read(address=CTRL_ADDR) - regval = int(regval.value) - - assert regval == 0x21 - assert dut.last_block_o.value == 0x1 - - dut._log.debug(">> Hash enabled") - - # Reset computation + # Read register before write operation + prev_value = await master.read(address=addr) + exp_value: int = pseudowrite(prev_value.value, data, addr, be, err) - await master.write(address=CTRL_ADDR, value=0x2) + # Write request + await master.write(address=addr, value=data, strobe=be) + dut._log.debug(f"Write: {data:#x} at address {addr:#x} with byte enable {be:b}") + # Wait next cycle for response await RisingEdge(dut.clk_i) - assert dut.reset_hash_o.value == 0x1 + # Check response status + rsperr: bool = bool(dut.rsperror_o.value) + rspvalid: bool = bool(dut.rspvalid_o.value) - regval = await master.read(address=CTRL_ADDR) - assert regval == 0x0 - assert dut.enable_hash_o.value == 0x0 + # Error and valid should never be asserted in the same cycle + assert rsperr & rspvalid == False - dut._log.debug(">> Hash reset") + # Error response + assert ( + rsperr == err + ), f"Test {id}: Incorrect response error at {addr:#x}: expected {err}." - # Deassert enable with idle or hold + # Valid response + assert ( + rspvalid == valid + ), f"Test {id}: Incorrect valid response at {addr:#x}: expected {valid}." - await master.write(address=CTRL_ADDR, value=0x1) + # Read value back + read_value = await master.read(address=addr) + read_value = int(read_value.value) if valid else 0 - await ClockCycles(dut.clk_i, 5) + dut._log.debug(f"Read: {read_value:#x} at address {addr:#x}") - dut.idle_i.value = 0x1 + # Compare read value and expected value + assert read_value == exp_value, ( + f"Test {id}:", + f"Expected {exp_value:#x} at address {addr:#x}, read {read_value:#x}", + ) await ClockCycles(dut.clk_i, 2) - assert dut.enable_hash_o.value == 0x0 + # Export coverage on last iteration + if VSC == "1" and id == (ITERATIONS): + vsc.write_coverage_db(f"{VSC_FILE}") + dut._log.info(f"Coverage file written in {VSC_FILE}") - dut._log.debug(">> Hash deasserted by idle signal") + +# Automatic tests generation depending on requested number of iterations +factory = TestFactory(register_accesses) +factory.add_option(name="id", optionlist=range(1, ITERATIONS + 1)) +factory.generate_tests() -@pytest.mark.parametrize("DataWidth", ["8", "16", "32", "64", "128"]) +@pytest.mark.parametrize("DataWidth", ["16", "32", "64"]) @pytest.mark.parametrize("BlockWidth", ["512", "1024"]) @pytest.mark.parametrize("ByteAlign", ["1'b0", "1'b1"]) -@pytest.mark.parametrize("DigestWidth", ["224", "256"]) -def test_sha_regs(DataWidth, BlockWidth, ByteAlign, DigestWidth): +@pytest.mark.parametrize("DigestWidth", ["224", "256", "384", "512"]) +def test_interface_regs(DataWidth, BlockWidth, ByteAlign, DigestWidth): """Run cocotb tests on sha1 registers for different combinations of parameters. Args: - DataWidth: Data bus width. - BlockWidth: Width of the block to compute - ByteAlign: Whether we want an alignment on bytes or words. + DataWidth: Data bus width. + BlockWidth: Width of the block to compute. + ByteAlign: Whether we want an alignment on bytes or words. + DigestWidth: Width of the final digest. """ # skip test if there is an invalid combination of parameters - if ByteAlign == "1'b0" and DataWidth in ["8", "16"]: + if ByteAlign == "1'b0" and DataWidth == "16": pytest.skip( f"Invalid combination: ByteAlign = {ByteAlign} and DataWidth = {DataWidth}" ) diff --git a/tb/interface/utils.py b/tb/interface/utils.py index f433487..b338c85 100644 --- a/tb/interface/utils.py +++ b/tb/interface/utils.py @@ -1,4 +1,10 @@ -from typing import Dict +# Copyright 2023 - cryptopen contributors +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List + +import vsc # Base addresses CTRL_ADDR = 0x000 @@ -20,6 +26,52 @@ } -def align(addr: int, bytealign: bool): - step = 8 if bytealign else 32 +def align(addr: int, step: int): return int(addr / step) + + +@vsc.randobj +class Request(object): + def __init__(self, addr_width: int, data_width: int, step: int): + self.addr = vsc.rand_bit_t(addr_width) + self.data = vsc.rand_bit_t(data_width) + self.be = vsc.rand_bit_t(data_width >> 3) + self.step = step + + @vsc.constraint + def addr_c(self): + self.addr <= 0x2FF + self.addr % self.step == 0 + + +@vsc.covergroup +class RequestCovergroup(object): + def __init__( + self, + addr_width: int, + data_width: int, + step: int, + block_addrs: List[int], + digest_addrs: List[int], + valid_addrs: List[int], + ): + # Define the parameters accepted by the sample function + self.with_sample(dict(req=Request(addr_width, data_width, step))) + + invalid_addrs: List[int] = [ + addr for addr in range(0x000, 0x2FF, step) if addr not in valid_addrs + ] + + self.addr_cp = vsc.coverpoint( + self.req.addr, + bins={ + "CtrlRegAddr": vsc.bin(0x00), + "BlockRegsAddr": vsc.bin(*block_addrs), + "DigestRegsAddr": vsc.bin(*digest_addrs), + "Other": vsc.bin(*invalid_addrs), + }, + ) + + self.be_cp = vsc.coverpoint( + self.req.be, bins={"ByteEnable": vsc.bin([0x0, 0xFF])} + ) diff --git a/tb/test_tops.py b/tb/test_tops.py index 4a2b4e6..0681d95 100644 --- a/tb/test_tops.py +++ b/tb/test_tops.py @@ -15,6 +15,7 @@ from cocotb.triggers import ClockCycles, RisingEdge, Timer from driver import Driver +from interface.utils import BLOCK_ADDR, CTRL_ADDR, DIGEST_ADDR, align from sha1.model.sha1_model import sha1 from sha2.model.sha256_model import sha256 from sha2.model.sha512_model import sha512 @@ -31,6 +32,18 @@ BYTE_ALIGN = int(cocotb.top.ByteAlign) DIGEST_WIDTH = int(cocotb.top.DigestWidth) + ADDR_STEP: int = 8 if BYTE_ALIGN else 32 + + DIGEST_REGS_ADDR: List[int] = [ + align(addr, ADDR_STEP) + DIGEST_ADDR + for addr in range(0, DIGEST_WIDTH, DATA_WIDTH) + ] + + BLOCK_REGS_ADDR: List[int] = [ + align(addr, ADDR_STEP) + BLOCK_ADDR + for addr in range(0, BLOCK_WIDTH, DATA_WIDTH) + ] + MAPPING: Dict[str, str] = { "reqdata": "sha_s_reqdata_i", "reqaddr": "sha_s_reqaddr_i", @@ -116,6 +129,9 @@ async def run_one_block_message(dut) -> None: byte_align=BYTE_ALIGN, block_width=BLOCK_WIDTH, digest_width=DIGEST_WIDTH, + block_addrs=BLOCK_REGS_ADDR, + digest_addrs=DIGEST_REGS_ADDR, + ctrl_addr=CTRL_ADDR, bus_mapping=MAPPING, ) @@ -185,6 +201,9 @@ async def run_two_block_message(dut) -> None: byte_align=BYTE_ALIGN, block_width=BLOCK_WIDTH, digest_width=DIGEST_WIDTH, + block_addrs=BLOCK_REGS_ADDR, + digest_addrs=DIGEST_REGS_ADDR, + ctrl_addr=CTRL_ADDR, bus_mapping=MAPPING, ) @@ -263,6 +282,9 @@ async def run_random_message(dut, message) -> None: byte_align=BYTE_ALIGN, block_width=BLOCK_WIDTH, digest_width=DIGEST_WIDTH, + block_addrs=BLOCK_REGS_ADDR, + digest_addrs=DIGEST_REGS_ADDR, + ctrl_addr=CTRL_ADDR, bus_mapping=MAPPING, )