Dezvoltare drivere pentru Linux
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