💻 C und Assembler mit Raspberry Pi

USB-Maus-Treiber (HID Boot Protocol) (RPI4, C)

Nachdem unser USB-Stack nun in der Lage ist, den xHCI-Controller zu starten, die Root-Ports zu verwalten und USB-Hubs (wie den internen VL805-Chip des Raspberry Pi 4) einzubinden, folgt der nächste logische Schritt: Ein echtes Eingabegerät zum Leben erwecken.

In diesem Kapitel binden wir eine USB-Maus ein. Dazu erweitern wir die CUSBDeviceFactory um die Klassenerkennung für HID-Mäuse und betrachten die Funktionsweise der neuen Datei mouse.c.


1. Erweiterung der CUSBDeviceFactory

Bisher konnte unsere CUSBDeviceFactory_GetDevice()-Funktion lediglich USB-Hubs (USB Class 9) erkennen. Wenn wir ein Gerät an den Raspberry Pi 4 anschließen, liest der USB-Stack die Konfigurationsdeskriptoren aus und baut daraus einen Identifikations-String nach folgendem Schema auf:

Format: int<BaseClass>-<SubClass>-<Protocol>

Für eine USB-Maus im Boot Protocol-Modus ergibt sich die Kennung int3-1-2:

  • Class 3 (HID): Human Interface Device (Mäuse, Tastaturen, Gamepads).
  • SubClass 1 (Boot): Das BIOS/Bare-Metal-kompatible Boot-Protokoll.
  • Protocol 2 (Mouse): Kennzeichnung als Maus (Protocol 1 wäre eine Tastatur).

In der erweiterten CUSBDeviceFactory_GetDevice() wird dieser String nun abgefangen:

/* USB Maus (Class 3 = HID, SubClass 1 = Boot, Protocol 2 = Mouse) */
else if (strcmp(pName, "int3-1-2") == 0)
{
    USB_LOG(green, " -> Treffer! Das ist eine USB-Maus (Boot Protocol).");
    USB_LOG(white, " -> Lade Maus-Treiber (CUSBMouseDevice)...");

    CUSBMouseDevice *pNewDevice = (CUSBMouseDevice *)malloc(sizeof(CUSBMouseDevice));
    if (!pNewDevice)
    {
        USB_LOG(red, " -> FEHLER: malloc für Maus fehlgeschlagen!");
        return 0;
    }

    CUSBMouseDevice_Create(pNewDevice, pParent);
    pNewDevice->m_HIDDevice.m_function.m_pOwnerMemory = pNewDevice;
    pResult = &pNewDevice->m_HIDDevice.m_function;

    USB_LOG(green, " -> Maus-Treiber erfolgreich geladen!");
}

Warum das Boot Protocol nutzen?

Normalerweise senden HID-Geräte einen komplexen HID Report Descriptor, der von einem Parser analysiert werden müsste, um zu verstehen, an welchem Byte-Offset welche Taste liegt. Das Boot Protocol (SubClass 1) zwingt die Maus in ein standardisiertes, festes 3- bis 4-Byte-Format. So können wir die Daten ohne aufwendiges Parsing direkt verarbeiten.


2. Der Maus-Treiber (mouse.c) im Detail

Die Datei mouse.c setzt auf einem mehrschichtigen Softwaredesign auf:

+-------------------------------------------------------+
|  CMouseDevice / CMouseBehaviour (High-Level Logik)    |  <-- Position, Scaling, Mailbox/Cursor
+-------------------------------------------------------+
                          |
                          v
+-------------------------------------------------------+
|  CUSBMouseDevice (Maus-spezifische Anbindung)         |  <-- Report-Parsing (Buttons, X, Y, Wheel)
+-------------------------------------------------------+
                          |
                          v
+-------------------------------------------------------+
|  CUSBHIDDevice / CUSBFunction (USB-Stack Basis)       |  <-- Interrupt-Transfers & Polling
+-------------------------------------------------------+

A. Konfiguration & Boot Protocol erzwingen (CUSBMouseDevice_Configure)

Sobald der Treiber geladen ist, ruft der USB-Stack CUSBMouseDevice_Configure() auf:

  1. SET_PROTOCOL Request senden: Über einen Control Message Transfer an EP0 senden wir das Kommando SET_PROTOCOL (bRequest = 0x0B) mit wValue = 0, um das Boot-Protokoll explizit zu aktivieren.
  2. HID-Interrupt-Endpoint starten: Mit CUSBHIDDevice_ConfigureHID() und CUSBHIDDevice_StartRequest() wird der Interrupt-IN Endpoint gestartet.
  3. Asynchrones Polling: Der xHCI-Controller fragt den Endpoint der Maus ab jetzt in regelmäßigen Intervallen (z. B. alle 8 ms) ab. Sobald die Maus bewegt wird, löst der Controller einen Event-TRB aus.

B. Daten-Empfang & Parson (CUSBMouseDevice_ReportHandler)

Sobald Daten von der Maus eintreffen, ruft die HID-Schicht den Callback CUSBMouseDevice_ReportHandler() auf. Das 4-Byte-Datenpaket des Boot Protocols sieht wie folgt aus:

Byte Inhalt Wertebereich / Format
Byte 0 Tasten-Bitmaske Bit 0 = Links, Bit 1 = Rechts, Bit 2 = Mitte
Byte 1 Relativer X-Versatz Signed 8-Bit (s8): -128 bis +127
Byte 2 Relativer Y-Versatz Signed 8-Bit (s8): -128 bis +127
Byte 3 Scrollrad (optional) Signed 8-Bit (s8): -128 bis +127
/* WICHTIG: Cast zu (s8) für korrekte Vorzeichendarstellung! */
s32 xMove = (s8)pReport[1];
s32 yMove = (s8)pReport[2];
s32 wheelMove = (nReportSize >= 4) ? (s8)pReport[3] : 0;

Wichtiger Hinweis zum Datentyp:  
Da USB-Pakete als Vorzeichenlose Bytes (u8) empfangen werden, muss der Wert zwingend nach s8 gecastet werden! Eine Bewegung nach links entspricht z. B. dem Hex-Wert 0xFF. Ohne den Cast würde der Wert als +255 statt als -1 interpretiert werden.

C. Positionsberechnung & Beschleunigung (CMouseBehaviour_MouseStatusChanged)

In der High-Level-Schicht CMouseBehaviour werden die relativen Verschiebungsdaten verarbeitet:

  1. Beschleunigung (Scaling): Der Versatz wird mit einem Beschleunigungsfaktor skaliert:
    nDisplacementX = (nDisplacementX * ACCELERATION) / 10;
    nDisplacementY = (nDisplacementY * ACCELERATION) / 10;
  2. Positionsberechnung & Clamping: Die relative Bewegung wird auf die aktuelle absolute Cursorposition aufaddiert. Dabei wird sichergestellt, dass die Koordinaten den Bildschirmbereich (0 bis ScreenWidth - 1 bzw. ScreenHeight - 1) nicht verlassen.
  3. Event-Erkennung: Durch den Vergleich der alten und neuen Button-Bitmasken werden gezielt MouseDown-, MouseUp- und MouseMove-Events generiert.

D. Hardware-Cursor über die GPU-Mailbox (CMouseBehaviour_SetCursorState)

Eine Besonderheit auf dem Raspberry Pi ist die Steuerung des Hardware-Cursors. Anstatt den Mauszeiger manuell in den Framebuffer zeichnen zu müssen (was Pixel sichern und Wiederherstellen erfordern würde), nutzen wir die Videocore-GPU-Schnittstelle:

// Baut eine Mailbox-Nachricht auf im Coherent Memory
CursorStateMessage* msg = (CursorStateMessage*)Memory_GetCoherentPage(COHERENT_SLOT_PROP_MAILBOX);

msg->tagCursorState.header.id = Set_Cursor_State;
msg->tagCursorState.enable = bVisible ? CURSOR_ENABLE_VISIBLE : CURSOR_ENABLE_INVISIBLE;
msg->tagCursorState.posX = nPosX;
msg->tagCursorState.posY = nPosY;

// Sendet die Nachricht an den GPU-Mailbox-Kanal
MailBox_WriteRead(MAILBOX_PROP_OUT, nBusAddress);

Über die Set_Cursor_State-Property schieben wir die Koordinaten direkt an die GPU, welche das Zeichnen und Überlagern des Cursors auf der Anzeige in Hardware übernimmt.


Zusammenfassung

Mit dem Hinzufügen der Erweiterung in der CUSBDeviceFactory und der Datei mouse.c kann unser Bare-Metal-Betriebssystem nun USB-Mäuse an einen beliebigen Port (oder Hub) erkennen, per Interrupt-Polling abfragen und die Position auf dem Bildschirm über die GPU nachführen.

Den Code bekommst du hier: https://www.satyria.de/arm/sources/RPI4/C/USB5_Mouse1.zip