Skip to content

Commit 663c993

Browse files
authored
Merge pull request #1639 from mndza/generic-sgpio-intf
gateware: generic SGPIOInterface, simpler capture mgmt, fix resampler bugs
2 parents 0e05bda + 7631832 commit 663c993

File tree

13 files changed

+676
-907
lines changed

13 files changed

+676
-907
lines changed

firmware/fpga/board.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ class PralinePlatform(LatticeICE40Platform):
3737
Attrs(IO_STANDARD="SB_LVCMOS")),
3838
Resource("host_data", 0, Pins("21 19 6 13 10 3 4 18", dir="io"),
3939
Attrs(IO_STANDARD="SB_LVCMOS")),
40-
Resource("q_invert", 0, Pins("9", dir="i"),
41-
Attrs(IO_STANDARD="SB_LVCMOS")),
4240
Resource("direction", 0, Pins("12", dir="i"),
4341
Attrs(IO_STANDARD="SB_LVCMOS")),
4442
Resource("disable", 0, Pins("23", dir="i"),
4543
Attrs(IO_STANDARD="SB_LVCMOS")),
4644
Resource("capture_en", 0, Pins("11", dir="o"),
4745
Attrs(IO_STANDARD="SB_LVCMOS")),
46+
47+
# Other I/O.
48+
Resource("q_invert", 0, Pins("9", dir="i"),
49+
Attrs(IO_STANDARD="SB_LVCMOS")),
4850
Resource("trigger_in", 0, Pins("48", dir="i"),
4951
Attrs(IO_STANDARD="SB_LVCMOS")),
5052
Resource("trigger_out", 0, Pins("2", dir="o"),
11.2 KB
Binary file not shown.

firmware/fpga/dsp/fir.py

Lines changed: 208 additions & 149 deletions
Large diffs are not rendered by default.

firmware/fpga/dsp/fir_mac16.py

Lines changed: 186 additions & 199 deletions
Large diffs are not rendered by default.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
from .max586x import MAX586xInterface
1+
from .max586x import MAX586xInterface
2+
from .spi import SPIRegisterInterface
3+
from .sgpio import SGPIOInterface

firmware/fpga/interface/max586x.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@
99

1010
from util import IQSample
1111

12+
1213
class MAX586xInterface(wiring.Component):
13-
adc_stream: Out(stream.Signature(IQSample(8), always_ready=True))
14+
adc_stream: Out(stream.Signature(IQSample(8), always_ready=True, always_valid=True))
1415
dac_stream: In(stream.Signature(IQSample(8), always_ready=True))
15-
16-
adc_capture: In(1)
17-
dac_capture: In(1)
18-
q_invert: In(1)
16+
q_invert: In(1)
1917

2018
def __init__(self, bb_domain):
2119
super().__init__()
@@ -47,10 +45,9 @@ def elaborate(self, platform):
4745
m.d.comb += [
4846
adc_stream.p.i .eq(adc_in.i[0] ^ 0x80), # I: non-inverted between MAX2837 and MAX5864.
4947
adc_stream.p.q .eq(adc_in.i[1] ^ rx_q_mask), # Q: inverted between MAX2837 and MAX5864.
50-
adc_stream.valid .eq(self.adc_capture),
5148
]
5249

53-
# Output the transformed data to the DAC using a DDR output buffer.
50+
# Output to the DAC using a DDR output buffer.
5451
m.submodules.dac_out = dac_out = io.DDRBuffer("o", platform.request("dd", dir="-"), o_domain=self._bb_domain)
5552
with m.If(dac_stream.valid):
5653
m.d.comb += [

firmware/fpga/interface/sgpio.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#
2+
# This file is part of HackRF.
3+
#
4+
# Copyright (c) 2025 Great Scott Gadgets <[email protected]>
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
7+
from amaranth import Module, Signal, DomainRenamer, EnableInserter, ClockSignal, Instance
8+
from amaranth.lib import io, fifo, stream, wiring, cdc
9+
from amaranth.lib.wiring import Out, In
10+
11+
from util import LinearFeedbackShiftRegister
12+
13+
14+
class SGPIOInterface(wiring.Component):
15+
16+
def __init__(self, sample_width=8, rx_assignments=None, tx_assignments=None, domain="sync"):
17+
self.sample_width = sample_width
18+
if rx_assignments is None:
19+
rx_assignments = _default_rx_assignments(sample_width // 8)
20+
if tx_assignments is None:
21+
tx_assignments = _default_tx_assignments(sample_width // 8)
22+
self.rx_assignments = rx_assignments
23+
self.tx_assignments = tx_assignments
24+
self._domain = domain
25+
super().__init__({
26+
"adc_stream": In(stream.Signature(sample_width, always_ready=True)),
27+
"dac_stream": Out(stream.Signature(sample_width)),
28+
"trigger_en": In(1),
29+
"prbs": In(1),
30+
})
31+
32+
def elaborate(self, platform):
33+
m = Module()
34+
35+
adc_stream = self.adc_stream
36+
dac_stream = self.dac_stream
37+
rx_cycles = len(self.rx_assignments)
38+
tx_cycles = len(self.tx_assignments)
39+
40+
direction_i = platform.request("direction").i
41+
enable_i = ~platform.request("disable").i
42+
capture_en = platform.request("capture_en").o
43+
m.d.comb += capture_en.eq(1)
44+
45+
# Determine data transfer direction.
46+
direction = Signal()
47+
m.submodules.direction_cdc = cdc.FFSynchronizer(direction_i, direction, o_domain=self._domain)
48+
transfer_from_adc = (direction == 0)
49+
50+
# SGPIO clock and data lines.
51+
tx_clk_en = Signal()
52+
rx_clk_en = Signal()
53+
data_to_host = Signal(self.sample_width)
54+
byte_to_host = Signal(8)
55+
data_from_host = Signal(self.sample_width)
56+
byte_from_host = Signal(8)
57+
58+
m.submodules.clk_out = clk_out = io.DDRBuffer("o", platform.request("host_clk", dir="-"), o_domain=self._domain)
59+
m.submodules.host_io = host_io = io.DDRBuffer('io', platform.request("host_data", dir="-"), i_domain=self._domain, o_domain=self._domain)
60+
61+
m.d.sync += clk_out.o[0].eq(tx_clk_en)
62+
m.d.sync += clk_out.o[1].eq(rx_clk_en)
63+
m.d.sync += host_io.oe.eq(transfer_from_adc)
64+
m.d.comb += host_io.o[0].eq(byte_to_host)
65+
m.d.comb += host_io.o[1].eq(byte_to_host)
66+
m.d.comb += byte_from_host.eq(host_io.i[1])
67+
68+
# Transmission is handled differently to account for the latency before the data
69+
# becomes available in the FPGA fabric.
70+
ddr_in_latency = 2 # for iCE40 DDR inputs in Amaranth.
71+
tx_write_latency = tx_cycles + ddr_in_latency
72+
tx_write_pipe = Signal(tx_write_latency)
73+
m.d.sync += tx_write_pipe.eq(tx_write_pipe << 1)
74+
for i in range(tx_cycles-1): # don't store last byte
75+
with m.If(tx_write_pipe[ddr_in_latency + i]):
76+
m.d.sync += self.tx_assignments[i](data_from_host, byte_from_host)
77+
78+
# Small TX FIFO to avoid missing samples when the consumer deasserts its ready
79+
# signal and transfers are in progress.
80+
m.submodules.tx_fifo = tx_fifo = fifo.SyncFIFOBuffered(width=self.sample_width, depth=16)
81+
m.d.comb += [
82+
tx_fifo.w_data .eq(data_from_host),
83+
self.tx_assignments[-1](tx_fifo.w_data, byte_from_host),
84+
tx_fifo.w_en .eq(tx_write_pipe[-1]),
85+
dac_stream.p .eq(tx_fifo.r_data),
86+
dac_stream.valid .eq(tx_fifo.r_rdy),
87+
tx_fifo.r_en .eq(dac_stream.ready),
88+
]
89+
90+
# Pseudo-random binary sequence generator.
91+
prbs_advance = Signal()
92+
prbs_count = Signal(2)
93+
m.submodules.prbs = prbs = EnableInserter(prbs_advance)(
94+
LinearFeedbackShiftRegister(degree=8, taps=[8,6,5,4], init=0b10110001))
95+
96+
97+
# Capture signal generation.
98+
capture = Signal()
99+
m.submodules.trigger_gen = trigger_gen = FlowAndTriggerControl(domain=self._domain)
100+
m.d.comb += [
101+
trigger_gen.enable.eq(enable_i),
102+
trigger_gen.trigger_en.eq(self.trigger_en),
103+
capture.eq(trigger_gen.capture),
104+
]
105+
106+
107+
# Main state machine.
108+
with m.FSM():
109+
with m.State("IDLE"):
110+
111+
with m.If(transfer_from_adc):
112+
with m.If(self.prbs):
113+
m.next = "PRBS"
114+
with m.Elif(adc_stream.valid & capture):
115+
m.d.comb += rx_clk_en.eq(1)
116+
m.d.sync += data_to_host.eq(adc_stream.p)
117+
m.d.sync += byte_to_host.eq(self.rx_assignments[0](adc_stream.p))
118+
if rx_cycles > 1:
119+
m.next = "RX0"
120+
with m.Else():
121+
with m.If(dac_stream.ready & capture):
122+
m.d.comb += tx_clk_en.eq(1)
123+
m.d.sync += tx_write_pipe[0].eq(capture)
124+
if tx_cycles > 1:
125+
m.next = "TX0"
126+
127+
for i in range(rx_cycles-1):
128+
with m.State(f"RX{i}"):
129+
m.d.comb += rx_clk_en.eq(1)
130+
m.d.sync += byte_to_host.eq(self.rx_assignments[i+1](data_to_host))
131+
m.next = "IDLE" if i == rx_cycles-2 else f"RX{i+1}"
132+
133+
for i in range(tx_cycles-1):
134+
with m.State(f"TX{i}"):
135+
m.d.comb += tx_clk_en.eq(1)
136+
m.next = "IDLE" if i == tx_cycles-2 else f"TX{i+1}"
137+
138+
with m.State("PRBS"):
139+
m.d.comb += rx_clk_en.eq(prbs_count == 0)
140+
m.d.comb += prbs_advance.eq(prbs_count == 0)
141+
m.d.sync += byte_to_host.eq(prbs.value)
142+
m.d.sync += prbs_count.eq(prbs_count + 1)
143+
with m.If(~self.prbs):
144+
m.next = "IDLE"
145+
146+
# Convert to other clock domain if necessary.
147+
if self._domain != "sync":
148+
m = DomainRenamer(self._domain)(m)
149+
150+
return m
151+
152+
153+
def _default_rx_assignments(n):
154+
def rx_assignment(i):
155+
def _f(w):
156+
return w.word_select(i, 8)
157+
return _f
158+
return [ rx_assignment(i) for i in range(n) ]
159+
160+
def _default_tx_assignments(n):
161+
def tx_assignment(i):
162+
def _f(w, v):
163+
return w.word_select(i, 8).eq(v)
164+
return _f
165+
return [ tx_assignment(i) for i in range(n) ]
166+
167+
168+
class FlowAndTriggerControl(wiring.Component):
169+
trigger_en: In(1)
170+
enable: In(1)
171+
capture: Out(1)
172+
173+
def __init__(self, domain):
174+
super().__init__()
175+
self._domain = domain
176+
177+
def elaborate(self, platform):
178+
m = Module()
179+
180+
#
181+
# Signal synchronization and trigger logic.
182+
#
183+
trigger_enable = self.trigger_en
184+
trigger_in = platform.request("trigger_in").i
185+
trigger_out = platform.request("trigger_out").o
186+
m.d.comb += trigger_out.eq(self.enable)
187+
188+
# Create a latch for the trigger input signal using a special FPGA primitive.
189+
trigger_in_latched = Signal()
190+
trigger_in_reg = Instance("SB_DFFES",
191+
i_D = 0,
192+
i_S = trigger_in, # async set
193+
i_E = ~self.enable,
194+
i_C = ClockSignal(self._domain),
195+
o_Q = trigger_in_latched
196+
)
197+
m.submodules.trigger_in_reg = trigger_in_reg
198+
199+
# Export signal for capture gating.
200+
m.d[self._domain] += self.capture.eq(self.enable & (trigger_in_latched | ~trigger_enable))
201+
202+
return m

firmware/fpga/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
amaranth==v0.5.8
22
amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git@23c66d6
33
lz4
4+
numpy

0 commit comments

Comments
 (0)