#import "@preview/ilm:1.4.2": * #let link-text(body) = text(blue, body) #set text(lang: "de") #show: ilm.with( title: [Gerätetreiber unter Linux], author: "Konstantin Veltmann", date: datetime.today(), date-format: "[day padding:zero].[month repr:short].[year repr:full]", raw-text: (use-typst-defaults: true), bibliography: bibliography("refs.bib"), ) #counter(page).update(1) = Einleitung Damit Computersysteme mit ihrer Umwelt interagieren können, ist die Kommunikation mit externen Sensoren und Aktoren erforderlich. Eine der Aufgaben eines Betriebssystems besteht darin, gemeinsame Schnittstellen für verschiedene Geräte bereitzustellen@os-tasks. Unter Linux werden hierfür sogenannte _device files_@device-files verwendet, die den Zugriff auf diese Geräte über klassische Dateioperationen ermöglichen. == Gerätezugriff per Dateisystem Nach der UNIX-Philosophie #quote("Everything is a file")@everythings-a-file melden Gerätetreiber spezielle Dateien im virtuellen Dateisystem an (in der Regel im Verzeichnis `/dev` oder `/sys`). Wird auf einer solchen Datei ein Systemaufruf wie `write()` ausgeführt, so wird eine Funktion im Kerneltreiber aufgerufen, die die 'geschriebenen' Daten an das physische Gerät weiterleitet. === Device-Dateien Folgender Beispielcode@i2c-example zeigt die Kommunikation mit einem BME280-Sensor mithilfe des Userspace I²C-Treibers auf einem Raspberry Pi: ```c int main() { // Öffne die Device-Datei int driver = open("/dev/i2c-1"); // (1) // Setze die Slave-Adresse für nachfolgende Transaktionen ioctl(driver, I2C_SLAVE, 0x76); // (2) // Schreibe die Adresse für das ID-Register uint8_t const write_buf[] = {0xd0}; write(driver, write_buf, sizeof(write_buf)); // (3) // Lese aus dem ID-Register. NOTE: Das ist eine separate I²C-Transaktion! uint8_t id; read(driver, &id, 1); // (4) printf("ID: %"PRIu8, id); } ``` Der Beispielcode zeigt die vier typischen Datei-Operationen bei der Arbeit mit Device-Dateien: 1. Wie jede andere Datei muss die Device-Datei geöffnet werden. Der Treiber richtet dabei die notwendigen Verwaltungsstrukturen für die weitere Nutzung durch die Anwendung ein. 2. Der `ioctl`-Systemaufruf dient dazu, Treibereinstellungen zu ändern oder Operationen auszuführen, die nicht über `read` oder `write` abgebildet werden können. Die Flags und Parameter für `ioctl` sind treiberabhängig. 3. Der `write`-Systemaufruf sendet eine schreibende Transaktion auf den I²C-Bus. In der Regel wird `write` verwendet, um Daten auf Busse zu schreiben oder Ausgänge zu schalten. 4. `read` führt eine lesende Transaktion auf dem I²C-Bus aus. `read` wird typischerweise verwendet, um Gerätedaten auszulesen oder von Bussen zu empfangen. Typischerweise stellt der Kernel eine Begleitbibliothek wie `libi2c` für I²C-Operationen bereit, um versionsabhängige Unterschiede zu abstrahieren. Diese Bibliotheken werden hier nicht näher beschrieben, stellen jedoch die empfohlene Schnittstelle dar. === Zugriff über Sysfs Einige Gerätetreiber registrieren keine Dateien in `/dev`, sondern werden über Dateien im virtuellen Dateisystem unter `/sys` kontrolliert. Die Konvention dafür ist, dass für Eingabe-/Ausgabe-Operationen mit Geräten die Dateien in `/dev` verwendet werden, während `/sys` für strukturierte Zugriffe und Konfiguration verwendet wird@devfs-vs-sysfs. Geräte-Dateien in `/sys` haben eine String-basierte Schnittstelle, es werden also menschenlesbare Werte in verschiedenen Dateien geschrieben. Das macht die Interaktion mit `/sys`-Dateien in der Shell attraktiv. Folgender Shell-Code liest die momentane Batteriespannung meines Laptops aus. ```sh # ADC-Dateien finden, haben i.d.R voltage im Namen find /sys -iname "*voltage*" BAT='/sys/devices/[...]/BAT1/voltage_now' # Pfad gekürzt cat $BAT # > 12832000 [µV] ``` Folgender Shell-Code stellt auf einem #link("https://wiki.banana-pi.org/index.php?title=Banana_Pi_BPI-R3&oldid=17314", link-text[Banana Pi R3]) die Drehgeschwindigkeit des CPU-Kühlers auf 40%: ```sh echo 40 > /sys/devices/platform/pwm-fan/hwmon/hwmon1/pwm1 ``` = Beschreibung hardwarespezifischer Schnittstellen unter Linux == I²C Wie in der Einleitung beschrieben stellt der I²C-Treiber device-Dateien unter `/dev/i2c-*` bereit. - Implementiert in #link("https://github.com/torvalds/linux/blob/master/drivers/i2c/i2c-dev.c", link-text[`drivers/i2c/i2c-dev.c`]) - #link("https://www.kernel.org/doc/html/latest/i2c/dev-interface.html", link-text[Offizielle Dokumentation]) Implementierte Systemaufrufe: #figure( table( columns: (auto, 1fr), align: horizon, table.header([Syscall], [Funktion]), [`ioctl(, I2C_SLAVE, )`], [Setzt die Slave-Adresse für alle folgenden I²C-Transaktionen], [`write(, , )`], [Sendet die Daten aus `buf` in einer einzelnen I²C-Schreib-Operation an die gesetzte Slave-Adresse], [`read(, , )`], [Liest `len` Bytes vom ausgewählten Slave in `buf`], [`ioctl(, I2C_RDWR, )`], [Sendet mehrere Schreibe- und Lese-Operationen an den ausgewählten Slave in einer Transaktion ohne Stop-Conditions], ), caption: [Systemaufrufe des I²C-Treibers] ) In modernen Computersystemen wird der mit I²C kompatible SMBus verwendet. Daher stellt der Treiber noch weitere `ioctls` bereit, die hier jedoch nicht besprochen werden. == GPIO Der GPIO-Treiber stellt zwei Schnittstellen bereit, eine unter `/dev` und eine veraltete unter `/sys`. Hier wird die aktuelle empfohlene Variante beschrieben. - Implementiert in #link("https://github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib-cdev.c", link-text(`drivers/gpio/gpiolib-cdev.c`)); - #link("https://www.kernel.org/doc/html/latest/userspace-api/gpio/chardev.html", link-text([Offizielle Dokumentation])). Implementierte Systemaufrufe: #figure( table( columns: (auto, 1fr), align: horizon, table.header([Syscall], [Funktion]), [`ioctl(, GPIO_GET_CHIPINFO_IOCTL, )`], [Informationen über einen Gpio-Chip holen], [`ioctl(, GPIO_GET_LINEINFO_UNWATCH_IOCTL , )`], [Stoppt das Beobachten eines GPIO-Pins], [`ioctl(, GPIO_V2_GET_LINEINFO_IOCTL, )`], [Beschafft Informationen über einen spezifischen GPIO-Pin], [`ioctl(, GPIO_V2_GET_LINEINFO_WATCH_IOCTL, )`], [Beschafft Informationen über einen GPIO-Pin und macht nachfolgende Änderungen über `read` verfügbar], [`ioctl(, GPIO_V2_GET_LINE_IOCTL, )`], [Reserviert und konfiguriert einen GPIO-Pin für das aufrufende Programm], [`ioctl(, GPIO_V2_LINE_SET_CONFIG_IOCTL, )`], [Setzt Attribute für einen Pin, zum Beispiel Input/Output oder active LOW/HIGH], [`ioctl(, GPIO_V2_LINE_GET_VALUES_IOCTL, )`], [Liest Werte von mehreren Eingangs-Pins], [`ioctl(, GPIO_V2_LINE_SET_VALUES_IOCTL, )`], [Setzt/Cleared mehrere Ausgangs-Pins], ), caption: [Systemaufrufe des GPIO-Treibers] ) == ADC ADCs werden in Linux nicht direkt als eigene Geräteklasse verwaltet, sondern sind in der Regel als _Hardware Monitoring_ (Überwachung) oder _Industrial I/O_ (iio) gelistet. Als Beispiel wird hier der Kernel-eigene Treiber für den ADC des Cirrus Logic EP93xx SoC@adc-driver genutzt. Dabei wird für jeden der ADC-Pins ein eigener Eintrag unter `/sys/bus/iio/devices/iio:device/` angelegt, wobei $N$ die Geräte-ID ist: #figure( table( columns: (auto, 1fr), align: horizon, table.header([Sysfs-Eintrag], [Name des gesampleten Pins]), [in_voltage0_raw], [`Y-`], [in_voltage1_raw], [`sX+`], [in_voltage2_raw], [`sX-`], [in_voltage3_raw], [`sY+`], [in_voltage4_raw], [`sY-`], [in_voltage5_raw], [`X+`], [in_voltage6_raw], [`X-`], [in_voltage7_raw], [`Y+`], ), caption: [Sysfs-Einträge des ADC-Treibers] ) Das Auslesen einer dieser Datein führt synchron eine ADC-Umwandlung durch. Das Format der gelesenen Daten ist nicht klar dokumentiert. = Design einer Hardwareschnittstelle für AT91SAM7-Timer Der AT91SAM7-Mikrocontroller@sam7s-datasheet stellt das _Timer Counter Peripheral_ bereit; Drei unabhängige 16-bit Zähler, Kanäle genannt, mit einstellbaren Taktgeschwindigkeiten, Überlaufgrenzen und _Triggern_. == Features Jeder Kanal kann in einem der folgenden Modi sein: - _Capture_ zum Festhalten von Zeitpunkten, zu denen Eingänge geschaltet wurden - _Waveform_ zum Erzeugen von einstellbaren Rechtecksignalen Außerdem hat jeder Kanal drei Eingangssignale `XC0-2`, zwei Ausgangssignale `A/B` und kann einen von fünf Vorteilern wählen. === Capture-Modus Im _Capture_-Modus zählt der Zähler kontinuierlich und es wir bei einem konfigurierbaren _Event_ (eine Flanke auf `TIOA` oder `TIOB`) der Zählerstand in eins der Register geschrieben. Dieser Modus ist unter anderem für die Bestimmung von Frequenz, Pulszeit und Pahsenbestimmung eins oder mehrerer anliegender Signale gedacht. === Waveform-Modus Dieser Modus ist für die Erzeugung von Rechtecksignalen gedacht. Es gibt vier Untermodi: #figure( table( columns: (auto, auto, 1fr), align: horizon, table.header([Modus], [Zählrichtung], [Verhalten wenn $="RC"$]), [`00`], [Hoch], [Nichts, nur durch Überlauf zurückgesetzt], [`10`], [Hoch], [Zurücksetzen auf 0], [`01`], [Hoch, dann Runter], [Nichts, Richtungswechsel wenn $=0$ oder $="0xFFFF"$], [`11`], [Hoch, dann Runter], [Richtungswechsel], ), caption: [Wellenmodi im Waveform-Modus] ) Außerdem wird der Zählerwert immer mit den Werten in den Registern `RA/RB/RC` auf Gleichheit verglichen. Die daraus entstehenden Trigger-Signale können dann die Ausganspins `A/B` jeweils entweder einschalten, ausschalten oder umschalten. == Umsetzung Die API ist an der Struktur der GPIO-API orientiert. Jeder Kanal muss mit `REQ_CHANNEL` vom Kernel angefragt werden, damit ein Kanal von genau einem Prozess verwaltet wird. Mithilfe der `SET_MODE_CAPUTE` und `SET_MODE_WAVE` `ioctl`s wird der Kanal in den jeweiligen Modus versetzt und konfiguriert. Der `TIMER_START`-Befehl startet einen einzelnen Kanal. Wenn der aufrufende Prozess alle Kanäle kontrolliert, kann `TIMER_START` auf dem Timer selbst aufgerufen werden, was das SYNC-Signal für alle Kanäle setzt. Folgend eine Beispielanwendung: ```c int main() { int timer_fd = open("/dev/timer0"); int ch0 = ioctl(timer_fd, REQ_CHANNEL_IOCTL, 0); int some_free_channel = ioctl(timer_fd, REQ_CHANNEL_IOCTL, -1); struct capture_config capture_config = { .clock = CLOCK_1, // = TIMER_CLOCK1 .clock_burst = CLOCK_BURST_NONE, // Oder CLOCK_BURST_TIOA0/1/2 .clock_invert = false, .a_edge = EDGE_RISING, .b_edge = EDGE_NONE, .external_trigger = EXT_TRIGGER_A, .interrupt_on = INT_LDRA | INT_LDRB | INT_OVF, // Aktivierte interrupts .compare = -1, //Deaktiviert CPCTRG, >0 aktiviert CPCTRG }; ioctl(ch0, SET_MODE_CAPTURE, &capture_config); struct wave_config wave_config = { .clock = CLOCK_TIOA2, //-EINVAL wenn nicht verfügbar .clock_invert = true, .wave_mode = WAVE_MODE_UP_RC_TRIGGER, // WAVSEL = 10 .ra = 100, .rb = 0x4000, .rc = 0x9fff, .tioa = (struct mtio) { .a_mode = MTIO_MODE_SET, .b_mode = MTIO_MODE_CLEAR, .c_mode = MTIO_MODE_TOGGLE, .sw_mode = MTIO_MODE_NONE, } .tiob = (struct mtio) {0}, // TIOB ist deaktiviert }; ioctl(some_free_channel, SET_MODE_WAVE, &wave_config); //Würde mit dem SYNC-Signal alle Kanäle starten, //allerdings hat dieser Prozess nicht alle Kanäle angefragt. //Der Aufruf würde also fehlschlagen //ioctl(timer_fd, TIMER_START); ioctl(ch0, TIMER_START); //Setzt SWTRG struct capture_event capture_event; // Blockiert bis mindestens eins der Signale in interrupt_on ausgelöst wurde read(ch0, &capture_event, sizeof(capture_event)); } ``` = Scheduling bei geteilten Bussystemen Angenommen man habe ein smartes Thermostat mit folgenden Sensoren und Aktoren, alle angeschlossen über einen geteilten I²C-Bus: - OLED-Display - BME280 Temperatur- und Luftfeuchtigkeitsmesser Jedes der Geräte hat einen eigenen Treiber, der über die kerneleigenen I²C-Funktionen auf den Bus zugreift. Da Displays typischerweise hohe Datenraten brauchen, verbringt der Display-Treiber viel Zeit auf dem Bus. Zudem sollen in regelmäßigen Abständen Temperatur und Luftfeuchtigkeit vom Sensor angefragt werden. #figure( image("./i2c-starvation-gantt.excalidraw.png"), caption: [Gantt-Diagramm des smarten Thermostats], alt: "A gantt diagram showing process starvation", ) Wie in @fig-i2c-starvation gezeigt, wird durch die häufigen Display-Übertragungen der Temperatur-Sensor "ausgehungert" (schraffierter Hintergrund) und kann seine Daten nicht rechtzeitig übertragen. Dieses Problem gehört zur Klasse der _Scheduling_-Aufgaben. Ein klassischer Lösungsansatz wird im nächsten Abschnitt besprochen == Lösungsansatz Da hier eine geteilte Ressource (der Bus) *fair* zwischen mehreren Clients (den Treibern) verteilt werden soll, bietet sich ein _Scheduling_-Verfahren@wiki-scheduling an. Fragt ein Client einen I²C-Transfer an, so wird er nicht direkt ausgeführt, sondern mit anderen ausstehenden Anfragen in einer _Queue_ (dt. Warteschlange)@wiki-queue gespeichert. Nun kann der I²C-Scheduler die nächste anstehende Transaktion nach einem Scheduling-Verfahren wie dem _Completely Fair Scheduler_@wiki-cfs aussuchen und durchführen, um Aushungern zu vermeiden. Nachfolgend ist der Ablauf mit dem simplen _Round-Robin-Verfahren_@wiki-round-robin gezeigt, das *keine* Fairness garantiert: #figure( image("./i2c-scheduler-rr.excalidraw.png"), caption: [I²C-Scheduling mit Round Robin], alt: "A vertical scheduling diagram showing round robin scheduling", ) = Quelltext Der Quelltext dieser Arbeit ist unter #link-text([https://git.veltko.de/Weckyy702/uc-ausarbeitung-linux-treiber]) mit der GPL lizensiert zu finden