{{{#!html

Pilote de périphérique

}}} = Objectif général L'objectif de la séance est de commander les LEDS et le bouton poussoir (BP) et en passant par un (des) pilote(s) installé(s) dans le noyau sur la !RaspberryPi 1. Puis de faire la même chose pour l'afficheur LCD. Vous savez déjà contrôler les LEDS et accéder au BP depuis l'espace utilisateur. Pour cela, vous avez dû mapper dans l'espace virtuel du processus utilisateur la zone de l'espace d'adressage physique permettant l'accès aux GPIO. Mais, il vous fallait avoir les droits du root. En passant par un pilote, les LED et le BP seront accessibles par un utilisateur standard. C'est pareil pour l'afficheur LCD que vous avez normalement déjà commandé en mode utilisateur. = Driver pour les LEDs et le bouton poussoir {{{#!div {{{#!td **Nous allons donc créer un pilote pour le périphérique LED et BP.**\\ Ce pilote sera accessible dans par le pseudo-fichier `/dev/ledXY`\\ '''XY''' correspond aux initiales de votre binôme pour éviter les conflits avec vos camarades\\ Par exemple, pour '''A'''lmada et '''F'''omentin, il faudrait créer /dev/ledbp'''AF'''\\\\ **Dans le texte de TP, nous n'avons pas toujours fait apparaître les lettres XY, vous devez les ajouter vous-même en les remplaçant par vos initiales.** }}} }}} Par exemple, vous pourrez écrire un programme test (cf fichier `test.c` ci-dessous) qui accède aux LED et BP en s'exécutant entièrement en mode utilisateur. {{{#!div {{{#!td **Le comportement proposé ici du pilote est le suivant :**\\ * **Pour les LEDS** * on envoie un tableau de caractères. * La case `'i'` définit l'état de la LED `'i'` (`'0'` LED éteinte sinon LED allumée). * Puisqu'il y a deux LEDs, le pilote acceptera un tableau à 2 caractères. * **Pour les boutons** * on propose un tableau de caractères. Le pilote lit l'état des boutons et met dans la case `'i'` l'état du bouton `'i'`. * Quand le bouton est relâché, le pilote met le caractère `'0'`. * Quand le bouton est enfoncé, le pilote met une valeur différente de `'0'`. C'est une proposition, vous pouvez faire comme bon vous semble. On peut imaginer d'autres manières, mais celle-ci me semble plus simple. Ce programme (de principe) est censé faire clignoter la led `'0'` jusqu'à ce qu'on appuie sur le bouton. {{{#!c #include #define NBLED 2 #define NBBP 1 char led[NBLED]; char bp[NBBP]; int main() { int i; int fd = open("/dev/ledbpXY", O_RDWR); if (fd < 0) { fprintf(stderr, "Erreur d'ouverture du pilote LED et Boutons\n"); exit(1); } for( i = 0; i < NBLED; i ++) { led[i] = '0'; } do { led[0] = (led[0] == '0') ? '1' : '0'; write( fd, led, NBLED); sleep( 1); read( fd, bp, 1); } while (bp[0] == '0'); return 0; } }}} }}} }}} **Références** * Vous trouverez pas mal d'informations sur internet : - [http://doc.ubuntu-fr.org/tutoriel/tout_savoir_sur_les_modules_linux] - [http://pficheux.free.fr/articles/lmf/drivers/] - [https://broux.developpez.com/articles/c/driver-c-linux/] * si vous voulez allez plus loin, il y a le livre : - [http://www.xml.com/ldd/chapter/book/ Linux Device Drivers, 2nd Edition] == Étape 1 : création et test d'un module noyau == === Code du module * La première étape consiste à créer un module noyau, l'insérer puis l'effacer du noyau. * Le module minimal se compose d'une fonction d'initialisation et d'une fonction de cleanup, dans le fichier `module.c` suivant: {{{#!c #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Charlie, 2015"); MODULE_DESCRIPTION("Module, aussitot insere, aussitot efface"); static int __init mon_module_init(void) { printk(KERN_DEBUG "Hello World !\n"); return 0; } static void __exit mon_module_cleanup(void) { printk(KERN_DEBUG "Goodbye World!\n"); } module_init(mon_module_init); module_exit(mon_module_cleanup); }}} * **Questions** * Quelle fonction est exécutée lorsqu'on insère le module du noyau ? * Quelle fonction est exécutée lorsqu'on enlève le module du noyau ? {{{#!protected Les réponses sont respectivement mon_module_init et mon_module_cleanup }}} === Compilation du module * Ce programme est cross compilé, puis copié sur la Raspberry Pi cible avec le fichier `Makefile` ci-après. * Ce Makefile a besoin des sources compilées du noyau présent sur la !RaspberryPi. Comme elles sont volumineuses, elles sont copiées dans le répertoire `/dsk/l1/misc/linux-rpi-3.18.y`. * Si vous voulez le faire chez vous, il faut que vous preniez les sources de votre distribution. Vous pouvez suivre le tutoriel suivant : [http://www.chicoree.fr/w/Compilation_crois%C3%A9e_d%27un_module_Linux_pour_Rasberry_Pi Compilation croisée d'un module linux pour Raspberry Pi]''. * Sur votre PC, vous allez commencer par regarder si le répertoire `/dsk/l1/misc/linux-rpi-3.18.y` existe avec \\`ls -d /dsk/l1/misc/linux-rpi-3.18.y`. \\**s'il existe**, c'est bon, il n'y a rien à faire.\\**s'il n'existe pas** vous allez le créer : * en téléchargeant l'archive [[htdocs:linux-rpi-3.18.y.tbz2]] (192Mb) **dans le répertoire** `/dsk/l1/misc` * puis en la décompressant \\`tar xjf /dsk/l1/misc/linux-rpi-3.18.y.tbz2 -C /dsk/l1/misc` (1.1Gb après décompression) * puis autorisant ces sources à tous `chmod -R 755 /dsk/l1/misc/linux-rpi-3.18.y` {{{#!make CARD_NUMB = 2X ROUTER = peri LOGIN = nom1-nom2 LAB = lab2 MODULE = module CROSSDIR = /users/enseig/franck/IOC KERNELDIR = /dsk/l1/misc/linux-rpi-3.18.y CROSS_COMPILE = $(CROSSDIR)/arm-bcm2708hardfp-linux-gnueabi/bin/bcm2708hardfp- obj-m += $(MODULE).o default:; make -C $(KERNELDIR) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) modules clean:; make -C $(KERNELDIR) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) clean upload:; scp -P622$(CARD_NUMB) $(MODULE).ko pi@$(ROUTER):$(LOGIN)/$(LAB) }}} === **Travail à faire** * Sur votre compte enseignement, vous devez: * **Créer** ces fichiers, vous pouvez les commenter en cherchant dans les manuels (ou Google). * **Changer la valeur des variables** `CARD_NUMB`, `LOGIN`et `LAB` afin de les adapter respectivement à votre numéro carte, au nom du répertoire créé par vous sur la !RaspberryPI et au nom du sous-répertoire créé par vous pour ce TP. Les répertoires et sous-répertoires doivent exister et vous devez donc commencer par vous logger sur votre carte !RaspberryPI avec `ssh` pour les créer. * **Compiler** le module avec la commande `make`. * **Copier** sur la !RaspberryPi avec scp avec la commande `make upload`.\\\\ * Sur la carte !RaspberryPi, vous devez: * Dans le répertoire `$(LOGIN)/$(LAB)` (par exemple `alamada-fromentin/lab2`) où vous avez copié votre module {{{#!sh $ sudo insmod ./module.ko $ lsmod $ dmesg $ sudo rmmod module $ lsmod $ dmesg }}} * Les commandes `lsmod`et `dmesg` permettent de voir les actions du module. * **Résumez dans le CR** ce que vous avez fait et ce que vous observez. == Étape 2 : ajout des paramètres au module == * Votre driver devra être paramétré pour lui indiquer le numéro de ports utilisés pour les LEDS et les boutons. Dans un premier temps vous allez vous contenter d'indiquer le numero du bouton pour le module de test. * **Vous devez ajouter** dans module.c {{{#!c static int btn; module_param(btn, int, 0); MODULE_PARM_DESC(btn, "numéro du port du bouton"); static int __init mon_module_init(void) { printk(KERN_DEBUG "Hello World !\n"); printk(KERN_DEBUG "btn=%d !\n", btn); return 0; } }}} * Le paramètre est défini au moment de l'insertion. {{{#!sh $ sudo insmod ./module.ko btn=18 }}} * Pour les numéros de GPIO de LEDs, comme il peut y en avoir plusieurs, vous pouvez utiliser `module_param_array`. {{{#!c #define NBMAX_LED 32 static int leds[NBMAX_LED]; static int nbled; module_param_array(leds, int, &nbled, 0); MODULE_PARM_DESC(LEDS, "tableau des numéros de port LED"); static int __init mon_module_init(void) { int i; printk(KERN_DEBUG "Hello World !\n"); for (i=0; i < nbled; i++) printk(KERN_DEBUG "LED %d = %d\n", i, leds[i]); return 0; } }}} * Le paramètre est défini au moment de l'insertion. {{{#!sh $ sudo insmod ./module.ko leds=4,17 }}} * **Questions** : * Comment **voir** que le paramètre a bien été lu ? == Étape 3 : création d'un driver qui ne fait rien, mais qui le fait dans le noyau == === Création du driver * Votre driver va être intégré dans un module. Vous allez donc créer un module **nommé `ledbp`** (et non plus `module`) paramétré avec les numéros de ports pour les LEDS et le bouton. Vous utiliserez un nouveau répertoire. Vous modifierez le Makefile en conséquence. * Vous ajoutez dans le fichier `.c` du module `ledbp`: {{{#!c #include static int open_ledbp(struct inode *inode, struct file *file) { printk(KERN_DEBUG "open()\n"); return 0; } static ssize_t read_ledbp(struct file *file, char *buf, size_t count, loff_t *ppos) { printk(KERN_DEBUG "read()\n"); return count; } static ssize_t write_ledbp(struct file *file, const char *buf, size_t count, loff_t *ppos) { printk(KERN_DEBUG "write()\n"); return count; } static int release_ledbp(struct inode *inode, struct file *file) { printk(KERN_DEBUG "close()\n"); return 0; } struct file_operations fops_ledbp = { .open = open_ledbp, .read = read_ledbp, .write = write_ledbp, .release = release_ledbp }; }}} * Vous allez **enregistrer** ce driver dans ce module en ajoutant la fonction d'enregistrement dans la fonction init du module. Vous devez aussi prendre en compte les paramètres. C'est à vous de décider comment.\\\\ * Au **début du fichier c du module**, vous déclarez une nouvelle variable statique. {{{#!c static int major; }}} * et **dans la fonction d'initialisation du module**, vous ajoutez l'enregistrement du driver, {{{#!c major = register_chrdev(0, "ledbp", &fops_ledbp); // 0 est le numéro majeur qu'on laisse choisir par linux }}} * et **dans la fonction exit du module**, vous allez décharger le driver dans ce module en ajoutant : {{{#!c unregister_chrdev(major, "ledbp"); }}} === Compilation * Vous devez compiler, déplacer le module (upload du Makefile) et le charger (insmod) dans la !RaspberryPi. * Vous allez chercher dans le fichier `/proc/devices` le numéro `major` choisi par linux. * vous allez maintenant créer le noeud dans le répertoire `/dev` et le rendre accessible par tous. Le numéro mineur est 0 car il n'y a qu'une seule instance. {{{ sudo mknod /dev/ledbp c major 0 sudo chmod a+rw /dev/ledbp }}} === Questions * Dans votre CR, je vous suggère d'expliquer chaque étape. * **Comment savoir** que le device a été créé ? * Le test de votre driver peut se faire par les commandes suivantes (avant de faire un vrai programme): dites ce que vous observez, en particulier, quelles opérations de votre driver sont utilisées. {{{#!sh $ echo "rien" > /dev/ledbpXY $ dd bs=1 count=1 < /dev/ledbp $ dmesg }}} * Nous pouvons automatiser le chargement du driver et son effacement en créant deux scripts shell:\\\\ * Dans un fichier `insdev` {{{#!bash #!/bin/sh module=$1 shift /sbin/insmod ./$module.ko $* || exit 1 rm -f /dev/$module major=$(awk "\$2==\"$module\" {print \$1;exit}" /proc/devices) mknod /dev/$module c $major 0 chmod 666 /dev/$module echo "=> Device /dev/$module created with major=$major" }}} * Dans un fichier `rmdev` {{{#!bash #!/bin/sh module=$1 /sbin/rmmod $module || exit 1 rm -f /dev/$module echo "=> Device /dev/$module removed" }}} * Ces deux scripts doivent être copiés dans votre répertoire de la !RaspberryPi. Ils doivent être exécutables et exécutés avec sudo. {{{#!bash chmod u+x insdev rmdev }}} * Pour les exécuter : {{{#!bash $ sudo ./insdev ledbp LED=2 => Device /dev/ledbp created with major=237 $ sudo ./rmdev ledbp LED=2 => Device /dev/ledbp removed }}} **Question**: * Expliquer comment `insdev` récupère le numéro `major` == Étape 4 : accès aux GPIO depuis les fonctions du pilote == = Pilotage d'un écran LCD en mode utilisateur et par un driver = L'objectifs est de comprendre le fonctionnement d'un écran LCD et ses fonctions de base Ressources: * newhaven display est le fabriquant du module LCD, c'est donc, en principe la référence. * Dans ce document ([[http://www.newhavendisplay.com/specs/NHD-0420DZ-FL-YBW.pdf | Datasheet du LCD du module]]), il est dit que le contrôleur du LCD est le ST7066U, mais, sauf preuve du contraire, la séquence d'initialisation du LCD décrite dans la documentation du [[http://www.newhavendisplay.com/app_notes/ST7066U.pdf | ST7006U]] ne fonctionne pas... * HD44780 est le contrôleur historique de la majorité des LCD de ce genre. La procédure d'initialisation fonctionne. * [[http://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller | LCD Display HD44780]] [[https://www.sparkfun.com/datasheets/LCD/HD44780.pdf | Datasheet du HD44780]] * Le document suivant définit comment est adressé la mémoire "vidéo" du LCD. * [[http://web.alfredstate.edu/faculty/weimandn/lcd/lcd_addressing/lcd_addressing_index.html | Adressage de la mémoire interne de l'afficheur]] Prenez le temps de parcourir les documents sur les afficheurs avant de commencer à programmer. Le code à modifier se trouve en pièces jointes de cette page [htdocs:docs/lcd/lcd_user.c lcd_user.c] et [htdocs:docs/lcd/Makefile Makefile]. Vous pourrez les copier dans un répertoire `lcd_user` == 1. Configuration des GPIO pour le pilotage de l'écran == L'écran LCD de type HD44780 dispose de plusieurs signaux de contrôle et de données. Les signaux de contrôle sont au nombre de 3: RS, RW et E. Les signaux de données sont au nombre de 4 ou 8 suivant le mode. Dans ce TP, nous utiliserons l'écran en mode 4-bit car la carte Raspberry Pi dispose d'un nombre limité de GPIO. De plus, le signal RW sera connecté directement à 0V (donc toujours en écriture) car nous n'allons gérer que les opérations d'écriture (Note: les GPIO de la carte Raspberry Pi fonctionnent en 3.3V, ils ne supportent pas les niveaux de tension 5V émis par l'afficheur, demandez-moi cela n'est pas clair). Les signaux de contrôle RS et E sont utilisés de la manière suivante: * RS vaut 1 pour l'envoi d'une donnée (e.g. un caractère) et vaut 0 pour l'envoi d'une commande (instruction). * E est un signal de validation; la valeur sur le bus de données (4 bits) est pris en compte à chaque front descendant de ce signal. Voici le mapping des GPIO pour les différents signaux: ||= Signal LCD =||= GPIO =|| || RS || 7 || || E || 27 || || D4, D5, D6, D7 || 22, 23, 24, 25 || Dans la documentation de l'afficheur, on peut savoir : * Comment faut-il configurer les GPIOs pour les différents signaux de l'afficheur LCD ? * Comment écrire des valeurs vers le LCD ? * Quelles valeurs doivent être envoyées vers l'afficheur pour réaliser l'initialisation ? * Comment demander l'affichage d'un caractère ? * Comment envoyer des commandes telles que : l'effacement de l'écran, le déplacement du curseur, etc. ? En particulier, page 11 de la documentation ([[http://www.newhavendisplay.com/specs/NHD-0420DZ-FL-YBW.pdf | Datasheet du LCD de la plateforme]]) se trouve du pseudo-code pour l'usage de l'afficheur. * P1 est un registre dont l'état est recopié sur les broches D0 à D7 (bit7 de P1 sur D7, bit6 sur D6, etc. jusqu'à D4). * D_I est un registre dont l'état est recopié sur RS qui indique si on envoie un caractère (D) ou une instruction (I). * R_W est un registre dont l'état est recopié sur RW * Delay() est une attente en microsecondes. {{{#!c 4-bit Initialization: /**********************************************************/ void command(char i) { P1 = i; //put data on output Port D_I =0; //D/I=LOW : send instruction R_W =0; //R/W=LOW : Write Nybble(); //Send lower 4 bits i = i<<4; //Shift over by 4 bits P1 = i; //put data on output Port Nybble(); //Send upper 4 bits } /**********************************************************/ void write(char i) { P1 = i; //put data on output Port D_I =1; //D/I=HIGH : send data R_W =0; //R/W=LOW : Write Nybble(); //Clock lower 4 bits i = i<<4; //Shift over by 4 bits P1 = i; //put data on output Port Nybble(); //Clock upper 4 bits } /**********************************************************/ void Nybble() { E = 1; Delay(1); //enable pulse width >= 300ns E = 0; //Clock enable: falling edge } /**********************************************************/ void init() { P1 = 0; P3 = 0; Delay(100); //Wait >40 msec after power is applied P1 = 0x30; //put 0x30 on the output port Delay(30); //must wait 5ms, busy flag not available Nybble(); //command 0x30 = Wake up Delay(10); //must wait 160us, busy flag not available Nybble(); //command 0x30 = Wake up #2 Delay(10); //must wait 160us, busy flag not available Nybble(); //command 0x30 = Wake up #3 Delay(10); //can check busy flag now instead of delay P1= 0x20; //put 0x20 on the output port Nybble(); //Function set: 4-bit interface command(0x28); //Function set: 4-bit/2-line command(0x10); //Set cursor command(0x0F); //Display ON; Blinking cursor command(0x06); //Entry Mode set } /**********************************************************/ }}} == 2. Fonctionnement de l'écran et fonctions de base == La prise en compte de la donnée est réalisée lors d'un front descendant du signal E. Pour créer un front descendant: * on place la donnée, * puis le signal E est mis à 1 pendant 1µs. [[Image(htdocs:png/command_lcd.png, width=900px, nolink)]] Nous utilisons l'afficheur LCD en mode 4 bits. Or, les commandes et les données sont transmises sur 8 bits ou 1 octet. Ainsi, toutes les commandes et toutes les données sont transmises en deux étapes: les 4 bits de poids fort et ensuite les 4 bits de poids faible. Nous avons toutes les fonctions dont nous avons besoin. Maintenant regardons d'un peu plus près la phase d'initialisation de l'afficheur LCD. Au démarrage, l'afficheur est dans un mode non défini (8 bits ou 4 bits). Il faut donc le forcer en mode 4 bits. Vous êtes prêt à tester votre code et vérifier qu'il affiche correctement une chaine de caractère. erry Pi. Il peut être utile de manipuler la position du curseur pour choisir où placer les caractères sur l'afficheur. Pour cela, l'afficheur dispose de trois instructions: Cursor home, Display clear et Set DDRAM address. La dernière instruction est relative à la mémoire interne de l'afficheur (Display Data RAM). La mémoire DDRAM est construite de la manière suivante: ||0x00 ..... Ligne 1 ..... 0x13|| ||0x40 ..... Ligne 2 ..... 0x53|| ||0x14 ..... Ligne 3 ..... 0x27|| ||0x54 ..... Ligne 4 ..... 0x67|| On souhaites utiliser toutes les lignes || **Questions:**\\1. Répondez aux questions qui se trouve dans le code [htdocs:docs/lcd/lcd_user.c lcd_user.c] \\2. Ecrire une fonction lcd_set_cursor qui positionne le curseur aux coordonnées (x,y) avec x la colonne, y la ligne.\\3. Ecrire un programme qui affiche la chaine passée en paramètre sur chacune des lignes de l'afficheur. [[BR]] === Création du driver qui accède aux GPIO * Nous devons pouvoir accéder aux registres de configuration des GPIO. * Pour l'accès aux GPIOs, vous allez reprendre le principe des écritures dans les registre en passant par une structure. * Vous noterez que l'adresse physique de base des GPIO est mappée dans l'espace virtuel du noyau à l'adresse '''io_addresse''' et récupérer avec la macro du noyau `__io_address()`. * GPIO_BASE est prédéfini dans les includes à 0x20200000. {{{#!c #include #include #include #include static const int LED0 = 4; struct gpio_s { uint32_t gpfsel[7]; uint32_t gpset[3]; uint32_t gpclr[3]; uint32_t gplev[3]; uint32_t gpeds[3]; uint32_t gpren[3]; uint32_t gpfen[3]; uint32_t gphen[3]; uint32_t gplen[3]; uint32_t gparen[3]; uint32_t gpafen[3]; uint32_t gppud[1]; uint32_t gppudclk[3]; uint32_t test[1]; } volatile *gpio_regs = (struct gpio_s *)__io_address(GPIO_BASE); }}} * Les deux fonctions `gpio_fsel()` et `gpio_write()` possibles sont données juste après. Vous pouvez voir comment exploiter la structure. * Vous devez écrire `gpio_read()`, puis invoquer ces fonctions dans les fonctions `open_ledbp()`, `read_ledbp()` et write_ledbp`. {{{#!c static void gpio_fsel(int pin, int fun) { uint32_t reg = pin / 10; uint32_t bit = (pin % 10) * 3; uint32_t mask = 0b111 << bit; gpio_regs->gpfsel[reg] = (gpio_regs->gpfsel[reg] & ~mask) | ((fun << bit) & mask); } static void gpio_write(int pin, bool val) { if (val) gpio_regs->gpset[pin / 32] = (1 << (pin % 32)); else gpio_regs->gpclr[pin / 32] = (1 << (pin % 32)); } }}} === Travail à faire * Ecrivez le driver complet pour le * Un script de chargement. * un programme de validation utilisant le driver. = Driver pour le LCD * Reprenez le pilote que vous aviez écrit pour les leds et modifiez la commande `write` de telle sorte que si vous appelez votre device /dev/lcd_xy (où xy sont les initiales de votre binôme), vous puissiez écrire. {{{#!bash $ echo "Bonjour" > /dev/lcd_xy }}} Vous allez devoir remplacer usleep() par udelay() et ajouter un `#include ` au début du code de votre module (merci Arthur). Vous devez (en principe) allouer de la mémoire noyau pour ce driver et utiliser la fonction `copy_from_user(unsigned long dest, unsigned long src, unsigned long len);` pour le message à écrire. * Ajoutez la gestion de l'appel système ioctl() pour * effacer l'écran : LCDIOCT_CLEAR * position le curseur : LCDIOCT_SETXY (vous pouvez coder les coordonnées dans l'argument).