Skip to content

06. Enumeration

Nathanael Schneider edited this page Feb 9, 2023 · 4 revisions

In this step we will complete the enumeration process. Also, we will implement support for data that requires more space than the buffers can handle.

6.0 Theory

The host will now request the full device descriptor. In this part we will not yet implement a fully functional descriptor, but a placeholder, as descriptor design requires a bit of knowledge that we'll cover later.

This part mainly finishes the enumeration process and gives the functionality needed to transmit data that does not quite fit into the transmit buffer.

6.1 Device configuration

First we need some additional info about the device, including interfaces and endpoints, which we will place in usb_config.

typedef struct {
    unsigned char Length;
    unsigned char Type;
    unsigned short TotalLength;
    unsigned char Interfaces;
    unsigned char ConfigurationID;
    unsigned char strConfiguration;
    unsigned char Attributes;
    unsigned char MaxPower;
} USB_DESCRIPTOR_CONFIG;

typedef struct {
    unsigned char Length;
    unsigned char Type;
    unsigned char InterfaceID;
    unsigned char AlternateID;
    unsigned char Endpoints;
    unsigned char Class;
    unsigned char SubClass;
    unsigned char Protocol;
    unsigned char strInterface;
} USB_DESCRIPTOR_INTERFACE;

typedef struct {
    unsigned char Length;
    unsigned char Type;
    unsigned char Address;
    unsigned char Attributes;
    unsigned short MaxPacketSize;
    unsigned char Interval;
} USB_DESCRIPTOR_ENDPOINT;

We also need to fill those descriptors with data and put everything in one concise buffer, so that we can send the configuration with all interfaces in one go. The ConfigurationBuffer should be big enough to hold the configuration, all interfaces, endpoints and additional data.

static const USB_DESCRIPTOR_CONFIG ConfigDescriptor = {
    .Length = 9,
    .Type = 0x02,
    .TotalLength = 32,
    .Interfaces = 1,
    .ConfigurationID = 1,
    .strConfiguration = 0,
    .Attributes = (1 << 7),
    .MaxPower = 50};

static const USB_DESCRIPTOR_INTERFACE InterfaceDescriptors[] = {
    {.Length = 9,
     .Type = 0x04,
     .InterfaceID = 0,
     .AlternateID = 0,
     .Endpoints = 2,
     .Class = 0x0A,
     .SubClass = 0x00,
     .Protocol = 0x00,
     .strInterface = 0}};

static const USB_DESCRIPTOR_ENDPOINT EndpointDescriptors[] = {
    {.Length = 7,
     .Type = 0x05,
     .Address = (1 << 7) | 0x01,
     .Attributes = 0x03,
     .MaxPacketSize = 64,
     .Interval = 0xFF},
    {.Length = 7,
     .Type = 0x05,
     .Address = 0x01,
     .Attributes = 0x03,
     .MaxPacketSize = 64,
     .Interval = 0xFF}};

static char ConfigurationBuffer[32] = {0};

And lastly we add a function to fetch the buffer and fill it on the first fetch.

static void AddToDescriptor(char *data, short *offset) {
    short length = data[0];

    for (int i = 0; i < length; i++) {
        ConfigurationBuffer[i + *offset] = data[i];
    }

    *offset += length;
}

char *USB_GetConfigDescriptor(short *length) {
    if(ConfigurationBuffer[0] == 0) {
        short offset = 0;
        AddToDescriptor(&ConfigDescriptor, &offset);
        AddToDescriptor(&InterfaceDescriptors[0], &offset);
        AddToDescriptor(&EndpointDescriptors[0], &offset);
        AddToDescriptor(&EndpointDescriptors[1], &offset);
    }

    *length = sizeof(ConfigurationBuffer);
    return ConfigurationBuffer;
}

6.2 Transfer Buffering

Buffering the transfer is (with this configuration) not yet necessary, but it will be needed eventually. First we add a structure to contain transfer info and add that to the control state.

typedef struct {
    unsigned short Length;
    unsigned short BytesSent;
    unsigned char *Buffer;
} USB_TRANSFER_STATE;

typedef struct {
    USB_SETUP_PACKET Setup;
    USB_TRANSFER_STATE Transfer;
} USB_CONTROL_STATE;

Next, we write a function that is able to chunk the transmission of data.

#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))

static void USB_PrepareTransfer(USB_TRANSFER_STATE *transfer, short *ep, char *txBuffer, short *txBufferCount, short txBufferSize) {
    *txBufferCount = MIN(txBufferSize, transfer->Length - transfer->BytesSent);
    if (*txBufferCount > 0) {
        USB_CopyMemory(transfer->Buffer + transfer->BytesSent, txBuffer, *txBufferCount);
        transfer->BytesSent += *txBufferCount;
        USB_SetEP(ep, USB_EP_TX_VALID, USB_EP_TX_VALID);
    } else {
        USB_SetEP(ep, USB_EP_TX_NAK, USB_EP_TX_VALID);
    }
}

And lastly, we implement this feature into the control message transmission.

if (USB->EP0R & USB_EP_CTR_TX) {
    // We just sent a control message
    if(ControlState.Setup.Request == 0x05) {
        USB->DADDR = USB_DADDR_EF | ControlState.Setup.Value;
    }

    // check for running transfers
    if (ControlState.Transfer.Length > 0) {
        if (ControlState.Transfer.Length > ControlState.Transfer.BytesSent) {
            USB_PrepareTransfer(&ControlState.Transfer, &USB->EP0R, &EP0_Buf[1], &BTable[0].COUNT_TX, 64);
        }
    }

    USB_SetEP(&USB->EP0R, 0x00, USB_EP_CTR_TX);
}

6.3 Configuration Descriptor

Now we need to distinguish our GetDescriptor-Requests as for which descriptor is actually requested and send the correct one. We also stall the request for a qualifier descriptor, which would indicate that we can do HighSpeed. If we ignore it and don't stall, it might take ~5s for the host to get that we don't have that descriptor and continue enumerating our device.

case 0x06: // Get Descriptor
    switch (setup->DescriptorType) {
    case 0x01: { // Device Descriptor
        USB_DESCRIPTOR_DEVICE *descriptor = USB_GetDeviceDescriptor();
        USB_CopyMemory(descriptor, EP0_Buf[1], sizeof(USB_DESCRIPTOR_DEVICE));
        BTable[0].COUNT_TX = sizeof(USB_DESCRIPTOR_DEVICE);

        USB_SetEP(&USB->EP0R, USB_EP_TX_VALID, USB_EP_TX_VALID);
    } break;
    case 0x02: { // Configuration Descriptor
        short length;
        char *descriptor = USB_GetConfigDescriptor(&length);
        ControlState.Transfer.Buffer = descriptor;
        ControlState.Transfer.BytesSent = 0;
        ControlState.Transfer.Length = MIN(length, setup->Length);

        USB_PrepareTransfer(&ControlState.Transfer, &USB->EP0R, &EP0_Buf[1], &BTable[0].COUNT_TX, 64);
    } break;
    case 0x06: // Device Qualifier Descriptor
        USB_SetEP(&USB->EP0R, USB_EP_TX_STALL, USB_EP_TX_VALID);
        break;
    }
    break;

At the top of the function we also clear the last transfer, as a setup packet cancels every action that happened before.

static void USB_HandleSetup(USB_SETUP_PACKET *setup) {
    USB_CopyMemory(setup, &ControlState.Setup, sizeof(USB_SETUP_PACKET));
    ControlState.Transfer.Length = 0;

6.4 Verification & Debugging

If it works, Windows should now enumerate a STMicroelectronics Virtual COM Port.

If not, check the memory layout of the complete device descriptor. Check the Vendor & Product-IDs and Class of all descriptors.