USB Gateware: Part 3 - Control Transfers ######################################## This series of tutorial walks through the process of implementing a complete USB device with Cynthion and `LUNA `__: * :doc:`/tutorials/gateware_usb_device_01` * :doc:`/tutorials/gateware_usb_device_02` * :doc:`/tutorials/gateware_usb_device_03` *(This tutorial)* * :doc:`/tutorials/gateware_usb_device_04` The goal of this tutorial is to define a control interface for the device we created in Part 1 that will allow it to receive and respond to control requests from a host. Prerequisites ============= * Complete the :doc:`/tutorials/gateware_usb_device_01` tutorial. * Complete the :doc:`/tutorials/gateware_usb_device_02` tutorial. *(Optional, required for Windows support)* Data Transfer between a Host and Device ======================================= USB is a *host-centric bus*, what this means is that all transfers are initiated by the host irrespective of the direction of data transfer. For data transfers to the device, the host issues an OUT token to notify the device of an incoming data transfer. When data has to be transferred from the device, the host issues an IN token to notify the device that it should send some data to the host. The USB 2.0 specification defines four endpoint or transfer types: * **Control Transfers:** Typically used for command and status operations, control transfers are the only transfer type with a defined USB format. * **Bulk Transfers:** Bulk transfers are best suited for large amounts of data delivered in bursts such as file transfers to/from a storage device or the captured packet data from Cynthion to the control host. * **Interrupt Transfers:** Interrupt transfers are a bit of a misnomer as the host needs to continuously poll the device to check if an interrupt has occurred but the principle is the same. Commonly used for peripherals that generate input events such as a keyboard or mouse. * **Isochronous Transfers:** Finally, isochronous transfers occur continuously with a fixed periodicity. Suited for time-sensitive information such as video or audio streams they do not offer any guarantees on delivery. If a packet or frame is dropped it's is up to the host driver to decide on how to best handle it. By default all LUNA devices have a default implementation for two endpoints: An OUT Control Endpoint and an IN Control endpoint. These endpoints are used by the host to enumerate the device but they can also be extended to support various other class or custom vendor requests. We'll start by extending our control endpoints to support two vendor requests: One to set the state of the Cynthion **FPGA LEDs** and another to get the state of the Cynthion **USER BUTTON**. Extend Default Control Endpoints -------------------------------- To implement vendor requests, begin by adding a ``VendorRequestHandler`` to our device's control endpoint: .. code-block :: python :caption: gateware-usb-device.py :linenos: :emphasize-lines: 12-14, 19-43, 83-84 from amaranth import * from luna.usb2 import USBDevice from usb_protocol.emitters import DeviceDescriptorCollection from luna.gateware.usb.request.windows import ( MicrosoftOS10DescriptorCollection, MicrosoftOS10RequestHandler, ) from usb_protocol.emitters.descriptors.standard import get_string_descriptor from usb_protocol.types.descriptors.microsoft10 import RegistryTypes from luna.gateware.stream.generator import StreamSerializer from luna.gateware.usb.request.control import ControlRequestHandler from luna.gateware.usb.usb2.transfer import USBInStreamInterface VENDOR_ID = 0x1209 # https://pid.codes/1209/ PRODUCT_ID = 0x0001 class VendorRequestHandler(ControlRequestHandler): VENDOR_SET_FPGA_LEDS = 0x01 VENDOR_GET_USER_BUTTON = 0x02 def elaborate(self, platform): m = Module() # shortcuts interface: RequestHandlerInterface = self.interface setup: SetupPacket = self.interface.setup # get a reference to the FPGA LEDs and USER button fpga_leds = Cat(platform.request("led", i).o for i in range(6)) user_button = platform.request("button_user").i # create a streamserializer for transmitting IN data back to the host serializer = StreamSerializer( domain = "usb", stream_type = USBInStreamInterface, data_length = 1, max_length_width = 1, ) m.submodules += serializer return m class GatewareUSBDevice(Elaboratable): ... def elaborate(self, platform): m = Module() # configure cynthion's clocks and reset signals m.submodules.car = platform.clock_domain_generator() # request the physical interface for cynthion's TARGET C port ulpi = platform.request("target_phy") # create the USB device m.submodules.usb = usb = USBDevice(bus=ulpi) # create our standard descriptors and add them to the device's control endpoint descriptors = self.create_standard_descriptors() control_endpoint = usb.add_standard_control_endpoint(descriptors) # add microsoft os 1.0 descriptors and request handler descriptors.add_descriptor(get_string_descriptor("MSFT100\xee"), index=0xee) msft_descriptors = MicrosoftOS10DescriptorCollection() with msft_descriptors.ExtendedCompatIDDescriptor() as c: with c.Function() as f: f.bFirstInterfaceNumber = 0 f.compatibleID = 'WINUSB' with msft_descriptors.ExtendedPropertiesDescriptor() as d: with d.Property() as p: p.dwPropertyDataType = RegistryTypes.REG_SZ p.PropertyName = "DeviceInterfaceGUID" p.PropertyData = "{88bae032-5a81-49f0-bc3d-a4ff138216d6}" msft_handler = MicrosoftOS10RequestHandler(msft_descriptors, request_code=0xee) control_endpoint.add_request_handler(msft_handler) # add our vendor request handler control_endpoint.add_request_handler(VendorRequestHandler()) # configure the device to connect by default when plugged into a host m.d.comb += usb.connect.eq(1) return m Vendor requests are unique to a device and are identified by the 8-bit ``bRequest`` field of the control transfer setup packet. Here we've defined two id's corresponding to setting the led states and getting the button state. So far our ``VendorRequestHandler`` contains references to Cynthion's **FPGA LEDs** and **USER BUTTON**, as well as a **StreamSerializer** we'll be using to send data back to the host when it asks for the **USER BUTTON** status. Implement Vendor Request Handlers --------------------------------- Let's implement that functionality below: .. code-block :: python :caption: gateware-usb-device.py :linenos: :emphasize-lines: 25-65 class VendorRequestHandler(ControlRequestHandler): VENDOR_SET_FPGA_LEDS = 0x01 VENDOR_GET_USER_BUTTON = 0x02 def elaborate(self, platform): m = Module() # Shortcuts. interface: RequestHandlerInterface = self.interface setup: SetupPacket = self.interface.setup # Grab a reference to the FPGA LEDs and USER button. fpga_leds = Cat(platform.request("led", i).o for i in range(6)) user_button = platform.request("button_user").i # Create a StreamSerializer for sending IN data back to the host serializer = StreamSerializer( domain = "usb", stream_type = USBInStreamInterface, data_length = 1, max_length_width = 1, ) m.submodules += serializer # we've received a setup packet containing a vendor request. with m.If(setup.type == USBRequestType.VENDOR): # use a state machine to sequence our request handling with m.FSM(domain="usb"): with m.State("IDLE"): with m.If(setup.received): with m.Switch(setup.request): with m.Case(self.VENDOR_SET_FPGA_LEDS): m.next = "HANDLE_SET_FPGA_LEDS" with m.Case(self.VENDOR_GET_USER_BUTTON): m.next = "HANDLE_GET_USER_BUTTON" with m.State("HANDLE_SET_FPGA_LEDS"): # take ownership of the interface m.d.comb += interface.claim.eq(1) # if we have an active data byte, set the FPGA LEDs to the payload with m.If(interface.rx.valid & interface.rx.next): m.d.usb += fpga_leds.eq(interface.rx.payload[0:6]) # once the receive is complete, respond with an ACK with m.If(interface.rx_ready_for_response): m.d.comb += interface.handshakes_out.ack.eq(1) # finally, once we reach the status stage, send a ZLP with m.If(interface.status_requested): m.d.comb += self.send_zlp() m.next = "IDLE" with m.State("HANDLE_GET_USER_BUTTON"): # take ownership of the interface m.d.comb += interface.claim.eq(1) # write the state of the user button into a local data register data = Signal(8) m.d.comb += data[0].eq(user_button) # transmit our data using a built-in handler function that # automatically advances the FSM back to the 'IDLE' state on # completion self.handle_simple_data_request(m, serializer, data) return m When handling a control request in LUNA the first thing we look at is the ``setup.type`` field of the setup packet interface. We could check for other types such as ``USBRequestType.CLASS`` or ``USBRequestType.DEVICE`` if we wanted to implement handlers for them but, in this case, we're only interested in vendor requests. Next, we take ownership of the interface, in order to avoid conflicting with the standard, or other registered request handlers. Then we sequence the actual request handling with an `Amaranth Finite State Machine `__, starting in the ``IDLE`` state. While in ``IDLE`` we wait for the ``setup.received`` signal to go high and signal the arrival of a new control request. We then parse the ``setup.request`` field to identify the next state to advance our FSM to. (We could also use the other setup packet fields such as ``wValue`` and ``wIndex`` for dispatch or as arguments but for now we're just intered in ``bRequest``.) We then implement two handlers, the first is ``HANDLE_SET_FPGA_LEDS``, which needs to read the data sent with our OUT control request in order to set the fpga leds state. Then the second, in ``HANDLE_GET_USER_BUTTON`` we will use one of the built-in LUNA helper function to respond to our IN control request with the data containing the state of the user button. Test Control Endpoints ====================== First, remember to build and upload the device gateware to your Cynthion with: .. code-block :: sh python ./gateware-usb-device.py Then, open your ``test-gateware-usb-device.py`` script from the previous tutorials and add the following code to it: .. literalinclude:: ../../../cynthion/python/examples/tutorials/test-gateware-usb-device-03.py :caption: test-gateware-usb-device.py :language: python :linenos: :emphasize-lines: 2, 7-8, 22-68, 78-87 Run the file with: .. code-block :: sh python ./test-gateware-usb-device.py And, if all goes well you should see the **FPGA LEDs** on Cynthion counting in binary. If you press and release the **USER** button you should see the count reset back to zero and the following text in the terminal. .. code-block :: sh USER button is: ON USER button is: OFF Job done! In the next part of the tutorial we'll finish up by adding IN and OUT Bulk endpoints to our device. Exercises ========= 1. Add a vendor request to retrieve the current state of the FPGA LEDs. 2. Add a vendor request that will disconnect and then re-connect your device to the USB bus. More information ================ * Beyond Logic's `USB in a NutShell `__. * `LUNA Documentation `__ Source Code =========== .. literalinclude:: ../../../cynthion/python/examples/tutorials/gateware-usb-device-03.py :caption: gateware-usb-device-03.py :language: python :linenos: .. literalinclude:: ../../../cynthion/python/examples/tutorials/test-gateware-usb-device-03.py :caption: test-gateware-usb-device-03.py :language: python :linenos: