Finish (for now)
This commit is contained in:
156
main.typ
156
main.typ
@@ -1,5 +1,7 @@
|
||||
#import "@preview/ilm:1.4.2": *
|
||||
|
||||
#let link-text(body) = text(blue, body)
|
||||
|
||||
#set text(lang: "de")
|
||||
|
||||
#show: ilm.with(
|
||||
@@ -9,34 +11,31 @@
|
||||
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.
|
||||
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") 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.
|
||||
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 zeigt die Kommunikation mit einem BME280-Sensor mithilfe des Userspace I²C-Treibers auf einem Raspberry Pi:
|
||||
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-Addresse, die für die nachfolgenden Transaktionen verwendet wird
|
||||
// Setze die Slave-Adresse für nachfolgende Transaktionen
|
||||
ioctl(driver, I2C_SLAVE, 0x76); // (2)
|
||||
|
||||
// Schreibe die Addresse für das ID-Register
|
||||
// Schreibe die Adresse für das ID-Register
|
||||
uint8_t const write_buf[] = {0xd0};
|
||||
write(driver, write_buf, sizeof(write_buf)); // (3)
|
||||
|
||||
@@ -49,17 +48,19 @@ int main() {
|
||||
```
|
||||
|
||||
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.
|
||||
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 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.
|
||||
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.
|
||||
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
|
||||
@@ -83,7 +84,7 @@ Wie in der Einleitung beschrieben stellt der I²C-Treiber device-Dateien unter `
|
||||
- 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:
|
||||
Implementierte Systemaufrufe:
|
||||
|
||||
#figure(
|
||||
table(
|
||||
@@ -107,7 +108,7 @@ Der GPIO-Treiber stellt zwei Schnittstellen bereit, eine unter `/dev` und eine v
|
||||
- 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:
|
||||
Implementierte Systemaufrufe:
|
||||
|
||||
#figure(
|
||||
table(
|
||||
@@ -117,7 +118,7 @@ Implementierte Syscalls:
|
||||
[`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_LINEINFO_WATCH_IOCTL, <line_info>)`], [Beschafft Informationen über einen GPIO-Pin und macht nachfolgende Änderungen über `read` verfügbar],
|
||||
[`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],
|
||||
@@ -129,17 +130,16 @@ Implementierte 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:
|
||||
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<N>/` angelegt, wobei $N$ die Geräte-ID ist:
|
||||
|
||||
#figure(
|
||||
table(
|
||||
columns: (auto, 1fr),
|
||||
align: horizon,
|
||||
table.header([Sysfs-Eintrag], [Pin-Name]),
|
||||
table.header([Sysfs-Eintrag], [Name des gesampleten Pins]),
|
||||
[in_voltage0_raw], [`Y-`],
|
||||
[in_voltage1_raw], [`sX+`],
|
||||
[in_voltage2_raw], [`sX-`],
|
||||
@@ -152,15 +152,105 @@ Hier wird für jeden der ADC-Pins ein eigener Eintrag unter `/sys/bus/iio/device
|
||||
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.
|
||||
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 stellt das _Timer Counter_ Peripheral bereit; Drei 16-bit Zähler
|
||||
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
|
||||
|
||||
TODO
|
||||
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
|
||||
|
||||
@@ -181,20 +271,22 @@ Zudem sollen in regelmäßigen Abständen Temperatur und Luftfeuchtigkeit vom Se
|
||||
|
||||
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.
|
||||
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 #link("https://de.wikipedia.org/wiki/Prozess-Scheduler", link-text([Scheduling Verfahren])) an.
|
||||
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 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:
|
||||
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",
|
||||
)<fig-i2c-round-robin>
|
||||
|
||||
= Quelltext
|
||||
Der Quelltext dieser Arbeit ist unter #link-text([https://git.veltko.de/Weckyy702/uc-ausarbeitung-linux-treiber]) mit der GPL lizensiert zu finden
|
||||
|
||||
Reference in New Issue
Block a user