Denník „kernel developera“

03.07.2022 | 06:12 | Mirecove dristy | Miroslav Bendík

Článok bol prvý kárt uverejnený na wisdomtech.sk.
Tento príbeh je o riešení jednej nepríjemnej vlastnosti trackpointu a o zdĺhavej premene jedného obyčajného web developera na kernel developera. V článku sú opísané čiastkové kroky, ale aj slepé uličky, ktorými som sa nakoniec dostal k funkčnému riešeniu.

Opis problému

Frekvencia posielania polohy kurzoru sa výrazne znižuje, keď používam priblížim ruku k touchpadu, alebo keď používam touchpad ako opierku dlane. V nasledujúcom výpise je výstup programu evhz:

TPPS/2 Elan TrackPoint: Latest    26Hz, Average    36Hz
TPPS/2 Elan TrackPoint: Latest    47Hz, Average    36Hz
TPPS/2 Elan TrackPoint: Latest    43Hz, Average    36Hz
TPPS/2 Elan TrackPoint: Latest    47Hz, Average    36Hz
TPPS/2 Elan TrackPoint: Latest    33Hz, Average    36Hz
TPPS/2 Elan TrackPoint: Latest    34Hz, Average    36Hz
TPPS/2 Elan TrackPoint: Latest    28Hz, Average    36Hz
TPPS/2 Elan TrackPoint: Latest    51Hz, Average    36Hz

Pri tak nízkej frekvencii kurzor pri pohybe výrazne seká. Zastavenie kurzora na presnej pozícii je výrazne ťažšie, pretože pohyb je nerovnomerný.

Hardvér, alebo softvér

Skôr než som sa pustil do práce som musel zistiť, či je problém hardvérový, alebo softvérový. Našťastie som nechal dualboot s Windowsom, takže stačilo reštartovať a vyskúšať. Vo windowse sa kurzor pohybuje plynulo bez ohľadu na to, či sa opieram dlaňou o touchpad, alebo nie.

S týmto problémom som sa stretol aj u starších thinkpadov, menovite napríklad T420. Tam sa dal problém vyriešiť jednoducho vypnutím touchpadu v BIOSe. Môj nový thinkpad má síce túto možnosť v BIOSe, ale tá nefunguje. Staršie modely skutočne vypínali touchpad, ale v nových modeloch sa iba uloží príznak do NVRAM a je na operačnom systéme, aby sa s tým vysporiadal.

Čo fyzické odpojenie touchpadu? Pri práci používam iba trackpoint, takže fyzické odpojenie touchpadu by mi nevadilo. Ani toto riešenie nie je schodné. Po odpojení flex kábla touchpadu totiž prestal fungovať aj trackpoint. Trackpoint nie je pripojený priamo na matičnú dosku, ale putuje najskôr do touchpadu, kde sa multiplexuje. Tento dizajn je nevyhnutný napríklad pre touchpady, ktoré nemajú fyzické tlačidlá (T440). Tu síce máme fyzické tlačidlá, ale zapojenie je rovnaké.

Konektor
Obrázok 1: Konektor touchpadu (ľavý modrý)

Hľadanie príčiny

Najskôr je vhodné identifikovať modul jadra, ktorý by mal obsluhovať zariadenie. Väčšina zariadení sa dá identifikovať nástrojom lshw, kde sú prehľadne zobrazené zariadenia. Zariadenie je buď rozpoznané a lshw zobrazí modul jadra, ktorý ho obsluhuje, alebo nie je rozpoznané a je potrebné zistiť podporu pomocou zadania ID zariadenia do internetového vyhľadávača. Pri trackpointe viem, že niektorý modul ho obsluhuje. Takto vyzerá výpis lshw:

  *-input:2
       product: TPPS/2 Elan TrackPoint
       physical id: 4
       logical name: input13
       logical name: /dev/input/event12
       capabilities: i8042

V tomto výpise síce nie je priamo zobrazený modul jadra, ale i8042 je kontrolér PS/2. Letmý pohľad na zavedené moduly (lsmod) prezradí, že sa tam nachádza modul psmouse. Po odstránení modulu (modprobe -r psmouse) prestane trackpoint reagovať. Po opätovnom zavedení (modprobe psmouse) opäť funguje, čo znamená, že som našiel správny modul.

Vo výpise dmesg pribudli nasledujúce riadky:

psmouse serio1: synaptics: queried max coordinates: x [..5678], y [..4694]
psmouse serio1: synaptics: queried min coordinates: x [1266..], y [1162..]
psmouse serio1: synaptics: Your touchpad (PNP: LEN2073 PNP0f13) says it can support a different bus. If i2c-hid and hid-rmi are not used, you might want to try setting psmouse.synaptics_intertouch to 1 and report this to linux-input@vger.kernel.org.
psmouse serio1: synaptics: Touchpad model: 1, fw: 10.32, id: 0x1e2a1, caps: 0xf01ea3/0x940300/0x12e800/0x500000, board id: 3471, fw id: 3418235
psmouse serio1: synaptics: serio: Synaptics pass-through port at isa0060/serio1/input0
input: SynPS/2 Synaptics TouchPad as /devices/platform/i8042/serio1/input/input20
psmouse serio3: trackpoint: Elan TrackPoint firmware: 0x12, buttons: 3/3
input: TPPS/2 Elan TrackPoint as /devices/platform/i8042/serio1/serio3/input/input21

Podľa všetkého je touchpad pripojený k 2 zberniciam súčasne. Aktuálne sa používa rozhranie PS/2, cez ktoré sa prenášajú pakety maximálnou frekvenciou 80Hz, alebo 40Hz pri pri striedaní každého druhého packetu z touchpadu / trackpointu. Výpis zároveň ponúka jednoduché riešenie, ktorým by sa celý tento príbeh mohol skončiť. Ovládač kontroluje, či touchpad podporuje alternatívny protokol, ale zároveň má whitelist podporovaných touchpadov.

Je tu veľká nádej, že problém sa podarí vyriešiť pomocou pokynov z výpisu. Podľa neho by malo stačiť skontrolovať, či náhodou nie sú zavedené moduly i2c-hid a hid-rmi. Ak áno, mali by byť odstránené (modprobe -r i2c-hid hid-rmi). Následne stačí zaviesť psmouse s parametrom synaptics_intertouch=1. V prípade, že to funguje, je dobré to nahlásiť na adresu linux-input@vger.kernel.org, aby sa zariadenie pridalo do whitelistu.

modprobe -r psmouse
modprobe psmouse synaptics_intertouch=1

Na mojom notebooku to nepomohlo. Výpis dmesg zostal rovnaký až na tieto 2 riadky:

psmouse serio1: synaptics: Trying to set up SMBus access
psmouse serio1: synaptics: SMbus companion is not ready yet

Hľadanie príčiny začínam príkazom grep -r "SMbus companion is not ready yet" /usr/src/linux v zdrojových kódoch jadra. Týmto spôsobom som našiel riadok riadok s vypisom. Výpis sa zobrazí, ak zlyhá funkcia synaptics_create_intertouch:

static int synaptics_create_intertouch(struct psmouse *psmouse,
                                       struct synaptics_device_info *info,
                                       bool leave_breadcrumbs)
{
        // ...
        const struct i2c_board_info intertouch_board = {
                I2C_BOARD_INFO("rmi4_smbus", 0x2c),
                .flags = I2C_CLIENT_HOST_NOTIFY,
        };

        return psmouse_smbus_init(psmouse, &intertouch_board,
                                  &pdata, sizeof(pdata), true,
                                  leave_breadcrumbs);
}

Funkcia psmouse_smbus_init skončí neúspechom, pretože i2c_for_each_dev(smbdev, psmouse_smbus_create_companion) nenájde žiadne zariadenie. Úlohou tejto funkcie je nájsť zariadenie s adresou 0x2c (toto je adresa touchpadu značky synaptics) vyhovujúce funkcii psmouse_smbus_create_companion.

Debugger

Aby som zistil, čo sa deje vo funkcii psmouse_smbus_create_companion použijem jeden z najprimitívnejších spôsobov ladenia - vloženie výpisov do konzoly. V C sa na výpis používa funkcia printf. V kerneli sa používa veľmi podobná funkcia printk. Drobný rozdiel je hlavne v určení úrovne závažnosti výpisu (KERN_INFO, KERN_WARNING…). V nasledujúcom kóde je upravená funkcia psmouse_smbus_create_companion s pridanými výpismi.

static int psmouse_smbus_create_companion(struct device *dev, void *data)
{
        struct psmouse_smbus_dev *smbdev = data;
        unsigned short addr_list[] = { smbdev->board.addr, I2C_CLIENT_END };
        struct i2c_adapter *adapter;
        struct i2c_client *client;

        adapter = i2c_verify_adapter(dev);
        if (!adapter) {
                printk(KERN_INFO "%s Adapter error\n", dev_name(dev));
                return 0;
        }

        if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_HOST_NOTIFY)) {
                printk(KERN_INFO "%s No host notify\n", dev_name(dev));
                return 0;
        }

        client = i2c_new_scanned_device(adapter, &smbdev->board,
                                        addr_list, NULL);
        if (IS_ERR(client)) {
                printk(KERN_INFO "%s New device error\n", dev_name(dev));
                return 0;
        }

        /* We have our(?) device, stop iterating i2c bus. */
        smbdev->client = client;
        return 1;
}

Po zavedení modulu sa vo výstupe dmesg objavilo:

i2c-0 No host notify
i2c-1 No host notify
i2c-2 No host notify
i2c-3 No host notify
i2c-4 No host notify
i2c-5 No host notify
i2c-6 No host notify
i2c-7 No host notify
i2c-8 No host notify
i2c-9 No host notify
i2c-10 No host notify
i2c-11 No host notify

Žiaden I2C / SMBus adaptér v mojom systéme nemá podporu I2C_FUNC_SMBUS_HOST_NOTIFY. To je dôvod, prečo sa neinicializuje touchpad cez I2C / SMBus.

I2C / SMBus

Pred ďalším pokračovaním je vhodné vysvetliť si pojmy I2C, SMBus, aký vzťah je medzi nimi a prečo sa často zamieňajú.

Zbernica I2C slúži na prenos dát s pomocou 2 vodičov. Každá správa začína najskôr 7-bitovou adresou nasledovanou príznakom čítania / zápisu. Pre pokračovanie komunikácie je oslovené zariadenie povinné odpovedať príznakom ACK. Nasleduje ľubovoľne dlhá sekvencia čítaní a zápisov 8-bitových slov.

Rovnako funguje aj SMBus, len s jedným rozdielom - namiesto ľubovoľnej štruktúry správy má SMBus povolené len niektoré typy a dĺžky. SMBus je teda podmnožinou I2C a podporuje len nasledujúce správy:

Príkaz quick je správa nulovej dĺžky. Na zbernicu sa pošle len adresa, R/W príznak a prečíta sa príznak ACK. To je presne tá istá sekvencia, ktorou začína každá komunikácia na I2C zbernici. Vďaka tomu sa dá pomocou quick príkazu zistiť, či je zariadenie pripojené na zbernicu. Postupným odosielaním quick príkazu na adresy 0-127 sa dá skenovať zoznam zariadení pripojených na zbernici.

Zostáva ešte zistiť, čo je SMBUS_HOST_NOTIFY.

Host notify prerušenia

Dovolím si spraviť malú odbočku k prerušeniam. Udalosti od externého zariadenia sa dajú spracovať dvoma spôsobmi - periodickým dotazovaním a prerušením.

Pri periodickom dotazovaní sa operačný systém pravidelne pýta zariadenia, či nastala nejaká udalosť. Tento spôsob má vyššiu latenciu, pretože zariadenie musí čakať na na moment, keď sa ho operačný systém spýta na stav. Zároveň je časté prebúdzanie CPU zo šetriaceho režimu energeticky náročné a nevhodné pre notebooky.

Riešením je prerušenie. Pri udalosti zariadenie požiada procesor o obsluhu len keď nastala udalosť (pohyb myši, stlačenie klávesy …). Udalosť sa spracuje bez čakania na nasledujúci interval. Procesor môže byť počas celej doby medzi udalosťami v šetriacom režime.

Protokol SMBus host notify je špeciálny formát správy, ktorú posiela zariadenie žiadajúce o obsluhu prerušenia. Je opísaný v špecifikácii SMBus v kapitole 6.5.9 (strana 44). Správa začína adresou SMBus Host, čo je špeciálna adresa definovaná v tabuľke 17 appendixu C (strana 79). Po host adrese nasleduje adresa zariadenia žiadajúceho o prerušenie a voliteľne 2 byty s dátami. Ak chce synaptics touchpad (s adresou 0x2c) požiadať o obsluhu prerušenia, pošle najskôr host adresu (0x08) a potom vlastnú adresu (0x2c).

ACPI

Na notebooku mám niekoľko I2C / SMBus zberníc. Potreboval som presne zistiť, ku ktorej je pripojený trackpoint. Podľa výpisu z dmesgu sa trackpoint identifikuje ako Elan TrackPoint.

Informácie o hardvéri sa na x86 dajú zvyčajne získať z ACPI. Štandard ACPI nie je len jednoduchá tabuľka obsahujúca zoznam hardvéru. Je to veľmi komplexný štandard. Zoznam zariadení nemusí byť statický, ale môže obsahovať aj spustiteľný kód - ACPI Machine Language - AML.

ACPI tabuľky sú dostupné v adresári /sys/firmware/acpi/tables/. Nasledujúce príkazy skopírujú ACPI tabuľky do pracovného adresára a dekompiluje ich do .dsl zdrojového kódu:

cp -R /sys/firmware/acpi/tables/* .
for file in *; do iasl -d $file; done

Celý obsah ACPI tabuliek a dekompilované zdrojové kódy sú priložené k blogu v súbore p14s_gen2_amd_acpi_tables.tar.xz.

Hlavná tabuľka je DSDT.dsl. Zaujímavo vyzerá zbernica _SB.I2CB. Nachádza sa tam niekoľko zariadení s názvom začínajúcim sa na ELAN. V AML kóde je definovaná metóda _INI, ktorá na základe príznakov z BIOS-u vyberie konkrétny model ELAN* zariadenia. Je tu definovaná aj metóda _STA, ktorej úlohou je vrátiť stav zariadenia. Pre funkčné zariadenie by mala vrátiť minimálne bitovú masku 0xB (väčšinou to bude 0xF). Význam jednotlivých bitov je nasledovný:

Scope (_SB.I2CB)
{
    Device (TPNL)
    {
        Name (_HID, "XXXX0000")  // _HID: Hardware ID
        Name (_CID, "PNP0C50" /* HID Protocol Device (I2C bus) */)  // _CID: Compatible ID
        Name (_S0W, 0x03)  // _S0W: S0 Device Wake State
        Name (HID2, 0x00)
        Name (POIO, 0x00)
        Name (SBFB, ResourceTemplate ()
        {
            I2cSerialBusV2 (0x0000, ControllerInitiated, 0x00061A80,
                AddressingMode7Bit, "\\_SB.I2CB",
                0x00, ResourceConsumer, _Y0C, Exclusive,
                )
        })
        Name (SBFG, ResourceTemplate ()
        {
            GpioInt (Level, ActiveLow, ExclusiveAndWake, PullNone, 0x0000,
                "\\_SB.GPIO", 0x00, ResourceConsumer, ,
                )
                {   // Pin list
                    0x0005
                }
        })
        CreateWordField (SBFB, \_SB.I2CB.TPNL._Y0C._ADR, BADR)  // _ADR: Address
        CreateDWordField (SBFB, \_SB.I2CB.TPNL._Y0C._SPE, SPED)  // _SPE: Speed
        Name (ITML, Package (0x0A)
        {
            Package (0x07)
            {
                0x04F3,
                0x2A3B,
                0x10,
                0x01,
                0x01,
                "ELAN901C",
                0x01
            },
            // ...
        })

        // ...

        Method (_STA, 0, NotSerialized)  // _STA: Status
        {
            If (((PNVD == 0x00) || (PNPD == 0x00)))
            {
                Return (0x00)
            }

            If ((TPOS >= 0x60))
            {
                Return (0x0F)
            }
            Else
            {
                Return (Zero)
            }
        }
    }
}

Je dobré skontrolovať si, ACPI vráti, že zariadenie je pripojené. Ak má kernel skompilovanú podporu ACPI_DEBUGGER a ACPI_DEBUGGER_USER je možné použiť utilitku acpidbg. Kiež by som o nej vedel dávnejšie … V každom prípade acpidbg som našiel neskôr, než som sa celý deň trápil s modulom acpi_call. Utilitka acpidbg je celkom príjemné command line rozhranie k ACPI. Pomoc sa dá vypísať príkazom help. Ja potrebujem spustiť metódu _STA. Príkazom find nájdem všetky objekty s touto metódou:

- find _STA
…
     \_SB.I2CA._STA Method       0000000069360f17 001 Args 0 Len 0012 Aml 0000000038f2dff5
\_SB.I2CA.NFC1._STA Method       00000000faf2813c 001 Args 0 Len 0023 Aml 000000009fb7f0d9
     \_SB.I2CB._STA Method       00000000d6066f3a 001 Args 0 Len 001D Aml 00000000f303d8db
\_SB.I2CB.TPNL._STA Method       000000004272e64a 001 Args 0 Len 0025 Aml 00000000fa4f29fe
…

Metóda sa spúšťa príkazom execute:

Evaluating \_SB.I2CB.TPNL._STA
Evaluation of \_SB.I2CB.TPNL._STA returned object 000000007b765997, external buffer length 18
 [Integer] = 0000000000000000

Takže zariadenie podľa metódy _STA vôbec nie je v systéme nainštalované. Buď je to chyba v ACPI, alebo je potrebné niečo urobiť pre zapnutie zariadenia (napríklad aktivovať GPIO), alebo pozerám na zlú zbernicu. Pre istotu som si pozrel, čo by mohlo byť zariadenie ELAN901C. Google hovorí, že je to kontrolér dotykovej vrstvy. Z tohto zistenia predpokladám, že k portu I2CB býva na niektorých notebookoch pripojený kontrolér dotykovej vrstvy. Ja dotykovú vrstvu nemám, preto je logické že výsledkom volania _STA je 0x0.

Prehľadanie ACPI tabuliek ma k nájdeniu zbernice touchpadu nepriviedlo. Môžem skúsiť opačný postup - vyhľadám SMBus a zistím, čo je k nemu pripojené. V ACPI preto vyhľadávam text 'SMB' a nachádzam nasledujúci záznam:

Scope (_SB.PCI0)
{
    Device (SMB1)
    {
        Name (_HID, "SMB0001")  // _HID: Hardware ID
        Name (_CRS, ResourceTemplate ()  // _CRS: Current Resource Settings
        {
            IO (Decode16,
                0x0B20,             // Range Minimum
                0x0B20,             // Range Maximum
                0x20,               // Alignment
                0x20,               // Length
                )
            IRQ (Level, ActiveLow, Shared, )
                {7}
        })
        Method (_STA, 0, NotSerialized)  // _STA: Status
        {
            Return (0x0F)
        }
    }
}

Pripojené zariadenie tu nevidím, ale sekcia obsahuje iné zaujímavé informácie. V prvom rade je to Hardware ID SMB0001. K tomu sa o chvíľu dostaneme. Ďalej tu máme adresu zariadenia 0x0B20 a informáciu, že zariadenie používa prerušenie 7.

PIIX4

Podľa lshw je v systéme jediné SMBus zariadenie obsluhované ovládačom piix4_smbus:

*-serial
     description: SMBus
     product: FCH SMBus Controller
     vendor: Advanced Micro Devices, Inc. [AMD]
     physical id: 14
     bus info: pci@0000:00:14.0
     version: 51
     width: 32 bits
     clock: 66MHz
     configuration: driver=piix4_smbus latency=0

Zoznam I2C / SMBus portov sa dá zobraziť príkazom i2cdetect -l:

i2cdetect -l|sort
i2c-0   i2c             Synopsys DesignWare I2C adapter         I2C adapter
i2c-1   i2c             Synopsys DesignWare I2C adapter         I2C adapter
i2c-2   i2c             AMDGPU DM i2c hw bus 0                  I2C adapter
i2c-3   i2c             AMDGPU DM i2c hw bus 1                  I2C adapter
i2c-4   i2c             AMDGPU DM i2c hw bus 2                  I2C adapter
i2c-5   i2c             AMDGPU DM i2c hw bus 3                  I2C adapter
i2c-6   i2c             AMDGPU DM aux hw bus 0                  I2C adapter
i2c-7   i2c             AMDGPU DM aux hw bus 2                  I2C adapter
i2c-8   i2c             AMDGPU DM aux hw bus 3                  I2C adapter
i2c-9   smbus           SMBus PIIX4 adapter port 0 at ff00      SMBus adapter
i2c-10  smbus           SMBus PIIX4 adapter port 2 at ff00      SMBus adapter
i2c-11  smbus           SMBus PIIX4 adapter port 1 at ff20      SMBus adapter

Zbernice I2C obsluhované ovládačom amdgpu môžem pokojne ignorovať, slúžia totiž na komunikáciu s monitorom cez rozhranie DDC/CI (ovládanie jasu, kontrastu, prepínanie výstupov …). Taktiež ignorujem zvyšné I2C adaptéry, pretože nepodporujú prerušenia. Zostávajú SMBus adaptéry 9-11. Zoznam pripojených zariadení sa dá získať pomocou i2cdetect, ktorý pošle quick príkaz na všetky relevantné adresy:

i2cdetect -y -q 9
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

To isté platí aj pre porty 10 a 11. Výpis dmesg potom obsahuje mnoho takýchto riadkov:

i2c i2c-9: Failed! (ff)
i2c i2c-9: Failed! (ff)
…

Chyba sa dá nájsť pomerne ľahko. V zozname adaptérov je zobrazená základná I/O adresa adaptéra:

i2c-9   smbus           SMBus PIIX4 adapter port 0 at ff00      SMBus adapter
i2c-10  smbus           SMBus PIIX4 adapter port 2 at ff00      SMBus adapter
i2c-11  smbus           SMBus PIIX4 adapter port 1 at ff20      SMBus adapter

Podľa ACPI je základná adresa 0x0b20, modul však používa chybnú adresu 0xff00/ff20. Napraviť by sa to malo dať zavedením modulu s voliteľným parametrom force_addr:

modinfo i2c-piix4|grep addr
parm:           force_addr:Forcibly enable the PIIX4 at the given address. EXTREMELY DANGEROUS! (int)

Odstránim modul z jadra a načítam ho znovu so správnou adresou.

modprobe -r i2c-piix4
modprobe i2c-piix4 force_add=0x0b20

Výpis dmesgu vyzerá takto:

piix4_smbus 0000:00:14.0: SMBus does not support forcing address!
piix4_smbus: probe of 0000:00:14.0 failed with error -22

Síce parameter je uvedený vo výpise modinfo, ale nedá sa použiť. Pred hľadaním príčiny si dovolím malú odbočku k PCI.

PCI

Podľa zdrojového kódu i2c-piix4.c je SMBus zbernica pripoojená na PCI. Sken PCI to potvrdí:

lspci -nn|grep SMBus
00:14.0 SMBus [0c05]: Advanced Micro Devices, Inc. [AMD] FCH SMBus Controller [1022:790b] (rev 51)

lspci -vvv -b -x -xxx -xxxx -nn -s 00:14.0
00:14.0 SMBus [0c05]: Advanced Micro Devices, Inc. [AMD] FCH SMBus Controller [1022:790b] (rev 51)
	Subsystem: Lenovo FCH SMBus Controller [17aa:5094]
	Control: I/O- Mem- BusMaster- SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
	Status: Cap- 66MHz+ UDF- FastB2B- ParErr- DEVSEL=medium >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
	IOMMU group: 9
	Kernel modules: i2c_piix4, sp5100_tco
00: 22 10 0b 79 00 04 20 02 51 00 05 0c 00 00 80 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 aa 17 94 50
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
f0: a7 df 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Číslo 1022:790b označuje ID výrobcu a ID zariadenia. Konkrétne 1022 je AMD a 790b označuje SMBus na chipsete AMD kerncz.

Pri tomto PCI ID sa volá funkcia piix4_setup_sb800. Hneď na začiatku súboru je kontrola parametra force_addr. Pri AMD sa modul spolieha výlučne na automatickú detekciu adresy a nie je možné ju vynútiť parametrom.

SB800

Odbočím trochu k SB800. Podľa všetkého je SB800 south bridge chipset od AMD. Dokumentácia k nemu sa dá nájsť na stránke AMD. Zvýšenú pozornosť si zaslúži dokument AMD SB8xx Register Reference Guide.

Kód detekcie adresy v i2c-piix4.c vyzerá takto:

#define SB800_PIIX4_SMB_IDX 0xcd6

// …

outb_p(smb_en, SB800_PIIX4_SMB_IDX);
smba_en_lo = inb_p(SB800_PIIX4_SMB_IDX + 1);
outb_p(smb_en + 1, SB800_PIIX4_SMB_IDX);
smba_en_hi = inb_p(SB800_PIIX4_SMB_IDX + 1);

Našťastie je tu magická konštanta cd6, ktorá zjednoduší vyhľadávanie v referenčnej príručke AMD SB8xx. Podľa sekcie Power Management (PM) Registers (strana 146) je 0xcd6 PM index register a 0xcd7 data register. Po zápise čísla PM registra na IO adresu 0xcd6 je možné čítaním z adresy 0xcd7 zistiť hodnotu PM registra.

Štruktúra PM registrov je na strane 147. PM register Smbus0En je zdokumentovaný na strane 151. Tu sa nachádza adresa SMBusu a stav zariadenia (zapnuté / vypnuté). Kód vyzerá teda správne, najskôr zapíše do PM index registra 0xcd6 číslo PM registra 0x2c, prečíta spodné bity adresy, potom zapíše 0x2c+1 do 0xcd6 a z 0xcd7 prečíta horné bity.

Všetko vyzerá byť správne, ale detekcia adresy evidentne zlyháva. Skúšam teda zapísať do en_hi pevnú adresu 0xb (spodná adresa aspoň pri AUX porte vyzerá správna).

outb_p(smb_en, SB800_PIIX4_SMB_IDX);
smba_en_lo = inb_p(SB800_PIIX4_SMB_IDX + 1);
outb_p(smb_en + 1, SB800_PIIX4_SMB_IDX);
smba_en_hi = inb_p(SB800_PIIX4_SMB_IDX + 1);

smba_en_hi = 0xb;

Výpis z dmesgu vyzerá správne:

piix4_smbus 0000:00:14.0: SMBus Host Controller at 0xb00, revision 0
piix4_smbus 0000:00:14.0: Using register 0x02 for SMBus port selection
piix4_smbus 0000:00:14.0: Auxiliary SMBus Host Controller at 0xb20

Teraz môžme spustiť detekciu zariadení. Ak je všetko správne, potom na AUX (0xb20) by malo byť zariadenie 0x2c (čo je adresa pridelená touchpadu synaptics). V mojom systéme je to port 11, ale pre istotu uvádzam aj výpis z 9 a 10 na adrese 0xb00.

i2cdetect -q -y 9
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- 36 37 -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: 50 -- -- -- -- -- -- -- 58 -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

i2cdetect -q -y 10
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- 36 37 -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: 50 -- -- -- -- -- -- -- 58 -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

i2cdetect -q -y 11
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- 1c -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- 2c 2d 2e 2f
30: 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f
40: 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f
50: 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f
60: 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f
70: 70 71 72 73 74 75 76 77

Prvé 2 výpisy sú identické. To nie je až také prekvapenie, pretože výber portu funguje cez PM register, ktorý som práve obišiel. Na prvých výpisoch aj tak nič zaujímavé nie je, ale posledný výpis má zariadenie 0x2c a práve quick write na túto adresu dostal zbernicu do nekonzistentného stavu, po ktorom odpovedá na všetky adresy. Druhý sken je už absolútne nepoužiteľný:

i2cdetect -q -y 11
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         08 09 0a 0b 0c 0d 0e 0f
10: 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
20: 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f
30: 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f
40: 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f
50: 50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f
60: 60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f
70: 70 71 72 73 74 75 76 77

Podľa prvého výpisu hádam, že som našiel správnu zbernicu, na ktorej je touchpad. Do funkcie piix4_func pridávam flag I2C_FUNC_SMBUS_HOST_NOTIFY. Tým deklarujem podporu host notify protokolu, aj keď v ovládači zatiaľ nie je implementovaná.

Po načítaní modulu i2c-piix4 zároveň s modulom psmouse vyzerá výpis dmesgu nasledovne:

psmouse serio1: synaptics: queried max coordinates: x [..5678], y [..4694]
psmouse serio1: synaptics: queried min coordinates: x [1266..], y [1162..]
psmouse serio1: synaptics: Trying to set up SMBus access
rmi4_smbus 11-002c: registering SMbus-connected sensor
rmi4_physical rmi4-00: rmi_driver_probe: Starting probe.
rmi4_physical rmi4-00: rmi_probe_interrupts: Counting IRQs.
rmi4_physical rmi4-00: rmi_init_functions: Creating functions.
rmi4_physical rmi4-00: Initializing F34.
rmi4_physical rmi4-00: Registered F34.
rmi4_physical rmi4-00: Initializing F01.
rmi4_f01 rmi4-00.fn01: found RMI device, manufacturer: Synaptics, product: TM3471-030, fw id: 3418235
rmi4_physical rmi4-00: Registered F01.
rmi4_physical rmi4-00: Initializing F12.
rmi4_f12 rmi4-00.fn12: rmi_f12_probe
rmi4_physical rmi4-00: rmi_read_register_desc: reg: 0 reg size: 1 subpackets: 1
…
rmi4_f12 rmi4-00.fn12: rmi_f12_probe: data packet size: 79
rmi4_f12 rmi4-00.fn12: rmi_f12_read_sensor_tuning: max_x: 1162 max_y: 778
rmi4_f12 rmi4-00.fn12: rmi_f12_read_sensor_tuning: Inactive Border xlo:0 xhi:0 ylo:0 yhi:0
rmi4_f12 rmi4-00.fn12: rmi_f12_read_sensor_tuning: x_mm: 96 y_mm: 64
rmi4_physical rmi4-00: Registered F12.
rmi4_physical rmi4-00: Initializing F54.
rmi4_physical rmi4-00: Registered F54.
rmi4_physical rmi4-00: Initializing F3A.
rmi4_physical rmi4-00: Registered F3A.
rmi4_physical rmi4-00: Initializing F03.
rmi4_physical rmi4-00: Registered F03.
rmi4_physical rmi4-00: Initializing F55.
rmi4_physical rmi4-00: Registered F55.
input: Synaptics TM3471-030 as /devices/rmi4-00/input/input56
serio: RMI4 PS/2 pass-through port at rmi4-00.fn03
rmi4_smbus 11-002c: rmi_register_transport_device: Registered 11-002c as rmi4-00.
rmi4_f03 rmi4-00.fn03: rmi_f03_pt_open: Consumed 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (14) from PS2 guest
rmi4_f03 rmi4-00.fn03: rmi_f03_pt_write: Wrote f2 to PS/2 passthrough address

To je úspešne inicializovaný touchpad! Síce nereaguje na udalosti, pretože nie je implementovaný host notify protokol, ale aj to je úspech. Od teraz viem, ku ktorej zbernici je touchpad pripojený, aj to, že komunikácia s nim vyzerá byť v poriadku.

I/O adresa

Ešte sa trochu vrátim k adrese. Samozrejme takáto záplata nemôže putovať do kernelu. Najskôr je potrebné zistiť, prečo čítanie z PM registra vracia nesprávnu hodnotu. Dokumentácia k 10-ročnému chipsetu, ktorý v mojom notebooku ani nie je asi nebude tým najlepším miestom pre zistenie príčiny.

Súčasné procesory sú navrhnuté skôr ako SoC (system on chip). Rôzne zbernice sa stali priamo súčasťou CPU, takže na stránkach AMD hľadám priamo bios kernel developer guide k CPU. Najnovší dokument v čase písania je k family 15h, modelom 70h-7fh. Ja mám síce family 19h, ale uspokojím sa aj s týmto. V nasledujúcom texte budem tento dokument volať jednoducho BKDG.

Podľa novej dokumentácie sa adresa SMBus zbernice zisťuje … úplne rovnako. Predpokladám, že zmena nastala niekde v novších procesoroch a predpokladám, že adresa bude uložená v niektorom inom z 256 možných registrov. Nie je to tak veľké číslo, aby som si nemohol prečítať každý register a vyhľadať s v nich adresu. Pridávam do piix4_setup_sb800 nasledujúci kód:

for (reg = 0; reg < 256; ++reg) {
        outb_p(reg, SB800_PIIX4_SMB_IDX);
        reg_val = inb_p(SB800_PIIX4_SMB_IDX + 1);
        printk(KERN_INFO "register=%02x value=%02x\n", reg, reg_val);
}

Všetky čítania registra však vracajú hodnotu 0xff.

register=00 value=ff
register=01 value=ff
register=02 value=ff
register=03 value=ff
register=04 value=ff
…

Podľa BKDG (strana 984) sú PM registre dostupné cez nepriamy IO prístup na adresách 0xcd6/7, alebo cez MMIO na od adresy FED8_0000h+300h.

Nakoniec som našiel v kernel mailing liste patch s podporou prístupu k PM registrom cez MMIO. Novšie procesory majú totiž štandardne vypnutý prístup cez nepriame I/O adresovanie. Ak je prístup cez I/O vypnutý, vrátia všetky pokusy o čítanie hodnotu 0xff, čo presne zodpovedá výpisu. Po aplikácii patchu už prepínanie medzi portmi funguje a i2cdetect 9/10 má rozdielny výstup.

Naivná implementácia prerušení

Touchpad je obsluhovaný ovládačom pre protokol RMI4. Z ACPI už viem, že SMBus používa prerušenie 7. Nemám síce ešte naprogramovaný host notify protokol, ale budem trochu optimistický a hovorím si, že keď v ovládači RMI4 použijem priamo prerušenie 7, bude to fungovať. Namiesto pdata->irq som zapísal konštantu 7, znovu načítal psmouse a trakpoint funguje! Funguje bez sekania!

No dobre, trochu preháňam s tým „funguje“. Niekedy po načítaní ovládača funguje, niekedy funguje pár sekúnd, niekedy vôbec. Uspávanie notebooku prestalo fungovať. Ovládač permanentne spotrebuje 10% času CPU. Podľa súboru /proc/interrupts sa vykonáva prerušenie 2 000x za sekundu. Nie úplne dobrý výsledok, ale som na správnej ceste.

Dostať sa do tohto bodu ma stálo niekoľko víkendov. Veľa času som strávil hrabaním sa v dokumentácií, učením sa C a písaním otrasného kódu, aby som zistil, či som vôbec na správnej ceste. Nebolo to zložité, ale potreboval som preskúmať veľa slepých uličiek. Zatiaľ som prakticky nepotreboval žiadne špeciálne vedomosti. Všetko, čo som potreboval som si naštudoval priebežne. Teraz, keď už mám skoro funkčnú podporu touchpadu cez SMBus zostáva hádam len implementovať host notify prtokol.

Host notify

Doteraz to bolo len také hranie sa s vypisovaním premenných. Teraz nastupuje skutočná práca pre skutočných hrdinov. Žiaden Bruce Willis na asteroide, ani ďalšia mňamka. Teraz idem reálne implementovať novú funkcionalitu do jadra.

Na začiatok je dobrý nápad pozrieť si, ako túto funkciu implementujú iné ovládače. Príkaz grep -r I2C_FUNC_SMBUS_HOST_NOTIFY . nad kódom kernelu nájde 3 relevantné výsledky. Ak vyradíme rôzne mikrokontroléry a zameriame sa na x86, zostane jediný výsledok - i2c-i801.c. Celá obsluha je extrémne jednoduchá, stačí prečítanie jedného registra, zavolanie i2c_handle_smbus_host_notify, vyčistenie stavového registra a vrátenie stavu IRQ_HANDLED.

static irqreturn_t i801_host_notify_isr(struct i801_priv *priv)
{
        unsigned short addr;

        addr = inb_p(SMBNTFDADD(priv)) >> 1;
        2c_handle_smbus_host_notify(&priv->adapter, addr);

        /* clear Host Notify bit and return */
        outb_p(SMBSLVSTS_HST_NTFY_STS, SMBSLVSTS(priv));
        return IRQ_HANDLED;
}

Zostáva už len registrovať prerušenie, nájsť relevantné registre v BGKD a napísať obsluhu prerušenia.

Začíname prerušením. Do piix4_probe pridávam nasledujúci kód:

static irqreturn_t piix4_isr(int irq, void *dev_id)
{
        printk(KERN_INFO "isr\n");
        return IRQ_HANDLED;
}

// … piix4_probe
        retval = devm_request_irq(&dev->dev, dev->irq, piix4_isr, IRQF_SHARED, "piix4_smbus", piix4_aux_adapter);
        if (!retval) {
                printk(KERN_INFO "smbus Using irq %d\n", dev->irq);
        }
        else {
                printk(KERN_INFO "smbus No irq %d\n", dev->irq);
        }

Po zavedení modulu sa v dmesgu objaví: No irq -1. Pole irq štruktúry pci_dev je nastavené podľa PCI konfiguračného priestoru. Hodnota interrupt line býva štandardne v konfiguračnom priestore na adrese 0x3c. Pripomeniem výpis lspci:

00: 22 10 0b 79 00 04 20 02 51 00 05 0c 00 00 80 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 aa 17 94 50
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Podľa BKDG, strana 866, register D14F0x3C Interrupt Line bity 7:0 je hodnota tohto registra vždy 0:

InterruptLine: Interrupt Line. Value: 0. 0=This module does not generate interrupts.

Konfigurácia prerušenia by sa mala dať načítať z acpi_device volaním ACPI_COMPANION, ale to nefunguje. K riešeniu sa vrátim neskôr. Zatiaľ si vystačím s pevne nastaveným číslom prerušenia. Asi najčistejšie riešenie je pridať nasledujúci kód do súboru drivers/pci/quirks.c:

static void quirk_piix4_amd(struct pci_dev *dev) {
        printk(KERN_INFO "piix4 fixing interrupt line");
        pci_write_config_byte(dev, PCI_INTERRUPT_LINE, 7);
        dev->irq = 7;
}
DECLARE_PCI_FIXUP_EARLY(PCI_VENDOR_ID_AMD, PCI_DEVICE_ID_AMD_KERNCZ_SMBUS, quirk_piix4_amd);

Prerušenie sa teraz už registruje bez problémov. Vyzerá to tak, že sa spúšťa pri každom prenose dát (možno aj pri host notify, ale tým som si nebol istý). Je čas preskúmať poriadne registre v BKDG. Dokumentácia sa začína popisom PCI konfiguračného priestoru na strane 863 a pokračuje ASF (Alert Standard Format) nasledované SMBusom. Trochu sa zastavím u ASF. Podľa dokumentácie by na 0x0b20 malo byť zariadenie ASF:

SmbusAsfIoBase. Read-write. Reset: 0Bh. Specifiies SMBus and ASF IO base address.
  • SMBus IO base = {Smbus0AsfIoBase[7:0], 0x00}.
  • ASF IO base = {Smbus0AsfIoBase[7:0], 0x20}.
By default SMBus IO base is B00h and ASF IO base is B20h.

V kerneli sa nenachádza žiadna podpora ASF. Podľa štandardu ide o rozhranie pre vzdialenú správu počítača.

The term "system manageability" represents a wide range of technologies that enable remote system access and control in both OS-present and OS-absent environments.

Je dosť nepravdepodobné, že by bol touchpad pripojený na rozhranie pre vzdialenú správu počítača. Budem predpokladať, že mám starú, alebo chybnú dokumentáciu a na novších procesoroch je na adrese 0x0b20 tiež SMBus.

Pripravil som si funkciu ktorá vypíše všetky hodnoty registrov okrem 0x02 a 0x07, aby som sa dozvedel, ktoré stavové registre sa nastavia po prerušení. Spomenuté registre vynechávam, pretože pri ich čítaní prestane fungovať komunikácia s touchpadom. Pri registri 0x07 je to logické, pretože je to SMBusBlockData - špeciálny register so vstavaným pointerom, ktorý sa inkrementuje každým čítaním a zápisom. V dokumentácii nie je napísané, prečo má side efekt aj čítanie registra SMBusControl, ale empiricky som zistil, že čítaním sa resetuje interný pointer SMBusBlockData registra. Kód výpisu vyzerá nasledovne:

static void piix4_dump_registers(struct i2c_adapter *piix4_adapter, char *label)
{
        struct i2c_piix4_adapdata *adapdata = i2c_get_adapdata(piix4_adapter);
        unsigned short piix4_smba = adapdata->smba;
        int i;
        u8 d[0x17];

        for (i = 0; i < 0x17; ++i) {
                if (i == 2 || i == 7) {
                        d[i] = 0;
                }
                else {
                        d[i] = inb_p(i + piix4_smba);
                }
        }

        printk(KERN_INFO "%02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x    %02x%02x %02x%02x %02x%02x %02x%02x\n", label, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], d[11], d[12], d[13], d[14], d[15], d[16], d[17], d[18], d[19], d[20], d[21], d[22], d[23]);
}

Samotný výpis vyzerá takto:

┌------------------------------------------------------------ SMBusStatus
| ┌---------------------------------------------------------- SMBusSlaveStatus
| |  ┌------------------------------------------------------- SMBusControl
| |  | ┌----------------------------------------------------- SMBusHostCmd
| |  | |  ┌-------------------------------------------------- SMBusAddress
| |  | |  | ┌------------------------------------------------ SMBusData0
| |  | |  | |  ┌--------------------------------------------- SMBusData1
| |  | |  | |  | ┌------------------------------------------- SMBusBlockData
| |  | |  | |  | |  ┌---------------------------------------- SMBusSlaveControl
| |  | |  | |  | |  | ┌-------------------------------------- SMBusShadowCmd
| |  | |  | |  | |  | |  ┌----------------------------------- SMBusSlaveEvent
| |  | |  | |  | |  | |  |    ┌------------------------------ SlaveData
| |  | |  | |  | |  | |  |    |    ┌------------------------- SMBusTiming
| |  | |  | |  | |  | |  |    |    |       ┌----------------- I2CbusConfig
| |  | |  | |  | |  | |  |    |    |       | ┌--------------- I2CCommand
| |  | |  | |  | |  | |  |    |    |       | |  ┌------------ I2CShadow1
| |  | |  | |  | |  | |  |    |    |       | |  | ┌---------- I2Cshadow2
| |  | |  | |  | |  | |  |    |    |       | |  | |  ┌------- SMBusAutoPoll
| |  | |  | |  | |  | |  |    |    |       | |  | |  | ┌----- SMBusCounter
| |  | |  | |  | |  | |  |    |    |       | |  | |  | |  ┌-- SMBusStop
| |  | |  | |  | |  | |  |    |    |       | |  | |  | |  | ┌ SMBusHostCmd2
0000 0006 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0087 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0007 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0080 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0000 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0081 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0082 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0002 5902 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0083 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0003 590e 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0084 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0004 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 008d 0002 0400

Posledným príkazom sa skončila inicializácia. Z dokumentáciu touchpadu (synaptics) som zistil, že vždy po inicializáciii pošle notifikáciu, takže posledný riadok by sa mal v niečom líšiť od predchádzajúcich. V skutočnosti sa aj líši v registri I2CShaow2. Vo výpise je okrem toho niekoľko anomálii, ktorým som nerozumel.

V prvom rade som čakal, že SMBusStatus bude mať nastavený bit SMBusInterrupt indikujúci ukončenie operácie (prípadne aj iné bity podľa toho, či došlo napríklad ku kolízii). Status je však vždy nulový.

Ďalšou anomáliou je SMBusSlaveEvent. Pri inicializácii som explicitne do oboch častí registra zapísal 0xff, aby som prijímal všetky udalosti. Napriek tomu je horná polovica registra prázdna.

outb_p(0xff, (0x0a + piix4_smba));
outb_p(0xff, (0x0b + piix4_smba));

Potom tu máme SMBusStop, ktorý obsahuje neprípustnú hodnotu, SMBusCounter, ktorý z nejakého záhadného dôvodu obsahuje 0x02 a prečo sa mení iba I2CShadow2?

Nezrovnalosti zatiaľ ignorujem a skúšam zapnúť prerušenie pri príchode host notify správy. Podľa dokumentácie by sa to mohlo dosiahnuť zápisom buď adresy 0x2c, 0x08, alebo ich ekvivalentov s bitovým posunom o 1 doľava do registra I2CCommand. Zároveň by mal mať bit I2CbusInterrupt registra I2CbusConfig hodnotu 1. Pokúsil som sa nastaviť všetky udalosti v registri SMBusSlaveEvent na 1 a taktiež SlaveEnable a SMBusAlertEnable bity SMBusSlaveControl registra. Skúšal som všetky možné kombinácie vyše týždňa, absolútne bez úspechu. Prerušenia boli generované len pre prenosy dát, ale nie pre notifikácie od zariadenia.

Keď už som nevedel, ako ďalej, skúsil som sa pozrieť na GPIO. Podľa RMI4 Intrfacing Guide vie touchpad generovať buď host notify správy, alebo vie žiadať o prerušenie na nejakom výstupnom pine. Čo ak prerušenie 7 slúži len na indikáciu ukončenia prenosu a touchpad je pripojený na pin GPIO čipu?

Podľa /sys/kernel/debug/pinctrl/AMDI0030:00/pins má môj notebook 183 pinov. Podobá sa to trochu hľadaniu ihly v kope sena. Našťastie v tom istom adresári existuje súbor pingroups s nasledujúcim obsahom:

registered pin groups:
…
group: i2c3
pin 19 (GPIO_19)
pin 20 (GPIO_20)

group: uart0
…

Zaujímavý je záznam i2c3. Nie, že by ostatné I2C porty neboli zaujímavé, ale keď som dal monitorovať práve tento príkazom gpiomon --num-events=5000 gpiochip0 19 20 zaznamenal som aktivitu na zbernici hneď po načítaní psmouse a potom nastalo ticho. Keď som sa dotkol touchpadu, okamžite sa obnovila aktivita na zbernici a prestala až 5 minút po poslednom dotyku. Pravdepodobne po tejto zbernici komunikuje touchpad a je celkom slušná šanca, že objavím interrupt pin. Bez popisu GPIO pinov to však nepôjde a AMD zatiaľ nevydalo dokumentáciu k môjmu CPU.

Zoznam GPIO som našiel v zdrojovom kóde corebootu. Ešte zaujímavejší je súbor gpio.c, v ktorom sa dajú nájsť piny priradené k udalostiam. Nebudem dlho napínať a poviem, že ani jeden nevyzeral ako prerušenie touchpadu. Pri pokuse o prečítanie väčšiny GPIO došlo k zhodeniu systému. Zase slepá ulička, ale dozvedel som sa aspoň, že na I2C3 je aktivita zodpovedajúca dotyku. Rýchlosť čítania GPIO je síce nedostatočná na to, aby som dokázal dekódovať, čo sa tam posiela, ale aj tak skvelá indícia.

Pokračujem ďalej v študovaní dokumentácie k chipsetu PIIX4. Podľa AMD sa pri použití SlaveEnable sa musí adresa, na ktorej počúva zapísať do slave control registra.

SlaveEnable. Read-write. Reset: 0. Enable the generation of an interrupt or resume event upon an external SMBus master generating a transaction with an address that matches the host controller slave port of 10h, a command field that matches the SMBus slave control register, and a match of corresponding enabled events.

Dokumentácia AMD nikde inde nespomína slave control register. Práve tu prichádza na scénu dokumentácia k PIIX4, ktorá definuje control register ako 0xD4 register PCI konfiguračného priestoru. Síce fajn, ale zápis na mojom zariadení nefunguje. Po zvážení odhadujem, že tadiaľ cesta tiež nevedie. AMD si zrejme nejak implementovalo host notify, ale dokumentáciu skopírovalo od Intelu. Preto sa v nej spomínajú neexistujúce registre.

Vraciam sa späť k SMBus registrom. Do očí mi tentoraz udreli 2 veci. Zápis do I2CCommand nemení hodnotu registra (ako keby bol read only, aj keď podľa dokumentácie je read/write). Ďalej je tu záhadná hodnota a8 aa registra SMBusTiming. Ak by to bolo ASF, potom by adresa SMBusTiming registra zodpovedala registrom RemoteCtrlAdr a SensorAdr. Prvá má štandardne hodnotu 0x54 posunutú o 1 bit doľava, čo zodpovedá 0xa8. Druhá 0x55, čo zodpovedá 0xaa. Zároveň register I2CCommand zodpovedá DataReadPointer v ASF a ten je read only. Začínam mať tušenie, že sa k ASF snažím pristupovať, ako keby to bol SMBus.

S týmto podozrením som sa rozhodol pozrieť na ovládač pre windows. Medzi súbormi vidím Smb_driver_AMDASF.sys a Smb_driver_Intel.sys. Ono to je vážne na AMD pripojené k ASF!

ASF

Pustil som sa hneď do štúdia ASF čítaním špecifikácie. Skúšal som podľa špecifikácie implementovať Get Event Data, ale absolútne bez úspechu. Skúšal som zapnúť kontrolné súčty packetov (pretože tie sú pre ASF povinné) a nič. Skúšal som napísať vlastnú implementáciu kontrolných súčtov, bez úspechu. Zariadenie uvedené v SensorAdr vôbec neodpovedalo. Hrabal som sa podrobnejšie aj v špecifikácii SMBusu, aby som cez ARP zistil pripojené zariadenie, ale vyzerá to tak, že AMD neimplementuje ani ARP. Zase raz víkend, pri ktorom som sa nikam nepomohol.

Neskôr som len tak náhodou zapísal adresu 0x08 do ListenAdr registra. Zrazu sa začali spontánne generovať prerušenia. Nie síce úplne pravidelne a nebol som si istý, či reagujú na dotyk touchpadu (video), ale rozhodne zaujímavé zistenie. Ostatné adresy nefungovali, ale 0x08 áno. Vlastne je to úplne logické, pretože na túto adresu má zariadenie posielať požiadavku na obsluhu prerušenia. Teraz už len zistiť, či prerušenie bolo generované prenosom, alebo slave zariadením, prečítať adresu slave a zavolať i2c_handle_smbus_host_notify. Malou komplikáciou je, že adresu 0x2c nevidím v žiadnom registri a nevidím žiadny zásadný rozdiel medzi prerušením z prenosu a prerušením od slave.

Ešte raz zopakujem výpis registrov, ale tentoraz so správnymi názvami:

┌------------------------------------------------------------ HostStatus
|    ┌------------------------------------------------------- HostControl
|    | ┌----------------------------------------------------- HostCommand
|    | |  ┌-------------------------------------------------- SlaveAddress
|    | |  | ┌------------------------------------------------ Data0
|    | |  | |  ┌--------------------------------------------- Data1
|    | |  | |  | ┌------------------------------------------- DataIndex
|    | |  | |  | |  ┌---------------------------------------- PEC
|    | |  | |  | |  | ┌-------------------------------------- ListenAdr
|    | |  | |  | |  | |  ┌----------------------------------- ASFStatus
|    | |  | |  | |  | |  | ┌--------------------------------- StatusMask0
|    | |  | |  | |  | |  | |  ┌------------------------------ StatusMask1
|    | |  | |  | |  | |  | |  | ┌---------------------------- SlaveStatus
|    | |  | |  | |  | |  | |  | |  ┌------------------------- RemoteCtrlAdr
|    | |  | |  | |  | |  | |  | |  | ┌----------------------- SensorAdr
|    | |  | |  | |  | |  | |  | |  | |     ┌----------------- DataReadPointer
|    | |  | |  | |  | |  | |  | |  | |     | ┌--------------- DataWritePointer
|    | |  | |  | |  | |  | |  | |  | |     | |  ┌------------ SetDataReadPointer
|    | |  | |  | |  | |  | |  | |  | |     | |  | ┌---------- DataBankSel
|    | |  | |  | |  | |  | |  | |  | |     | |  | |  ┌------- Semaphore
|    | |  | |  | |  | |  | |  | |  | |     | |  | |  | ┌----- SlaveEn
|    | |  | |  | |  | |  | |  | |  | |     | |  | |  | |  ┌-- DelayMasterTimer
0000 0006 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0087 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0007 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0080 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0000 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0081 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0082 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0002 5902 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0083 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0003 590e 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0084 5804 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0004 5801 0000 0f59 00ff ff00 a8aa    0000 0081 0002 0400
0000 0001 5802 0000 0f59 00ff ff00 a8aa    0000 008d 0002 0400

Na konci inicializačnej sekvencie register sa mení register DataBankSel. Pribudli príznaky DatabankXFull.

Keďže som sa nevedel ďalej moc pohnúť, skúsil som dekompilovať starý SMBus ovládač a tiež nový (trochu zložitejší) ovládač (ako dekompilátor používam ghidra).

V zdrojových kódoch som explicitne hľadal IO volania in a out. Nasledujúci kód som identifikoval ako obsluhu host notify prerušenia. V komentári je tabuľka, ktorú som si pripravil pre hodnoty registra DataBankSel & 0x0c. Prvý stĺpec je hodnota registra, nasledujú 2 stĺpce reprezentujúce čítanie (prvá číslica určuje banku z ktorej sa číta a druhá číslica banku, do ktorej sa načítaná hodnota zapíše). Posledným stĺpcom je výstupná hodnota.

/*

0 00x0       ret0
1 00x1       ret0
4 01x0    00 ret0
5 01x1       ret0
8 10x0       ret0
9 10x1    10 ret1
c 11x0 10 01 ret1
d 11x1 00 11 ret1

*/

ulonglong FUN_1400044c4(longlong param_1)

{
  byte bVar1;
  ulonglong uVar2;
  ulonglong uVar3;
  longlong lVar4;
  longlong lVar6;

  bVar1 = in((short)*(undefined4 *)(param_1 + 0x14) + 0x13);
  bVar1 = bVar1 & 0xd;
  uVar2 = 0;
  if (bVar1 < 2) {
LAB_1400045d8:
    return uVar2 & 0xffffffffffffff00;
  }
  if (bVar1 == 4) {
    *(undefined8 *)(param_1 + 0xb0) = 0;
    lVar6 = 0;
  }
  else {
    if (bVar1 == 9) {
      *(undefined8 *)(param_1 + 0xb0) = 0;
      lVar6 = 0;
      lVar4 = 1;
      goto LAB_14000458b;
    }
    if (bVar1 != 0xc) {
      if (bVar1 != 0xd) {
        out((ulonglong)*(uint *)(param_1 + 0x14) + 0x13,bVar1 & 0xc);
        return 0;
      }
      *(undefined8 *)(param_1 + 0xb0) = 0;
      read_notify_address(param_1,0,0);
      lVar6 = 1;
      lVar4 = 1;
      goto LAB_14000458b;
    }
    *(undefined8 *)(param_1 + 0xb0) = 0;
    read_notify_address(param_1,1,0);
    lVar6 = 1;
  }
  lVar4 = 0;
LAB_14000458b:
  uVar2 = read_notify_address(param_1,lVar4,lVar6);
  uVar2 = uVar2 & 0xffffffffffffff00;
  out((short)*(undefined4 *)(param_1 + 0x14) + 0x13,bVar1 & 0xc);
  return uVar2 & 0xffffffffffffff00 | 1;
}

void read_notify_address(longlong param_1,longlong param_2,longlong param_3)
{
  byte bVar1;
  undefined uVar2;
  undefined uVar3;

  *(int *)(param_1 + 0xa0) = *(int *)(param_1 + 0xa0) + 1;
  out((short)*(undefined4 *)(param_1 + 0x14) + 0x13,(&DAT_1400051d0)[param_2]);
  in((short)*(undefined4 *)(param_1 + 0x14) + 2);
  in((short)*(undefined4 *)(param_1 + 0x14) + 7);
  bVar1 = in((short)*(undefined4 *)(param_1 + 0x14) + 7);
  uVar2 = in((short)*(undefined4 *)(param_1 + 0x14) + 7);
  uVar3 = in((short)*(undefined4 *)(param_1 + 0x14) + 7);
  *(byte *)(param_1 + 0xc0 + param_3 * 4) = bVar1 >> 1;
  *(ushort *)(param_1 + 0xc2 + param_3 * 4) = CONCAT11(uVar3,uVar2);
  *(longlong *)(param_1 + 0xb8) = param_3;
  return;
}

Stále mi unikal bit SetReadHostDataBank registra DataBankSel. Ak je tento bit 1, potom čítanie z DataIndex postupne prečíta dáta z posledného blokového prenosu, lenže ak je 0 prečíta adresu, ktoré žiadalo o prerušenie. Presne toto som potreboval.

Register DataBankSel má trochu zložitejšiu štruktúru. V dobe písania jej celkom nerozumiem, ale mám akú-takú implementáciu, ktorú považujem za dostatočne spoľahlivú. Teraz skúsim opísať, čo o tomto registri viem.

Zariadenie má 2 data banky. Prepínať sa dajú zápisom 0/1 do SetReadRevDataBank. Dáta na SMBuse môžu mať rôznu dĺžku a neexistuje sekvencia, ktorá by jednoznačne oddeľovala správy. Preto odhadujem, že bity Databank0Full a Databank1Full znamenajú, že do banky bola zapísaná práve 1 správa. Význam DataBank[1] nepoznám. Predpokladám, že bit DataBank[0] (0=Data Bank 0 is the latest touched data bank) by mal slúžiť len pre jednoznačné určenie, do ktorej banky prišla správa skôr. Windowsový ovládač tam robí ešte nejakú mágiu okolo tohto registra. Moja implementácia vyzerá byť logicky správna, ale s niektorými kombináciami bitov mi neprečíta celú adresu, alebo prečíta adresy v opačnom poradí, ale stáva sa to tak zriedkavo, že zatiaľ to ignorujem. Moja implementácia vyzerá takto:

static u8 read_asf_data_bank(struct i2c_adapter *piix4_adapter, u8 bank_number)
{
        struct i2c_piix4_adapdata *adapdata = i2c_get_adapdata(piix4_adapter);
        unsigned short piix4_smba = adapdata->smba;
        u8 host_addr, addr, bank_sel;

        outb_p(bank_number << ASF_SET_READ_DATA_BANK_OFFSET, ASF_DATA_BANK_SEL);
        bank_sel = inb_p(ASF_DATA_BANK_SEL);
        inb_p(SMBHSTCNT); // reset DataIndex
        host_addr = inb_p(SMBBLKDAT);
        addr = inb_p(SMBBLKDAT);

        //dev_dbg(&piix4_adapter->dev, "BankSel=%02x Data=%02x %02x\n", bank_sel, host_addr, addr);

        if (host_addr != 0x10) {
                return 0;
        }

        return addr;
}


static irqreturn_t piix4_isr(int irq, void *dev_id)
{
        struct i2c_adapter *piix4_adapter = (struct i2c_adapter *)dev_id;
        struct i2c_piix4_adapdata *adapdata = i2c_get_adapdata(piix4_adapter);
        unsigned short piix4_smba = adapdata->smba;

        u8 bank_sel;
        u8 asf_status;
        u8 address[2] = {0x00, 0x00};
        u8 *current_address;

        current_address = &address[0];

        bank_sel = inb_p(ASF_DATA_BANK_SEL); // DataBankSel

        if ((bank_sel & ASF_DATA_BANK_LAST_TOUCH) == 0) { // Last touched bank is 0
                if (bank_sel & ASF_DATA_BANK_1_FULL) {
                        *current_address = read_asf_data_bank(piix4_adapter, 1);
                        current_address++;
                }
                if (bank_sel & ASF_DATA_BANK_0_FULL) {
                        *current_address = read_asf_data_bank(piix4_adapter, 0);
                }
        }
        else { // Last touched bank is 1
                if (bank_sel & ASF_DATA_BANK_0_FULL) {
                        *current_address = read_asf_data_bank(piix4_adapter, 0);
                        current_address++;
                }
                if (bank_sel & ASF_DATA_BANK_1_FULL) {
                        *current_address = read_asf_data_bank(piix4_adapter, 1);
                }
        }

        outb_p(bank_sel & (ASF_DATA_BANK_0_FULL | ASF_DATA_BANK_1_FULL), ASF_DATA_BANK_SEL); // Clear DataBankxFull

        // Trigger notifications
        if (address[0] != 0x00) {
                i2c_handle_smbus_host_notify(piix4_aux_adapter, address[0] >> 1);
        }
        if (address[1] != 0x00) {
                i2c_handle_smbus_host_notify(piix4_aux_adapter, address[1] >> 1);
        }

        // Clean ASFStatus SlaveIntr
        asf_status = inb_p(ASF_STATUS);
        if (asf_status & ASF_SLAVE_INTR) {
                outb_p(ASF_SLAVE_INTR, (ASF_STATUS)); // ASFStatus SlaveIntr? (in doc 0x20)
        }

        return IRQ_HANDLED;
}

Takto to viac-menej funguje, ale hrozne seká. Vlastne ani to nie je prekvapenie keď vidím, ako katastrofálne je to implementované v linuxe. Namiesto jednoduchého čakania na prerušenie je tu aktívne čakanie v slučke, kým nebude nastavený príznak dokončenia operácie. Ukážka linuxového kódu:

while ((++timeout < MAX_TIMEOUT) &&
       ((temp = inb_p(SMBHSTSTS)) & 0x01))
        usleep_range(250, 500);

Rozhodol som sa aktívne čakanie nahradiť prerušením. Podľa kernel dokumentácie som alokoval a inicializoval completion objekt a čakal na dokončenie operácie s limitom čakania 100ms.

timeout = wait_for_completion_timeout(adapdata-&gt;completion, msecs_to_jiffies(100));
if (timeout == 0) {
        dev_err(&piix4_adapter-&gt;dev, "SMBus Timeout!\n");
        result = -ETIMEDOUT;
}

V obsluhe prerušenia stačí zavolať complete:

host_status = inb_p(SMBHSTSTS);

// Clear HostStatus Intr and complete waiting
if (host_status & ASF_HOST_INTR) {
        outb_p(ASF_HOST_INTR, SMBHSTSTS);

        if (adapdata->completion) {
                complete(adapdata->completion); // Notify caller
        }
}

Teraz som skúsil poslať jeden quick command a nič. Žiadne prerušenie. Znovu som poslal príkaz a tentoraz som dostal prerušenie. Znovu a ďalšie prerušenie. Zaujímavý fakt, že prvé volanie nikdy nevyvolá prerušenie. Zároveň v prerušení mám stále prázdny HostStatus.

Ďalej som sa pozrel na časovanie. Prerušenie sa mi spúšťa 7μs po spustení prenosu. No počkať. Podľa PM registra (práve som ho kontroloval) je frekvencia zbernice nastavená na 100kHz. Teraz počítajme. Príkaz quick command sa skladá zo štart bitu, 7-bitovej adresy, write bitu a potvrdenia. Spolu 10 bitov. Pri frekvencii 100kHz jeden tak trvá 10μs. Prenos 10 bitov nemôže byť kratší než 100μs. Niečo tu vážne nesedí …

Tak si dajme dokopy fakty. Prerušenie sa spustí po druhom prenose. Prerušenie sa spustí skôr než sa môže odoslať čo i len jeden bit. Prerušenie sa nespustí keď zbernicu inicializujem a resetujem. Prerušenie sa spustí ak urobím 1 prenos a potom resetujem zbernicu. Teraz si dovolím vysloviť kacírsku myšlienku, že prerušenie sa spúšťa v nesprávnom momente. Pripomeňme si nastavenie prerušení z ACPI:

IRQ (Level, ActiveLow, Shared, )
    {7}

Prerušenie je citlivé na úroveň a aktívna hodnota je logická 0. Dovolím si jednu malú odbočku k prerušeniam.

Typy prerušení

Existujú 2 typy prerušení - citlivé na hranu a na úroveň. K obom existujú 2 polarity:

Typ
Polarita Nábežná hrana Aktívna 1
Dobežná hrana Aktívna 0

Prerušenia citlivé na hranu sa spúšťajú iba pri prechode. Obyčajne sa používajú pri jednoduchých zariadeniach, ktoré majú jediný zdroj prerušenia. Nábežnú hranu používa napríklad ISA zbernica. Naopak PCI zbernica používa aktívnu 0. Dôvodom citlivosti na úroveň je to, že PCI má zvyčajne viacej zdrojov prerušenia, ale iba jednú interrupt line. Ukážme si modelovú situáciu zariadenia, ktoré má 2 samostatné porty a jednú interrupt line, ktorá indikuje, že niečo sa stalo.

Port 1 - prerušenie
Port 2 - prerušenie
Spúšťa sa obsluha prerušenia
Obsluha portu 1
Port 1 obslúžený
Obsluha portu 2
Port 1 - prerušenie
Port 2 obslúžený

Ak by bolo prerušenie citlivé na hranu, prechod do aktívneho stavu by nastal hneď pri prvom riadku. Obsluha prerušenia postupne kontroluje všetky potenciálne príčiny prerušenia (napr. port 1, 2 atď), obslúži ich a vynuluje príznak prerušenia pre jednotlivé porty. Ak by na porte 1 počas obsluhy portu 2 došlo znovu k prerušeniu, nikdy by nenastal prechod do neaktívneho stavu. Zariadenie by zostalo v aktívnom stave aj po obsluhe prerušenia, ale keďže nedošlo k prechodu, ďalšia obsluha by sa už nikdy nespustila.

Pri citlivosti na úroveň sa toto nestane. Ak by sa znovu prerušil port 1 počas obsluhy portu 2, zostal by stále aktívny stav. Po dokončení obsluhy prerušenia by sa znovu spustila obsluha a tá by sa spúšťala až dovtedy, kým by neboli vyčistené / obslúžené všetky zdroje prerušenia.

Prerušenia

Podľa príznakov budem hádať, že mám nesprávnu polaritu aj typ prerušenia. Pozrime sa na modelovú situáciu, kde body r-1 a r-2 sú body, kde vyčistím HostStatus. Body s-1 a s-2 sú reálne začiatky prenosu na zbernici. Body e-1 a e-2 sú konce prenosu na zbernici. Takto zrejme vyzerá priebeh signálu.

Priebeh signálu
Obrázok 2: Priebeh signálu

Ak by to bolo takto, presne by to vysvetľovalo, prečo prvý prenos nespustí prerušenie, prečo je obsluha spustená v tak krátkom čase, prečo sa spustí pri resete zariadenia ak predtým bol inicializovaný prenos. Takto vyzerá môj /proc/interrupts:

IR-IO-APIC    2-edge     timer
IR-IO-APIC    1-edge     i8042
IR-IO-APIC    7-edge     piix4_smbus
IR-IO-APIC    8-edge     rtc0
IR-IO-APIC    9-fasteoi  acpi, pinctrl_amd
IR-IO-APIC   10-edge     AMDI0010:00
IR-IO-APIC   11-edge     AMDI0010:01

Prerušenie je citlivé na hranu, presne ako som očakával. Na nastavenie typu prerušenia sa podľa dokumentácie používa funkcia irq_set_irq_type. Pred, alebo po registráciu prerušenia (devm_request_irq) preto vkladám nasledujúci kód:

irq_set_irq_type(dev->irq, IRQ_TYPE_LEVEL_LOW);

Takto vyzerá teraz výpis /proc/interrupts.

IR-IO-APIC    7-edge     piix4_smbus

Prerušenie je stále citlivé na hranu, namiesto úrovne. Podľa kódu typu prerušenia je za nastavenie zodpovedná inštancia irq_chipu. Štruktúra IR-IO-APIC nemá nastavenú operáciu irq_set_type, takže nie je možné počas behu zmeniť typ prerušenia.

Podľa všetkého to vyzerá tak, že chyba je niekde v io-apic. Zaujímavé je, že prerušenie 9 využíva tiež io-apic, ale má správny typ. V zozname parametrov kernelu sa nachádza apic=debug. Po nabootovaní kernelu s týmto parametrom je v dmesgu tento zaujímavý výpis:

ACPI: INT_SRC_OVR (bus 0 bus_irq 0 global_irq 2 dfl dfl)
Int: type 0, pol 0, trig 0, bus 00, IRQ 00, APIC ID 20, APIC INT 02
ACPI: INT_SRC_OVR (bus 0 bus_irq 9 global_irq 9 low level)
Int: type 0, pol 3, trig 3, bus 00, IRQ 09, APIC ID 20, APIC INT 09

Vyhľadávanie reťazca INT_SRC_OVR grepom nájde funkciu acpi_table_print_madt_entry. Tabuľka MADT (Multiple APIC Description Table) je súčasťou ACPI. Nasledujúci výpis je časť dekompilovanej APIC ACPI tabuľky:

[0C4h 0196   1]                Subtable Type : 02 [Interrupt Source Override]
[0C5h 0197   1]                       Length : 0A
[0C6h 0198   1]                          Bus : 00
[0C7h 0199   1]                       Source : 00
[0C8h 0200   4]                    Interrupt : 00000002
[0CCh 0204   2]        Flags (decoded below) : 0000
                                    Polarity : 0
                                Trigger Mode : 0

[0CEh 0206   1]                Subtable Type : 02 [Interrupt Source Override]
[0CFh 0207   1]                       Length : 0A
[0D0h 0208   1]                          Bus : 00
[0D1h 0209   1]                       Source : 09
[0D2h 0210   4]                    Interrupt : 00000009
[0D6h 0214   2]        Flags (decoded below) : 000F
                                    Polarity : 3
                                Trigger Mode : 3

Presne podľa predchádzajúceho výpisu je v tejto tabuľke nastavenie prerušenia 2 a 9. Prerušenie číslo 7 chýba. Podľa kódu inicializácie APIC sa polarita a typ prerušenia zisťujú vo funkcii acpi_get_override_irq. Pri bližšom pohľade táto funkcia len skenuje APIC tabuľku ACPI, v ktorej chýba prerušenie IRQ. Podľa ACPI špecifikácie, sekcia 5.2.12.5 Interrupt Source Override Structure má byť prvých 16 prerušení citlivých na nábežnú hranu ak to nie je definované inak v APIC/MADT tabuľke. Ak to správne chápem, tento záznam je povinný bez ohľadu na to, či je prerušenie definované v niektorej z iných ACPI tabuliek. Windows buď skenuje aj _CRS resource záznamy, alebo dovoľuje zmeniť typ prerušenia počas behu, čo asi nieje v súlade so špecifikáciou.

Z toho dôvodu som kontaktoval Lenovo a nahlásil chýbajúci záznam pre prerušenie 7. Zároveň som do mp_save_irq pridal takúto príšernosť, aby som nebol blokovaný, kým Lenovo vydá aktualizáciu BIOSu.

if (mp_irq_entries == 7) {
        m->irqflag = MP_IRQPOL_ACTIVE_LOW | MP_IRQTRIG_LEVEL;
}

Po tejto zmene už prerušenia fungujú v správny moment. Obsluha prerušenia teraz musí korektne vyčistiť bit Intr registra HostStatus a SlaveIntr registra ASFStatus, inak sa bude obsluha spúšťať donekonečna.

Pred ďalším pokračovaním trochu zhrniem fakty. Ovládač i2c-piix4 má implementovaný protokol host notify. Prerušenia či už pri transakciách, alebo aj pri prerušeniach od zariadenia fungujú bez problémov. Touchpad / trackpoint by mal fungovať …

Stav

Synaptics

Ak je touchpad synaptics pripojený k I2C, alebo SPI rozhraniu, používa na komunikáciu protokol RMI4. Pred ďalším pokračovaním vysvetlím základy RMI4 protokolu.

Pri komunikácii cez I2C/SMBus sa vysiela najskôr adresa touchpadu (0x2c) nasledovaná príznakom read / write a 16-bitovou adresou registra RMI4. Registre sa delia na 4 typy - data (na čítanie / zápis dát), control (ovládanie zariadenia a čítanie výsledku), command (ovládanie zariadenia) a query (získanie informácie). Ďalej sú registre členené podľa funkcie, napr F12 je touchpad, F03 je trackpoint (alebo presnejšie povedané PS/2 pass-though, F34 je aktualizácia firmvéru atď. Špeciálnu úlohu má F01 - povolenie iných funkcií, nastavenie režimu šetrenia energie, povolenie prerušení, čítanie stavu prerušenia …

Adresy registrov nie sú statické. Statickú adresu má len PDT (page description table) tabuľka. Prečítaním PDT tabuľky je možné zistiť, aké funkcie má touchpad implementované, aké sú základné adresy registrov a ktoré prerušenia sú priradené jednotlivým funkciám.

Page description talbe
Obrázok 3: Page description table

Pri inicializácii touchpadu cez RMI4 protokol sa najskôr musí prečítať PDT tabuľka. Následne sa podľa nej registrujú podporované funkcie a povolia sa ich prerušenia. Po dokončení inicializácie zašle touchpad 1 host notify požiadavku.

Zatiaľ to vyzerá v poriadku. Touchpad normálne reaguje na pohyb a reporting rate je zhruba 80 Hz, čo zodpovedá maximálnej podporovanej rýchlosti synapticsu. Trackpoint hlási rate stabilných 100 Hz. Tu je video. Sú tu však 2 problémy. Prvým je strata približne 10% packetov v dôsledku kolízií. Druhým problémom je, že obsluha prerušenia sa volá približne 1000x za sekundu, čo výrazne zvyšuje spotrebu notebooku (v mojom prípade z 3W na 4W).

Prerušenia sa nezačnú generovať hneď po inicializácii, ale až po prvom dotyku. Generovanie sa zastaví asi 5 minút po poslednom dotyku.

Podobne by sa správalo prerušenie, ak by som v obsluhe nevyčistil všetky príznaky prerušenia. Ak by to tak aj bolo, nikdy by sa generovanie nezastavilo, ale tu sa zastaví asi po 5 minútach po poslednom dotyku.

Môj druhý typ je, že touchpad má vlastný príznak prerušenia, ktorý sa v jeho obsluhe nevyčistí a preto sa stále vysiela host notify signál, kým sa zariadenie neprepne do šetriaceho režimu. Ako teória dobré. Čo hovorí dokumentácia?

The attention signal is de-asserted by reading all of the Interrupt Status registers in a device. This means that a host driver should process the interrupt handlers for all interrupt sources that are reporting ‘1’ when the Interrupt Status is read.

Dokumentácia hovorí jasne, že stačí prečítať všetky interrupt status registre. Žiadna dodatočná akcia nie je potrebná. Čítanie všetkých interrupt status registrov sa deje vo funkcii rmi_process_interrupt_requests. Funkcia sa skutočne spúšťa pri každej prijatej notifikácii. Pridávam preto výpis interrupt status registra.

printk(KERN_INFO "IRQ 0x%lx\n", data->irq_status[0]);

Výpis podľa činnosti vyzerá nasledovne:

IRQ 0x0 // Žiaden dotyk
IRQ 0x80 // pohyb trackpointom
IRQ 0x18 // pohyb touchpadom

Podľa dokumentácie by sa vôbec nemal generovať host notify signál ak je interrupt status nulový. U mňa sa však generuje aj keď čítam 0x00. Pre istotu kontrolujem, ku ktorej funkcii patrí prerušenie. Najskôr však potrebujem výpis funkcií a príslušných prerušení. Do funkcie rmi_init_functions pridávam tento riadok rmi_scan_pdt(rmi_dev, &irq_count, rmi_debug_function), ktorý spustí rmi_debug_function pre každý záznam PDT tabuľky. Funkcia pre výpis vyzerá takto:

static int rmi_debug_function(struct rmi_device *rmi_dev,
                               void *ctx, const struct pdt_entry *pdt)
{
        int *current_irq_count = ctx;
        printk(KERN_INFO "PDT %02x: start=%04x cmd=%02x ctrl=%02x data=%02x, IRQ=%d+%d\n", pdt->function_number, pdt->page_start, pdt->command_base_addr, pdt->control_base_addr, pdt->data_base_addr, *current_irq_count, pdt->interrupt_source_count);
        *current_irq_count += pdt->interrupt_source_count;
        return 0;
}

Výpis vyzerá takto:

PDT 34: start=0000 cmd=00 ctrl=13 data=00, IRQ=0+1
PDT 01: start=0000 cmd=28 ctrl=14 data=06, IRQ=1+2
PDT 12: start=0000 cmd=00 ctrl=1d data=0c, IRQ=3+2
PDT 54: start=0100 cmd=3d ctrl=0f data=00, IRQ=5+1
PDT 3a: start=0200 cmd=00 ctrl=11 data=00, IRQ=6+1
PDT 03: start=0200 cmd=00 ctrl=00 data=01, IRQ=7+1
PDT 55: start=0300 cmd=03 ctrl=00 data=00, IRQ=8+1

Podľa výpisu bitová maska 0x18 zodpovedá touchpadu a 0x80 trackpointu. Teraz pár pokusov s rmi_enable_irq. Ak nikdy nepovolím prerušenia, prerušenia nie sú generované. Ak povolím prerušenia iba pre touchpad, prerušenia sú generované po prvom dotyku s hodnotou interrupt status registra 0x18 kým mám prst na touchpade, potom 0x00 až kým po pár minútach neprestanú. Trackpoint neovplyvňuje interrupt status register. Ak povolím len trackpoint, potom pri pohybe má interrupt status hodnotu 0x80, ale zároveň pri dotyku touchpadu sa nastavuje 0x18. Podľa príznakov tipujem, že je chyba niekde vo firmvéri, ale dokázať to neviem. Pre nepriestrelný dôkaz by som potreboval presne vidieť, ako vyzerajú odosielané bity na zbernici.

Logický analyzátor

Počas predchádzajúcich pokusov som zistil, že po aktivácií touchpadu / trackpointu sa začnú posielať dáta na GPIO pinoch 19 a 20. Podľa zdrojových kódov corebootu tieto piny patria I2C rozhraniu. Je celkom slušná šanca, že práve aktivita na týchto pinoch mi definitívne zodpovie otázku, či touchpad posila dáta sám, alebo je vyvolané nevhodnou komunikáciou zo strany SMBus ovládača.

Na sledovanie aktivity na zbernici sa používajú špeciálne zariadenia - logické analyzátory. Väčšinou ide o zariadenie s mnohými vstupmi, veľmi rýchlou pamäťou a FPGA, alebo ASIC obvodom na spracovanie signálov. Notebook som už síce mal otvorený, ale aj tak sa vyhýbam invazívnym metódam odpočúvania zbernice. Namiesto toho sa spolieham na to, že dokážem dostatočne rýchlo čítať hodnoty GPIO pinov priamo z linuxu.

Libgpiod

Môj prvý pokus bol s knižnicou libgpiod. Hosting na kernel.org naznačuje, že by mohlo ísť o pomerne kvalitnú knižnicu. Podľa zdrojových kódov (pretože dokumentácia je rovnako ako v prípade kernelu veľmi strohá) podporuje operácie nad viacerými vstupmi / výstupmi súčasne, čo sa bude hodiť.

Kompletný zdrojový kód sa dá skompilovať príkazom gcc -O3 -lgpiod dump_gpio_gpiod.c -o dump_gpio_gpiod. V nasledujúcom výpise je komentovaná časť kódu, ktorý pristupuje k GPIO:

struct gpiod_chip *chip;
struct gpiod_line_bulk lines;

unsigned int offsets[2] = {19, 20}; // Piny 19/20
unsigned int values[2]; // Buffer pre načítané hodnoty
int ret;

chip = gpiod_chip_open("/dev/gpiochip0");
if (!chip) {
        printf("GPIO not opened\n");
        return -1;
}

ret = gpiod_chip_get_lines(chip, &offsets[0], 2, &lines);
if (ret < 0) {
        printf("Lines not opened\n");
        return -1;
}

gpiod_line_request_input(lines.lines[0], NAME);
gpiod_line_request_input(lines.lines[1], NAME);
// Nastavenie open drain vstupu
gpiod_line_set_flags(lines.lines[0], GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW | GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN);
gpiod_line_set_flags(lines.lines[1], GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW | GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN);

while (true) {
        // súčasné načítanie vstupov
        gpiod_line_get_value_bulk(&lines, values);
        // uloženie poľa values
}

Teraz trocha výpočtov. Frekvencia SMBus zbernice je 100kHz. Na každý bit sú potrebne 2 prechody hodín (prechod z 1 do 0 a naspäť z 0 do 1). Čítať hodnotu je potrebné s minimálnou frekvenciou 200kHz, čo znamená, že na jedno čítanie máme maximálne 5µs. Maximálna frekvencia plánovača jadra je 1kHz, čo je absolútne nedostatočné. Pri tak vysokej citlivosti na časovanie je potrebné zabezpečiť, aby proces nebol prerušený plánovačom.

Kernel by mal mať vyhradené 1 jadro CPU na 1 neprerušiteľnú úlohu. Presne pre tento účel existuje parameter jadra isolcpus. Po naštartovaní s parametrom isolcpus=1 nebude kernel vôbec používať, ani plánovať úlohy pre jadro 1. Úloha sa dá spustiť na tomto jadre len pri explicitnom určení afinity. Program spúšťam cez utilitu schedtool nasledujúcim príkazom:

schedtool -a 0x01 -F -p 99 -n -20 -e ./dump_gpio_gpiod

Parameter -a určuje zoznam CPU, na ktorých môže byť proces spustený. Zoznam je implementovaný ako bitová maska, takže 0x01 znamená prvý CPU, 0x03 prvý a druhý atď. Pre fifo plánovač je určený parameter -F. Parameter -p je priorita fifo plánovača v rozsahu 1-99 (najvyššia je 99) a -n je hodnota nice.

Na papieri to funguje, v realite samozrejme, že nie. V zásade čítanie hodnoty GPIO trvá okolo 2µs +/- 20µs. Latencia musí byť však pod 5µs za každých okolností. Okrem toho, čítanie viacerých pinov na mojom stroji vôbec nefungovalo. Prvý áno, druhý vráti vždy 0. Ak ich čítam sekvenčne, tak áno, prečítajú sa oba s časom 2µs +/- 20µs, čo je nepoužiteľné.

Ioctl

V druhom pokuse som vynechal akékoľvek knižnice a hrabol som rovno svojimi ioctl volaniami po /dev/gpiochip0. V nasledujúcom výpise sú zaujímavé časti kódu:

int fd, ret;
struct gpiohandle_request rq;
struct gpiohandle_data data;

fd = open(DEV_NAME, O_RDONLY);
if (fd < 0) {;
        printf("Device not opened\n");
        return -1;
}

rq.lineoffsets[0] = 19;
rq.lineoffsets[1] = 20;
rq.flags = GPIOHANDLE_REQUEST_INPUT;
rq.lines = 2;
ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &rq);
close(fd);
if (ret == -1) {
        printf("Cant get handle\n");
        return -1;
}

while(true) {
        ioctl(rq.fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
        // práca s data.values[0,1]
}

Tento kód síce načíta oba piny, ale réžia prepínania kontextu kernelu je stále príliš veľká. Výsledok je zatiaľ nepoužiteľný. Aby som sa vyhol réžii pri prepínaní kontextu, presunul som kód do kernelu.

Kernel

Ku GPIO sa dá pristupovať len cez MMIO. Adresa registra je v BKDG príručke, časť 3.26.11.1 GPIO Registers. Nasledujúci kód mapuje MMIO do adresného priestoru kernelu na adresu gpi_addr. Od tohto momentu sa dá pristupovať k I/O zariadeniu pomocou priameho čítania, alebo zápisu do regiónu pamäte gpio_addr.

#define GPIO_ADDR   0xFED81500
#define GPIO_SIZE   0x00000400

struct resource *gpio_res;
void __iomem *gpio_addr;

gpio_res = request_mem_region(GPIO_ADDR, GPIO_SIZE, "amd-pinctrl");
if (!gpio_res) {
        printk(KERN_INFO "GPIO resource not acquired\n");
}
else {
        gpio_addr = ioremap(GPIO_ADDR, GPIO_SIZE);
        if (!gpio_addr) {
                printk(KERN_INFO "Failed to map GPIO\n");
        }
}

Kernel potrebuje sprístupniť zaznamenané dáta. Na tento účel sa dá veľmi jednoducho zneužiť debugfs.

#define GPIO_RECORDING_SIZE (1024 * 1024 * 8)

u8 gpio_recording[GPIO_RECORDING_SIZE];
struct dentry *gpio_dfs;
struct debugfs_blob_wrapper gpio_blob;

gpio_blob.data = &gpio_recording[0];
gpio_blob.size = GPIO_RECORDING_SIZE;
gpio_dfs = debugfs_create_blob("piix4_bus_dump", 0644, NULL, &gpio_blob);

Tento krátky kód sprístupní zaznamenané dáta ako súbor /sys/kernel/debug/piix4_bus_dump. Jeho obsah sa dá štandardnými nástrojmi skopírovať na disk. Hodnoty pinov sa budú zaznamenávať v samostatnom vlákne spustenom na izolovanom CPU jadre. Podľa tabuľky 278 v BKDG má každý pin šírku 32 registrov (4 byty). Relevantné I2C piny začínajú na adrese 0x4c (19 * 4). Z každej sady registrov je zaujímavý len register 16 (PinSts). Keďže sú piny hneď vedľa seba, dajú sa prečítať jedinou inštrukciou reqdq pre načítanie 64-bitovej hodnoty. Po troche mágie s bitmi sa zapíše do bufferu 1 byte s hodnotou hodinového signálu na najnižšej pozícii a hodnotou dátového signálu na druhej najnižšej pozícii.

static int gpio_thread_func(void *data) {
        u64 gpio_val;
        size_t pos;
        for (pos = 0; pos < GPIO_RECORDING_SIZE; pos++) {
                if (kthread_should_stop()) {
                        break;
                }
                gpio_val = readq(gpio_addr + 19*4);
                // bit 16 z prvého registra + bit 16 z druhého registra (16+32) posunuý o 1 pozíciu doľava
                gpio_recording[pos] = ((gpio_val & BIT(16)) >> 16) | ((gpio_val & BIT(48)) >> 47);
        }
        return 0;
}

gpio_thread = kthread_create(gpio_thread_func, NULL, "piix4_bus_dump-work");
kthread_bind(gpio_thread, 0);

V kóde transakcie stačí už len pri skenovaní naštartovať vlákno, ktoré bude zaznamenávať aktivitu:

if (size == I2C_SMBUS_QUICK) {
        wake_up_process(gpio_thread);
}

Teraz krátka kontrola. Funguje to? No povedzme, že lepšie než predchádzajúci kód, ale horšie než som čakal. Jedna iterácia trvá približne 1µs +/- 15µs. Pripomínam, že doba musí byť pod 5. Na druhej strane, čas vyskakuje približne pri každých 100 bitoch. Nie je to dosť dobré, aby som kompletne dekódoval, čo sa deje na zbernici, ale je to dosť dobré, aby som si urobil predstavu, čo sa tam deje.

Čo keď o nestačí

Ak by to nestačilo, pokračoval by som použitím DMA (direct memory access). Princíp činnosti DMA je pomerne jednoduchý. Operačný systém nakonfiguruje, čo má DMA kontrolér urobiť, spustí prenos a voliteľne počká na prerušenie, ktoré oznámi ukončenie prenosu. DMA prenosy majú obyčajne vysokú prioritu a je tu veľká šanca, že by toto riešenie fungovalo stabilne bez straty jediného bitu. Pre inicializáciu DMA prenosu je potrebné:

Ako zdroj prerušenia by bol použitý niektorý z časovačov. Do tohto riešenia som nešiel, pretože mi stačí základný prehľad o dianí na zbernici.

Analýza

Výsledný súbor sa dá načítať napríklad pomocou pulseview. Po importe raw binárneho súboru a pridaní I2C dekódera je pekne viditeľná komunikácia.

I2C
Obrázok 4: Aktivita zdiaľky

Po miernom priblížení je viditeľné pravidelné opakovanie.

I2C
Obrázok 5: Mierne priblíženie

Pri priblížení na 1 blok je viditeľný najskôr zápis 0x2c << 1 (0x58) na adresu 0x08. Týmto spôsobom požaduje zariadenie 0x2c host 0x08 o obsluhu prerušenia. Na základe tejto požiadavky host osloví zariadenie 0x2c a vyžiada si hodnotu interrupt status registra 0x00. Touchpad odpovie svojou vlastnou adresou 0x2c, dĺžkou bloku dát 0x02 a hodnotou interrupt status registra 0x00,0x00. Toto je konečný dôkaz, že touchpad posiela notifikácie, aj keď na to nemá dôvod.

I3C
Obrázok 6: Priblíženie na 1 blok

Okrem opísaných blokov sa niekedy objavujú aj krátke jednobytové zápisy. Tie sú však len dôsledkom faktu, že pred transakciou vypínam host funkciu, takže zápis na adresu 0x08 nie je úspešný. Ak nevypnem host funkciu, host bude na adresu 0x08 odpovedať vždy. Mierne sa vtedy zvýši frekvencia kolízií.

Vyhodnotenie

Pomocou záznamu aktivity na zbernici som zistil, že touchpad sám odosiela požiadavky na prerušenie. V ovládači SMBusu nie je žiadna zásadná chyba, ktorá by prekážala v používaní touchpadu. Ovládač touchpadu sa správa korektne podľa starej dokumentácie, ktorú som našiel na webe. K novšej dokumentácii nemám prístup.

Ako by som mohol pokračovať? Napríklad vyhľadaním problémov synapticsu. Vyzerá to tak, že problém sa prejavuje niekedy aj vo windows. Jeden komentár z lenovo fóra hovorí za všetko:

I'll forward those on to Synaptics as well in case it's useful - though it sounds like it matches what they're looking at but there are some interesting pieces in there with the interrupts being so bad. Synaptics have asked me not to discuss their investigation details publicly and I'm respecting that.

I will note that my understanding is that this issue is impacting Windows too.

I don't have an ETA on the solution yet I'm afraid, but it is being actively worked on and I've been checking in on it.

Teraz by som mohol pokračovať skúmaním windows ovládača. Ak zaznamenám veľa prerušení aj vo windowse, je to jednoznačne vadný firmvér. Ak nie, pravdepodobne windows ovládač nejakým spôsobom explicitne vyčistí interrupt status register.

Vo windowse podľa všetkého funguje ovládač správne. V tomto momente by som sa mohol ďalej babrať s debuggerom a disassemblerom, ale úprimne, nechce sa mi. Niekto s dokumentáciou by to možno zvládol za 5 minút, mne sa nechce riešiť toto celý deň, takže som len poslal e-mail zamestnancovi synapticsu, ktorý pracuje na linuxuovom ovládači.

Detekcia hardvéru

Ovládač touchpadu zatiaľ nechám tak a vraciam sa k SMBusu. Ten je síce po mojich úpravách funkčný, ale nechal som tam napríklad napevno číslo prerušenia 7. Informácia o čísle prerušenia však musí byť odniekiaľ načítaná, pretože nie na každom počítači bude SMBusu priradené to isté prerušenie.

Rozdiel medzi pci_driver a platform_driver

V aktuálnej master verzii kernelu je 410 výskytov module_pci_driver a 2846 výskytov module_platform_driver. Čo je pci_driver je celkom jasné z názvu.. Čo je však platform_driver, prečo je ich omnoho viac než PCI ovládačov a prečo sa vlastne pri PCI zariadení zaoberám nejakým platform_driverom?

Dovolím si malú citáciu z dokumentácie kernelu.

This pseudo-bus is used to connect devices on busses with minimal infrastructure, like those used to integrate peripherals on many system-on-chip processors, or some "legacy" PC interconnects; as opposed to large formally specified ones like PCI or USB.
Platform devices are devices that typically appear as autonomous entities in the system. This includes legacy port-based devices and host bridges to peripheral buses, and most controllers integrated into system-on-chip platforms.

Z uvedenej časti dokumentácie vyplýva, že platform_driver mám zahodiť za hlavu a plne sa sústrediť na pci_driver.

Registrácia PCI ovládača

Operačný systém dokáže získať zoznam zariadení pripojených na zbernici. Každé PCI zariadenie má 256 konfiguračných registrov.

Štruktúra registrov PCI konfiguračného priestoru
Obrázok 7: Štruktúra registrov PCI konfiguračného priestoru

Pripomeniem ešte výpis z lspci s parametrom -xxx pre výpis 256 registrov PCI konfiguračného priestoru:

lspci -vvv -b -x -xxx -xxxx -nn -s 00:14.0
00: 22 10 0b 79 00 04 20 02 51 00 05 0c 00 00 80 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 aa 17 94 50
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
…

Najdôležitejšími registrami sú dvojica Vendor ID a Device ID. Tieto 2 hodnoty tvoria tzv. PCI ID. Ide o jednoznačný identifikátor zariadenia, podľa ktorého sa načítava príslušný ovládač. Linuxové ovládače PCI zariadení majú v sebe metadáta so zoznamom podporovaných PCI ID. Napríklad ovládač i2c-piix4 podporuje tieto PCI ID.

V PCI konfiguračnom priestore sa zvyčajne nachádza aj základná adresa zariadenia (tá, ktorá sa horko-ťažko dolovala z PM registra) a tiež interrupt line. Kernel na základe PCI zariadenia automaticky nastaví pole irq štruktúry pci_dev. Správne by mal nasledujúci kód vypísať číslo 7.

printk(KERN_INFO "pci_dev irq %d\n", dev->irq);

// alebo

pci_read_config_byte(dev, 0x3c, &irq_num);
printk(KERN_INFO "pci_dev irq %d\n", irq_num);

Vo výpise sa však objaví 0. Dôvod prezradí BKDG dokument, časť 3.26.6 SMBus Host Controller. Podľa neho register 0x3c vracia vždy 0. To isté vráti aj základná adresa 0x10. Celý konfiguračný priestore je viac-menej nepoužiteľný okrem PCI ID a čísla revízie.

Ovládače platform_driver

Pre správne fungovanie musí operačný systém vedieť, ktoré zariadenia má pripojené, cez aké adresy s nimi môže komunikovať a aké prerušenia môže očakávať.

V prípade ARM dosiek sa to rieši väčšinou cez devicetree. Zoznam zariadení sa zapíše do súboru, ten sa integruje do kernelu (buď staticky, alebo cez initrd) a na základe neho OS inicializuje ovládače. Dôsledkom tohto prístupu je, že kernel z jednej ARM dosky (napr. raspberry pi) nie je možné preniesť na inú dosku.

Ovládače platform_driver sú práve ovládače inicializované pomocou tabuľky zariadení. Aby som to zhrnul pci_driver využíva výsledok skenovania PCI zbernice a platform_driver využíva konfiguráciu z tabuľky, na základe ktorej inicializuje ovládače.

Konfigurácia v tabuľke … čo mi to len pripomína? ACPI tabuľky! ACPI poskytuje na x86 platforme presne to, čo devicetree na rôznych ARM doskách a iných embedded zariadeniach. Linux dokáže automaticky vytvoriť platform_device štruktúry z ACPI záznamov. Ovládače platform_driver nie sú vôbec zastaralé a vôbec neplatí, že by mala byť preferovaná konfigurácia cez PCI konfiguračný priestor. Naopak konfigurácia kernelu je v tomto bode zastaralá.

Pomôcť by mohlo, keby sa ovládač neviazal len na PCI ID, ale aj na _HID záznam ACPI tabuľky. Ovládač však môže byť len jeden, teda pci_driver, alebo platform_driver. Ktorý by mal byť teda použitý? Pre osvieženie pamäte pripomeniem časť DSDT tabuľky z ACPI:

Name (_HID, "SMB0001")  // _HID: Hardware ID
Name (_CRS, ResourceTemplate ()  // _CRS: Current Resource Settings
{
    IO (Decode16,
        0x0B20,             // Range Minimum
        0x0B20,             // Range Maximum
        0x20,               // Alignment
        0x20,               // Length
        )
    IRQ (Level, ActiveLow, Shared, )
        {7}
})

Je tu aj základná adresa, aj číslo prerušenia. Problémom je _HID SMB0001, ktorý označuje virtuálne SMBus zariadenie. Pre prístup k týmto dátam musí byť ovládač implementovaný ako platform_driver, čo nie je možné, pretože v ACPI je definovaný len ako virtuálne zariadenie, takže by obsluhoval aj nekompatibilné SMBus zariadenia.

Zmeniť ovládač na platform_driver síce nemôžem, ale môžem skúsiť získať prístup k ACPI z pci_device. To som samozrejme aj skúsil, ale moja naivná predstava, že by nasledujúci kód mohol fungovať skončila NULL derefrenciou.

struct platform_device *pdev = NULL;
struct resource *res;

pdev = to_platform_device(&dev->dev);
if (pdev) {
        res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
        if (res) {
                printk(KERN_INFO "IRQ: %llu\n", res->start);
        }
}
BUG: kernel NULL pointer dereference, address: 0000000000000018
#PF: supervisor read access in kernel mode
#PF: error_code(0x0000) - not-present page

Kernel by mal automaticky konvertovať ACPI záznamy na platform_device. Aby som videl čo sa deje, rozhodol som sa ísť o úroveň nižšie a použiť ACPI rozhranie priamo.

Pre získanie ACPI záznamu má kernel makro ACPI_COMPANION. Prístup k IRQ z acpi_device nie je úplne priamočiary, ale našiel som v kerneli funkciu acpi_dev_gpio_irq_get, ktorú používajú niektoré ovládače. Nasledujúci kód by teoreticky mohol fungovať:

int irq;
struct acpi_device *adev;

adev = ACPI_COMPANION(&dev->dev);
irq = acpi_dev_gpio_irq_get(adev, 0);
printk(KERN_INFO "irq %d\n", irq);

// výstup irq -2

Ani tento kód nefunguje. Začínam mať pocit, že ACPI_COMPANION vracia nesprávne zariadenie. Pridám výpis pár metadát:

char *hid = NULL;

acpi_device = ACPI_COMPANION(&dev->dev);
hid = acpi_device_hid(acpi_device);
if (hid) {
        printk(KERN_INFO "hid '%s'\n", hid);
}
printk(KERN_INFO "pnp bus_id '%s'\n", acpi_device->pnp.bus_id);
printk(KERN_INFO "pnp device_name '%s'\n", acpi_device->pnp.device_name);
printk(KERN_INFO "pnp unique_id '%s'\n", acpi_device->pnp.unique_id);
hid 'device'
pnp bus_id 'SMB'
pnp device_name ''
pnp unique_id '(null)'

Podľa výstupu vráti ACPI_COMPANION len dummy záznam. Záznam v ACPI tabuľke totiž nie je priradený k PCI zariadeniu. Viem, že v ACPI má záznam _HID hodnotu SMB0001a môžem explicitne vyžiadať záznam podľa jeho názvu a následne zase skúsiť získať IRQ resource z platform_device priradeného k ACPI záznamu. Vraciam sa k použitiu platform_device získaného zo správneho ACPI záznamu pomocou acpi_platform_device_find_by_companion.

struct acpi_device *adev;
struct resource *res;
struct device *dev;

adev = acpi_dev_get_first_match_dev("SMB0001", NULL, -1);
if (adev) {
        dev = bus_find_device_by_acpi_dev(&platform_bus_type, adev);
        if (dev) {
                pdev = to_platform_device(dev);
                if (pdev) {
                        res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
                        if (res) {
                                printk(KERN_INFO "IRQ %llu\n", res->start);
                        }
                }
        }
}

Tento kód je konečne správny. Lenže nefunguje. Vlastne funguje ak sa zahodí tento commit. Jeho úlohou je zabrániť vytvoreniu inštancie platform_device z ACPI zariadenia, ktoré má _HID SMB0001. Odstránenie tohto kódu by mohlo mať za následok problémy na inom hardvéri, takže idem na priamy prístup k ACPI bez inštancie platform_device.

struct acpi_device *adev;
acpi_status status;
int irq;

static acpi_status acpi_device_find_irq(struct acpi_resource *res, void *data)
{
        int *irq = data;

        switch (res->type) {
        case ACPI_RESOURCE_TYPE_IRQ:
                *irq = res->data.irq.interrupts[0];
                return AE_OK;
        case ACPI_RESOURCE_TYPE_END_TAG:
                if (*irq)
                        return AE_OK;
                else
                        return AE_NOT_FOUND;
        default:
                return AE_OK;
        }
}

acpi_device = acpi_dev_get_first_match_dev("SMB0001", NULL, -1);
status = acpi_walk_resources(acpi_device->handle, METHOD_NAME__CRS, acpi_device_find_irq, &irq);
if (ACPI_FAILURE(status)) {
        printk(KERN_WARNING "IRQ not found\n");
        return;
}

printk(KERN_INFO "IRQ %d\n", irq);

Tento kód konečne nájde číslo prerušenia bez rozbitia podpory linuxu na iných systémoch. Teraz už zostáva len začleniť kód do kernelu.

Moje úpravy sú väčšine než samotný ovládač i2c-piix4. Ďalej budem pokračovať pokusom o začlenenie do jadra, ale pravdepodobne ako samostatný ovládač. Aktuálne sa používa pomalý IO prístup, ale ak by som išiel cestou samostatného ovládača, nahradil by som kompletne všetky IO volania rýchlejším MMIO prístupom.

Pokus z opačnej strany

Pôvodne som chcel len opraviť sekanie trackpointu. Nakoniec som doplnil i2c-piix4 o nevyhnutný kód, ale riešenie pôvodného problému som vzdal. Problém sa objavuje len keď sa priblížim k touchpadu. Preto som na začiatku skúšal fyzicky odpojiť touchpad. Vôbec by mi nevadilo, ak by bol kompletne vypnutý.

Medzitým som si len tak pre zaujímavosť prechádzal rozšírenia PS/2 protokolu, ktoré má implementované synaptics. Do oka mi padol režim SLEEP_MODE. Keďže od začiatku mám problém iba keď je touchpad aktívny, tento režim vyzeral pomerne nádejne.

Po nastavení SLEEP_MODE sa síce nič nestalo, ale všimol som si medzi nastavením režimov chýbajúce bity 4 a 5. Nastavenie bitu 4 neznamenalo žiadnu zmenu, ale po nastavení bitu 5 prestal touchpad / trackpoint úplne fungovať. Skúsil som teda znovu načítať modul psmouse (modprobe -r psmouse; modprobe psmouse) a touchpad vôbec nebol rozpoznaný. Zostal len trackpoint. Skúšam teda utilitku evhz a vidím stabilných 100Hz namiesto pôvodných ani nie 40.

TPPS/2 Elan TrackPoint: Latest   114Hz, Average    99Hz
TPPS/2 Elan TrackPoint: Latest    99Hz, Average    99Hz
TPPS/2 Elan TrackPoint: Latest    99Hz, Average    99Hz
TPPS/2 Elan TrackPoint: Latest    99Hz, Average   100Hz
TPPS/2 Elan TrackPoint: Latest   108Hz, Average   100Hz
TPPS/2 Elan TrackPoint: Latest    99Hz, Average   100Hz
TPPS/2 Elan TrackPoint: Latest   100Hz, Average   100Hz
TPPS/2 Elan TrackPoint: Latest   100Hz, Average   100Hz
TPPS/2 Elan TrackPoint: Latest   101Hz, Average   100Hz
TPPS/2 Elan TrackPoint: Latest   100Hz, Average   100Hz
TPPS/2 Elan TrackPoint: Latest    99Hz, Average   100Hz

Frekvencia sa drží stabilne bez ohľadu na to, či sa touchpadu dotýkam, alebo nie. Odpojenie touchpadu je čiastočne perzistentné. Teda čiastočne znamená, že nebude rozpoznaný ani po reštarte. Touchpad nenájde ani diagnostický nástroj pre testovanie hardvéru v UEFI. V grafickej nadstavbe UEFI tak isto nefunguje. Znovu sa zapne až po úplnom vypnutí notebooku.

Takže svoje riešenie by som mal, teraz zostáva zistiť prečo to funguje. Môj pôvodný odhad bol, že som touchpad dostal do nejakého režimu pre upload firmvéru. Začal som hľadať na internete dokumentáciu, až som sa dopátral k starému manuálu k PS/2 protokolu pre synaptics.

Bit 5 je zdokumentovaný na strane 39 ako Transparent Mode. Podľa dokumentácie je to režim, v ktorom sú PS/2 packety z druhého zariadenia (v tomto prípade trackpointu) posielané priamo operačnému systému. Bežne sa tieto packety zabalia do väčších packetov, aby sa dali mixovať signály z oboch zariadení. Zariadenie sa dá vrátiť do pôvodného režimu odoslaním príkazov 0xe7, 0xe6 v presne tomto poradí.

Teraz fungoval trackpoint presne tak, ako som chcel, ale bolo tu ešte pár malých problémov, ktoré bolo potrebné vyriešiť. V prvom rade po prvom načítaní modulu psmouse bolo potrebné odstrániť modul a znovu ho načítať, pretože pred prepnutím akceptoval len zabalené PS2 packety.

Svoj kernel patch som mohol implementovať 2 spôsobmi. V tom jednoduchšom by som nastavil TRANSPARENT_MODE, odstránil zariadenie a spustil novú detekciu. To má však niekoľko nevýhod. V prvom rade nie je možné zariadenie vrátiť späť do pôvodného režimu bez úplného vypnutia notebooku. Druhým problémom je, že touchpad sa môže samovoľne reštartovať (napríklad v dôsledku elektrostatického výboja) a po reštarte by zostal v zlom režime.

Pri zložitejšom spôsobe by som packety nespracovával na úrovni hlavného zariadenia, ale jednoducho by som posielal každý prijatý bit priamo passthrough zariadeniu. Spracovanie prijatého bytu vyzerá takto:

static psmouse_ret_t transparent_process_byte(struct psmouse *psmouse)
{
    struct synaptics_data *priv = psmouse->private;

    if (!priv->pt_port)
        return PSMOUSE_BAD_DATA;

    serio_interrupt(priv->pt_port, psmouse->packet[psmouse->pktcnt - 1], 0);
    return PSMOUSE_FULL_PACKET;
}

Ďalej som musel aktualizovať nastavenie režimu, aby sa po aktivácii TRANSPARENT_MODE začal používať transparent_process_byte namiesto synaptics_process_byte:

static void synaptics_update_protocol_handler(struct psmouse *psmouse)
{
    struct synaptics_data *priv = psmouse->private;
    struct serio *pt_port = priv->pt_port;

    bool absolute_mode = priv->absolute_mode;
    bool transparent_mode = priv->transparent_mode;

    if (transparent_mode && pt_port) {
        psmouse->protocol_handler = transparent_process_byte;
    }
    else {
        if (absolute_mode) {
            psmouse->protocol_handler = synaptics_process_byte;
            psmouse->pktsize = 6;
        } else {
            /* Relative mode follows standard PS/2 mouse protocol */
            psmouse->protocol_handler = psmouse_process_byte;
            psmouse->pktsize = 3;
        }
    }
}

Tento prístup mi umožňuje dokonca vytvoriť súbor transparent_mode v /sys/devices/platform/i8042/serio1 a prepínať režim bez toho, aby som musel reloadovať modul psmouse.

static ssize_t synaptics_show_transparent_mode(struct psmouse *psmouse,
                                               void *data, char *buf)
{
        struct synaptics_data *priv = psmouse->private;

        return sprintf(buf, "%c\n", priv->transparent_mode ? '1' : '0');
}

static ssize_t synaptics_set_transparent_mode(struct psmouse *psmouse,
                                              void *data, const char *buf,
                                              size_t len)
{
        struct synaptics_data *priv = psmouse->private;
        unsigned int value;
        int err;

        err = kstrtouint(buf, 10, &value);
        if (err)
                return err;

        if (value > 1)
                return -EINVAL;

        if (value == priv->transparent_mode)
                return len;

        priv->transparent_mode = value;

        synaptics_update_protocol_handler(psmouse);

        if (value) {
                if (synaptics_enter_transparent_mode(psmouse))
                        return -EIO;
        }
        else {
                if (synaptics_exit_transparent_mode(psmouse))
                        return -EIO;
        }

        return len;
}

PSMOUSE_DEFINE_ATTR(transparent_mode, S_IWUSR | S_IRUGO, NULL,
                    synaptics_show_transparent_mode,
                    synaptics_set_transparent_mode);

Celý patch je o kúsok zložitejší a je zverejnený v kerenel mailing liste.

Pár slov na záver

Tento príbeh by som rád zakončil slovami: ktokoľvek sa môže zapojiť do vývoja kernelu. Vedomosti nehrajú prakticky žiadnu úlohu. Sám som na začiatku nevedel o vývoji kernelu absolútne nič.

Jednoducho som začal riešiť problém krok za krokom. Od jednej chyby, k druhej. Dokumentáciu som čítal priebežne, podľa toho, čo som práve potreboval vedieť. Dokonca som si nepamätal nič z programovania v C, ale to, čo som potreboval sa dalo naštudovať za necelý deň.

Dopredu som nevedel, aká hlboká bude moja zajačia nora, ale nakoniec som sa po veľmi dlhej ceste s rôznymi odbočkami dostal k 2 dosť dobrým riešeniam.

Dokumenty

    • RE: Denník „kernel developera“ 03.07.2022 | 09:11
      Avatar Livan Manjaro s XFCE  Používateľ

      Tak teda musím povedať, že si sa s tým dobre pohral. To si sa musel buď dobre nudiť alebo Ťa takéto veci fascinujú. Nesúhlasím s tvrdením v závere, že na vedomostiach nezáleží. Človek musí mať alespoň základný prehľad o fungovaní, aby sa vôbec mohol o takéto niečo pokúsiť. Čítal som to iba povrchne ale aj tak som narazil na dve veci, ktoré ma zaujali. Prvá, citujem "Jedna iterácia trvá približne 1µs +/- 15µs.". Tam má byť prvá z jednotiek v ms nie? Ináč by vychádzali záporné časy, čo je nezmysel. Druhá vec na začiatku sú uvedené frekvencie merania polohy od cca 26 Hz vyššie. Pri takýchto frekvenciách by vďaka zotrvačnosti oka nemalo byť vidieť trhanie. Však niekedy (na začiatku vzniku filmov) sa pri takýchto frekvenciách snímkov premietalo.

      • RE: Denník „kernel developera“ 03.07.2022 | 11:16
        Avatar Miroslav Bendík Gentoo  Administrátor

        1µs +/- 15µs ... neviem ako presne zapísať. Niektoré iterácie sú omnoho kratšie, ale celkovo sa to pohybuje okolo 1µs a niekedy vyskakujú viac s hodnotou štandardnej odchýlky 15µs (odtiaľ to +/- aj keď do mínusu reálne ísť nemôže).

        Pri starších filmoch nižšia snímkovacia frekvencia nevadila, pretože obraz je prirodzene rozostrený, ale taký kurzor žiaden motion blur nemá. Aj preto sa teraz začínajú používať 120Hz panely, ktoré majú výrazne plynulejší pohyb (áno, dalo by sa to zrejme dosiahnuť motion blurom).

    • RE: Denník „kernel developera“ 06.07.2022 | 18:59
      Avatar zaphod Manjaro, MX Linux  Používateľ

      Vďaka za napínavé čítanie. Najlepšia detektívka čo som v poslednom čase čítal. A pritom ani nikoho nezavraždili :-).