Dezvoltare drivere pentru Linux

De la YO3ITI
Sari la navigare Sari la căutare

Aceste pagini conțin o colecție de informații necesare în special celor care doresc să programeze diverse pe Linux (sau Raspberry – fiindcă e același lucru). Am început să adun această documentație pentru dezvoltarea driverelor de care am nevoie pentru diferitele proiecte electronice pe Raspberry.

Pe măsură ce le adun, o să le și structurez mai bine.

Condiții inițiale

Verificarea versiunii kernel-ului

Cazul platformei mele este un Raspberry Pi 4, versiunea c03112 cu un kernel versiunea 5.4.51-v7l+ (pentru lista completă de coduri de versiuni, vezi aici):

tom@rpi-yo3iti:~/c/drivere/01 $ uname -a
Linux rpi-yo3iti 5.4.51-v7l+ #1327 SMP Thu Jul 23 11:04:39 BST 2020 armv7l GNU/Linux

Sau, prin /proc/version:

tom@rpi-yo3iti:~/c/drivere/01 $ cat /proc/version
Linux version 5.4.51-v7l+ (dom@buildbot) (gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) #1327 SMP Thu Jul 23 11:04:39 BST 2020

Sau prin dmesg:

tom@rpi-yo3iti:~/c/drivere/01 $ dmesg |grep Linux
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 5.4.51-v7l+ (dom@buildbot) (gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) #1327 SMP Thu Jul 23 11:04:39 BST 2020
[    1.126787] usb usb1: Manufacturer: Linux 5.4.51-v7l+ xhci-hcd
[    1.128576] usb usb2: Manufacturer: Linux 5.4.51-v7l+ xhci-hcd
[   19.684585] mc: Linux media interface: v0.10
[   19.719580] videodev: Linux video capture interface: v2.00

Verificarea versiunii antetelor de kernel (kernel headers)

Fișierele antet (header) sunt necesare compilatorului pentru a verifica dacă o anumită funcție este corect utilizată într-un program. Această verificare se mai numește verificarea semnăturii unei funcții. Pentru verificarea semnăturii funcțiilor nu este necesar codul complet al implementării funcțiilor ci doar definiția lor. Ca atare, limitarea doar la includerea header-elor oferă o mare economie de cod și memorie.

În cazul concret al dezvoltării driverelor, header-ele kernelului trebuie să se potrivească cu versiunea de kernel:

tom@rpi-yo3iti:~/c/drivere/01 $ apt search linux-header*
Sorting... Done
Full Text Search... Done
aufs-dkms/stable 4.19+20190211-1 all
  DKMS files to build and install aufs

linux-headers-4.18.0-3-common/stable 4.18.20-2+rpi1 all
  Common header files for Linux 4.18.0-3

linux-headers-4.18.0-3-common-rt/stable 4.18.20-2+rpi1 all
  Common header files for Linux 4.18.0-3-rt

linux-headers-4.9.0-6-all/stable 4.9.82-1+deb9u3+rpi2 armhf
  All header files for Linux 4.9 (meta-package)

linux-headers-4.9.0-6-all-armhf/stable 4.9.82-1+deb9u3+rpi2 armhf
  All header files for Linux 4.9 (meta-package)

[...]

... sper să nu avem probleme. :D Mai întâi trebuie instalate header-ele pentru kernel:

sudo rpi-update stable

Apoi:

apt get install raspberrypi-kernel-headers

Exemple generice, elemente de bază

Primul exemplu super simplu

#include <linux/module.h> /* necesar tuturor modulelor */
#include <linux/kernel.h> /* necesar KERN_INFO */

int init_module(void)
{
        printk(KERN_INFO "Hello world 1.\n");

        /*
         * A non 0 return means init module failed; module can't be loaded
         */
        return 0;
}

void cleanup_module(void) 
{
        printk(KERN_INFO "Goodbye world 1.\n");
}

Un fișier Makefile simplu:

obj-m += hello-1.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Apoi:

tom@rpi-yo3iti:~/c/drivere/Salzman/01 $ make
make -C /lib/modules/5.4.51-v7l+/build M=/home/tom/c/drivere/Salzman/01 modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.51-v7l+'
  CC [M]  /home/tom/c/drivere/Salzman/01/hello-1.o
  Building modules, stage 2.
  MODPOST 1 modules
WARNING: modpost: missing MODULE_LICENSE() in /home/tom/c/drivere/Salzman/01/hello-1.o
see include/linux/module.h for more information
  CC [M]  /home/tom/c/drivere/Salzman/01/hello-1.mod.o
  LD [M]  /home/tom/c/drivere/Salzman/01/hello-1.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.51-v7l+'

Odată cu versiunea 2.6 de kernel este adoptată o nouă convenție pentru denumirea fișierelor: modulele kernel au extensia .ko în locul extensiei vechi .o pentru a fi diferențiate de fișierele-obiect clasice. Modulele kernel conțin o secțiune .modinfo suplimentară, unde este păstrată informația despre modul. Informația poate fi accesată cu comanda modinfo hello-*.ko:

tom@rpi-yo3iti:~/c/drivere/Salzman/01 $ modinfo hello-1.ko
filename:       /home/tom/c/drivere/Salzman/01/hello-1.ko
srcversion:     140276773A3090F6F33891F
depends:        
name:           hello_1
vermagic:       5.4.51-v7l+ SMP mod_unload modversions ARMv7 p2v8

Al doilea exemplu

Metoda acceptată (modernă) pentru definirea metodelor de inițializare (init) și ieșire (exit) este prezentată mai jos. Se poate alege orice denumire pentru aceste metode cu condiția de a menționa funcționalitatea prin module_init și module_exit ambele evidențiate în exemplul de mai jos:

/*
 * hello-2.c - demo pentru module_init() și module_exit()
 * Metoda mai nouă, preferată pentru init_module() și cleanup_module()
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_2_init(void)
{
        printk(KERN_INFO "Hello, world 2\n");
        return 0;
}

static void __exit hello_2_exit(void)
{
        printk(KERN_INFO "Goodbye, world 2\n");
}

module_init(hello_2_init);
module_exit(hello_2_exit);

Acum avem două module kernel. Fișierul Makefile arată așa (pentru ambele module):

obj-m += hello-1.o
obj-m += hello-2.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Compilarea:

Fișierele existente:

tom@rpi-yo3iti:~/c/drivere/Salzman/02 $ ls -lsa
total 20
4 drwxr-xr-x 2 tom  tom  4096 Jul 28 19:25 .
4 drwxr-xr-x 4 tom  tom  4096 Jul 28 19:16 ..
4 -rw-r--r-- 1 root root  335 Jul 28 19:25 hello-1.c
4 -rw-r--r-- 1 root root  448 Jul 28 19:23 hello-2.c
4 -rw-r--r-- 1 root root  176 Jul 28 19:25 Makefile

Compilarea se face cu comanda make. Se observă o eroare la liniile 8 și 10 (evidențiate) datorată lipsei directivei MODULE_LICENSE() prin care se specifică tipulde licențiere al codului:

tom@rpi-yo3iti:~/c/drivere/Salzman/02 $ make
make -C /lib/modules/5.4.51-v7l+/build M=/home/tom/c/drivere/Salzman/02 modules
make[1]: Entering directory '/usr/src/linux-headers-5.4.51-v7l+'
  CC [M]  /home/tom/c/drivere/Salzman/02/hello-1.o
  CC [M]  /home/tom/c/drivere/Salzman/02/hello-2.o
  Building modules, stage 2.
  MODPOST 2 modules
WARNING: modpost: missing MODULE_LICENSE() in /home/tom/c/drivere/Salzman/02/hello-1.o
see include/linux/module.h for more information
WARNING: modpost: missing MODULE_LICENSE() in /home/tom/c/drivere/Salzman/02/hello-2.o
see include/linux/module.h for more information
  CC [M]  /home/tom/c/drivere/Salzman/02/hello-1.mod.o
  LD [M]  /home/tom/c/drivere/Salzman/02/hello-1.ko
  CC [M]  /home/tom/c/drivere/Salzman/02/hello-2.mod.o
  LD [M]  /home/tom/c/drivere/Salzman/02/hello-2.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.4.51-v7l+'

Al treilea exemplu - gestiunea automată a memoriei

/*  
 *  hello-3.c - Exemplu pentru comenzile-macro __init, __initdata și __exit.
 */
#include <linux/module.h>	/* Needed by all modules */
#include <linux/kernel.h>	/* Needed for KERN_INFO */
#include <linux/init.h>		/* Needed for the macros */

static int hello3_data __initdata = 3;

static int __init hello_3_init(void)
{
	printk(KERN_INFO "Hello, world %d\n", hello3_data);
	return 0;
}

static void __exit hello_3_exit(void)
{
	printk(KERN_INFO "Goodbye, world 3\n");
}

module_init(hello_3_init);
module_exit(hello_3_exit);

Comanda macro __init determină eliberarea memoriei alocată funcției init odată ce funcția de inițializare este completă, doar pentru driverele încorporate în kernel. Echivalentul pentru variabile este __initdata. Acest lucru nu este valabil pentru modulele (driverele) încărcate dinamic. La fel se comportă comanda macro __exit. Driverele incluse în kernel nu necesită funcții de eliberare a memoriei, în timp ce cele dinamice au nevoie de un mecanism de eliberare a memoriei. Aceste comenzi macro sunt definite în linux/init.h; în cazul meu, calea completă este:

/usr/src/linux-headers-5.4.51-v7+/include/linux/init.h

iată cum arată secțiunea coresponzătoare din init.h:

/* These are for everybody (although not all archs will actually
   discard it in modules) */
#define __init          __section(.init.text) __cold  __latent_entropy __noinitretpoline
#define __initdata      __section(.init.data)
#define __initconst     __section(.init.rodata)
#define __exitdata      __section(.exit.data)
#define __exit_call     __used __section(.exitcall.exit)

La boot, un mesaj similar cu: ...Freeing unused kernel memory: 236k freed... înseamnă exact eliberarea memoriei de de aceste module.

Al patrulea exemplu

În acest exemplu adăugăm comenzile maro care definesc tipul licenței, autorul codului, descrierea etc.

/*
 * hello-4.c - demo pentru documentarea modulelor
 *
 */

#include <linux/module.h> /* necesar tuturor modulelor */
#include <linux/kernel.h> /* necesar KERN_INFO */
#include <linux/init.h>   /* necesar pentru macro-uri */
#define AUTORUL_DRIVERULUI "Miron Iancu YO3ITI"
#define DRIVER_DESC "Un driver test, demo"

static int __init init_hello_4(void)
{
        printk(KERN_INFO "Hello, world 4.\n");
        return 0;
}

static void __exit cleanup_hello_4(void)
{
        printk(KERN_INFO "Goodbye, world 4.\n");
}

module_init(init_hello_4);
module_exit(cleanup_hello_4);

/*
 * Putem utiliza șiruri text, în felul următor:
 */

/*
 * Scăpăm de mesajele enervante legate de licență...
 */
MODULE_LICENSE("GPL");

/*
 * Sau cu ajutorul macro-urilor:
 */
MODULE_AUTHOR(AUTORUL_DRIVERULUI);      /* cine a scris acest modul ? */
MODULE_DESCRIPTION(DRIVER_DESC);        /* ce face acest modul ? */

/*
 * Acest modul folosește /dev/testdevice directiva macro MODULE_SUPPORTED_DEVICE
 * poate fi folosită (pe viitor) la configurarea automată a modulelor.
 * Pe moment nu este utilizată pentru altceva decât documentare.
 */
MODULE_SUPPORTED_DEVICE("testdevice");

Aceste informații pot fi afșare cu comanda modinfo: tom@rpi-yo3iti:~/c/drivere/02 $ modinfo hello-4.ko

filename:       /home/tom/c/drivere/Salzman/02/hello-4.ko
description:    Un driver test, demo
author:         Miron Iancu YO3ITI <miancuster at gmail dot com>
license:        GPL
srcversion:     C6368761702CBF9D1D1740D
depends:        
name:           hello_4
vermagic:       5.4.51-v7l+ SMP mod_unload modversions ARMv7 p2v8

Al cincilea exemplu

În codul de mai jos se exemplifică cum se pot schimba mesaje cu un modul kernel (driver) din spațiul utilizator, prin intermediul parametrilor introduși cu funcția module_param:

/*
 * hello-5.c - demo pentru interactivitate = cum se pot schimba mesaje prin parametri
 */

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/stat.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Miron Iancu, YO3ITI, <miancuster at gmail dot com>");

static short int myshort = 1;
static int myint = 420;
static long mylong = 9999;
static char *mystring = "blah";
static int myintArray[2] = {-1, 1};
static int arr_argc = 0;

/*
 * module_param(foo, int, 0000)
 * primul parametru este denumirea parametrului
 * al doilea parametru este tipul
 * al treilea argument sunt biții pentru permisiuni,
 * pentru expunerea mai târziu a parametrilor în sysfs
 * (dacă sunt diferiți de zero
 */

module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myshort, "Un număr întreg de dimensiuni mici");

module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
MODULE_PARM_DESC(myint, "Un număr întreg");

module_param(mylong, long, S_IRUSR);
MODULE_PARM_DESC(mylong, "Un număr întreg de dimensiuni mari");

module_param(mystring, charp, 0000); /* charp = char pointer */
MODULE_PARM_DESC(mystring, "Un șir de caractere");

/*
 * module_param_array(name, type, num, perm);
 * primul argument este denumirea parametrului (în acest caz denumirea șirului)
 * al doilea argument este tipul de dată al elementelor șirului
 * al treilea argument este un pointer către variabila care va stoca numărul
 * elementelor șirului inițializat de utilizat în momentul încărcării modulului în kernel
 * al patrulea argument sunt biții care stabilesc permisiunile
 */

module_param_array(myintArray, int, &arr_argc, 0000);
MODULE_PARM_DESC(myintArray, "Un șir de numere întregi");

static int __init hello_5_init(void)
{
        int i;
        printk(KERN_INFO "Hello, world 5\n===================\n");
        printk(KERN_INFO "myshort este un număr întreg de dimensiuni mici: %hd\n", myshort);
        printk(KERN_INFO "myint este un număr întreg: %d\n", myint);
        printk(KERN_INFO "mylong este un număr întreg de dimensiuni mari %ld\n", mylong);
        printk(KERN_INFO "mystring este un șir de caractere: %s\n", mystring);
        for (i = 0; i < (sizeof myintArray / sizeof(int)); i++)
        {
                printk(KERN_INFO "myintArray[%d] = %d\n", i, myintArray[i]);
        }
        printk(KERN_INFO "am primit %d argumente pentru myintArray.\n", arr_argc);
        return 0;
}

static void __exit hello_5_exit(void)
{
        printk(KERN_INFO "Goodbye, world 5\n");
}

module_init(hello_5_init);
module_exit(hello_5_exit);

Pentru adăugarea driver-ului se rulează comanda insmod cu parametri. Această metodă de inițializare este deosebit de utilă pentru a obține drivere care pot fi utilizate pentru o plajă largă de parametri de funcționare:

sudo insmod hello-5.ko mystring="un_exemplu_de_șir" myint=45 myshort=3 mylong=765765 myintArray=-1,2

Log-urile de kernel pot fi consultate cu dmesg

[13561.437899] myshort este un număr întreg de dimensiuni mici: 3
[13561.437938] myint este un număr întreg: 45
[13561.437948] mylong este un număr întreg de dimensiuni mari 765765
[13561.437958] mystring este un șir de caractere: un_exemplu_de_șir
[13561.437968] myintArray[0] = -1
[13561.437978] myintArray[1] = 2
[13561.437988] am primit 2 argumente pentru myintArray.

Exemple pe tipuri de drivere

Drivere de tip char

TODO

Drivere de sistem

TODO

Link-uri externe