Click here to Skip to main content
14,383,961 members

Simple I/O device driver for RaspberryPi

Rate this:
4.98 (19 votes)
Please Sign up or sign in to vote.
4.98 (19 votes)
26 Sep 2015CPOL
Writing a I/O device driver for Raspberry Pi

Introduction

The fancy little gadget Raspberry Pi is for sure a nice toy to play with. But running the wheezy Linux it also is a complete Linux embedded system running on an ARM platform. That makes it quite interesting for programming and brought me to the idea to implement an I/O device driver on it, just to set on and off a digital output and to read the state of a input.

To build a device driver on RaspberryPi is basically the same as building one on another Linux system. But there are some small differences that makes it worth sharing it :-)

Building the environment

To compile a device driver on Linux there are some special source files necessary. These files build the interface to the kernel and they are called kernel header files. These header files must be of the same version as the kernel the driver should work whit later on and they are not included in the Wheezy distributions. So they have to be downloaded from the Internet. Here now already is a first little difference between Raspi and the rest of the Linux world:

Normally the kernel header files can be obtained by the simple command (entered in a XTerminal)

sudo apt-get install linux-headers-$(uname -r)

This command reads the actual kernel version by “uname -r”, downloads the correct header files and installs them in the correct directory.

But this does not work on Raspi. By some reason the kernel header files that fit to the available Wheezy distributions cannot be found like this. Even though there are header files of almost the same version available, that should not be considered. A kernel driver can be compiled whit these header files but it will not be loadedinto the Kernel if the version does not fit exactly.

So there is only one solution: A new Kernel must be compiled and installed. That installs the suitable kernel header files as well.

On the RaspberryPi Page there are many good hints to be found. On

https://www.raspberrypi.org/documentation/linux/kernel/

There is a good description how the download, compile and install an new kernel with following commands:

Get the new source with

git clone --depth=1 https://github.com/raspberrypi/linux

Add some missing dependencies by (Whatever this means :-))

sudo apt-get install bc

Now there should be a directory “linux” in the working directory of the current user. Change to this directory by “cd linux” and from there the new kernel should be configured before it can be compiled. For this there are different opinions now: The Raspi page recommends to use the default configuration by the command

make bcmrpi_defconfig

I did so and that worked fine.

In some descriptions they use the old configuration of the current kernel by

make oldconfig

This is basically possible. But keep in mind: The new kernel will offer many new features that were not configured in the old configuration. So you might be asked many strange questions like

Support for paging of anonymous memory yes or no

System V IPC yes or no

And you have to make the right decision each time. If you go wrong in one of these questions, Raspi will show that by an error message after maybe 8 hours of compiling time. That’s quite frustrating. So better just make the default configuration :-)

Now the kernel compilation can be started and Raspy can be left on its one for a while. Call the commands

make
make modules
sudo make modules_install

If everything goes well, a new file “Image” should be in the directory “arch/arm/boot/” something around 8 hours later. This is our new kernel. This file should be renamed to “kernel.img” and copied into the “/boot” directory of the system by

sudo cp arch/arm/boot/ kernel.img /boot/

Now we are ready to reboot the new kernel. After a reboot the new version should be displayed at the command

uname –r

The kernel header files are installed now and can be found in the directory “/lib/modules/”

Implementing the driver

A Linux device driver must have a defined structure that contains at least following functions:

int init_module(void)

to load the driver

void cleanup_module(void)

to unload the driver.

Beside these two functions we need some more to read or write into our device and a function to open and one close the device.

static ssize_t device_read(struct file *filp,  char *buffer,  size_t length, loff_t * offset)
static ssize_t device_write(struct file *filp, const char *buff, size_t len, loff_t * off)
static int device_open(struct inode *inode, struct file *file)
static int device_release(struct inode *inode, struct file *file)

These functions have to be registered in the kernel when the driver is loaded. Therefore the structure file_operations is used. In this structure for each function there is a pre-defined reference that gets a pointer to the corresponding function. That looks like this

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = device_open,
    .release = device_release,
    .read    = device_read,
    .write   = device_write
};
init_module(void)

In the function init_module the driver will be registered in the kernel. It is called automatically when the driver is loaded into the kernel by the insmod command. The registration is done by the command

Major = register_chrdev(0,DEVICE_NAME, &fops);

The register_chrdev function basically gets the Major number of the driver as first parameter. For the kernel the Major number is the identifier of a driver and it will be linked to the device name handed to the register_chrdev function as the second parameter.

The Major number can be a fixed number or just a 0 as here. If it is 0, the kernel will check what Major number is available on the system and give this to our driver by self. This might be the best solution if a driver is meant to be distributed and installed on different systems. On a small embedded system where the environment does not change during its live time, the Major number can be set to a fixed number as well. In this case an available Major number must be defined by the developer himself. Therefore the file /proc/devices must be checked and a number that is not already in use there must be found.

The last parameter given to register_chrdev is the file_operations structure.

If the registration succeeds, the function returns the Major number that has been given to our device. This number must be kept as we need it again for unloading the driver.

Now the driver needs to check whether the I/O region or the Mem region it wants to read or write to is not occupied by somebody else and reserve this area for himself. Therefore we have to have a look into the hardware of Raspi. Now there is another small difference to at least the i86 world. On Raspi the I/O’s are kept in the memory range and not in the I/O range and if that wouldn’t be enough. This memory area is mapped to another area J

In the description under

https://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf

the control addresses of the GPIO port are explained like that.

Image 1

But as I mentioned: That’s not where they really are. The address 0x7E20 0000 is mapped to 0x2020 0000 and so on. So to check the memory area we need the command

check_mem_region(PORT, RANGE)

with

static unsigned PORT = 0x20200000;
static unsigned RANGE =  0x40;

and if that succeeds

request_mem_region(PORT, RANGE, DEVICE_NAME);

to reserve this region (in case of a I/O port area it would be check_region(PORT, RANGE) and request_ region(PORT, RANGE, DEVICE_NAME)). If this also works, the memory area is reserved and the driver can use it. That means the task of the init_module function is done and it can return SUCCESS to indicate that.

cleanup_module(void)

The cleanup_module function is called when the driver is unloaded. It releases the memory area and unregisters the driver. Therefore the commands

release_mem_region(PORT, RANGE);
unregister_chrdev(Major, DEVICE_NAME);

should be called (here we need the Major number again).

That’s for the loading and unloading of the driver. How exactly the loading and unloading is done will be explained further down.

device_open(struct inode *inode, struct file *file)

Before we can write into or read from our memory area we have to open the device that covers this “thing” now. The device_open function prepares the device for being used. That means here we have to make sure that only one application can use the device at once, make sure the driver cannot be unloaded while somebody is using it and set up the I/O ports the way we want to use them. For the first check the variable Device_Open is used. If the device will be opened this variable will be incremented and at the entrance of the function we check whether it is 0 or not to see if it is already in use or not. For the protection against unloading we use try_module_get(THIS_MODULE). This function increments a internal counter that prevents the driver from being unloaded as long as this counter is not 0. The counter will be decremented again by module_put(THIS_MODULE) when der device is closed later on.

If everything went fine so far, we can start using the device now. That means, we can set up the ports the way we want to use them. Therefore we have to do another remapping of the address range we want to use. The kernel function ioremap returns the needed remapped address to a pointer.

addr = ioremap(PORT, RANGE);

Here it’s important to note, that our address pointer “addr” is a byte pointer of the type u8 even if we want to write 32 bit values. We will set up a port by the command

writel(cmd, (addr+4));

to the base address plus an offset of 4 we get to the “function select 1” register. The offset 4 will be converted to 4 byte addresses because addr is a byte pointer. That’s important to know. If addr would be of another type, that would have to be considered in the addressing. In case we want to use a u32 pointer the offset found in the hardware documentation needed to be divided by 4. Of course it is possible and o.k. to do it like this, but it’s not too clear if one wants to see how things work. So maybe better stay with the u8 pointer for the addressing :-)

Setting up the ports

To set up the function of each port Raspy uses the 4 “function select” registers. Each of these registers controls 10 GPIO pins except the last one. That controls 8. The function of each pin is bit coded in these registers. Each pin gets 3 bits to set up its function. The coding is as follows

000 = GPIO Pin is an input
001 = GPIO Pin is an output
100 = GPIO Pin takes alternate function 0
101 = GPIO Pin takes alternate function 1
110 = GPIO Pin takes alternate function 2
111 = GPIO Pin takes alternate function 3
011 = GPIO Pin takes alternate function 4
010 = GPIO Pin takes alternate function 5

With my device driver I want to set just the GPIO pin 10 as output and read GPIO pin 14 as input. So I have to write a set up command into the “function set 1” register with offset 4. To get a clear situation I first set all GPIO pins 10 to 19 as inputs by the command

cmd = 0;
writel(cmd, (addr+4));

Then I can set up pin 10 as output by:

cmd = 1;
writel(cmd, (addr+4));

That’s all we have to do here.

static ssize_t device_write(struct file *filp, const char *buff, size_t len, loff_t * off)

To set or clear our GPIO pin we implement the device_write function. Raspy provides the “GPIO pin output set 0” and “GPIO pin output set 1” register to set an output. We have to use the first one to set pin 10. The pins are set bit coded. That means we have a 32 bit value and each bit represents one pin. Pin 10 is represented by the bit 10 and to set pin 10 we can use the commends

cmd = 1 << 10;
writel(cmd, (addr+0x1c));

This command has to be carried out just once to set the pin. The pin will stay set until a clear command is called. That’s a bit different from other peripherals where an output has to be set by a static command.

To clear the pin we have to do the same on the “GPIO pin output clear 0” register.

cmd = 1 << 10;
writel(cmd, (addr+0x28));
static ssize_t device_read(struct file *filp, char *buffer, size_t length, loff_t * offset)

Reading the state of the inputs is implemented in the device_read function. We can read the state of the pins 0 to 31 in the “GPIO pin level 0” register. Here we get the state of each pin bit coded again.

res = readl(addr+0x34);

returns the state into the u32 variable res. From this variable I extract the 4 u8 parts and copy them into the array buf by:

buf[0] = res & 0xFF;
buf[1] = (res >> 8) & 0xFF;
buf[2] = (res >> 16) & 0xFF;
buf[3] = (res >> 24) & 0xFF;

and, as we return this array in a char* buffer, we have to terminate buf by a 0.

buf[4] = 0;

To move this data from kernel space to user space we use the put_user function:

index = 4;
while (length && (index >= 0))
{
    put_user(*(msg_Ptr++), buffer++);
    length--;
    index--;
    bytes_read++;
}
return bytes_read;

Whth this implementation an application can read the bit coded state of all the GPIO pins by one device_read call.

Compiling the driver

To compile a device driver module there is a kind of a special procedure. We have to work with a make file and define the compiling in this file. There are several kernel modules that have to be loaded during compilation. The only way to get this done correctly is to use a make file. My make file contains of following 5 lines:

obj-m := raspy_io.o
all:
    make -C /lib/modules/4.0.9+/build M=$(PWD) modules
clean:
    make -C /lib/modules/4.0.9+/build M=$(PWD) clean

I’m using the Kernel version 4.0.9+ and the Kernel header files are in /lib/modules/4.0.9+. From there the Kernel modules should be included. My own source files are in the same directory as the make file is. Therefore I can just set the current path as source path by “M=$(PWD)”. When we start compilation be the command “make” (in the same directory), the compiler looks for the raspy_io.c file, compiles and links it to a raspy_io.ko. This is the module that can by loaded into the kernel.

Loading the driver into the kernel

To load the driver into the kernel is quite a task :-). We have to insert the module into the kernel, then we have to create a device knot in the /dev directory and for this knot we have to set the correct permission’s to make it loadable by an application. But, step by step.

To insert the module into the kernel we can call

Sudo insmod raspy_io.ko

That’s quite easy.

But now the complicate part begins. To create the knot we need to know the “Major number” of the device we implemented in our driver. There are two ways to get this number now. One way is to set the Major number fix in the code of our driver in the function register_chrdev that we have to call in the init_module function. But for this we need to know a Major number that is available on the system we want to use our driver. Basically we can look up all the used numbers in the file devices in the /proc directory and us one that is not listed there. That could work on a small embedded system that would remain as is for all the time. But as we probably want to use our driver on a system where other drivers could be installed later on, we should go the other way and let the system give us an available Major number dynamically while inserting the driver into the Kernel and find out which one we got by self. I implemented my driver in this way already.

The insmod command calls the init_module function and orders a Major number from the Kernel. This number is listed in the /pro/devices file immediately and we can read this number from the devices file. To do this I use the magnificent AWK tool. AWK is a mighty tool, but a hell of complicate :-)

The command to get the Major number in a shell script looks like this:

module="raspy_io.ko"
device="io_dev"
major=`cat /proc/devices | awk "{if(\\$2==\"$device\")print \\$1}"`

The command cat /proc/devices lists all entries from devices an hands this to awk as a table containing all the Major numbers in the first row and the device names in the second row. Awk parses all lines now and checks if the value of the second row (the $2) is our "io_dev". If yes, it thakes the value of the first row ($1) and returns it to our “major” script variable. Using this major variable I first make sure the knot I want to create is deleted and create it new by

rm -f /dev/${device} c $major 0
mknod /dev/${device} c $major 0

Now I have to set the permission’s to read and write the device by

chmod 666 /dev/${device}

and that’s it. The device can be used now. It has the name "io_dev" and all users can read or write it.

I put this commands into a shell script named “load.sh”. This script can be called as root and inserts and registers the device ready for use.

Using the driver

To use the device that we created we just have to open it first in read/write mode by

int fd;
fd = open("/dev/io_dev", O_RDWR);

If this succeeds, fd will be > 0 and we can read or write by

char buf[4];
char wbuf[4];
read(fd, buf, sizeof(buf));

Set pin 14 by

wbuf[0] = 1;
wbuf[1] = 0;
write(fd, wbuf, sizeof(wbuf));

and clear it by

wbuf[0] = 0;
wbuf[1] = 0;
write(fd, wbuf, sizeof(wbuf));

At the end we have to release the driver again by

close(fd);

To compile an application using a device driver the fcntl.h mus be included by

#include <fcntl.h>

To edit the driver source and the test application is used Geany (to install “sudo apt-get install geany”). Geany is a handy little C-editor. The GCC compiler should be pre-installed on Raspy. That’s all I needed :-)

Points of Interest

For deeper studies I recommend the book Linux Device Drivers by O’Reilly‘s. That’s a very comprehensive book (as O’Reilly‘s books usually are :-) ) very helpful for programming Linux Device Drivers.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mosi_62
Software Developer (Senior) Ewag AG, Switzerland
Switzerland Switzerland
Computers are very straight... They always do exactly what we tell them to do... Only, much too often what we tell them to do is not really what we want them to do Smile | :)

Writing Software is one of the most creative tings one can do. I have been doing this for more than ten years now and still having a lot of fun with it. Besides doing software for HMI's on C# for business, I enjoy very much to implement interesting algorithms and analyse the mathematics they are based on in my leisure time Smile | :)

For more detailed descriptions and math visit me on my own page

www.mosismath.com

Comments and Discussions

 
Questionnot generating raspy.ko file Pin
Member 1353093521-Nov-17 21:54
memberMember 1353093521-Nov-17 21:54 
QuestionHow to release memory allocated to GPIOs ? Pin
Member 1218590114-Sep-17 0:11
memberMember 1218590114-Sep-17 0:11 
AnswerRe: How to release memory allocated to GPIOs ? Pin
Member 1348691026-Oct-17 6:33
memberMember 1348691026-Oct-17 6:33 
Bugre:implicit declaration of function Pin
Member 1272911426-Oct-16 21:15
memberMember 1272911426-Oct-16 21:15 
Bugimplicit declaration of function Pin
Member 1272911426-Oct-16 21:14
memberMember 1272911426-Oct-16 21:14 
Bugimplicit declaration of function'check_mem_region' Pin
Member 127291149-Sep-16 21:38
memberMember 127291149-Sep-16 21:38 
GeneralRe: implicit declaration of function'check_mem_region' Pin
Mosi_6226-Sep-16 8:47
professionalMosi_6226-Sep-16 8:47 
QuestionHelp needed for Pi2 Pin
Member 1263426529-Jul-16 3:11
memberMember 1263426529-Jul-16 3:11 
AnswerRe: Help needed for Pi2 Pin
Mosi_6221-Aug-16 0:43
professionalMosi_6221-Aug-16 0:43 
GeneralRe: Help needed for Pi2 Pin
Urban Edstrom21-Sep-16 22:02
memberUrban Edstrom21-Sep-16 22:02 
QuestionQuery regarding pin 14 Pin
Nirav Trivedi16-Apr-16 5:28
memberNirav Trivedi16-Apr-16 5:28 
AnswerRe: Query regarding pin 14 Pin
Mosi_6225-Apr-16 10:24
professionalMosi_6225-Apr-16 10:24 
Questioninfo Pin
ciel_bd15-Jan-16 17:16
memberciel_bd15-Jan-16 17:16 
AnswerRe: info Pin
Mosi_622-Mar-16 8:44
professionalMosi_622-Mar-16 8:44 
QuestionMy Vote +5 Pin
D V L7-Nov-15 0:01
professionalD V L7-Nov-15 0:01 
GeneralMy vote of 5 Pin
Mihai MOGA17-Oct-15 16:52
professionalMihai MOGA17-Oct-15 16:52 
GeneralRe: My vote of 5 Pin
Mosi_6219-Oct-15 8:23
professionalMosi_6219-Oct-15 8:23 
QuestionNice work Pin
Mike Hankey9-Oct-15 15:37
professionalMike Hankey9-Oct-15 15:37 
GeneralMy vote of 5 Pin
koothkeeper28-Sep-15 12:12
professionalkoothkeeper28-Sep-15 12:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 26 Sep 2015

Tagged as

Stats

59.7K views
1.3K downloads
37 bookmarked