Coss-compilation
Les systèmes Linux embarqués sont assez légers, généralement ils n’embarquent pas de compilateur gcc à l’intérieur, ce qui signifie que les compilations doivent êtres croisées. Sur Openwrt notamment, il n y a pas GDB ni GCC.
Il existe plusieurs façons de cross compiler, le plus simple serait d’intégrer directement notre module dans le système et le compiler avec. C’est d’ailleurs ce que je vais faire à la fin, créer un module buildroot.
Pour la phase de développement, j’ai choisi de cross-compiler avec les headers du Kernel. Les headers du Kernel sont un ensemble de fichiers d’entête écrits en C requis pour compiler n’importe quel code pour le Kernel. En gros c’est une définition des fonctions et des structures qu’on trouve trouve dans le kernel source tree, les plus importantsheaders sont dans include/linux
et include/asm
mais d’autres aussi sont ailleurs.
Pour compiler notre driver, on doit générer ces headers à partir des sources Kernel de Openwrt qui tourne sur notre RPi4, ensuite on cross-compile sur la target avec la bonne toolchain. J’ai fait cette manipe car c’est plus simple que de compiler tout le Kernel.
Ce fichier dessous sert à générer ces Kernel headers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
# Ajouter au PATH le repo de la toolchain qui a compilé openwrt
export PATH=$PATH:~/openwrt/staging_dir/toolchain-aarch64_cortex-a72_gcc-11.3.0_musl/bin/
# Prefix pour la CROSS_COMPILATION
Prefix=aarch64-openwrt-linux
# Dossier qui va contenir nos KernelHeaders-
KernelHeader=$PWD/KernelHeaders/
# Compile à partir du dossier kernel de Openwrt
cd ~/openwrt/build_dir/target-aarch64_cortex-a72_musl/linux-bcm27xx_bcm2711/linux-5.15.53
make ARCH=arm64 CROSS_COMPILE=$Prefix O=$KernelHeader mrproper;
# Default kernel config for the board
make ARCH=arm64 CROSS_COMPILE=$Prefix O=$KernelHeader defconfig;
# Preparer les prérequis necessaires pour compiler un out-of-tree module
make -j 8 ARCH=arm64 CROSS_COMPILE=$Prefix O=$KernelHeader modules_prepare;
Le clean se fait sur trois niveaux.
Make clean : supprimer la plupart des fichiers générés sauf la config et ceux utiles pour contruire des modules
make mrproper : supprimer la configuration actuelle et tous les fichiers générés
make distclean: supprime les fichiers de sauvegarde de l’éditeur, les fichies de patchs , etc.
On a maintenant les headers du Kernel, avec ceux ci on doit pouvoir cross-compiler.
Pour ça il nous faut le petit module kernel :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Monkey");
MODULE_DESCRIPTION("A simple Linux Kernel Module");
MODULE_VERSION("0.1");
static int __init hello_init(void)
{
printk(KERN_ALERT "Hello all\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_ALERT "Goodbye all\n");
}
module_init(hello_init);
module_exit(hello_exit);
Modules Kernel
Il y a quelques différences entre la programmation au niveau Kernel et en niveau User Space. Un module ou un driver suit les conventions de codage au niveau Kernel.
Le module définit deux fonctions : hello_init()
invoquée au chargement et hello_exit()
invoquée au déchargement du module. Ces fonctions servent à enregistrer le driver au Kernel pour de futurs appelles . La fonction exit()
doit défaire tout ce que la fonction init()
a fait, par exemple désallouer de la mémoire.
module_init()
et module exit()
utilisent des macros pour indiquer le rôle de chacune des fonctions “hello” cités précédemment. MODULE_LICENCE
pour la licence, sans elle le Kernel va crier et Richard STALLMAN aussi d’ailleur.
printk()
se comporte comme printf()
de la GNU C library. le Kernel tourne de lui même et ne nécessite pas des librairies C.
Un programme peut appeler une fonction qu’il n’a pas définit, la phase de linkage résout ça en utilisant des librairies C, un module par contre n’est linké qu’au Kernel, donc les seuls fonctions qu’il peut appeler sont celles exportés par ce dérnier. Exemple: printk
est définit au Kernel et exporté aux modules.
KERNEL_ALERT
définit la priorité du message (info, alterte , erreur ,debug..etc).
Le Makefile de compilation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# if KERNELRELEASE is defined, we've been invoked from kernel build system
# and can use its langage
ifneq ($(KERNELRELEASE),)
obj-m := test.o
# otherwise we were called directly from the command line; invok kbuild system
else
#safe asignement
KERNELDIR ?="./KernelHeaders"
#call out the shell to execute command pwd
PWD := $(shell pwd)
.PHONY: modules
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
.PHONY: clean
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
CFLAGS_test.o := -DDEBUG
Explication du makefile
Normalement pour compiler un module, une simple ligne de code obj-m := module.o
suffit, le responsable kbuild system
fera le reste.
Ce makefile est lu en deux fois:
Quand le makefile est invoqué par une ligne de commande: cela signifie que la variable KERNELRELEASE
est pas définit, dans ce cas kbuild system va se charger de trouver le Kernel source tree.
Quand c’est invoqué par Kbuild system: Si le kernel pour lequel on compile n’est pas celui de la machine actuelle (eg. notre cas de cross compilation) alors il faut définir ou se trouve le Kernel source tree, une fois trouvé, le makefile appelle la target default et sa commande va compiler nos modules à partir des fichier objets spécifiés dans obj-m
, c’est la seconde lecture (invocation par ligne de commande).
Dans cette ligne, on change de directory pour aller dans le source tree du kernel pointé avec -C
la où se trouvele top-level makefile du kernel. La seconde M=PWD
fait déplacer ce top-level makefile sur le dossier courant ou sont nos source pour enfin compiler les fichiers objets specifiés par obj-m
et générer des fichiers .ko
Voici un code qui apelle le Makefile avec les bons paramétres de cross-compilation:
1
2
3
4
5
6
7
#!/bin/bash
export PATH=$PATH:/home/vicious/openwrt/staging_dir/toolchain-aarch64_cortex-a72_gcc-11.3.0_musl/bin/
# Build the module
make ARCH=arm64 CROSS_COMPILE=aarch64-openwrt-linux-
# Informations about it
modinfo test.ko
Pour plus d’informations voir le chapitre II de Linux Device Drivers 3rd edition.
Pour le makefile, un excellent Tuto de développez.com
J’envois le module par ssh sur mon RaspberryPi et je charge le module et HOP!! la cross-compialtion a fonctionné. dmesg
Pour décharger le module, il faut compiler OpenWRT avec l’option module unload.
Les choses sérieuses commencent ici, il faut discuter d’une chose, le type de notre driver.
Type du driver
On peut faire le driver de plusieurs façons, on peut le faire comme un driver de type char driver
, c’est a dire que notre driver aura une interface de type char dans sysFs
, on interagit avec le driver via un nœud dans /dev
et on va initialiser et enregistrer notre LCD grâce à des fonctions init()
et exit()
au chargement du module. Le driver dans ce cas va utiliser directement les GPIO
pour envoyer les donnés au LCD, c’est pas intéressant.
Une autre façon serait d’utiliser le Bus SPI
et donc l’API SPI du kernel. On va toujours interagir avec le LCD via /dev
mais sans utiliser les fonctions de l’interface char (file_operations)
.
En faisant des recherches sur le type d’API, helpers qu’il faut utiliser, je suis tombé sur une API très intéressants: Direct Randering Manager .
J’ai trouvé ces helpers DRM car j’ai consulté le driver d’un autre écran LCD TFT que j’ai dans mon stock.
Par contre, j’en ai trouvé deux types, les helpers DRM et les tiny-drm. Les tiny-drm se veulent plus faciles.
Perdu , j’ai donc envoyé un message à un des mainteneurs du kernel Andy Shevchenko, il m’a répondu et a transféré mon message pour le mainteneur de la partie DRM du kernel.
Voici sa réponse très intéressante.
DRM vs Tiny-drm
Donc décidé, je vais utiliser les helpers DRM.
D’ailleurs c’est ce qu’on va faire !
Device Tree
Un device tree est un arbre de structures de données avec des nœuds décrivants le composant matériel. Selon ePAPR (power.org)
Le device tree compilé porte une extension .dtb
device tree blob Le fichier avant compilation est un .dts
device tree source.
pour le compiler on utilise le compilateur dtc
:
1
2
3
4
5
6
7
8
9
10
11
12
# installation dtc
sudo apt-get install device-tree-compiler
# Compialtion dts ou dtsi
dtc -I dts -O dtb -o devicetree_file_name.dtb devicetree_file_name.dts
# conversion dts->dtb
dtc -I dts -O dtb -f devicetree_file_name.dts -o devicetree_file_name.dtb
# conversion dtb->dts
dtc -I dtb -O dts -f devicetree_file_name.dtb -o devicetree_file_name.dts
les dtb et dts se trouvent dans le dossier ou sont décompressés et compilés les packages : /build_dir
.
bcm2711-rpi-4-b.dtb
se trouve la ou il y a l’image linux et bcm2711-rpi-4-b.dts
est dans build_dir/target-aarch64_cortex-a72_musl/linux-bcm27xx_bcm2711/linux-5.15.53/arch/arm/boot/dts
le device tree se compose de cette forme :
Device tree.
Source: Bootlin, Embedded Linux training by Thommas Petazzoni
Il ya des nœuds qui représentent le device, chaque node est suivis de son adresse dans la mémoire.
Le node a des propriétés, des sous node qu’on apelle child nodes et on voit qu’un node peut faire référence à un autre node, on apelle ça un phandle.
Le node peut aussi avoir un label.
Pour intégrer le device tree du LCD dans celui du kernel, un certains nombre de propriétés doit exister.
tout d’abord, il faut que le node décrivant le LCD doit un un “child node” du contrôleur spi, donc toutes les propriétés ce ce controleurs doivent être spécifiés.
voici la section qu’il faut ajouter en dessous du device spi numero 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
&spi0 {
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins &spi0_cs_pins>;
cs-gpios = <&gpio 8 1>, <&gpio 7 1>;
spidev0: spidev@0{
compatible = "spidev";
reg = <0>; /* CE0 */
#address-cells = <1>;
#size-cells = <0>;
spi-max-frequency = <125000000>;
};
spidev1: spidev@1{
compatible = "spidev";
reg = <1>; /* CE1 */
#address-cells = <1>;
#size-cells = <0>;
spi-max-frequency = <125000000>;
};
5110lcd@3{
compatible = "adafruit,Nokia5110 LCD";
reg = <3>;
spi-max-frequency = <32000000>;
dc-gpios = <&gpio 9 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio 11 GPIO_ACTIVE_HIGH>;
backlight = <&bgpio 7 GPIO_ACTIVE_HIGH>;
};
};
Propriétés
compatible <manifacture, modele>
: avec cette propriété , l’OS va décider quel driver va se lier à ce composant.
Reg
: Cette propriété dépend du type de bus sur lequel on est raccordé, elle est composé comme suite <adresse , length>
, cela veut dire que la zone réservée pour le node lcd commence à partir de adresse
et sa taille est length
. Chaque adressse est une liste de un ou plusieurs cellules (cells).
La taille aussi peut etre une liste de cellules de 32 bits.
Puisque les adresses sont de variables de taille length , dans le node parent on va specifier:
#address-cells
: nombre de cellules 32 bits necessaires pour former l’adresse de reg
#size-cells
: nombre de cellules 32 bits necessaires pour former la taille de reg
il y a deux façons d’ajouter la section dans le DT, soit en cherchant le .dts de la carte cité précédemment, soit en décompilant le dtb, je préfère cette dernière car on a plus d’informations sur la taille des zones mémoires et leurs offsets .
Le contrôleur de gpio est spécifié avec 2 cellules #gpio-cells
donc pour spécifier les gpio, on besoin d’un phandle sur le contrôleur de gpio, du numéro de la ligne gpio (celle 1) et d’un flag active low/high (cell 2).
Il faut regarder le dts décompilé pour voir comment est définit le contrôleur gpio.
Data/Control gpio : comme dit dans la partie 1 est une entrée pour choisir le type de donnée selon ce qu’on lui envoi :
- 0 ou
GPIO_ACTIVE_HIGH
pour DATA - 1 ou
GPIO_ACTIVE_LOW
pour Controle
Le controleur gpio déclare des pins soit en mode GPIO soit en mode ALT, ce qui veut dire alternate function, cela veut dire qu’elle sont utilisé dans un autre mode que le gpio simple exemple : chip-select du SPI, Clock pour i2c,PWM …etc chaque pin peut avoir plusieurs alternate functions
Pour choisir tel ou tel fonction, il faut changer les 3 bits du registre AF .
En résumé:
J’ai choisis dans le device tree un GPIO pour le backlight, un GPIO de RESET et un autre de controle. les autres je fais apelle à eux depuis le driver.
Fonction de probe ()
Fonction de probe()
La fonction de probe est appelée au démarrage du kernel, ou quand on plug le device (dans le cas d’un device déconnectable). On dit qu’elle est appelée à chaque fois que device est vu (À compléter plus tard ).
Son rôle en gros est de détecter le device (dans notre cas le LCD).
Cette fonction fait l’initialisation du device, l’initialisation du hardware, enregistrer le framework kernel nécessaire ..etc
Greg-Kroah Hartman disait “les drivers sont simples à écrire, ce qui est difficle est de comprendre le materiel”
Alors !! C’est simple, le chantier est difficle, je dois convetir un driver deja existant qui est un Driver frambuffer en driver DRM. yen a plusieurs mais j’ai choisis détudier le fb_hx8357d.c et son equivalent DRM hx8357d.c