#import "@preview/ilm:1.4.2": * #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"), figure-index: (enabled: true, title: "Bilderverzeichnis"), ) #let link-text(body) = text(blue, body) #counter(page).update(1) = Einleitung Damit Computersysteme mit der Umwelt interagieren können, muss mit externen Sensoren und Aktoren kommuniziert werden. Eine der Aufgaben eines Betriebssystems ist es, gemeinsame Schnittstellen für verschiedene Geräte darzustellen; Unter Linux werden dafür sogenannte _device files_ verwendet, so dass über klassische Dateioperationen auf diese Geräte zugegriffen werden kann. == Gerätezugriff per Dateisystem Nach der UNIX-Philosophie #quote("Everything is a file") melden Gerätetreiber spezielle Dateien im virtuellen Dateisystem an (in der Regel im Verzeichnis `/dev` oder `/sys`). Wird auf dieser Datei zum Beispiel `write()` aufgerufen, so wird mit den geschriebenen Daten eine Funktion im Kerneltreiber aufgerufen, der sie dann an das physische Gerät weitergibt. === Device-Dateien Folgender Beispielcode 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-Addresse, die für die nachfolgenden Transaktionen verwendet wird ioctl(driver, I2C_SLAVE, 0x76); // (2) // Schreibe die Addresse 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 Datei muss auch die Device-Datei geöffnet werden. Der Treiber setzt die notwendigen Buchhaltungsstrukturen für eine weitere Anwendung auf. 2. Der `ioctl`-Syscall wird verwendet, um Treibereinstellungen zu ändern oder Operationen auszuführen, die nicht mit `read` oder `write` dargestellt werden können. Die Flags und Parameter für `ioctl` sind treiberabhängig. 3. Der `write`-Syscall 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. === 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 I/O-Geräte typsicherweise in `/dev` registriert werden, während andere Geräte über `/sys` konfiguriert werden. Außerdem wird `/sys` verwendet um Geräte zu finden. 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 Syscalls: #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 Syscalls: #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], [`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). Als Beispiel wird hier der Kernel-eigene Treiber für den #link("https://www.kernel.org/doc/html/v6.12/iio/ep93xx_adc.html", link-text[ADC des Cirrus Logic EP93xx SoC]) genutzt. Hier 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], [Pin-Name]), [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 startet führt synchron eine ADC-Umwandlung durch und gibt den ganzzahligen µV-Wert als String aus. = Design einer Hardwareschnittstelle für AT91SAM7-Timer Der AT91SAM7-Mikrocontroller stellt das _Timer Counter_ Peripheral bereit; Drei 16-bit Zähler == Features TODO = 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. Bei geteilten Ressourcen wie Bussen tritt dieses Problem häufig auf, weswegen im nächsten Schritt ein typsicher Lösungsansatz besprochen wird. == Lösungsansatz Da hier eine geteilte Ressource (der Bus) _fair_ zwischen mehreren Clients (den Treibern) verteilt werden soll, bietet sich ein #link("https://de.wikipedia.org/wiki/Prozess-Scheduler", link-text([Scheduling Verfahren])) an. Fragt ein Client einen I²C-Transfer an, so wird er nicht direkt ausgeführt, sondern mit anderen ausstehenden Anfragen in einer Warteschlange (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: #figure( image("./i2c-scheduler-rr.excalidraw.png"), caption: [I²C-Scheduling mit Round Robin], alt: "A vertical scheduling diagram showing round robin scheduling", )