|
| 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 |
0 commit comments