Ich werde ein HowTo darüber schreiben, wie man ein Kernelmodul (Gerätetreiber) für eingebettetes Linux erstellt. Der gesamte Inhalt dieses Artikels kann auf Razpie ausgeführt werden.
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/04_01 https://github.com/take-iwiw/DeviceDriverLesson/tree/master/04_02
Bis zum letzten Mal haben wir die Methode durchgeführt, den erstellten Gerätetreiber als Kernelmodul in den Kernel zu integrieren. Dieses Mal werde ich Werte mit diesem Gerätetreiber durch Lesen / Schreiben aus dem Anwenderprogramm austauschen.
Computer haben normalerweise einen Speicher wie DDR. Im Fall von Raspberry Pi 2 ist 1 GB SDRAM installiert. Für Raspeye beginnt die physikalische Adresse dieses Speichers bei 0x0000_0000. Wenn das von uns geschriebene Programm jedoch beim Zugriff auf die Daten im Speicher direkt die physikalische Adresse verwendet, ist dies nicht der Fall. Die CPU hat eine Funktion namens MMU, und die MMU konvertiert die physikalische Adresse ⇔ virtuelle Adresse. Der Kernel und jeder Prozess werden in unabhängigen virtuellen Adressräumen ausgeführt. Wenn beispielsweise 50 Prozesse ausgeführt werden, gibt es 51 virtuelle Adressräume (Anzahl der Prozesse + Kernel). Die Adressen in jedem virtuellen Raum sind unabhängig. Beispielsweise unterscheiden sich 0xAAAA_AAAA für Prozess A und 0xAAAA_AAAA für Prozess B. Dies verhindert, dass der Speicher durch andere Prozesse beschädigt wird. Sie können auch mehr Speicherplatz als die tatsächliche physische Speicherkapazität haben. Dies sind die Vorteile der Verwendung von MMU.
Das Obige ist die Speicherkarte von Raspeye. Erstens ist das Zentrum die physikalische Adresse, die von der CPU (ARM) aus gesehen wird. Wenn Sie sich das ansehen, können Sie sehen, dass das SDRAM von 0x0000_0000 zugewiesen wird. Die rechte Seite ist die Speicherzuordnung der logischen Adresse nach der Übersetzung durch die ARM-MMU. 0x0000_0000 bis 0xC000_0000 sind virtuelle Adressen für User Space-Prozesse. In diesem virtuellen Speicherbereich werden normale Programme ausgeführt. Zwischen verschiedenen Prozessen treten doppelte virtuelle Adressen auf. Die MMU übersetzt es jedoch so, dass es sich nicht mit der physischen Adresse überschneidet. Ab 0xC000_0000 ist die obere Ebene der virtuelle Speicherbereich für den Kernelbereich.
Ich habe bereits erwähnt, dass die physikalische Adresse des SDRAM aus Sicht des ARM bei 0x0000_0000 beginnt, aber wenn die CPU tatsächlich auf den Speicher zugreift, geht sie über den Bus (wahrscheinlich AXI). Dies ist wahrscheinlich auch Raspais eigene Konfiguration, aber es scheint, dass sie über einen Videoprozessor (VC (Video Core) auf der linken Seite in der Abbildung) mit dem Bus verbunden ist. Daher ist die Konvertierung von VC mit MMU enthalten. In der Abbildung ist 0xC000_0000 am Ende die Busadresse von SDRAM (Nicht-Cache). Daher ist es erforderlich, diese Adresse bei Verwendung von DMA usw. festzulegen. Übrigens scheint es 0x8000_0000 über den L2-Cache zu sein. Wenn die CPU beispielsweise Daten erstellt, die von DMA übertragen werden sollen, wird die Kohärenz nicht beibehalten, wenn Sie an diese Adresse schreiben. Dateninkonsistenz tritt auf, weil die DMA-Übertragung durchgeführt wird, bevor das Zurückschreiben vom Cache zum SDRAM durchgeführt wird. Sie müssen es in eine nicht zwischengespeicherte Adresse konvertieren, bevor Sie auf den Speicher zugreifen können.
Es ist mühsam, diese Adressen einzeln in der Speicherzuordnung zu überprüfen, aber es scheint, dass Sie sie mit der folgenden Funktion erhalten können. Der Build lautet "gcc get_memory.c -L / opt / vc / lib -lbcm_host". Für Raspberry Pi2 gab bcm_host_get_sdram_address C0000000 und bcm_host_get_peripheral_address 3F000000 zurück.
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;
}
Weitere Informationen zur Speicherkarte von Raspeye finden Sie unter Datenblatt: BCM2835-ARM-Peripherals.pdf. ..
Es ist etwas abseits der ausgetretenen Pfade, aber diesmal ist es wichtig, dass der Aufrufer des Systemaufrufs (User-Space-Prozess) und der virtuelle Adressraum innerhalb des Kernel-Moduls unterschiedlich sind. Das Übergeben eines Zeigers garantiert nicht, dass er auf dieselbe Adresse verweist.
Beim letzten Mal habe ich vorerst den folgenden Read System Call Handler in der Implementierung implementiert. Wann immer Sie lesen, wird 1Byte of'A' in buf gespeichert. Dieser Puffer ist der vom Anrufer reservierte Bereich (Benutzerbereich). Daher sollte ein Zugriff außerhalb des Bereichs erfolgen. Es hat jedoch tatsächlich gut funktioniert. Der Wert wurde ebenfalls korrekt gespeichert. Dies liegt daran, dass der virtuelle Adressraum in 1 GB für den Kernel und 3 GB für den Benutzer unterteilt ist, wie in der Speicherzuordnung am Anfang gezeigt. Daher kann der Kernel auf den virtuellen Adressraum von Prozessen zugreifen, die im selben Kontext ausgeführt werden (dh auf den Prozess, der den Systemaufruf aufgerufen hat). In der entgegengesetzten Richtung denke ich, dass eine Verletzung des Speicherschutzes auftreten wird. Dies funktioniert jedoch zufällig, und ich glaube nicht, dass es funktionieren wird, wenn die Zieladresse beispielsweise ausgetauscht wird.
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;
}
Die obige Methode funktioniert zufällig, aber Lehrbücher verwenden "copy_to_user" und "copy_from_user", wenn Daten in den User Space-Kernel Space kopiert werden. Im folgenden Code wird die Zeichenkette, die der Benutzer zum Zeitpunkt des Schreibens mit "copy_from_user" gesetzt hat, in der statischen Variablen "saved_value" gespeichert. Der zum Zeitpunkt des Lesens gespeicherte Inhalt wird von "copy_to_user" zurückgegeben.
#define NUM_BUFFER 256
static char stored_value[NUM_BUFFER];
/*Funktion beim Lesen aufgerufen*/
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;
}
/*Funktion zum Zeitpunkt des Schreibens aufgerufen*/
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;
}
Der obige Code hält die Daten statisch. Selbst wenn verschiedene Benutzer sie separat öffnen, greifen sie daher auf dieselbe Variable zu. Außerdem greifen Sie mit einem anderen Gerät auf dieselben Variablen zu. Reservieren Sie beim Öffnen einen Datenbereich, damit Sie ihn individuell verwalten können.
Die Dateistruktur wird im offenen Argument angegeben. In dieser Dateistruktur befindet sich ein Mitglied namens "private_data", und Sie können den Zeiger frei speichern. Diese Dateistruktur selbst wird vom Benutzer als Dateideskriptor gespeichert / verwaltet. Verwenden Sie "kmalloc", um Speicher zuzuweisen. Im geschlossenen Zustand mit "kfree" loslassen. Nehmen wir diesmal an, dass die Struktur _mydevice_file_data
der Typ ist, der die Daten enthält.
/***Jede Datei(Dateideskriptor für jedes Öffnen erstellt)Informationen im Zusammenhang mit***/
#define NUM_BUFFER 256
struct _mydevice_file_data {
unsigned char buffer[NUM_BUFFER];
};
/*Funktion beim Öffnen aufgerufen*/
static int mydevice_open(struct inode *inode, struct file *file)
{
printk("mydevice_open");
/*Reservieren Sie einen Bereich zum Speichern von Daten, die für jede Datei eindeutig sind*/
struct _mydevice_file_data *p = kmalloc(sizeof(struct _mydevice_file_data), GFP_KERNEL);
if (p == NULL) {
printk(KERN_ERR "kmalloc\n");
return -ENOMEM;
}
/*Dateispezifische Daten initialisieren*/
strlcat(p->buffer, "dummy", 5);
/*Lassen Sie den gesicherten Zeiger von fd auf der Benutzerseite halten*/
file->private_data = p;
return 0;
}
/*Funktion wird beim Schließen aufgerufen*/
static int mydevice_close(struct inode *inode, struct file *file)
{
printk("mydevice_close");
if (file->private_data) {
/*Geben Sie den Datenbereich frei, der für jede zum Zeitpunkt des Öffnens reservierte Datei eindeutig ist*/
kfree(file->private_data);
file->private_data = NULL;
}
return 0;
}
Wenn der Benutzer liest oder schreibt, wird der durch open erhaltene Dateideskriptor als Argument festgelegt. Bei der Implementierung auf der Gerätetreiberseite kann auf den zum Zeitpunkt des Öffnens zugewiesenen Datenbereich zugegriffen werden, indem auf das als Argument übergebene Element "private_data" in der Datei "file" verwiesen wird. Auf diese Weise können Sie Daten für jedes Öffnen einzeln verwalten (Dateideskriptor).
/*Funktion beim Lesen aufgerufen*/
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;
}
/*Funktion zum Zeitpunkt des Schreibens aufgerufen*/
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;
}
Es ist ein wenig abseits des Themas, aber der Kernel-Rückgabewert ist normalerweise 0. Das Lese- / Schreibsystem ist die Anzahl der tatsächlich verarbeiteten Bytes. Im Fehlerfall wird ein negativer Wert zurückgegeben.
Auf der Benutzerprogrammseite wird der Fehlercode durch Einfügen von "#include <errno.h>" in der Variablen "errno" gespeichert. Sie können es direkt überprüfen, aber mit einer Funktion namens "perror" wird der Fehlercode in einen leicht verständlichen Satz umgewandelt. Gehen Sie zum Beispiel folgendermaßen vor: 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;
}
Kompilieren Sie den Testcode und führen Sie ihn wie oben aus.
gcc test.c
./a.out
0_A
0_B
1_A
Das Ergebnis sieht folgendermaßen aus, und Sie können sehen, dass Sie auch dann, wenn Sie auf dasselbe Gerät zugreifen, unterschiedliche Werte beibehalten können, wenn Sie sie separat öffnen.
Recommended Posts