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.
Pregătiri
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/Salzman/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/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
Sau prin dmesg
:
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
Verificarea versiunii header-elor pentru kernel
Fișierele header definesc interfețele dintre implementarea unor funcții și exteriorul și sunt necesare unui compilator pentru a verifica dacă utilizarea unei anumite funcții într-un program este corectă. Această validare se mai numește verificarea semnăturii unei funcții. Deoarece nu este necesară includerea surselor complete pentru verificarea semnăturii funcțiilor, includerea doar a header-elor permite o mare economie în ceea ce privește dimensiunea codului și memoriei utilizate.
În cazul concret al dezvoltării driverelor, header-ele kernelului trebuie să se potrivească cu versiunea de kernel:
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)
[...]
... 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
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 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 patrulea exemplu
/*
* hello-5.c - demonstrează modul în care se transmit variabile modulului
*/
#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@gmai.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);
La rularea insmod
cu parametri se obține:
sudo insmod hello-5.ko mystring="un_exemplu_de_șir" myint=45 myshort=3 mylong=765765 myintArray=-1,2
Și dmesg
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.