|
| 1 | +from collections import defaultdict |
| 2 | +import logging |
| 3 | +import argparse |
| 4 | +from vcd import VCDWriter |
| 5 | +from amaranth import * |
| 6 | +from amaranth.lib.cdc import FFSynchronizer |
| 7 | + |
| 8 | +from ....gateware.pads import * |
| 9 | +from ....gateware.analyzer import * |
| 10 | +from ... import * |
| 11 | +from .signal_compressor import SignalCompressor |
| 12 | +from .arbeiter import LAArbeiter |
| 13 | + |
| 14 | +# This LA uses a simple protocol for sending compressed values over the FIFO: |
| 15 | +# Each packet starts with a 8 bit size word. The size can be 0, then the word only consists of that |
| 16 | +# word. If the size is n != 0, the packet is n*2 bytes long. Each 16bit word is encoded acording |
| 17 | +# to the format described in the SignalCompressor value. The packets are round-robin for each pin. |
| 18 | + |
| 19 | +class BetterLASubtarget(Elaboratable): |
| 20 | + def __init__(self, pads, in_fifo): |
| 21 | + self.pads = pads |
| 22 | + self.in_fifo = in_fifo |
| 23 | + |
| 24 | + self.la = LAArbeiter(in_fifo) |
| 25 | + |
| 26 | + def elaborate(self, platform): |
| 27 | + m = Module() |
| 28 | + m.submodules += self.la |
| 29 | + |
| 30 | + pins_i = Signal.like(self.pads.i_t.i) |
| 31 | + m.submodules += FFSynchronizer(self.pads.i_t.i, pins_i) |
| 32 | + m.d.comb += self.la.input.eq(pins_i) |
| 33 | + |
| 34 | + return m |
| 35 | + |
| 36 | + |
| 37 | +class BetterLAApplet(GlasgowApplet): |
| 38 | + logger = logging.getLogger(__name__) |
| 39 | + help = "capture logic waveforms" |
| 40 | + description = """ |
| 41 | + A somewhat better logic analyzer applet that allows for the capture of traces as VCD files. |
| 42 | + """ |
| 43 | + |
| 44 | + @classmethod |
| 45 | + def add_build_arguments(cls, parser, access): |
| 46 | + super().add_build_arguments(parser, access) |
| 47 | + |
| 48 | + access.add_pin_set_argument(parser, "i", width=range(1, 17), default=1) |
| 49 | + |
| 50 | + def build(self, target, args): |
| 51 | + self.mux_interface = iface = target.multiplexer.claim_interface(self, args) |
| 52 | + iface.add_subtarget(BetterLASubtarget( |
| 53 | + pads=iface.get_pads(args, pin_sets=("i",)), |
| 54 | + in_fifo=iface.get_in_fifo(depth=512*16), |
| 55 | + )) |
| 56 | + |
| 57 | + self._sample_freq = target.sys_clk_freq |
| 58 | + self._pins = getattr(args, "pin_set_i") |
| 59 | + |
| 60 | + @classmethod |
| 61 | + def add_run_arguments(cls, parser, access): |
| 62 | + super().add_run_arguments(parser, access) |
| 63 | + |
| 64 | + g_pulls = parser.add_mutually_exclusive_group() |
| 65 | + g_pulls.add_argument( |
| 66 | + "--pull-ups", default=False, action="store_true", |
| 67 | + help="enable pull-ups on all pins") |
| 68 | + g_pulls.add_argument( |
| 69 | + "--pull-downs", default=False, action="store_true", |
| 70 | + help="enable pull-downs on all pins") |
| 71 | + |
| 72 | + async def run(self, device, args): |
| 73 | + pull_low = set() |
| 74 | + pull_high = set() |
| 75 | + if args.pull_ups: |
| 76 | + pull_high = set(args.pin_set_i) |
| 77 | + if args.pull_downs: |
| 78 | + pull_low = set(args.pin_set_i) |
| 79 | + iface = await device.demultiplexer.claim_interface(self, self.mux_interface, args, |
| 80 | + pull_low=pull_low, pull_high=pull_high) |
| 81 | + return iface |
| 82 | + |
| 83 | + @classmethod |
| 84 | + def add_interact_arguments(cls, parser): |
| 85 | + parser.add_argument( |
| 86 | + "file", metavar="VCD-FILE", type=argparse.FileType("w"), |
| 87 | + help="write VCD waveforms to VCD-FILE") |
| 88 | + |
| 89 | + async def interact(self, device, args, iface): |
| 90 | + pins = defaultdict(list) |
| 91 | + overrun = False |
| 92 | + |
| 93 | + zero_chunks = 0 |
| 94 | + chunks = 0 |
| 95 | + try: # this try catches Ctrl+C for being able to manually interrupt capture |
| 96 | + while not overrun: |
| 97 | + for p in self._pins: |
| 98 | + pkgs = await LAArbeiter.read_chunk(iface.read) |
| 99 | + if len(pkgs) == 0: |
| 100 | + zero_chunks += 1 |
| 101 | + chunks += 1 |
| 102 | + pins[p].extend(pkgs) |
| 103 | + if len(pkgs) > 255 - len(self._pins): |
| 104 | + overrun = True |
| 105 | + print("overrun") |
| 106 | + finally: |
| 107 | + events = [] |
| 108 | + cycles = 0 |
| 109 | + for p, pkgs in pins.items(): |
| 110 | + cycle = 0 |
| 111 | + for pkg in pkgs: |
| 112 | + for value, duration in SignalCompressor.decode_pkg(pkg): |
| 113 | + timestamp = cycle * 1_000_000_000 // self._sample_freq |
| 114 | + events.append((timestamp, p, value)) |
| 115 | + cycle += duration |
| 116 | + cycles = max(cycle, cycles) |
| 117 | + events.sort(key=lambda e: e[0]) |
| 118 | + |
| 119 | + total_pkgs = sum(len(pkgs) for pkgs in pins.values()) |
| 120 | + total_bytes = chunks + total_pkgs * 2 |
| 121 | + |
| 122 | + print(f"captured {cycles} cycles") |
| 123 | + print(f"chunking overhead: {chunks / total_bytes * 100}%") |
| 124 | + print(f"zero chunks overhead: {zero_chunks / total_bytes * 100}%") |
| 125 | + print(f"compression gain: {100 - (total_bytes * 8 / cycle * 100)}%") |
| 126 | + |
| 127 | + |
| 128 | + vcd_writer = VCDWriter(args.file, timescale="1 ns", check_values=False) |
| 129 | + vcd_signals = { |
| 130 | + p: vcd_writer.register_var(scope="", name="pin[{}]".format(p), var_type="wire", |
| 131 | + size=1, init=0) |
| 132 | + for p in pins.keys() |
| 133 | + } |
| 134 | + for timestamp, p, value in events: |
| 135 | + signal = vcd_signals[p] |
| 136 | + vcd_writer.change(signal, timestamp, value) |
| 137 | + vcd_writer.close(timestamp) |
0 commit comments