Skip to content

Commit a0b430a

Browse files
author
Jean THOMAS
committed
cores/ila: Add ILA core
1 parent 7040c88 commit a0b430a

File tree

2 files changed

+249
-0
lines changed

2 files changed

+249
-0
lines changed

lambdalib/cores/ila.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from amaranth import * # type: ignore
2+
from lambdalib.interface import stream
3+
from lambdalib.cores.mem.stream import MemoryStream
4+
from lambdalib.cores.serial import AsyncSerialTXStream
5+
6+
__all__ = ["ILA"]
7+
8+
9+
class ILA(Elaboratable):
10+
"""Integrated Logic Analyzer (ILA) for capturing and analyzing digital signals.
11+
12+
This is a simple implementation of an Integrated Logic Analyzer (ILA).
13+
It captures data on a trigger signal and allows reading it out later via
14+
a serial interface at 115200 baud (or any other baudrate if specified).
15+
16+
An accompagnying tool can be used to read the captured data from the serial
17+
port and generate a VCD file for waveform analysis:
18+
19+
ila.py --depth 65536 --layout "data:8,valid:1,ready:1" \
20+
/dev/tty.usbserial-102 capture.vcd
21+
22+
The ILA operates in four states:
23+
- IDLE: Waiting for a trigger signal
24+
- CAPTURE: Recording incoming data into memory
25+
- REWIND: Preparing to read out captured data
26+
- READOUT: Streaming captured data via serial interface
27+
28+
Parameters:
29+
data_width: Width of the data bus to capture (in bits)
30+
depth: Number of samples to capture in memory
31+
sys_clk_freq: System clock frequency for serial baud rate calculation
32+
33+
Attributes:
34+
data_in: Input signal to capture
35+
trigger: Signal to start data capture
36+
tx: Serial output for data readout
37+
"""
38+
def __init__(self, data_width: int, depth: int, sys_clk_freq: int, baudrate: int = 115200):
39+
self._data_width = data_width
40+
self._depth = depth
41+
self._sys_clk_freq = sys_clk_freq
42+
self._baudrate = baudrate
43+
44+
self.data_in = Signal(data_width)
45+
self.trigger = Signal()
46+
self.tx = Signal()
47+
48+
def elaborate(self, platform) -> Module:
49+
m = Module()
50+
51+
# Calculate upper multiple of 8 for better data alignment
52+
aligned_width = ((self._data_width + 7) // 8) * 8
53+
54+
m.submodules.mem = mem = MemoryStream(dw=aligned_width, depth=self._depth)
55+
56+
# Pad input data to aligned width
57+
padded_data = Signal(aligned_width)
58+
m.d.comb += padded_data[:self._data_width].eq(self.data_in)
59+
m.d.comb += [
60+
mem.sink.data.eq(padded_data),
61+
]
62+
63+
m.submodules.downconverter = downconverter = stream._DownConverter(
64+
nbits_from=aligned_width,
65+
nbits_to=8,
66+
ratio=aligned_width // 8,
67+
reverse=False
68+
)
69+
70+
m.submodules.tx = tx = AsyncSerialTXStream(
71+
o=self.tx,
72+
divisor=self._sys_clk_freq // self._baudrate,
73+
)
74+
m.d.comb += downconverter.source.connect(tx.sink)
75+
76+
count = Signal(range(self._depth))
77+
78+
with m.FSM():
79+
with m.State("IDLE"):
80+
m.d.sync += count.eq(0)
81+
82+
with m.If(self.trigger):
83+
m.d.comb += mem.sink.valid.eq(1)
84+
m.d.sync += count.eq(count + 1)
85+
m.next = "CAPTURE"
86+
87+
with m.State("CAPTURE"):
88+
m.d.comb += mem.sink.valid.eq(1)
89+
m.d.sync += count.eq(count + 1)
90+
with m.If(count == self._depth - 1):
91+
m.next = "REWIND"
92+
93+
with m.State("REWIND"):
94+
m.d.comb += mem.rewind.eq(1)
95+
m.d.sync += count.eq(0)
96+
m.next = "READOUT"
97+
98+
with m.State("READOUT"):
99+
m.d.comb += mem.source.connect(downconverter.sink)
100+
with m.If(mem.source.valid & downconverter.sink.ready):
101+
m.d.sync += count.eq(count + 1)
102+
with m.If(count == self._depth - 1):
103+
m.next = "STUCK"
104+
105+
with m.State("STUCK"):
106+
pass
107+
108+
return m

lambdalib/software/ila.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""Host-side software for ILA"""
2+
from argparse import ArgumentParser
3+
from serial import Serial
4+
from vcd import VCDWriter
5+
6+
7+
def parse_layout(layout_str):
8+
"""Parse signal layout string into list of (name, width) tuples.
9+
10+
Args:
11+
layout_str: String like 'data_in:10,trigger:1,address:8'
12+
13+
Returns:
14+
List of (signal_name, width) tuples
15+
16+
Raises:
17+
ValueError: If layout string is malformed
18+
"""
19+
signals = []
20+
if not layout_str.strip():
21+
raise ValueError("Layout string cannot be empty")
22+
23+
for signal_def in layout_str.split(','):
24+
signal_def = signal_def.strip()
25+
if ':' not in signal_def:
26+
raise ValueError(f"Invalid signal definition '{signal_def}'. Expected format 'name:width'")
27+
28+
name, width_str = signal_def.split(':', 1)
29+
name = name.strip()
30+
width_str = width_str.strip()
31+
32+
if not name:
33+
raise ValueError(f"Signal name cannot be empty in '{signal_def}'")
34+
35+
try:
36+
width = int(width_str)
37+
if width <= 0:
38+
raise ValueError(f"Signal width must be positive, got {width} for '{name}'")
39+
except ValueError as e:
40+
if "invalid literal" in str(e):
41+
raise ValueError(f"Invalid width '{width_str}' for signal '{name}'. Width must be a positive integer")
42+
raise
43+
44+
signals.append((name, width))
45+
46+
return signals
47+
48+
49+
def extract_signal_value(data_value, bit_offset, width):
50+
"""Extract a signal value from the packed data.
51+
52+
Args:
53+
data_value: Full packed data value
54+
bit_offset: Starting bit position (LSB = 0)
55+
width: Number of bits to extract
56+
57+
Returns:
58+
Extracted signal value
59+
"""
60+
mask = (1 << width) - 1
61+
return (data_value >> bit_offset) & mask
62+
63+
64+
if __name__ == "__main__":
65+
parser = ArgumentParser(description="ILA Capture Tool")
66+
parser.add_argument("port", help="Serial port for ILA data")
67+
parser.add_argument("output", help="Output VCD file")
68+
parser.add_argument("--baudrate", type=int, default=115200, help="Baud rate for serial communication (default: 115200)")
69+
parser.add_argument("--depth", type=int, required=True, help="Number of samples captured by ILA")
70+
parser.add_argument("--layout", type=str, required=True, help="Signal layout description (e.g. 'data_in:10,trigger:1,address:8')")
71+
args = parser.parse_args()
72+
73+
# Parse and validate the layout
74+
try:
75+
signals = parse_layout(args.layout)
76+
except ValueError as e:
77+
print(f"Error parsing layout: {e}")
78+
exit(1)
79+
80+
# Calculate data width from layout
81+
data_width = sum(width for _, width in signals)
82+
83+
print(f"Parsed layout: {signals}")
84+
print(f"Inferred data width: {data_width} bits")
85+
print(f"Waiting for {args.depth} samples of {data_width}-bit data from {args.port}")
86+
87+
try:
88+
with Serial(args.port, args.baudrate, timeout=None) as ser, open(args.output, "w") as vcd_file:
89+
with VCDWriter(vcd_file, timescale="1 ns") as vcd:
90+
# Register clock signal
91+
clk_signal = vcd.register_var("ila", "clk", "wire", size=1)
92+
93+
# Register all signals in the VCD
94+
vcd_signals = []
95+
for name, width in signals:
96+
vcd_signal = vcd.register_var("ila", name, "wire", size=width)
97+
vcd_signals.append(vcd_signal)
98+
99+
# Initialize clock signal to low
100+
vcd.change(clk_signal, 0, 0)
101+
102+
# Capture and decode samples
103+
bytes_to_read = (data_width + 7) // 8 # Round up to nearest byte
104+
print(f"Waiting for data on {args.port}... (Press Ctrl+C to abort)")
105+
106+
for sample_index in range(args.depth):
107+
# Read raw data from serial port - this will block until data is available
108+
raw_data = ser.read(bytes_to_read)
109+
110+
if len(raw_data) < bytes_to_read:
111+
print(f"Incomplete data received at sample {sample_index}. Expected {bytes_to_read} bytes, got {len(raw_data)}. Exiting.")
112+
break
113+
114+
# Convert bytes to integer (little-endian)
115+
data_value = int.from_bytes(raw_data, byteorder='little')
116+
117+
# Extract and log individual signal values
118+
bit_offset = 0
119+
timestamp = sample_index * 1000 # 1ns timestep, 1us per sample
120+
121+
# Generate clock signal - rising edge at start of each sample
122+
vcd.change(clk_signal, timestamp, 1)
123+
124+
for (name, width), vcd_signal in zip(signals, vcd_signals):
125+
signal_value = extract_signal_value(data_value, bit_offset, width)
126+
vcd.change(vcd_signal, timestamp, signal_value)
127+
bit_offset += width
128+
129+
# Falling edge at middle of sample period
130+
vcd.change(clk_signal, timestamp + 500, 0)
131+
132+
if sample_index % 100 == 0: # Progress indicator
133+
print(f"Processed {sample_index + 1}/{args.depth} samples")
134+
135+
print(f"Data capture complete. Output written to {args.output}")
136+
137+
except KeyboardInterrupt:
138+
print(f"\nCapture interrupted by user. Partial data written to {args.output}")
139+
except Exception as e:
140+
print(f"Error during capture: {e}")
141+
exit(1)

0 commit comments

Comments
 (0)