💻 C und Assembler mit Raspberry Pi

Dynamische Geräteerkennung (Plug & Play) (RPI4, C)

Nachdem wir im vorherigen Kapitel den Maustreiber entwickelt haben, konnten Geräte bisher nur einmalig beim Systemstart erkannt werden. In diesem Kapitel erweitern wir das System um Plug & Play (PnP). Dadurch ist es möglich, USB-Geräte wie Mäuse oder Hubs im laufenden Betrieb beliebig anzustecken (Hot-Plug) und wieder zu trennen (Hot-Unplug), ohne das System neu starten zu müssen.

Die Nutzung im Hauptprogramm ist denkbar einfach. In der main.c wird die Hauptschleife um den zyklischen Aufruf von USBHostController_UpdatePlugAndPlay() ergänzt:

while (1)
{
    if (USBHostController_UpdatePlugAndPlay())
    {
        // Eventuell reagieren, wenn sich die PnP-Topologie geändert hat
    }
}

1. Das Architekturkonzept hinter Plug & Play

Um Hot-Plug effizient zu verarbeiten, teilt der USB-Stack die Aufgaben auf zwei Schichten auf:

  1. Interrupt-Kontext (IRQ / Event-Manager):
    Sobald ein Gerät an- oder ausgesteckt wird, löst der xHCI-Controller oder der USB-Hub ein Event aus (XHCI_TRB_TYPE_EVENT_PORT_STATUS_CHANGE). Der IRQ-Handler arbeitet extrem schnell: Er erzeugt lediglich ein kleines Event-Objekt (TPortStatusEvent) und reiht es in eine interne Queue (m_HubEvents) ein. Hier finden keine zeitintensiven Operationen wie malloc oder Bus-Resets statt, um Timeouts zu vermeiden.
  2. Hauptprogramm-Kontext (USBHostController_UpdatePlugAndPlay):
    Das Hauptprogramm fragt die Queue regelmäßig in der while(1)-Schleife ab. Hier findet die eigentliche, zeitaufwendige Bus-Enumeration (Resets, Speicherallokation, Laden von Treibern) sicher statt.
+-------------------------------------------------------------------+
|                     INTERRUPT CONTEXT (IRQ)                       |
|   Hardware-Event -> Queueing in 'm_HubEvents' & schnelles Return  |
+-------------------------------------------------------------------+
                                   |
                                   v
+-------------------------------------------------------------------+
|                   MAIN LOOP CONTEXT (Main Program)                |
|   USBHostController_UpdatePlugAndPlay() verarbeitet Events,       |
|   führt Resets durch, allokiert Speicher & bindet Treiber.        |
+-------------------------------------------------------------------+

2. Der PnP-Loop im Detail (USBHostController_UpdatePlugAndPlay)

Schauen wir uns die zentrale Verarbeitungsfunktion in `usb.c` an:

boolean USBHostController_UpdatePlugAndPlay(void)
{
    boolean bResult = s_hostController.m_bFirstUpdateCall;
    s_hostController.m_bFirstUpdateCall = FALSE;

    CSpinLock_Acquire(&s_hostController.m_SpinLock);

    unsigned startEventsThisPass = 0;
    unsigned numEventsThisPass = 0;

    while (numEventsThisPass < s_hostController.m_nNumUsedHubEvents)
    {
        numEventsThisPass = s_hostController.m_nNumUsedHubEvents;
        
        for (unsigned eventIndex = startEventsThisPass; eventIndex < numEventsThisPass; ++eventIndex)
        {
            TPortStatusEvent *pEvent = &s_hostController.m_HubEvents[eventIndex];
            
            /* WICHTIG: Lock VOR der Verarbeitung freigeben! */
            CSpinLock_Release(&s_hostController.m_SpinLock);

            if (pEvent->bFromRootPort)
            {
                CXHCIRootPort_HandlePortStatusChange(pEvent->pRootPort);
            }
            else
            {
                USBStandardHub_HandlePortStatusChange(pEvent->pHub);
            }

            bResult = TRUE;
            
            /* Lock für den nächsten Schleifendurchlauf wieder holen */
            CSpinLock_Acquire(&s_hostController.m_SpinLock);
        }
        startEventsThisPass = numEventsThisPass;
    }
    
    s_hostController.m_nNumUsedHubEvents = 0;
    CSpinLock_Release(&s_hostController.m_SpinLock);

    return bResult;
}

Kritisches Detail: Der SpinLock-Tanz  
Vor dem Aufruf von CXHCIRootPort_HandlePortStatusChange() oder USBStandardHub_HandlePortStatusChange() muss der SpinLock zwingend freigegeben werden! Während einer Enumeration werden Control-Messages über den Bus gesendet. Würde der Lock gehalten werden, käme es sofort zu einem Deadlock, da die Control-Transfers denselben Lock anfordern.


3. Hot-Plug an den Root-Ports (CXHCIRootPort_HandlePortStatusChange)

Wenn sich der Status eines Direkt-Ports am Controller (Root Port) ändert, prüft die Funktion den aktuellen Verbindungszustand über das CCS-Bit (Current Connect Status) des Ports:

void CXHCIRootPort_HandlePortStatusChange(CXHCIRootPort *pPort)
{
    if (CXHCIRootPort_IsConnected(pPort))
    {
        if (pPort->m_pUSBDevice == 0)
        {
            // Gerät neu eingesteckt: Initialisierung und Konfiguration
            CXHCIRootPort_ReScanDevices(pPort);
        }
    }
    else
    {
        if (pPort->m_pUSBDevice != 0)
        {
            // Gerät entfernt: Treiber zerstören & Speicher freigeben
            CXHCIRootPort_RemoveDevice(pPort);
        }
    }
}

Die Hilfsfunktion CXHCIRootPort_ReScanDevices() führt bei einem neuen Gerät automatisch die zwei Phasen aus, die wir bereits kennengelernt haben: CXHCIRootPort_Initialize() (Slot anfordern, USB-Adresse vergeben) und CXHCIRootPort_Configure() (Treiber suchen und binden).


4. Hot-Plug an externen USB-Hubs (USBStandardHub_HandlePortStatusChange)

USB-Hubs verhalten sich bei Statusänderungen etwas komplexer als Root-Ports. Sie senden ein Status Change Bitmap über ihren Interrupt-Endpoint.

Dabei gilt eine wichtige Besonderheit der USB-Spezifikation: Hubs speichern (latchen) Zustandsänderungen. Solange das entsprechende Change-Bit nicht explizit vom Host gelöscht wird, meldet der Hub die Änderung kontinuierlich weiter und erkennt keine neuen Ereignisse an diesem Port.

Der Ablauf in USBStandardHub_HandlePortStatusChange() gliedert sich daher in vier Schritte:

  1. Status abfragen: Den aktuellen Status des Ports via GET_STATUS Request lesen.
  2. Change-Bits löschen (Clear Feature): Die Bits C_PORT_CONNECTION, C_PORT_ENABLE oder C_PORT_RESET mittels CLEAR_FEATURE Requests am Hub explizit zurücksetzen.
  3. Gerät verarbeiten:
    • Ist das Bit PORT_CONNECTION gesetzt und noch kein Objekt vorhanden, wird USBStandardHub_ReScanDevices() aufgerufen (Enumeration startet).
    • Ist die Verbindung getrennt, wird USBStandardHub_RemoveDeviceAt() aufgerufen (Treiber wird zerstört, Speicher freigegeben).
  4. Polling-Zyklus neu starten:
    Der Interrupt-Transfer für Statusänderungen ist ein "One-Shot". Damit der Hub auch das nächste Ein- oder Ausstecken meldet, wird am Ende der Funktion sofort ein neuer asynchroner Request mittels USBStandardHub_StartStatusChangeRequest() an den Hub gesendet.

Zusammenfassung

Mit der Erweiterung um USBHostController_UpdatePlugAndPlay() und den dazugehörigen Event-Handlern ist das USB-Subsystem nun vollständig dynamisch. Das Hauptprogramm kann beliebig lang laufen, während Geräte wie Mäuse oder Hubs jederzeit im laufenden Betrieb angeschlossen oder getrennt werden können, ohne dass Speicherlecks entstehen oder das System hängen bleibt.

Den Sourcecode bekommst du hier: https://www.satyria.de/arm/sources/RPI4/C/USB5_Mouse2_(PlugAndPlay).zip