I will write a HowTo on how to make an embedded Linux kernel module (device driver). All the content of this article can be moved on Raspberry Pi.
-1st time: Build environment preparation and simple kernel module creation -Second time: System call handler and driver registration (static method) -Third time: System call handler and driver registration (dynamic method) --__ 4th time: Read / write implementation and memory story <---------------- ----- Contents of this time __ -5th time: Implementation of GPIO driver for Raspberry Pi -6th time: Implementation of ioctl -7th time: Interface for procfs -8th time: Interface for debugfs -9th time: Call a function of another kernel module / Use GPIO control function -10th time: Create a device driver using I2C -11th time: Add I2C device to device tree -12th time: Load the created device driver at startup
https://github.com/take-iwiw/DeviceDriverLesson/tree/master/04_01 https://github.com/take-iwiw/DeviceDriverLesson/tree/master/04_02
Up to the last time, we have done the method of incorporating the created device driver into the kernel as a kernel module. This time, I will exchange values with this device driver by read / write from the user program.
Computers usually have memory such as DDR. In the case of Raspberry Pi 2, 1GByte SDRAM is installed. For Raspberry Pi, the physical address of this memory starts at 0x0000_0000. However, if the program we write uses the physical address directly when accessing the data in memory, that is not the case. The CPU has a function called MMU, and the MMU converts the physical address ⇔ virtual address. The kernel and each process run in their own virtual address space. For example, if 50 processes are running, there will be 51 (number of processes + kernel) virtual address space. The addresses in each virtual space are independent. For example, process A's 0xAAAA_AAAA and process B's 0xAAAA_AAAA are different. This prevents memory from being corrupted by other processes. Also, you can have more memory space than the actual physical memory capacity. These are the advantages of using an MMU.
The above is the memory map of Raspberry Pi. First, the center is the physical address seen from the CPU (ARM). If you look at this, you can see that the SDRAM is allocated from 0x0000_0000. The right side is the memory map of the logical address after being translated by the ARM MMU. 0x0000_0000 to 0xC000_0000 are virtual addresses for user-space processes. Normal programs run in this virtual memory space. Duplicate virtual addresses occur between different processes. However, the MMU will translate it so that it does not overlap on the physical address. From 0xC000_0000, the upper level is the virtual memory space for the kernel space.
I mentioned above that the physical address of SDRAM as seen by ARM starts from 0x0000_0000, but when the CPU actually accesses the memory, it goes through the bus (probably AXI). Also, this is probably Raspberry Pi's own configuration, but it seems that it is connected to the bus via a video processor (VC (Video Core) on the left side in the figure). Therefore, the conversion of VC with MMU is included. Looking at the figure, 0xC000_0000 is the bus address of SDRAM (non-cache) in the end. Therefore, it is necessary to set this address when using DMA etc. By the way, it seems to be 0x8000_0000 via L2 cache. For example, when creating data to be transferred by DMA with the CPU, if you write to this address, coherency will not be maintained. Data inconsistency occurs because DMA transfer is performed before writeback from cache to SDRAM is performed. You need to convert it to a non-cached address before accessing the memory.
It is troublesome to check these addresses one by one in the memory map, but it seems that you can get them with the following function. The build is gcc get_memory.c -L / opt / vc / lib -lbcm_host
. For Raspberry Pi2, bcm_host_get_sdram_address returned C0000000 and bcm_host_get_peripheral_address returned 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;
}
Please see Datasheet: BCM2835-ARM-Peripherals.pdf for details on the Raspberry Pi memory map. ..
It's a bit off the beaten track, but what's important this time is that the caller of the system call (user-space process) and the virtual address space within the kernel module are different. Passing a pointer does not guarantee that it points to the same address.
Last time, I implemented the following read system call handler in the implementation for the time being. Whenever you read, 1Byte of'A'is stored in buf. This buf is the area reserved by the caller (user space). Therefore, out-of-area access should occur. However, it actually worked fine. The value was also stored correctly. This is because the virtual address space is divided into 1 Gbyte for the kernel and 3 Gbyte for the user, as shown in the memory map at the beginning. Therefore, the kernel can access the virtual address space of processes running in the same context (that is, the process that called the system call). In the opposite direction, I think a memory protection violation will occur. However, this happens to work, and I don't think it will work if, for example, the target address is page swapped out.
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;
}
The above method happens to work, but textbooks use copy_to_user
and copy_from_user
when copying data in userspace-kernel space. In the code below, the character string set by the user with copy_from_user
at the time of writing is stored in the static variable stored_value
. The contents retained at the time of reading are returned by copy_to_user
.
#define NUM_BUFFER 256
static char stored_value[NUM_BUFFER];
/*Function called when reading*/
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;
}
/*Function called when writing*/
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;
}
The above code keeps the data static. Therefore, even if different users open them separately, they will access the same variable. In addition, you will access the same variables using a different device. Reserve a data area when you open it so that you can manage it individually.
The file structure is given as an argument of open. There is a member called private_data
in this file structure, and you can freely save pointers. This file structure itself is saved / managed by the user as a file descriptor. Use kmalloc
to allocate memory. Release with kfree
when closed. This time, let's assume that the structure _mydevice_file_data
is the type that holds the data.
/***Each file(File descriptor created for each open)Information associated with***/
#define NUM_BUFFER 256
struct _mydevice_file_data {
unsigned char buffer[NUM_BUFFER];
};
/*Function called at open*/
static int mydevice_open(struct inode *inode, struct file *file)
{
printk("mydevice_open");
/*Reserve an area to store data unique to each file*/
struct _mydevice_file_data *p = kmalloc(sizeof(struct _mydevice_file_data), GFP_KERNEL);
if (p == NULL) {
printk(KERN_ERR "kmalloc\n");
return -ENOMEM;
}
/*Initialize file-specific data*/
strlcat(p->buffer, "dummy", 5);
/*Have the secured pointer held by fd on the user side*/
file->private_data = p;
return 0;
}
/*Function called at close*/
static int mydevice_close(struct inode *inode, struct file *file)
{
printk("mydevice_close");
if (file->private_data) {
/*Release the data area unique to each file reserved at the time of open*/
kfree(file->private_data);
file->private_data = NULL;
}
return 0;
}
When the user reads and writes, the file descriptor obtained by open is set as an argument. In the implementation on the device driver side, the data area allocated at the time of opening can be accessed by referring to the private_data
member in the file
structure passed as an argument. This allows you to manage data individually for each open (file descriptor).
/*Function called when reading*/
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;
}
/*Function called when writing*/
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;
}
It's a little off topic, but the kernel return value is usually 0. The read / write system is the number of bytes actually processed. In case of an error, a negative value is returned.
On the user program side, by including #include <errno.h>
, the error code is stored in the ʻerrnovariable. You can check it directly, but by using a function called
perror, the error code will be converted into an easy-to-understand sentence. For example, do this: ʻ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;
}
Compile and run the test code as above.
gcc test.c
./a.out
0_A
0_B
1_A
The result looks like this, and you can see that even if you access the same device, you can keep / get different values if you open them separately.
Recommended Posts