J'écrirai un HowTo sur la façon de créer un module de noyau (pilote de périphérique) pour Linux embarqué. Tout le contenu de cet article peut être exécuté sur Razpie.
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/04_01 https://github.com/take-iwiw/DeviceDriverLesson/tree/master/04_02
Jusqu'à la dernière fois, nous avons mis au point une méthode pour incorporer le pilote de périphérique créé dans le noyau en tant que module du noyau. Cette fois, j'échangerai des valeurs avec ce pilote de périphérique en lecture / écriture depuis le programme utilisateur.
Les ordinateurs ont généralement une mémoire telle que DDR. Dans le cas de Raspberry Pi 2, 1 Go de SDRAM est installé. Pour Raspeye, l'adresse physique de cette mémoire commence à 0x0000_0000. Cependant, si le programme que nous écrivons utilise directement l'adresse physique lors de l'accès aux données en mémoire, ce n'est pas le cas. La CPU a une fonction appelée MMU, et la MMU convertit l'adresse physique ⇔ l'adresse virtuelle. Le noyau et chaque processus s'exécutent dans des espaces d'adressage virtuels indépendants. Par exemple, si 50 processus sont en cours d'exécution, il y aura 51 espaces d'adressage virtuels (nombre de processus + noyau). Les adresses de chaque espace virtuel sont indépendantes. Par exemple, 0xAAAA_AAAA pour le processus A et 0xAAAA_AAAA pour le processus B sont différents. Cela empêche la mémoire d'être corrompue par d'autres processus. En outre, vous pouvez avoir plus d'espace mémoire que la capacité de mémoire physique réelle. Ce sont les avantages de l'utilisation de MMU.
Ce qui précède est la carte mémoire de Raspeye. Tout d'abord, le centre est l'adresse physique vue depuis le CPU (ARM). Si vous regardez cela, vous pouvez voir que la SDRAM est allouée à partir de 0x0000_0000. Le côté droit est la carte mémoire de l'adresse logique après avoir été traduite par la MMU ARM. 0x0000_0000 à 0xC000_0000 sont des adresses virtuelles pour les processus de l'espace utilisateur. Les programmes normaux s'exécutent dans cet espace de mémoire virtuelle. Des adresses virtuelles dupliquées se produisent entre différents processus. Cependant, le MMU le traduira afin qu'il ne chevauche pas l'adresse physique. À partir de 0xC000_0000, le niveau supérieur est l'espace de mémoire virtuelle pour l'espace noyau.
J'ai mentionné plus tôt que l'adresse physique de la SDRAM vue par l'ARM commence à 0x0000_0000, mais lorsque le processeur accède réellement à la mémoire, il passe par le bus (probablement AXI). De plus, il s'agit probablement de la propre configuration de Raspai, mais il semble qu'il soit connecté au bus via un processeur vidéo (VC (Video Core) sur le côté gauche de la figure). Par conséquent, la conversion de VC avec MMU est incluse. En regardant la figure, 0xC000_0000 est l'adresse de bus de la SDRAM (sans cache) à la fin. Par conséquent, il est nécessaire de définir cette adresse lors de l'utilisation de DMA, etc. Au fait, cela semble être 0x8000_0000 via le cache L2. Par exemple, lorsque la CPU crée des données à transférer par DMA, si vous écrivez à cette adresse, la cohérence ne sera pas conservée. L'incohérence des données se produit car le transfert DMA est effectué avant la réécriture depuis le cache vers la SDRAM. Vous devez le convertir en une adresse non mise en cache avant d'accéder à la mémoire.
Il est difficile de vérifier ces adresses une par une dans la carte mémoire, mais il semble que vous puissiez les obtenir avec la fonction suivante. La construction est gcc get_memory.c -L / opt / vc / lib -lbcm_host
. Pour Raspberry Pi2, bcm_host_get_sdram_address a renvoyé C0000000 et bcm_host_get_peripheral_address a renvoyé 3F000000.
get_memory.c
#include <stdio.h>
int main()
{
/* https://www.raspberrypi.org/documentation/hardware/raspberrypi/peripheral_addresses.md */
extern unsigned bcm_host_get_sdram_address(void);
printf("%08X\n", bcm_host_get_sdram_address());
extern unsigned bcm_host_get_peripheral_address(void);;
printf("%08X\n", bcm_host_get_peripheral_address());
return 0;
}
Veuillez consulter la Fiche de données: BCM2835-ARM-Peripherals.pdf pour plus de détails sur la carte mémoire de Raspeye. ..
C'est un peu en dehors des sentiers battus, mais ce qui est important cette fois, c'est que l'appelant de l'appel système (processus de l'espace utilisateur) et l'espace d'adressage virtuel dans le module du noyau sont différents. Passer un pointeur ne garantit pas qu'il pointe vers la même adresse.
La dernière fois, j'ai implémenté le gestionnaire d'appels système de lecture suivant dans l'implémentation pour le moment. Chaque fois que vous lisez, 1 octet de A est stocké dans buf. Ce buf est la zone réservée par l'appelant (espace utilisateur). Par conséquent, un accès hors zone doit avoir lieu. Cependant, cela fonctionnait bien. La valeur a également été stockée correctement. En effet, l'espace d'adressage virtuel est divisé en 1 Go pour le noyau et 3 Go pour l'utilisateur, comme indiqué dans la carte mémoire au début. Par conséquent, le noyau peut accéder à l'espace d'adressage virtuel des processus s'exécutant dans le même contexte (c'est-à-dire au processus qui a appelé l'appel système). Dans la direction opposée, je pense qu'une violation de la protection de la mémoire se produira. Cependant, cela fonctionne et je ne pense pas que cela fonctionnera si l'adresse cible est permutée, par exemple.
static ssize_t mydevice_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_read");
buf[0] = 'A';
return 1;
}
La méthode ci-dessus fonctionne, mais les manuels utilisent copy_to_user
et copy_from_user
lors de la copie de données dans l'espace utilisateur-noyau. Dans le code ci-dessous, la chaîne de caractères définie par l'utilisateur avec copy_from_user
au moment de l'écriture est stockée dans la variable statique valeur_stockée
. Le contenu conservé au moment de la lecture est retourné par copy_to_user
.
#define NUM_BUFFER 256
static char stored_value[NUM_BUFFER];
/*Fonction appelée lors de la lecture*/
static ssize_t mydevice_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_read");
if(count > NUM_BUFFER) count = NUM_BUFFER;
if (copy_to_user(buf, stored_value, count) != 0) {
return -EFAULT;
}
return count;
}
/*Fonction appelée au moment de l'écriture*/
static ssize_t mydevice_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_write");
if (copy_from_user(stored_value, buf, count) != 0) {
return -EFAULT;
}
printk("%s\n", stored_value);
return count;
}
Le code ci-dessus maintient les données statiques. Par conséquent, même si différents utilisateurs les ouvrent séparément, ils accéderont à la même variable. De plus, vous accéderez aux mêmes variables en utilisant un appareil différent. Réservez une zone de données lorsque vous l'ouvrez afin de pouvoir la gérer individuellement.
La structure du fichier est donnée dans l'argument open. Il y a un membre appelé private_data
dans cette structure de fichier, et vous pouvez librement enregistrer le pointeur. Cette structure de fichier elle-même est enregistrée / gérée par l'utilisateur en tant que descripteur de fichier. Utilisez kmalloc
pour allouer de la mémoire. Relâchez avec kfree
une fois fermé. Cette fois, supposons que la structure _mydevice_file_data
est le type qui contient les données.
/***Chaque dossier(Descripteur de fichier créé pour chaque ouverture)Informations associées à***/
#define NUM_BUFFER 256
struct _mydevice_file_data {
unsigned char buffer[NUM_BUFFER];
};
/*Fonction appelée à l'ouverture*/
static int mydevice_open(struct inode *inode, struct file *file)
{
printk("mydevice_open");
/*Réservez une zone pour stocker des données uniques à chaque fichier*/
struct _mydevice_file_data *p = kmalloc(sizeof(struct _mydevice_file_data), GFP_KERNEL);
if (p == NULL) {
printk(KERN_ERR "kmalloc\n");
return -ENOMEM;
}
/*Initialiser les données spécifiques au fichier*/
strlcat(p->buffer, "dummy", 5);
/*Avoir le pointeur sécurisé tenu par fd côté utilisateur*/
file->private_data = p;
return 0;
}
/*Fonction appelée à la fermeture*/
static int mydevice_close(struct inode *inode, struct file *file)
{
printk("mydevice_close");
if (file->private_data) {
/*Libérez la zone de données propre à chaque fichier réservé au moment de l'ouverture*/
kfree(file->private_data);
file->private_data = NULL;
}
return 0;
}
Lorsque l'utilisateur lit ou écrit, le descripteur de fichier obtenu par open est défini comme argument. Dans l'implémentation côté pilote de périphérique, la zone de données allouée au moment de l'ouverture est accessible en se référant au membre private_data
dans la structure file
passé comme argument. Cela vous permet de gérer les données individuellement pour chaque ouverture (descripteur de fichier).
/*Fonction appelée lors de la lecture*/
static ssize_t mydevice_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_read");
if(count > NUM_BUFFER) count = NUM_BUFFER;
struct _mydevice_file_data *p = filp->private_data;
if (copy_to_user(buf, p->buffer, count) != 0) {
return -EFAULT;
}
return count;
}
/*Fonction appelée au moment de l'écriture*/
static ssize_t mydevice_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_write");
struct _mydevice_file_data *p = filp->private_data;
if (copy_from_user(p->buffer, buf, count) != 0) {
return -EFAULT;
}
return count;
}
C'est un peu hors sujet, mais la valeur de retour du noyau est généralement 0. Le système de lecture / écriture est le nombre d'octets réellement traités. En cas d'erreur, une valeur négative est renvoyée.
Du côté du programme utilisateur, en incluant #include <errno.h>
, le code d'erreur est stocké dans la variable ʻerrno. Vous pouvez le vérifier directement, mais en utilisant une fonction appelée
perror, le code d'erreur sera converti en une phrase facile à comprendre. Par exemple, procédez comme suit: ʻIf ((fd = open ("/ dev / mydevice0", O_RDWR)) <0) perror ("open");
test.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main()
{
char buff[256];
int fd0_A, fd0_B, fd1_A;
printf("%08X\n", buff);
if ((fd0_A = open("/dev/mydevice0", O_RDWR)) < 0) perror("open");
if ((fd0_B = open("/dev/mydevice0", O_RDWR)) < 0) perror("open");
if ((fd1_A = open("/dev/mydevice1", O_RDWR)) < 0) perror("open");
if (write(fd0_A, "0_A", 4) < 0) perror("write");
if (write(fd0_B, "0_B", 4) < 0) perror("write");
if (write(fd1_A, "1_A", 4) < 0) perror("write");
if (read(fd0_A, buff, 4) < 0) perror("read");
printf("%s\n", buff);
if (read(fd0_B, buff, 4) < 0) perror("read");
printf("%s\n", buff);
if (read(fd1_A, buff, 4) < 0) perror("read");
printf("%s\n", buff);
if (close(fd0_A) != 0) perror("close");
if (close(fd0_B) != 0) perror("close");
if (close(fd1_A) != 0) perror("close");
return 0;
}
Compilez et exécutez le code de test comme ci-dessus.
gcc test.c
./a.out
0_A
0_B
1_A
Le résultat ressemble à ceci, et vous pouvez voir que même si vous accédez au même appareil, vous pouvez conserver / obtenir des valeurs différentes si vous les ouvrez séparément.
Recommended Posts