diff --git a/.github/workflows/_cocotb_test.yml b/.github/workflows/_cocotb_test.yml new file mode 100644 index 000000000..bfca17c01 --- /dev/null +++ b/.github/workflows/_cocotb_test.yml @@ -0,0 +1,40 @@ +on: + workflow_call: + +jobs: + cocotb_test: + runs-on: ubuntu-latest + steps: + # Git repositories + - name: Checkout Source + uses: actions/checkout@v2 + with: + path: PandABlocks-fpga + # require history to get back to last tag for version number of branches + fetch-depth: 0 + + # Login into ghcr + - name: login to ghcr + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Run cocotb tests + run: | + docker pull ghcr.io/pandablocks/pandablocks-dev-container:4.0a7 + docker run \ + --net=host \ + -v "${{ github.workspace }}:/repos" \ + -v "${{ github.workspace }}/build:/build" \ + ghcr.io/pandablocks/pandablocks-dev-container:4.0a7 \ + /bin/bash -c \ + "cd PandABlocks-fpga && ln -s CONFIG.example CONFIG && make cocotb_tests && sed -i 's/\/repos\/pandablocks-fpga\///i' cocotb_coverage.xml" + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + name: nvc-coverage + files: cocotb_coverage.xml + # env: + # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index bc2da45df..9d118d576 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -20,9 +20,12 @@ jobs: make_zpkg: uses: ./.github/workflows/_make_zpkg.yml - # Release on push to tag + # cocotb tests + cocotb_test: + uses: ./.github/workflows/_cocotb_test.yml + release: - needs: [ make_boot, make_zpkg, test_hdl, test_matrix, test_python_autogen ] + needs: [ make_boot, make_zpkg, test_hdl, test_matrix, test_python_autogen, cocotb_test ] uses: ./.github/workflows/_release.yml # Generate job matrix to evenly split tests @@ -38,4 +41,5 @@ jobs: needs: [test_matrix, test_python_autogen] uses: ./.github/workflows/_test_hdl.yml with: - matrix: ${{needs.test_matrix.outputs.matrix}} \ No newline at end of file + matrix: ${{needs.test_matrix.outputs.matrix}} + diff --git a/.gitignore b/.gitignore index ab283856d..620544102 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ # For mac *.DS_Store + +# cocotb build dir +sim_build* diff --git a/Makefile b/Makefile index 689d64b71..a6032cf7f 100644 --- a/Makefile +++ b/Makefile @@ -250,6 +250,12 @@ single_hdl_test: $(TIMING_BUILD_DIRS) $(BUILD_DIR)/hdl_timing/pcap carrier_ip hdl_timing: $(TIMING_BUILD_DIRS) .PHONY: hdl_timing +MODULE = all +SIMULATOR = nvc +# e.g. make cocotb_tests MODULE=pulse TEST="No delay or stretch" +cocotb_tests: $(AUTOGEN_BUILD_DIR) + $(PYTHON) $(TOP)/common/python/cocotb_timing_test_runner.py -c --panda-build-dir $(BUILD_DIR) --sim $(SIMULATOR) $(MODULE) '$(TEST)' +.PHONY: cocotb_tests # ------------------------------------------------------------------------------ # FPGA build diff --git a/common/hdl/defines/support.vhd b/common/hdl/defines/support.vhd index a9d6baddf..f07de6ca2 100644 --- a/common/hdl/defines/support.vhd +++ b/common/hdl/defines/support.vhd @@ -9,6 +9,8 @@ constant c_UNSIGNED_GRAY_ENCODING : std_logic_vector(1 downto 0) := "01"; constant c_SIGNED_BINARY_ENCODING : std_logic_vector(1 downto 0) := "10"; constant c_SIGNED_GRAY_ENCODING : std_logic_vector(1 downto 0) := "11"; +type vector_array is array(natural range <>) of std_logic_vector; + -- -- Functions -- @@ -96,4 +98,5 @@ begin return index; end function; + end support; diff --git a/common/hdl/fifo.vhd b/common/hdl/fifo.vhd new file mode 100644 index 000000000..545688154 --- /dev/null +++ b/common/hdl/fifo.vhd @@ -0,0 +1,139 @@ +-- FIFO with AXI style handshaking. The valid signal is asserted by the +-- producer when data is available to be transferred, and the ready signal is +-- asserted when the receiver is ready: transfer happens on the clock cycle when +-- ready and valid are asserted. Two further AXI rules are followed: when valid +-- is asserted it must remain asserted until ready is seen; and the assertion of +-- valid must be independent of the state of ready. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.support.all; +USE work.top_defines.all; + +entity fifo is + generic ( + FIFO_BITS : natural := 5; -- log2 FIFO depth + DATA_WIDTH : natural; -- Width of data path + MEM_STYLE : string := "" -- Can override tool default + ); + port ( + clk_i : in std_logic; + + -- Write interface + write_valid_i : in std_logic; + write_ready_o : out std_logic := '0'; + write_data_i : in std_logic_vector(DATA_WIDTH-1 downto 0); + + -- Read interface + read_valid_o : out std_logic := '0'; + read_ready_i : in std_logic; + read_data_o : out std_logic_vector(DATA_WIDTH-1 downto 0); + + -- Control and status + reset_fifo_i : in std_logic := '0'; + fifo_depth_o : out unsigned(FIFO_BITS downto 0) := (others => '0') + ); +end; + +architecture arch of fifo is + subtype DATA_RANGE is natural range DATA_WIDTH-1 downto 0; + subtype ADDRESS_RANGE is natural range 0 to 2**FIFO_BITS-1; + subtype ADDRESS_RANGE_BITS is natural range FIFO_BITS downto 0; + + signal fifo : vector_array(ADDRESS_RANGE)(DATA_RANGE); + attribute RAM_STYLE : string; + attribute RAM_STYLE of fifo : signal is MEM_STYLE; + + -- This is just computing a mask with only the top bit set to be used for + -- detecting the FIFO full condition + function COMPARE_MASK return unsigned + is + variable result : unsigned(ADDRESS_RANGE_BITS) := (others => '0'); + begin + result(FIFO_BITS) := '1'; + return result; + end; + + signal write_pointer : unsigned(ADDRESS_RANGE_BITS) := (others => '0'); + signal read_pointer : unsigned(ADDRESS_RANGE_BITS) := (others => '0'); + -- The read valid state is separated from read_valid_o to improve data flow + signal read_valid : std_logic := '0'; + +begin + process (clk_i) + variable read_enable : std_logic; + variable next_write_pointer : unsigned(ADDRESS_RANGE_BITS); + variable next_read_pointer : unsigned(ADDRESS_RANGE_BITS); + variable write_address : ADDRESS_RANGE; + variable read_address : ADDRESS_RANGE; + variable next_read_valid : std_logic; + + begin + if rising_edge(clk_i) then + next_write_pointer := write_pointer; + next_read_pointer := read_pointer; + if reset_fifo_i then + next_write_pointer := (others => '0'); + next_read_pointer := (others => '0'); + -- Block writes during reset + write_ready_o <= '0'; + read_valid <= '0'; + read_enable := '0'; + else + -- Advance write pointer if writing + if write_valid_i and write_ready_o then + next_write_pointer := write_pointer + 1; + end if; + + -- Advance read pointer if reading. We keep read_data_o valid + -- at all times when possible + read_enable := + read_valid and (read_ready_i or not read_valid_o); + if read_enable then + next_read_pointer := read_pointer + 1; + end if; + + -- Compute full and empty conditions. Empty is simply equality + -- of pointers, full is when the write pointer is exactly one + -- FIFO depth ahead of the read pointer. This computation can + -- safely be done on the next_ pointers, which gives a small + -- flow optimisation. + write_ready_o <= to_std_logic( + next_write_pointer /= (next_read_pointer xor COMPARE_MASK)); + read_valid <= to_std_logic( + next_write_pointer /= next_read_pointer); + end if; + write_pointer <= next_write_pointer; + read_pointer <= next_read_pointer; + + -- Throw away the top bit of the read/write pointers for access, the + -- top bit is only used as a cycle counter to distinguish full and + -- empty states + write_address := to_integer(write_pointer(FIFO_BITS-1 downto 0)); + read_address := to_integer(read_pointer(FIFO_BITS-1 downto 0)); + + -- Write + if write_valid_i and write_ready_o then + fifo(write_address) <= write_data_i; + end if; + + -- Read + next_read_valid := read_valid_o; + if reset_fifo_i then + next_read_valid := '0'; + elsif read_enable then + read_data_o <= fifo(read_address); + next_read_valid := '1'; + elsif read_ready_i then + next_read_valid := '0'; + end if; + read_valid_o <= next_read_valid; + + -- Compute number of points in FIFO, also counting the output buffer + fifo_depth_o <= + next_write_pointer - next_read_pointer + next_read_valid; + end if; + end process; +end; diff --git a/common/hdl/spbram.vhd b/common/hdl/spbram.vhd index 7da56a726..7baf004f6 100644 --- a/common/hdl/spbram.vhd +++ b/common/hdl/spbram.vhd @@ -33,22 +33,18 @@ end spbram; architecture rtl of spbram is type mem_type is array (2**AW-1 downto 0) of std_logic_vector (DW-1 downto 0); -shared variable mem : mem_type := (others => (others => '0')); begin -process (clka) +process (clka, clkb) + variable mem : mem_type := (others => (others => '0')); begin - if (clka'event and clka = '1') then + if rising_edge(clka) then if (wea = '1') then mem(to_integer(unsigned(addra))) := dina; end if; end if; -end process; - -process (clkb) -begin - if (clkb'event and clkb = '1') then + if rising_edge(clkb) then doutb <= mem(to_integer(unsigned(addrb))); end if; end process; diff --git a/common/python/cocotb_simulate_test.py b/common/python/cocotb_simulate_test.py new file mode 100644 index 000000000..ab3dff0bd --- /dev/null +++ b/common/python/cocotb_simulate_test.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python +import csv +import os +from configparser import ConfigParser +from pathlib import Path + +import cocotb +import cocotb.handle +import pandas as pd +from cocotb.clock import Clock +from cocotb.triggers import ReadOnly, RisingEdge +from dma_driver import DMADriver + +SCRIPT_DIR_PATH = Path(__file__).parent.resolve() +TOP_PATH = SCRIPT_DIR_PATH.parent.parent +MODULES_PATH = TOP_PATH / "modules" + +Dut = cocotb.handle.HierarchyObject +SignalsInfo = dict[str, dict[str, str | int]] + + +def read_ini(path: list[str] | str) -> ConfigParser: + """Read INI file and return its contents. + + Args: + path: Path to INI file. + Returns: + ConfigParser object containing INI file. + """ + app_ini = ConfigParser() + app_ini.read(path) + return app_ini + + +def get_timing_inis(module: str) -> dict[str, ConfigParser]: + """Get a module's timing ini files. + + Args: + module: Name of module. + Returns: + Dictionary of filepath: file contents for any timing.ini files in the + module directory. + """ + ini_paths = (MODULES_PATH / module).glob("*.timing.ini") + return {str(path): read_ini(str(path.resolve())) for path in ini_paths} + + +def get_block_ini(module: str) -> ConfigParser: + """Get a module's block INI file. + + Args: + module: Name of module. + Returns: + Contents of block INI. + """ + ini_path = MODULES_PATH / module / "{}.block.ini".format(module) + return read_ini(str(ini_path.resolve())) + + +def is_input_signal(signals_info: SignalsInfo, signal_name: str) -> bool: + """Check if a signal is an input signal based on it's type. + + Args: + signals_info: Dictionary containing information about signals. + signal_name: Name of signal. + Returns: + True if signal is an input signal, otherwise False. + """ + return not ( + "_out" in signals_info[signal_name]["type"] + or "read" in signals_info[signal_name]["type"] + or "valid_data" in signals_info[signal_name]["type"] + ) + + +async def initialise_dut(dut: Dut, signals_info: SignalsInfo): + """Initialise input signals to 0. + Args: + dut: cocotb dut object. + signals_info: Dictionary containing information about signals. + """ + for signal_name in signals_info.keys(): + dut_signal_name = signals_info[signal_name]["name"] + if is_input_signal(signals_info, signal_name): + getattr(dut, "{}".format(dut_signal_name)).value = 0 + wstb_name: str = signals_info[signal_name].get("wstb_name", "") # type: ignore + if wstb_name: + getattr(dut, wstb_name).value = 0 + + +def parse_assignments(assignments: str) -> dict[str, int]: + """Get assignements (or conditions) for a certain tick, + from timing INI file format. + + Args: + assignements: Assignments as written in timing INI file. + Returns: + Dictionary of assignments (or conditions). + """ + result: dict[str, int] = {} + for assignment in assignments.split(","): + if assignment.strip() == "": + continue + signal_name, val = assignment.split("=") + signal_name = signal_name.strip() + if val.startswith("0x") or val.startswith("0X"): + val = int(val[2:], 16) + elif val.startswith("-0x") or val.startswith("-0X"): + val = int(val[0] + val[3:], 16) + else: + val = int(val) + result[signal_name] = val + return result + + +def assign_bus(dut: Dut, name: str, val: int, index: int, n_bits: int): + # Unused + lsb, msb = index * n_bits, (index + 1) * n_bits - 1 + getattr(dut, name).value[msb:lsb] = val + + +def assign(dut: Dut, name: str, val: int): + """Assign value to a signal. + + Args: + dut: cocotb dut object. + name: Name of signal being assigned. + val: Value being assigned to signal. + """ + getattr(dut, name).set(val) + + +def do_assignments(dut: Dut, assignments: dict[str, int], signals_info: SignalsInfo): + """Assign values to input signals. + + Args: + dut: cocotb dut object. + assignments: Dictionary of signals and values to assign to them. + signals_info: Dictionary containing information about signals. + """ + for signal_name, val in assignments.items(): + if "[" in signal_name: # partial assignent to bus + index = int(signal_name.split("[")[1][:-1]) + signal_name = signal_name.split("[")[0] + ini_signal_name = get_ini_signal_name(signal_name, signals_info) + if ini_signal_name is None: + raise ValueError("Could not get ini signal name", {signal_name}) + n_bits = signals_info[ini_signal_name]["bits"] + assert isinstance(n_bits, int), f"{n_bits} is not an integer" + val = get_bus_value( + int(getattr(dut, signal_name).value), n_bits, val, index + ) + assign(dut, signal_name, val) + + +def check_conditions( + dut: Dut, conditions: dict[str, int], ts: int +) -> tuple[list[str], dict[str, tuple[int, int]]]: + """Check value of output signals. + + Args: + dut: cocotb dut object. + conditions: Dictionary of signals and their expected values. + ts: Current tick. + """ + errors: list[str] = [] + values: dict[str, tuple[int, int]] = {} + for signal_name, val in conditions.items(): + if val < 0: + sim_val = getattr(dut, signal_name).value.signed_integer + else: + sim_val = getattr(dut, signal_name).value + values[signal_name] = (int(val), int(sim_val)) + if sim_val != val: + error = "Signal {} = {}, expecting {}. Ticks = {}".format( + signal_name, sim_val, val, ts + ) + dut._log.error(error) + errors.append(error) + return errors, values + + +def update_conditions(conditions, conditions_to_update, signals_info: SignalsInfo): + """Update dictionary of conditions with any needed for the current tick. + Other than for signals of type 'valid_data', old conditions are propogated + to the next tick, unless they are overwritten by a new condition. + + Args: + conditions: Dictionary of conditions for the previous tick. + conditions_to_update: Dictionary of conditions for current tick. + signals_info: Dictionary containing information about signals. + """ + for signal in dict(conditions).keys(): + ini_signal_name = get_ini_signal_name(signal, signals_info) + if signals_info[ini_signal_name]["type"] == "valid_data": + conditions.pop(signal) + conditions.update(conditions_to_update) + return conditions + + +def get_signals(dut: Dut): + """Get the names of signals in the dut. + + Args: + dut: cocotb dut object. + Returns: + List of signal names. + """ + return [ + getattr(dut, signal_name) + for signal_name in dir(dut) + if isinstance(getattr(dut, signal_name), cocotb.handle.ModifiableObject) + and not signal_name.startswith("_") + ] + + +def get_schedules( + timing_ini: ConfigParser, signals_info: SignalsInfo, test_name: str +) -> tuple[dict[int, dict[str, int]], dict[int, dict[str, int]]]: + """Get schedules for assignements and conditions for a test. + + Args: + timing_ini: INI file containing timing tests. + signals_info: Dictionary containing information about signals. + test_name: Name of current test. + Returns: + Two dictionaries containing a schedule for assignments and conditions + respectively for a certain test. + """ + conditions_schedule: dict[int, dict[str, int]] = {} + assignments_schedule: dict[int, dict[str, int]] = {} + for ts_str, line in timing_ini.items(test_name): + ts = int(ts_str) + for i in (ts, ts + 1): + assignments_schedule.setdefault(i, {}) + conditions_schedule.setdefault(i, {}) + parts = line.split("->") + for sig_name, val in parse_assignments(parts[0]).items(): + index = f'[{sig_name.split("[")[1]}' if "[" in sig_name else "" + sig_name = sig_name[: len(sig_name) - len(index)] + name = signals_info.get(sig_name)["name"] + assignments_schedule[ts][name + index] = val + wstb_name = signals_info.get(sig_name).get("wstb_name") + if wstb_name is not None: + assignments_schedule[ts][wstb_name] = 1 + assignments_schedule[ts + 1].update({wstb_name: 0}) + + if len(parts) > 1: + conditions_schedule[ts + 1] = {} + for sig_name, val in parse_assignments(parts[1]).items(): + name = signals_info.get(sig_name)["name"] + conditions_schedule[ts + 1][name] = val + return assignments_schedule, conditions_schedule + + +def get_signals_info(block_ini: ConfigParser) -> SignalsInfo: + """Get information about signals from a module's block INI file, including + a mapping between signal names in the INI files and VHDL files. + + Args: + block_ini: INI file containing signals information. + Returns: + Dictionary containing signals information. + """ + signals_info = {} + expected_signal_names = [name for name in block_ini.sections() if name != "."] + for signal_name in expected_signal_names: + if "type" in block_ini[signal_name]: + _type = block_ini[signal_name]["type"].strip() + suffixes = [] + if _type == "time": + suffixes = ["_L", "_H"] + elif _type == "table short": + suffixes = ["_START", "_DATA", "_LENGTH"] + elif _type == "table": + suffixes = ["_ADDRESS", "_LENGTH"] + if suffixes: + for suffix in suffixes: + new_signal_name = f"{signal_name}{suffix}" + signals_info[new_signal_name] = {} + signals_info[new_signal_name].update(block_ini[signal_name]) + signals_info[new_signal_name]["name"] = new_signal_name + if "wstb" in block_ini[signal_name]: + signals_info[new_signal_name]["wstb_name"] = "{}_wstb".format( + new_signal_name.lower() + ) + else: + signals_info[signal_name] = {} + signals_info[signal_name].update(block_ini[signal_name]) + + if _type.endswith("_mux"): + signals_info[signal_name]["name"] = "{}_i".format( + signal_name.lower() + ) + elif _type.endswith("_out"): + signals_info[signal_name]["name"] = "{}_o".format( + signal_name.lower() + ) + else: + signals_info[signal_name]["name"] = signal_name + + if "wstb" in block_ini[signal_name]: + signals_info[signal_name]["wstb_name"] = "{}_wstb".format( + signal_name.lower() + ) + return signals_info + + +def get_ini_signal_name(name: str, signals_info: SignalsInfo) -> str | None: + """Get signal name as seen in the INI files from the VHDL signal name. + + Args: + name: VHDL signal name. + signals_info: Dictionary containing information about signals. + Returns: + Signal name as it appears in the INI files. + """ + for key, info in signals_info.items(): + if info["name"] == name: + return key + return None + + +def check_signals_info(signals_info: SignalsInfo): + """Check there are no duplicate signal names in signals_info dictionary. + + Args: + signals_info: Dictionary containing information about signals. + """ + signal_names: list[str] = [] + for signal_info in signals_info.values(): + if signal_info["name"] in signal_names: + raise ValueError("Duplicate signal names in signals info dictionary.") + else: + signal_names.append(signal_info["name"]) + + +def block_has_dma(block_ini: ConfigParser): + """Check if module requires a dma to work. + + Args: + block_ini: INI file containing signals information about a module. + """ + return block_ini["."].get("type", "") == "dma" + + +def get_bus_value(current_value: int, n_bits: int, value: int, index: int) -> int: + """When doing a partial assignent to a bus, get the value we need to + assign to the entire bus. This is needed as partial assignment appears to + not be supported. + + Args: + current value: Current value of bus signal. + n_bits: Number of bits each index refers to. + value: Value we are attemping to assign to part of the bus. + index: Bus index at which we are attempting to assign. + Returns: + Value to assign to an entire bus to assign the intended value to the + correct index, while keeping all other indexes unchanged. + """ + val_copy = value + capacity = 2**n_bits + if value < 0: + value += capacity + if value < 0 or value >= capacity: + raise ValueError( + f"Value {val_copy} too large in magnitude for " + + f"{n_bits} bit allocation on bus." + ) + value_at_index = ((capacity - 1) << n_bits * index) & current_value + new_value_at_index = value << n_bits * index + return current_value - value_at_index + new_value_at_index + + +def get_extra_signals_info(module: str, panda_build_dir: str | Path): + """Get extra signals information from a module's test config file. + + Args: + module: Name of module. + panda_build_dir: Path to autogenerated HDL files + Returns: + Dictionary containing extra signals information. + """ + test_config_path = MODULES_PATH / module / "test_config.py" + if test_config_path.exists(): + g = {"TOP_PATH": TOP_PATH, "BUILD_PATH": Path(panda_build_dir)} + with open(str(test_config_path)) as file: + code = file.read() + exec(code, g) + extra_signals_info = g.get("EXTRA_SIGNALS_INFO", {}) + return extra_signals_info + return {} + + +async def simulate( + dut: Dut, + assignments_schedule: dict[int, dict[str, int]], + conditions_schedule: dict[int, dict[str, int]], + signals_info: SignalsInfo, + collect: bool, +) -> tuple[dict[int, dict[str, tuple[int, int]]], dict[int, list[str]]]: + """Run the simulation according to the schedule found in timing ini. + + Args: + dut: cocotb dut object. + assignments_schedule: Schedule for signal assignments. + conditions_schedule: Schedule for checking conditions. + signals_info: Dictionary containing information about signals. + collect: Collect signals expected and actual values when True. + Returns: + Dictionaries containing signal values and timing errors. + """ + last_ts = max(max(assignments_schedule.keys()), max(conditions_schedule.keys())) + clkedge = RisingEdge(dut.clk_i) + cocotb.start_soon(Clock(dut.clk_i, 1, units="ns").start(start_high=False)) + ts = 0 + await initialise_dut(dut, signals_info) + await clkedge + conditions: dict[str, int] = {} + timing_errors: dict[int, list[str]] = {} + values: dict[int, dict[str, tuple[int, int]]] = {} + while ts <= last_ts: + do_assignments(dut, assignments_schedule.get(ts, {}), signals_info) + update_conditions(conditions, conditions_schedule.get(ts, {}), signals_info) + await ReadOnly() + errors, values[ts] = check_conditions(dut, conditions, ts) + if errors: + timing_errors[ts] = errors + await clkedge + ts += 1 + return values, timing_errors + + +def collect_values(values: dict[int, dict[str, tuple[int, int]]], test_name: str): + """Saves collected signal values to csv and html. + + Args: + values: Dictionary of signal values to save. + test_name: Name of test. + """ + filename = f'{test_name.replace(' ', '_').replace('/', '_')}_values' + values_df = pd.DataFrame(values) + values_df = values_df.transpose() + values_df.index.name = "tick" + values_df.to_csv(f"{Path.cwd()}/{filename}.csv", index=True) + values_df.to_html(f"{Path.cwd()}/{filename}.html") + + +async def section_timing_test( + dut: Dut, + module: str, + test_name: str, + block_ini: ConfigParser, + timing_ini: ConfigParser, + panda_build_dir: str | Path, + collect: bool = True, +): + """Perform one test. + + Args: + dut: cocotb dut object. + module: Name of module we are testing. + test_name: Name of test we are applying to the module. + block_ini: INI file containing information about the module's signals. + timing_ini: INI file containing timing tests. + collect: Collect signals expected and actual values when True. + """ + if block_has_dma(block_ini): + dma_driver = DMADriver(dut, module) + + signals_info = get_signals_info(block_ini) + signals_info.update(get_extra_signals_info(module, panda_build_dir)) + check_signals_info(signals_info) + + assignments_schedule, conditions_schedule = get_schedules( + timing_ini, signals_info, test_name + ) + + values, timing_errors = await simulate( + dut, assignments_schedule, conditions_schedule, signals_info, collect + ) + + if timing_errors: + filename = f'{test_name.replace(' ', '_').replace('/', '_')}_errors.csv' + with open(filename, mode="w", newline="", encoding="utf-8") as file: + writer = csv.writer(file) + for tick, messages in timing_errors.items(): + for message in messages: + writer.writerow([tick, message]) + dut._log.info(f"Errors written to {filename}") + if collect: + collect_values(values, test_name) + + assert not timing_errors, "Timing errors found, see above." + + +@cocotb.test() +async def module_timing_test(dut: Dut): + """Function with cocotb test decorator that cocotb calls to run tests. + + Args: + dut: cocotb dut object. + """ + module = os.getenv("module") + test_name = os.getenv("test_name") + simulator = os.getenv("simulator") + sim_build_dir = os.getenv("sim_build_dir") + panda_build_dir = os.getenv("panda_build_dir") + timing_ini_path = os.getenv("timing_ini_path") + collect = True if os.getenv("collect") == "True" else False + block_ini = get_block_ini(module) + timing_inis = get_timing_inis(module) + timing_ini = timing_inis[timing_ini_path] + if test_name.strip() != ".": + await section_timing_test( + dut, module, test_name, block_ini, timing_ini, panda_build_dir, collect + ) diff --git a/common/python/cocotb_timing_test_runner.py b/common/python/cocotb_timing_test_runner.py new file mode 100644 index 000000000..8505b8080 --- /dev/null +++ b/common/python/cocotb_timing_test_runner.py @@ -0,0 +1,739 @@ +#!/usr/bin/env python +import argparse +import csv +import logging +import os +import shutil +import subprocess +import time +from configparser import ConfigParser +from pathlib import Path +from typing import Any, List + +from cocotb_simulate_test import get_block_ini +from cocotb_tools import runner # type: ignore + +logger = logging.getLogger(__name__) + +SCRIPT_DIR_PATH = Path(__file__).parent.resolve() +TOP_PATH = SCRIPT_DIR_PATH.parent.parent +MODULES_PATH = TOP_PATH / "modules" +WORKING_DIR = Path.cwd() + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument("module") + parser.add_argument("test_name", nargs="?", default=None) + parser.add_argument("--sim", default="nvc") + parser.add_argument("--skip", default=None) + parser.add_argument("--panda-build-dir", default="/build") + parser.add_argument("-c", action="store_true") + return parser.parse_args() + + +def read_ini(path: List[str] | str) -> ConfigParser: + """Read INI file and return its contents. + + Args: + path: Path to INI file. + Returns: + ConfigParser object containing INI file. + """ + app_ini = ConfigParser() + app_ini.read(path) + return app_ini + + +def get_timing_inis(module: str) -> dict[str, ConfigParser]: + """Get a module's timing ini files. + + Args: + module: Name of module. + Returns: + Dictionary of filepath: file contents for any timing.ini files in the + module directory. + """ + ini_paths = (MODULES_PATH / module).glob("*.timing.ini") + return {str(path): read_ini(str(path.resolve())) for path in ini_paths} + + +def block_has_dma(block_ini: ConfigParser) -> bool: + """Check if module requires a dma to work. + + Args: + block_ini: INI file containing signals information about a module. + """ + return block_ini["."].get("type", "") == "dma" + + +def get_module_build_args(module: str, panda_build_dir: str | Path) -> list[str]: + """Get simulation build arguments from a module's test config file. + + Args: + module: Name of module. + panda_build_dir: Path to autogenerated HDL files. + Returns: + List of extra build arguments. + """ + test_config_path = MODULES_PATH / module / "test_config.py" + if test_config_path.exists(): + g = {"TOP_PATH": TOP_PATH, "BUILD_PATH": Path(panda_build_dir)} + code = open(str(test_config_path)).read() + exec(code, g) + args: list[str] = g.get("EXTRA_BUILD_ARGS", []) # type: ignore + return args + return [] + + +def order_hdl_files( + hdl_files: list[Path], build_dir: str | Path, top_level: str +) -> list[Path]: + """Put vhdl source files in compilation order. This is neccessary for the + nvc simulator as it does not order the files iself before compilation. + + Args: + hdl_files: List of vhdl source files. + build_dir: Build directory for simulation. + top_level: Name of the top-level entity. + """ + command: list[str] = [ + "vhdeps", + "dump", + top_level, + "-o", + f'{WORKING_DIR / build_dir / "order"}', + ] + for file in hdl_files: + command.append(f"--include={str(file)}") + command_str = " ".join(command) + Path(WORKING_DIR / build_dir).mkdir(exist_ok=True) + subprocess.run(["/usr/bin/env"] + command) + try: + with open(TOP_PATH / build_dir / "order") as order: + ordered_hdl_files = [ + Path(line.strip().split(" ")[-1]) for line in order.readlines() + ] + return ordered_hdl_files + except FileNotFoundError as error: + logger.warning(f"Likely that the following command failed:\n{command_str}") + logger.warning(error) + logger.warning("HDL FILES HAVE NOT BEEN PUT INTO COMPILATION ORDER!") + return hdl_files + + +def get_module_hdl_files( + module: str, top_level: str, build_dir: str | Path, panda_build_dir: str | Path +): + """Get HDL files needed to simulate a module from its test config file. + + Args: + module: Name of module. + top_level: Top level entity of module being tested. + build_dir: Name of simulation build directory. + panda_build_dir: Path to autogenerated HDL files. + Returns: + List of paths to the HDL files. + """ + module_dir_path = MODULES_PATH / module + test_config_path = module_dir_path / "test_config.py" + g = {"TOP_PATH": TOP_PATH, "BUILD_PATH": Path(panda_build_dir)} + if test_config_path.exists(): + code = open(str(test_config_path)).read() + exec(code, g) + g.get("EXTRA_HDL_FILES", []) + extra_files: list[Path] = list(g.get("EXTRA_HDL_FILES", [])) # type: ignore + extra_files_2: list[Path] = [] + for my_file in extra_files: + if str(my_file).endswith(".vhd"): + extra_files_2.append(my_file) + else: + extra_files_2 = extra_files_2 + list(my_file.glob("**/*.vhd")) + else: + extra_files_2 = [] + result = extra_files_2 + list((module_dir_path / "hdl").glob("*.vhd")) + ordered = order_hdl_files(result, build_dir, top_level) + logger.info("Gathering the following VHDL files:") + for my_file in ordered: + logger.info(my_file) + return ordered + + +def get_module_top_level(module: str, panda_build_dir: str | Path) -> str: + """Get top level entity from a module's test config file. + If none is found, assume top level entity is the same as the module name. + + Args: + module: Name of module. + panda_build_dir: Path to autogenerated HDL files. + Returns: + Name of top level entity. + """ + test_config_path = MODULES_PATH / module / "test_config.py" + if test_config_path.exists(): + g = {"TOP_PATH": TOP_PATH, "BUILD_PATH": Path(panda_build_dir)} + code = open(str(test_config_path)).read() + exec(code, g) + top_level: str = g.get("TOP_LEVEL", None) # type: ignore + if top_level: + return top_level + return module + + +def print_results( + module: str, passed: list[str], failed: list[str], time: float | None = None +): + """Format and print results from a module's tests. + + Args: + module: Name of module. + passed: List of the names of tests that passed. + failed: List of the names of tests that failed. + time: Time taken to run the tests. + """ + print("__") + print("\nModule: {}".format(module)) + if len(passed) + len(failed) == 0: + print("\033[0;33m" + "No tests ran." + "\033[0m") + else: + percentage = round(len(passed) / (len(passed) + len(failed)) * 100) + print( + "{}/{} tests passed ({}%).".format( + len(passed), len(passed) + len(failed), percentage + ) + ) + if time is not None: + print("Time taken = {}s.".format(time)) + if failed: + print("\033[0;31m" + "Failed tests:" + "\x1b[0m", end=" ") + print( + *[ + test + (", " if i < len(failed) - 1 else ".") + for i, test in enumerate(failed) + ] + ) + else: + print("\033[92m" + "ALL PASSED" + "\x1b[0m") + + +def summarise_results(results: dict[str, list[list[str]]]): + """Format and print summary of results from a test run. + + Args: + Results: Dictionary of all results from a test run. + """ + failed: list[str] = [module for module in results if results[module][1]] + passed: list[str] = [module for module in results if not results[module][1]] + total_passed, total_failed = 0, 0 + for module in results: + total_passed += len(results[module][0]) + total_failed += len(results[module][1]) + total = total_passed + total_failed + print("\nSummary:\n") + if total == 0: + print("\033[1;33m" + "No tests ran." + "\033[0m") + else: + print( + "{}/{} modules passed ({}%).".format( + len(passed), + len(results.keys()), + round(len(passed) / len(results.keys()) * 100), + ) + ) + print( + "{}/{} tests passed ({}%).".format( + total_passed, total, round(total_passed / total * 100) + ) + ) + if failed: + print("\033[0;31m" + "\033[1m" + "Failed modules:" + "\x1b[0m", end=" ") + print( + *[ + module + (", " if i < len(failed) - 1 else ".") + for i, module in enumerate(failed) + ] + ) + else: + print("\033[92m" + "\033[1m" + "ALL MODULES PASSED" + "\x1b[0m") + + +def get_simulator_build_args(simulator: str) -> list[str]: + """Get arguments for the build stage. + + Args: + simulator: Name of simulator being used. + Returns: + List of build arguments. + """ + if simulator == "ghdl": + return ["--std=08", "-fsynopsys", "-Wno-hide"] + elif simulator == "nvc": + return ["--std=2008"] + else: + raise NotImplementedError(f"{simulator} is not a valid simulator") + + +def get_test_args(simulator: str, build_args: list[str], test_name: str) -> list[str]: + """Get arguments for the test stage. + + Args: + simulator: Name of simulator being used. + build_args: Arguments used for the build stage. + test_name: Name of test being carried out. + Returns: + List of test arguments. + """ + test_name = test_name.replace(" ", "_").replace("/", "_") + if simulator == "ghdl": + return build_args + elif simulator == "nvc": + return ["--ieee-warnings=off", f"--wave={test_name}.vcd"] + else: + raise NotImplementedError(f"{simulator} is not a valid simulator") + + +def get_elab_args(simulator: str) -> list[str]: + """Get arguments for the elaboration stage. + + Args: + simulator: Name of simulator being used. + Returns: + List of elaboration arguments. + """ + if simulator == "nvc": + return ["--cover"] + else: + return [] + + +def get_plusargs(simulator: str, test_name: str) -> list[str]: + """Get plusargs to for the test stage. + + Args: + simulator: Name of simulator being used. + test_name: Name of test being carried out. + + Returns: + """ + test_name = test_name.replace(" ", "_").replace("/", "_") + vcd_filename = f"{test_name}.vcd" + if simulator == "ghdl": + return [f"--vcd={vcd_filename}"] + elif simulator == "vcd": + return [] + return [] + + +def collect_coverage_file( + build_dir: str | Path, top_level: str, test_name: str +) -> Path: + """Move coverage file to the coverage directory + + Args: + build_dir: Simulation build directory. + top_level: Top level entity being tested. + test_name: Name of test being carried out. + Returns: + New file path of the coverage file. + """ + coverage_path = Path(WORKING_DIR / build_dir / "coverage") + Path(coverage_path).mkdir(exist_ok=True) + old_file_path = Path( + WORKING_DIR / build_dir / "top" / f"_TOP.{top_level.upper()}.elab.covdb" + ) + test_name = test_name.replace(" ", "_").replace("/", "_") + new_file_path = Path( + coverage_path / f"_TOP.{top_level.upper()}.{test_name}.elab.covdb" + ) + subprocess.run(["mv", old_file_path, new_file_path]) + return new_file_path + + +def merge_coverage_data( + build_dir: str | Path, module: str, file_paths: list[Path] +) -> Path: + """Merges coverage files from each test to create an overall coverage + report for a module. + + Args: + build_dir: Simulation build directory. + module: Name of module. + file_paths: List of paths to coverage files from each test. + Returns: + File path for the coverage report file. + """ + merged_path = Path(WORKING_DIR / build_dir / "coverage" / f"merged.{module}.covdb") + command = ( + ["nvc", "--cover-merge", "-o"] + + [str(merged_path)] + + [str(file_path) for file_path in file_paths] + ) + subprocess.run(command) + return merged_path + + +def export_coverage_data( + output_path: Path, + file_paths: list[Path], + format: str = "cobertura", +): + """Merges merge coverage files from each module to create an overall coverage + report in an xml format suitable for codecov. + + Args: + output_path: Path of the output coverage report to write + file_paths: List of Paths to coverage files from each module. + format: coverage format to pass to nvc simulator + """ + command = ( + ["nvc", "--cover-export", f"--format={format}", "-o"] + + [str(output_path)] + + [str(file_path) for file_path in file_paths] + ) + subprocess.run(command) + + +def cleanup_dir(test_name: str, build_dir: str | Path): + """Creates a subdirectory for a test and moves all files generated from + that test into it. + + Args: + test_name: Name of test. + build_dir: Simulation build directory. + """ + test_name = test_name.replace(" ", "_").replace("/", "_") + (WORKING_DIR / build_dir / test_name).mkdir(exist_ok=True) + logger.info(f'Putting all files related to "{test_name}" in {str( + WORKING_DIR / build_dir / test_name)}') + for file in (WORKING_DIR / build_dir).glob(f"{test_name}*"): + if file.is_file(): + new_name = str(file).split("/")[-1].replace(test_name, "") + if new_name.endswith(".vcd"): + new_name = "wave" + new_name + new_name = new_name.lstrip("_") + file.rename(WORKING_DIR / build_dir / test_name / new_name) + + +def print_errors(failed_tests: list[str], build_dir: str | Path): + """Print out timing errors. + + Args: + failed_tests: List of tests that failed. + build_dir: Simulation build directory. + """ + for test_name in failed_tests: + logger.info(f' See timing errors for "{test_name}" below') + test_name = test_name.replace(" ", "_").replace("/", "_") + with open(WORKING_DIR / build_dir / test_name / "errors.csv") as file: + reader = csv.reader(file) + for row in reader: + log_timing_error(row[1]) + + +def print_coverage_data(coverage_report_path: Path): + """Print coverage report + + Args: + coverage_report_path: Path to coverage report file. + """ + print("Code coverage:") + coverage_path = coverage_report_path.parent + command = [ + "nvc", + "--cover-report", + "-o", + str(coverage_path), + str(coverage_report_path), + ] + subprocess.run(command) + + +def log_timing_error(message: str, *args: Any, **kwargs: Any): + timing_error_level = 30 + if logger.isEnabledFor(timing_error_level): + logger._log(timing_error_level, message, *args, **kwargs) + + +def test_module( + module: str, + test_name: str | None = None, + simulator: str = "nvc", + panda_build_dir: str | Path = "/build", + collect: bool = False, +) -> tuple[list[str], list[str], Path | None]: + """Run tests for a module. + + Args: + module: Name of module. + test_name: Name of specific test to run. If not specified, all tests + for that module will be run. + simulator: Name of simulator to use for simulation. + panda_build_dir: Location of autogenerated HDL files. + collect: If True, collect output signals expected and actual values. + Returns: + Lists of tests that passed and failed respectively, path to coverage. + """ + if not Path(MODULES_PATH / module).is_dir(): + raise FileNotFoundError( + "No such directory: '{}'".format(Path(MODULES_PATH / module)) + ) + + sim: runner.Simulator = runner.get_runner(simulator) # type: ignore + build_dir = f"sim_build_{module}" + build_args = get_simulator_build_args(simulator) + build_args += get_module_build_args(module, panda_build_dir) + top_level = get_module_top_level(module, panda_build_dir) + sim.build( # type: ignore + sources=get_module_hdl_files(module, top_level, build_dir, panda_build_dir), + build_dir=build_dir, + hdl_toplevel=top_level, + build_args=build_args, + clean=True, + ) + passed: list[str] = [] + failed: list[str] = [] + coverage_file_paths: list[Path] = [] + coverage_report_path: Path | None = None + + timing_inis = get_timing_inis(module) + for path, timing_ini in timing_inis.items(): + if test_name: + sections: list[str] = [] + for test in test_name.split(",,"): # Some test names contain a comma + if test in timing_ini.sections(): + sections.append(test) + else: + print( + 'No test called "{}" in {} INI timing file.'.format( + test, module + ).center(shutil.get_terminal_size().columns) + ) + if not sections: + return [], [], None + else: + sections = timing_ini.sections() + + for section in sections: + if section.strip() != ".": + test_name = section + print() + print('Test: "{}" in module {}.\n'.format(test_name, module)) + xml_path: Path = sim.test( # type: ignore + hdl_toplevel=top_level, + test_module="cocotb_simulate_test", + build_dir=build_dir, + test_args=get_test_args(simulator, build_args, test_name), + elab_args=get_elab_args(simulator), + plusargs=get_plusargs(simulator, test_name), + extra_env={ + "module": module, + "test_name": test_name, + "simulator": simulator, + "sim_build_dir": str(build_dir), + "timing_ini_path": str(path), + "panda_build_dir": str(panda_build_dir), + "collect": str(collect), + }, + ) + results: tuple[int, int] = runner.get_results(xml_path) # type: ignore + if simulator == "nvc": + coverage_file_paths.append( + collect_coverage_file(build_dir, top_level, test_name) + ) + if results == (1, 0): + # ran 1 test, 0 failed + passed.append(test_name) + elif results == (1, 1): + # ran 1 test, 1 failed + failed.append(test_name) + else: + raise ValueError(f"Results unclear: {results}") + cleanup_dir(test_name, build_dir) + test_name = None + if simulator == "nvc": + coverage_report_path = merge_coverage_data( + build_dir, module, coverage_file_paths + ) + return passed, failed, coverage_report_path + + +def get_cocotb_testable_modules(): + """Get list of modules that contain a timing.ini file file. + + Returns: + List of names of testable modules. + """ + modules = MODULES_PATH.glob("*/*.timing.ini") + return list(module.parent.name for module in modules) + + +def run_tests(): + """Perform test run.""" + t_time_0 = time.time() + args = get_args() + if args.module.lower() == "all": + modules = get_cocotb_testable_modules() + else: + modules = args.module.split(",") + skip_list: list[str] = args.skip.split(",") if args.skip else [] + for module in skip_list: + if module in modules: + modules.remove(module) + print(f"Skipping {module}.") + else: + print(f"Cannot skip {module} as it was not going to be tested.") + simulator = args.sim + collect = bool(args.c) + results: dict[str, list[list[str]]] = {} + times: dict[str, float | None] = {} + coverage_reports: dict[str, Path | None] = {} + for module in modules: + t0 = time.time() + module = module.strip("\n") + results[module] = [[], []] + # [[passed], [failed]] + print() + print( + "* Testing module \033[1m{}\033[0m *".format(module.strip("\n")).center( + shutil.get_terminal_size().columns + ) + ) + print( + "---------------------------------------------------".center( + shutil.get_terminal_size().columns + ) + ) + results[module][0], results[module][1], coverage_reports[module] = test_module( + module, + test_name=args.test_name, + simulator=simulator, + panda_build_dir=args.panda_build_dir, + collect=collect, + ) + t1 = time.time() + times[module] = round(t1 - t0, 2) + print("___________________________________________________") + print("\nResults:") + for module in results: + print_results(module, results[module][0], results[module][1], times[module]) + path = coverage_reports[module] + if path is not None: + print_coverage_data(path) + build_dir = f"sim_build_{module}" + print_errors(results[module][1], build_dir) + print("___________________________________________________") + summarise_results(results) + t_time_1 = time.time() + print("\nTime taken: {}s.".format(round(t_time_1 - t_time_0, 2))) + print("___________________________________________________\n") + print(f"Simulator: {simulator}\n") + + report_paths = [path for path in coverage_reports.values() if path is not None] + export_coverage_data(Path("cocotb_coverage.xml"), report_paths) + + +def get_ip(module: str | None = None, verbose: bool = False) -> dict[str, str | None]: + if not module: + modules: list[str] = os.listdir(MODULES_PATH) + else: + modules: list[str] = [module] + ip: dict[str, str | None] = {} + for module in modules: + ini = get_block_ini(module) + if not ini.sections(): + if verbose: + print("\033[1m" + f"No block INI file found in {module}!" + "\033[0m") + continue + info = {} + if "." in ini.keys(): + info = ini["."] + spaces = " " + "-" * (16 - len(module)) + " " + if "ip" in info: + ip[module] = info["ip"] + if verbose: + print( + "IP needed for module \033[1m" + + module + + "\033[0m:" + + spaces + + "\033[0;33m" + + str(ip[module]) + + "\033[0m" + ) + else: + ip[module] = None + if verbose: + print( + "IP needed for module " + + "\033[1m" + + module + + "\033[0m:" + + spaces + + "None found" + ) + return ip + + +def check_timing_ini( + module: str | None = None, verbose: bool = False +) -> dict[str, bool]: + if not module: + modules = os.listdir(MODULES_PATH) + else: + modules = [module] + has_timing_ini: dict[str, bool] = {} + for module in modules: + ini = get_timing_inis(module) + if ini: + has_timing_ini[module] = False + if verbose: + print( + "\033[0;33m" + + f"No timing INI file found in - \033[1m{module}\033[0m" + ) + else: + has_timing_ini[module] = True + if verbose: + print(f"Timing ini file found in ---- \033[1m{module}\033[0m") + return has_timing_ini + + +def get_some_info(): + ini_and_ip: list[str] = [] + ini_no_ip: list[str] = [] + no_ini: list[str] = [] + has_timing_ini = check_timing_ini() + has_ip = get_ip() + for module in has_timing_ini.keys(): + if has_timing_ini[module]: + if module in has_ip and has_ip[module]: + ini_and_ip.append(module) + else: + ini_no_ip.append(module) + else: + no_ini.append(module) + print("\nModules with no timing INI:") + for i, module in enumerate(no_ini): + print(f"{i + 1}. {module}") + print("\nModules with timing INI and IP:") + for i, module in enumerate(ini_and_ip): + print(f"{i + 1}. {module}") + print("\nModules with timing INI and no IP:") + for i, module in enumerate(ini_no_ip): + print(f"{i + 1}. {module}") + + +def main(): + args = get_args() + if args.module.lower() == "ip": + get_ip(args.test_name, verbose=True) + elif args.module.lower() == "ini": + check_timing_ini(verbose=True) + elif args.module.lower() == "info": + get_some_info() + else: + run_tests() + + +if __name__ == "__main__": + main() diff --git a/common/python/dma_driver.py b/common/python/dma_driver.py new file mode 100644 index 000000000..f40713fa8 --- /dev/null +++ b/common/python/dma_driver.py @@ -0,0 +1,42 @@ +import cocotb + +from cocotb.triggers import RisingEdge +from collections import deque +from pathlib import Path + +SCRIPT_DIR_PATH = Path(__file__).parent.resolve() +TOP_PATH = SCRIPT_DIR_PATH.parent.parent +MODULES_PATH = TOP_PATH / 'modules' + + +class DMADriver(object): + def __init__(self, dut, module): + self.dut = dut + self.dut.dma_ack_i.value = 0 + self.dut.dma_done_i.value = 0 + self.dut.dma_data_i.value = 0 + self.dut.dma_valid_i.value = 0 + self.module = module + cocotb.start_soon(self.run()) + + async def run(self): + while True: + await RisingEdge(self.dut.dma_req_o) + await RisingEdge(self.dut.clk_i) + self.dut.dma_ack_i.value = 1 + addr = self.dut.dma_addr_o.value.integer + length = self.dut.dma_len_o.value.integer + data = deque([int(item) for item in open( + MODULES_PATH / self.module / f'{addr}.txt', + 'r').read().splitlines()[1:]]) + await RisingEdge(self.dut.clk_i) + self.dut.dma_ack_i.value = 0 + for i in range(length): + self.dut.dma_data_i.value = data.popleft() + self.dut.dma_valid_i.value = 1 + await RisingEdge(self.dut.clk_i) + + self.dut.dma_valid_i.value = 0 + self.dut.dma_done_i.value = 1 + await RisingEdge(self.dut.clk_i) + self.dut.dma_done_i.value = 0 diff --git a/common/python/dma_monitor.py b/common/python/dma_monitor.py new file mode 100644 index 000000000..9fd56d0b8 --- /dev/null +++ b/common/python/dma_monitor.py @@ -0,0 +1,36 @@ +import cocotb + +from cocotb.triggers import RisingEdge, Edge, ReadOnly +from pathlib import Path + +SCRIPT_DIR_PATH = Path(__file__).parent.resolve() +TOP_PATH = SCRIPT_DIR_PATH.parent.parent +MODULES_PATH = TOP_PATH / 'modules' + + +class DMAMonitor(object): + def __init__(self, dut): + self.dut = dut + self.value = 0 + self.ts = 0 + self.valid = 0 + cocotb.start_soon(self.run()) + cocotb.start_soon(self.check_valid()) + self.expect = [] + + async def run(self): + while True: + await RisingEdge(self.dut.clk_i) + if self.valid == 1: + await ReadOnly() + if self.valid == 1: + self.value = self.dut.pcap_dat_o.value.signed_integer + self.ts += 1 + + async def check_valid(self): + while True: + await RisingEdge(self.dut.pcap_dat_valid_o) + self.valid = 1 + self.value = self.dut.pcap_dat_o.value.signed_integer + await Edge(self.dut.pcap_dat_valid_o) + self.valid = 0 diff --git a/common/python/simulations.py b/common/python/simulations.py index 52632b5a7..f848fcb1d 100644 --- a/common/python/simulations.py +++ b/common/python/simulations.py @@ -4,7 +4,7 @@ import struct import time import bisect -import imp +import importlib import os import sys from collections import namedtuple, deque @@ -261,9 +261,9 @@ def create_block(self, ini_path, number, block_address): ini_path = os.path.join(ROOT, ini_path) module_path = os.path.dirname(ini_path) try: - f, pathname, description = imp.find_module( + f, pathname, description = importlib.import_module( block_name + "_sim", [module_path]) - package = imp.load_module( + package = importlib.exec_module( block_name + "_sim", f, pathname, description) clsnames = [n for n in dir(package) if n.lower() == block_name + "simulation"] diff --git a/common/python/sphinx_timing_directive.py b/common/python/sphinx_timing_directive.py index 6edf333ae..aaa9b23c6 100644 --- a/common/python/sphinx_timing_directive.py +++ b/common/python/sphinx_timing_directive.py @@ -181,8 +181,9 @@ def make_long_tables(self, sequence, sequence_dir, path): for ts, inputs, outputs in timing_entries(sequence, sequence_dir): if 'TABLE_ADDRESS' in inputs: # open the table - file_dir = os.path.join(path, inputs["TABLE_ADDRESS"]) - assert os.path.isfile(file_dir), "%s does not exist" %(file_dir) + file_dir = os.path.join(path, f'{inputs["TABLE_ADDRESS"]}.txt') + assert os.path.isfile(file_dir) or os.path.isfile( + file_dir), "%s does not exist" %(file_dir) with open(file_dir, "r") as table: reader = csv.DictReader(table, delimiter='\t') table_data = [line for line in reader] diff --git a/common/templates/dma_hdl_timing_extra b/common/templates/dma_hdl_timing_extra index d45f0514c..e49d1128d 100644 --- a/common/templates/dma_hdl_timing_extra +++ b/common/templates/dma_hdl_timing_extra @@ -1,7 +1,7 @@ // This template works for pgen. It may not be generic enough to work for any // other blocks with a DMA interface. -// {{ block.entity|upper }}_1000.txt +// 1000.txt // data_mem parameter STATE_IDLE = 0; parameter STATE_TABLE_ADDR = 1; @@ -21,8 +21,8 @@ initial begin @(posedge clk_i); - // Open "{{ block.entity|upper }}_1000" file - pfid = $fopen("{{ block.entity|upper }}_1000.txt", "r"); // VAL + // Open "1000" file + pfid = $fopen("1000.txt", "r"); // VAL // Read and ignore description field pr = $fscanf(pfid, "%s\n", preg_in); @@ -57,7 +57,7 @@ always @(posedge clk_i) begin STATE_IDLE: begin // Wait until the TABLE_ADDRESS_WSTB is active - if (TABLE_ADDRESS_wstb == 1) begin + if (dma_req_o == 1) begin STATE <= STATE_TABLE_ADDR; end end diff --git a/common/templates/module.tcl.jinja2 b/common/templates/module.tcl.jinja2 index efa383d35..38d7eed3a 100644 --- a/common/templates/module.tcl.jinja2 +++ b/common/templates/module.tcl.jinja2 @@ -12,6 +12,7 @@ array set tests { # add the module vhd code add_files -norecurse $TOP_DIR/modules/{{block.entity}}/hdl +set_property -quiet FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/modules/{{block.entity}}/hdl/*.vhd] # read xci files for any IP required by module {% if block.ip %} diff --git a/docs/index.rst b/docs/index.rst index e9d312fef..b98cca08a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,3 +73,4 @@ framework reference/changelog reference/glossary reference/testing + reference/cocotb diff --git a/docs/reference/cocotb.rst b/docs/reference/cocotb.rst new file mode 100644 index 000000000..50d725bb6 --- /dev/null +++ b/docs/reference/cocotb.rst @@ -0,0 +1,148 @@ +.. _cocotb_reference: + +Testing with cocotb +=================== + +Any module with a `timing.ini` file can be tested using the **cocotb** test runner. + +About cocotb +------------ + +**Cocotb** is an open-source Python library for simulating VHDL or Verilog +designs. It allows you to write and drive testbenches in Python, using a +simulator like **GHDL** or **NVC** under the hood. + +Running the tests +----------------- + +You can run the tests via the following command:: + + make cocotb_tests MODULE=module1,module2,... TEST="test1,,test2,,..." SIMULATOR=nvc/ghdl + +By default, all testbenches will be run, and the NVC simulator will be used. +When parsing multiple tests, a double comma must be used to seperate them as +the names of some tests contain commas. + + +Results +------- + +A simulation build directory will be created for every module tested in a run. +This directory is called `sim_build_{module_name}`, and contains subdirectories +for each test that was run. + +.. image:: sim_build_clock.png + +Timing Errors +~~~~~~~~~~~~~ + +If a condition fails, a timing error is recorded. A test has failed if any +timing errors have occurred during its execution. + +As well as being displayed in the terminal, any timing errors will be saved to +a file in the subdirectory of the test where the error occurred. + +.. image:: errors_csv.png + +A table of expected vs actual signal values is produced and can be found in the +same place, in the form of a `.csv` and `.html` file. This is collected for any +signal whos value is checked at some point during the test. + +.. image:: values_table.png + +Waveforms +~~~~~~~~~ + +A `wave.vcd` file can be found in each tests subdirectory. This can be opened +with **GTKWave** to inspect the signal waveforms. + +.. image:: waveform.png + +Coverage +~~~~~~~~ +A coverage report will be generated if the NVC simulator has been used. This +be found in the coverage subdirectory of the simulation directory. + +.. image:: coverage_report.png + +How It Works +------------ + +The runner utilizes a module's `timing.ini` file to create a schedule for: + +- Assigning input signals. +- Checking conditions on output signals. + +The runner uses Cocotb to simulate and verify the module according to this +schedule. To set up tests and initialize signals correctly, some information +about the module is needed. Most of this information can be derived from the +module's `block.ini` file. For more complex modules (e.g. those dependent on +other HDL files), some additional information might be necessary, such as: + +- Paths to required HDL files. +- Specific signal details. +- Name of top-level entity + +This extra information is provided in a `test_config.py` file located in the +module directory. + +Script Arguments and Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The runner script can be run directly without using `make`:: + + python3 common/python/cocotb_timing_test_runner.py {module} {test} + +**Positional Arguments**: + + - `module`: The name of the module to test. Multiple modules can be specified, separated by commas. Use `all` to test all modules with a `timing.ini` file. + - `test` (optional): The name of a specific test to run. + +**Optional Flags**: + + - `--sim {nvc/ghdl}`: Specify the simulator (default: `nvc`). With NVC, coverage data will be collected. + - `--skip {module1,module2,...}`: List of modules to skip during testing. + - `--panda-build-dir {path}`: Specify the directory where autogenerated HDL files (e.g., from `make autogen`) are located. Defaults to `/build`. + - `-c`: Save expected and actual output signal values in a `.csv` and `.html` file. + +Writing Tests for New Modules +----------------------------- + +To test a new module using this runner, ensure the module directory contains: + + - `{module}.block.ini`: Describes signals involved in the module. + - `{module}.timing.ini`: Contains schedule for signal assignments and condition checks. + +Add a `test_config.py` file (if needed) to include: + + - Paths to additional HDL files. + - Any extra signal information not covered in `block.ini`. + - Top-level entity to be tested. + +Modules with `timing.ini` files are automatically identified as testable. The +module may fail tests until `block.ini` and `test_config.py` are configured +correctly. + +Modules using IP are currently unsupported. + +More details +------------ + +Cocotb provides a Python simulator object to: + +1. **Build the Simulation**: The `.build()` method runs the analysis and evaluation stages. +2. **Run the Test**: The `.test()` method starts the simulation. + +**Key points**: + +- The `test()` method takes a `test_module` argument to locate the test. +- Cocotb scans the test module for functions decorated with `@cocotb.test()`, which define the test logic. + +In this setup: + +- The test module is `cocotb_simulate_test.py`. +- The decorated function is `module_timing_test(dut)`. +- Cocotb calls this function, passing the **Design Under Test (DUT)** object, which provides access to the module's signals for assignments and condition checks. + +Timing is measured by counting clock signal rising edges to synchronize +assignments and condition checks. diff --git a/docs/reference/coverage_report.png b/docs/reference/coverage_report.png new file mode 100644 index 000000000..d9dafcd8b Binary files /dev/null and b/docs/reference/coverage_report.png differ diff --git a/docs/reference/errors_csv.png b/docs/reference/errors_csv.png new file mode 100644 index 000000000..99669dbf2 Binary files /dev/null and b/docs/reference/errors_csv.png differ diff --git a/docs/reference/testing.rst b/docs/reference/testing.rst index 61a7aa52f..873ef079a 100644 --- a/docs/reference/testing.rst +++ b/docs/reference/testing.rst @@ -23,12 +23,14 @@ The python simulation tests, can be run with the following Makefile command:: HDL tests ~~~~~~~~~ -There are two Makefile functions which can be used to run the hdl testbenches:: +There are three Makefile functions which can be used to run the hdl testbenches:: make hdl_test MODULES="module name" make single_hdl_test TEST="MODULE_NAME TEST_NUMBER" + make cocotb_tests MODULE="module1,module2,..." TEST="test1,test2,..." SIMULATOR="nvc/ghdl" + The first, by default, will run every testbench. However if the optional argument of MODULES is given it will instead run every test for the specified module. Please note that the module name is the entity name for the top level @@ -36,3 +38,9 @@ hdl filein that module. The second command will run a single testbench as specified by the module name, and the test number separated by a space. + +The third command will run testbenches using cocotb. By default, all tests for +all modules will be run, and the NVC simulator will be used, as this provides +coverage reporting whereas GHDL does not. Multiple modules. Multiple tests can +be passed, separated by a double comma, as some of the test names contain a +comma. \ No newline at end of file diff --git a/docs/reference/values_table.png b/docs/reference/values_table.png new file mode 100644 index 000000000..42e6833c2 Binary files /dev/null and b/docs/reference/values_table.png differ diff --git a/docs/reference/waveform.png b/docs/reference/waveform.png new file mode 100644 index 000000000..e0f1f89f7 Binary files /dev/null and b/docs/reference/waveform.png differ diff --git a/modules/counter/hdl/counter.vhd b/modules/counter/hdl/counter.vhd index 58ed13df1..e3b35bdab 100644 --- a/modules/counter/hdl/counter.vhd +++ b/modules/counter/hdl/counter.vhd @@ -71,7 +71,7 @@ signal MAX_VAL : signed(31 downto 0) := c_max_val; signal MIN_VAL : signed(31 downto 0) := c_min_val; signal counter_carry : std_logic; signal carry_latch : std_logic; -signal carry_end : std_logic; +signal carry_end : std_logic := '0'; begin diff --git a/modules/pcap/hdl/pcap_arming.vhd b/modules/pcap/hdl/pcap_arming.vhd index 888bd493c..5a1eae2d4 100644 --- a/modules/pcap/hdl/pcap_arming.vhd +++ b/modules/pcap/hdl/pcap_arming.vhd @@ -46,7 +46,7 @@ signal timestamp : unsigned(63 downto 0); signal enable_prev : std_logic; signal enable_fall : std_logic; signal abort_trig : std_logic; -signal pcap_armed : std_logic; +signal pcap_armed : std_logic := '0'; signal disable_armed : std_logic; signal first_enable : std_logic := '0'; diff --git a/modules/pcap/hdl/pcap_buffer.vhd b/modules/pcap/hdl/pcap_buffer.vhd index d6682dd72..06fee72e7 100644 --- a/modules/pcap/hdl/pcap_buffer.vhd +++ b/modules/pcap/hdl/pcap_buffer.vhd @@ -50,13 +50,13 @@ constant c_bits1 : std_logic_vector(3 downto 0) := "1000"; -- 8 constant c_bits2 : std_logic_vector(3 downto 0) := "1001"; -- 9 constant c_bits3 : std_logic_vector(3 downto 0) := "1010"; -- 10 -signal ongoing_trig : std_logic; +signal ongoing_trig : std_logic := '0'; signal mask_length : unsigned(5 downto 0) := "000000"; signal mask_addra : unsigned(5 downto 0) := "000000"; signal mask_addrb : unsigned(5 downto 0); signal mask_doutb : std_logic_vector(31 downto 0); -signal trig_dly : std_logic; -signal ongoing_trig_dly : std_logic; +signal trig_dly : std_logic := '0'; +signal ongoing_trig_dly : std_logic := '0'; begin diff --git a/modules/pcap/hdl/pcap_core.vhd b/modules/pcap/hdl/pcap_core.vhd index 234267acd..21da34063 100644 --- a/modules/pcap/hdl/pcap_core.vhd +++ b/modules/pcap/hdl/pcap_core.vhd @@ -68,7 +68,7 @@ signal pcap_buffer_error: std_logic; signal pcap_error : std_logic; signal pcap_status : std_logic_vector(2 downto 0); signal pcap_dat_valid : std_logic; -signal pcap_armed : std_logic; +signal pcap_armed : std_logic := '0'; signal trig_en : std_logic; diff --git a/modules/pcap/hdl/pcap_core_wrapper.vhd b/modules/pcap/hdl/pcap_core_wrapper.vhd index 4317a532d..c64450329 100644 --- a/modules/pcap/hdl/pcap_core_wrapper.vhd +++ b/modules/pcap/hdl/pcap_core_wrapper.vhd @@ -45,8 +45,8 @@ end pcap_core_wrapper; architecture rtl of pcap_core_wrapper is -signal pos_bus : pos_bus_t; -signal extbus : extbus_t; +signal pos_bus : pos_bus_t := (others => (others => '0')); +signal extbus : extbus_t := (others => (others => '0')); signal count : integer := 0; begin diff --git a/modules/pcap/pcap_sim.py b/modules/pcap/pcap_sim.py index 1c630a7e5..dd8bc5143 100644 --- a/modules/pcap/pcap_sim.py +++ b/modules/pcap/pcap_sim.py @@ -168,7 +168,7 @@ def __init__(self, idx, shift): def latch_value(self, ts): self.prev_ts = ts - self.prev_value = self.value**2 + self.prev_value = int(self.value)**2 def yield_data(self): data =int(self.data >> self.shift) @@ -529,7 +529,8 @@ def push_data(self, ts, new_data): self.pend_error = ts + 4 else: new_size = len(new_data) - self.buf[self.buf_len:self.buf_len + new_size] = new_data + self.buf[self.buf_len:self.buf_len + new_size] = [np.int32(item) if item < 2**31 else np.int32(np.uint32(item)) + for item in new_data] self.buf_len += len(new_data) def read_data(self, max_length): diff --git a/modules/pcap/test_config.py b/modules/pcap/test_config.py new file mode 100644 index 000000000..6d74c65df --- /dev/null +++ b/modules/pcap/test_config.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +EXTRA_HDL_FILES = [TOP_PATH / 'common' / 'hdl' / 'defines' / 'support.vhd', + TOP_PATH / 'common' / 'hdl' / 'defines' / 'top_defines.vhd', + TOP_PATH / 'common' / 'hdl' / 'defines' / 'operator.vhd', + TOP_PATH / 'common' / 'hdl' / 'fifo.vhd', + TOP_PATH / 'common' / 'hdl' / 'spbram.vhd', + BUILD_PATH / 'apps' / 'PandABox-fmc_lback-sfp_lback' / 'autogen' / 'hdl' / 'top_defines_gen.vhd'] + +TOP_LEVEL = 'pcap_core_wrapper' + +EXTRA_SIGNALS_INFO = { + 'ARM': {'type': 'bit_mux', 'name': 'arm', 'wstb_name': 'arm'}, + 'DISARM': {'type': 'bit_mux', 'name': 'disarm', 'wstb_name': 'disarm'}, + 'START_WRITE': {'type': 'bit_mux', 'name': 'start_write', + 'wstb_name': 'start_write'}, + 'WRITE': {'type': 'bit_mux', 'name': 'write', 'wstb_name': 'write_wstb'}, + 'POS': {'type': 'bus', 'name': 'pos_bus_i', 'bits': 32, 'bus_width': 26}, + 'ACTIVE': {'type': 'bit_out', 'name': 'pcap_actv_o'}, + 'DATA': {'type': 'valid_data', 'name': 'pcap_dat_o', + 'valid_name': 'pcap_dat_valid_o'}, + 'dma_full_i': {'type': 'bit_mux', 'name': 'dma_full_i'}, + 'extbus_i': {'type': 'bus', 'name': 'extbus_i'}, + 'BIT': {'type': 'bus', 'name': 'bit_bus_i', 'bits': 1, 'bus_width': 128} +} diff --git a/modules/pcomp/test_config.py b/modules/pcomp/test_config.py new file mode 100644 index 000000000..3a660020f --- /dev/null +++ b/modules/pcomp/test_config.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +EXTRA_HDL_FILES = [TOP_PATH / 'common' / 'hdl' / 'defines' / 'support.vhd'] diff --git a/modules/pgen/PGEN_1000.txt b/modules/pgen/1000.txt similarity index 100% rename from modules/pgen/PGEN_1000.txt rename to modules/pgen/1000.txt diff --git a/modules/pgen/hdl/pgen.vhd b/modules/pgen/hdl/pgen.vhd index ce184b979..6e906f328 100644 --- a/modules/pgen/hdl/pgen.vhd +++ b/modules/pgen/hdl/pgen.vhd @@ -51,20 +51,6 @@ end pgen; architecture rtl of pgen is -component fifo_1K32 -port ( - clk : in std_logic; - srst : in std_logic; - din : in std_logic_vector(31 DOWNTO 0); - wr_en : in std_logic; - rd_en : in std_logic; - dout : out std_logic_vector(31 DOWNTO 0); - full : out std_logic; - empty : out std_logic; - data_count : out std_logic_vector(9 downto 0) -); -end component; - type state_t is (IDLE, WAIT_FIFO, DMA_REQ, DMA_READ, IS_FINISHED, FINISHED); signal pgen_fsm : state_t; signal reset : std_logic; @@ -78,8 +64,10 @@ signal fifo_rd_en : std_logic; signal fifo_dout : std_logic_vector(DW-1 downto 0); signal fifo_count : integer range 0 to 1023; signal fifo_full : std_logic; +signal write_ready_o : std_logic; signal fifo_empty : std_logic; -signal fifo_data_count : std_logic_vector(9 downto 0); +signal read_valid_o : std_logic; +signal fifo_data_count : std_logic_vector(10 downto 0); signal fifo_available : std_logic; signal trig : std_logic; @@ -96,36 +84,38 @@ signal table_end : std_logic; signal active : std_logic := '0'; +signal out_buffer :std_logic_vector(DW-1 downto 0) := (others => '0'); begin -- Assign outputs dma_len_o <= std_logic_vector(dma_len(7 downto 0)); dma_addr_o <= std_logic_vector(dma_addr); -out_o <= fifo_dout; +out_o <= out_buffer; -- Reset for state machine reset <= not table_ready or enable_fall; --- --- 32bit FIFO with 1K sample depth --- -dma_fifo_inst : fifo_1K32 -port map ( - srst => fifo_reset, - clk => clk_i, - din => dma_data_i, - wr_en => dma_valid_i, - rd_en => fifo_rd_en, - dout => fifo_dout, - full => fifo_full, - empty => fifo_empty, - data_count => fifo_data_count +dma_fifo_inst : entity work.fifo generic map( + data_width => 32, + fifo_bits => 10 +) port map ( + clk_i => clk_i, + reset_fifo_i => fifo_reset, + write_data_i => dma_data_i, + write_valid_i => dma_valid_i, + read_ready_i => fifo_rd_en, + read_data_o => fifo_dout, + write_ready_o => write_ready_o, + read_valid_o => read_valid_o, + std_logic_vector(fifo_depth_o) => fifo_data_count ); fifo_reset <= reset; fifo_rd_en <= trig_pulse; fifo_count <= to_integer(unsigned(fifo_data_count)); +fifo_full <= not write_ready_o; +fifo_empty <= not read_valid_o; -- There is space (>256 words) in the fifo, so perform data read from -- host memory. @@ -141,6 +131,16 @@ process(clk_i) begin end if; end process; +process(clk_i) begin + if rising_edge(clk_i) then + if reset = '1' then + out_buffer <= (others => '0'); + elsif trig_pulse = '1' then + out_buffer <= fifo_dout; + end if; + end if; +end process; + -- Trigger pulse pops data from fifo and tick data counter when block -- is enabled and table is ready. trig_pulse <= (trig_i and not trig) and active and table_ready; diff --git a/modules/pgen/pgen.block.ini b/modules/pgen/pgen.block.ini index bb9d58634..0d7a4590c 100644 --- a/modules/pgen/pgen.block.ini +++ b/modules/pgen/pgen.block.ini @@ -2,7 +2,6 @@ description: Position generator entity: pgen type: dma -ip: fifo_1K32 [ENABLE] type: bit_mux diff --git a/modules/pgen/pgen.timing.ini b/modules/pgen/pgen.timing.ini index 27b8e545e..381736fa6 100644 --- a/modules/pgen/pgen.timing.ini +++ b/modules/pgen/pgen.timing.ini @@ -3,50 +3,49 @@ description: Timing diagrams for the pgen block scope: pgen.block.ini [Normal operation] -3 : REPEATS=2 -4 : TABLE_ADDRESS=PGEN_1000.txt -5 : TABLE_LENGTH=40 -6 : ENABLE=1 -> ACTIVE=1 -10 : TRIG=1 -> OUT=10 -11 : TRIG=0 -12 : TRIG=1 -> OUT=11 -13 : TRIG=0 -14 : TRIG=1 -> OUT=12 -15 : TRIG=0 -16 : TRIG=1 -> OUT=13 -17 : TRIG=0 -18 : TRIG=1 -> OUT=14 -19 : TRIG=0 -20 : TRIG=1 -> OUT=15 -21 : TRIG=0 -22 : TRIG=1 -> OUT=16 -23 : TRIG=0 -24 : TRIG=1 -> OUT=21 -25 : TRIG=0 -26 : TRIG=1 -> OUT=52 -27 : TRIG=0 -28 : TRIG=1 -> OUT=32 -29 : TRIG=0 -30 : TRIG=1 -> OUT=10 -31 : TRIG=0 -32 : TRIG=1 -> OUT=11 -33 : TRIG=0 -34 : TRIG=1 -> OUT=12 -35 : TRIG=0 -36 : TRIG=1 -> OUT=13 -37 : TRIG=0 -38 : TRIG=1 -> OUT=14 -39 : TRIG=0 -40 : TRIG=1 -> OUT=15 -41 : TRIG=0 -42 : TRIG=1 -> OUT=16 -43 : TRIG=0 -44 : TRIG=1 -> OUT=21 -45 : TRIG=0 -46 : TRIG=1 -> OUT=52 -47 : TRIG=0 -48 : TRIG=1 -> OUT=32, ACTIVE=0 -49 : TRIG=0 -50 : TRIG=1 -51 : TRIG=0 - +1 : REPEATS=2 +2 : TABLE_ADDRESS=1000 +3 : TABLE_LENGTH=40 +10 : ENABLE=1 -> ACTIVE=1 +11 : TRIG=1 -> OUT=10 +12 : TRIG=0 +13 : TRIG=1 -> OUT=11 +14 : TRIG=0 +15 : TRIG=1 -> OUT=12 +16 : TRIG=0 +17 : TRIG=1 -> OUT=13 +18 : TRIG=0 +19 : TRIG=1 -> OUT=14 +20 : TRIG=0 +21 : TRIG=1 -> OUT=15 +22 : TRIG=0 +23 : TRIG=1 -> OUT=16 +24 : TRIG=0 +25 : TRIG=1 -> OUT=21 +26 : TRIG=0 +27 : TRIG=1 -> OUT=52 +28 : TRIG=0 +29 : TRIG=1 -> OUT=32 +30 : TRIG=0 +31 : TRIG=1 -> OUT=10 +32 : TRIG=0 +33 : TRIG=1 -> OUT=11 +34 : TRIG=0 +35 : TRIG=1 -> OUT=12 +36 : TRIG=0 +37 : TRIG=1 -> OUT=13 +38 : TRIG=0 +39 : TRIG=1 -> OUT=14 +40 : TRIG=0 +41 : TRIG=1 -> OUT=15 +42 : TRIG=0 +43 : TRIG=1 -> OUT=16 +44 : TRIG=0 +45 : TRIG=1 -> OUT=21 +46 : TRIG=0 +47 : TRIG=1 -> OUT=52 +48 : TRIG=0 +49 : TRIG=1 -> OUT=32, ACTIVE=0 +50 : TRIG=0 +51 : TRIG=1 +52 : TRIG=0 diff --git a/modules/pgen/pgen_sim.py b/modules/pgen/pgen_sim.py index ec21b339b..f65c28478 100644 --- a/modules/pgen/pgen_sim.py +++ b/modules/pgen/pgen_sim.py @@ -23,7 +23,8 @@ def on_changes(self, ts, changes): if NAMES.TABLE_ADDRESS in changes: # open the table file_dir = os.path.join( - os.path.dirname(__file__), self.TABLE_ADDRESS) + os.path.dirname(__file__), + '{}.txt'.format(self.TABLE_ADDRESS)) assert os.path.isfile(file_dir), "%s does not exist" % file_dir with open(file_dir, "r") as table: diff --git a/modules/pgen/test_config.py b/modules/pgen/test_config.py new file mode 100644 index 000000000..472f7504c --- /dev/null +++ b/modules/pgen/test_config.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +EXTRA_HDL_FILES = [TOP_PATH / 'common' / 'hdl' / 'defines' / 'support.vhd', + TOP_PATH / 'common' / 'hdl' / 'defines' / 'top_defines.vhd', + BUILD_PATH / 'apps' / 'PandABox-fmc_lback-sfp_lback' / 'autogen' / 'hdl' / 'top_defines_gen.vhd', + TOP_PATH / 'common' / 'hdl' / 'fifo.vhd'] diff --git a/modules/posenc/hdl/posenc.vhd b/modules/posenc/hdl/posenc.vhd index 0764212c8..9d78b418f 100644 --- a/modules/posenc/hdl/posenc.vhd +++ b/modules/posenc/hdl/posenc.vhd @@ -2,8 +2,6 @@ library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; -library unisim; -use unisim.vcomponents.all; entity posenc is port ( diff --git a/modules/posenc/test_config.py b/modules/posenc/test_config.py new file mode 100644 index 000000000..78650a619 --- /dev/null +++ b/modules/posenc/test_config.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +EXTRA_HDL_FILES = [TOP_PATH / 'common' / 'hdl' / 'qenc.vhd', + TOP_PATH / 'common' / 'hdl' / 'qencoder.vhd', + TOP_PATH / 'common' / 'hdl' / 'prescaler_pos.vhd'] diff --git a/modules/pulse/hdl/pulse.vhd b/modules/pulse/hdl/pulse.vhd index 71e794e63..f141e6e2f 100644 --- a/modules/pulse/hdl/pulse.vhd +++ b/modules/pulse/hdl/pulse.vhd @@ -38,20 +38,6 @@ end pulse; architecture rtl of pulse is --- The pulse queue; keeps track of the timestamps of incoming pulses -component pulse_queue -port ( - clk : in std_logic; - srst : in std_logic; - din : in std_logic_vector(48 DOWNTO 0); - wr_en : in std_logic; - rd_en : in std_logic; - dout : out std_logic_vector(48 DOWNTO 0); - full : out std_logic; - empty : out std_logic; - data_count : out std_logic_vector(8 downto 0) -); -end component; -- Variable declarations @@ -66,7 +52,9 @@ signal got_trigger : std_logic := '0'; signal pulse : std_logic := '0'; signal pulse_override : std_logic := '0'; signal pulse_queued_empty : std_logic := '0'; +signal read_valid_o : std_logic := '1'; signal pulse_queued_full : std_logic := '0'; +signal write_ready_o : std_logic := '1'; signal pulse_queued_reset : std_logic := '0'; signal pulse_queued_rstb : std_logic := '0'; signal pulse_queued_wstb : std_logic := '0'; @@ -100,21 +88,25 @@ signal edges_remaining : unsigned(31 downto 0) := (others => '0'); begin -- The pulse queue; keeps track of the timestamps of incoming pulses, maps to component above, attached to this architecture -pulse_queue_inst : pulse_queue -port map ( - clk => clk_i, - srst => pulse_queued_reset, - din => pulse_queued_din, - wr_en => pulse_queued_wstb, - rd_en => pulse_queued_rstb, - dout => pulse_queued_dout, - full => pulse_queued_full, - empty => pulse_queued_empty, - data_count => pulse_queued_data_count +pulse_queue_inst : entity work.fifo generic map( + data_width => 49, + fifo_bits => 8 +) port map ( + clk_i => clk_i, + reset_fifo_i => pulse_queued_reset, + write_data_i => pulse_queued_din, + write_valid_i => pulse_queued_wstb, + read_ready_i => pulse_queued_rstb, + read_data_o => pulse_queued_dout, + write_ready_o => write_ready_o, + read_valid_o => read_valid_o, + std_logic_vector(fifo_depth_o) => pulse_queued_data_count ); -- Bits relating to the FIFO queue +pulse_queued_full <= not write_ready_o; +pulse_queued_empty <= not read_valid_o; queue_pulse_ts <= unsigned(pulse_queued_dout(47 downto 0)); queue_pulse_value <= pulse_queued_dout(48); pulse_queued_reset <= not is_enabled; diff --git a/modules/pulse/pulse.block.ini b/modules/pulse/pulse.block.ini index 6393a3bff..41463533c 100644 --- a/modules/pulse/pulse.block.ini +++ b/modules/pulse/pulse.block.ini @@ -1,7 +1,6 @@ [.] description: One-shot pulse delay and stretch entity: pulse -ip: pulse_queue [ENABLE] type: bit_mux diff --git a/modules/pulse/test_config.py b/modules/pulse/test_config.py new file mode 100644 index 000000000..472f7504c --- /dev/null +++ b/modules/pulse/test_config.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +EXTRA_HDL_FILES = [TOP_PATH / 'common' / 'hdl' / 'defines' / 'support.vhd', + TOP_PATH / 'common' / 'hdl' / 'defines' / 'top_defines.vhd', + BUILD_PATH / 'apps' / 'PandABox-fmc_lback-sfp_lback' / 'autogen' / 'hdl' / 'top_defines_gen.vhd', + TOP_PATH / 'common' / 'hdl' / 'fifo.vhd'] diff --git a/modules/qdec/test_config.py b/modules/qdec/test_config.py new file mode 100644 index 000000000..af392fe79 --- /dev/null +++ b/modules/qdec/test_config.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +EXTRA_HDL_FILES = [TOP_PATH / 'common' / 'hdl' / 'qdec.vhd', + TOP_PATH / 'common' / 'hdl' / 'qdecoder.vhd'] diff --git a/modules/seq/test_config.py b/modules/seq/test_config.py new file mode 100644 index 000000000..7a5c0beca --- /dev/null +++ b/modules/seq/test_config.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +EXTRA_HDL_FILES = [TOP_PATH / 'common' / 'hdl' / 'defines' / 'support.vhd', + TOP_PATH / 'common' / 'hdl' / 'defines' / 'top_defines.vhd', + TOP_PATH / 'common' / 'hdl' / 'spbram.vhd', + BUILD_PATH / 'apps' / 'PandABox-fmc_lback-sfp_lback' / 'autogen' / 'hdl' / 'top_defines_gen.vhd'] diff --git a/modules/srgate/test_config.py b/modules/srgate/test_config.py new file mode 100644 index 000000000..9a5b7e9b7 --- /dev/null +++ b/modules/srgate/test_config.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +# EXTRA_HDL_FILES = [] diff --git a/tests/hdl/regression_tests.tcl b/tests/hdl/regression_tests.tcl index 033e38ef4..9c39f1aa5 100644 --- a/tests/hdl/regression_tests.tcl +++ b/tests/hdl/regression_tests.tcl @@ -56,7 +56,8 @@ add_files -norecurse \ $TOP_DIR/common/hdl/defines \ $APP_BUILD_DIR/autogen/hdl/top_defines_gen.vhd -set_property FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/common/hdl/defines/top_defines.vhd] +set_property FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/common/hdl/defines/*.vhd] +set_property FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/common/hdl/*.vhd] # Loop through all the tests foreach test [array names tests] { diff --git a/tests/hdl/single_test.tcl b/tests/hdl/single_test.tcl index e7f581af4..8c6eb2558 100755 --- a/tests/hdl/single_test.tcl +++ b/tests/hdl/single_test.tcl @@ -35,7 +35,8 @@ add_files -norecurse \ $TOP_DIR/common/hdl/defines \ $APP_BUILD_DIR/autogen/hdl/top_defines_gen.vhd -set_property FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/common/hdl/defines/top_defines.vhd] +set_property FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/common/hdl/defines/*.vhd] +set_property FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/common/hdl/*.vhd] puts "###############################################################################################"; puts " $test" ; diff --git a/tests/python/test_data/timing-expected/testblock.tcl b/tests/python/test_data/timing-expected/testblock.tcl index cbca64906..b8a48b51c 100644 --- a/tests/python/test_data/timing-expected/testblock.tcl +++ b/tests/python/test_data/timing-expected/testblock.tcl @@ -11,6 +11,7 @@ array set tests { # add the module vhd code add_files -norecurse $TOP_DIR/modules/testblock/hdl +set_property -quiet FILE_TYPE "VHDL 2008" [get_files $TOP_DIR/modules/testblock/hdl/*.vhd] # read xci files for any IP required by module diff --git a/tests/test_python_sim_timing.py b/tests/test_python_sim_timing.py index d7317da13..66e1900d3 100755 --- a/tests/test_python_sim_timing.py +++ b/tests/test_python_sim_timing.py @@ -9,11 +9,13 @@ import sys import os -import imp +import importlib.util import numpy import unittest +from pathlib import Path + from common.python.ini_util import read_ini, timing_entries ROOT = os.path.join(os.path.dirname(__file__), "..") @@ -46,11 +48,13 @@ def __init__(self, module_path, timing_ini, timing_section): def runTest(self): # Load _sim.py into common.python._sim - file, pathname, description = imp.find_module( - self.block_name + "_sim", [self.module_path]) - mod = imp.load_module( - "common.python." + self.block_name, - file, pathname, description) + mod_name = self.block_name + "_sim" + mod_spec = importlib.util.spec_from_file_location( + mod_name, + self.module_path + '/' + self.block_name + "_sim.py") + mod = importlib.util.module_from_spec( + mod_spec) + mod_spec.loader.exec_module(mod) # Make instance of Simulation block = getattr(mod, self.block_name.title() + "Simulation")() # Start prodding the block and checking its outputs @@ -118,7 +122,9 @@ def runTest(self): # Check all outputs have correct field values for name in outputs: - expected = numpy.int32(int(outputs[name], 0)) + expected_val = int(outputs[name], 0) + expected = numpy.int32( + expected_val if expected_val < 2**31 else numpy.uint32(expected_val)) actual = getattr(block, name) assert actual == expected, \ "%d: Attr %s = %d != %d" % (