add docstrings

This commit is contained in:
fischerm 2024-01-07 15:13:35 -08:00
parent 4c48035201
commit 61d6479805
4 changed files with 134 additions and 20 deletions

View File

@ -8,7 +8,29 @@ from .playback import LogicAnalyzerPlayback
class LogicAnalyzerCore(Elaboratable):
""" """
"""A logic analzyer, implemented in the FPGA fabric. Connects to the rest of the cores
over Manta's internal bus, and may be operated from a user's machine through the Python API.
Parameters:
----------
config : dict
Configuration options. This is taken from the section of Manta's configuration YAML that
describes the core.
base_addr : int
Where to place the core in Manta's internal memory map. This determines the beginning of
the core's address space. The end of the core's address space may be obtained by calling
the get_max_addr() method.
interface : UARTInterface or EthernetInterface
The interface used to communicate with the core.
Attributes:
----------
None
"""
def __init__(self, config, base_addr, interface):
self.config = config
@ -39,15 +61,6 @@ class LogicAnalyzerCore(Elaboratable):
self.config, self.trig_blk.get_max_addr() + 1, interface
)
# Top-Level Probes:
for name, width in self.config["probes"].items():
if hasattr(self, name):
raise ValueError(
f"Unable to assign probe name '{name}' as it clashes with a reserved name in the backend. Please rename the probe."
)
setattr(self, name, Signal(width, name=name))
def check_config(self, config):
# Check for unrecognized options
valid_options = [
@ -196,16 +209,31 @@ class LogicAnalyzerCore(Elaboratable):
if p.name == name:
return p
raise ValueError(f"Probe '{name}' not found in Logic Analyzer core.")
def get_max_addr(self):
return self.sample_mem.get_max_addr()
def capture(self, verbose=False):
"""Perform a capture, recording the state of all input probes to the FPGA's memory, and
then reading that out on the host.
Parameters:
----------
verbose : bool
Whether or not to print the status of the capture to stdout as it progresses.
Defaults to False.
Returns:
----------
An instance of LogicAnalyzerCapture.
"""
print_if_verbose = lambda x: print(x) if verbose else None
# If core is not in IDLE state, request that it return to IDLE
print_if_verbose(" -> Resetting core...")
state = self.fsm.r.get_probe("state")
if state != self.states["IDLE"]:
if state != self.fsm.states["IDLE"]:
self.fsm.r.set_probe("request_stop", 0)
self.fsm.r.set_probe("request_stop", 1)
self.fsm.r.set_probe("request_stop", 0)
@ -215,7 +243,7 @@ class LogicAnalyzerCore(Elaboratable):
# Set triggers
print_if_verbose(" -> Setting triggers...")
self.trig_blk.set_triggers()
self.trig_blk.set_triggers(self.config)
# Set trigger mode, default to single-shot if user didn't specify a mode
print_if_verbose(" -> Setting trigger mode...")
@ -258,14 +286,52 @@ class LogicAnalyzerCore(Elaboratable):
class LogicAnalyzerCapture:
"""A container for the data collected during a capture from a LogicAnalyzerCore. Contains
methods for exporting the data as a VCD waveform file, or as a Verilog module for playing
back captured data in simulation/synthesis.
Parameters:
----------
data : list[int]
The raw captured data taken by the LogicAnalyzerCore. This consists of the values of
all the input probes concatenated together at every timestep.
config : dict
The configuration of the LogicAnalyzerCore that took this capture.
"""
def __init__(self, data, config):
self.data = data
self.config = config
def get_trigger_location(self):
"""Gets the location of the trigger in the capture. This will match the value of
"trigger_location" provided in the configuration file at the time of capture.
Parameters:
----------
None
Returns:
----------
The trigger location as an `int`.
"""
return self.config["trigger_location"]
def get_trace(self, probe_name):
"""Gets the value of a single probe over the capture.
Parameters:
----------
probe_name : int
The name of the probe in the LogicAnalyzer Core. This must match the name provided
in the configuration file.
Returns:
----------
The value of the probe at every timestep in the capture, as a list of integers.
"""
# sum up the widths of all the probes below this one
lower = 0
for name, width in self.config["probes"].items():
@ -282,6 +348,19 @@ class LogicAnalyzerCapture:
return [int(b[lower:upper], 2) for b in binary]
def export_vcd(self, path):
"""Export the capture to a VCD file, containing the data of all probes in the core.
Parameters:
----------
path : str
The path of the output file, including the ".vcd" file extension.
Returns:
----------
None
"""
from vcd import VCDWriter
from datetime import datetime
@ -310,7 +389,7 @@ class LogicAnalyzerCapture:
writer.change(clock, timestamp, timestamp % 2 == 0)
# set the trigger
triggered = (timestamp // 2) >= self.get_trigger_loc()
triggered = (timestamp // 2) >= self.get_trigger_location()
writer.change(trigger, timestamp, triggered)
# add other signals
@ -322,11 +401,35 @@ class LogicAnalyzerCapture:
vcd_file.close()
def export_playback_module(self):
def get_playback_module(self):
"""Gets an Amaranth module that will playback the captured data. This module is
synthesizable, so it may be used in either simulation or synthesis.
Parameters:
----------
None
Returns:
----------
An instance of LogicAnalyzerPlayback, which is a synthesizable Amaranth module.
"""
return LogicAnalyzerPlayback(self.data, self.config)
def export_playback_verilog(self, path):
lap = self.export_playback_module()
"""Exports a Verilog module that will playback the captured data. This module is
synthesizable, so it may be used in either simulation or synthesis.
Parameters:
----------
path : str
The path of the output file, including the ".v" file extension.
Returns:
----------
None
"""
lap = self.get_playback_module()
from amaranth.back import verilog
with open(path, "w") as f:

View File

@ -2,6 +2,17 @@ from amaranth import *
class LogicAnalyzerPlayback(Elaboratable):
"""A synthesizable module that plays back data captured by a LogicAnalyzerCore.
Parameters:
----------
data : list[int]
The raw captured data taken by the LogicAnalyzerCore. This consists of the values of
all the input probes concatenated together at every timestep.
config : dict
The configuration of the LogicAnalyzerCore that took this capture.
"""
def __init__(self, data, config):
self.data = data
self.config = config

View File

@ -116,6 +116,7 @@ def xilinx_tools_installed():
(ie, /tools/Xilinx/Vivado/2023.1/bin/vivado, not /tools/Xilinx/Vivado/2023.1/bin)
"""
from shutil import which
return ("VIVADO" in os.environ) or (which("vivado") is not None)
@ -136,9 +137,8 @@ def ice40_tools_installed():
# Check PATH
binaries = ["yosys", "nextpnr-ice40", "icepack", "iceprog"]
from shutil import which
if all([which(b) for b in binaries]):
return True
return False

View File

@ -20,7 +20,7 @@ class LogicAnalyzerCounterTest(Elaboratable):
"la": {
"type": "logic_analyzer",
"sample_depth": 1024,
"trigger_loc": 500,
"trigger_location": 500,
"probes": {"larry": 1, "curly": 3, "moe": 9},
"triggers": ["moe RISING"],
},
@ -76,9 +76,9 @@ class LogicAnalyzerCounterTest(Elaboratable):
@pytest.mark.skipif(not xilinx_tools_installed(), reason="no toolchain installed")
def test_logic_analyzer_core_xilinx():
LogicAnalyzerCounterTest(Nexys4DDRPlatform(), "/dev/ttyUSB2").verify()
LogicAnalyzerCounterTest(Nexys4DDRPlatform(), "/dev/ttyUSB3").verify()
@pytest.mark.skipif(not ice40_tools_installed(), reason="no toolchain installed")
def test_logic_analyzer_core_ice40():
LogicAnalyzerCounterTest(ICEStickPlatform(), "/dev/ttyUSB1").verify()
LogicAnalyzerCounterTest(ICEStickPlatform(), "/dev/ttyUSB2").verify()