Skip to content

Commit

Permalink
docs: use a simpler example for the bulk transfer tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinevg committed Jan 27, 2025
1 parent 86597d9 commit 2dd24e1
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 203 deletions.
84 changes: 17 additions & 67 deletions cynthion/python/examples/tutorials/gateware-usb-device-04.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# SPDX-License-Identifier: BSD-3-Clause

from amaranth import *
from amaranth.lib.fifo import SyncFIFO
from luna.usb2 import USBDevice
from usb_protocol.emitters import DeviceDescriptorCollection

Expand All @@ -24,7 +25,6 @@
from usb_protocol.types import USBRequestType

from luna.usb2 import USBStreamInEndpoint, USBStreamOutEndpoint
from luna.gateware.stream import StreamInterface
from usb_protocol.types import USBDirection, USBTransferType

VENDOR_ID = 0x1209 # https://pid.codes/1209/
Expand Down Expand Up @@ -106,68 +106,6 @@ def elaborate(self, platform):
return m


class StreamingMemoryStore(Elaboratable):
""" A simple memory storage module that exposes a read/write streaming interface. """

def __init__(self, stream_out: StreamInterface, stream_in: StreamInterface):
self.stream_out = stream_out
self.stream_in = stream_in

# high when a memory write is in process
self.write_active = Signal()

def elaborate(self, platform):
m = Module()

# create a memory we will use as a data source/sink for our bulk endpoints
m.submodules.ram = ram = Memory(
width = 8,
depth = MAX_PACKET_SIZE,
init = [0] * MAX_PACKET_SIZE
)
w_port = ram.write_port(domain="usb")
r_port = ram.read_port(domain="usb")

# set the write_active status to the write port's enable status
m.d.comb += self.write_active.eq(w_port.en)

# shortcuts
stream_out = self.stream_out
stream_in = self.stream_in

# - EP 0x01 OUT logic ------------------------------------------------

# let the stream know we're always ready to start reading
m.d.comb += stream_out.ready.eq(1)

# wire the payload from the host up to our memory write port
m.d.comb += w_port.data.eq(stream_out.payload)

# read each byte coming in on the stream and write it to memory
with m.If(stream_out.valid & stream_out.ready):
m.d.comb += w_port.en.eq(1)
m.d.usb += w_port.addr.eq(w_port.addr + 1);
with m.Else():
m.d.comb += w_port.en.eq(0)
m.d.usb += w_port.addr.eq(0)

# - EP 0x82 IN logic -------------------------------------------------

# wire the payload to the host up to our memory read port
m.d.comb += stream_in.payload.eq(r_port.data)

# when the stream is ready and the write port is not active,
# read each byte from memory and write it out to the stream
with m.If(stream_in.ready & ~w_port.en):
m.d.usb += stream_in.valid.eq(1)
m.d.usb += r_port.addr.eq(r_port.addr + 1)
with m.Else():
m.d.usb += stream_in.valid.eq(0)
m.d.usb += r_port.addr.eq(0)

return m


class GatewareUSBDevice(Elaboratable):
""" A simple USB device that can communicate with the host via vendor and bulk requests. """

Expand Down Expand Up @@ -259,11 +197,23 @@ def elaborate(self, platform):
)
usb.add_endpoint(ep_in)

# create a simple streaming memory storage module
m.submodules.store = store = StreamingMemoryStore(ep_out.stream, ep_in.stream)
# create a FIFO queue we'll connect to the stream interfaces of our
# IN & OUT endpoints
m.submodules.fifo = fifo = DomainRenamer("usb")(
SyncFIFO(width=8, depth=MAX_PACKET_SIZE)
)

# invalidate any data queued on ep_in when the memory performs a write operation
m.d.comb += ep_in.discard.eq(store.write_active)
# connect our Bulk OUT endpoint's stream interface to the FIFO's write port
stream_out = ep_out.stream
m.d.comb += fifo.w_data.eq(stream_out.payload)
m.d.comb += fifo.w_en.eq(stream_out.valid)
m.d.comb += stream_out.ready.eq(fifo.w_rdy)

# connect our Bulk IN endpoint's stream interface to the FIFO's read port
stream_in = ep_in.stream
m.d.comb += stream_in.payload.eq(fifo.r_data)
m.d.comb += stream_in.valid.eq(fifo.r_rdy)
m.d.comb += fifo.r_en.eq(stream_in.ready)

# configure the device to connect by default when plugged into a host
m.d.comb += usb.connect.eq(1)
Expand Down
167 changes: 31 additions & 136 deletions docs/source/tutorials/gateware_usb_device_04.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Open ``gateware-usb-device.py`` and add the highlighted lines:
.. code-block :: python
:caption: gateware-usb-device.py
:linenos:
:emphasize-lines: 19-27, 32, 51-60
:emphasize-lines: 19-26, 31, 50-59
from amaranth import *
from luna.usb2 import USBDevice
Expand All @@ -57,7 +57,6 @@ Open ``gateware-usb-device.py`` and add the highlighted lines:
from luna.gateware.usb.usb2.transfer import USBInStreamInterface
from usb_protocol.types import USBRequestType
from luna.gateware.stream import StreamInterface
from luna.usb2 import (
USBStreamInEndpoint,
USBStreamOutEndpoint,
Expand Down Expand Up @@ -115,7 +114,7 @@ Add USB Stream Endpoints

Once our endpoint descriptors have been added to our device configuration we will need some gateware that will be able to respond to USB requests from the host and allow us to receive and transmit data.

LUNA provides the ``USBStreamOutEndpoint`` and ``USBStreamInEndpoint`` modules which conform to the `Amaranth Data streams <https://amaranth-lang.org/docs/amaranth/latest/stdlib/stream.html>`__ interface. Simply put, streams provide a uniform mechanism for unidirectional exchange of arbitrary data between gateware modules.
LUNA provides the ``USBStreamOutEndpoint`` and ``USBStreamInEndpoint`` components which conform to the `Amaranth Data streams <https://amaranth-lang.org/docs/amaranth/latest/stdlib/stream.html>`__ interface. Simply put, streams provide a uniform mechanism for unidirectional exchange of arbitrary data between gateware components.

.. code-block :: python
:caption: gateware-usb-device.py
Expand Down Expand Up @@ -155,57 +154,33 @@ We now have two streaming endpoints that are able to receive and transmit data b

However, before we can stream any data across these endpoints we first need to come up with a *USB Function* for each of our endpoints. In other words, what does our device actually _do_?

This could be any data source and/or sink but for the purposes of this tutorial let's create a (very) simple storage device.
This could be any data source and/or sink but for the purposes of this tutorial let's create a simple loopback function that will accept a bulk OUT request from the host and then return the request payload when the host makes a bulk IN request.


Define Endpoint Functions
-------------------------

Our device's endpoint functions will be a simple streaming memory store module that will allow us to read & write data over our bulk endpoints.
A simple implementation for our device's endpoint functions could be a simple FIFO (First In First Out) queue with enough space to hold the 512 bytes of a bulk transfer.

Using the OUT endpoint we can transmit a stream of data from the host to Cynthion and write into a local memory. Then, we'd like to be able to transmit a request from the host to the IN endpoint and retrieve the previously stored data.
Using the OUT endpoint we could then transmit a stream of data from the host to Cynthion and write it into the FIFO. Then, when we transmit a request from the host to the IN endpoint we can stream the previously queued data back to the host.

We're only working in a single clock-domain so we can use a `SyncFIFO <https://amaranth-lang.org/docs/amaranth/latest/stdlib/fifo.html#amaranth.lib.fifo.SyncFIFO>`__ from the Amaranth standard library for our queue:

This means we'll need some memory we can read and write to, so let's begin by creating an `Amaranth Memory component <https://amaranth-lang.org/docs/amaranth/latest/stdlib/memory.html>`__ which uses the FPGA's Block RAM for storage:

.. code-block :: python
:caption: gateware-usb-device.py
:linenos:
:emphasize-lines: 6-33, 41-57
:emphasize-lines: 2, 28-44
from amaranth import *
from amaranth.lib.fifo import SyncFIFO
from luna.usb2 import USBDevice
from usb_protocol.emitters import DeviceDescriptorCollection
...
class VendorRequestHandler(ControlRequestHandler):
...
class StreamingMemoryStore(Elaboratable):
def __init__(self, stream_out: StreamInterface, stream_in: StreamInterface):
self.stream_out = stream_out
self.stream_in = stream_in
# high when a memory write is in process
self.write_active = Signal()
def elaborate(self, platform):
m = Module()
# create a memory we can use as a data source/sink for our bulk endpoints
m.submodules.ram = ram = Memory(
width = 8,
depth = MAX_PACKET_SIZE,
init = [0] * MAX_PACKET_SIZE
)
w_port = ram.write_port(domain="usb")
r_port = ram.read_port(domain="usb")
# set the write_active status to the write port's enable status
m.d.comb += self.write_active.eq(w_port.en)
# shortcuts
stream_out = self.stream_out
stream_in = self.stream_in
return m
class GatewareUSBDevice(Elaboratable):
...
Expand All @@ -224,108 +199,30 @@ This means we'll need some memory we can read and write to, so let's begin by cr
)
usb.add_endpoint(ep_in)
# create a simple streaming memory storage module
m.submodules.store = store = StreamingMemoryStore(ep_out.stream, ep_in.stream)
# create a FIFO queue we'll connect to the stream interfaces of our
# IN & OUT endpoints
m.submodules.fifo = fifo = DomainRenamer("usb")(
fifo.SyncFIFO(width=8, depth=MAX_PACKET_SIZE)
)
# connect our Bulk OUT endpoint's stream interface to the FIFO's write port
stream_out = ep_out.stream
m.d.comb += fifo.w_data.eq(stream_out.payload)
m.d.comb += fifo.w_en.eq(stream_out.valid)
m.d.comb += stream_out.ready.eq(fifo.w_rdy)
# invalidate any data queued on ep_in when the memory performs a write operation
m.d.comb += ep_in.discard.eq(store.write_active)
# connect our Bulk IN endpoint's stream interface to the FIFO's read port
stream_in = ep_in.stream
m.d.comb += stream_in.payload.eq(fifo.r_data)
m.d.comb += stream_in.valid.eq(fifo.r_rdy)
m.d.comb += fifo.r_en.eq(stream_in.ready)
# configure the device to connect by default when plugged into a host
m.d.comb += usb.connect.eq(1)
return m
Great, now we have a 8-bit wide memory that's large enough to store a full high-speed transfer packet! Let's implement the logic to handle read/write requests to our memory store module.


Bulk OUT Endpoint Gateware
--------------------------

To implement our OUT endpoint's stream we'll need to read the data stream coming from the host and write each bit into our memory component.

Let's implement this by adding the following lines:

.. code-block :: python
:caption: gateware-usb-device.py
:linenos:
:emphasize-lines: 11-25
...
class StreamingMemoryStore(Elaboratable):
def elaborate(self, platform):
...
# shortcuts
stream_out = self.stream_out
stream_in = self.stream_in
# - EP 0x01 OUT logic ------------------------------------------------
# let the stream know we're always ready to start reading
m.d.comb += stream_out.ready.eq(1)
# wire the payload from the host up to our memory write port
m.d.comb += w_port.data.eq(stream_out.payload)
# read each byte coming in on the stream and write it to memory
with m.If(stream_out.valid & stream_out.ready):
m.d.comb += w_port.en.eq(1)
m.d.usb += w_port.addr.eq(w_port.addr + 1);
with m.Else():
m.d.comb += w_port.en.eq(0)
m.d.usb += w_port.addr.eq(0)
return m
...
When the host makes a Bulk OUT request to the device we read each byte of the data packet from the endpoint stream as it is received and then write it to a consecutive address in our memory. We'll talk more about LUNA stream interfaces in a future tutorial but all you need to know for now is that they provide a uniform interface between LUNA endpoints and other components.

Finally, let's do the same for our IN endpoint's stream.

Bulk IN Endpoint Gateware
-------------------------

Add the following lines:

.. code-block :: python
:caption: gateware-usb-device.py
:linenos:
:emphasize-lines: 11-26
...
class StreamingMemoryStore(Elaboratable):
def elaborate(self, platform):
...
# - EP 0x01 OUT logic ------------------------------------------------
...
# - EP 0x82 IN logic -------------------------------------------------
# wire the payload to the host up to our memory read port
m.d.comb += stream_in.payload.eq(r_port.data)
# discard streamed data when memory write port is active
m.d.comb += self.ep_in.discard.eq(w_port.en)
# when the stream is ready and the write port is not active,
# read each byte from memory and write it out to the stream
with m.If(stream_in.ready & ~w_port.en):
m.d.usb += stream_in.valid.eq(1)
m.d.usb += r_port.addr.eq(r_port.addr + 1)
with m.Else():
m.d.usb += stream_in.valid.eq(0)
m.d.usb += r_port.addr.eq(0)
return m
...
This time, when the host makes a Bulk IN request, we write the content of each consecutive address of our memory to the stream. Let's try it out!
And that's it, we've defined our endpoint functions! Let's try it out.


Test Bulk Endpoints
Expand Down Expand Up @@ -358,10 +255,8 @@ Congratulations, if you made it this far then you've just finished building your
Exercises
=========

1. Add a vendor request to zero the memory.
2. Create a benchmark to test the speed of your device when doing Bulk IN and OUT transfers.
3. Use the contents of the memory to drive the pattern of the FPGA LEDs.
4. Move the device endpoint to ``aux_phy`` and attempt to capture the enumeration of a device plugged into a host via the ``target_phy`` port.
1. Create a benchmark to test the speed of your device when doing Bulk IN and OUT transfers.
2. Move the device endpoint to ``aux_phy`` and attempt to capture the packets exchanged between a device plugged into a host via the ``target_phy`` port.


More information
Expand Down

0 comments on commit 2dd24e1

Please sign in to comment.