OpenBSD Treiberentwicklung Beispiel anhand meines PCF8574 GPIO Treibers

#1
Hallo Community!

Ich habe vor kurzer Zeit einen GPIO Treiber für den I2C Baustein PCF8574(A) von NXP geschrieben
und möchte gerne dieses Wissen anhand dieses Treiber weitergeben. Dieses Tutorial soll also eine
kleine Hilfe für eure eigenen Treiber sein.

Was Ihr benötigt:
- Optimal wären zwei Systeme (Ein System zum entwickeln und ein System zum testen)
- Dokumentation für den Baustein (Falls ein öffentliches Datenblatt nicht verfügbar ist, dann
könnt Ihr auch die Informationen im Quellcode von anderen freien Betriebssystemen erhalten, was
natürlich meiner Meinung nach schwerer ist)
- Quellcode von OpenBSD

In diesem Beispiel nehme ich meinen Treiber her. Also wir schreiben einen GPIO Treiber. Codefragmente
davon sind jeweils bei anderen Treibern immer dieselben.

Die Treiber befinden sich im Quellcode von OpenBSD unter /usr/src/sys/dev. Schaut in diese Treiber
auch mal rein. Hier findet Ihr auch nützliche Informationen für eure eigenen Treiber. Eine gute Anlaufstelle
ist folgendes Dokument:

https://www.openbsd.org/papers/eurobsdcon2017-device-drivers.pdf

Wir schreiben einen Treiber für den guten alten I2C Baustein PCF8574. Da dieser Baustein über das I2C Interface
angesprochen wird, platzieren wir den Treiber unter /usr/src/sys/dev/i2c. Ich habe die Datei vom Treiber
hier pcf8574.c benannt.

Das Grundkonstrukt einer Treibers in OpenBSD Treibers, damit er vom Betriebssystem eingebunden werden kann,
besteht aus folgenden Codefragmenten:

Code:
struct pcfgpio_softc {
    struct device             sc_dev;
    i2c_tag_t                sc_tag;
    i2c_addr_t                sc_addr;
    int                        sc_node;

    u_int8_t                sc_npins;

    struct gpio_chipset_tag    sc_gpio_gc;
    gpio_pin_t                sc_gpio_pins[PCFGPIO_NPINS];
};
Das ist die Struktur die wir nachfolgend in unseren Funktionen verwenden. sc_dev (struct device) und
sc_node ist das Minimum (für den Kernel) was enthalten sein muss. Die anderen Variablen und Strukturen
sind dementsprechend anzupassen. Das es sich um einen I2C Treiber handelt, definieren wir in dieser Struktur noch
sc_tag (i2c_tag_t) und die I2C Adresse sc_addr (i2c_addr_t) des Bausteines. Jetzt kommen wir zu den
nötigen Definitionen für das GPIO Interface. Diese besteht aus den Variablen sc_npins (die Anzahl der PINs die
angesteuert werden können), sc_gpio_gc ist der gpio controller (struct gpio_chipset_tag) und noch
sc_gpio_pins (gpio_pin_t). Dieses Array enthält die PIN Information (Status, Input oder Output und
sonstige Information).

Code:
struct cfattach pcfgpio_ca = {
    sizeof(struct pcfgpio_softc), pcfgpio_match, pcfgpio_attach
};
Eine Struktur mit der der Kernel den Treiber ansteuert. Es enthält die Größe der vorigen Struktur und dann kommen
die Funktionszeiger mit der das Gerät erkannt wird (match) und der Funktionszeiger mit der das Gerät eingebunden
wird (attach).

Code:
struct cfdriver pcfgpio_cd = {
    NULL, "pcfgpio", DV_DULL
};
Hier wird der Name des Treibers definiert.

Code:
int
pcfgpio_match(struct device *parent, void *match, void *aux)
{
    struct i2c_attach_args *ia = aux;

    if ((strcmp(ia->ia_name, "nxp,pcf8574") == 0) ||
        (strcmp(ia->ia_name, "nxp,pcf8574a") == 0))
        return (1);
    return (0);
}
Die Match Funktion. Wie Ihr sehen könnt werden zwei Typen vom PCF8574 unterstützt.
Beide haben unterschiedliche I2C Adressen.

Code:
void
pcfgpio_attach(struct device *parent, struct device *self, void *aux)
{
    struct pcfgpio_softc *sc = (struct pcfgpio_softc *)self;
    struct i2c_attach_args *ia = aux;
    struct gpiobus_attach_args gba;
    int i;

    sc->sc_tag = ia->ia_tag;
    sc->sc_addr = ia->ia_addr;
    sc->sc_npins = PCFGPIO_NPINS;
    sc->sc_node = *(int *)ia->ia_cookie;

    printf("\n");

    for (i = 0; i < sc->sc_npins; i++) {
        sc->sc_gpio_pins[i].pin_num = i;
        sc->sc_gpio_pins[i].pin_caps = GPIO_PIN_INOUT | 
            GPIO_PIN_OPENDRAIN | GPIO_PIN_INVOUT;

        sc->sc_gpio_pins[i].pin_flags = GPIO_PIN_INPUT;
        sc->sc_gpio_pins[i].pin_state = 0;
    }

    pcfgpio_write(sc, 0xFF);

    sc->sc_gpio_gc.gp_cookie = sc;
    sc->sc_gpio_gc.gp_pin_read = pcfgpio_pin_read;
    sc->sc_gpio_gc.gp_pin_write = pcfgpio_pin_write;
    sc->sc_gpio_gc.gp_pin_ctl = pcfgpio_pin_ctl;

    gba.gba_name = "gpio";
    gba.gba_gc = &sc->sc_gpio_gc;
    gba.gba_pins = sc->sc_gpio_pins;
    gba.gba_npins = sc->sc_npins;
    config_found(&sc->sc_dev, &gba, gpiobus_print);
}
Die Attach Funktion. Diese Funktion wird für die Initialisierung benötigt. Die for Schleife setzt die Eigenschaften
der PINs vom GPIO device. Das Statement pcfgpio_write(sc, 0xFF) sorgt dafür, dass im Anfangsstadium die PINs
als Eingänge definiert sind. Danach werden die Funktionszeiger für den GPIO controller zugewiesen. Anschließend
werden noch die jeweiligen Variablen gesetzt, damit der Treiber im Userland angesprochen werden kann.

Die restlichen Funktionen sollten eigentlich selbsterklärend sein. Ich habe den Treiber zwar in einem anderen Thread
schon gepostet, aber hier nochmal komplett:
Code:
/* Driver for the NXP PCF8574(A) Remote 8-bit I/O expander */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/gpio.h>

#include <dev/i2c/i2cvar.h>
#include <dev/gpio/gpiovar.h>

#define PCFGPIO_NPINS        8

struct pcfgpio_softc {
    struct device             sc_dev;
    i2c_tag_t                sc_tag;
    i2c_addr_t                sc_addr;
    int                        sc_node;

    u_int8_t                sc_npins;

    struct gpio_chipset_tag    sc_gpio_gc;
    gpio_pin_t                sc_gpio_pins[PCFGPIO_NPINS];
};

int pcfgpio_match(struct device *, void *, void *);
void pcfgpio_attach(struct device *, struct device *, void *);
uint8_t pcfgpio_read(struct pcfgpio_softc *);
void pcfgpio_write(struct pcfgpio_softc *, uint8_t);
int pcfgpio_pin_read(void *, int);
void pcfgpio_pin_write(void *, int, int);
void pcfgpio_pin_ctl(void *, int, int);

struct cfattach pcfgpio_ca = {
    sizeof(struct pcfgpio_softc), pcfgpio_match, pcfgpio_attach
};

struct cfdriver pcfgpio_cd = {
    NULL, "pcfgpio", DV_DULL
};

int
pcfgpio_match(struct device *parent, void *match, void *aux)
{
    struct i2c_attach_args *ia = aux;

    if ((strcmp(ia->ia_name, "nxp,pcf8574") == 0) ||
        (strcmp(ia->ia_name, "nxp,pcf8574a") == 0))
        return (1);
    return (0);
}

void
pcfgpio_attach(struct device *parent, struct device *self, void *aux)
{
    struct pcfgpio_softc *sc = (struct pcfgpio_softc *)self;
    struct i2c_attach_args *ia = aux;
    struct gpiobus_attach_args gba;
    int i;

    sc->sc_tag = ia->ia_tag;
    sc->sc_addr = ia->ia_addr;
    sc->sc_npins = PCFGPIO_NPINS;
    sc->sc_node = *(int *)ia->ia_cookie;

    printf("\n");

    for (i = 0; i < sc->sc_npins; i++) {
        sc->sc_gpio_pins[i].pin_num = i;
        sc->sc_gpio_pins[i].pin_caps = GPIO_PIN_INOUT | 
            GPIO_PIN_OPENDRAIN | GPIO_PIN_INVOUT;

        sc->sc_gpio_pins[i].pin_flags = GPIO_PIN_INPUT;
        sc->sc_gpio_pins[i].pin_state = 0;
    }

    pcfgpio_write(sc, 0xFF);

    sc->sc_gpio_gc.gp_cookie = sc;
    sc->sc_gpio_gc.gp_pin_read = pcfgpio_pin_read;
    sc->sc_gpio_gc.gp_pin_write = pcfgpio_pin_write;
    sc->sc_gpio_gc.gp_pin_ctl = pcfgpio_pin_ctl;

    gba.gba_name = "gpio";
    gba.gba_gc = &sc->sc_gpio_gc;
    gba.gba_pins = sc->sc_gpio_pins;
    gba.gba_npins = sc->sc_npins;
    config_found(&sc->sc_dev, &gba, gpiobus_print);
}

uint8_t
pcfgpio_read(struct pcfgpio_softc *sc)
{
    uint8_t val;

    iic_acquire_bus(sc->sc_tag, 0);
    if (iic_exec(sc->sc_tag, I2C_OP_READ_WITH_STOP, sc->sc_addr,
        NULL, 0, &val, sizeof(val), 0)) {
        printf("%s: pcfgpio_read: failed to read\n",
            sc->sc_dev.dv_xname);
        iic_release_bus(sc->sc_tag, 0);
        return (0);
    }
    iic_release_bus(sc->sc_tag, 0);
    return val;
}

void
pcfgpio_write(struct pcfgpio_softc *sc, uint8_t val)
{
    iic_acquire_bus(sc->sc_tag, 0);
    if (iic_exec(sc->sc_tag, I2C_OP_WRITE_WITH_STOP, sc->sc_addr,
        &val, sizeof val, NULL, 0, 0)) {
        printf("%s: pcfgpio_write: failed to write\n",
            sc->sc_dev.dv_xname);
        iic_release_bus(sc->sc_tag, 0);
        return;
    }
    iic_release_bus(sc->sc_tag, 0);
}

int
pcfgpio_pin_read(void *arg, int pin)
{
    struct pcfgpio_softc *sc = arg;
    uint8_t tmp;

    if (pin >= sc->sc_npins)
        return 0;
    if (!ISSET(sc->sc_gpio_pins[pin].pin_flags, GPIO_PIN_INPUT))
        pcfgpio_pin_write(sc, pin, GPIO_PIN_HIGH);
    tmp = pcfgpio_read(sc);
    if (tmp & (1 << pin))
        sc->sc_gpio_pins[pin].pin_state = GPIO_PIN_HIGH;
    else
        sc->sc_gpio_pins[pin].pin_state = GPIO_PIN_LOW;
    return sc->sc_gpio_pins[pin].pin_state;
}

void
pcfgpio_pin_write(void *arg, int pin, int val)
{
    struct pcfgpio_softc *sc = arg;
    int i;
    uint8_t tmp = 0x00;

    if (pin >= sc->sc_npins)
        return;
    for (i = 0; i < sc->sc_npins; i++)
        if (sc->sc_gpio_pins[i].pin_state == GPIO_PIN_LOW)
            tmp |= (1 << i);
    if (val == GPIO_PIN_HIGH) {
        tmp &= ~(1 << pin);
        pcfgpio_write(sc, tmp);
        sc->sc_gpio_pins[pin].pin_state = GPIO_PIN_HIGH;
    } else {
        tmp |= 1 << pin;
        pcfgpio_write(sc, tmp);
        sc->sc_gpio_pins[pin].pin_state = GPIO_PIN_LOW;
    }
}

void
pcfgpio_pin_ctl(void *arg, int pin, int flags)
{
    struct pcfgpio_softc *sc = arg;

    if (ISSET(flags, GPIO_PIN_INPUT)) {
        pcfgpio_pin_write(sc, pin, GPIO_PIN_HIGH);
        sc->sc_gpio_pins[pin].pin_flags = GPIO_PIN_INPUT;
    } else
        sc->sc_gpio_pins[pin].pin_flags = GPIO_PIN_OUTPUT;
}
Damit der Treiber vom Config System vom Kernel erkannt wird und kompiliert werden kann, folgendes in
der Datei /usr/src/sys/dev/i2c/files.i2c hinzufügen:
Code:
device    pcfgpio: gpiobus
attach    pcfgpio at i2c
file    dev/i2c/pcf8574.c            pcfgpio
Da dies mein erster OpenBSD Treiber ist und ich somit noch ein Grünschnabel bin, können in den Erklärungen
noch falsche Annahmen (bezüglich Kernel) von mir enthalten sein. Sollte jemand diese finden, dann könnt Ihr
mich gerne darauf hinweisen und korrigieren.

Ich hoffe euch gefällt mein Tutorial. Freue mich über jedes Feedback!

Edit:
Habe gerade bemerkt, dass es ein eigenes Unterforum mit Tutorials gibt. Bitte diesen wenn möglich Thread verschieben.
 
Zuletzt bearbeitet:

Chromatin

Moderator
Mitarbeiter
#4
Super Text, Danke Dir!

Zur Praktischen Anwendung vielleicht noch:

Wie stellt sich das Device denn dem OS/User/Coder dar und wie kann es angesprochen werden?
 
#5
[...] Zur Praktischen Anwendung vielleicht noch:

Wie stellt sich das Device denn dem OS/User/Coder dar und wie kann es angesprochen werden?
Device Tree:
Damit der Baustein erkannt wird muss dieser zusätliche Eintrag in den Device Trees stehen:
Code:
/dts-v1/;
/include/ "rk3328-rock64.dts"

&i2c0 {
    status = "okay";

    pcf8574: gpio@20 {
        compatible = "nxp,pcf8574";
        status = "okay";
        reg = <0x20>;
        #gpio-cells = <2>;
        gpio-controller;
    };
};
Das ist ein Beispiel von meinem Testboard. Die Adresse des Bausteines (reg) und des I2C Interface (&i2c0) dementsprechend Eures
Boards und Schaltung anpassen.

Nützliche Erklärung bezüglich Device Trees:
https://events.static.linuxfound.org/sites/events/files/slides/petazzoni-device-tree-dummies.pdf

Kernel Konfiguration und Kompilieren:
Code:
$ cd /sys/arch/$(machine)/conf
$ cp GENERIC PCF8574
Damit der Treiber vom Kernel eingebunden wird, folgendes in der Datei PCF8574, die wir erstellt haben
hinzufügen (Ihr könnt auch wie Ihr wollt einen anderen Namen für eure Kernel Konfiguration verwenden):
Code:
pcfgpio*    at iic?
gpio*       at pcfgpio?
Denn Kernel kompilieren:
Code:
$ config PCF8574
$ cd ../compile/PCF8574
$ make -j4
Details zum Kompilieren entnehmt Ihr von der man-page von OpenBSD:
OpenBSD FAQ: Building the System from Source

Hier ist die dmesg Ausgabe vom Treiber:
Code:
pcfgpio0 at iic0 addr 0x20
gpio0 at pcfgpio0: 8 pins
Der Treiber wurde erfolgreich eingebunden und kann bentutzt werden.

Sicherheitslevel
Damit der Treiber im Userland verwendet werden kann, müsst Ihr den Standard Sicherheitslevel (securelevel) von OpenBSD anpassen.
In der Standardeinstellung erlaubt der Kernel nicht die Kommunikation mit GPIO Treibern. Auch nicht wenn Ihr als root eingeloggt seid.

Dazu eine Datei mit den Namen rc.securlevel unter /etc anlegen und folgenden Inhalt hinzufügen:
Code:
sysctl kern.securelevel=-1
Der Sicherheitslevel wird hier auf -1 gesetzt, weil der Kernel beim Booten den Sicherheitslevel um einen Schritt erhöht. Deswegen
ist nach dem Booten der Sicherheitslevel auf 0 eingestellt. Mit diesem Level kann der GPIO Treiber angesprochen werden.

Details zum Securelevel entnehmt Ihr von der man-page von OpenBSD:
securelevel(7) - OpenBSD manual pages

Ansteuerung mit dem Progamm gpioctl
FolgenderBefehl schaltet einen Pin ein:
Code:
gpioctl /dev/gpio0 0 on
Hier wird die Gerätedatei /dev/gpio0 angegeben, die wir mit dmesg ermittelt haben. Danach folgt die Pinnummer die gesetzt werden
soll und der Status.

Details zu gpioctl entnehmt Ihr von der man-page von OpenBSD:
gpioctl(8) - OpenBSD manual pages

Ansteuerung mit ioctls (natives Progamm):
Wenn wer ein natives Programm lieber mag, hier ein simples Lauflicht:
Code:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/gpio.h>
#include <sys/ioctl.h>

int main(int argc, char *argv[])
{
    int i, fd;
    int ret;
    struct gpio_info ginfo;
    struct gpio_pin_op gop;
    
    if (argc < 2) {
        printf("usage: gpio [DEV]\n");
        return 1;
    }

    fd = open(argv[1], O_RDWR);

    if (fd == -1) {
        printf("error: couldn't open file (%s)\n", strerror(errno));
        return 1;
    }

    ret = ioctl(fd, GPIOINFO, &ginfo);

    if (ret == -1) {
        printf("error: ioctl GPIOINFO failed (%s)\n", strerror(errno));
        close(fd);
        return 1;
    }

    printf("gpio: Number of pins: %d\n", ginfo.gpio_npins);

    while (1) {
        for (i = 0; i < ginfo.gpio_npins; i++) {
            gop.gp_pin = i;
            gop.gp_value = GPIO_PIN_HIGH;
            ret = ioctl(fd, GPIOPINWRITE, &gop);

            if (ret == -1) {
                printf("error: ioctl GPIOPINWRITE failed (%s)\n", strerror(errno));
                close(fd);
                return 1;
            }

            sleep(1);
            gop.gp_value = GPIO_PIN_LOW;
            ret = ioctl(fd, GPIOPINWRITE, &gop);

            if (ret == -1) {
                printf("error: ioctl GPIOPINWRITE failed (%s)\n", strerror(errno));
                close(fd);
                return 1;
            }
        }
    }

    close(fd);
    return 0;
}
Details zu den ioctls entnehmt Ihr von der man-page von OpenBSD:
gpio(4) - OpenBSD manual pages
 
Zuletzt bearbeitet:
Oben