💻 C und Assembler mit Raspberry Pi

USB Mass Storage – Einbindung von USB-Sticks (Klasse 8) (RPI4, C)

Nachdem wir asynchrone Eingabegeräte wie Maus und Tastatur erfolgreich angebunden haben, nehmen wir nun ein wesentliches Element der Speicherverwaltung in Angriff: USB Mass Storage Devices (Class 8) wie USB-Sticks, SD-Kartenleser oder externe Festplatten.

Bevor wir in die technischen Details eintauchen, müssen wir eine dringende Warnung aussprechen!


⚠️ WICHTIGE WARNUNG VORAB:

In diesem und den folgenden Kapiteln beschränken wir uns nicht mehr nur auf das Auslesen von Geräten. Wir führen echte Lese- und Schreibzugriffe auf physikalischer Sektorebene (Raw Block I/O) durch!

  • Gefahr von Datenverlust: Ein fehlerhafter Schreibbefehl oder eine falsche Sektoradresse kann den Partitionstabelle-Header (MBR) oder das Dateisystem des USB-Sticks auf einen Schlag unbrauchbar machen.
  • Sicherheitsempfehlung: Verwende für das Tutorial ausschließlich einen leeren oder entbehrlichen Test-USB-Stick, auf dem sich keine wichtigen Daten befinden!
  • Hardware-Kompatibilität bei Bulk-Transfers: Wenn dein Test-USB-Stick im ersten Teil (oder spätestens in Teil 2) nicht wie erwartet reagiert, Hänger verursacht oder das System bei SCSI-Kommandos blockiert, ist der Controller des Sticks höchstwahrscheinlich ungeeignet oder hält sich nicht strikt an das USB-BOT-Standardprotokoll. Nutze in diesem Fall am besten einen einfachen, älteren USB-2.0-Stick (z. B. 2 GB bis 16 GB).

1. Was unterscheidet den USB-Stick von Maus & Tastatur?

Mäuse und Tastaturen sind HID-Geräte (Class 3), die ereignisgesteuert über Interrupt-Endpoints kleine Datenpakete an das System senden.

Ein USB-Stick hingegen ist ein Massenspeicher (Class 8). Er arbeitet im Bulk-Only Transport (BOT)-Modus und verwendet vorrangig Bulk-Endpoints:

  • Keine garantierte Latenz, aber maximale Bandbreite: Bulk-Transfers nutzen die ungenutzte Bus-Bandbreite vollständig aus, um große Datenmengen extrem schnell zu übertragen.
  • Kein Polling, sondern interaktive Befehlskette: Der Treiber fragt nicht passiv ab, ob Eingaben vorliegen, sondern tritt aktiv in Dialog mit dem Speichermedium. Er führt Steuerungsbefehle wie "Lies Sektor 0" oder "Schreibe Sektor 12" aus.

2. Erweiterung der CUSBDeviceFactory in usb.c

Damit das System beim Einstecken eines Massenspeichers den Treiber lädt, fangen wir die Interface-Kennung int8- (Class 8 = Mass Storage) ab:

Format: int8-x-x = Class 8 (Mass Storage)
/* USB Mass Storage (Class 8) */
else if (strncmp(pName, "int8-", 5) == 0)
{
    USB_LOG(green, "       -> Treffer! USB Mass Storage Device erkannt (Class 8).");
    USB_LOG(white, "       -> Protokoll: Bulk-Only Transport (BOT) mit SCSI-Befehlssatz.");
    USB_LOG(white, "       -> Lade Mass-Storage-Treiber (CUSBBulkOnlyMassStorageDevice)...");

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

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

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

3. Der Treiber-Aufbau (massstorage.c) & Die Initialisierungs-Sequenz

Sobald das Massenspeicher-Objekt erzeugt ist, schlägt in CUSBBulkOnlyMassStorageDevice_Configure() die Geburtsstunde des Speichermediums. Ein USB-Stick wird dabei in fünf fest definierten Schritten schrittweise ins System eingebunden:

+---------------------------------------------------------+
|  Schritt 1: Bulk-Endpoints suchen (Bulk-IN & Bulk-OUT)  |
+---------------------------------------------------------+
                             |
                             v
+---------------------------------------------------------+
|  Schritt 2: SCSI INQUIRY ("Wer bist du?")               |
+---------------------------------------------------------+
                             |
                             v
+---------------------------------------------------------+
|  Schritt 3: SCSI TEST UNIT READY ("Bist du bereit?")    |
+---------------------------------------------------------+
                             |
                             v
+---------------------------------------------------------+
|  Schritt 4: SCSI READ CAPACITY 10 ("Wie groß bist du?") |
+---------------------------------------------------------+
                             |
                             v
+---------------------------------------------------------+
|  Schritt 5: PartitionManager & MBR-Analyse              |
+---------------------------------------------------------+

Schritt 1: Bulk-IN und Bulk-OUT Endpoints identifizieren

Der Treiber durchsucht die Deskriptoren nach den zwei erforderlichen Kanälen:

  • Bulk-OUT Endpoint: Sendet Kommandos (CBW) und Daten von der CPU an den Stick.
  • Bulk-IN Endpoint: Empfängt Daten und Statusmeldungen (CSW) vom Stick an die CPU.

Schritt 2: SCSI INQUIRY (Hardware-Identifikation)

Der Treiber sendet das SCSI-Kommando SCSI_OP_INQUIRY (0x12). Er prüft, ob sich das Gerät als Direct Access Block Device (0x00) zu erkennen gibt.

Schritt 3: SCSI TEST UNIT READY (Warte-Schleife)

Da der Flash-Controller eines USB-Sticks beim Anlegen der Versorgungsspannung Bedenkzeit benötigt, fragen wir mit SCSI_OP_TEST_UNIT_READY in einer Schleife (bis zu 100 Versuche mit je 100 ms Pause) ab, ob der Speicher einsatzbereit ist.

Schritt 4: SCSI READ CAPACITY (Größe ermitteln)

Mit SCSI_OP_READ_CAPACITY10 erfragen wir die Anzahl der Blöcke sowie die Sektorgröße. Standardmäßige USB-Sticks melden in der Regel eine Sektorgröße von 512 Bytes (UMSD_BLOCK_SIZE):

pThisDevice->m_nBlockCount = le2be32(SCSIReadCapacityResponse.ReturnedLogicalBlockAddress) + 1;
unsigned nSizeMB = pThisDevice->m_nBlockCount / (0x100000 / UMSD_BLOCK_SIZE);

USB_LOG(green, "       -> Kapazität: %u MByte (%u Blöcke à %u Bytes)", 
        nSizeMB, pThisDevice->m_nBlockCount, UMSD_BLOCK_SIZE);

Schritt 5: Einbindung in die System-Registry & PartitionManager

Das physische Laufwerk wird als Block-Device unter dem Namen umsd1 (oder umsd2 etc.) registriert. Direkt im Anschluss liest der PartitionManager den Sektor 0 aus, um den Master Boot Record (MBR) und die vorhandenen Partitionen (z. B. FAT32) zu analysieren:

boolean PartitionManager_Initialize(CDevice *pDevice, const char *pDeviceName)
{
    TMasterBootRecord MBR;

    // Sektor 0 (MBR) lesen
    if (s_PartitionManager.m_pStorageDevice->Seek(..., 0) != 0 ||
        s_PartitionManager.m_pStorageDevice->Read(..., &MBR, sizeof MBR) != sizeof MBR)
    {
        return FALSE;
    }

    // Prüfen der magischen Boot-Signatur (0xAA55)
    if (MBR.BootSignature != BOOT_SIGNATURE)
    {
        USB_LOG(yellow, "       -> Keine gültige Partitionstabelle (Keine MBR Signatur 0xAA55).");
        return TRUE;
    }

    // Primäre Partitionen registrieren (z.B. "umsd1p1", "umsd1p2")
    ...
}

4. Einbindung in der main.c

In der Hauptschleife main.c wird das Erscheinen von Massenspeichern durch die Plug-and-Play-Überwachung registriert. Da die usb_Initialize()-Routine in Verbindung mit massstorage.c den USB-Stick vollautomatisch erkennt und im DeviceNameService als Block-Device anmeldet, genügt der Aufruf von USBHostController_UpdatePlugAndPlay().

Die Ausgabe beim Einstecken eines Sticks bestätigt den erfolgreichen Verbindungsaufbau:

Gerät gefunden: Hersteller/Produkt: SanDisk Ultra
       -> Treffer! USB Mass Storage Device erkannt (Class 8).
[MassStorage] === Konfiguration START ===
       -> Bulk-IN Endpoint gefunden: EP1
       -> Bulk-OUT Endpoint gefunden: EP2
       -> Gerätetyp: Direct Access Block (OK)
       -> Kapazität: 14848 MByte (30408704 Blöcke à 512 Bytes)
[PartitionManager] Initialisiere Speichermedium 'umsd1'...
       -> MBR gültig (Signature 0xAA55 erkannt).
       -> Partition 1 gefunden: Typ=0x0C, Start=2048, Größe=30406656 Sektoren
          -> Registriert als Block-Device 'umsd1p1'.

Zusammenfassung

Mit der Implementierung des ersten Teils kann das Betriebssystem USB-Massenspeicher erkennen, deren Hardware-Eigenschaften via SCSI abfragen, MBR-Partitionstabellen parsen und virtuelle Partitionsobjekte (umsd1p1) zur Verfügung stellen.

In den nächsten Kapiteln betrachten wir die Kernschicht von Bulk-Only Transport (BOT) im Detail: Die Abwicklung von Datenübertragungen über Command Block Wrapper (CBW) und Command Status Wrapper (CSW)!

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