Dezvoltare drivere pentru Linux

De la YO3ITI
Salt la: navigare, 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): <syntaxhighlight lang="shell"> tom@rpi-yo3iti:~/c/drivere/Salzman/01 $ uname -a Linux rpi-yo3iti 5.4.51-v7l+ #1327 SMP Thu Jul 23 11:04:39 BST 2020 armv7l GNU/Linux </syntaxhighlight> Sau, prin /proc/version: <syntaxhighlight lang="shell"> tom@rpi-yo3iti:~/c/drivere/Salzman/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 </syntaxhighlight>

Sau prin dmesg:

<syntaxhighlight lang="shell"> tom@rpi-yo3iti:~/c/drivere/Salzman/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 </syntaxhighlight>

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: <syntaxhighlight lang="shell" highlight="4,7,10,13,16"> tom@rpi-yo3iti:~/c/drivere/Salzman/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)

[...] </syntaxhighlight> ... sper să nu avem probleme. :D Mai întâi trebuie instalate header-ele pentru kernel: <syntaxhighlight lang="shell"> sudo rpi-update stable </syntaxhighlight> Apoi: <syntaxhighlight lang="shell"> apt get install raspberrypi-kernel-headers </syntaxhighlight>

Exemple generice, elemente de bază

Primul exemplu super simplu

În forma cea mai simplă (și care e complet inutilă) un driver are nevoie de cel puțin două funcții: una de inițializare și una de ieșire. În cazul programării driverelor, mesajele nu pot fi logate la consola normală ci doar în spațiul adresabil de kernel, cu ajutorul funcției printk. <syntaxhighlight lang="c">

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

int init_module(void) {

       printk(KERN_INFO "Hello world 1.\n");
       /*
        * O valoare diferită de zero înseamnă că funcția de inițializare a eșuat; modulul nu poate fi încărcat.
        */
       return 0;

}

void cleanup_module(void) {

       printk(KERN_INFO "Goodbye world 1.\n");

} </syntaxhighlight>

Un fișier Makefile simplu: <syntaxhighlight lang="Makefile"> 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 </syntaxhighlight> Apoi: <syntaxhighlight lang="shell" highlight="1"> 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+' </syntaxhighlight>

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:

<syntaxhighlight lang="shell"> 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 </syntaxhighlight>

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: <syntaxhighlight lang="c" highlight="21,22"> /*

* hello-2.c - demo pentru module_init() și module_exit()
* Metoda mai nouă, preferată pentru init_module() și cleanup_module()
*/
  1. include <linux/module.h>
  2. include <linux/kernel.h>
  3. 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); </syntaxhighlight>

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

<syntaxhighlight lang="Makefile"> 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

</syntaxhighlight>

Compilarea:

Fișierele existente: <syntaxhighlight lang="shell"> 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 </syntaxhighlight> 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: <syntaxhighlight lang="shell" highlight="8,10"> 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+' </syntaxhighlight>

Al treilea exemplu - gestiunea automată a memoriei

<syntaxhighlight lang="c"> /*

*  hello-3.c - Exemplu pentru comenzile-macro __init, __initdata și __exit.
*/
  1. include <linux/module.h> /* Needed by all modules */
  2. include <linux/kernel.h> /* Needed for KERN_INFO */
  3. 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); </syntaxhighlight> 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: <syntaxhighlight lang="shell"> /usr/src/linux-headers-5.4.51-v7+/include/linux/init.h </syntaxhighlight> iată cum arată secțiunea coresponzătoare din init.h: <syntaxhighlight lang="c"> /* These are for everybody (although not all archs will actually

  discard it in modules) */
  1. define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
  2. define __initdata __section(.init.data)
  3. define __initconst __section(.init.rodata)
  4. define __exitdata __section(.exit.data)
  5. define __exit_call __used __section(.exitcall.exit)

</syntaxhighlight> La boot, un mesaj similar cu: ...Freeing unused kernel memory: 236k freed... înseamnă exact eliberarea memoriei de de aceste module. <syntaxhighlight lang="c"> </syntaxhighlight>

Al patrulea exemplu

În acest exemplu adăugăm comenzile maro care definesc tipul licenței, autorul codului, descrierea etc. <syntaxhighlight lang="c"> /*

* hello-4.c - demo pentru documentarea modulelor
*
*/
  1. include <linux/module.h> /* necesar tuturor modulelor */
  2. include <linux/kernel.h> /* necesar KERN_INFO */
  3. include <linux/init.h> /* necesar pentru macro-uri */
  4. define AUTORUL_DRIVERULUI "Miron Iancu YO3ITI"
  5. 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"); </syntaxhighlight> Aceste informații pot fi afșare cu comanda modinfo: tom@rpi-yo3iti:~/c/drivere/02 $ modinfo hello-4.ko <syntaxhighlight lang="shell"> 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 </syntaxhighlight>

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: <syntaxhighlight lang="c"> /*

* hello-5.c - demo pentru interactivitate = cum se pot schimba mesaje prin parametri
*/
  1. include <linux/module.h>
  2. include <linux/moduleparam.h>
  3. include <linux/kernel.h>
  4. include <linux/init.h>
  5. 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); </syntaxhighlight> 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: <syntaxhighlight lang="shell"> sudo insmod hello-5.ko mystring="un_exemplu_de_șir" myint=45 myshort=3 mylong=765765 myintArray=-1,2 </syntaxhighlight> Log-urile de kernel pot fi consultate cu dmesg <syntaxhighlight lang="shell"> [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. </syntaxhighlight>

Exemple pe tipuri de drivere

Drivere de tip char

TODO

Drivere de sistem

TODO

Link-uri externe