Aus Linux-Magazin 07/2021

Kernel- und Treiberprogrammierung unter Linux – Folge 116

© 4itolino / 123RF.com

Damit Anwender intuitiv auf Sensoren und Aktoren zugreifen können, müssen Entwickler einiges beachten. Der Treiber für ein virtuelles Industrial-IO-Gerät zeigt, wie es geht.

Seitdem das Industrial-IO-Subsystem [1] vor neun Jahren produktiv im Linux-Kernel verankert wurde, hat es mächtig an Funktionalität, aber auch an Komplexität gewonnen [2]. Der Benutzer liest Sensorwerte und steuert Aktoren über gut spezifizierte und dokumentierte IO-Kanäle. Die bildet das Subsystem für den intuitiven Zugriff praktischerweise auf virtuelle Dateien ab.

Dabei kann der Anwender sich darauf verlassen, dass diese Kanäle die angeforderten Daten in der definierten Auflösung liefern, beispielsweise in Millivolt oder in Hektopascal. Falls vom Treiber unterstützt, konfiguriert er sich eine automatisierte Messwerterfassung, die unterschiedliche IO-Kanäle zeitgleich beackert und damit konsistente Datensätze liefert.

So praktisch sauber definierte Interfaces zu IIO-Devices für den Nutzer sind, so unübersichtlich gerät zunächst für den Entwickler die Programmierung der benötigten Treibersoftware. Es gilt, den Treiber als solchen im System zu verankern, die unterstützten Geräte detailliert zu spezifizieren, Ressourcen zu reservieren und die eigentlichen Zugriffsfunktionen auf die Hardware zu implementieren.

Dass mit parallelen Zugriffen zu rechnen ist, macht den Code nicht wirklich übersichtlicher. Auf der anderen Seite sorgt das Industrial-IO-Subsystem für die Ablaufsteuerung und die Interaktion mit dem Anwender und entlastet damit den Entwickler ganz entscheidend.

Tradition und Moderne

Der wesentliche Unterschied zwischen einem traditionellen Treiber und einem für IIO-Devices besteht darin, dass beim traditionellen Treiber der Zugriff über eine oder mehrere Gerätedateien stattfindet. Der Entwickler verankert den Treiber also im Kernel und schreibt geeignete »driver_read()«- und »driver_write()«-Funktionen. Dabei hat er alle Freiheiten. Insbesondere legt er die Repräsentation der mit den Applikationen ausgetauschten Daten fest.

Bei der Implementierung als IIO-Device hingegen gilt es, die Geräte respektive die ausgetauschten Daten detailliert in Datenstrukturen zu beschreiben. Anstelle der »driver_read()«- und »driver_write()«-Funktionen gibt es in der einfachsten Variante »read_raw()«- und »write_raw()«- Routinen, die die Hardwaredaten über vordefinierte Variablen austauschen.

Solche Funktionen lassen sich für einfache Anwendungsfälle rasch implementieren. Ein typisches Beispiel zeigt Listing 1: Innerhalb der Funktion »read_raw()« findet der tatsächliche Zugriff auf die Hardware statt, wobei das Ergebnis in zwei Variablen (»val« und »val2«) landet, deren Adressen als Parameter übergeben wurden. Die erste Variable enthält typischerweise den ganzzahligen Anteil, die zweite Variable falls benötigt einen Nachkommaanteil, etwa einen ganzzahligen Nachkommawert in der Größenordnung Micro oder Nano.

Listing 1

Einfache Lesefunktion

static int foo_iio_read_raw(
  struct iio_dev *indio_dev,
  struct iio_chan_spec const *channel,
  int *val, int *val2, long mask)
{
  if (channel->channel==0) {
    read_voltage_from_hardware( val, val2 );
    return IIO_VAL_INT_PLUS_MICRO;
  }
  return -EINVAL;
}

Die Funktion gibt im Erfolgsfall den Typ der abgelegten Daten zurück. »IIO_VAL_INT« beispielsweise besagt, dass sich in »val« der Vorkommawert als Integer befindet. »IIO_VAL_INT_PLUS_NANO« signalisiert, dass in »val2« der Nachkommaanteil in einer Auflösung 10-9 liegt (Abbildung 1). Das IIO-Subsystem bereitet das Ergebnis beim Zugriff durch Nutzer entsprechend dieses Rückgabetyps als Fließkommawert auf.

Abbildung 1: Fließkommawerte werden im Kernel auf Integer-Variablen abgebildet.

Abbildung 1: Fließkommawerte werden im Kernel auf Integer-Variablen abgebildet.

Komplizierter wird es erst dadurch, dass es für alle Kanäle (und deren Attribute) eines IIO-Geräts nur diese eine Lesefunktion gibt. Folglich muss die Funktion sowohl die als Parameter übergebene Kanalnummer (Parameter »channel«) auswerten als auch die Anfrage nach einem potenziellen Attribut (Parameter »mask«). Abhängig davon gilt es anschließend, die Daten zu lesen, aufzubereiten und an den per »val« und »val2« übergebenen Adressen abzulegen (Listing 2). Zu guter Letzt gibt die Funktion zurück, welche Daten (mit oder ohne Nachkomma) sie an den Speicheradressen abgelegt hat.

Listing 2

Kanalauswertung inklusive Attributtypen

static int foo_iio_read_raw(
  struct iio_dev *indio_dev,
  struct iio_chan_spec const *channel,
  int *val, int *val2, long mask)
{
  switch (mask) {
  case IIO_CHAN_INFO_SCALE:
    *val = FOO_SCALE;
    return IIO_VAL_INT;
  case IIO_CHAN_INFO_OFFSET:
    *val = 0;
    *val2= FOO_OFFSET;
    return IIO_VAL_INT_PLUS_MICRO;
  case IIO_CHAN_INFO_PROCESSED:
    if (channel->channel==1) {
      *val = count*FOO_SCALE+FOO_OFFSET;  // fake data
      *val2 = count*FOO_SCALE+FOO_OFFSET; // fake data
      return IIO_VAL_INT_PLUS_NANO;
    }
    return -EINVAL;
  }
  return -EINVAL;
}

Die Schreibfunktion »write_raw()« muss der Entwickler analog implementieren. Sie bekommt die zu schreibende Größe ebenfalls über die zwei Variablen »val« und »val2« übergeben. Auch hier gilt es wieder, die Parameter »channel« und »mask« auszuwerten, falls der Treiber mehrere Kanäle und unterschiedliche Kanalattribute unterstützt.

Einmal Init, bitte!

Neben den Zugriffsfunktionen muss der Entwickler noch die Einbindung des IIO-Treibers in den Kernel durch Aufruf der Funktion »iio_device_register()« implementieren. Dieser Funktion übergibt er eine Datenstruktur des Typs »struct iio_dev« mit der Beschreibung des IIO-Geräts (Abbildung 2). Die gilt es zu initialisieren.

Abbildung 2: IIO-Geräte werden über drei Datenstrukturen beschrieben.

Abbildung 2: IIO-Geräte werden über drei Datenstrukturen beschrieben.

Eine zentrale Rolle spielen dabei die Unterstrukturen »struct iio_info« und »struct iio_chan_spec«. Erstere enthält die Adressen der eigentlichen Zugriffsfunktionen, die die Interaktion mit der Hardware realisieren. Die nicht minder wichtige Struktur »iio_chan_spec« definiert über ein Array die Kanäle, also die vom Treiber zur Verfügung gestellten Daten (Listing 3), gern auch als Makro (siehe Kasten “Kanäle über Makros”).

Listing 3

Kanaldefinition per Datenstruktur

static const struct iio_chan_spec foo_channels[] = {
  {
    .type = IIO_VOLTAGE,
    .indexed  = 1,
    .channel  = 0,
    .address  = 0,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
    .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) | BIT(IIO_CHAN_INFO_OFFSET ),
  },
  {
    .type = IIO_VOLTAGE,
    .indexed  = 1,
    .channel  = 1,
    .address  = 1,
    .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
    .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
  },
  IIO_CHAN_SOFT_TIMESTAMP(2),
};

Kanäle über Makros

Programmierer sind bekanntlich faule Menschen, die unnötige Redundanzen gern vermeiden. So findet man in vielen Treibern des Industrial-IO-Subsystems die Definition eines Kanals per Makro. Das bietet sich immer dann an, wenn ein Sensor mehrere gleichartige Ressourcen bietet, wie etwa die acht Kanäle eines AD-Wandlers. Listing 4 zeigt repräsentativ ein solches Makro, das mehrere Kanäle definiert, die eine Spannung liefern.

Listing 4

Kanalspezifikation per Makro

#define FAKE_VOLTAGE_CHANNEL(num) \
{                                 \
  .type = IIO_VOLTAGE,            \
  .indexed = 1,                   \
  .channel = (num),               \
  .address = (num),               \
  .scan_index = (num),            \
  .info_mask_separate = BIT(IIO_CHAN_INFO_RAW),       \
  .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE)\
}
FAKE_VOLTAGE_CHANNEL(0),
FAKE_VOLTAGE_CHANNEL(1),

Neben dem Typ (zum Beispiel Spannung, Entfernung, Luftfeuchte) werden die Kanalnummer und – über ein Bitfeld – die unterstützten Kanalattribute angegeben. Dabei entscheidet der Entwickler, welche Attribute für jeden Kanal separat (Variable »info_mask_separate«) und welche für alle Kanäle gemeinsam (»info_mask_shared_by_type«) gelten. Zurzeit stehen 27 unterschiedliche Attribute zur Verfügung. Neben den Klassikern Rohwert, aufbereiteter Wert, Offset und Skalierung tummeln sich darunter auch Attribute wie Phase, Hysterese oder Frequenz.

Als Letztes steht noch die Antwort auf die Frage aus, wo und wann die Anmeldung des IIO-Geräts innerhalb eines Gerätetreibers stattfindet. Das hängt von der Art der Anbindung der Hardware ab.

Bei einem per I2C angebundenen Sensor beispielsweise meldet sich der Gerätetreiber beim I2C-Subsystem an und registriert das IIO-Gerät, sobald I2C die zugehörige Hardware erkannt respektive als vorhanden gemeldet bekommen hat.

Für Hardware, die sich nicht direkt einem klassischen Bussystem zuordnen lässt, hat Kernel-Chef Torvalds den Plattformbus integriert. Den verwenden wir als Basis in unserem Beispielcode und definieren daher einen Plattformtreiber samt Plattformgerät.

Plattform-Device

Das Plattform-Device, das eine Probe- und eine Remove-Funktion zur Verfügung stellt, wird beim Laden des Treibers instanziiert, was einen Aufruf der Probe-Funktion auslöst. Innerhalb dieser Funktion wird das virtuelle IIO-Device beim Kernel über »iio_device_register()« angemeldet (Listing 5).

Listing 5

Probe-Funktion

static int foo_pdrv_probe
 (struct platform_device *pdev)
{
  int ret;
  struct iio_dev *indio_dev;
  struct foo_private_data *data;
  indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*data));
  if (!indio_dev)
    return -ENOMEM;
  indio_dev->dev.parent = &pdev->dev;
  indio_dev->info = &foo_iio_info;
  indio_dev->name = KBUILD_MODNAME;
  indio_dev->modes = INDIO_DIRECT_MODE;
  indio_dev->channels = foo_channels;
  indio_dev->num_channels = ARRAY_SIZE(foo_channels);
  ret = iio_device_register(indio_dev);
  if (ret < 0) {
    return -EINVAL;
  }
  platform_set_drvdata(pdev, indio_dev);
  return 0;
}

Den vollständigen Beispielcode »iio-foo.c« zusammen mit einem Makefile haben wir für Sie zum Download bereitgestellt [5]. Haben Sie schon einmal Kernel-Code auf Ihrer Maschine generiert, dann genügt die Eingabe von »make« auf der Konsole, damit das Kernel-Build-System aus dem Quellcode »iio-foo.c« den Treiber »iio-foo.ko« erzeugt (Abbildung 3). Ansonsten müssten Sie unter Ubuntu oder Pi OS vorher noch über »sudo apt install build-essential flex bison« notwendige Pakete nachinstallieren.

Abbildung 3: Direkt nach dem Generieren und Laden steht das IIO-Ger&auml;t zur Verf&uuml;gung.

Abbildung 3: Direkt nach dem Generieren und Laden steht das IIO-Gerät zur Verfügung.

Vor dem Laden des per »make« generierten Treibers muss allerdings der Treibercode für das IIO-Subsystem im Kernel liegen. Daher steht vor dem »insmod iio-foo.ko« erst einmal ein »modprobe industrialio« an. Geht alles gut – erscheint also keine Fehlermeldung – ploppt unterhalb des Verzeichnisses »/sys/bus/iio/devices/« ein neuer Ordner auf. War bisher kein weiteres IIO-Device aktiviert, heißt er »iio:device0«.

Ein in dem Verzeichnis ausgeführtes »ls« zeigt die instanziierten Kanäle, insbesondere »in_voltage0_raw« und »in_voltage1_input«. Der Zugriff per »cat in_voltage0_raw« gibt die über einen simplen Zähler simulierte Spannung als Fließkommawert zurück. Der Zähler und damit die Spannung wird übrigens bei jedem Zugriff inkrementiert.

Der Zugriff auf »in_voltage1_input« liefert die bereits skalierte Spannung. Im Beispiel wird unser Fake-Wert zur Differenzierung skaliert und der Offset aufaddiert. Außerdem unterstützt der Treiber die Attribute »in_voltage_scale« und »in_voltage_offset«, die beide beim Lesen einen konstanten Wert liefern.

Mit Puffer

In der letzten Kern-Technik haben wir bereits gezeigt, dass und wie das IIO-Subsystem Trigger und Puffer unterstützt, um unterschiedliche Messwerte zeitgleich als konsistente Datensätze zu erfassen. Das funktioniert allerdings nur dann, wenn der Treiber das auch unterstützt.

Schade, dass sich viele Programmierer den Code dazu sparen. Das mag daran liegen, dass man dafür nicht auf die bereits vorhandenen Lese- oder Schreibfunktionen (»read_raw()«, »write_raw()«) zurückgreifen kann, sondern eine eigene Zugriffsfunktion her muss (Listing 6). Sie nimmt für jeden überwachten Kanal den Zugriff vor und legt das Ergebnis im Rohformat an die zugehörige Stelle in einen Datenpuffer.

Listing 6

Mehrere Kanäle zeitgleich Lesen

static irqreturn_t foo_trigger_handler( int irq, void *p )
{
  struct iio_poll_func *pf = p;
  struct iio_dev *indio_dev = pf->indio_dev;
  struct foo_private_data *data;
  int bit=0, i=0;
  s64 time_ns = iio_get_time_ns( indio_dev );
  data = iio_priv(indio_dev);
  // get data...
  mutex_lock( &data->lock );
  for_each_set_bit( bit, indio_dev->active_scan_mask,
    indio_dev->masklength ) {
    ((s16 *)data->buffer)[i] = i+1; // fake data
    i++; // fake data
  }
  mutex_unlock( &data->lock );
  iio_push_to_buffers_with_timestamp( indio_dev,
      data->buffer, time_ns );
  iio_trigger_notify_done( indio_dev->trig );
  return IRQ_HANDLED;
}

Den liest der Anwender schließlich über eine klassische Gerätedatei aus (in Abgrenzung zu den Dateien im Sys-Filesystem). Dazu übergibt man dem IIO-Subsystem den Puffer inklusive Zeitstempel. Da parallel zum automatisierten Zugriff auch ein manueller Zugriff möglich ist, gibt es einen kritischen Abschnitt, den zumeist ein Mutex sichert.

Mutex und Puffer benötigen Speicher, den der Entwickler pfiffigerweise direkt zusammen mit der Datenstruktur »iio_dev« reserviert. Das spart ein unnötiges Malloc und später die zugehörige Freigabe. Die Größe des Buffers muss übrigens eine Zweierpotenz sein.

Außerdem implementiert der Entwickler die Zugriffsfunktion, die das IIO-Subsystem aufruft. Dabei handelt es sich um eine Interrupt-Service-Routine, weshalb beim Zugriff ein Schlafenlegen tabu ist. Der Code holt in einer Schleife für jeden überwachten Kanal die Daten und legt sie im Puffer ab. Außerdem gilt es, einen Zeitstempel als 64-Bit-Wert zu lesen, was über die Funktion »iio_get_time_ns()« erfolgen kann. Die Daten werden dem IIO-Subsystem übergeben, das sie dann quittiert.

Der Entwickler darf nicht vergessen, auch die Kanalspezifikation noch für jeden Kanal zu erweitern (Listing 7). Der Zugriff über die Puffer findet grundsätzlich auf die Rohwerte statt. Daher wird die exakte Repräsentationsform des zugehörigen Werts angegeben, also ob er im Little- oder im Big-Endian-Format vorliegt, wie viele Bits er hat und ob er vor der Nutzung noch um ein paar Bits verschoben werden muss. Außerdem gilt es, beim Kanal über die Angabe einer Nummer zu spezifizieren, an welcher Position im Buffer der Rohwert abgelegt wird.

Listing 7

Spezifikation erweitern

.scan_index = 0,
.scan_type = {
  .sign = 's',
  .realbits = 12,
  .storagebits = 16,
  .shift = 4,
  .endianness = IIO_LE,
},

Ein Bitfeld, die Scan-Maske, spezifiziert die zu überwachenden Kanäle (Listing 8). Das Bit an der Position X steht für den Kanal X, ein Null-Eintrag signalisiert das Ende des Felds. Die Scan-Maske wird vor dem Anmelden des IIO-Geräts beim IIO-Subsystem an die Datenstruktur »struct iio_dev« geknüpft.

Listing 8

Scan-Maske

static const unsigned long foo_scan_masks[] = {
  3, // scan support for channel 0 and 1 (b0011)
  0  // end
};

Das eigentliche Einklinken des Trigger-Mechanismus findet direkt vor dem Registrieren des Geräts beim IIO-Subsystem (»iio_device_register()«) durch Aufruf der Funktion »iio_triggered_buffer_setup()« statt (Listing 9). Dieser Funktion übergibt der Entwickler neben dem IIO-Gerät die Adresse der implementierten Trigger-Funktion sowie die gewünschte Funktion (hier »io_pollfunc_store_time«).

Listing 9

Trigger-Support initialisieren

[...]
indio_dev->available_scan_masks = foo_scan_masks;
data = iio_priv(indio_dev);
mutex_init(&data->lock);
ret = iio_triggered_buffer_setup(indio_dev,
  iio_pollfunc_store_time,
  foo_trigger_handler, NULL);
if (ret < 0)
  return -EINVAL;
ret = iio_device_register(indio_dev);
[...]

Jetzt fehlt nur noch das Aufräumen, sprich: das Abmelden des Triggers durch Aufruf von »iio_triggered_buffer_cleanup()«. Das wird zum einen im Fehlerfall aufgerufen (falls das Anmelden fehlschlägt), zum anderen vor dem Abmelden des IIO-Geräts vom IIO-Subsystem (Listing 10).

Listing 10

Abmelden des IIO-Geräts

static int foo_pdrv_remove(struct platform_device *pdev)
{
  struct iio_dev *indio_dev = platform_get_drvdata(pdev);
  iio_triggered_buffer_cleanup( indio_dev );
  iio_device_unregister( indio_dev );
  return 0;
}

Damit der Trigger auslöst, muss man noch die Module »industrialio_triggered_buffer« und »iio_trig_hrtimer« laden. Dann wird der Trigger angelegt [3] und dem IIO-Gerät zugeordnet, die Sampling-Frequenz eingestellt, das Buffering konfiguriert, die zu überwachenden Kanäle aktiviert und schließlich die Datenerfassung gestartet (Abbildung 4).

Abbildung 4: Es erfordert etwas Konfiguration, die Trigger zum Laufen zu bringen.

Abbildung 4: Es erfordert etwas Konfiguration, die Trigger zum Laufen zu bringen.

Weiter geht’s

Greift die Applikation anschließend auf die Gerätedatei »/dev/iio:device0« zu, liest sie die beiden Kanäle inklusive des Zeitstempels aus. Letzterer liefert die Anzahl der Nanosekunden an, die seit dem 1.1.1970 vergangen sind.

Neben dem Trigger bietet das IIO-Subsystem noch einen Event-Mechanismus an, den wir hier nicht weiter thematisieren. Daneben haben die Macher einen High-Speed-Zugriff implementiert, der hardwaretechnisch auf die Datenerfassung per DMA setzt und softwaretechnisch ein möglichst weitgehendes Zero-Copy realisiert [4]. Damit wäre treiberseitig alles an Bord, es fehlen nur noch echte Sensoren. (jlu)

Die Autoren

Eva-Katharina Kunst ist seit den Anfängen von Linux Fan von Open Source. Jürgen Quade, Professor an der Hochschule Niederrhein, führt auch für Unternehmen Schulungen zu den Themen Treiberprogrammierung und Embedded Linux durch.

Infos

  1. Industrial IO (Linux-Kernel-Dokumentation): https://www.kernel.org/doc/html/latest/driver-api/iio/index.html
  2. Kern-Technik: Eva-Katharina Kunst, Jürgen Quade, “Industrielle Kanäle”, LM 05/2021, S. 78, https://www.lm-online.de/44255
  3. Kern-Technik: Eva-Katharina Kunst, Jürgen Quade, “Busfahrt”, LM 03/2021, S. 72, https://www.lm-online.de/44070
  4. “High-Speed Data Acquisition using the Linux Industrial IO Framework”: https://events.static.linuxfound.org/sites/events/files/slides/iio_high_speed.pdf
  5. Beispielcode: https://www.lm-online.de/dl/44538
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben