├── .gitignore ├── LICENSE ├── README.md ├── colorlight-hspi.py ├── colorlight.py ├── hspi-receiver.gtkw ├── hspi-transmitter.gtkw ├── hspi ├── __init__.py └── hspi.py ├── ila.py ├── initialize-python-environment.sh ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__/ 3 | build/ 4 | venv/ 5 | ila.P 6 | test_* 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Hans Baier 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gateware for the WCH-Tech CH569 High Speed Parallel Interface (HSPI) 2 | 3 | This package provides gateware to communicate with the 4 | High Speed Parallel Interface of the [WCH-Tech CH569 USB3 Super Speed chip](https://github.com/openwch/ch569). 5 | The gateware is written in amaranth HDL and can be easily converted into Verilog, 6 | if necessary. 7 | 8 | Both, transmitter and receiver cores are provided, using a simple stream-like interface. 9 | 10 | ![image](https://user-images.githubusercontent.com/148607/187302706-f1881097-d995-49b3-b044-a7a7b7d7c661.png) 11 | ![image](https://user-images.githubusercontent.com/148607/187303444-c219446b-a1ff-4c3b-b18e-674f6a842718.png) 12 | -------------------------------------------------------------------------------- /colorlight-hspi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | from amaranth import * 5 | from amaranth.lib.cdc import ResetSynchronizer 6 | from amaranth.lib.fifo import SyncFIFOBuffered 7 | 8 | from amaranth.build import * 9 | from amaranth.vendor.lattice_ecp5 import * 10 | 11 | from amlib.stream import connect_fifo_to_stream, connect_stream_to_fifo 12 | 13 | from usb_protocol.types import USBRequestType, USBDirection, USBStandardRequests 14 | from usb_protocol.emitters import DeviceDescriptorCollection 15 | 16 | from luna import top_level_cli 17 | from luna.usb2 import USBDevice 18 | from luna.gateware.usb.usb2.request import StallOnlyRequestHandler 19 | from luna.gateware.usb.usb2.endpoints.stream import USBMultibyteStreamInEndpoint 20 | 21 | from amlib.debug.ila import StreamILA, ILACoreParameters 22 | 23 | from hspi import HSPITransmitter, HSPIReceiver 24 | 25 | class ColorlightHSPI(Elaboratable): 26 | ILA_MAX_PACKET_SIZE = 512 27 | USE_ILA = True 28 | USE_ACK = False 29 | 30 | def create_descriptors(self): 31 | """ Creates the descriptors that describe our audio topology. """ 32 | 33 | descriptors = DeviceDescriptorCollection() 34 | 35 | with descriptors.DeviceDescriptor() as d: 36 | d.bcdUSB = 2.00 37 | d.bDeviceClass = 0xEF 38 | d.bDeviceSubclass = 0x02 39 | d.bDeviceProtocol = 0x01 40 | d.idVendor = 0x1209 41 | d.idProduct = 0x4711 42 | 43 | d.iManufacturer = "Hans Baier" 44 | d.iProduct = "HSPI-ILA" 45 | d.iSerialNumber = "4711" 46 | d.bcdDevice = 0.01 47 | 48 | d.bNumConfigurations = 1 49 | 50 | with descriptors.ConfigurationDescriptor() as configDescr: 51 | with configDescr.InterfaceDescriptor() as i: 52 | i.bInterfaceNumber = 0 53 | 54 | with i.EndpointDescriptor() as e: 55 | e.bEndpointAddress = USBDirection.IN.to_endpoint_address(1) # EP 1 IN 56 | e.wMaxPacketSize = self.ILA_MAX_PACKET_SIZE 57 | 58 | return descriptors 59 | 60 | 61 | def elaborate(self, platform): 62 | m = Module() 63 | 64 | # Generate our domain clocks/resets. 65 | m.submodules.car = platform.clock_domain_generator() 66 | 67 | hspi_pads = platform.request("hspi", 0) 68 | 69 | m.submodules.hspi_tx = hspi_tx = HSPITransmitter(domain="hspi") 70 | m.submodules.hspi_rx = hspi_rx = HSPIReceiver(domain="hspi") 71 | m.submodules.looback_fifo = loopback_fifo = DomainRenamer("hspi")(SyncFIFOBuffered(width=34, depth=4096)) 72 | 73 | m.d.comb += [ 74 | ## connect HSPI receiver 75 | *hspi_rx.connect_to_pads(hspi_pads), 76 | *connect_stream_to_fifo(hspi_rx.stream_out, loopback_fifo, firstBit=-2, lastBit=-1), 77 | 78 | ## connect HSPI transmitter 79 | hspi_tx.user_id0_in.eq(0x3ABCDEF), 80 | hspi_tx.user_id1_in.eq(0x3456789), 81 | hspi_tx.tll_2b_in.eq(0b11), 82 | hspi_tx.sequence_nr_in.eq(hspi_rx.sequence_nr_out), 83 | 84 | *hspi_tx.connect_to_pads(hspi_pads), 85 | *connect_fifo_to_stream(loopback_fifo, hspi_tx.stream_in, firstBit=-2, lastBit=-1), 86 | ] 87 | 88 | if self.USE_ACK: 89 | with m.FSM(domain="hspi"): 90 | with m.State("WAIT_RX"): 91 | with m.If(hspi_rx.stream_out.first & hspi_rx.stream_out.valid): 92 | m.next = "WAIT_RX_DONE" 93 | 94 | with m.State("WAIT_RX_DONE"): 95 | with m.If(~hspi_pads.tx_ack): 96 | m.d.comb += hspi_tx.send_ack.eq(1) 97 | m.next = "WAIT_ACK" 98 | 99 | with m.State("WAIT_ACK"): 100 | with m.If(hspi_tx.ack_done): 101 | m.next = "WAIT_RX" 102 | else: 103 | m.d.comb += hspi_tx.send_ack.eq(0) 104 | 105 | if self.USE_ILA: 106 | trace_transmit = False 107 | trace_receive = False 108 | trace_loopback = False 109 | use_enable = False 110 | 111 | ulpi = platform.request(platform.default_usb_connection) 112 | m.submodules.usb = usb = USBDevice(bus=ulpi) 113 | 114 | # Connect our device as a high speed device 115 | m.d.comb += [ 116 | usb.connect .eq(1), 117 | usb.full_speed_only .eq(0), 118 | ] 119 | 120 | # Add our standard control endpoint to the device. 121 | descriptors = self.create_descriptors() 122 | control_ep = usb.add_control_endpoint() 123 | control_ep.add_standard_request_handlers(descriptors, blacklist=[ 124 | lambda setup: (setup.type == USBRequestType.STANDARD) 125 | & (setup.request == USBStandardRequests.SET_INTERFACE) 126 | ]) 127 | 128 | # Attach class-request handlers that stall any vendor or reserved requests, 129 | # as we don't have or need any. 130 | stall_condition = lambda setup : \ 131 | (setup.type == USBRequestType.VENDOR) | \ 132 | (setup.type == USBRequestType.RESERVED) 133 | control_ep.add_request_handler(StallOnlyRequestHandler(stall_condition)) 134 | 135 | debug = platform.request("debug") 136 | 137 | signals = [ 138 | debug.led1, 139 | debug.led2, 140 | 141 | hspi_pads.tx_req, 142 | hspi_pads.tx_ready, 143 | hspi_pads.tx_valid, 144 | ] 145 | 146 | if self.USE_ACK: 147 | signals += [ 148 | hspi_tx.send_ack, 149 | hspi_tx.ack_done, 150 | ] 151 | 152 | signals += [ 153 | hspi_pads.rx_act, 154 | hspi_pads.tx_ack, 155 | hspi_pads.rx_valid, 156 | ] 157 | 158 | if trace_transmit: 159 | signals = signals + [ 160 | hspi_pads.hd.oe, 161 | hspi_pads.hd.o, 162 | ] 163 | 164 | if trace_receive: 165 | signals = signals + [ 166 | hspi_rx.stream_out.crc_error, 167 | hspi_pads.hd.i, 168 | ] 169 | 170 | if trace_transmit: 171 | traced_stream = hspi_tx.stream_in 172 | else: 173 | traced_stream = hspi_rx.stream_out 174 | 175 | if trace_loopback: 176 | signals += [ 177 | traced_stream.payload, 178 | traced_stream.valid, 179 | traced_stream.ready, 180 | traced_stream.first, 181 | traced_stream.last, 182 | ] 183 | 184 | signals_bits = sum([s.width for s in signals]) 185 | depth = 8 * 6 * 1024 #int(33*8*1024/signals_bits) 186 | m.submodules.ila = ila = \ 187 | StreamILA( 188 | signals=signals, 189 | sample_rate=96e6, 190 | sample_depth=depth, 191 | domain="hspi", o_domain="usb", 192 | samples_pretrigger=256, 193 | with_enable=use_enable) 194 | 195 | stream_ep = USBMultibyteStreamInEndpoint( 196 | endpoint_number=1, # EP 1 IN 197 | max_packet_size=self.ILA_MAX_PACKET_SIZE, 198 | byte_width=ila.bytes_per_sample 199 | ) 200 | usb.add_endpoint(stream_ep) 201 | 202 | m.d.comb += stream_ep.stream.stream_eq(ila.stream), 203 | 204 | if use_enable: 205 | m.d.comb += ila.trigger.eq(1), 206 | if trace_transmit: 207 | m.d.comb += ila.enable.eq(hspi_pads.tx_req) 208 | if trace_receive: 209 | m.d.comb += ila.enable.eq(hspi_pads.rx_act) 210 | if trace_loopback: 211 | m.d.comb += ila.enable.eq(traced_stream.valid) 212 | else: 213 | if trace_transmit: 214 | m.d.comb += ila.trigger.eq(hspi_pads.tx_req) 215 | if trace_receive: 216 | m.d.comb += ila.trigger.eq(hspi_pads.rx_act) 217 | if trace_loopback: 218 | m.d.comb += ila.trigger.eq(traced_stream.valid) 219 | if not (trace_loopback or trace_receive or trace_transmit): 220 | m.d.comb += ila.trigger.eq(debug.led1) 221 | 222 | 223 | ILACoreParameters(ila).pickle() 224 | 225 | return m 226 | 227 | if __name__ == "__main__": 228 | os.environ["AMARANTH_verbose"] = "True" 229 | os.environ["AMARANTH_synth_opts"] = "-abc9" 230 | os.environ["AMARANTH_nextpnr_opts"] = "--timing-allow-fail" 231 | os.environ["LUNA_PLATFORM"] = "colorlight:ColorlightHSPIPlatform" 232 | top_level_cli(ColorlightHSPI) -------------------------------------------------------------------------------- /colorlight.py: -------------------------------------------------------------------------------- 1 | from amaranth import * 2 | from amaranth.lib.cdc import ResetSynchronizer 3 | 4 | from amaranth.build import * 5 | from amaranth.vendor.lattice_ecp5 import * 6 | 7 | from amaranth_boards.resources import * 8 | from amaranth_boards.colorlight_i5 import ColorLightI5Platform 9 | from amaranth_boards.colorlight_i9 import ColorLightI9Platform 10 | from amaranth_boards.colorlight_qmtech import ColorlightQMTechPlatform 11 | 12 | from luna.gateware.platform.core import LUNAPlatform 13 | 14 | class ColorlightDomainGenerator(Elaboratable): 15 | HSPI_FREQ_MHZ="96" 16 | 17 | """ Clock generator for the Colorlight I5/I9 board. """ 18 | def __init__(self, clock_frequencies=None): 19 | pass 20 | 21 | def elaborate(self, platform): 22 | m = Module() 23 | 24 | # Create our domains. 25 | m.domains.sync = ClockDomain("sync") 26 | m.domains.usb = ClockDomain("usb") 27 | m.domains.hspi = ClockDomain("hspi") 28 | 29 | 30 | # Grab our clock and global reset signals. 31 | clk25 = platform.request(platform.default_clk) 32 | 33 | main_locked = Signal() 34 | hspi_locked = Signal() 35 | reset = Signal() 36 | 37 | # USB PLL 38 | main_feedback = Signal() 39 | m.submodules.main_pll = Instance("EHXPLLL", 40 | # Status. 41 | o_LOCK=main_locked, 42 | 43 | # PLL parameters... 44 | p_PLLRST_ENA="DISABLED", 45 | p_INTFB_WAKE="DISABLED", 46 | p_STDBY_ENABLE="DISABLED", 47 | p_DPHASE_SOURCE="DISABLED", 48 | p_OUTDIVIDER_MUXA="DIVA", 49 | p_OUTDIVIDER_MUXB="DIVB", 50 | p_OUTDIVIDER_MUXC="DIVC", 51 | p_OUTDIVIDER_MUXD="DIVD", 52 | 53 | # 60 MHz 54 | p_CLKI_DIV = 5, 55 | p_CLKOP_ENABLE = "ENABLED", 56 | p_CLKOP_DIV = 10, 57 | p_CLKOP_CPHASE = 15, 58 | p_CLKOP_FPHASE = 0, 59 | 60 | p_FEEDBK_PATH = "CLKOP", 61 | p_CLKFB_DIV = 12, 62 | 63 | # Clock in. 64 | i_CLKI=clk25, 65 | 66 | # Internal feedback. 67 | i_CLKFB=main_feedback, 68 | 69 | # Control signals. 70 | i_RST=reset, 71 | i_PHASESEL0=0, 72 | i_PHASESEL1=0, 73 | i_PHASEDIR=1, 74 | i_PHASESTEP=1, 75 | i_PHASELOADREG=1, 76 | i_STDBY=0, 77 | i_PLLWAKESYNC=0, 78 | 79 | # Output Enables. 80 | i_ENCLKOP=0, 81 | 82 | # Generated clock outputs. 83 | o_CLKOP=main_feedback, 84 | 85 | # Synthesis attributes. 86 | a_FREQUENCY_PIN_CLKI="25", 87 | a_FREQUENCY_PIN_CLKOP="60", 88 | 89 | a_ICP_CURRENT="6", 90 | a_LPF_RESISTOR="16", 91 | a_MFG_ENABLE_FILTEROPAMP="1", 92 | a_MFG_GMCREF_SEL="2" 93 | ) 94 | 95 | # HSPI PLL 96 | hspi_clocks = platform.request("hspi-clocks", 0) 97 | hspi_feedback = Signal() 98 | m.submodules.hspi_pll = Instance("EHXPLLL", 99 | # Status. 100 | o_LOCK=hspi_locked, 101 | 102 | # PLL parameters... 103 | p_PLLRST_ENA="DISABLED", 104 | p_INTFB_WAKE="DISABLED", 105 | p_STDBY_ENABLE="DISABLED", 106 | p_DPHASE_SOURCE="DISABLED", 107 | p_OUTDIVIDER_MUXA="DIVA", 108 | p_OUTDIVIDER_MUXB="DIVB", 109 | p_OUTDIVIDER_MUXC="DIVC", 110 | p_OUTDIVIDER_MUXD="DIVD", 111 | 112 | p_CLKI_DIV = 1, 113 | 114 | p_CLKOP_ENABLE = "ENABLED", 115 | p_CLKOP_DIV = 10, 116 | p_CLKOP_CPHASE = 15, 117 | p_CLKOP_FPHASE = 0, 118 | 119 | p_FEEDBK_PATH = "CLKOP", 120 | p_CLKFB_DIV = 1, 121 | 122 | # Clock in. 123 | i_CLKI=hspi_clocks.rx_clk, 124 | 125 | # Internal feedback. 126 | i_CLKFB=hspi_feedback, 127 | 128 | # Control signals. 129 | i_RST=reset, 130 | i_PHASESEL0=0, 131 | i_PHASESEL1=0, 132 | i_PHASEDIR=1, 133 | i_PHASESTEP=1, 134 | i_PHASELOADREG=1, 135 | i_STDBY=0, 136 | i_PLLWAKESYNC=0, 137 | 138 | # Output Enables. 139 | i_ENCLKOP=0, 140 | 141 | # Generated clock outputs. 142 | o_CLKOP=hspi_feedback, 143 | 144 | # Synthesis attributes. 145 | a_FREQUENCY_PIN_CLKI=self.HSPI_FREQ_MHZ, 146 | a_FREQUENCY_PIN_CLKOP=self.HSPI_FREQ_MHZ, 147 | 148 | a_ICP_CURRENT="6", 149 | a_LPF_RESISTOR="16", 150 | a_MFG_ENABLE_FILTEROPAMP="1", 151 | a_MFG_GMCREF_SEL="2" 152 | ) 153 | 154 | 155 | reset = Signal() 156 | # Control our resets. 157 | m.d.comb += [ 158 | ClockSignal("usb") .eq(main_feedback), 159 | ClockSignal("sync") .eq(ClockSignal("usb")), 160 | ClockSignal("hspi") .eq(hspi_feedback), 161 | 162 | hspi_clocks.tx_clk.eq(ClockSignal("hspi")), 163 | 164 | reset.eq(~(main_locked)), 165 | ] 166 | 167 | led = platform.request("led", 0) 168 | m.d.comb += [ 169 | led.eq(~hspi_locked), 170 | ] 171 | 172 | m.submodules.reset_sync_hspi = ResetSynchronizer(reset, domain="hspi") 173 | m.submodules.reset_sync_usb = ResetSynchronizer(reset, domain="usb") 174 | m.submodules.reset_sync_sync = ResetSynchronizer(reset, domain="sync") 175 | 176 | return m 177 | 178 | 179 | GND = None 180 | left_row = list(range(7, 60, 2)) 181 | right_row = list(range(8, 61, 2)) 182 | 183 | btb_upper = [None, None, GND, "HD12", "HD13", "HD14", "HD15", 184 | GND, "HD16", "HD17", "HD18", "HD19", 185 | GND, "HD20", "HD21", "HD22", "HD23", 186 | GND, "HD24", "HD25", "HD26", "HD27", 187 | GND, "HD28", "HD29", "HD30", "HD31", 188 | GND, "LED1", "LED2"] # these can't be connected to the baseboards 189 | 190 | btb_lower = ["HD10", "HD11", GND, "HRCLK", "HRACT", "HRVLD", "HTRDY", 191 | GND, "HD0", "HD1", "HD2", "HD3", 192 | GND, "HD4", "HD5", "HD6", "HD7", 193 | GND, "HD8", "HD9", "HTVLD", "HTREQ", 194 | GND, "HTACK", "HTCLK"] 195 | 196 | left = list(zip(btb_upper, left_row)) 197 | right = list(zip(btb_lower, right_row)) 198 | pinmap = dict(filter(lambda t: t[0] != None, left + right)) 199 | hd_pins = " ".join([f"J_2:{pinmap[pin]}" for pin in [f"HD{i}" for i in range(0, 32)]]) 200 | control_pin = lambda pin: "J_2:" + str(pinmap[pin]) 201 | 202 | class ColorlightHSPIPlatform(ColorlightQMTechPlatform, LUNAPlatform): 203 | clock_domain_generator = ColorlightDomainGenerator 204 | default_usb_connection = "ulpi" 205 | ignore_phy_vbus = False 206 | 207 | def __init__(self) -> None: 208 | colorlight = ColorLightI5Platform 209 | 210 | colorlight.resources += [ 211 | *ButtonResources(pins="R1", attrs=Attrs(IO_TYPE="LVCMOS33")), 212 | # HSPI 213 | Resource("hspi", 0, 214 | Subsignal("hd", Pins(hd_pins, dir="io")), 215 | 216 | Subsignal("tx_ack", Pins(control_pin('HTRDY'), dir="o")), 217 | Subsignal("tx_ready", Pins(control_pin('HTACK'), dir="i")), 218 | 219 | Subsignal("tx_req", Pins(control_pin('HRACT'), dir="o")), 220 | Subsignal("rx_act", Pins(control_pin('HTREQ'), dir="i")), 221 | 222 | Subsignal("tx_valid", Pins(control_pin('HRVLD'), dir="o")), 223 | Subsignal("rx_valid", Pins(control_pin('HTVLD'), dir="i")), 224 | Attrs(IO_TYPE="LVCMOS33") 225 | ), 226 | Resource("hspi-clocks", 0, 227 | Subsignal("tx_clk", Pins(control_pin('HRCLK'), dir="o")), 228 | Subsignal("rx_clk", Pins(control_pin('HTCLK'), dir="i")), 229 | Attrs(IO_TYPE="LVCMOS33") 230 | ), 231 | 232 | Resource("debug", 0, 233 | Subsignal("led1", Pins("J_3:57", dir="i")), 234 | Subsignal("led2", Pins("J_3:58", dir="i")), 235 | Attrs(IO_TYPE="LVCMOS33") 236 | ), 237 | 238 | # HSPI Slave wiring (Master connected on pinmap) 239 | Resource("ulpi", 0, 240 | Subsignal("reset", Pins("J_3:9", dir="o", invert=True), Attrs(IO_TYPE="LVCMOS33")), 241 | Subsignal("clk", Pins("J_3:10", dir="o"), Attrs(IO_TYPE="LVCMOS33", DRIVE="4")), 242 | Subsignal("stp", Pins("J_3:11", dir="o"), Attrs(IO_TYPE="LVCMOS33")), 243 | Subsignal("dir", Pins("J_3:12", dir="i"), Attrs(IO_TYPE="LVCMOS33")), 244 | Subsignal("nxt", Pins("J_3:13", dir="i"), Attrs(IO_TYPE="LVCMOS33")), 245 | Subsignal("data", Pins("J_3:17 J_3:19 J_3:21 J_3:23 J_3:18 J_3:20 J_3:22 J_3:24", dir="io"), Attrs(IO_TYPE="LVCMOS33")), 246 | ), 247 | ] 248 | 249 | super().__init__(colorlight, False) 250 | 251 | def toolchain_program(self, products, name): 252 | import os 253 | import subprocess 254 | tool = os.environ.get("OPENFPGALOADER", "openFPGALoader") 255 | with products.extract("{}.bit".format(name)) as bitstream_filename: 256 | subprocess.check_call([tool, '-c', 'cmsisdap', '-m', bitstream_filename]) -------------------------------------------------------------------------------- /hspi-receiver.gtkw: -------------------------------------------------------------------------------- 1 | [*] 2 | [*] GTKWave Analyzer v3.4.0 (w)1999-2022 BSI 3 | [*] Fri Jun 24 11:38:35 2022 4 | [*] 5 | [dumpfile] "test_HSPIReceiverTest_test_hspi_rx.vcd" 6 | [dumpfile_mtime] "Fri Jun 24 11:37:03 2022" 7 | [dumpfile_size] 170503 8 | [savefile] "hspi-receiver.gtkw" 9 | [timestart] 3840 10 | [size] 3828 2090 11 | [pos] -1 -1 12 | *-14.000000 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 | [treeopen] bench. 14 | [treeopen] bench.top. 15 | [sst_width] 590 16 | [signals_width] 420 17 | [sst_expanded] 1 18 | [sst_vpaned_height] 658 19 | @28 20 | bench.top.clk 21 | @200 22 | -HSPI In 23 | @28 24 | bench.top.rx_act 25 | bench.top.tx_ack 26 | bench.top.rx_valid 27 | @22 28 | bench.top.hd__i[31:0] 29 | @200 30 | -HSPI-Receiver 31 | @28 32 | bench.top.fsm_state 33 | bench.top.enable_in 34 | @22 35 | bench.top.data_in[31:0] 36 | bench.top.crc_out[31:0] 37 | @28 38 | bench.top.crc_equal 39 | @200 40 | -Output 41 | @22 42 | bench.top.rx_data_out__payload[31:0] 43 | @28 44 | bench.top.rx_data_out__valid 45 | bench.top.rx_data_out__first 46 | bench.top.rx_data_out__last 47 | @29 48 | bench.top.rx_data_out__crc_error 49 | @22 50 | bench.top.sequence_nr_out[3:0] 51 | bench.top.user_data_out[25:0] 52 | @28 53 | bench.top.tll_2b_out[1:0] 54 | [pattern_trace] 1 55 | [pattern_trace] 0 56 | -------------------------------------------------------------------------------- /hspi-transmitter.gtkw: -------------------------------------------------------------------------------- 1 | [*] 2 | [*] GTKWave Analyzer v3.4.0 (w)1999-2022 BSI 3 | [*] Thu Jun 23 21:59:41 2022 4 | [*] 5 | [dumpfile] "test_HSPITransmitterTest_test_hspi_tx.vcd" 6 | [dumpfile_mtime] "Thu Jun 23 21:56:46 2022" 7 | [dumpfile_size] 333467 8 | [savefile] "hspi-transmitter.gtkw" 9 | [timestart] 0 10 | [size] 3828 2090 11 | [pos] -1 -1 12 | *-16.000000 346300 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 | [treeopen] bench. 14 | [treeopen] bench.top. 15 | [sst_width] 347 16 | [signals_width] 405 17 | [sst_expanded] 1 18 | [sst_vpaned_height] 658 19 | @28 20 | bench.top.clk 21 | @200 22 | -Input 23 | @28 24 | bench.top.tx_data_in__valid 25 | bench.top.tx_data_in__ready 26 | @23 27 | bench.top.tx_data_in__payload[31:0] 28 | @28 29 | bench.top.tx_data_in__first 30 | bench.top.tx_data_in__last 31 | @200 32 | -HSPI TX 33 | @28 34 | bench.top.fsm_state 35 | @22 36 | bench.top.sequence_nr[25:0] 37 | @200 38 | -CRC 39 | @22 40 | bench.top.crc.data_in[31:0] 41 | @28 42 | bench.top.crc.reset_in 43 | bench.top.crc.enable_in 44 | @22 45 | bench.top.crc.crc_out[31:0] 46 | @200 47 | -HSPI 48 | @28 49 | bench.top.tx_req 50 | bench.top.tx_ready 51 | bench.top.tx_valid 52 | @22 53 | bench.top.hd__o[31:0] 54 | @28 55 | bench.top.hd__oe 56 | [pattern_trace] 1 57 | [pattern_trace] 0 58 | -------------------------------------------------------------------------------- /hspi/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Hans Baier 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | from .hspi import HSPIInterface, HSPITransmitter, HSPIReceiver, CRC 5 | 6 | __all__ = [ 7 | "HSPIInterface", "HSPITransmitter", "HSPIReceiver", "CRC" 8 | ] 9 | -------------------------------------------------------------------------------- /hspi/hspi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 Hans Baier 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import operator 5 | from functools import reduce 6 | 7 | from amaranth import * 8 | from amaranth.lib.cdc import FFSynchronizer 9 | from amaranth.lib.fifo import SyncFIFO 10 | from amaranth.hdl.ast import Past 11 | from amaranth.hdl.rec import Layout, DIR_FANIN, DIR_FANOUT 12 | from amaranth.build import Platform 13 | 14 | from amlib.stream import StreamInterface 15 | 16 | class CRC(Elaboratable): 17 | def __init__(self, *, polynomial, crc_size, datawidth, init=None, delay=False): 18 | self.datawidth = datawidth 19 | self.crc_size = crc_size 20 | self.init = init 21 | self.polynomial = polynomial 22 | self.delay = delay 23 | 24 | if init is None: 25 | self.init = (1 << crc_size) - 1 26 | 27 | self.reset_in = Signal() 28 | self.enable_in = Signal() 29 | self.data_in = Signal(datawidth) 30 | self.crc_out = Signal(crc_size) 31 | 32 | def elaborate(self, platform): 33 | m = Module() 34 | 35 | crcreg = [Signal(self.crc_size, reset=self.init) for i in range(self.datawidth + 1)] 36 | 37 | for i in range(self.datawidth): 38 | inv = self.data_in[i] ^ crcreg[i][self.crc_size - 1] 39 | tmp = [] 40 | tmp.append(inv) 41 | for j in range(self.crc_size - 1): 42 | if((self.polynomial >> (j + 1)) & 1): 43 | tmp.append(crcreg[i][j] ^ inv) 44 | else: 45 | tmp.append(crcreg[i][j]) 46 | m.d.comb += crcreg[i + 1].eq(Cat(*tmp)) 47 | 48 | with m.If(self.reset_in): 49 | m.d.sync += crcreg[0].eq(self.init) 50 | with m.Elif(self.enable_in): 51 | m.d.sync += crcreg[0].eq(crcreg[self.datawidth]) 52 | 53 | domain = m.d.sync if self.delay else m.d.comb 54 | domain += self.crc_out.eq(crcreg[self.datawidth][::-1] ^ self.init) 55 | 56 | return m 57 | 58 | 59 | class HSPIInterface(Record): 60 | """ Record that represents a HSPI interface. """ 61 | 62 | LAYOUT = [ 63 | ('hd', [('i', 32, DIR_FANIN), ('o', 32, DIR_FANOUT), ('oe', 1, DIR_FANOUT)]), 64 | ("tx_ack", 1, DIR_FANOUT), 65 | ("tx_ready", 1, DIR_FANIN), 66 | ("tx_req", 1, DIR_FANOUT), 67 | ("rx_act", 1, DIR_FANIN), 68 | ("tx_valid", 1, DIR_FANOUT), 69 | ("rx_valid", 1, DIR_FANIN), 70 | ] 71 | 72 | def __init__(self, name=None): 73 | super().__init__(self.LAYOUT, name=name) 74 | 75 | class HSPITransmitter(Elaboratable): 76 | def __init__(self, name=None, domain=None): 77 | self.send_ack = Signal() 78 | self.ack_done = Signal() 79 | self.tll_2b_in = Signal(2) 80 | self.sequence_nr_in = Signal(4) 81 | self.user_id0_in = Signal(26) 82 | self.user_id1_in = Signal(26) 83 | self.stream_in = StreamInterface(name="tx_data_in", payload_width=32) 84 | self.hspi_out = HSPIInterface(name=name) 85 | 86 | self.state = Signal(3) 87 | 88 | self.domain = domain 89 | 90 | def connect_to_pads(self, hspi_pads): 91 | hspi_out = self.hspi_out 92 | 93 | return [ 94 | # HSPI inputs 95 | hspi_out.tx_ready .eq(hspi_pads.tx_ready), 96 | hspi_out.tx_ack .eq(hspi_pads.tx_ack), 97 | 98 | # HSPI outputs 99 | hspi_pads.tx_req .eq(hspi_out.tx_req), 100 | hspi_pads.tx_valid .eq(hspi_out.tx_valid), 101 | hspi_pads.hd.oe .eq(hspi_out.hd.oe), 102 | hspi_pads.hd.o .eq(hspi_out.hd.o), 103 | ] 104 | 105 | def elaborate(self, platform: Platform) -> Module: 106 | m = Module() 107 | domain = "sync" if self.domain is None else self.domain 108 | sync = m.d.__getattr__(domain) 109 | comb = m.d.comb 110 | 111 | m.submodules.crc = crc = DomainRenamer(domain)(CRC(polynomial=0x04C11DB7, crc_size=32, datawidth=32, delay=True)) 112 | 113 | hspi = self.hspi_out 114 | stream_in = self.stream_in 115 | last_seen = Signal() 116 | 117 | header = Signal(32) 118 | user_id = Signal(26) 119 | # maximum frame size is 4096 in 120 | word_index = Signal(range(4096)) 121 | 122 | comb += [ 123 | header.eq(Cat( 124 | Mux(~self.sequence_nr_in[0], self.user_id0_in, self.user_id1_in), 125 | self.sequence_nr_in, 126 | self.tll_2b_in)) 127 | ] 128 | 129 | ack_in_process = Signal() 130 | rx_complete = Signal() 131 | 132 | with m.FSM(domain=domain) as fsm: 133 | comb += [ 134 | self.state.eq(fsm.state), 135 | rx_complete.eq(~hspi.tx_ack & ~hspi.tx_ready), 136 | ] 137 | 138 | with m.State("WAIT_INPUT"): 139 | with m.If(self.send_ack): 140 | sync += ack_in_process.eq(1) 141 | m.next = "START" 142 | with m.Elif(stream_in.valid & stream_in.first & rx_complete): 143 | sync += last_seen.eq(0) 144 | m.next = "START" 145 | 146 | with m.State("START"): 147 | # wait until an ongoing RX is complete 148 | with m.If(rx_complete): 149 | sync += hspi.tx_req.eq(1) 150 | m.next = "WAIT_TX_READY" 151 | 152 | with m.State("WAIT_TX_READY"): 153 | with m.If(hspi.tx_ready): 154 | with m.If(ack_in_process): 155 | m.next = "TX_ACK" 156 | with m.Else(): 157 | m.next = "TX_HEADER" 158 | 159 | with m.State("TX_ACK"): 160 | comb += [ 161 | hspi.hd.oe.eq(1), 162 | hspi.hd.o.eq(0xf0), 163 | hspi.tx_valid.eq(1), 164 | ] 165 | m.next = "ACK_DONE" 166 | 167 | with m.State("ACK_DONE"): 168 | sync += ack_in_process.eq(0) 169 | sync += hspi.tx_req.eq(0) 170 | comb += self.ack_done.eq(1) 171 | m.next = "WAIT_INPUT" 172 | 173 | with m.State("TX_HEADER"): 174 | comb += [ 175 | hspi.hd.oe.eq(1), 176 | hspi.hd.o.eq(header), 177 | hspi.tx_valid.eq(1), 178 | 179 | crc.data_in.eq(header), 180 | crc.enable_in.eq(1), 181 | ] 182 | 183 | m.next = "TX_DATA" 184 | 185 | with m.State("TX_DATA"): 186 | comb += [ 187 | stream_in.ready.eq(1), 188 | 189 | hspi.hd.oe.eq(1), 190 | hspi.hd.o.eq(stream_in.payload), 191 | hspi.tx_valid.eq(stream_in.valid), 192 | 193 | crc.data_in.eq(stream_in.data), 194 | crc.enable_in.eq(1), 195 | ] 196 | 197 | with m.If(stream_in.valid): 198 | sync += word_index.eq(word_index + 1) 199 | 200 | with m.If(stream_in.last | (word_index == 4095)): 201 | with m.If(stream_in.last): 202 | sync += last_seen.eq(1) 203 | m.next = "TX_CRC" 204 | 205 | with m.State("TX_CRC"): 206 | comb += [ 207 | hspi.hd.oe.eq(1), 208 | hspi.hd.o.eq(crc.crc_out), 209 | hspi.tx_valid.eq(1), 210 | crc.reset_in.eq(1), 211 | ] 212 | sync += hspi.tx_req.eq(0) 213 | m.next = "WAIT_HTRDY" 214 | 215 | with m.State("WAIT_HTRDY"): 216 | with m.If(~hspi.tx_ready): 217 | with m.If(~last_seen): 218 | m.next = "WAIT_LAST" 219 | with m.Else(): 220 | m.next = "WAIT_INPUT" 221 | 222 | with m.State("WAIT_LAST"): 223 | comb += stream_in.ready.eq(1) 224 | with m.If(stream_in.last): 225 | m.next = "WAIT_INPUT" 226 | 227 | return m 228 | 229 | class HSPIReceiver(Elaboratable): 230 | def __init__(self, domain=None): 231 | self.hspi_in = HSPIInterface() 232 | self.stream_out = StreamInterface(name="rx_data_out", payload_width=32, extra_fields=[("crc_error", 1)]) 233 | self.packet_done_out = Signal(1) 234 | self.tll_2b_out = Signal(2) 235 | self.sequence_nr_out = Signal(4) 236 | self.user_data_out = Signal(26) 237 | self.num_words_out = Signal(13) 238 | 239 | self.state = Signal(3) 240 | 241 | self.domain = domain 242 | 243 | def connect_to_pads(self, hspi_pads): 244 | hspi_in = self.hspi_in 245 | 246 | return [ 247 | # HSPI inputs 248 | hspi_in.rx_act.eq(hspi_pads.rx_act), 249 | hspi_in.rx_valid.eq(hspi_pads.rx_valid), 250 | hspi_in.hd.i.eq(hspi_pads.hd.i), 251 | 252 | # HSPI outputs 253 | hspi_pads.tx_ack.eq(hspi_in.tx_ack), 254 | ] 255 | 256 | def elaborate(self, platform: Platform) -> Module: 257 | m = Module() 258 | hspi = self.hspi_in 259 | stream_out = self.stream_out 260 | domain = "sync" if self.domain is None else self.domain 261 | sync = m.d.__getattr__(domain) 262 | comb = m.d.comb 263 | 264 | m.submodules.crc = crc = DomainRenamer(domain)(CRC(polynomial=0x04C11DB7, crc_size=32, datawidth=32, delay=True)) 265 | 266 | word_pos = Signal(13) 267 | crc_equal = Signal() 268 | valid = Signal() 269 | 270 | with m.FSM(domain=domain) as fsm: 271 | comb += [ 272 | self.state.eq(fsm.state), 273 | stream_out.payload .eq(Past(hspi.hd.i, clocks=2, domain=domain)), 274 | stream_out.valid .eq(Past(valid, clocks=2, domain=domain)), 275 | ] 276 | 277 | with m.State("WAIT"): 278 | comb += stream_out.valid.eq(0), 279 | with m.If(hspi.rx_act): 280 | sync += [ 281 | word_pos.eq(0), 282 | hspi.tx_ack.eq(1), 283 | ] 284 | m.next = "RX" 285 | 286 | with m.State("RX"): 287 | with m.If(hspi.rx_valid): 288 | sync += word_pos.eq(word_pos + 1) 289 | comb += [ 290 | crc.enable_in.eq(1), 291 | crc.data_in.eq(hspi.hd.i), 292 | ] 293 | sync += crc_equal.eq(crc.crc_out == hspi.hd.i) 294 | 295 | # extract header 296 | with m.If(word_pos == 0): 297 | sync += Cat(self.user_data_out, self.sequence_nr_out, self.tll_2b_out).eq(hspi.hd.i) 298 | 299 | # don't include header in stream data. 300 | # valid is delayed by 2, so this comes at the same time as first 301 | with m.If(word_pos >= 1): 302 | comb += valid.eq(hspi.rx_valid) 303 | 304 | with m.If(word_pos == 3): 305 | comb += stream_out.first.eq(1) 306 | 307 | with m.If(~hspi.rx_act): 308 | comb += [ 309 | stream_out.last.eq(1), 310 | self.packet_done_out.eq(1), 311 | stream_out.crc_error.eq(~crc_equal), 312 | self.num_words_out.eq(word_pos - 1), 313 | ] 314 | sync += [ 315 | hspi.tx_ack.eq(0), 316 | crc_equal.eq(0), 317 | ] 318 | m.next = "WAIT" 319 | 320 | return m 321 | 322 | from amlib.test import GatewareTestCase, sync_test_case 323 | 324 | class HSPITransmitterTest(GatewareTestCase): 325 | FRAGMENT_UNDER_TEST = HSPITransmitter 326 | FRAGMENT_ARGUMENTS = dict() 327 | 328 | @sync_test_case 329 | def test_hspi_tx(self): 330 | dut = self.dut 331 | data = 0 332 | 333 | for i in range(5): 334 | yield from self.advance_cycles(3) 335 | yield dut.hspi_out.tx_ready.eq(1) 336 | yield dut.tll_2b_in.eq(0b11) 337 | yield dut.user_id0_in.eq(0x3ABCDEF) 338 | yield dut.user_id1_in.eq(0x3456789) 339 | yield dut.stream_in.payload.eq(data) 340 | yield dut.stream_in.first.eq(1) 341 | yield dut.stream_in.valid.eq(1) 342 | yield 343 | yield 344 | yield 345 | yield 346 | 347 | for i in range(0x400): 348 | yield dut.stream_in.first.eq(1 if i == 0 else 0) 349 | yield dut.stream_in.payload.eq(data) 350 | if i == 0x3ff: 351 | yield dut.stream_in.last.eq(1) 352 | else: 353 | yield dut.stream_in.last.eq(0) 354 | yield 355 | data += 1 356 | 357 | yield dut.stream_in.last.eq(0) 358 | yield dut.stream_in.valid.eq(0) 359 | yield 360 | 361 | yield dut.hspi_out.tx_ready.eq(0) 362 | 363 | class HSPIReceiverTest(GatewareTestCase): 364 | FRAGMENT_UNDER_TEST = HSPIReceiver 365 | FRAGMENT_ARGUMENTS = dict() 366 | 367 | @sync_test_case 368 | def test_hspi_rx(self): 369 | dut = self.dut 370 | hspi = dut.hspi_in 371 | data = 0 372 | 373 | yield from self.advance_cycles(3) 374 | yield hspi.rx_act.eq(1) 375 | yield 376 | yield 377 | yield 378 | yield hspi.hd.i.eq(0xc3abcdef) 379 | yield hspi.rx_valid.eq(1) 380 | yield 381 | for i in range(0x40): 382 | yield hspi.hd.i.eq(i) 383 | yield 384 | yield hspi.rx_valid.eq(0) 385 | yield 386 | yield 387 | yield 388 | yield hspi.rx_valid.eq(1) 389 | for i in range(0x40, 0x80): 390 | yield hspi.hd.i.eq(i) 391 | yield 392 | yield hspi.hd.i.eq(0x1106c501) 393 | yield 394 | yield hspi.hd.i.eq(0) 395 | yield hspi.rx_valid.eq(0) 396 | yield hspi.rx_act.eq(0) 397 | yield 398 | self.assertEqual((yield dut.stream_out.crc_error), 0) 399 | yield 400 | yield 401 | yield 402 | -------------------------------------------------------------------------------- /ila.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import usb 3 | from luna.gateware.usb.devices.ila import USBIntegratedLogicAnalyzerFrontend 4 | from amlib.debug.ila import ILACoreParameters 5 | 6 | 7 | dev=usb.core.find(idVendor=0x1209, idProduct=0x4711) 8 | print(dev) 9 | 10 | frontend = USBIntegratedLogicAnalyzerFrontend(ila=ILACoreParameters.unpickle(), delay=0, idVendor=0x1209, idProduct=0x4711, endpoint_no=1) 11 | frontend.interactive_display() 12 | -------------------------------------------------------------------------------- /initialize-python-environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 -m venv venv 3 | . venv/bin/activate 4 | pip3 install -r requirements.txt 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyusb 2 | git+https://github.com/amaranth-lang/amaranth.git 3 | git+https://github.com/amaranth-community-unofficial/amaranth-boards.git 4 | git+https://github.com/amaranth-community-unofficial/python-usb-descriptors.git 5 | git+https://github.com/amaranth-community-unofficial/usb2-highspeed-core.git 6 | git+https://github.com/amaranth-community-unofficial/amlib.git 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | def scm_version(): 5 | def local_scheme(version): 6 | return version.format_choice("+{node}", "+{node}.dirty") 7 | return { 8 | "relative_to": __file__, 9 | "version_scheme": "guess-next-dev", 10 | "local_scheme": local_scheme, 11 | } 12 | 13 | setup( 14 | name="hspi", 15 | use_scm_version=scm_version(), 16 | author="Hans Baier", 17 | author_email="hansfbaier@gmail.com", 18 | description="CH56x HSPI FPGA interface written in amaranth HDL", 19 | license="Apache License 2.0", 20 | setup_requires=["wheel", "setuptools", "setuptools_scm"], 21 | install_requires=[ 22 | "amaranth>=0.2,<=4", 23 | "importlib_metadata; python_version<'3.10'", 24 | ], 25 | packages=find_packages(), 26 | project_urls={ 27 | "Source Code": "https://github.com/amaranth-community-unofficial/ch569-hspi-fpga", 28 | "Bug Tracker": "https://github.com/amaranth-community-unofficial/ch569-hspi-fpga/issues", 29 | }, 30 | ) 31 | 32 | --------------------------------------------------------------------------------