diff --git a/examples/usb/hid.py b/examples/usb/hid.py new file mode 100644 index 000000000..bd02c4951 --- /dev/null +++ b/examples/usb/hid.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# +# This file is part of LUNA. +# +# Copyright (c) 2020 Great Scott Gadgets +# SPDX-License-Identifier: BSD-3-Clause + +from nmigen import Elaboratable, Module, Signal + +from luna import top_level_cli +from luna.gateware.usb.devices.hid import HIDDevice + +class USBHIDExample(Elaboratable): + def elaborate(self, platform): + m = Module() + + # Generate our domain clocks/resets. + m.submodules.car = platform.clock_domain_generator() + + # Create the 32-bit counter we'll be using as our status signal. + counter = Signal(32) + m.d.usb += counter.eq(counter + 1) + + # Create our USB device interface... + ulpi = platform.request(platform.default_usb_connection) + m.submodules.hid = hid = \ + HIDDevice(bus=ulpi, idVendor=0x1337, idProduct=0x1337) + + # Connect counter as a pollable report + hid.add_input(counter) + + m.d.comb += [ + hid.connect.eq(1) + ] + + return m + +if __name__ == "__main__": + top_level_cli(USBHIDExample) \ No newline at end of file diff --git a/luna/gateware/usb/devices/hid.py b/luna/gateware/usb/devices/hid.py new file mode 100644 index 000000000..de197a1a3 --- /dev/null +++ b/luna/gateware/usb/devices/hid.py @@ -0,0 +1,123 @@ +from nmigen import Elaboratable, Module, Signal, Cat + +from ...stream import StreamInterface +from ..usb2.device import USBDevice +from ..usb2.request import USBRequestHandler, StallOnlyRequestHandler +from ..usb2.endpoints.status import USBSignalInEndpoint + +from usb_protocol.types import USBRequestType,USBTransferType +from usb_protocol.emitters import DeviceDescriptorCollection +from usb_protocol.emitters.descriptors.hid import HIDDescriptor, HIDPrefixes + + + + +class HIDDevice(Elaboratable): + + _STATUS_ENDPOINT_NUMBER = 1 + + def __init__(self, *, bus, idVendor, idProduct, + manufacturer_string="LUNA", + product_string="HID Register Reader", + serial_number=None, max_packet_size=64, + usage_page=0x01, usage=0x0): + + self._bus = bus + self._idVendor = idVendor + self._idProduct = idProduct + self._manufacturer_string = manufacturer_string + self._product_string = product_string + self._serial_number = serial_number + self._max_packet_size = max_packet_size + self._usage_page = usage_page + self._usage = usage + + # + # I/O port + # + self.connect = Signal() + self.inputs = [] + + def _int_to_le_bytes(self, i): + for n in (1, 2, 4): + try: + return int.to_bytes(i, n, byteorder="little") + except OverflowError: + pass + raise OverflowError("Value cannot be represented in 4 bytes") + + def add_input(self, signal, input_range=None, usage=0x0): + if(input_range == None): + shape = signal.shape() + if(shape.signed): + input_range = range(-(1 << (shape.width - 1)) + 1, 1 << (shape.width - 1)) + else: + input_range = range(0, (1 << shape.width) - 1) + self.inputs.append((signal, input_range, usage)) + + + def create_descriptors(self): + descriptors = DeviceDescriptorCollection() + with descriptors.DeviceDescriptor() as d: + d.idVendor = self._idVendor + d.idProduct = self._idProduct + + d.iManufacturer = self._manufacturer_string + d.iProduct = self._product_string + d.iSerialNumber = self._serial_number + + d.bNumConfigurations = 1 + with descriptors.ConfigurationDescriptor() as c: + with c.InterfaceDescriptor() as i: + i.bInterfaceNumber = 0 + + i.bInterfaceClass = 0x03 # HID + i.bInterfaceSubclass = 0x00 # No SubClass + i.bInterfaceProtocol = 0x00 # No Protocol + + i.iInterface = 0x00 + + hid_header = HIDDescriptor(descriptors) + hid_header.add_report(HIDPrefixes.USAGE_PAGE, self._usage_page) + hid_header.add_report(HIDPrefixes.USAGE, self._usage) + hid_header.add_report(HIDPrefixes.COLLECTION, 0x01) + for (signal, input_range, usage) in self.inputs: + hid_header.add_report(HIDPrefixes.LOGICAL_MIN, *self._int_to_le_bytes(input_range.start)) + hid_header.add_report(HIDPrefixes.LOGICAL_MAX, *self._int_to_le_bytes(input_range.stop)) + hid_header.add_report(HIDPrefixes.REPORT_SIZE, *self._int_to_le_bytes(signal.shape().width)) + hid_header.add_report(HIDPrefixes.REPORT_COUNT, 0x01), + hid_header.add_report(HIDPrefixes.USAGE, usage) + hid_header.add_input() + hid_header.add_report(HIDPrefixes.END_COLLECTION) + + i.add_subordinate_descriptor(hid_header) + + with i.EndpointDescriptor() as e: + e.bEndpointAddress = 0x80 | self._STATUS_ENDPOINT_NUMBER + e.bmAttributes = USBTransferType.INTERRUPT + e.wMaxPacketSize = self._max_packet_size + e.bInterval = 1 + + return descriptors + + def elaborate(self, platform): + m = Module() + + # Create our core USB device, and add a standard control endpoint. + m.submodules.usb = usb = USBDevice(bus=self._bus) + _control_ep = usb.add_standard_control_endpoint(self.create_descriptors()) + + # Cram all the registers into a single report + statuses = Cat([signal for (signal, _input_range, _usage) in self.inputs]) + + # Create an endpoint to emit our report every time we get polled + status_ep = USBSignalInEndpoint(width=statuses.shape().width, endpoint_number=1, endianness="little") + usb.add_endpoint(status_ep) + + # Connect our USB device + m.d.comb += [ + status_ep.signal.eq(statuses), + usb.connect .eq(self.connect) + ] + + return m