USB lux - MarekBykowski/readme GitHub Wiki

USB SuperSpeed — architektura, kernel, debug

Notatka robocza z bring-upu. Punkt wyjścia: kamera OAK (Luxonis) na kernelu 5.15 nie trzyma stabilnie linku na 10G (Gen2), po zejściu na 5G (Gen1) się uspokaja. Poniżej cały łańcuch: od topologii, przez xHCI i LTSSM, po stos kernela, metryki i drzewo decyzyjne naprawy.

Jednozdaniowa diagnoza: jeśli zejście z 10G na 5G leczy link, problem prawie na pewno siedzi w warstwie fizycznej/łącza (signal integrity albo LPM), a nie w protokole, sterowniku czy aplikacji. Debuguje się od dołu stosu w górę.


Spis treści

  1. Model działania i topologia
  2. Prędkości i generacje
  3. xHCI — model kontrolera
  4. URB i TRB
  5. LTSSM — warstwa łącza
  6. Stos kernela
  7. Enumeracja
  8. LPM — Link Power Management
  9. Debug: metryki, wartości, instrumentacja
  10. Drzewo decyzyjne naprawy
  11. Ściągawki
  12. Case study: OAK4 10G / DWC3 compliance mode

1. Model działania i topologia

USB jest host-centryczny: jeden host (kontroler xHCI) na magistralę, wszystko inicjuje host. Urządzenia (funkcje) są bierne — odzywają się dopiero odpytane. Topologia to drzewo (tiered-star): root hub w warstwie 1, huby rozgałęziają, urządzenia są liśćmi.

graph TD
    H["Host + Root Hub<br/><i>xHCI — inicjuje wszystko</i>"] --> HUB1["Hub"]
    H --> CAM["Kamera OAK<br/><i>urządzenie SS</i>"]
    HUB1 --> HUB2["Hub"]
    HUB1 --> KBD["Klawiatura<br/><i>interrupt</i>"]
    HUB2 --> DISK["Dysk USB<br/><i>bulk</i>"]
Loading

Limity: 7 warstw (budżet opóźnień), 127 adresów (adres 0 zarezerwowany dla świeżo podłączonego urządzenia przed SET_ADDRESS).

Kto inicjuje: zawsze host. Na USB2 to dosłowny polling (token → data → handshake). Na SuperSpeed urządzenie sygnalizuje NRDY/ERDY asynchronicznie ("mam gotowe, odpytaj mnie"), ale właściwy transfer i tak uruchamia host.

Port vs urządzenie

Port to punkt przyłączenia na hubie — istnieje niezależnie od tego, czy coś jest wpięte, i to on trzyma stan (connect, reset, PLS). Kluczowy niuans:

Jedno fizyczne gniazdo USB3 = dwa porty dla kontrolera: osobny USB2 i osobny SuperSpeed. Bo SS i USB2 to fizycznie dwie niezależne magistrale w jednym kablu (USB2 po D+/D−, SS po osobnych parach SSTX/SSRX). Dlatego urządzenie "spada" z SS do USB2 bez wyjmowania z gniazda — przełazi z jednej magistrali na drugą.


2. Prędkości i generacje

USB 3.x / USB4 — drabinka Gen

Nazwa techniczna Marketing Linia Lane Kodowanie ~MB/s speed bcdUSB
USB 3.2 Gen 1 (3.0 / 3.1 Gen1) USB 5Gbps 5 Gb/s 1 8b/10b ~500 5000 0x0300/10
USB 3.2 Gen 2 (3.1 Gen2) USB 10Gbps 10 Gb/s 1 128b/132b ~1210 10000 0x0320
USB 3.2 Gen 2x2 USB 20Gbps 2×10 Gb/s 2 128b/132b ~2420 20000 0x0320
USB4 Gen 2x2 USB 20Gbps 2×10 Gb/s 2 64b/66b ~2420 20000 0x0400
USB4 Gen 3x2 USB 40Gbps 2×20 Gb/s 2 128b/132b ~4850 — 0x0400
USB4 v2 Gen 4 USB 80Gbps 2×40 Gb/s 2 PAM-3 ~9700 — 0x0400

USB 2.0 i starsze

Tryb Linia speed
Low Speed 1.5 Mb/s 1.5
Full Speed 12 Mb/s 12
High Speed 480 Mb/s 480

Dlaczego Gen1→Gen2 to sedno problemu: 8b/10b (Gen1) ma 20% narzutu, ale jest odporne i prosto się trenuje. 128b/132b (Gen2) ma ~3% narzutu — ceną jest cięższy equalization (TSEQ) i dużo ciaśniejszy margines SI. Zejście na 5G "leczy", bo wraca do odporniejszego kodowania.

Lane'y = tylko Type-C. Warianty x2 potrzebują dwóch par TX/RX; na Type-A/microB SuperSpeed jest zawsze jednoliniowy → maks Gen2 10G.

W kernelu ignoruj marketing, patrz na twarde speed (5000/10000/20000) i bcdUSB. bcdUSB 0x0320 + speed 5000 pod obciążeniem = czarno na białym fallback z Gen2 do Gen1.


3. xHCI — model kontrolera

xHCI (eXtensible Host Controller Interface) — jeden kontroler obsługuje wszystkie prędkości (zastąpił UHCI/OHCI/EHCI). Sterowanie przez struktury w pamięci DMA, nie port I/O. Trzy filary:

  • Ringi (kolejki cykliczne):
    • command ring — jeden na kontroler (Enable Slot, Address Device, Configure EP)
    • transfer ring — jeden na endpoint (tu lądują TRB z URB)
    • event ring — kontroler tu produkuje Transfer/Command/Port Status events
  • Konteksty — DCBAA → Device Context (stan + do 31 Endpoint Contexts), Input Context (do zmian konfiguracji). Każde urządzenie = jeden slot.
  • Doorbell + MSI-X — sterownik dokłada TRB i "dzwoni" doorbellem; kontroler wykonuje, wystawia event, podnosi przerwanie.

Rejestry (MMIO)

Blok Zawiera
Capability RO: liczba portów/slotów, offsety
Operational USBCMD, USBSTS, CRCR, DCBAAP, PORTSC per port (tu PLS)
Runtime interruptery, event ring (ERSTBA, ERDP)
Doorbell po jednym na slot (slot 0 = command)

PORTSC/PLS = chleb powszedni przy problemie z linkiem.


4. URB i TRB

URB (USB Request Block) — programowa abstrakcja, struct urb, HCD-niezależna. Jedno żądanie na jednym endpoincie. Sterownik funkcji mówi wyłącznie URB-ami.

TRB (Transfer Request Block) — 16-bajtowy deskryptor sprzętowy z spec xHCI, żyje na ringu w DMA. Typy: Normal, Setup/Data/Status, Isoch, Link, Event Data, No-op.

xhci-hcd tłumaczy: 1 URB → 1..N TRB
  bulk >64KB / scatter-gather → wiele Normal TRB (chain bit)
  control                     → 3 TRB (Setup / Data / Status)
  isoc N pakietów             → N Isoch TRB
URB TRB
Warstwa usbcore / sterownik HCD (xHCI)
Definiuje kernel Linux spec xHCI (sprzęt)
Przenośność dowolny HCD tylko xHCI
Gdzie żyje pamięć kernela ring w DMA
Liczność 1 żądanie 1..N na URB

Cycle bit — bit własności na ringu; rozjazd producenta/konsumenta to klasyczny bug przy ręcznym grzebaniu w ringu.

Diagnostyka: usbmon/wireshark = poziom URB; debugfs xhci + tracepointy = poziom TRB/ringów/eventów.


5. LTSSM — warstwa łącza

LTSSM (Link Training and Status State Machine) rządzi linkiem SuperSpeed. Żyje poniżej URB/TRB/xHCI — najniższy programowo-widoczny poziom, tuż nad PHY. To tu objawia się problem z Gen2, zanim cokolwiek dotrze do warstwy URB.

stateDiagram-v2
    [*] --> RxDetect
    RxDetect --> Polling : wykrycie odbiornika
    Polling --> U0 : lock + equalization
    Polling --> Compliance : trening pada (SSP)
    U0 --> Recovery : błąd na łączu
    Recovery --> U0 : re-training OK
    U0 --> U1U2U3 : bezczynność (LPM)
    U1U2U3 --> Recovery : wyjście z U-state
    Recovery --> SSInactive : trening pada
    SSInactive --> [*] : fallback do USB2
    Compliance --> [*] : link martwy (test state)

    note right of Recovery
        Cykliczne U0 ↔ Recovery
        pod obciążeniem = marginalny link.
        PUNKT OBSERWACJI.
    end note
    note left of Compliance
        Stan dla sprzętu testowego.
        W polu = link martwy.
        Tu ląduje bug OAK4 10G
        (patrz sekcja 12).
    end note
Loading
  • U0 — link aktywny, dane płyną.
  • U1/U2/U3 — oszczędzanie energii (patrz LPM).
  • Recovery — re-training po błędach lub przy wyjściu z U-state. Sporadycznie = norma; cyklicznie pod obciążeniem = marginalny link.
  • SS.Inactive — trening padł twardo, link zjeżdża do USB2. Wymaga warm reset.
  • Compliance — stan ze specyfikacji dla sprzętu testowego, wchodzony z Polling gdy trening SSP się nie uda. W polu = link martwy (zero transferu). To tu ląduje bug OAK4 10G — patrz sekcja 12.

Podgląd: stan LTSSM = PLS (Port Link State) w PORTSC, czyli /sys/kernel/debug/usb/xhci/*/ports/* + tracepointy xhci-hcd.


6. Stos kernela

usbcore to framework-broker. Nikt nie rozmawia ze sprzętem wprost — wszystko przez usbcore, które trzyma model sterowników, robi enumerację i zarządza URB.

graph TD
    subgraph FN["Sterowniki funkcji / userspace"]
        UVC["uvcvideo → V4L2"]
        HID["usbhid → input"]
        LIB["libusb / usbfs<br/>(depthai, bez klasy)"]
    end
    subgraph CORE["usbcore — framework USB"]
        HUBD["Hub driver<br/>(hub_wq)"]
        ENUM["Enumeracja"]
        URBC["Rdzeń URB"]
        MODEL["Model sterown.<br/>(usb_bus_type)"]
    end
    subgraph HCD["HCD — sterownik kontrolera"]
        XHCI["xhci-hcd<br/>URB→TRB, ringi"]
        XPCI["xhci-pci<br/>glue + quirki"]
    end
    HW["Kontroler xHCI (sprzęt)<br/>PCI, MMIO, DMA, MSI-X"]
    FN --> CORE --> HCD --> HW
Loading

Model sterowników

  • Jeden usb_bus_type; rejestrują się na nim i urządzenia, i interfejsy.
  • Sterownik zwykle bindowany do interfejsu, nie urządzenia. Jedno usb_device = wiele usb_interface, każdy z osobnym sterownikiem (kamera UVC: video → uvcvideo, audio → snd-usb-audio).
  • Dwa typy: struct usb_driver (per interfejs, codzienny) i struct usb_device_driver (per urządzenie, rzadki — m.in. generyk dla usbfs).
  • Match po id_table: USB_DEVICE(vid,pid) albo USB_INTERFACE_INFO(cls,sub,proto).
  • Rejestracja: module_usb_driver() → usb_register_driver().

Anatomia sterownika funkcji

probe(intf, id):
    // przejrzyj endpointy: intf->cur_altsetting->endpoint[i].desc
    // usb_endpoint_is_bulk_in() / usb_rcvbulkpipe() ...
    // usb_alloc_urb() → bufory DMA → usb_set_intfdata()

praca:
    usb_fill_bulk_urb(); usb_submit_urb(GFP_*)   // wraca od razu
    // complete() leci w softirq (giveback); anchory: usb_anchor_urb()

disconnect():
    usb_kill_urb() / usb_kill_anchored_urbs()
    // MUSI przeżyć zniknięcie urządzenia (SS.Inactive w trakcie streamingu)

Sterownik funkcji nie wie nic o xHCI/TRB/ringach — stąd przenośność.

Kontrakt HCD

struct hc_driver = tablica callbacków: .urb_enqueue, .urb_dequeue, .add_endpoint, .address_device, .hub_control... usb_submit_urb() → usbcore → hcd->driver->urb_enqueue() = xhci_urb_enqueue() → rozbicie na TRB.

Podział: xhci-hcd (rdzeń: ringi, konteksty) + xhci-pci (glue: probe PCI, BAR-y, IRQ, usb_add_hcd(), quirki po PCI ID/DMI np. XHCI_COMP_MODE_QUIRK). Na SoC ten sam xhci-hcd dostaje xhci-plat.

USB nie ma własnego userspace

Sterowniki klasowe to mostki do innych podsystemów: uvcvideo→V4L2, usbhid→input, usb-storage→SCSI/block. Wyjątek: usbfs (/dev/bus/usb/) — libusb składa URB-y z userspace przez USBDEVFS_SUBMITURB (ścieżka depthai).

Konsekwencja diagnostyczna: uvcvideo loguje -EPROTO do dmesg; depthai/libusb dostaje błąd w userspace jako LIBUSB_ERROR_IO/_NO_DEVICE. Trzeba łapać oba końce.


7. Enumeracja

Prowadzi ją hub driver w kontekście hub_wq (workqueue budzony zmianą statusu portu).

graph TD
    A["Podłączenie<br/>hub → Port Status Change"] --> B["Reset portu<br/>ustala prędkość"]
    B --> C["GET_DESCRIPTOR 8B @ adr 0<br/>poznaje max packet EP0"]
    C --> D["SET_ADDRESS<br/>Enable Slot + Address Device"]
    D --> E["Deskryptory + SET_CONFIGURATION<br/>device/config/iface/endpoint"]
    E --> F["Dopasowanie sterownika<br/>probe: uvcvideo / libusb"]
Loading
  • Adres 0 to wąskie gardło — enumeracja jednego urządzenia naraz.
  • Prędkość ustala reset portu, nie deskryptory. Na SS to moment przejścia Polling+EQ → U0. Marginalny Gen2 może się tu w ogóle nie zenumerować jako SS → fallback do USB2 przed odczytem deskryptorów.
  • Match po interfejsie (bInterfaceClass/Protocol albo VID:PID). Depthai omija — bierze urządzenie surowo przez usbfs.

Gdzie pada (z dmesg):

  • device descriptor read/8, error -71 → krok 3 (link nie utrzymał control transferu)
  • device not accepting address → krok 4
  • cykliczne re-enumeracje po udanej konfiguracji → link wpada w Recovery/Inactive w trakcie streamingu, urządzenie znika, host enumeruje od nowa

8. LPM — Link Power Management

Usypianie linku dla oszczędności energii (stany U1/U2/U3 z LTSSM).

Stan Sen Wybudzenie
U0 aktywny —
U1 lekki (TX/RX off) µs
U2 głębszy wolniejsze
U3 suspend najwolniejsze

Klucz: wyjście z U1/U2 do U0 przechodzi przez Recovery — każde wybudzenie to mini-retrening. Na zdrowym Gen2 niewidoczne; na marginalnym link może się nie pozbierać i wpaść w pętlę Recovery.

  • Hardware LPM (U1/U2) — sprzęt sam wg wynegocjowanych progów; sterowane power/usb3_hardware_lpm_u1/_u2.
  • Software (U3/suspend) — autosuspend; sterowane power/control.

Dlaczego to kandydat na fix: LPM domyślnie ON, strojony pod energię nie stabilność. Kamery często deklarują zbyt optymistyczne exit-latency U1/U2 w BOS → host włącza agresywny LPM → wybudzenia się sypią. Urządzenie streamujące wideo i tak nigdy nie jest bezczynne, więc LPM daje głównie kłopoty. Wyłączenie = link siedzi w U0 stale, brak przejść power-state, brak tej klasy dropów.


9. Debug: metryki, wartości, instrumentacja

USB nie ma odpowiednika PCIe AER. Brak znormalizowanych liczników correctable/uncorrectable. Metryki linku SS się syntezuje — z completion code'ów w tracepointach, ze zmian PLS i z dmesg.

Metryki i progi

Metryka Źródło Dobrze Źle
Prędkość .../speed 10000 stabilnie spadek do 5000 pod ruchem
Lane'y .../rx_lanes,.../tx_lanes wg deklaracji mniej niż deklaruje
PLS portsc / tracepoint U0 pod ruchem Recovery cyklicznie; jakiekolwiek Inactive
Błędy transakcji completion code / dmesg -71 ~0 przy streamingu dowolny rate; >kilka/min = SI
Resety SS dmesg reset SuperSpeed 0 po enumeracji powtarzalne pod ruchem
Re-enumeracje dmesg connect/disconnect raz, zostaje pętle enumerate/disconnect
Isoc missed/underrun tracepointy xhci 0 dowolne = zgubione klatki
runtime PM .../power/runtime_status active gdy streamuje suspended w trakcie

Spec zakłada BER 1e-12 — na zdrowym Gen2 błędy transakcji są praktycznie niewidoczne. Rate błędów transakcji = Twój proxy na BER. Widać gołym okiem pod obciążeniem → margines przepalony.

Completion code / errno — słownik złych wartości

Completion code (xHCI) errno Znaczenie
4 — USB Transaction Error -71 (EPROTO) błąd na łączu = SI/link
3 — Babble Detected -75 za dużo danych / clock — często SI
6 — Stall -32 (EPIPE) endpoint/protokół, nie fizyka
timeout -62 (ETIME) / -110 link nie odpowiedział / zniknął
13 — Short Packet status 0, actual<len zwykle OK (normalny koniec)
Missed Service / Ring Under/Overrun — isoc timing — zgubione klatki

Workflow: różnice A/B, nie wartości absolutne

  1. Baseline spoczynek vs pełne obciążenie → błędy tylko pod ruchem = margines.
  2. Gen1 vs Gen2 — rate błędów na 5G vs 10G. Zero/niezero = potwierdzenie SI.
  3. LPM on vs off — spadek do zera po echo 0 = gałąź LPM, nie SI.
  4. Korelacja czasowa — nałóż PLS + błędy. U1→Recovery→błąd vs U0→Recovery→Inactive→warm reset rozstrzyga gałąź.

Instrumentacja — 3 poziomy

Poziom 1 — pasywny monitoring:

cat /sys/kernel/debug/usb/xhci/*/ports/port*/portsc    # PLS:U0 / Recovery / Inactive
echo 'module xhci_hcd +p' > /sys/kernel/debug/dynamic_debug/control
dmesg -w | grep -Ei 'error|reset|inactive|link|-71|-62'

Poziom 2 — ftrace:

ls /sys/kernel/debug/tracing/events/xhci-hcd/          # co dostępne w 5.15
echo 1 > /sys/kernel/debug/tracing/events/xhci-hcd/enable
echo 1 > /sys/kernel/debug/tracing/events/usb/usb_giveback_urb/enable
cat /sys/kernel/debug/tracing/trace_pipe | grep -Ei 'recovery|inactive|u1|u2|status=-'

Poziom 3 — bpftrace (metryki, których kernel nie daje):

# histogram statusów giveback — rate błędów wg typu
bpftrace -e 'tracepoint:usb:usb_giveback_urb { @[args->status] = count(); }'

# rate błędów co sekundę
bpftrace -e 'tracepoint:usb:usb_giveback_urb /args->status != 0/ {
  @err[args->status] = count(); }
  interval:1s { print(@err); clear(@err); }'

# latencja submit → giveback (ogon = sygnatura re-treningu)
bpftrace -e 't:usb:usb_submit_urb { @s[args->urb] = nsecs; }
  t:usb:usb_giveback_urb /@s[args->urb]/ {
    @lat = hist(nsecs - @s[args->urb]); delete(@s[args->urb]); }'

Sprawdź pola w .../events/usb/usb_giveback_urb/format — nazwy bywają różne między wersjami; urb służy jako klucz do sparowania submit z giveback.

Poziom poza zasięgiem software'u

Eye diagram, faktyczny BER, jitter — nie ma w sysfs. Gdy trzeba stroić fizykę: retimer/redriver (rejestry LOS/signal-detect/profil EQ przez i2c/mdio), PHY receiver margining (jeśli wspiera), analizator protokołu (Ellisys/LeCroy) lub scope z eye monitorem na retimerze — ostateczny arbiter "kabel vs ścieżka PCB".


10. Drzewo decyzyjne naprawy

Obie ścieżki kończą w Recovery, ale mają różne przyczyny i różny fix. SS.Inactive nigdy nie jest problemem LPM — to twardy pad treningu linku.

graph TD
    S["Pad 10G / link niestabilny"] --> Q0{"Ląduje w Compliance<br/>przy plug-inie?"}
    Q0 -->|TAK| ORIENT["Link training race<br/>orientacja USB-C / PD timing"]
    Q0 -->|NIE| Q{"Poprzedza je<br/>wejście w U1/U2?"}
    Q -->|TAK| LPM["LPM (U-states)<br/>power state gubi link"]
    Q -->|"NIE / SS.Inactive"| SI["Signal integrity<br/>trening linku pada"]
    ORIENT --> FIX0["compliance-recovery quirk<br/>DWC3: timeout-ms / xHCI: COMP_MODE_QUIRK"]
    LPM --> FIX1["Wyłącz LPM<br/>U1/U2, autosuspend, quirk NO_LPM"]
    SI --> FIX2["Napraw fizykę<br/>kabel, retimer, EQ, Gen1"]
Loading

Czwarty liść (Compliance): odrębny od LPM i SI. Link ląduje w stanie Compliance (martwy) zwykle przy pierwszym plug-inie, nie pod obciążeniem po godzinie pracy. Przyczyna to wyścig treningu — link trenuje, zanim ustali się orientacja USB-C / PD. Fix to timing quirk (recovery), nie kabel. Pełny rozbiór w sekcji 12.

Gałąź LPM (Recovery po U1/U2)

# 1. znajdź węzeł
lsusb -t ; ls /sys/bus/usb/devices/      # np. 3-1

# 2. wyłącz U1/U2 na żywo (test)
dev=3-1
echo 0 > /sys/bus/usb/devices/$dev/power/usb3_hardware_lpm_u1
echo 0 > /sys/bus/usb/devices/$dev/power/usb3_hardware_lpm_u2
echo on > /sys/bus/usb/devices/$dev/power/control

# 3. utrwal quirkiem NO_LPM (flaga 'k'); VID Movidius = 03e7
#    (sysfs nie przeżywa replug/reboot)
usbcore.quirks=03e7:XXXX:k               # w cmdline
usbcore.autosuspend=-1                    # globalnie, na czas testów

Uwaga na TLP / powertop --auto-tune — potrafią odkręcić autosuspend w tle.

Gałąź SI (SS.Inactive / Recovery znikąd pod ruchem)

Tego nie naprawisz z sysfs. Od najtańszego:

  1. Kabel — certyfikowany 10G, krótki (<1 m), reseat. #1 w terenie.
  2. Retimer/redriver — konfiguracja/profil na płycie (częsty problem na bring-upie).
  3. PHY tuning — EQ, de-emphasis, TX swing w rejestrach SerDes.
  4. PCB — długość/dopasowanie par, przelotki, masy.
  5. Zostań na Gen1 — świadomy workaround, jeśli 5G daje dość pasma.

Rzadki trzeci przypadek: kontroler błędnie wpada w Compliance/Inactive bez ruchu, zaraz po enumeracji, też na Gen1 → sprawdź XHCI_COMP_MODE_QUIRK.


11. Ściągawki

Ścieżki sysfs / debugfs

/sys/bus/usb/devices/<dev>/speed                      # 5000 / 10000
/sys/bus/usb/devices/<dev>/rx_lanes , tx_lanes
/sys/bus/usb/devices/<dev>/bcdUSB                      # 0x0320 = Gen2
/sys/bus/usb/devices/<dev>/power/usb3_hardware_lpm_u1  # LPM U1
/sys/bus/usb/devices/<dev>/power/usb3_hardware_lpm_u2  # LPM U2
/sys/bus/usb/devices/<dev>/power/control              # autosuspend (on = off)
/sys/bus/usb/devices/<dev>/power/runtime_status
/sys/kernel/debug/usb/xhci/<hc>/ports/port*/portsc    # PLS
/sys/kernel/debug/tracing/events/xhci-hcd/
/sys/kernel/debug/tracing/events/usb/

Mapa "warstwa → narzędzie obserwacji"

Warstwa Obiekt Narzędzie
Fizyka / PHY eye, BER, jitter scope, analizator, retimer regs
Łącze (LTSSM) PLS, Recovery, Inactive portsc, tracepointy xhci-hcd
xHCI TRB, ringi, eventy debugfs xhci, tracepointy
URB / protokół transakcje, status usbmon, wireshark, usb_giveback_urb
Funkcja klatki, IO dmesg (klasa) / logi userspace (libusb)

Cheatsheet cmdline

usbcore.quirks=VID:PID:k     # k = USB_QUIRK_NO_LPM
usbcore.autosuspend=-1       # globalnie wyłącz autosuspend

12. Case study: OAK4 10G / DWC3 compliance mode

Zadanie: "zbadaj, czemu USB 10Gbps czasem sprawia problemy" na platformie Luxonis RVC4 / OAK4 (fork linux-msm-5.15). Konkretna instancja całej teorii powyżej — z jedną istotną korektą modelu.

Korekta platformy: to nie xHCI, to DWC3 — i głównie strona device

Cały stos z sekcji 6 zakładał xHCI po stronie hosta. RVC4 to co innego:

  • kontroler = DWC3 (Synopsys DesignWare, dual-role), nie samodzielne xHCI;
  • właściwy glue = downstreamowy dwc3-msm-core.c (7500+ linii, Qualcomm), nie mainline dwc3-qcom.c (ten jest tu martwym kodem — wzorzec "decoy");
  • OAK4 występuje w dużej mierze jako urządzenie (gadget) — wystawia się hostowi jako UVC (f_uvc.c) i sieć-over-USB (u_ether.c).

Framework się przenosi mimo to: DWC3 w trybie host = interfejs xHCI (więc sekcje 3–4 wciąż obowiązują, gdy DWC3 hostuje np. hub qps615), a LTSSM/Compliance/rejestry stanu łącza są symetryczne — obie strony linku prowadzą własną maszynę stanów.

graph TD
    HOST["Host: xhci-hcd<br/>stos z sekcji 3-6 (hub qps615)"] --> DWC3
    DEV["Device: gadget UVC<br/>f_uvc, configfs — OAK jako kamera"] --> DWC3
    DWC3["DWC3 core + dwc3-msm-core.c<br/>glue Qualcomma, własny SM compliance"] --> LTSSM
    LTSSM["Warstwa łącza SS — LTSSM<br/>Compliance = pad treningu SSP"] --> PHY
    PHY["PHY / USB-C<br/>orientacja, PD tps25750, brak SS mux"]
Loading

Bug

Commit Luxonisa 78a89311afa8 ("Fix the controller entering compliance mode") dokumentuje: kontroler DWC3 wchodzi w compliance mode przy pierwszym plug-inie, odtwarzalne na hostach z SSP (= SuperSpeedPlus = 10G) i bez PD. DWC3_LINK_STATE_CMPLY to stan LTSSM ze specyfikacji USB3 przeznaczony dla sprzętu testowego — w polu oznacza martwy link. To mapuje się jeden do jednego na objaw "10G czasem nie działa" i na czwarty liść drzewa.

Słowniczek warstwy fizycznej USB-C

Terminy potrzebne, żeby zrozumieć root cause poniżej.

  • Para SS — para różnicowa (dwa przewody, sygnał = różnica napięć). Jeden kierunek = jedna para: TX+/TX− do nadawania, RX+/RX− do odbioru. Jeden lane SS = dwie pary (TX + RX) = 4 przewody, pełny dupleks.
  • Dwa komplety par w USB-C — złącze jest odwracalne, więc fizycznie ma dwa komplety par SS: lane 1 (TX1/RX1) i lane 2 (TX2/RX2), po przeciwnych stronach wtyku. Zwykłe USB 3.2 Gen1/Gen2 (5G/10G) to jeden lane — używa naraz tylko kompletu zgodnego z orientacją wtyku. Drugi jest wtedy nieaktywny. (Gen2x2 20G używa obu → tylko Type-C.)
  • SS mux — przełącznik 2:1 (crosspoint), który wybiera, który komplet par podłączyć do jednego PHY SS kontrolera, wg wykrytej orientacji. Bywa osobnym układem albo zintegrowany w retimerze/redriverze lub w kontrolerze PD. Bez niego kontroler z jednym PHY nie obsłuży obu orientacji czysto.
  • PD / CC / tps25750 — USB Power Delivery negocjuje zasilanie po liniach CC (Configuration Channel). Ten sam układ CC/PD wykrywa orientację wtyku (po tym, która linia CC1/CC2 jest podciągnięta) i mówi muxowi, którą parę wybrać. tps25750 to układ PD od TI. Detekcja zajmuje czas po wpięciu — nie jest natychmiastowa.
  • Pływająca para — para elektrycznie niepodłączona do niczego określonego, w stanie wysokiej impedancji (high-Z), bez sygnału. W połączeniu USB-C tylko jeden komplet niesie sygnał; drugi często wisi (daleki koniec kabla niepodłączony). Trening linku na pływającej parze nie ma jak się udać — nie ma nadajnika po drugiej stronie.

Pinout — gdzie te pary siedzą fizycznie:

Pin 1 2 3 4 5 6 7 8 9 10 11 12
A GND TX1+ TX1− VBUS CC1 D+ D− SBU1 VBUS RX2− RX2+ GND
B GND TX2+ TX2− VBUS CC2 D+ D− SBU2 VBUS RX1− RX1+ GND
  • Lane 1 = para TX1 (A2/A3) + para RX1 (B10/B11)
  • Lane 2 = para TX2 (B2/B3) + para RX2 (A10/A11)
  • CC1/CC2 = linie orientacji (stąd PD ją odczytuje)

Lane 1 leży po jednej przekątnej (TX góra-lewo, RX dół-prawo), lane 2 lustrzanie. Flip wtyku o 180° zamienia rzędy A↔B → lane 1 i lane 2 zamieniają się rolami. Dlatego są dwa komplety par: żeby oba włożenia wtyku dały działający link. DWC3 ma jeden PHY SS (1 para TX + 1 RX), więc w danej orientacji jeden komplet jest aktywny, drugi pływa — i to na tym pływającym trening pada w Compliance.

graph LR
    P1["Para SS #1<br/>TX1/RX1 — aktywna"] --> MUX
    P2["Para SS #2<br/>TX2/RX2 — pływa (high-Z)"] -.-> MUX
    MUX["SS mux 2:1<br/>wybiera parę"] --> PHY["DWC3 PHY SS<br/>1 para TX + 1 RX"]
    PD["PD tps25750<br/>orientacja z CC"] -.->|steruje| MUX
Loading

Normalnie mux wybiera aktywną parę wg orientacji z CC. Bug OAK4: brak muxa + PD nie ustali orientacji przed treningiem → DWC3 trenuje na złej/pływającej parze → Compliance. "Pierwszy plug-in" = wyścig na starcie; "hosty z SSP" = dopiero trening 10G jest tak wrażliwy. Quirk compliance-recovery-timeout-ms odczekuje, aż orientacja się ustali, i restartuje trening — łata na ten wyścig.

Root cause (oznaczony w źródle jako niepotwierdzony)

Teoria z commita: gdy obie pary SS są podłączone do kontrolera, a płyta nie ma SS-muxa, i detekcja orientacji USB-C (PD tps25750) nie zdąży się ustalić przed pierwszym treningiem — DWC3 trenuje na złej/pływającej parze SS → Compliance.

Obejście istnieje, ale jest uśpione na OAK4

Łata to delayed check-and-restart: po resecie odczekaj compliance_recovery_timeout_ms, przeczytaj DSTS, jeśli link state = CMPLY, wymuś pełny restart USB. Włączane przez devicetree:

&usb0 {
    qcom,compliance-recovery-timeout-ms = <300>;
};

Ale: DTS OAK4 (kalamap-oak4s_r0-overlay.dts → dziedziczy z kalamap-rb5-gen2.dtsi) ustawia redriver SS i tuning eUSB2 PHY, lecz nie ustawia tego knoba. Znaleziony włączony tylko na referencyjnej płycie Qualcomma (kalama-jf6961.dtsi, 450 ms) — nie na produkcie Luxonisa. Fix na tę dokładną klasę buga istnieje w sterowniku i jest martwy na Twojej płycie.

Mapowanie rejestrów: host ↔ device

Ta sama warstwa LTSSM, inny rejestr zależnie od strony:

Strona Rejestr Pole Wartość "martwy link"
Host (xHCI) PORTSC PLS Compliance / Inactive
Device (DWC3) DWC3_DSTS USBLNKST DWC3_LINK_STATE_CMPLY

grep po DWC3_LINK_STATE_CMPLY i USBLNKST w core.h daje enum i offset.

Ścieżka potwierdzenia (potwierdź przed naprawą)

  1. Potwierdź CMPLY — dmesg z awarii + odczyt DWC3_DSTS.USBLNKST w momencie padu (debug print). CMPLY = teoria potwierdzona. Inny stan (Recovery/SS.Inactive) → wróć do gałęzi LPM/SI z sekcji 10.
  2. A/B Gen1/Gen2 — czy na wymuszonym 5G Compliance znika. Odróżnia "trening SSP pada" od głębszego problemu.
  3. Dopiero potem włącz uśpiony quirk na DTS OAK4 (300 ms) i sprawdź, czy intermittentne padnięcia znikają.

Plaster vs fix: quirk to recovery — wykrywa Compliance i restartuje link, więc zamaskuje objaw nawet przy innej przyczynie. Właściwy fix wymaga potwierdzenia CMPLY (krok 1) i korelacji z momentem ustalania orientacji PD. Jeśli root cause to naprawdę timing tps25750, docelowo lepszy jest fix po stronie sekwencji orientacja→trening niż stały timeout.

Powiązane commity Luxonisa (drivers/usb/, @luxonis.com)

Commit Obszar Opis
78a89311afa8 dwc3-msm fix wchodzenia w compliance mode
7e1ecf5762f0 dwc3-msm init compliance_recovery_timeout_ms=0 bez DTS
0a5dc85279a8 dwc3 core hardening teardown gadgetu na races suspend/resume
3fe3466c5c6d typec/tipd maskowanie IRQ w tps25750 (PD controller)
ffc08081698f gadget uvc port zmian UVC z kernela RPi

Kolejność debugowania: potwierdź fallback (speed) → skoreluj PLS z błędami pod obciążeniem → A/B Gen1/Gen2 i LPM on/off → wskazuje gałąź i mierzy skuteczność fixu, zanim zejdziesz do retimera/scope.

Case OAK4: potwierdź USBLNKST=CMPLY przy plug-inie → A/B Gen1/Gen2 → włącz uśpiony qcom,compliance-recovery-timeout-ms na DTS OAK4. Root cause (orientacja/PD timing) pozostaje do potwierdzenia — quirk to recovery, nie właściwy fix.

Based on axxia

Device Tree: dr_mode = "host"
        │
        ▼
┌─────────────────────────────────────────────────────────┐
│ DWC3 core.c  (DWC3 = DesignWare USB3 — blok IP Synopsys) │
│                                                           │
│  dwc3_probe()                                            │
│      │                                                    │
│      ├─ dwc3_get_dr_mode()   ← czyta/weryfikuje dr_mode  │
│      │  (dr_mode = "dual role mode": host/peripheral/otg)│
│      │                                                    │
│      ├─ dwc3_core_init()     ← init rejestrów, PHY       │
│      │                                                    │
│      └─ dwc3_core_init_mode()                            │
│               │                                          │
│               │  (bo dr_mode == HOST)                    │
│               ▼                                          │
│         dwc3_host_init()   [host.c]                      │
└─────────────────────────────────────────────────────────┘
        │
        │  platform_device_alloc("xhci-hcd", ...)
        ▼
┌─────────────────────────────────────────────────────────┐
│ nowe urządzenie platformowe: "xhci-hcd"                  │
│ (rodzic = urządzenie DWC3, dostaje zasoby MMIO + IRQ)    │
└─────────────────────────────────────────────────────────┘
        │
        │  binduje się do sterownika
        ▼
┌─────────────────────────────────────────────────────────┐
│ xhci-plat.c  (glue driver dla xHCI na platform_device)   │
│      │                                                    │
│      ▼                                                    │
│ xhci.c  (rdzeń logiki zgodnej ze specyfikacją xHCI)      │
│                                                           │
│ xHCI = eXtensible Host Controller Interface              │
│ (standard Intela: jak software steruje kontrolerem       │
│  hosta USB — enumeracja portów, transfery, zarządzanie   │
│  deskryptorami TRB itd.)                                 │
└─────────────────────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────────────────────┐
│ usbcore  (ogólna warstwa USB w jądrze Linuksa)           │
│      │                                                    │
│      ▼                                                    │
│ realne urządzenia USB podłączone do portu                │
│ (pendrive, kamera, hub, itd. — enumerowane standardowo)  │
└─────────────────────────────────────────────────────────┘

Czyli finalnie: dr_mode → dwc3_host_init → urządzenie "xhci-hcd" → xhci_plat_probe → dwa usb_hcd (USB2 + USB3) → usbcore → realne urządzenie na porcie

xhci_plat_probe()
    └─ usb_add_hcd(hcd, irq, ...)
           └─ (standardowy callback hc_driver->reset)
                  └─ xhci_init()   [xhci.c:574]
                         └─ xhci_mem_init()   [xhci.c:589 → xhci-mem.c:2359]
                                ├─ alokuje DCBAA
                                ├─ xhci_ring_alloc(..., TYPE_COMMAND, ...)  → cmd_ring
                                └─ xhci_ring_alloc(..., TYPE_EVENT, ...)   → event_ring

Linux kernel for DWC3 https://docs.kernel.org/driver-api/usb/dwc3.html

⚠️ **GitHub.com Fallback** ⚠️