💻 C and Assembly with Raspberry Pi
🌐 This article is currently only available in German.

USB-Tastatur-Anbindung (Teil 1: RAW-Daten-Empfang) (RPI4, C)

Nachdem wir erfolgreich einen USB-Maus-Treiber sowie das Plug-and-Play-Subsystem (Hot-Plug) im Bare-Metal-Kernel etabliert haben, wenden wir uns dem nächsten universellen Eingabegerät zu: der USB-Tastatur.

In diesem ersten Schritt erweitern wir unser Treiber-System (CUSBDeviceFactory), um Tastaturen zu erkennen, und implementieren in keyboard.c den Empfang von unverschlüsselten RAW-Daten (Hex-Codes) des USB-HID Boot Protocols.


1. Erweiterung der CUSBDeviceFactory in usb.c

Damit der USB-Stack beim Einstecken einer Tastatur den richtigen Treiber lädt, muss die CUSBDeviceFactory_GetDevice()-Funktion um die Erkennung der Klasse int3-1-1 erweitert werden:

Format: int3-1-1 = Class 3 (HID) + SubClass 1 (Boot) + Protocol 1 (Keyboard)

Im Quellcode von usb.c sieht diese Erkennung wie folgt aus:

/* USB Tastatur (Class 3 = HID, SubClass 1 = Boot, Protocol 1 = Keyboard) */
else if (strcmp(pName, "int3-1-1") == 0)
{
    USB_LOG(green, "       -> Treffer! Das ist eine USB-Tastatur (Boot Protocol).");
    USB_LOG(white, "       -> Class 3 (HID) + SubClass 1 (Boot) + Protocol 1 (Keyboard)");

    CString Vendor = {0};
    CUSBDevice_GetName(CUSBFunction_GetDevice(pParent), &Vendor, DeviceNameVendor);

    // Schutzprüfung für fehlerhafte Hardware (z. B. HP Vendor 0x03f0 / Product 0x1198)
    if (CString_Compare(&Vendor, "ven3f0-1198") != 0)
    {
        CUSBKeyboardDevice* pNewDevice = (CUSBKeyboardDevice*)malloc(sizeof(CUSBKeyboardDevice));
        if (!pNewDevice)
        {
            CString_Free(&Vendor);
            return 0;
        }

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

        USB_LOG(green, "       -> Tastatur-Treiber erfolgreich geladen!");
    }
    else
    {
        USB_LOG(yellow, "       -> WARNUNG: Bekannter fehlerhafter Vendor. Treiber wird nicht geladen.");
    }

    CString_Free(&Vendor);
}

Nach erfolgreicher Erkennung registriert der USB-Stack die Tastatur automatisch unter dem Gerätenamen "ukbd1" im DeviceNameService.


2. Das 8-Byte HID Boot Protocol Layout für Tastaturen

Ebenso wie bei der Maus zwingen wir die Tastatur über das HID Boot Protocol in ein fest definiertes, herstellerunabhängiges Berichtsformat (HID Report). Eine USB-Tastatur sendet bei jeder Zustandsänderung (Tastendruck, Loslassen, Modifier-Aktivierung) ein genau 8 Bytes großes Datenpaket:

Byte-Index Feld Beschreibung
Byte 0 Modifier Bitmaske für Spezialtasten:
Bit 0=Strg Links,
1=Shift Links,
2=Alt Links,
3=GUI Links,
4=Strg Rechts,
5=Shift Rechts,
6=Alt Rechts,
7=GUI Rechts
Byte 1 Reserved Reserviertes Byte (standardmäßig 0x00)
Byte 2-7 Keycodes Bis zu 6 gleichzeitig gedrückte Tasten als HID-Scancodes (z. B. 0x04 für Taste A)

Durch die Verwendung der 6 Keycode-Bytes unterstützt das Boot-Protokoll hardwareseitiges 6-Key-Rollover (6KRO).


3. Der Treiber-Aufbau in keyboard.c

Der Tastaturtreiber kapselt die HID-Anbindung und bereitet den RAW-Datenstrom für den Kernel vor.

A. Erstellung & Konfiguration (`CUSBKeyboardDevice_Configure`)

Bei der Konfiguration der Tastatur werden zwei wichtige Schritte durchgeführt:

  1. LED-Aufweck-Workaround (CUSBKeyboardDevice_SetLEDs): Manche USB-Tastaturen verbleiben nach dem Einschalten im Stromsparmodus, bis der Host ihnen einen initialen SET_REPORT-Befehl für die Status-LEDs (NumLock, CapsLock, ScrollLock) sendet. Wir senden hierzu initial ein 0x00-Byte über den Control-Endpoint (EP0).
  2. Interrupt-Polling starten (CUSBHIDDevice_StartRequest): Über den Interrupt-IN Endpoint wird das kontinuierliche, asynchrone Abfragen der Tastatur gestartet.
boolean CUSBKeyboardDevice_Configure(CUSBKeyboardDevice *pKeyboardDevice)
{
    // 1. HID-Layer konfigurieren
    if (!CUSBHIDDevice_ConfigureHID(&pKeyboardDevice->m_HIDDevice, 0))
        return FALSE;

    // 2. Initialen LED-Status senden (Aufweck-Workaround)
    CUSBKeyboardDevice_SetLEDs(pKeyboardDevice, pKeyboardDevice->m_ucLastLEDStatus);

    // 3. Im DeviceNameService registrieren als "ukbd1"
    pKeyboardDevice->m_nDeviceNumber = CNumberPool_AllocateNumber(&s_DeviceNumberPool, TRUE, "FromUSBKbd");
    DeviceNameService_AddDeviceWithPrefix(DevicePrefix, pKeyboardDevice->m_nDeviceNumber,
                                          &pKeyboardDevice->m_HIDDevice.m_function.m_thisDevice, FALSE);

    // 4. Asynchronen Interrupt-Polling-Zyklus starten
    return CUSBHIDDevice_StartRequest(&pKeyboardDevice->m_HIDDevice);
}

B. Weiterleitung im Interrupt-Handler (CUSBKeyboardDevice_ReportHandler)

Sobald der xHCI-Controller ein neues 8-Byte-Paket empfängt, wird CUSBKeyboardDevice_ReportHandler() aufgerufen. In diesem ersten Teil des Tutorials entfällt noch die Übersetzung in ASCII-Zeichen; die Rohdaten werden direkt an den registrierten m_pKeyStatusHandlerRaw-Callback weitergegeben:

void CUSBKeyboardDevice_ReportHandler(CUSBHIDDevice *pHIDDevice, const u8 *pReport, unsigned nReportSize)
{
    CUSBKeyboardDevice *pThisDevice = (CUSBKeyboardDevice*)pHIDDevice->m_function.m_pOwner;

    if (pReport == 0 || nReportSize != USBKEYB_REPORT_SIZE)
        return;

    if (pThisDevice->m_pKeyStatusHandlerRaw != 0)
    {
        // Parameter: Modifier (Byte 0), Keycode-Array (Byte 2 bis 7)
        (*pThisDevice->m_pKeyStatusHandlerRaw)(pReport[0], pReport + 2, pThisDevice->m_pKeyStatusHandlerRawArg);

        if (!pThisDevice->m_bMixedMode)
        {
            return; // In Teil 1: Rein visuelle RAW-Ausgabe ohne ASCII-Keymapping
        }
    }
}

4. Anbindung in der main.c

Im Hauptprogramm erweitern wir die PnP-Schleife, um neben der Maus nun auch das Gerät "ukbd1" zu erkennen und einzurichten:

// --- KEYBOARD DETECTION (Plug & Play) ---
if (Kernel.m_pKeyboard == 0)
{
    CDevice* pKeyboard = DeviceNameService_GetDevice("ukbd1", FALSE);
    if (pKeyboard)
    {
        printf("\nKeyboard gefunden! Initialisiere Raw-Daten-Empfang...\n");
        
        Kernel.m_pKeyboard = (CUSBKeyboardDevice *)pKeyboard->pOwner;
        
        // Registrierung unseres lokalen RAW-Event-Handlers
        Kernel.m_pKeyboard->m_pKeyStatusHandlerRaw = KeyboardRawHandler;
        Kernel.m_pKeyboard->m_pKeyStatusHandlerRawArg = 0;
        
        printf("-> Raw-Handler registriert. Drücke Tasten, um die Hex-Codes zu sehen!\n");
    }
}

Filtern identischer Reports (KeyboardRawHandler)

Da USB-Tastaturen bei gedrückter Taste periodisch identische Reports senden (z. B. alle 8 ms), würde die Konsole ohne Überprüfung augenblicklich mit gleichen Ausgaben überflutet werden. Unser KeyboardRawHandler vergleicht das neu eingetroffene 8-Byte-Paket mittels memcmp mit dem letzten Zustand g_LastKeyboardReport und gibt die Werte nur bei einer tatsächlichen Änderung aus:

static void KeyboardRawHandler(unsigned char ucModifiers, const unsigned char RawKeys[6], void* pArg)
{
    u8 currentReport[8];
    currentReport[0] = ucModifiers;
    currentReport[1] = 0x00; // Reserved
    for (int i = 0; i < 6; i++) {
        currentReport[i + 2] = RawKeys[i];
    }

    // Nur ausgeben, wenn sich der Tasten-Zustand verändert hat!
    if (memcmp(g_LastKeyboardReport, currentReport, 8) != 0)
    {
        memcpy(g_LastKeyboardReport, currentReport, 8);

        SetFrontColor(cyan);
        printf("\r[KB RAW] Mod: 0x%02X | Keys: %02X %02X %02X %02X %02X %02X   ",
               ucModifiers, 
               RawKeys[0], RawKeys[1], RawKeys[2], RawKeys[3], RawKeys[4], RawKeys[5]);
        SetFrontColor(white);
    }
}

Zusammenfassung

Mit dieser Erweiterung ist das USB-Subsystem in der Lage, Tastaturen vollautomatisch per Hot-Plug zu erkennen, per Interrupt-Polling abzufragen und deren ungefilterten HID-Scancodes in Echtzeit auf dem Bildschirm darzustellen. Im nächsten Kapitel widmen wir uns der Übersetzung dieser Hex-Codes in ein deutsches Tastaturlayout (CKeyMap) zur Ausgabe echter ASCII-Zeichen!

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