Kampis Elektroecke

Konfiguration als USB-Maus

Nachdem wir im letzten Teil einen Blick auf das HID-Protokoll geworfen haben, wollen wir nun schauen, welche Anpassungen an der Mikrocontrollersoftware vorgenommen werden müssen, damit sich dieser beim Host als generische USB-Maus anmeldet.

Im ersten Schritt wird die Event-Struktur um das zusätzliche Event ConfigurationChanged erweitert. Dieses Event soll später genutzt werden, um die klassenspezifischen Requests zu bearbeiten.

typedef struct
{
    void (*ConfigurationChanged)(const uint8_t Configuration);
    void (*ControlRequest)(const uint8_t bRequest, const uint8_t bmRequestType, const uint16_t wValue);
    void (*Error)();
    void (*EndOfReset)();
} USB_DeviceCallbacks_t;

Die verwendeten Events werden nun definiert:

void USB_Event_OnError(void);
void USB_Event_EndOfReset(void);
void USB_Event_ConfigurationChanged(const uint8_t Configuration);
void USB_Event_ControlRequest(const uint8_t bRequest, const uint8_t bmRequestType, const uint16_t wValue);

const USB_DeviceCallbacks_t Events_USB =
{
    .EndOfReset = USB_Event_EndOfReset,
    .Error = USB_Event_OnError,
    .ConfigurationChanged = USB_Event_ConfigurationChanged,
    .ControlRequest = USB_Event_ControlRequest,
};

Als nächstes wird die Funktion USB_Device_ControlRequest angepasst. Diese Funktion muss dahingehend erweitert werden, als das die REQUEST_GET_DESCRIPTOR-Abfrage die Deskriptoren der HID-Klasse berücksichtigt.

Gemäß der HID-Spezifikation werden die Bitfelder Type und Recipient des Feldes bmRequestType auf 0x01 gesetzt um den klassenspezifischen Request zu definieren. Die Abfrage muss dem entsprechend erweitert werden:

case REQUEST_GET_DESCRIPTOR:
{
    if(_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE) ||
      (_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_INTERFACE)))
    {
        ...
    }

    break;
}

Zudem muss das neu erstellte ConfigurationChanged-Event aufgerufen werden:

case REQUEST_GET_DESCRIPTOR:
{
    if(_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE) ||
      (_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_INTERFACE)))
    {
        ...
    }

    break;
}

if(_USBEvents.ControlRequest != NULL)
{
    _USBEvents.ControlRequest(_ControlRequest.bRequest, _ControlRequest.bmRequestType, _ControlRequest.wValue);
}

Im nächsten Schritt werden dann die vorhandenen Deskriptoren abgepasst und die fehlenden Deskriptoren erstellt. Der Geräte-Deskriptor und die String-Deskriptoren nehmen dabei die folgende Form an:

const USB_StringDescriptor_t PROGMEM LANGID = LANG_TO_STRING_DESCRIPTOR(CONV_LANG(LANG_ENGLISH, SUBLANG_ARABIC_SAUDI_ARABIA));
const USB_StringDescriptor_t PROGMEM ManufacturerString = WCHAR_TO_STRING_DESCRIPTOR(L"Daniel Kampert");
const USB_StringDescriptor_t PROGMEM ProductString = WCHAR_TO_STRING_DESCRIPTOR(L"AT90USBKey Mouse example");
const USB_StringDescriptor_t PROGMEM SerialString = WCHAR_TO_STRING_DESCRIPTOR(L"123456");

const USB_DeviceDescriptor_t PROGMEM DeviceDescriptor =
{
    .bLength            = sizeof(USB_DeviceDescriptor_t), 
    .bDescriptorType    = DESCRIPTOR_TYPE_DEVICE,
    .bcdUSB             = USB_VERSION(1, 1, 0),
    .bDeviceClass       = USB_CLASS_USE_INTERFACE,
    .bDeviceSubClass    = USB_SUBCLASS_NONE,
    .bDeviceProtocol    = USB_PROTOCOL_NONE,
    .bMaxPacketSize0    = MOUSE_CTRL_EP_SIZE,

    .idVendor           = 0x03EB,
    .idProduct          = 0x2042,
    .bcdDevice          = USB_VERSION(1, 0, 0),
    
    .iManufacturer      = MOUSE_STRING_ID_MANUFACTURER,
    .iProduct           = MOUSE_STRING_ID_PRODUCT,
    .iSerialNumber      = MOUSE_STRING_ID_SERIAL,

    .bNumConfigurations = 1,
};

Der Konfigurations-Deskriptor wird um den HID-Deskriptor erweitert:

typedef struct
{
    uint8_t bLength;
    uint8_t bDescriptorType;
    uint16_t bcdHID;
    uint8_t bCountryCode;
    uint8_t bNumDescriptors;
    uint8_t bReportType;
    uint16_t wDescriptorLength;
} __attribute__((packed)) USB_HID_Descriptor_t;

typedef struct
{
    USB_ConfigurationDescriptor_t Configuration;
    USB_InterfaceDescriptor_t Interface;
    USB_HID_Descriptor_t MouseHID;
    USB_EndpointDescriptor_t DataINEndpoint;
} USB_Configuration_t;

Und anschließend ausgefüllt, wobei MouseReport den Report-Deskriptor der USB-Maus darstellt:

const USB_Configuration_t PROGMEM ConfigurationDescriptor[] =
{
    [0].Configuration =
    {
        .bLength             = sizeof(USB_ConfigurationDescriptor_t),
        .bDescriptorType     = DESCRIPTOR_TYPE_CONFIGURATION,
        .wTotalLength        = sizeof(USB_Configuration_t),
        .bNumInterfaces      = 0x01,
        .bConfigurationValue = 0x01,
        .iConfiguration      = 0x00,
        .bmAttributes        = USB_MASK2CONFIG(USB_CONFIG_SELF_POWERED),
        .bMaxPower           = USB_CURRENT_CONSUMPTION(100),
    },
    [0].Interface =
    {
        .bLength             = sizeof(USB_InterfaceDescriptor_t),
        .bDescriptorType     = DESCRIPTOR_TYPE_INTERFACE,
        .bInterfaceNumber    = 0x00,
        .bAlternateSetting   = 0x00,
        .bNumEndpoints       = 0x01,
        .bInterfaceClass     = USB_CLASS_HID,
        .bInterfaceSubClass  = HID_SUBCLASS_NONE,
        .bInterfaceProtocol  = HID_PROTOCOL_NONE,
        .iInterface          = 0x00,
    },
    [0].MouseHID =
    {
        .bLength             = sizeof(USB_HID_Descriptor_t),
        .bDescriptorType     = HID_DESCRIPTOR_TYPE_HID,
        .bcdHID              = USB_VERSION(1, 1, 0),
        .bCountryCode        = HID_COUNTRYCODE_GERMAN,
        .bNumDescriptors     = 0x01,
        .bReportType         = HID_DESCRIPTOR_TYPE_REPORT,
        .wDescriptorLength   = sizeof(MouseReport),
    },
    [0].DataINEndpoint =
    {
        .bLength             = sizeof(USB_EndpointDescriptor_t),
        .bDescriptorType     = DESCRIPTOR_TYPE_ENDPOINT,
        .bEndpointAddress    = MOUSE_IN_EP,
        .bmAttributes        = USB_ENDPOINT_USAGE_DATA | USB_ENDPOINT_SYNC_NO | USB_ENDPOINT_TRANSFER_INTERRUPT,
        .wMaxPacketSize      = MOUSE_EP_SIZE,
        .bInterval           = 0x0A,
    }
};

Jetzt fehlt nur noch der Report-Deskriptor. Der Aufbau des Deskriptors ist vorgegeben und kann z. B. dem Dokument Device Class Definition for Human Interface Devices (HID) entnommen werden. Mit Hilfe des HID Descriptor Tools kann dann aus dem vorgegebenen Format ein entsprechender Report-Deskriptor erzeugt werden.

const uint8_t PROGMEM MouseReport[] =
{
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x02,                    // USAGE (Mouse)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x09, 0x01,                    //   USAGE (Pointer)
    0xa1, 0x00,                    //   COLLECTION (Physical)
    0x05, 0x09,                    //     USAGE_PAGE (Button)
    0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
    0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x95, 0x03,                    //     REPORT_COUNT (3)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x95, 0x01,                    //     REPORT_COUNT (1)
    0x75, 0x05,                    //     REPORT_SIZE (5)
    0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
    0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
    0x09, 0x30,                    //     USAGE (X)
    0x09, 0x31,                    //     USAGE (Y)
    0x15, 0xff,                    //     LOGICAL_MINIMUM (-1)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x35, 0xff,                    //     PHYSICAL_MINIMUM (-1)
    0x45, 0x01,                    //     PHYSICAL_MAXIMUM (1)
    0x95, 0x02,                    //     REPORT_COUNT (2)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x81, 0x06,                    //     INPUT (Data,Var,Rel)
    0xc0,                          //   END_COLLECTION
    0xc0                           // END_COLLECTION
};

Damit die Deskriptoren bei einer entsprechenden Anfrage auch versendet werden, muss die Funktion USB_GetDescriptor erweitert werden.

const void* USB_GetDescriptor(const uint16_t wValue, const uint16_t wIndex, uint16_t* Size)
{
    uint8_t DescriptorType = (wValue >> 0x08);
    uint8_t DescriptorNumber = (wValue & 0xFF);

    switch(DescriptorType)
    {
        case DESCRIPTOR_TYPE_DEVICE:
        {
            ...
        }
        case DESCRIPTOR_TYPE_CONFIGURATION:
        {
            *Size = sizeof(USB_Configuration_t);
            return &ConfigurationDescriptor[DescriptorNumber];
        }
        case DESCRIPTOR_TYPE_STRING:
        {
            ...
        }
        case HID_DESCRIPTOR_TYPE_HID:
        {
            *Size = sizeof(USB_HID_Descriptor_t);
            return &ConfigurationDescriptor[DescriptorNumber].MouseHID;
        }
        case HID_DESCRIPTOR_TYPE_REPORT:
        {
            *Size = sizeof(MouseReport);
            return &MouseReport;
        }
    }

    *Size = 0x00;
    return NULL;
}

Wenn der Host eine Konfiguration ausgewählt hat, wird das Event USB_Event_ConfigurationChanged aufgerufen und der Endpunkt konfiguriert.

void USB_Event_ConfigurationChanged(const uint8_t Configuration)
{
    if(Endpoint_Configure(MOUSE_IN_EP, ENDPOINT_TYPE_INTERRUPT, MOUSE_EP_SIZE, 1))
    {
        GPIO_Set(GET_PERIPHERAL(LED0_GREEN), GET_INDEX(LED0_GREEN));
        GPIO_Set(GET_PERIPHERAL(LED0_RED), GET_INDEX(LED0_RED));
    }
    else
    {
        USB_Event_OnError();
    }
}

Der erste Schritt ist damit getan und die Anwendungssoftware soweit angepasst das sich der Mikrocontroller beim Host als USB-Maus anmeldet.

Als nächstes müssen die klassenspezifischen Requests und die Datenübertragung mit dem Host implementiert werden. Dazu wird zuerst die Funktion des ControlRequest-Events implementiert. In dieser Funktion werden dann die klassenspezifischen Requests behandelt:

void USB_Event_ControlRequest(const uint8_t bRequest, const uint8_t bmRequestType, const uint16_t wValue)
{
    switch(bRequest)
    {
        case HID_REQUEST_GET_REPORT:
        {
            if(bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_CLASS | REQUEST_RECIPIENT_INTERFACE))
            {
            }

            break;
        }
        case HID_REQUEST_SET_REPORT:
        {
            if(bmRequestType == (REQUEST_DIRECTION_HOST_TO_DEVICE | REQUEST_TYPE_CLASS | REQUEST_RECIPIENT_INTERFACE))
            {
            }

            break;
        }
        case HID_REQUEST_GET_IDLE:
        {
            if(bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_CLASS | REQUEST_RECIPIENT_INTERFACE))
            {
                Endpoint_WriteByte(Idle >> 0x02);
                Endpoint_FlushIN();

                Endpoint_HandleSTATUS(bmRequestType);
            }

            break;
        }
        case HID_REQUEST_SET_IDLE:
        {
            if(bmRequestType == (REQUEST_DIRECTION_HOST_TO_DEVICE | REQUEST_TYPE_CLASS | REQUEST_RECIPIENT_INTERFACE))
            {
                Endpoint_HandleSTATUS(bmRequestType);

                Idle = ((wValue & 0xFF00) >> 0x06);
            }

            break;
        }
        case HID_REQUEST_GET_PROTOCOL:
        {
            if(bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_CLASS | REQUEST_RECIPIENT_INTERFACE))
            {
                Endpoint_WriteByte(Protocol);
                Endpoint_FlushIN();

                Endpoint_HandleSTATUS(bmRequestType);
            }

            break;
        }
        case HID_REQUEST_SET_PROTOCOL:
        {
            if(bmRequestType == (REQUEST_DIRECTION_HOST_TO_DEVICE | REQUEST_TYPE_CLASS | REQUEST_RECIPIENT_INTERFACE))
            {
                Endpoint_HandleSTATUS(bmRequestType);

                Protocol = wValue;
            }

            break;
        }
    }
}

Der SET_REPORT-Request wird in diesem Beispiel nicht implementiert, weil die Maus keine steuerbaren Elemente (wie z. B. eine Caps-LED bei einer Tastatur) besitzt und der Host dem entsprechend keine nutzen kann. Falls dieser Request unterstützt werden soll, so muss auch der Report-Deskriptor um das entsprechende Element erweitert werden.

Der GET_REPORT-Request kann verwendet werden, um Daten während der Initialisierung vom Gerät über den Kontrollendpunkt abzufragen. Es wird empfohlen den Interrupt In-Endpunkt für die Abfrage nach neuen Daten zu verwenden. Daher wird auch dieser Report nicht implementiert.

Damit ist die Initialisierung der USB-Maus auch schon abgeschlossen. Somit fehlt nur noch die Datenübertragung zum Host. Das Vorgehen ist identisch zum ersten Beispiel. Dazu wird wieder eine entsprechende Funktion erstellt, die periodisch abgerufen werden soll und die dann die Daten in den dafür vorgesehenen Endpunkt schreibt.

int main(void)
{
    ...

    USB_Init(&ConfigUSB);

    sei();

    while(1) 
    {
        USB_Poll();
        Mouse_Task();
    }
}

void Mouse_Task(void)
{
    uint16_t BytesSend = 0x00;
    USB_MouseReport_t MouseReportData;

    if(USB_GetState() != USB_STATE_CONFIGURED)
    {
        return;
    }

    memset(&MouseReportData, 0x00, sizeof(MouseReportData));
    ...
}

Falls der Mikrocontroller mit einem Host verbunden ist, erzeugt die Funktion Mouse_Task eine neue Variable vom Typ USB_MouseReport_t und setzt anschließend alle Felder dieser Struktur mit der memset-Funktion auf 0.


Info:

In diesem Beispiel speichere ich zwar den Idle-Wert aus dem SET_IDLE-Request aber ich verwende ihn nicht. Bei Verwendung von Full-Speed kann man auf ein SOF-Paket zurückgreifen und so die vergangenen Millisekunden zählen. Bei Low-Speed Geräten (wie diese USB-Maus) ist dieses Paket nicht vorhanden und somit kann die Idle-Zeit nur über einen Timer (oder durch eine entsprechend lange Schleife) eingehalten werden.


Die Struktur USB_MouseReport_t beschreibt alle notwendigen Zustandsdaten für den Host, wobei sich der Aufbau  aus dem bereits definierten Report-Deskriptor ergibt. So wird über den Ausschnitt

0x05, 0x09,                    //     USAGE_PAGE (Button)
0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
0x95, 0x03,                    //     REPORT_COUNT (3)
0x75, 0x01,                    //     REPORT_SIZE (1)
0x81, 0x02,                    //     INPUT (Data,Var,Abs)
0x95, 0x01,                    //     REPORT_COUNT (1)
0x75, 0x05,                    //     REPORT_SIZE (5)
0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)

das Datenfeld für die Maustasten beschrieben. Es werden drei Maustasten definiert, die von 1 bis 3 durchnummeriert sind (USAGE_MINIMUM und USAGE_MAXIMUM). Jede dieser Tasten kann Werte von 0 bis 1 annehmen (also 1 Bit) und es werden insgesamt 3 Felder definiert (REPORT_COUNT). Diese Felder werden anschließend mit Daten gefüllt (INPUT). Die bereits definierten Bits werden dann noch mit 5 Paddingbits aufgefüllt. Das Vorgehen ist identisch mit den Maustasten, nur das in diesem Fall konstante Werte verwendet werden, die dann in der Regel ignoriert werden.

Analog werden die beiden Variablen für die X- und Y-Koordinaten beschrieben, sodass sich am Ende die folgende Struktur ergibt.

typedef struct
{
    uint8_t Button;
    int8_t X;
    int8_t Y;
} __attribute__((packed)) USB_MouseReport_t;

Für die Steuerung des Mauszeigers soll der Joystick auf dem Mikrocontrollerboard genutzt werden, welcher über die Funktion Joystick_Read ausgelesen wird. Entsprechend der Eingabe wird dann die X- oder Y-Koordinaten auf 1 oder -1 gesetzt.

switch(Joystick_Read())
{
    case JOYSTICK_NO_ACTION:
    {
        break;
    }
    case JOYSTICK_DOWN:
    {
        MouseReportData.Y = -1;
        break;
    }
    case JOYSTICK_UP:
    {
        MouseReportData.Y = 1;
        break;
    }
    case JOYSTICK_RIGHT:
    {
        MouseReportData.X = -1;
        break;
    }
    case JOYSTICK_LEFT:
    {
        MouseReportData.X = 1;
        break;
    }
    case JOYSTICK_PRESS:
    {
        MouseReportData.Button |= (0x01 << 0x00);
        break;
    }
}

Die Maus besitzt in diesem Beispiel nur die Maustaste 1, die über den Taster des Joysticks realisiert worden ist. Bei verwendung von zwei zusätzlichen Tastern können natürlich auch die beiden anderen Tasten implementiert werden.


Hinweis:

Gemäß der HID-Spezifikation ist das Koordinatensystem an der oberen linken Ecke des Bildschirms platziert, wobei die Breite die X-Achse und die Höhe die Y-Achse bilden.


Als nächstes wird der IN-Endpunkt ausgewählt und geprüft ob dieser für eine Übertragung genutzt werden kann.

Endpoint_Select(MOUSE_IN_EP);
if(Endpoint_IsReadWriteAllowed())
{
    USB_DeviceStream_DataIN(&MouseReportData, sizeof(USB_MouseReport_t), &BytesSend);
}

Falls der Endpunkt frei ist, werden die Daten aus dem Report in den Endpunkt kopiert und zum Host gesendet. Dies geschieht über die Funktion USB_DeviceStream_DataIN.

Endpoint_DS_ErrorCode_t USB_DeviceStream_DataIN(const void* Buffer, const uint16_t Length, uint16_t* BytesSend)
{
    uint16_t BytesSend_Temp = 0x00;
    uint16_t Length_Temp = Length;
    uint8_t* Buffer_Temp = (uint8_t*)Buffer;

    if(BytesSend != NULL)
    {
        Length_Temp -= *BytesSend;
        Buffer_Temp += *BytesSend;
    }

    while(Length_Temp)
    {
        if(Endpoint_IsReadWriteAllowed())
        {
            Endpoint_WriteByte(*Buffer_Temp++);
            Length_Temp--;
            BytesSend_Temp++;
        }
        else
        {
            Endpoint_FlushIN();

            Endpoint_DS_ErrorCode_t ErrorCode = USB_DeviceStream_WaitReady(100);
            if(ErrorCode != ENDPOINT_DS_NO_ERROR)
            {
                *Offset = BytesSend_Temp;

                return ErrorCode;
            }
        }
    }

    Endpoint_FlushIN();
    *BytesSend = BytesSend_Temp;
    return ENDPOINT_DS_NO_ERROR;
}

Diese Funktion kopiert die einzelnen Datenbytes in den aktiven Endpunkt. Sobald der Endpunkt voll ist, werden die Daten über die Funktion Endpoint_FlushIN an den Host gesendet.

Anschließen wartet die Funktion USB_DeviceStream_WaitReady bis der Endpunkt bereit ist um neue Daten aufzunehmen und überprüft gleichzeitig den Zustand des Automaten auf kritische Zustände.

static Endpoint_DS_ErrorCode_t USB_DeviceStream_WaitReady(uint8_t Timeout)
{	
    uint16_t StartFrame = USB_Device_GetFrameNumber();

    while(Timeout)
    {
        if((Endpoint_GetDirection() == ENDPOINT_DIRECTION_IN) && Endpoint_INReady())
        {
            return ENDPOINT_DS_NO_ERROR;
        }
        else if((Endpoint_GetDirection() == ENDPOINT_DIRECTION_OUT) && Endpoint_OUTReceived())
        {
            return ENDPOINT_DS_NO_ERROR;
        }

        if(_DeviceState == USB_STATE_UNATTACHED)
        {
            return ENDPOINT_DS_DISCONNECT;
        }
        else if(_DeviceState == USB_STATE_SUSPEND)
        {
            return ENDPOINT_DS_SUSPEND;
        }
        else if(Endpoint_IsSTALL())
        {
            return ENDPOINT_DS_STALLED;
        }

        uint16_t NewFrame = USB_Device_GetFrameNumber();
        if(NewFrame != StartFrame)
        {
            StartFrame = NewFrame;
            Timeout--;
        }
    }

    return ENDPOINT_DS_TIMEOUT;
}

Im Full-Speed-Modus wird zudem die übermittelte Framenummer aus dem UDFNUM-Register ausgelesen und verwendet um einen Timeout-Zähler zu dekrementieren.

Damit ist die USB-Maus fertig programmiert und einsatzbereit. Das Programm kann jetzt kompiliert und auf den Mikrocontroller übertragen werden. Anschließend kann der Cursor des Bildschirms mit dem Joystick kontrolliert werden.

Zurück

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert