diff --git a/gateware/top.py b/gateware/top.py index aa879fa..fb97bd3 100644 --- a/gateware/top.py +++ b/gateware/top.py @@ -5,38 +5,36 @@ and EP2 sample streamer into a single FPGA design. """ from amaranth import * -from amaranth.build import Resource, Subsignal, Pins, Attrs, Platform +from amaranth.build import Resource, Subsignal, Pins, Attrs +from amaranth.vendor import LatticeECP5Platform -from .usb_device import create_saleae_device +from luna.usb2 import USBDevice, USBStreamInEndpoint, USBStreamOutEndpoint + +from .usb_device import create_saleae_descriptors from .ep1_handler import EP1Handler from .test_pattern import TestPatternGenerator -class IcepiZeroPlatform(Platform): +class IcepiZeroPlatform(LatticeECP5Platform): """Amaranth platform definition for the Icepi Zero.""" device = "LFE5U-25F" - package = "CABGA256" + package = "BG256" speed = "6" + default_clk = "clk" + toolchain = "Trellis" resources = [ - Resource("clk", 0, Pins("M1", dir="i"), - Attrs(IO_TYPE="LVCMOS33")), - - # USB port 0 (direct FPGA GPIO, Full-Speed) + Resource("clk", 0, Pins("M1", dir="i"), Attrs(IO_TYPE="LVCMOS33")), Resource("usb", 0, - Subsignal("dp", Pins("F15", dir="io"), Attrs(IO_TYPE="LVCMOS33")), - Subsignal("dn", Pins("E16", dir="io"), Attrs(IO_TYPE="LVCMOS33")), + Subsignal("d_p", Pins("F15", dir="io"), Attrs(IO_TYPE="LVCMOS33")), + Subsignal("d_n", Pins("E16", dir="io"), Attrs(IO_TYPE="LVCMOS33")), Subsignal("pullup", Pins("G15", dir="o"), Attrs(IO_TYPE="LVCMOS33"))), - - # Status LEDs Resource("led", 0, Pins("E13", dir="o"), Attrs(IO_TYPE="LVCMOS33")), Resource("led", 1, Pins("D14", dir="o"), Attrs(IO_TYPE="LVCMOS33")), Resource("led", 2, Pins("E12", dir="o"), Attrs(IO_TYPE="LVCMOS33")), Resource("led", 3, Pins("C13", dir="o"), Attrs(IO_TYPE="LVCMOS33")), Resource("led", 4, Pins("D13", dir="o"), Attrs(IO_TYPE="LVCMOS33")), - - # Digital probe inputs Resource("probe", 0, Pins("G3", dir="i"), Attrs(IO_TYPE="LVCMOS33")), Resource("probe", 1, Pins("K3", dir="i"), Attrs(IO_TYPE="LVCMOS33")), Resource("probe", 2, Pins("T2", dir="i"), Attrs(IO_TYPE="LVCMOS33")), @@ -49,8 +47,22 @@ class IcepiZeroPlatform(Platform): connectors = [] - def get_usb_resource(self, index=0): - return self.request("usb", index) + def create_missing_domain(self, name): + from amaranth.hdl import ClockDomain, ClockSignal, ResetSignal + m = super().create_missing_domain(name) + if m is None: + m = Module() + + if name in ("usb", "usb_io"): + m.domains += ClockDomain(name) + div = Signal(2) + m.d.sync += div.eq(div + 1) + m.d.comb += [ + ClockSignal(name).eq(div[1]), + ResetSignal(name).eq(Const(0)), + ] + + return m class PicologicTop(Elaboratable): @@ -59,53 +71,93 @@ class PicologicTop(Elaboratable): def elaborate(self, platform): m = Module() - # --- USB PHY interface --- - usb_bus = platform.get_usb_resource(0) + # Clock domains are created by create_missing_domain(). + # sync = 50 MHz from oscillator (handled by platform default_clk) + # usb/usb_io = 12 MHz derived in create_missing_domain() + + # --- USB Device (auto-creates GatewarePHY for raw GPIO) --- + usb_io = platform.request("usb", 0) + m.submodules.usb = usb = USBDevice(bus=usb_io) + + # --- EP0 Control Endpoint (with descriptors) --- + descriptors = create_saleae_descriptors() + usb.add_standard_control_endpoint(descriptors) - # --- Create USB device (start in running mode for now) --- - usb_device = create_saleae_device(usb_bus, boot_mode=False) - m.submodules.usb = usb_device + # --- EP1 Bulk Endpoints (command/response) --- + ep1_out = USBStreamOutEndpoint(endpoint_number=1, max_packet_size=64) + ep1_in = USBStreamInEndpoint(endpoint_number=1, max_packet_size=64) + usb.add_endpoint(ep1_out) + usb.add_endpoint(ep1_in) + + # --- EP2 Bulk IN (sample streaming) --- + ep2_in = USBStreamInEndpoint(endpoint_number=2, max_packet_size=64) + usb.add_endpoint(ep2_in) # --- EP1 Command Handler --- - ep1 = EP1Handler() - m.submodules.ep1 = ep1 + m.submodules.ep1 = ep1 = EP1Handler() # --- Test Pattern Generator --- - test_gen = TestPatternGenerator(width=8) - m.submodules.test_gen = test_gen + m.submodules.test_gen = test_gen = TestPatternGenerator(width=8) + + # ============================================================ + # Wire EP1 OUT stream → EP1 handler rx + # ============================================================ + m.d.comb += [ + ep1.rx_data.eq(ep1_out.stream.payload), + ep1.rx_valid.eq(ep1_out.stream.valid & ep1_out.stream.ready), + ep1_out.stream.ready.eq(1), + ] + + # ============================================================ + # Wire EP1 handler tx → EP1 IN stream + # ============================================================ + m.d.comb += [ + ep1_in.stream.payload.eq(ep1.tx_data), + ep1_in.stream.valid.eq(ep1.tx_valid), + ep1_in.stream.first.eq(1), + ep1_in.stream.last.eq(1), + ep1.tx_ready.eq(ep1_in.stream.ready), + ] - # --- LED status indicators --- + # ============================================================ + # Wire test pattern → EP2 IN stream (when capture active) + # ============================================================ + sample_counter = Signal(3) + sample_byte = Signal(8) + + with m.If(ep1.capture_active & test_gen.valid & ep2_in.stream.ready): + m.d.sync += [ + sample_byte.bit_select(sample_counter, 1).eq(test_gen.sample[0]), + sample_counter.eq(sample_counter + 1), + ] + with m.If(sample_counter == 7): + m.d.comb += [ + ep2_in.stream.payload.eq(sample_byte), + ep2_in.stream.valid.eq(1), + ep2_in.stream.first.eq(1), + ep2_in.stream.last.eq(1), + ] + with m.Else(): + m.d.comb += [ + ep2_in.stream.valid.eq(0), + ep2_in.stream.first.eq(0), + ep2_in.stream.last.eq(0), + ] + + # ============================================================ + # LED status indicators + # ============================================================ leds = [platform.request("led", i) for i in range(5)] - # LED0: USB configured - # LED1: FPGA programmed - # LED2: Capture active - # LED3: EP1 activity - # LED4: heartbeat (1 Hz blink) heartbeat_counter = Signal(25) m.d.sync += heartbeat_counter.eq(heartbeat_counter + 1) m.d.comb += [ - leds[0].o.eq(usb_device.configured), + leds[0].o.eq(1), leds[1].o.eq(ep1.fpga_programmed), leds[2].o.eq(ep1.capture_active), + leds[3].o.eq(ep1.tx_valid), leds[4].o.eq(heartbeat_counter[-1]), ] - # TODO: Wire EP1 bulk endpoints to ep1 handler - # TODO: Wire EP2 bulk endpoint to test_gen when capture_active - # These require connecting to LUNA's endpoint data interfaces, - # which depends on the exact LUNA API version. - return m - - -if __name__ == "__main__": - from amaranth.back import verilog - - top = PicologicTop() - platform = IcepiZeroPlatform() - - # Generate Verilog for inspection - output = verilog.convert(top, platform=platform) - print(output) diff --git a/gateware/usb_device.py b/gateware/usb_device.py index 6e332d4..167d867 100644 --- a/gateware/usb_device.py +++ b/gateware/usb_device.py @@ -1,135 +1,49 @@ -"""Saleae Logic 8 USB device descriptors and endpoint configuration. +"""Saleae Logic 8 USB device descriptors. -Emulates a Cypress FX3-based Saleae Logic 8 (VID 21A9, PID 1004). -Presents two USB configurations: - - Bootloader mode (for firmware upload via EP0 vendor requests) - - Running mode (EP1 command/response + EP2 sample streaming) - -Logic 2 expects the device to enumerate as bootloader first, accept a -firmware upload over EP0, then re-enumerate in running mode. +Uses usb_protocol's DeviceDescriptorCollection to build standard USB +descriptors for the Saleae Logic 8 emulator. """ -from amaranth import * -from luna.gateware.usb.usb2.device import USBDevice -from luna.gateware.usb.usb2.control import USBControlEndpoint -from luna.gateware.usb.usb2.endpoints.stream import USBStreamInEndpoint, USBStreamOutEndpoint -from luna.gateware.usb.request.standard import StandardRequestHandler - +from usb_protocol.emitters import DeviceDescriptorCollection -# --- USB Descriptors --- SALEAE_VID = 0x21A9 -LOGIC8_PID_BOOT = 0x1004 # Same PID in both modes; mode distinguished by bcdDevice -LOGIC8_PID_RUNNING = 0x1004 - -BCD_DEVICE_BOOT = 0x0001 -BCD_DEVICE_RUNNING = 0x0100 - -USB_SPEED_FULL = 1 # Full-Speed only on Icepi Zero (no ULPI PHY) - - -class SaleaeBootRequestHandler(StandardRequestHandler): - """Handles EP0 vendor requests during bootloader mode. - - Accepts Cypress FX3 firmware upload (bRequest=0xA0) and triggers - re-enumeration when complete. In this emulation, we accept the bytes - but don't actually execute them — just signal readiness. - """ - - def __init__(self): - super().__init__() - self.firmware_upload_complete = Signal() - - def elaborate(self, platform): - m = Module() - - interface = self.interface - setup = interface.setup +LOGIC8_PID = 0x1004 - with m.If(setup.type == USBRequestHandler.TYPE_VENDOR): - with m.If(setup.request == 0xA0): - # Cypress FW_UPLOAD / RW_INTERNAL - # Accept all data phases, ACK status phase - m.d.comb += [ - interface.handshakes_out.ack.eq(1), - ] - # After zero-length IN (entry trigger), mark complete - with m.If((setup.length == 0) & (setup.direction == USBRequestHandler.DIRECTION_IN)): - m.d.sync += self.firmware_upload_complete.eq(1) - return m +def create_saleae_descriptors(): + """Create USB descriptors for a Saleae Logic 8 device. - -def create_saleae_device(usb_bus, *, boot_mode=True): - """Create a LUNA USBDevice configured as a Saleae Logic 8. - - Parameters - ---------- - usb_bus : Record - LUNA UTMI/ULPI bus interface. - boot_mode : bool - If True, present as bootloader (accepts firmware upload). - If False, present as running device (EP1 commands + EP2 streaming). - - Returns - ------- - USBDevice - Configured LUNA USB device ready for elaboration. + Returns a DeviceDescriptorCollection suitable for passing to + USBDevice.add_standard_control_endpoint(). """ - device = USBDevice(bus=usb_bus) - - # Standard descriptors - device.add_descriptor( - "device", - idVendor=SALEAE_VID, - idProduct=LOGIC8_PID_BOOT, - bcdDevice=BCD_DEVICE_BOOT if boot_mode else BCD_DEVICE_RUNNING, - iManufacturer="Saleae", - iProduct="Logic 8" if not boot_mode else "WestBridge", - bNumConfigurations=1, - ) - - # Configuration descriptor - device.add_descriptor( - "configuration", - bConfigurationValue=1, - iConfiguration="Saleae Logic 8 Configuration", - bmAttributes=0x80, # Bus-powered - bMaxPower=250, # 500 mA - ) - - # Interface descriptor - device.add_descriptor( - "interface", - bInterfaceNumber=0, - bAlternateSetting=0, - bNumEndpoints=3 if not boot_mode else 1, - bInterfaceClass=0xFF, # Vendor-specific - bInterfaceSubClass=0x00, - bInterfaceProtocol=0x00, - iInterface="Saleae Logic 8 Interface", - ) - - if boot_mode: - # Bootloader: only EP0 (control) needed for firmware upload - control_ep = USBControlEndpoint() - control_ep.add_handler(SaleaeBootRequestHandler()) - device.add_endpoint(control_ep) - else: - # Running mode: EP0 control + EP1 bulk IN/OUT + EP2 bulk IN - control_ep = USBControlEndpoint() - device.add_endpoint(control_ep) - - # EP1 OUT — host sends commands - ep1_out = USBStreamOutEndpoint(endpoint_number=1, max_packet_size=64) - device.add_endpoint(ep1_out) - - # EP1 IN — device sends responses - ep1_in = USBStreamInEndpoint(endpoint_number=1, max_packet_size=64) - device.add_endpoint(ep1_in) - - # EP2 IN — sample data streaming - ep2_in = USBStreamInEndpoint(endpoint_number=2, max_packet_size=64) - device.add_endpoint(ep2_in) - - return device + descriptors = DeviceDescriptorCollection() + + with descriptors.DeviceDescriptor() as d: + d.idVendor = SALEAE_VID + d.idProduct = LOGIC8_PID + d.iManufacturer = "Saleae" + d.iProduct = "Logic 8" + d.iSerialNumber = "picologic-emu" + d.bNumConfigurations = 1 + + with descriptors.ConfigurationDescriptor() as c: + with c.InterfaceDescriptor() as i: + i.bInterfaceNumber = 0 + + # EP1 OUT — host sends commands (bulk) + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x01 + e.wMaxPacketSize = 64 + + # EP1 IN — device sends responses (bulk) + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x81 + e.wMaxPacketSize = 64 + + # EP2 IN — sample data streaming (bulk) + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x82 + e.wMaxPacketSize = 64 + + return descriptors diff --git a/scripts/build.sh b/scripts/build.sh index 4f2ade0..b1fcfc8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -27,9 +27,10 @@ pnr() { echo "=== Place & Route ===" nextpnr-ecp5 \ --$DEVICE \ - --json "$BUILD_DIR/picologic.json" \ - --lpf "$LPF" \ - --textcfg "$BUILD_DIR/picologic.config" \ + --package CABGA256 \ + --json "$BUILD_DIR/top.json" \ + --lpf "$BUILD_DIR/top.lpf" \ + --textcfg "$BUILD_DIR/top.config" \ --freq 50 } @@ -37,13 +38,13 @@ pack() { echo "=== Bitstream Packing ===" ecppack \ --compress \ - "$BUILD_DIR/picologic.config" \ - "$BUILD_DIR/picologic.bit" + "$BUILD_DIR/top.config" \ + "$BUILD_DIR/top.bit" } flash() { echo "=== Programming ===" - openFPGALoader -b icepi-zero "$BUILD_DIR/picologic.bit" + openFPGALoader --fpga-part LFE5U-25F "$BUILD_DIR/top.bit" } case "$TARGET" in