"""
Testbench for the ascon module.
This module tests the ascon top level module by comparing the
output of the Python implementation with the verilog implementation.
@author: Timothée Charrier
"""
from __future__ import annotations
import os
import random
import sys
from pathlib import Path
from typing import TYPE_CHECKING
import cocotb
from ascon_model import AsconModel, convert_output_to_str
from cocotb.triggers import ClockCycles
from cocotb_tools.runner import get_runner
if TYPE_CHECKING:
from cocotb.handle import HierarchyObject
from cocotb_tools.runner import Runner
# Add the directory containing the utils.py file to the Python path
sys.path.insert(0, str(object=(Path(__file__).parent.parent).resolve()))
from cocotb_utils import (
generate_coverage_report_questa,
generate_coverage_report_verilator,
get_dut_state,
initialize_dut,
toggle_signal,
)
INIT_INPUTS: dict[str, int] = {
"i_start": 0,
"i_data_valid": 0,
"i_data": 0x0000000000000000,
"i_key": 0x00000000000000000000000000000000,
"i_nonce": 0x00000000000000000000000000000000,
}
PLAINTEXT: list[int] = [
0x3230323280000000,
0x446576656C6F7070,
0x657A204153434F4E,
0x20656E206C616E67,
0x6167652056484480,
]
[docs]
@cocotb.test()
async def reset_dut_test(dut: HierarchyObject) -> None:
"""
Test the DUT's behavior during reset.
Verifies that the output is correctly reset and remains stable.
Parameters
----------
dut : HierarchyObject
The device under test (DUT).
Raises
------
RuntimeError
If the DUT fails to reset.
"""
try:
# Expected outputs
expected_outputs = {
"o_state": [0] * 5,
"o_tag": 0,
"o_cipher": 0,
"o_valid_cipher": 0,
"o_done": 0,
}
# Initialize the DUT
await initialize_dut(dut=dut, inputs=INIT_INPUTS, outputs=expected_outputs)
except Exception as e:
dut_state: dict = get_dut_state(dut=dut)
formatted_dut_state: str = "\n".join(
[f"{key}: {value}" for key, value in dut_state.items()],
)
error_message: str = (
f"Failed in reset_dut_test with error: {e}\n"
f"DUT state at error:\n"
f"{formatted_dut_state}"
)
raise RuntimeError(error_message) from e
[docs]
@cocotb.test()
async def ascon_top_test(dut: HierarchyObject) -> None:
"""
Test the ascon top module.
Verifies that the output is correctly computed.
Parameters
----------
dut : HierarchyObject
The device under test (DUT).
Raises
------
RuntimeError
If the DUT fails to compute the correct output.
"""
try:
# Reset the DUT
await reset_dut_test(dut=dut)
# Define the ASCON inputs
inputs: dict[str, int] = {
"i_sys_enable": 1,
"i_start": 0,
"i_data_valid": 0,
"i_data": 0x80400C0600000000,
"i_key": 0x000102030405060708090A0B0C0D0E0F,
"i_nonce": 0x000102030405060708090A0B0C0D0E0F,
}
output_cipher: list[int] = [0] * 4
# Define the ASCON model
ascon_model = AsconModel(dut=dut, inputs=inputs, plaintext=PLAINTEXT)
output_dict: dict[str, str] = ascon_model.ascon128(inputs=inputs)
# Set the inputs
for key, value in inputs.items():
dut.__getattr__(name=key).value = value
# Wait for few clock cycles
await ClockCycles(signal=dut.clock, num_cycles=10)
# Send the start signal
await toggle_signal(dut=dut, signal_dict={"i_start": 1}, verbose=False)
#
# Initialisation phase
#
# Wait at least 12 clock cycles (12 rounds permutation)
await ClockCycles(signal=dut.clock, num_cycles=random.randint(13, 20))
#
# Associated Data phase
#
# Update i_data
dut.i_data.value = PLAINTEXT[0]
# Set i_data_valid to 1
await toggle_signal(dut=dut, signal_dict={"i_data_valid": 1}, verbose=False)
# Wait at least 6 clock cycles (6 rounds permutation)
await ClockCycles(signal=dut.clock, num_cycles=random.randint(7, 10))
#
# Plaintext phase
#
# Process blocks 1-3
for i in range(1, 4):
# Update i_data
dut.i_data.value = PLAINTEXT[i]
# Set i_data_valid to 1
await toggle_signal(dut=dut, signal_dict={"i_data_valid": 1}, verbose=False)
# Get the cipher
await dut.o_valid_cipher.rising_edge
assert dut.o_valid_cipher.value == 1, "Cipher is not valid"
# For some reason, the o_cipher signal is not stable when the o_valid_cipher
# signal is high. This probably comes from the fact that the o_cipher signal
# is updated at the rising edge of the clock while the o_valid_cipher signal
# is updated at the falling edge of the clock. This issue only arises with
# QuestaSim and not with Verilator.
await ClockCycles(signal=dut.clock, num_cycles=5)
output_cipher[i - 1] = int(dut.o_cipher.value)
# Wait at least 12 clock cycles (12 rounds permutation)
await ClockCycles(
signal=dut.clock,
num_cycles=random.randint(13 - 5, 20 - 5),
)
# Final phase
# Update i_data
dut.i_data.value = PLAINTEXT[4]
# Set i_data_valid to 1
await toggle_signal(dut=dut, signal_dict={"i_data_valid": 1}, verbose=False)
# Get the cipher
await dut.o_valid_cipher.rising_edge
assert dut.o_valid_cipher.value == 1, "Cipher is not valid"
# Same issue as above
await ClockCycles(signal=dut.clock, num_cycles=5)
output_cipher[3] = int(dut.o_cipher.value)
# Wait for the o_done signal
await dut.o_done.rising_edge
await ClockCycles(signal=dut.clock, num_cycles=10)
#
# Check the output
#
# Get output state, tag, and cipher
output_dut_dict: dict[str, str] = convert_output_to_str(
dut=dut,
cipher=output_cipher,
)
# Log the DUT output
dut._log.info("Model Output State : " + output_dict["o_state"])
dut._log.info("Model Output Tag : " + output_dict["o_tag"])
dut._log.info("Model Output Cipher: " + output_dict["o_cipher"] + "\n")
dut._log.info("DUT Output State : " + output_dut_dict["o_state"])
dut._log.info("DUT Output Tag : " + output_dut_dict["o_tag"])
dut._log.info("DUT Output Cipher : " + output_dut_dict["o_cipher"] + "\n")
# Check the output
assert output_dict["o_state"] == output_dut_dict["o_state"]
assert output_dict["o_tag"] == output_dut_dict["o_tag"]
assert output_dict["o_cipher"] == output_dut_dict["o_cipher"]
except Exception as e:
dut_state = get_dut_state(dut=dut)
formatted_dut_state: str = "\n".join(
[f"{key}: {value}" for key, value in dut_state.items()],
)
error_message: str = (
f"Failed in ascon_top_test with error: {e}\n"
f"DUT state at error:\n"
f"{formatted_dut_state}"
)
raise RuntimeError(error_message) from e
[docs]
def test_ascon() -> None:
"""
Function Invoked by the test runner to execute the tests.
Raises
------
RuntimeError
If the test fails to build or run.
"""
# Define the simulator to use
default_simulator: str = "verilator"
# Define the top-level library and entity
library: str = "lib_rtl"
entity: str = "ascon"
# Default Generics Configuration
generics: dict[str, str] = {}
# Define paths
rtl_path: Path = (Path(__file__).parent.parent.parent / "rtl/").resolve()
build_dir: Path = Path("sim_build")
# Define the coverage file and output folder
output_folder: Path = build_dir / "coverage_report"
if default_simulator == "questa":
ucdb_file: Path = build_dir / f"{entity}_coverage.ucdb"
elif default_simulator == "verilator":
dat_file: Path = build_dir / "coverage.dat"
# Define the sources
sources: list[str] = [
f"{rtl_path}/ascon_pkg.sv",
f"{rtl_path}/addition_layer/addition_layer.sv",
f"{rtl_path}/substitution_layer/sbox.sv",
f"{rtl_path}/substitution_layer/substitution_layer.sv",
f"{rtl_path}/diffusion_layer/diffusion_layer.sv",
f"{rtl_path}/xor/xor_begin.sv",
f"{rtl_path}/xor/xor_end.sv",
f"{rtl_path}/permutation/permutation.sv",
f"{rtl_path}/fsm/ascon_fsm.sv",
f"{rtl_path}/ascon/ascon.sv",
]
# Define the build and test arguments
if default_simulator == "questa":
build_args: list[str] = [
"-svinputport=net",
"-O5",
"+cover=sbfec",
]
test_args: list[str] = [
"-coverage",
"-no_autoacc",
]
pre_cmd: list[str] = [
f"coverage save {entity}_coverage.ucdb -onexit",
]
elif default_simulator == "verilator":
build_args: list[str] = [
"-j",
"0",
"-Wall",
"--coverage",
"--coverage-max-width",
"320",
]
test_args: list[str] = []
pre_cmd = None
try:
# Get simulator name from environment
simulator: str = os.environ.get("SIM", default=default_simulator)
# Initialize the test runner
runner: Runner = get_runner(simulator_name=simulator)
# Build HDL sources
runner.build(
build_args=build_args,
build_dir=str(build_dir),
clean=True,
hdl_library=library,
hdl_toplevel=entity,
parameters=generics,
sources=sources,
waves=True,
)
# Run tests
runner.test(
build_dir=str(build_dir),
hdl_toplevel=entity,
hdl_toplevel_library=library,
pre_cmd=pre_cmd,
test_args=test_args,
test_module=f"test_{entity}",
waves=True,
)
# Generate the coverage report
if simulator == "questa":
generate_coverage_report_questa(
ucdb_file=ucdb_file,
output_folder=output_folder,
)
elif simulator == "verilator":
generate_coverage_report_verilator(
dat_file=dat_file,
output_folder=output_folder,
)
# Log the wave file
wave_file: Path = (
build_dir / "dump.vcd"
if simulator == "verilator"
else build_dir / "vsim.wlf"
)
sys.stdout.write(f"Waveform file: {wave_file}\n")
except Exception as e:
error_message: str = f"Failed in {__file__} with error: {e}"
raise RuntimeError(error_message) from e
if __name__ == "__main__":
test_ascon()