💻 C und Assembler mit Raspberry Pi

USB-Tastatur-Anbindung (Teil 2: Keymapping & Terminal-Integration) (RPI4, C)

Im vorherigen Kapitel haben wir den ersten Schritt zur Tastatur-Anbindung gemacht: Wir konnten die rohen 8-Byte-HID-Reports (Hex-Codes) vom Interrupt-Endpoint der Tastatur abfangen und auf dem Bildschirm anzeigen.

In diesem zweiten Teil vervollständigen wir den Tastatur-Treiber. Wir bauen das Keymapping (Übersetzung von Scancodes in Zeichen/ANSI-Sequenzen) über keymap_de.h ein, integrieren ein zustandsbehaftetes Modifier-Tracking (Shift, AltGr, Strg, CapsLock) und fügen in screen.c einen blinkenden Text-Cursor ein, um ein echtes Terminal-Erlebnis zu schaffen!


1. Die Übersetzungstabelle (`keymap_de.h`)

USB-Tastaturen senden keine ASCII-Zeichen, sondern standardisierte USB-HID-Scancodes (z. B. 0x04 für die Taste A). Um aus diesen Werten deutsche Sonderzeichen, Großbuchstaben oder Steuersequenzen zu erzeugen, nutzen wir die Übersetzungstabelle in keymap_de.h.

Die Tabelle bildet jeden physikalischen Scancode (0x00 bis 0x7F) auf fünf verschiedene Ausgabe-Zustände ab:

// Layout: { Normal, Shift, AltGr, AltGr+Shift, Ctrl }
{ 'a', 'A', KeyNone, KeyNone, KeyNone }, // Scancode 0x04
{ 'q', 'Q', C('\x40'), KeyNone, KeyNone }, // Scancode 0x14 -> AltGr + q = '@'
{ '<', '>', '|', KeyNone, KeyNone }, // Scancode 0x64 -> AltGr + < = '|'
{ KeyReturn, KeyReturn, KeyNone, KeyNone, KeyNone }, // Scancode 0x28

2. Der Zustandstracker & Handler in keyboard.c

Da USB-Tastaturen beim Drücken mehrerer Tasten immer den gesamten Zustand (Modifier-Byte + bis zu 6 Scancodes) senden, muss der Treiber Differenzen zwischen dem vorherigen und dem aktuellen Report ermitteln.

A. Tasten-Vergleich im ReportHandler

In CUSBKeyboardDevice_ReportHandler() vergleichen wir das neue 8-Byte-Paket mit m_LastReport:

  1. Modifier auswerten: Byte 0 enthält Bitmasken für Shift, Ctrl, Alt und GUI. Bit-Änderungen lösen KeyPressed oder KeyReleased für die Modifier-Codes (0x80 + Bit-Index) aus.
  2. Key Release erkennen: Wir prüfen, welche Scancodes im alten Report vorhanden waren, im neuen jedoch fehlen.
  3. Key Press erkennen: Neue Scancodes im aktuellen Report lösen ein KeyPressed-Event aus.
void CUSBKeyboardDevice_ReportHandler(CUSBHIDDevice* pHIDDevice, const u8* pReport, unsigned nReportSize)
{
    CUSBKeyboardDevice* pThisDevice = (CUSBKeyboardDevice*)pHIDDevice->m_function.m_pOwner;

    // 1. Modifier-Tasten (Byte 0) auswerten
    for (unsigned i = 0; i < 8; i++)
    {
        unsigned nMask = 1 << i;
        boolean bIsPressedNow = (pReport[0] & nMask) != 0;
        boolean bWasPressedBefore = (pThisDevice->m_LastReport[0] & nMask) != 0;

        if (bIsPressedNow && !bWasPressedBefore)
            CKeyboardBehaviour_KeyPressed(&pThisDevice->m_Behaviour, 0x80 + i);
        else if (!bIsPressedNow && bWasPressedBefore)
            CKeyboardBehaviour_KeyReleased(&pThisDevice->m_Behaviour, 0x80 + i);
    }

    // 2. Losgelassene Tasten erkennen (im alten Report, aber nicht im neuen)
    for (unsigned i = 2; i < USBKEYB_REPORT_SIZE; i++)
    {
        u8 ucKeyCode = pThisDevice->m_LastReport[i];
        if (ucKeyCode != 0 && !CUSBKeyboardDevice_FindByte(pReport + 2, ucKeyCode, 6))
            CKeyboardBehaviour_KeyReleased(&pThisDevice->m_Behaviour, ucKeyCode);
    }

    // 3. Neu gedrückte Tasten erkennen (im neuen Report, aber nicht im alten)
    for (unsigned i = 2; i < USBKEYB_REPORT_SIZE; i++)
    {
        u8 ucKeyCode = pReport[i];
        if (ucKeyCode != 0 && !CUSBKeyboardDevice_FindByte(pThisDevice->m_LastReport + 2, ucKeyCode, 6))
            CKeyboardBehaviour_KeyPressed(&pThisDevice->m_Behaviour, ucKeyCode);
    }

    memcpy(pThisDevice->m_LastReport, pReport, USBKEYB_REPORT_SIZE);
}

Die Taste "GUI" ist ein synonym für:

  • Windows-Tastaturen: Es ist die Windows-Taste (mit dem Windows-Logo).
  • Mac-Tastaturen: Es ist die Command-Taste (auch Befehlstaste genannt).
  • Linux / Unix: Wird sie oft als Super-Taste oder Meta-Taste bezeichnet.

B. Die Übersetzungslogik (CKeyMap_Translate)

Die Funktion CKeyMap_Translate() ermittelt anhand der aktiven Modifier (z. B. KEY_LSHIFT_MASK oder KEY_ALTGR_MASK) die richtige Spalte in der Übersetzungstabelle keymap_de.h:

u16 CKeyMap_Translate(CKeyMap* pKeyMap, u8 nPhyCode, u8 nModifiers)
{
    unsigned nTable = K_NORMTAB;

    if (nModifiers & KEY_ALTGR_MASK)
    {
        nTable = (nModifiers & (KEY_LSHIFT_MASK | KEY_RSHIFT_MASK)) ? K_ALTSHIFTTAB : K_ALTTAB;
    }
    else if (nModifiers & (KEY_LSHIFT_MASK | KEY_RSHIFT_MASK))
    {
        nTable = K_SHIFTTAB;
    }

    u16 nLogCode = pKeyMap->m_KeyMap[nPhyCode][nTable];

    // Sonderfunktion: CapsLock Umschaltung
    if (nLogCode == KeyCapsLock)
    {
        pKeyMap->m_bCapsLock = !pKeyMap->m_bCapsLock;
        return ActionSwitchCapsLock;
    }

    return nLogCode;
}

In CKeyMap_GetString() werden Sondertasten schlussendlich in lesbare Zeichen oder ANSI-Escape-Sequenzen umgewandelt (z. B. wird KeyUp zu \x1b[A für die Cursorsteuerung im Terminal).


3. Der Text-Cursor in screen.c

Um dem Benutzer visuelles Feedback zu geben, wo das nächste Zeichen getippt wird, fügen wir in screen.c eine Cursor()-Funktion ein:

void Cursor(void)
{
    if (!CursorStatus)
    {
        DrawChar(' ', cursor_x * CHAR_WIDTH, cursor_y * CHAR_HEIGHT);
        CursorStatus = TRUE;
    }
    else
    {
        DrawChar('_', cursor_x * CHAR_WIDTH, cursor_y * CHAR_HEIGHT);
        CursorStatus = FALSE;
    }
}

Die Funktion toggelt das Zeichen an der aktuellen Textkoordinate (cursor_x, cursor_y) zwischen einem Leerzeichen ' ' und einem Unterstrich '_'.


4. Integration & Blinken in der main.c

Im Hauptprogramm führen wir nun alle Elemente zusammen:

A. Timer für das Cursor-Blinken

Über einen wiederkehrenden Kernel-Timer rufen wir alle 50 Ticks (ca. alle halbe Sekunde) die Cursor()-Funktion auf, sobald eine Tastatur erkannt wurde:

void TimerHandler(TKernelTimerHandle hTimer, void* pParam, void* pContext)
{
    if (Kernel.m_pKeyboard != 0)
    {
        Cursor(); // Invertiert/Toggelt den Unterstrich an der aktuellen Textposition
    }
    Timer_StartKernelTimer(50, TimerHandler, NULL, NULL); // Timer neu aufsetzen
}

B. Registrierung des String-Handlers

In der PnP-Hot-Plug-Überwachung registrieren wir bei der Tastatur-Erkennung nicht mehr den RAW-Handler, sondern CUSBKeyboardDevice_RegisterKeyPressedHandler() mit unserem KeyPressedHandler:

// --- KEYBOARD DETECTION ---
if (Kernel.m_pKeyboard == 0)
{
    CDevice* pKeyboard = DeviceNameService_GetDevice("ukbd1", FALSE);
    if (pKeyboard)
    {
        SetFrontColor(green);
        printf("Keyboard gefunden!\n");
        SetFrontColor(white);
        
        Kernel.m_pKeyboard = (CUSBKeyboardDevice *)pKeyboard->pOwner;
        
        // Jetzt registrieren wir den String-Handler!
        CUSBKeyboardDevice_RegisterKeyPressedHandler(Kernel.m_pKeyboard, KeyPressedHandler);
        
        printf("-> Tastatur-Treiber aktiviert. Tippe etwas...\n");
    }
}

C. Zeichen-Ausgabe (KeyPressedHandler)

Trifft ein übersetzter String vom Tastatur-Treiber ein, geben wir ihn direkt über printf() auf dem Bildschirm aus:

static void KeyPressedHandler(const char *pString)
{
    if (pString != 0)
    {
        // Gibt normale Zeichen (z.B. "a", "A", "@") oder ANSI-Steuersequenzen aus
        printf("%s", pString);
    }
}

Zusammenfassung

Mit der Fertigstellung des zweiten Teils haben wir eine vollwertige Eingabeeinheit geschaffen:

  • Tasteneingaben werden über den USB-Interrupt-Endpoint erfasst.
  • Layout-Übersetzung (keymap_de.h) wandelt HID-Scancodes korrekt in deutsche Zeichen inklusive Sonderzeichen (`@`, `€`, `ä`, `ö`, `ü`, `ß`) und Steuerzeichen um.
  • Sondertasten & Pfeiltasten erzeugen valide ANSI-Escape-Sequenzen, mit denen das Terminal gesteuert wird.
  • Der blinkende Cursor signalisiert die aktuelle Eingabeposition im Terminal.

Den Sourcecode kannst du hier herunterladen: https://www.satyria.de/arm/sources/RPI4/C/USB6_Keyboard2.zip