Files
uc-ausarbeitung-linux-treiber/main.typ
2026-01-20 00:49:39 +01:00

197 lines
9.7 KiB
Typst

#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(<file>, I2C_SLAVE, <addr>)`], [Setzt die Slave-Adresse für alle folgenden I²C-Transaktionen],
[`write(<file>, <buf>, <len>)`], [Sendet die Daten aus `buf` in einer einzelnen I²C-Schreib-Operation an die gesetzte Slave-Adresse],
[`read(<file>, <buf>, <len>)`], [Liest `len` Bytes vom ausgewählten Slave in `buf`],
[`ioctl(<file>, I2C_RDWR, <msgset>)`], [Sendet mehrere Schreibe- und Lese-Operationen an den ausgewählten Slave in einer Transaktion ohne Stop-Conditions],
),
caption: [Systemaufrufe des I²C-Treibers]
)<tab-i2c-syscalls>
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(<file>, GPIO_GET_CHIPINFO_IOCTL, <chip_info>)`], [Informationen über einen Gpio-Chip holen],
[`ioctl(<file>, GPIO_GET_LINEINFO_UNWATCH_IOCTL , <line_offset>)`], [Stoppt das Beobachten eines GPIO-Pins],
[`ioctl(<file>, GPIO_V2_GET_LINEINFO_IOCTL, <line_info>)`], [Beschafft Informationen über einen spezifischen GPIO-Pin],
[`ioctl(<file>, GPIO_V2_GET_LINEINFO_WATCH_IOCTL, <line_info>)`], [Beschafft Informationen über einen GPIO-Pin und],
[`ioctl(<file>, GPIO_V2_GET_LINE_IOCTL, <line_request>)`], [Reserviert und konfiguriert einen GPIO-Pin für das aufrufende Programm],
[`ioctl(<file>, GPIO_V2_LINE_SET_CONFIG_IOCTL, <line_config>)`], [Setzt Attribute für einen Pin, zum Beispiel Input/Output oder active LOW/HIGH],
[`ioctl(<file>, GPIO_V2_LINE_GET_VALUES_IOCTL, <line_values>)`], [Liest Werte von mehreren Eingangs-Pins],
[`ioctl(<file>, GPIO_V2_LINE_SET_VALUES_IOCTL, <line_values>)`], [Setzt/Cleared mehrere Ausgangs-Pins],
),
caption: [Systemaufrufe des GPIO-Treibers]
)<tab-gpio-syscalls>
== 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<N>/` 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]
)<tab-adc-syscalls>
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",
)<fig-i2c-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 #link("https://en.wikipedia.org/wiki/Completely_Fair_Scheduler", link-text([Completely Fair Scheduler])) aussuchen und durchführen, um Aushungern zu vermeiden.
Nachfolgend ist der Ablauf mit dem simplen #link("todo", link-text([Round-Robin-Verfahren])) gezeigt:
//TODO: