Skip to content

Commit a5c4a74

Browse files
committed
applets.interface.better_la: add
1 parent e79bad2 commit a5c4a74

6 files changed

Lines changed: 409 additions & 0 deletions

File tree

software/.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"python.testing.pytestArgs": [
3+
"."
4+
],
5+
"python.testing.unittestEnabled": false,
6+
"python.testing.pytestEnabled": true
7+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from typing import Callable
2+
from amaranth import *
3+
from amaranth.lib.fifo import SyncFIFOBuffered
4+
5+
from . import SignalCompressor
6+
7+
class LAArbeiter(Elaboratable):
8+
"""This Logic Analyzer Arbeiter instanciates n Signal compressors and n Fifos and arbeites the
9+
output of the fifos in a round robin fashion. Its output format is one length byte followed by
10+
2*length bytes of compressed channel data. After that the next channel is send with the same
11+
format.
12+
"""
13+
def __init__(self, output_fifo: SyncFIFOBuffered, n_channels=16, pressure_threshold=64):
14+
self.output_fifo = output_fifo
15+
assert output_fifo.width == 8
16+
self.input = Signal(n_channels)
17+
18+
self._pressure_threshold = pressure_threshold
19+
20+
self.fifos = [SyncFIFOBuffered(width=16, depth=256) for _ in range(n_channels)]
21+
self.compressors = [SignalCompressor(self.input[i]) for i in range(n_channels)]
22+
23+
def elaborate(self, platform):
24+
m = Module()
25+
26+
to_transfer = Signal(8)
27+
enough_pressure = Signal(len(self.fifos))
28+
any_enough_pressure = Signal()
29+
m.d.comb += any_enough_pressure.eq(enough_pressure.any())
30+
31+
with m.FSM():
32+
for i in range(len(self.input)):
33+
fifo = self.fifos[i]
34+
compressor = self.compressors[i]
35+
m.submodules[f"fifo_{i}"] = fifo
36+
m.submodules[f"compressor_{i}"] = compressor
37+
38+
m.d.comb += fifo.w_en.eq(compressor.valid)
39+
m.d.comb += fifo.w_data.eq(compressor.value)
40+
41+
m.d.sync += enough_pressure[i].eq(fifo.r_level > self._pressure_threshold)
42+
43+
def go_to_next(i):
44+
with m.If(any_enough_pressure):
45+
m.next = f"announce_{(i + 1) % len(self.input)}"
46+
with m.Else():
47+
m.next = f"wait_{(i + 1) % len(self.input)}"
48+
49+
50+
with m.State(f"wait_{i}"):
51+
with m.If(any_enough_pressure):
52+
m.next = f"announce_{i}"
53+
with m.State(f"announce_{i}"):
54+
m.d.comb += self.output_fifo.w_data.eq(fifo.r_level)
55+
m.d.comb += self.output_fifo.w_en.eq(1)
56+
m.d.sync += to_transfer.eq(fifo.r_level)
57+
with m.If(self.output_fifo.w_rdy):
58+
with m.If(fifo.r_level > 0):
59+
m.next = f"send_{i}_lower"
60+
with m.Else():
61+
go_to_next(i)
62+
63+
with m.State(f"send_{i}_lower"):
64+
m.d.comb += self.output_fifo.w_data.eq(fifo.r_data[0:8])
65+
m.d.comb += self.output_fifo.w_en.eq(1)
66+
with m.If(self.output_fifo.w_rdy):
67+
m.next = f"send_{i}_upper"
68+
69+
with m.State(f"send_{i}_upper"):
70+
m.d.comb += self.output_fifo.w_data.eq(fifo.r_data[8:16])
71+
m.d.comb += self.output_fifo.w_en.eq(1)
72+
with m.If(self.output_fifo.w_rdy):
73+
m.d.comb += fifo.r_en.eq(1)
74+
with m.If(to_transfer > 1):
75+
m.next = f"send_{i}_lower"
76+
m.d.sync += to_transfer.eq(to_transfer - 1)
77+
with m.Else():
78+
go_to_next(i)
79+
80+
return m
81+
82+
@staticmethod
83+
async def read_chunk(read: Callable[[int], bytes]):
84+
length = (await read(1))[0]
85+
contents = (await read(2 * length))
86+
return [contents[2*i+1] << 8 | contents[2*i] for i in range(length)]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from itertools import chain
2+
from typing import List, Tuple
3+
from amaranth import *
4+
5+
class SignalCompressor(Elaboratable):
6+
"""The SignalCompressor converts information about value changes into an efficient compressed
7+
format. It outputs a 16bit stream that is encoded in one of three ways:
8+
9+
0b0: plain, no compression [15 bit value dump]
10+
0b10: constant 0 for the following n [14 bit] cycles
11+
0b11: constant 1 for the following n [14 bit] cycles
12+
"""
13+
def __init__(self, signal):
14+
self.signal = signal
15+
16+
self.valid = Signal()
17+
self.value = Signal(16)
18+
19+
def elaborate(self, platform):
20+
m = Module()
21+
22+
last = Signal()
23+
m.d.sync += last.eq(self.signal)
24+
change = Signal()
25+
m.d.comb += change.eq(self.signal ^ last)
26+
27+
28+
counter = Signal(14)
29+
m.d.sync += counter.eq(counter + 1)
30+
31+
buffer = Signal(15)
32+
m.d.sync += buffer.eq((buffer << 1) | self.signal)
33+
34+
plain_mode = Signal()
35+
36+
with m.If(change):
37+
with m.If(counter < 15):
38+
m.d.sync += plain_mode.eq(1)
39+
with m.Elif(~plain_mode):
40+
m.d.comb += self.valid.eq(1)
41+
m.d.comb += self.value.eq(Cat(1, last, counter))
42+
m.d.sync += counter.eq(0)
43+
m.d.sync += plain_mode.eq(0)
44+
45+
with m.If(counter == 2**len(counter) - 1):
46+
m.d.comb += self.valid.eq(1)
47+
m.d.comb += self.value.eq(Cat(1, last, counter))
48+
m.d.sync += counter.eq(0)
49+
m.d.sync += plain_mode.eq(0)
50+
51+
with m.If(plain_mode & (counter == 14)):
52+
m.d.comb += self.valid.eq(1)
53+
m.d.comb += self.value.eq(Cat(0, buffer))
54+
m.d.sync += counter.eq(0)
55+
m.d.sync += plain_mode.eq(0)
56+
57+
return m
58+
59+
@staticmethod
60+
def decode_pkg(pkg) -> List[Tuple[int, int]]:
61+
if pkg & 0b01:
62+
value = pkg >> 1 & 0b01
63+
duration = pkg >> 2
64+
return [(value, duration + 1)]
65+
else:
66+
return [(int(x), 1) for x in list('{0:015b}'.format(pkg >> 1))]
67+
68+
@staticmethod
69+
def expand_duration_list(duration_list: List[Tuple[int, int]]) -> List[int]:
70+
return list(chain(*[[value] * duration for value, duration in duration_list]))

0 commit comments

Comments
 (0)