USB module reference

Header :
#include <usb.h>
Makefile : usb is already included by default

Description

A quick look at the USB protocol

USB is, as its name implies, universal and probably doesn't need much presentation from a user point of view. Usually, you plug your device and it just works. The protocol to achieve this behind the scenes is very interesting however, and before going into the specifics of libtungsten's USB API, we'll go through some generic USB concepts that you will need in order to use it.

USB is a master/slave protocol (like I2C or SPI for instance). The master is called the Host (usually a computer) and the slave is called the Device (a keyboard, a mass storage peripheral, a microcontroller, ...). A Host can manage multiple Devices (up to 127). The Host is responsible for all communications, the Devices cannot talk without being asked.

In order to be so versatile, the USB protocol takes a hierarcherical approch :

  • a device has 1 or more configurations
  • a configuration has 1 or more interfaces
  • an interface has 1 or more endpoints

Each device has a Vendor ID and a Product ID, which are both 2-byte values that identify respectively the manufacturer and the model of the device. For instance, Atmel's Vendor ID is 0x03EB (they bought that from the corporation that developped and maintains the protocol) and Carbide's Product ID is 0xCABD.

Multiple configurations are used by devices which need to completely change the way they are perceived by the host according to some software settings. This is rarely used (most devices only have one default configuration) and we will ignore this level from now on.

An interface represents a feature offered by a device. Devices can have multiple interfaces to allow multiple features to be offered through a single cable. For example, a wireless receiver can provide two interfaces : a keyboard and a mouse. A multi-function printer can provide a Printer interface and a Scanner interface. Devices with multiple interfaces are pretty common and offer a lot of flexibility to manufacturers.

An endpoint is a memory buffer shared by the host and the device to exchange requests and data. Endpoints have a number, a direction, and a type which can be :

  • Control : used to send and receive formatted requests (there are standard requests but you can also define your own)
  • Bulk : used to send large amount of unformatted data without timing or bandwith guaranties, such as files on a mass storage peripheral
  • Interrupt : used to send small amount of data with timing guaranties, useful for devices such as keyboards and mice when you don't want your pointer to feel slow because of a file transfer on a USB hard drive in the background
  • Isochronous : used to send a stream of data with a guaranteed bandwith but no error detection, such as the video feed of a webcam (which can tolerate an occasional corrupt packet)
Direction is always seen from the host's point of view : OUT (data is transfered from the host to the device) or IN (data is transfered from the device to the host). Endpoint 0 is always a Control endpoint supporting both OUT and IN, and is mandatory since it is used by the host to learn more about the device when it is plugged in. While not forbidden by the protocol, there is almost never a need to create another Control endpoint (some host-side usb libraries don't even support it).

When the host detects that a new device is plugged in, a process takes place where the host asks the device what configurations, interfaces and endpoints it provides. This is called enumeration. It is based on small chunks of data called descriptors. A descriptor is simply a small, standard data structure that gives some information on a related feature. For example, there are device descriptors, interface descriptors, endpoint descriptors, and so on, and an interface descriptor tells the host what feature this interface provides (via standard Class, Subclass and Protocol identifiers for standard features such as a keyboard or a printer, or a specific identifier for other custom features).

USB is a powerful but complex protocol and all of the above represents a good amount of new concepts and terminology to assimilate, but with this you already have a fair overview of the way it works. If you want to learn more, here are two great references :

Implementation

Atmel's ATSAM4 microcontrollers support the USB2.0 full-speed protocol in both Device and Host mode. Currently, only the Device mode is implemented in libtungsten. Once the USB subsystem is initialized with initDevice(), the library takes care of the whole process of enumeration by the host when the cable is connected. The API described below provides methods to extend the Control endpoint, create new endpoints, and define callbacks on some protocol-related events.

While the protocol is complex, using the library to perform basic request and data transfer with the host (which probably covers the majority of cases) is actually quite simple. All you need to do is call initDevice() (no argument required) to initialize the module and setControlHandler() to register a handler that will be called whenever there is a request from the host. See the examples for more in-depth information.

Important note about IN endpoints and their handlers : the handler is not called when a IN request is received (as you might have thought), but before that, as soon as the current bank is ready to receive some data. This is because the USB protocol requires that, when the host sends an IN request, the device should answer immediately (or at least, within a short delay) and there is no time for handlers to be called and data to be moved around at this moment. Instead, the data should be prepared beforehand in the bank and is sent by the hardware when the request is received. Note that this can take an indefinite amount of time : there is no guarantee that the host will ever request this data. If it does, as soon as the transfer is finished, the IN handler is called again to fill the bank with the next chunk of data. In case the host hasn't read the data after some time and you want to change it (for instance if you want to stream some real-time measurements to the host, or if a request on the Control endpoint asks the device to provide something else on this endpoint), you can use abortINTransfer() to drop the current data and ask the IN handler to be called again to fill the bank with some new data.

For more advance usage, the device, configuration and interface descriptors are declared as extern, meaning that they can be modified directly by your code. For example, you can change the bMaxPower setting in the configuration descriptor like this :

USB::_configurationDescriptor.bMaxPower=100;

And for even more advance features, take a look at the Hacking section below.

Common scenario

If you simply want your board to be able to connect to a computer through USB in order to receive some commands and respond some data, here are the steps to follow :

  • Make sure you the microcontroller has been initialized and is running from a high-frequency clock, for example :
    Core::init();
    SCIF::enableRCFAST(SCIF::RCFASTFrequency::RCFAST_12MHZ);
    PM::setMainClockSource(PM::MainClockSource::RCFAST);
    With Carbide, this is automatically done in
    Carbide::init();
  • Initialize the USB driver :
    USB::initDevice();
    Optionnally, this is where you can define vendorId, productId and deviceRevision :
    USB::initDevice(0x03EB, 0xCABD, 0x0001);
  • If you want your device to enumerate with a custom name, this is the moment to define it :
    USB::setStringDescriptor(USB::StringDescriptors::MANUFACTURER, ...);
    USB::setStringDescriptor(USB::StringDescriptors::PRODUCT, ...);
  • Define a handler function for requests received on the Control endpoint with this prototype :
    int myUSBControlHandler(USB::SetupPacket &lastSetupPacket, uint8_t* data, int size);
  • Register this handler :
    USB::setControlHandler(myUSBControlHandler);
  • In myUSBControlHandler(), check the 1-byte user-defined request code from lastSetupPacket.bRequest and the transfer direction from lastSetupPacket.direction (either USB::EPDir::OUT for data sent by the computer, or USB::EPDir::IN for data requested by the computer that you need to send) :
    • for IN transfers, copy the data you need to send (according to the request code) into data, up to size bytes (never copy more than that into the buffer), and return the number of bytes that you are sending (which is always equal to or lower than size)
    • for OUT transfers, this is usually a command that you need to process; however, do not process any lengthy command in the handler (which is called in an interrupt context), but handle it in your main loop instead :
      • copy the size bytes from data sent by the computer into your own buffer, and save the request code as well
      • call USB::setEndpointBusy(); to prevent the computer from sending another request while you are processing this one
      • in your main loop, check the value of the request code and process the command
      • when you are done, call USB::setEndpointReady(); to allow another command to be sent
    • either way, do not forget to set lastSetupPacket.handled = true; to acknowledge that you understood this command; otherwise, the driver will send an error response by default
  • The microcontroller should now enumerate as a USB device once connected, and you can communicate with it through standard USB libraries such as libusb in C or PyUSB in Python. For example with the latter :
    # Import the USB library
    import usb.core
    import usb.util
    # Get the device with its vendorId and productId
    dev = usb.core.find(idVendor=0x03EB, idProduct=0xCABD)
    if dev is None:
        raise ValueError('Device not found')
    dev.set_configuration()
    # Send (0x40) the custom request 0xAB with some data and a 2-second timeout
    dev.ctrl_transfer(0x40, 0xAB, 0, 0, "hello world", 2000)
    # Request (0xC0) up to 10 bytes of data with the custom request 0xCD
    print(dev.ctrl_transfer(0xC0, 0xCD, 0, 0, size, 2000))

API

void initDevice(uint16_t vendorId=DEFAULT_VENDOR_ID, uint16_t productId=DEFAULT_PRODUCT_ID, uint16_t deviceRevision=DEFAULT_DEVICE_REVISION)
Initialize and enable the USB module in Device mode. The Vendor ID, Product ID and Device Revision parameters of the device descriptor can be customized, otherwise they default to 03EB:CABD rev 0001. If calling this function triggers an error, make sure that the USB module wasn't already enabled previously, most notably by having called Carbide::init() with the autoBootloaderReset flag set to true.
void setStringDescriptor(StringDescriptors descriptor, const char* string, int size)
Set the string descriptor that will be reported to the host during enumeration to allow the user to identify your device. descriptor is either MANUFACTURER, PRODUCT or SERIALNUMBER and size is the length of the string, up to 30 characters.
Endpoint newEndpoint(EPType type, EPDir direction, EPBanks nBanks, EPSize size, uint8_t* bank0, uint8_t* bank1=nullptr)
Create a new endpoint of the given type (BULK, INTERRUPT or ISOCHRONOUS) and direction (IN or OUT). The banks are the shared memory buffers that are used to store sent or received data. The endpoint can be configured to use either 1 or 2 banks by setting nBanks to SINGLE or DOUBLE : a single bank is easier to manage, but using two banks allows to transfer large amounts of data more quickly by working on one bank while the other is being read or written by the hardware. Each bank size can be 8, 16, 32 or 64-byte long (respectively SIZE8, SIZE16, SIZE32, SIZE64). The endpoint number is returned and should be saved for future operation on the endpoint.
void setConnectedHandler(void (*handler)())

Register a handler to be called when the device is connected to a host.

This handler will be called by interrupt, do not use it to perform lengthy tasks.

void setDisconnectedHandler(void (*handler)())

Register a handler to be called when the device is disconnected from the host.

This handler will be called by interrupt, do not use it to perform lengthy tasks.

void setStartOfFrameHandler(void (*handler)())

Register a handler to be called when the host sends a Start Of Frame (SOF) packet. These packets are sent every millisecond to mark the start of a new frame (a frame is a low-level protocol concept) and can be used as a fairly precise time reference if needed.

This handler will be called by interrupt, do not use it to perform lengthy tasks.

void setControlHandler(int (*handler)(SetupPacket &lastSetupPacket, uint8_t* data, int size))
Register a handler to be called when a vendor-specific request is received on the Control endpoint (endpoint 0). When your handler is called, the content of the request is fully described by the SetupPacket structure, but its most important fields are :
  • bRequest is a 1-byte (uint8_t) user-defined request code
  • handled is a boolean that your handler should set to true if the request was recognised; otherwise, the library will automatically send a STALL in response to the host to inform it that this request is not supported
  • direction is either EPDir::IN or EPDir::OUT and is used when there is a need to transfer data with the request
The parameters data and size are used when there is a data transfer involved :
  • if lastSetupPacket.direction == USB::EPDir::IN, you can write up to size bytes to send to the host into data and you need to return the actual number of bytes written
  • if lastSetupPacket.direction == USB::EPDir::OUT, you can read up to size bytes sent by the host from data

This handler will be called by interrupt, do not use it to perform lengthy tasks.

void setEndpointHandler(Endpoint endpointNumber, EPHandlerType handlerType, int (*handler)(int))

Register a handler to be called when a request is received for the given endpointNumber. handlerType defines the type of request that this handler can manage, either IN, OUT or SETUP (used for the Control endpoint).

This handler will be called by interrupt, do not use it to perform lengthy tasks.

void setEndpointBusy(Endpoint endpointNumber=0)

Mark the given endpoint as busy. Any new request to this endpoint will be delayed until setEndpointReady() is called on the same endpoint. If the endpoint number is ommited, this will apply to the Control endpoint by default.

Use this in order to temporarily block an endpoint and prevent a new request to arrive when you are still processing the previous one. This can easily happen because the computer is much quicker than the microcontroller, and would cause a race condition difficult to debug.

void setEndpointReady(Endpoint endpointNumber=0)

Mark the given endpoint as ready to process a request again, after it was temporarily blocked with setEndpointBusy(). If the endpoint number is ommited, this will apply to the Control endpoint by default.

void enableINInterrupt(Endpoint endpointNumber)
Enable the IN interrupt on the specified endpoint.
void disableINInterrupt(Endpoint endpointNumber)
Disable the IN interrupt on the specified endpoint.
void abortINTransfer(Endpoint endpointNumber)
Clear the bank of the specified endpoint. This is useful if the bank data was prepared by an IN handler but was not requested by the host yet, and this data is now obsolete.
bool isINInterruptEnabled(Endpoint endpointNumber)
Return true if the IN interrupt is enabled on this endpoint.
void enableOUTInterrupt(Endpoint endpointNumber)
Enable the OUT interrupt on the specified endpoint.
void disableOUTInterrupt(Endpoint endpointNumber)
Disable the OUT interrupt on the specified endpoint.
bool isOUTInterruptEnabled(Endpoint endpointNumber)
Return true if the OUT interrupt is enabled on this endpoint.
void remoteWakeup()
Try to wake up a sleeping connected host.
void interruptHandler()
Internal handler for low-level protocol events.
int ep0SETUPHandler(int unused)
Internal SETUP handler for endpoint 0.
int ep0INHandler(int unused)
Internal IN handler for endpoint 0.
int ep0OUTHandler(int size)
Internal OUT handler for endpoint 0.

Hacking

USB is a powerful but complex protocol and only the most basic features are implemented, both at the driver level and the USB specification level.

One of the features you might want to have that is not currently implemented in the library is defining multiple interfaces for your device. For this, you would need to :

  • add new appropriate descriptors after _interfaceDescriptor with the required class and subclass
  • make sure _bankEP0 is large enough to store your new descriptor
  • update the calculation of _configurationDescriptor.wTotalLength in newEndpoint() to add the length of your descriptor in the default configuration
  • update ep0INHandler() to copy your descriptor into the bank at the end of the default interface descriptor when the hosts requests the configuration descriptor (it's the if branch where requestType equals STANDARD, bRequest equals USBREQ_GET_DESCRIPTOR, and descriptorType equals USBDESC_CONFIGURATION)

Code

Header

#ifndef _USB_H_
#define _USB_H_

#include <stdint.h>
#include "gpio.h"

// This module allows the chip to communicate on an USB
// bus, either as Device or Host. Only device mode is currently
// supported by the driver.
// References :
// http://www.beyondlogic.org/usbnutshell/usb1.shtml
// "USB Complete Fourth Edition" by Jan Axelson
namespace USB {

    // Peripheral memory space base address
    const uint32_t USB_BASE = 0x400A5000;

    // Registers addresses
    const uint32_t OFFSET_UDCON =      0x0000; // Device General Control Register
    const uint32_t OFFSET_UDINT =      0x0004; // Device Global Interrupt Register
    const uint32_t OFFSET_UDINTCLR =   0x0008; // Device Global Interrupt Clear Register
    const uint32_t OFFSET_UDINTSET =   0x000C; // Device Global Interrupt Set Register
    const uint32_t OFFSET_UDINTE =     0x0010; // Device Global Interrupt Enable Register
    const uint32_t OFFSET_UDINTECLR =  0x0014; // Device Global Interrupt Enable Clear Register
    const uint32_t OFFSET_UDINTESET =  0x0018; // Device Global Interrupt Enable Set Register
    const uint32_t OFFSET_UERST =      0x001C; // Endpoint Enable/Reset Register
    const uint32_t OFFSET_UDFNUM =     0x0020; // Device Frame Number Register
    const uint32_t OFFSET_UECFG0 =     0x0100; // Endpoint n Configuration Register
    const uint32_t OFFSET_UESTA0 =     0x0130; // Endpoint n Status Register
    const uint32_t OFFSET_UESTA0CLR =  0x0160; // Endpoint n Status Clear Register
    const uint32_t OFFSET_UESTA0SET =  0x0190; // Endpoint n Status Set Register
    const uint32_t OFFSET_UECON0 =     0x01C0; // Endpoint n Control Register
    const uint32_t OFFSET_UECON0SET =  0x01F0; // Endpoint n Control Set Register
    const uint32_t OFFSET_UECON0CLR =  0x0220; // Endpoint n Control Clear Register
    const uint32_t OFFSET_UHCON =      0x0400; // Host General Control Register
    const uint32_t OFFSET_UHINT =      0x0404; // Host Global Interrupt Register
    const uint32_t OFFSET_UHINTCLR =   0x0408; // Host Global Interrupt Clear Register
    const uint32_t OFFSET_UHINTSET =   0x040C; // Host Global Interrupt Set Register
    const uint32_t OFFSET_UHINTE =     0x0410; // Host Global Interrupt Enable Register
    const uint32_t OFFSET_UHINTECLR =  0x0414; // Host Global Interrupt Enable Clear Register
    const uint32_t OFFSET_UHINTESET =  0x0418; // Host Global Interrupt Enable Set Register
    const uint32_t OFFSET_UPRST =      0x041C; // Pipe Enable/Reset Register
    const uint32_t OFFSET_UHFNUM =     0x0420; // Host Frame Number Register
    const uint32_t OFFSET_UHSOFC =     0x0424; // Host Start Of Frame Control Register
    const uint32_t OFFSET_UPCFG0 =     0x0500; // Pipe n Configuration Register
    const uint32_t OFFSET_UPSTA0 =     0x0530; // Pipe n Status Register
    const uint32_t OFFSET_UPSTA0CLR =  0x0560; // Pipe n Status Clear Register
    const uint32_t OFFSET_UPSTA0SET =  0x0590; // Pipe n Status Set Register
    const uint32_t OFFSET_UPCON0 =     0x05C0; // Pipe n Control Register
    const uint32_t OFFSET_UPCON0SET =  0x05F0; // Pipe n Control Set Register
    const uint32_t OFFSET_UPCON0CLR =  0x0620; // Pipe n Control Clear Register
    const uint32_t OFFSET_UPINRQ0 =    0x0650; // Pipe n IN Request Register
    const uint32_t OFFSET_USBCON =     0x0800; // General Control Register
    const uint32_t OFFSET_USBSTA =     0x0804; // General Status Register
    const uint32_t OFFSET_USBSTACLR =  0x0808; // General Status Clear Register
    const uint32_t OFFSET_USBSTASET =  0x080C; // General Status Set Register
    const uint32_t OFFSET_UVERS =      0x0818; // IP Version Register
    const uint32_t OFFSET_UFEATURES =  0x081C; // IP Features Register
    const uint32_t OFFSET_UADDRSIZE =  0x0820; // IP PB Address Size Register
    const uint32_t OFFSET_UNAME1 =     0x0824; // IP Name Register 1
    const uint32_t OFFSET_UNAME2 =     0x0828; // IP Name Register 2
    const uint32_t OFFSET_USBFSM =     0x082C; // USB Finite State Machine Status Register
    const uint32_t OFFSET_UDESC =      0x0830; // USB Descriptor address

    // Subregisters
    const uint32_t USBCON_FRZCLK = 14;
    const uint32_t USBCON_USBE = 15;
    const uint32_t USBCON_UIMOD = 24; // Datasheet says 25, that's wrong
    const uint32_t USBSTA_VBUSRQ = 9;
    const uint32_t USBSTA_SPEED = 12;
    const uint32_t USBSTA_CLKUSABLE = 14;
    const uint32_t USBSTA_SUSPEND = 16;
    const uint32_t UDCON_UADD = 0;
    const uint32_t UDCON_ADDEN = 7;
    const uint32_t UDCON_DETACH = 8;
    const uint32_t UDCON_RMWKUP = 9;
    const uint32_t UDCON_LS = 12;
    const uint32_t UDCON_GNAK = 17;
    const uint32_t UDINT_SUSP = 0;
    const uint32_t UDINT_SOF = 2;
    const uint32_t UDINT_EORST = 3;
    const uint32_t UDINT_WAKEUP = 4;
    const uint32_t UDINT_EORSM = 5;
    const uint32_t UDINT_UPRSM = 6;
    const uint32_t UDINT_EP0INT = 12;
    const uint32_t UECFG_EPBK = 2;
    const uint32_t UECFG_EPSIZE = 4;
    const uint32_t UECFG_EPDIR = 8;
    const uint32_t UECFG_EPTYPE = 11;
    const uint32_t UECFG_REPNB = 17;
    const uint32_t UECON_TXINE = 0;
    const uint32_t UECON_RXOUTE = 1;
    const uint32_t UECON_RXSTPE = 2;
    const uint32_t UECON_ERRORFE = 2;
    const uint32_t UECON_NAKOUTE = 3;
    const uint32_t UECON_NAKINE = 4;
    const uint32_t UECON_STALLEDE = 6;
    const uint32_t UECON_CRCERRE = 6;
    const uint32_t UECON_NREPLY = 8;
    const uint32_t UECON_RAMACERE = 11;
    const uint32_t UECON_NBUSYBKE = 12;
    const uint32_t UECON_KILLBK = 13;
    const uint32_t UECON_FIFOCON = 14;
    const uint32_t UECON_RSTDT = 18;
    const uint32_t UECON_STALLRQ = 19;
    const uint32_t UECON_BUSY0E = 24;
    const uint32_t UECON_BUSY1E = 25;
    const uint32_t UESTA_TXINI = 0;
    const uint32_t UESTA_RXOUTI = 1;
    const uint32_t UESTA_RXSTPI = 2;
    const uint32_t UESTA_ERRORFI = 2;
    const uint32_t UESTA_NAKOUTI = 3;
    const uint32_t UESTA_NAKINI = 4;
    const uint32_t UESTA_STALLEDI = 6;
    const uint32_t UESTA_CRCERRI = 6;
    const uint32_t UESTA_DTSEQ = 8;
    const uint32_t UESTA_RAMACERI = 11;
    const uint32_t UESTA_NBUSYBK = 12;
    const uint32_t UESTA_CURRBK = 14;
    const uint32_t UESTA_CTRLDIR = 17;

    // Endpoints RAM registers
    const int N_EP_MAX = 8;
    const int EP_DESCRIPTOR_SIZE = 8;
    const int EP_DESCRIPTOR_BANK_SIZE = 4;
    const int EP_ADDR = 0;
    const int EP_PCKSIZE = 1;
    const int PCKSIZE_BYTE_COUNT = 0;
    const int PCKSIZE_BYTE_COUNT_MASK = 0x00007FFF;
    const int PCKSIZE_MULTI_PACKET_SIZE = 16;
    const int PCKSIZE_MULTI_PACKET_SIZE_MASK = 0x7FFF0000;
    const int PCKSIZE_AUTO_ZLP = 31;
    const int EP_CTR_STA = 2;
    const int CTR_STA_STALLRQ_NEXT = 0;
    const int CTR_STA_CRCERR = 16;
    const int CTR_STA_OVERF = 17;
    const int CTR_STA_UNDERF = 18;

    // Error codes
    const Error::Code ERR_CLOCK_NOT_USABLE = 0x0001;


    // USB Device states
    enum class State {
        SUSPEND,
        POWERED,
        DEFAULT,
        ADDRESS,
        CONFIGURED
    };


    // USB requests codes
    const int USBREQ_GET_STATUS = 0;
    const int USBREQ_CLEAR_FEATURE = 1;
    const int USBREQ_SET_FEATURE = 3;
    const int USBREQ_SET_ADDRESS = 5;
    const int USBREQ_GET_DESCRIPTOR = 6;
    const int USBREQ_SET_DESCRIPTOR = 7;
    const int USBREQ_GET_CONFIGURATION = 8;
    const int USBREQ_SET_CONFIGURATION = 9;
    const int USBREQ_GET_INTERFACE = 10;
    const int USBREQ_SET_INTERFACE = 11;
    const int USBREQ_SYNC_FRAME = 12;


    // USB Device features
    const int USBFEAT_DEVICE_REMOTE_WAKEUP = 1;


    // USB descriptors
    const int USBDESC_DEVICE = 1;
    const int USBDESC_CONFIGURATION = 2;
    const int USBDESC_STRING = 3;
    const int USBDESC_INTERFACE = 4;
    const int USBDESC_ENDPOINT = 5;
    const int USBDESC_DEVICE_QUALIFIER = 6;
    const int USBDESC_OTHER_SPEED_CONFIGURATION = 7;
    const int USBDESC_INTERFACE_POWER = 8;
    struct DeviceDescriptor {
        uint8_t bLength;
        uint8_t bDescriptorType;
        uint16_t bcdUSB;
        uint8_t bDeviceClass;
        uint8_t bDeviceSubClass;
        uint8_t bDeviceProtocol;
        uint8_t bMaxPacketSize0;
        uint16_t idVendor;
        uint16_t idProduct;
        uint16_t bcdDevice;
        uint8_t iManufacturer;
        uint8_t iProduct;
        uint8_t iSerialNumber;
        uint8_t bNumConfigurations;
    };
    struct ConfigurationDescriptor {
        uint8_t bLength;
        uint8_t bDescriptorType;
        uint16_t wTotalLength;
        uint8_t bNumInterfaces;
        uint8_t bConfigurationValue;
        uint8_t iConfiguration;
        uint8_t bmAttributes;
        uint8_t bMaxPower;
    };
    struct InterfaceDescriptor {
        uint8_t bLength;
        uint8_t bDescriptorType;
        uint8_t bInterfaceNumber;
        uint8_t bAlternateSetting;
        uint8_t bNumEndpoints;
        uint8_t bInterfaceClass;
        uint8_t bInterfaceSubclass;
        uint8_t bInterfaceProtocol;
        uint8_t iInterface;
    };
    struct EndpointDescriptor {
        uint8_t bLength;
        uint8_t bDescriptorType;
        uint8_t bEndpointAddress;
        uint8_t bmAttributes;
        uint16_t wMaxPacketSize;
        uint8_t bInterval;
    };
    struct String0Descriptor {
        uint8_t bLength;
        uint8_t bDescriptorType;
        uint16_t wLANGID0;
    };
    struct StringDescriptor {
        uint8_t bLength;
        uint8_t bDescriptorType;
        const char16_t* bString;
    };
    extern DeviceDescriptor _deviceDescriptor;
    extern ConfigurationDescriptor _configurationDescriptor;
    extern InterfaceDescriptor _interfaceDescriptor;

    // String descriptors
    enum class StringDescriptors {
        MANUFACTURER,
        PRODUCT,
        SERIALNUMBER,

        NUMBER // Number of string descriptors
    };
    const int MAX_STRING_DESCRIPTOR_SIZE = 30;
    const char16_t DEFAULT_IMANUFACTURER[] = u"libtungsten";
    const char16_t DEFAULT_IPRODUCT[] = u"Carbide";
    const char16_t DEFAULT_SERIALNUMBER[] = u"beta";
    extern String0Descriptor _string0Descriptor;
    extern StringDescriptor _stringDescriptors[];

    // Default descriptor ids
    const uint16_t DEFAULT_VENDOR_ID = 0x03eb; // Atmel Corp vendor ID
    const uint16_t DEFAULT_PRODUCT_ID = 0xcabd;
    const uint16_t DEFAULT_DEVICE_REVISION = 0x0001;


    // Endpoints
    enum class EPBanks {
        SINGLE = 0,
        DOUBLE = 1
    };
    enum class EPSize {
        SIZE8 = 0b000,
        SIZE16 = 0b001,
        SIZE32 = 0b010,
        SIZE64 = 0b011,
        SIZE128 = 0b100, // /!\ EP sizes above 64 are only possible for
        SIZE256 = 0b101, // isochronous transfers in low- and full-speed,
        SIZE512 = 0b110, // according to the USB specification
        SIZE1024 = 0b111
    };
    const int EP_SIZES[] = {
        8,
        16,
        32,
        64,
        128,
        256,
        512,
        1024
    };
    enum class EPDir {
        OUT = 0,
        IN = 1
    };
    enum class EPType {
        CONTROL = 0b00,
        ISOCHRONOUS = 0b01,
        BULK = 0b10,
        INTERRUPT = 0b11
    };
    enum class EPHandlerType {
        SETUP,
        IN,
        OUT,

        NUMBER
    };
    struct EndpointConfig {
        bool enabled;
        EPType type;
        EPDir direction;
        EPBanks nBanks;
        EPSize size;
        uint8_t* bank0;
        uint8_t* bank1;
        int (*handlers[static_cast<int>(EPHandlerType::NUMBER)])(int); // Array of function pointers of EPHandlerType
        EndpointDescriptor descriptor;
    };
    using Endpoint = int; // Helper type to manage endpoints, created by newEndpoint()
    const Endpoint EP_ERROR = -1;
    extern const int BANK_EP0_SIZE;


    // Packets
    enum class SetupRequestType {
        STANDARD =  0b00,
        CLASS =     0b01,
        VENDOR =    0b10
    };
    enum class SetupRecipient {
        DEVICE =    0b00000,
        INTERFACE = 0b00001,
        ENDPOINT =  0b00010,
        OTHER =     0b00011
    };
    struct SetupPacket {
        uint8_t bmRequestType;
        uint8_t bRequest;
        uint16_t wValue;
        uint16_t wIndex;
        uint16_t wLength;
        EPDir direction;
        SetupRequestType requestType;
        SetupRecipient recipent;
        bool handled;
    };


    // Package-dependant, defined in pins_sam4l_XX.cpp
    extern const struct GPIO::Pin PIN_DM;
    extern const struct GPIO::Pin PIN_DP;


    // Module API
    void initDevice(uint16_t vendorId=DEFAULT_VENDOR_ID, uint16_t productId=DEFAULT_PRODUCT_ID, uint16_t deviceRevision=DEFAULT_DEVICE_REVISION);
    void setStringDescriptor(StringDescriptors descriptor, const char* string, int size);
    Endpoint newEndpoint(EPType type, EPDir direction, EPBanks nBanks, EPSize size, uint8_t* bank0, uint8_t* bank1=nullptr);
    void setConnectedHandler(void (*handler)());
    void setDisconnectedHandler(void (*handler)());
    void setStartOfFrameHandler(void (*handler)());
    void setControlHandler(int (*handler)(SetupPacket &lastSetupPacket, uint8_t* data, int size));
    void setEndpointHandler(Endpoint endpointNumber, EPHandlerType handlerType, int (*handler)(int));
    void setEndpointBusy(Endpoint endpointNumber=0);
    void setEndpointReady(Endpoint endpointNumber=0);
    void enableINInterrupt(Endpoint endpointNumber);
    void abortINTransfer(Endpoint endpointNumber);
    void disableINInterrupt(Endpoint endpointNumber);
    bool isINInterruptEnabled(Endpoint endpointNumber);
    void enableOUTInterrupt(Endpoint endpointNumber);
    void disableOUTInterrupt(Endpoint endpointNumber);
    bool isOUTInterruptEnabled(Endpoint endpointNumber);
    void remoteWakeup();
    void interruptHandler();
    int ep0SETUPHandler(int unused);
    int ep0INHandler(int unused);
    int ep0OUTHandler(int size);
}

#endif

Module

#include "usb.h"
#include "core.h"
#include "pm.h"
#include "scif.h"
#include "utils.h"
#include "gpio.h"
#include <string.h>

namespace USB {

    // Current state of the driver during enumeration
    volatile State _state = State::SUSPEND;
    volatile State _stateBeforeSuspend = State::POWERED;

    // Endpoints
    // The three least significant bits of UDESC.UDESCA are always zero according to the datasheet,
    // so _epRAMDescriptors must be aligned to an 8-bytes boundary
    struct EndpointConfig _endpoints[N_EP_MAX];
    uint32_t _epRAMDescriptors[N_EP_MAX * EP_DESCRIPTOR_SIZE] __attribute__ ((aligned (0b1000)));
    volatile int _nEndpoints = 0;

    // Bank for EP0
    // This must be large enough to store the whole configuration descriptor :
    // 9 (config desc size) + 9 (iface desc size) + 8 (N_EP_MAX) * 7 (ep desc size) = 74 bytes max
    // and must be a multiple of the endpoint size (64)
    const int BANK_EP0_SIZE = 512;
    uint8_t _bankEP0[BANK_EP0_SIZE];

    // Struct where the parameters of the last Setup request are stored
    SetupPacket _lastSetupPacket;
    volatile bool _setupPacketAvailable = false;


    volatile bool _doEnableAddress = false;

    volatile bool _deviceRemoteWakeup = false;

    // Default device descriptor content
    DeviceDescriptor _deviceDescriptor = {
        .bLength = 18,
        .bDescriptorType = 0x01,
        .bcdUSB = 0x0200, // USB version 2.0.0
        .bDeviceClass = 0x00,
        .bDeviceSubClass = 0x00,
        .bDeviceProtocol = 0x00,
        .bMaxPacketSize0 = 64,
        .idVendor = DEFAULT_VENDOR_ID,
        .idProduct = DEFAULT_PRODUCT_ID,
        .bcdDevice = DEFAULT_DEVICE_REVISION,
        .iManufacturer = static_cast<uint8_t>(StringDescriptors::MANUFACTURER) + 1,
        .iProduct = static_cast<uint8_t>(StringDescriptors::PRODUCT) + 1,
        .iSerialNumber = static_cast<uint8_t>(StringDescriptors::SERIALNUMBER) + 1,
        .bNumConfigurations = 1
    };

    // Default configuration descriptor content
    // Full configuration descriptor content, with subordinate interface and endpoint descriptors
    ConfigurationDescriptor _configurationDescriptor = {
        .bLength = 9,
        .bDescriptorType = 0x02,
        .wTotalLength = 18,
        .bNumInterfaces = 1,
        .bConfigurationValue = 1,
        .iConfiguration = 0,
        .bmAttributes = 0xA0, // TODO : bus power settings
        .bMaxPower = 150 // In units of 2mA, therefore 300mA
    };
    InterfaceDescriptor _interfaceDescriptor = {
        .bLength = 9,
        .bDescriptorType = 0x04,
        .bInterfaceNumber = 0,
        .bAlternateSetting = 0,
        .bNumEndpoints = 0,
        .bInterfaceClass = 0xFF, // TODO cf p109
        .bInterfaceSubclass = 0xFF,
        .bInterfaceProtocol = 0xFF,
        .iInterface = 0,
    };

    // String0 descriptor, list of supported languages
    // See http://www.usb.org/developers/docs/USB_LANGIDs.pdf
    String0Descriptor _string0Descriptor = {
        .bLength = 4,
        .bDescriptorType = 0x03,
        .wLANGID0 = 0x0409, // English (United States)
    };

    // List of available String descriptors. This must match the enum StringDescriptors.
    // bLength should be 2 bytes longer to account for bLength and bDescriptorType,
    // but also 2 bytes shorter to remove the null-terminating character at the end
    // of the string : sizeof() + 2 - 2 = sizeof()
    StringDescriptor _stringDescriptors[] = {
        { // Device.iManufacturer
            .bLength = sizeof(DEFAULT_IMANUFACTURER),
            .bDescriptorType = 0x03,
            .bString = DEFAULT_IMANUFACTURER
        },
        { // Device.iProduct
            .bLength = sizeof(DEFAULT_IPRODUCT),
            .bDescriptorType = 0x03,
            .bString = DEFAULT_IPRODUCT
        },
        { // Device.iSerialNumber
            .bLength = sizeof(DEFAULT_SERIALNUMBER),
            .bDescriptorType = 0x03,
            .bString = DEFAULT_SERIALNUMBER
        }
    };
    char16_t _customStringDescriptors[static_cast<int>(StringDescriptors::NUMBER)][MAX_STRING_DESCRIPTOR_SIZE];

    extern uint8_t INTERRUPT_PRIORITY;

    // User handlers
    void (*_connectedHandler)() = nullptr;
    void (*_disconnectedHandler)() = nullptr;
    void (*_startOfFrameHandler)() = nullptr;
    int (*_controlHandler)(SetupPacket &_lastSetupPacket, uint8_t* data, int size) = nullptr;



    // Initialize the USB controller in Device mode
    void initDevice(uint16_t vendorId, uint16_t productId, uint16_t deviceRevision) {
        // Init state
        _state = State::SUSPEND;

        // Enable the clocks required by the USB
        PM::enablePeripheralClock(PM::CLK_USB_HSB);
        PM::enablePeripheralClock(PM::CLK_USB);
        SCIF::enableRCFAST(SCIF::RCFASTFrequency::RCFAST_12MHZ);
        SCIF::enablePLL(1, 0, SCIF::GCLKSource::RCFAST, 12000000UL);
        SCIF::enableGenericClock(SCIF::GCLKChannel::GCLK7_USB, SCIF::GCLKSource::PLL);

        // Check the clock
        if (!((*(volatile uint32_t*)(USB_BASE + OFFSET_USBSTA)) & (1 << USBSTA_CLKUSABLE))) {
            Error::happened(Error::Module::USB, ERR_CLOCK_NOT_USABLE, Error::Severity::CRITICAL);
        }

        // Enable signal pins
        GPIO::enablePeripheral(PIN_DM);
        GPIO::enablePeripheral(PIN_DP);

        // USBCON (General Control Register) : set USB controller in Device mode
        (*(volatile uint32_t*)(USB_BASE + OFFSET_USBCON))
            = 0 << USBCON_FRZCLK   // FRZCLK : unfreeze input clocks
            | 1 << USBCON_USBE     // USBE : enable controller
            | 1 << USBCON_UIMOD;   // UIMOD : set in device mode

        // Initialize memory buffers
        memset(_endpoints, 0, sizeof(_endpoints));
        memset(_epRAMDescriptors, 0, sizeof(_epRAMDescriptors));

        // UDESC (USB Descriptor Address) : set descriptor vector address
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UDESC)) = (uint32_t)_epRAMDescriptors;

        // UDCON (Device General Control Register) : attach pullup to start device enumeration
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UDCON))
            = 0 << UDCON_LS        // LS : full-speed mode
            | 0 << UDCON_DETACH;   // DETACH : attach device

        // Configure the device descriptor
        _deviceDescriptor.idVendor = vendorId;
        _deviceDescriptor.idProduct = productId;
        _deviceDescriptor.bcdDevice = deviceRevision;

        // Init the first endpoint (mandatory Control Endpoint)
        EndpointConfig* ep = &_endpoints[0];
        ep->enabled = true;
        ep->type = EPType::CONTROL;
        ep->direction = EPDir::OUT;
        ep->nBanks = EPBanks::SINGLE;
        ep->size = EPSize::SIZE64;
        ep->bank0 = _bankEP0;
        setEndpointHandler(0, EPHandlerType::SETUP, ep0SETUPHandler);
        setEndpointHandler(0, EPHandlerType::IN, ep0INHandler);
        setEndpointHandler(0, EPHandlerType::OUT, ep0OUTHandler);
        _nEndpoints = 1;

        // Enable interrupts
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTESET))
            = 1 << UDINT_SUSP
            | 1 << UDINT_EORST
            | 1 << UDINT_WAKEUP;
        Core::setInterruptHandler(Core::Interrupt::USBC, interruptHandler);
        Core::enableInterrupt(Core::Interrupt::USBC, INTERRUPT_PRIORITY);
    }

    // Set the content of a custom string descriptor
    void setStringDescriptor(StringDescriptors descriptor, const char* string, int size) {
        // Check the input string size
        if (size > MAX_STRING_DESCRIPTOR_SIZE) {
            size = MAX_STRING_DESCRIPTOR_SIZE;
        }

        // Since the input string is a char* but the USB protocol expects a char16*, we need to
        // copy each character manually
        char16_t* customString = _customStringDescriptors[static_cast<int>(descriptor)];
        for (int i = 0; i < size; i++) {
            if (string[i] == 0) {
                size = i;
                break;
            }
            customString[i] = string[i];
        }

        // The descriptor is also twice as long as the input string
        _stringDescriptors[static_cast<int>(descriptor)].bLength = 2 * size + 2;
        _stringDescriptors[static_cast<int>(descriptor)].bString = customString;
    }


    // Initialize an endpoint
    Endpoint newEndpoint(EPType type, EPDir direction, EPBanks nBanks, EPSize size, uint8_t* bank0, uint8_t* bank1) {
        // Check endpoint number
        const int n = _nEndpoints;
        if (n >= N_EP_MAX) {
            return EP_ERROR;
        }
        _nEndpoints++;

        // Internal configuration
        EndpointConfig* ep = &_endpoints[n];
        ep->enabled = true;
        ep->type = type;
        ep->direction = direction;
        ep->nBanks = nBanks;
        ep->size = size;
        ep->bank0 = bank0;
        ep->bank1 = bank1;

        if (_state >= State::DEFAULT) {
            // Reset and enable the endpoint
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UERST))
                &= ~(1 << n);
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UERST))
                |= 1 << n;

            // RAM descriptor configuration
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UECFG0 + n * 4))
                = static_cast<int>(nBanks) << UECFG_EPBK    // EPBK : single/multi-bank endpoint
                | static_cast<int>(size) << UECFG_EPSIZE   // EPSIZE : N-bytes long banks
                | static_cast<int>(direction) << UECFG_EPDIR     // EPDIR : direction IN/OUT
                | static_cast<int>(type) << UECFG_EPTYPE;  // EPTYPE : endpoint type (Control, ...)

            // Descriptor configuration
            ep->descriptor.bLength = 7;
            ep->descriptor.bDescriptorType = 0x05;
            ep->descriptor.bEndpointAddress = n | ((direction == EPDir::IN ? 1 : 0) << 7);
            ep->descriptor.bmAttributes = static_cast<int>(type);
            ep->descriptor.wMaxPacketSize = EP_SIZES[static_cast<int>(size)];
            ep->descriptor.bInterval = 10;

            // Set up descriptor for bank0
            //memset(bank0, 0, EP_SIZES[static_cast<int>(size)]);
            const int offset0 = n * EP_DESCRIPTOR_SIZE;
            _epRAMDescriptors[offset0 + EP_ADDR] = (uint32_t)bank0;
            _epRAMDescriptors[offset0 + EP_PCKSIZE] = 0;
            _epRAMDescriptors[offset0 + EP_CTR_STA] = 0;

            // Set up descriptor for bank1
            if (nBanks == EPBanks::DOUBLE && bank1 != nullptr) {
                //memset(bank1, 0, EP_SIZES[static_cast<int>(size)]);
                const int offset1 = n * EP_DESCRIPTOR_SIZE + EP_DESCRIPTOR_BANK_SIZE;
                _epRAMDescriptors[offset1 + EP_ADDR] = (uint32_t)bank1;
                _epRAMDescriptors[offset1 + EP_PCKSIZE] = 0;
                _epRAMDescriptors[offset1 + EP_CTR_STA] = 0;
            }

            // Enable interrupts
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTESET))
                = 1 << (UDINT_EP0INT + n);
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0SET + n * 4))
                = (type == EPType::CONTROL ? 1 << UECON_RXSTPE : 0)     // SETUP packet
                | 1 << UECON_RXOUTE;   // OUT packet (always enabled )

            // Update the configuration and interface descriptors with the new endpoint
            // -1 because endpoint 0 is not counted
            _configurationDescriptor.wTotalLength
                = _configurationDescriptor.bLength
                + _interfaceDescriptor.bLength
                + (_nEndpoints > 1 ? (_nEndpoints - 1) * ep->descriptor.bLength : 0);
            _interfaceDescriptor.bNumEndpoints = _nEndpoints - 1;
        }
        return n;
    }

    // Main interrupt handler called when an USB-related event happens
    void interruptHandler() {
        uint32_t udint = (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINT));
        uint32_t udinte = (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTE));

        // Suspend
        if (udinte & udint & (1 << UDINT_SUSP)) {
            // Save the current state
            _stateBeforeSuspend = _state;

            // Update the driver state
            _state = State::SUSPEND;

            // Disable this interrupt
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTECLR))
                = 1 << UDINT_SUSP;

            // Clear all interrupts
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTCLR))
                = 0xFFFFFFFF;

            // Enable the WAKEUP interrupt
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTESET))
                = 1 << UDINT_WAKEUP;

            // Freeze clock
            (*(volatile uint32_t*)(USB_BASE + OFFSET_USBCON))
                |= 1 << USBCON_FRZCLK;   // FRZCLK : freeze input clocks

            // Call user handler
            if (_disconnectedHandler != nullptr) {
                _disconnectedHandler();
            }

            // Since clocks are frozen, don't do anything more
            return;
        }

        // Wake up
        if (udinte & udint & (1 << UDINT_WAKEUP)) {
            // Restore the driver state
            _state = _stateBeforeSuspend;

            // Unfreeze clock
            (*(volatile uint32_t*)(USB_BASE + OFFSET_USBCON))
                &= ~(uint32_t)(1 << USBCON_FRZCLK);   // FRZCLK : unfreeze input clocks

            // Disable this interrupt
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTECLR))
                = 1 << UDINT_WAKEUP;

            // Clear interrupt
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTCLR))
                = 1 << UDINT_WAKEUP;

            // Enable the SUSP interrupt
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTCLR))
                = 1 << UDINT_SUSP;
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTESET))
                = 1 << UDINT_SUSP;

            // Call user handler
            if (_connectedHandler != nullptr) {
                _connectedHandler();
            }
        }

        // End of reset
        if (udinte & udint & (1 << UDINT_EORST)) {
            // Clear interrupt
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTCLR))
                = 1 << UDINT_EORST;

            // Reset state
            _state = State::DEFAULT;
            _stateBeforeSuspend = State::POWERED;
            _setupPacketAvailable = false;
            _doEnableAddress = false;

            // Recreate endpoints
            _nEndpoints = 0;
            for (int i = 0; i < N_EP_MAX; i++) {
                if (_endpoints[i].enabled) {
                    newEndpoint(_endpoints[i].type, 
                                _endpoints[i].direction, 
                                _endpoints[i].nBanks, 
                                _endpoints[i].size,
                                _endpoints[i].bank0,
                                _endpoints[i].bank1);
                } else {
                    break;
                }
            }
        } 

        // Start of frame
        if (udinte & udint & (1 << UDINT_SOF)) {
            // Clear interrupt
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDINTCLR))
                = 1 << UDINT_SOF;

            // Call user handler
            if (_startOfFrameHandler != nullptr) {
                _startOfFrameHandler();
            }
        }

        // Endpoints
        for (int i = 0; i < _nEndpoints; i++) {
            if (_endpoints[i].enabled && (udinte & udint & (1 << (UDINT_EP0INT + i)))) {
                // Check which packet type has been received in UESTA
                volatile uint32_t* uesta = (volatile uint32_t*)(USB_BASE + OFFSET_UESTA0 + i * 4);
                volatile uint32_t* uecon = (volatile uint32_t*)(USB_BASE + OFFSET_UECON0 + i * 4);
                EndpointConfig* ep = &_endpoints[i];

                // SETUP packet
                if (*uecon & *uesta & (1 << UESTA_RXSTPI)) {
                    // Call the handler to read the packet content
                    if (ep->handlers[static_cast<int>(EPHandlerType::SETUP)] != nullptr) {
                        ep->handlers[static_cast<int>(EPHandlerType::SETUP)](0);
                    }

                    // Clear interrupt to acknoledge the packet and free the bank
                    (*(volatile uint32_t*)(USB_BASE + OFFSET_UESTA0CLR + i * 4))
                        = 1 << UESTA_RXSTPI;

                }

                // IN packet
                // Note : this is not called when an IN packet is received, but whenever the bank is ready
                // to receive the content for the next IN packet. This is because the data must be sent 
                // immediately after the IN packet, with no time for a handler call, and must therefore be
                // prepared beforehand.
                if (*uecon & *uesta & (1 << UESTA_TXINI)) {
                    // Call the handler to write the packet content
                    int bytesToSend = 0;
                    if (ep->handlers[static_cast<int>(EPHandlerType::IN)] != nullptr) {
                        bytesToSend = ep->handlers[static_cast<int>(EPHandlerType::IN)](0);
                    }
                    _epRAMDescriptors[i * EP_DESCRIPTOR_SIZE + EP_PCKSIZE] = (1 << PCKSIZE_AUTO_ZLP) | (bytesToSend & PCKSIZE_BYTE_COUNT_MASK);
                    // Multi-packet mode is automatically enabled if BYTE_COUNT (ie bytesToSend) is larger than 
                    // the endpoint size (UECFG.EPSIZE)

                    // Clear interrupt (this will send the packet for a Control endpoint)
                    (*(volatile uint32_t*)(USB_BASE + OFFSET_UESTA0CLR + i * 4))
                        = 1 << UESTA_TXINI;

                    // If this is an IN endpoint, clear FIFOCON to free the bank and allow the
                    // hardware to send the data at the next IN packet
                    if (ep->type != EPType::CONTROL && ep->direction == EPDir::IN) {
                        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0CLR + i * 4))
                            = 1 << UECON_FIFOCON;
                    }
                }
                
                // OUT packet
                if (*uecon & *uesta & (1 << UESTA_RXOUTI)) {
                    // Number of bytes received
                    int receivedPacketSize = _epRAMDescriptors[i * EP_DESCRIPTOR_SIZE + EP_PCKSIZE] & PCKSIZE_BYTE_COUNT_MASK;

                    // Call the handler to read the packet content
                    if (ep->handlers[static_cast<int>(EPHandlerType::OUT)] != nullptr) {
                        ep->handlers[static_cast<int>(EPHandlerType::OUT)](receivedPacketSize);
                    }

                    // Clear interrupt
                    (*(volatile uint32_t*)(USB_BASE + OFFSET_UESTA0CLR + i * 4))
                        = 1 << UESTA_RXOUTI;

                    // If this is an OUT endpoint, clear FIFOCON to free the bank
                    if (ep->type != EPType::CONTROL && ep->direction == EPDir::OUT) {
                        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0CLR + i * 4))
                            = 1 << UECON_FIFOCON;
                    }

                    // If this is an IN or No Data transfer, an OUT packet means the transfer is finished or aborted
                    if (_lastSetupPacket.direction == EPDir::IN || _lastSetupPacket.wLength == 0) {
                        // Disable IN interrupt
                    }
                }
            }
        }
    }


    // Handlers for endpoint 0
    int ep0SETUPHandler(int unused) {
        // Read data received in bankEP0
        const int EP_N = 0; // Endpoint number : 0 in this handler
        uint8_t* bank = (uint8_t*)_epRAMDescriptors[EP_N * EP_DESCRIPTOR_SIZE + EP_ADDR];
        _lastSetupPacket = {
            .bmRequestType = bank[0],
            .bRequest = bank[1],
            .wValue = uint16_t(bank[2] | bank[3] << 8),
            .wIndex = uint16_t(bank[4] | bank[5] << 8),
            .wLength = uint16_t(bank[6] | bank[7] << 8),
            .direction = static_cast<EPDir>((bank[0] >> 7) & 0b1),
            .requestType = static_cast<SetupRequestType>((bank[0] >> 5) & 0b11),
            .recipent = static_cast<SetupRecipient>(bank[0] & 0b11111),
            .handled = false
        };

        // Kill any pending IN transfer
        abortINTransfer(0);

        // If this is an IN or No Data transfer
        if (_lastSetupPacket.direction == EPDir::IN || _lastSetupPacket.wLength == 0) {
            // Enable the IN interrupt to answer this request (or ACK it with a ZLP for a No Data transfer)
            enableINInterrupt(0);
        } else { // OUT
            // Configure the MULTI_PACKET_SIZE to allow multi-packet mode for OUT transfers
            _epRAMDescriptors[EP_N * EP_DESCRIPTOR_SIZE + EP_PCKSIZE] = (BANK_EP0_SIZE << PCKSIZE_MULTI_PACKET_SIZE) & PCKSIZE_MULTI_PACKET_SIZE_MASK;

            // Disable the IN interrupt to allow the OUT handler to be called
            disableINInterrupt(0);
        }

        // Mark that a SETUP packet has been received
        _setupPacketAvailable = true;

        return 0;
    }

    int ep0INHandler(int unused) {
        const int EP_N = 0; // Endpoint number : 0 in this handler

        // Answer the last SETUP packet received
        if (_setupPacketAvailable) {

            // Standard request
            if (_lastSetupPacket.requestType == SetupRequestType::STANDARD) {
                uint8_t* bank = (uint8_t*)_epRAMDescriptors[EP_N * EP_DESCRIPTOR_SIZE + EP_ADDR];

                // Request recipient : device
                if (_lastSetupPacket.recipent == SetupRecipient::DEVICE) {

                    // GET_STATUS standard request
                    if (_lastSetupPacket.bRequest == USBREQ_GET_STATUS) {
                        _lastSetupPacket.handled = true;
                        bank[0] = 0x00;
                        bank[1] = static_cast<int>(_deviceRemoteWakeup) << 1; // TODO : self-powered bit
                        return 2;

                    // USBREQ_CLEAR_FEATURE standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_CLEAR_FEATURE) {
                        if (_lastSetupPacket.wValue == USBFEAT_DEVICE_REMOTE_WAKEUP) {
                            _lastSetupPacket.handled = true;
                            _deviceRemoteWakeup = false;
                            return 0;
                        }

                    // USBREQ_SET_FEATURE standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_SET_FEATURE) {
                        if (_lastSetupPacket.wValue == USBFEAT_DEVICE_REMOTE_WAKEUP) {
                            _lastSetupPacket.handled = true;
                            _deviceRemoteWakeup = true;
                            return 0;
                        }

                    // GET_DESCRIPTOR standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_GET_DESCRIPTOR) {

                        // Select descriptor to send
                        const uint8_t descriptorType = _lastSetupPacket.wValue >> 8;
                        const uint8_t descriptorIndex = _lastSetupPacket.wValue & 0xFF;

                        // Default device descriptor
                        if (descriptorType == USBDESC_DEVICE && descriptorIndex == 0) {
                            _lastSetupPacket.handled = true;

                            // Copy the descriptor to the bank
                            const int length = min(_deviceDescriptor.bLength, _lastSetupPacket.wLength);
                            memcpy(bank, (uint8_t*)&_deviceDescriptor, length);
                            return length;

                        // Default configuration and interface descriptors
                        } else if (descriptorType == USBDESC_CONFIGURATION && descriptorIndex == 0) {
                            _lastSetupPacket.handled = true;

                            // Copy the configuration and interface descriptors to the bank
                            uint8_t* bankCursor = bank;
                            int length = min(_configurationDescriptor.wTotalLength, _lastSetupPacket.wLength);
                            memcpy(bankCursor, (uint8_t*)&_configurationDescriptor, _configurationDescriptor.bLength);
                            bankCursor += _configurationDescriptor.bLength;
                            memcpy(bankCursor, (uint8_t*)&_interfaceDescriptor, _interfaceDescriptor.bLength);
                            bankCursor += _interfaceDescriptor.bLength;
                            for (int i = 1; i < _nEndpoints; i++) {
                                // This loop starts at 1 because the descriptor of endpoint 0 must not be sent
                                memcpy(bankCursor, (uint8_t*)&_endpoints[i].descriptor, _endpoints[i].descriptor.bLength);
                                bankCursor += _endpoints[i].descriptor.bLength;
                            }
                            return length;

                        // String0 descriptor (list of available languages)
                        } else if (descriptorType == USBDESC_STRING && descriptorIndex == 0) {
                            _lastSetupPacket.handled = true;

                            // Copy the descriptor to the bank
                            const int length = min(_string0Descriptor.bLength, _lastSetupPacket.wLength);
                            memcpy(bank, (uint8_t*)&_string0Descriptor, length);
                            return length;

                        // String descriptor
                        } else if (descriptorType == USBDESC_STRING && descriptorIndex <= static_cast<int>(StringDescriptors::NUMBER)) {
                            _lastSetupPacket.handled = true;

                            // Copy the descriptor to the bank
                            const StringDescriptor stringDescriptor = _stringDescriptors[descriptorIndex - 1];
                            const int length = min(stringDescriptor.bLength, _lastSetupPacket.wLength);
                            memcpy(bank, (uint8_t*)&(stringDescriptor), 2); // bLength and bDescriptorType
                            memcpy(bank + 2, stringDescriptor.bString, length - 2); // Actual string content
                            return length;

                        }

                    // SET_ADDRESS standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_SET_ADDRESS) {
                        _lastSetupPacket.handled = true;

                        // After a SET_ADDRESS request, the new address is configured (UDCON.UADD) but
                        // not enabled (UDCON.ADDEN) immediately because the device must finish the
                        // current transaction with the default address.
                        // The IN interrupt is triggered for the first just after the SETUP request to
                        // prepare the answer of the next IN transfer (request ACK). During this transfer
                        // the address is written to UDCON but must not be enabled yet. When the ACK transfer
                        // is sent, the bank is free once again and the IN interrupt is triggered a second
                        // time. Since there is no Status Stage in a SET_ADDRESS request, this is a good
                        // time to enable the address.
                        if (!_doEnableAddress) {
                            // Write address (disabled)
                            uint32_t udcon = (*(volatile uint32_t*)(USB_BASE + OFFSET_UDCON));
                            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDCON))
                                = (udcon & ~(uint32_t)(0xFF << UDCON_UADD))
                                | _lastSetupPacket.wValue << UDCON_UADD;   // UADD : USB device address

                            // Enable the flag
                            _doEnableAddress = true;

                        } else {
                            // Enable address
                            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDCON))
                                |= 1 << UDCON_ADDEN;   // ADDEN : enable address

                            // There is no Status phase for a SET_ADDRESS, 
                            // so disable IN interrupt now
                            (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0CLR))
                                = 1 << UECON_TXINE;
                            
                            // Move through the next enumeration step
                            _state = State::ADDRESS;

                            // Disable the flag
                            _doEnableAddress = false;
                        }

                        // Answer with a ZLP
                        return 0;

                    // SET_CONFIGURATION standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_SET_CONFIGURATION) {

                        if (_state == State::ADDRESS || _state == State::CONFIGURED) {

                            const uint8_t configurationId = _lastSetupPacket.wValue & 0xFF;

                            if (configurationId == 0) {
                                _lastSetupPacket.handled = true;
                                
                                // The device is no longer configured, reset to Address state
                                _state = State::ADDRESS;
                            
                                // Answer with a ZLP
                                return 0;

                            // There is only one configuration available
                            } else if (configurationId == 1) {
                                _lastSetupPacket.handled = true;

                                // The device is now configured
                                _state = State::CONFIGURED;
                            
                                // Answer with a ZLP
                                return 0;
                            }
                        }
                    }

                // Request recipient : interface
                } else if (_lastSetupPacket.recipent == SetupRecipient::INTERFACE) {

                    // uint8_t interface = _lastSetupPacket.wIndex & 0xFF; // Currently unused

                    // GET_STATUS standard request
                    if (_lastSetupPacket.bRequest == USBREQ_GET_STATUS) {
                        _lastSetupPacket.handled = true;
                        bank[0] = 0x00; // Always zero, reserved for future use
                        bank[1] = 0x00;
                        return 2;

                    // GET_INTERFACE standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_GET_INTERFACE) {
                        // Not implemented

                    // SET_INTERFACE standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_SET_INTERFACE) {
                        // Not implemented
                    }

                // Request recipient : endpoint
                } else if (_lastSetupPacket.recipent == SetupRecipient::ENDPOINT) {

                    // GET_STATUS standard request
                    if (_lastSetupPacket.bRequest == USBREQ_GET_STATUS) {
                        _lastSetupPacket.handled = true;
                        bank[0] = 0x00; // Always zero for endpoint zero
                        bank[1] = 0x00;
                        return 2;

                    // CLEAR_FEATURE standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_CLEAR_FEATURE) {
                        // Not implemented for endpoint zero

                    // SET_FEATURE standard request
                    } else if (_lastSetupPacket.bRequest == USBREQ_SET_FEATURE) {
                        // Not implemented for endpoint zero
                    }
                }

            } else if (_lastSetupPacket.requestType == SetupRequestType::VENDOR) {
                // Call user handler if this is a IN or No Data request. For an OUT request with data,
                // the user handler will be called by ep0OUTHandler() when the data has been received
                if (_controlHandler != nullptr) {
                    // Make sure the IN handler will not be called again when the IN response is sent
                    disableINInterrupt(0);

                    if (_lastSetupPacket.direction == EPDir::IN || _lastSetupPacket.wLength == 0) {
                        int bytesToSend = _controlHandler(_lastSetupPacket, _bankEP0, min(_lastSetupPacket.wLength, BANK_EP0_SIZE));
                        return min(_lastSetupPacket.wLength, bytesToSend);
                    }
                    // For an OUT request, _lastSetupPacket.handled was set previously by ep0OUTHandler()
                }
            }

            // If the request couldn't be handled, send a STALL
            if (!_lastSetupPacket.handled) {
                (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0SET))
                    = 1 << UECON_STALLRQ;
                return 0;
            }

        } else {
            // No SETUP packet available
            disableINInterrupt(0);
        }

        return 0;
    }

    int ep0OUTHandler(int size) {
        const int EP_N = 0; // Endpoint number : 0 in this handler

        if (_lastSetupPacket.direction == EPDir::IN) {
            // This is an ACK from the host
            _setupPacketAvailable = false;
            disableINInterrupt(0);

        } else {
            // This is an OUT packet containing data
            if (_controlHandler != nullptr) {
                int size = _epRAMDescriptors[EP_N * EP_DESCRIPTOR_SIZE + EP_PCKSIZE] & PCKSIZE_BYTE_COUNT_MASK;
                _controlHandler(_lastSetupPacket, _bankEP0, min(size, BANK_EP0_SIZE));
            }

            // Enable the IN interrupt to ACK the received data
            enableINInterrupt(0);
        }
        return 0;
    }

    // Handlers management
    // Set an endpoint handler
    void setEndpointHandler(Endpoint endpointNumber, EPHandlerType handlerType, int (*handler)(int)) {
        EndpointConfig* ep = &_endpoints[endpointNumber];
        if (ep->enabled) {
            ep->handlers[static_cast<int>(handlerType)] = handler;
        }
    }

    // Mark the endpoint as busy : all requests will be NACKed until setEndpointReady() is called
    // on this endpoint
    void setEndpointBusy(Endpoint endpointNumber) {
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0SET + endpointNumber * 4))
            = 1 << UECON_BUSY0E;
    }

    // Mark the endpoint as ready to receive data again, after a call to setEndpointBusy()
    void setEndpointReady(Endpoint endpointNumber) {
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0CLR + endpointNumber * 4))
            = 1 << UECON_BUSY0E;
    }

    // Enable the IN interrupt on the specified endpoint
    void enableINInterrupt(Endpoint endpointNumber) {
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0SET + endpointNumber * 4))
            = 1 << UECON_TXINE;
    }

    // Clear the bank of the specified endpoint. This is useful if the bank data was prepared
    // by an IN handler, but no IN packet was sent by the host yet, and this data is now obsolete
    void abortINTransfer(Endpoint endpointNumber) {
        // Disable the IN interrupt
        disableINInterrupt(endpointNumber);

        _epRAMDescriptors[endpointNumber * EP_DESCRIPTOR_SIZE + EP_PCKSIZE] = 0;
        
        // Kill the bank
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0SET + endpointNumber * 4))
            = 1 << UECON_KILLBK;

        // Wait for the end of the procedure
        while ((*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0 + endpointNumber * 4)) & 1 << UECON_KILLBK);
    }

    // Disable the IN interrupt on the specified endpoint
    void disableINInterrupt(Endpoint endpointNumber) {
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0CLR + endpointNumber * 4))
            = 1 << UECON_TXINE;
    }

    // Return true if the IN interrupt is enabled on the specified endpoint
    bool isINInterruptEnabled(Endpoint endpointNumber) {
        return (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0 + endpointNumber * 4)) & (1 << UECON_TXINE);
    }

    // Enable the OUT interrupt on the specified endpoint
    void enableOUTInterrupt(Endpoint endpointNumber) {
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0SET + endpointNumber * 4))
            = 1 << UECON_RXOUTE;
    }

    // Disable the OUT interrupt on the specified endpoint
    void disableOUTInterrupt(Endpoint endpointNumber) {
        (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0CLR + endpointNumber * 4))
            = 1 << UECON_RXOUTE;
    }

    // Return true if the OUT interrupt is enabled on the specified endpoint
    bool isOUTInterruptEnabled(Endpoint endpointNumber) {
        return (*(volatile uint32_t*)(USB_BASE + OFFSET_UECON0 + endpointNumber * 4)) & (1 << UECON_RXOUTE);
    }

    // User handlers
    void setConnectedHandler(void (*handler)()) {
        _connectedHandler = handler;
    }

    void setDisconnectedHandler(void (*handler)()) {
        _disconnectedHandler = handler;
    }

    void setStartOfFrameHandler(void (*handler)()) {
        _startOfFrameHandler = handler;
    }

    void setControlHandler(int (*handler)(SetupPacket &lastSetupPacket, uint8_t* data, int size)) {
        _controlHandler = handler;
    }


    // Misc functions
    void remoteWakeup() {
        // TODO
        if (_state == State::SUSPEND) {
            (*(volatile uint32_t*)(USB_BASE + OFFSET_UDCON))
                |= 1 << UDCON_RMWKUP;
        }
    }
}