Skip to main content

RaspberryPI GPIO Access with Memory Mapped IOs inside the Kernel (Device Driver)

·1701 words·8 mins· loading ·
Table of Contents

I left you guys with cliff hanger for the last blog post the max toggling frequency I could achieve was around maximum frequency of 8.554 kHz.

Logic 2

Continuing our journey to the holy grail of high speed GPIO access (more than 500kHz toggling). The solution to the problem of context switch left me thinking,where is the place in Linux where I have least context switches. What if I could access the registers with utmost power. The place is inside the Linux Kernel. I thought what would happen if we write a small device driver that toggles the pin.

What is a Linux Device Driver
#

A Linux device driver is piece of code that can be inserted (on a running Linux Kernel) mostly without restarting the machine. A device driver might have specific use case like new Network card developed by a company. Or a simple pin toggle like in our case but mostly device driver are divided into following groups:

Character Device Drivers: Handle devices that send or receive data as a stream of characters (e.g.keyboards). Block Device Drivers: Manage devices that store data in blocks, such as hard drives and USB drives. Network Device Drivers: Manage network interfaces like Ethernet or Wi-Fi adapters. Miscellaneous Drivers: Handle other types of devices like cameras, sensors, or virtual devices. (pin toggling)

More information: Link


How to Write One
#

Contrary to popular belief device drivers are not that difficult to write. They are simple C code but needs some preparation to be executed. Now for Raspberry PI or an other Embedded SOM (System on Module) there are two ways to write them.

  • Use the Host Linux machine to write, compile it and install it later in the Raspberry PI (in our case). This is called Cross Compilation.
  • Use the RaspberryPi itself to write, compile and install.

This first method is useful for the use case when the code base is huge and takes lot of time or when you need to run a pipeline runner for compilation. I will be using the second method as my code will be small and easy to compile on a RPi.

To start development on RPi you need to install some dependencies. Log in into Raspberry Pi and open terminal

Run following command on the terminal:

sudo apt install build-essential bc bison flex libssl-dev make libc6-dev libncurses5-dev

Step-by-Step Writing a Device Driver
#

Before writing a device driver we need to clear some basics.

The Linux architecture is divided into two key spaces:

  • Kernel space, which is responsible for core OS operations and provides essential services
  • User space, where user applications are executed. Here, user applications harness services provided by the kernel

A System Call Interface facilitates interactions between user applications and the kernel. I did tell you in last blog that system calls can slow down a application because of something called context switch between Kernel and User space called Mode Switching. I will not go into details on why this slows down an application for now.

For the time being we are not interested in interactions between User and Kernel space. What we are more interested in is ro lower the context switch between Kernel and User space.

Creating a Kernel Module
#

We start with creating a very simple prototype of a kernel module. Here is a basic kernel module code:

#include <linux/init.h>
#include <linux/module.h>
   
static int my_init(void)
{
    printk(KERN_ALERT "my module inserted");
    return  0;
}
   
static void my_exit(void)
{
    printk(KERN_ALERT "bye bye!");
    return;
}
   
module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Code Embedded");
MODULE_DESCRIPTION("A simple kernel module");

Save it as my_module.c on Raspberry Pi and to compile it use the following Makefile:

Save the script below as Makefile

ifneq ($(KERNELRELEASE),)
obj-m := my_module.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

Run the following command in the terminal from the directory where these files were saved.

Note: these file should be saved in same directory.

this should create a my_module.ko file in the

Run the following command with root access to insert the module in Linux kernel.

sudo insmod my_module.ko

As you can see this has no functionality to it and will do nothing inside the kernel other than printing a message using printk api

// Print with LOG LEVEL your Message and arg[s] 
printk(KERN_INFO "Your Message %d", arg);

You can watch the module printing the message in kernel logs using following command:

dmesg | tail
dmesg

The module can be removed using executing following command with root access on terminal

sudo rmmod my_module.ko

Accessing GPIO using Device Driver
#

Accessing GPIO in Linux Kernel Module is not that different from user space memory mapped IO

First we need to Register address as we did in the previous blog

#define BCM2835_PERI_BASE      0xFE000000UL // BASE address
#define GPIO_BASE              (BCM2835_PERI_BASE + 0x200000) //GPIO Base addr 
#define BLOCK_SIZE             (4 * 1024) 
#define PIN                    3  // GPIO pin to toggle

To access the register address, unlike in previous blog we do not need to read the /dev/mem as we have already access to RAM.

We can use Linux Kernel provided api called ioremap to map the virtual addresses to physical addresses.

// get GPIO base address from virtual address
gpio_base = ioremap(GPIO_BASE, BLOCK_SIZE);

After we have the physical address for the gpio base we can play around with the registers using iowrite32 and ioread32 as we we did in previous blog.

reg_val = ioread32(gpio_base);
    reg_val &= ~(7 << (PIN * 3));  // Clear the 3 bits for pin 3
    reg_val |=  (1 << (PIN * 3));  // Set to output mode (001)
    iowrite32(reg_val, gpio_base);

We also create a new Kernel thread to toggle the PIN 3 using kthread_run api by passing it a toggle function shown below:

static int toggle_gpio(void *data)
{
   printk(KERN_INFO "GPIO toggle thread started.\n");

    while (!kthread_should_stop()) {
        iowrite32(1 << PIN, gpio_base + 0x1C);
        udelay(1);  /* Delay 1 microsecond */

        iowrite32(1 << PIN, gpio_base + 0x28);
        udelay(1);  /* Delay 1 microsecond */
    }

    printk(KERN_INFO "GPIO toggle thread stopping.\n");
    return 0;
}

The complete code given below:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/io.h>
#include <linux/kthread.h>
#include <linux/delay.h>

#define BCM2835_PERI_BASE      0xFE000000UL
#define GPIO_BASE              (BCM2835_PERI_BASE + 0x200000)
#define BLOCK_SIZE_MEM         (4 * 1024)
#define PIN                    3  /* GPIO pin to toggle */

/* Global variables */
static void __iomem *gpio_base;
static struct task_struct *toggle_thread;

static int toggle_gpio(void *data)
{
   printk(KERN_INFO "GPIO toggle thread started.\n");

    while (!kthread_should_stop()) {
        iowrite32(1 << PIN, gpio_base + 0x1C);
        udelay(1);  /* Delay 1 microsecond */

        iowrite32(1 << PIN, gpio_base + 0x28);
        udelay(1);  /* Delay 1 microsecond */
    }

    printk(KERN_INFO "GPIO toggle thread stopping.\n");
    return 0;
}

static int __init gpio_toggle_init(void)
{
    u32 reg_val;
 
    printk(KERN_INFO "GPIO toggle module init\n");
 
    gpio_base = ioremap(GPIO_BASE, BLOCK_SIZE_MEM);
     if (!gpio_base) {
        printk(KERN_ERR "Failed to map GPIO memory\n");
        return -ENOMEM;
    }

    reg_val = ioread32(gpio_base);
    reg_val &= ~(7 << (PIN * 3));
    reg_val |=  (1 << (PIN * 3));
    iowrite32(reg_val, gpio_base);

    toggle_thread = kthread_run(toggle_gpio, NULL, "gpio_toggle_thread");
    if (IS_ERR(toggle_thread)) {
        printk(KERN_ERR "Failed to create gpio toggle thread\n");
        iounmap(gpio_base);
        return PTR_ERR(toggle_thread);
    }

    return 0;
}

static void __exit gpio_toggle_exit(void)
{
    printk(KERN_INFO "GPIO toggle module exit\n");

    if (toggle_thread)
        kthread_stop(toggle_thread);

    if (gpio_base)
        iounmap(gpio_base);
}

module_init(gpio_toggle_init);
module_exit(gpio_toggle_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Code Embedded");
MODULE_DESCRIPTION("A kernel module to toggle GPIO PIN 3 on Raspberry Pi 4 ");

save the above code as gpio_toggle.c

Create and save a Makefile as shown below:

ifneq ($(KERNELRELEASE),)
obj-m := gpio_toggle.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

Inset module in the kernel

sudo insmod gpio_toggle.ko

I did the same measurement as I did in my previous blog and here is the result.

Logic 3

The maximum toggle frequency reached a whooping 458.716 kHz that is huge jump from approx 8 kHz we got with previous blog

But this is still not a stable 458.716 kHz as it suffers from context switches within Kernel. I will continue digging the rabbit hole in my upcoming blogs.


Update
#

I also found out that removing the udelay(1) completely gets me insane frequency of 6.25 MHz.

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/io.h>
#include <linux/kthread.h>
#include <linux/delay.h>

#define BCM2835_PERI_BASE      0xFE000000UL
#define GPIO_BASE              (BCM2835_PERI_BASE + 0x200000)
#define BLOCK_SIZE_MEM         (4 * 1024)
#define PIN                    3  /* GPIO pin to toggle */

/* Global variables */
static void __iomem *gpio_base;
static struct task_struct *toggle_thread;

static int toggle_gpio(void *data)
{
   printk(KERN_INFO "GPIO toggle thread started.\n");

    while (!kthread_should_stop()) {
        iowrite32(1 << PIN, gpio_base + 0x1C);
        iowrite32(1 << PIN, gpio_base + 0x28);
    }

    printk(KERN_INFO "GPIO toggle thread stopping.\n");
    return 0;
}

static int __init gpio_toggle_init(void)
{
    u32 reg_val;
 
    printk(KERN_INFO "GPIO toggle module init\n");
 
    gpio_base = ioremap(GPIO_BASE, BLOCK_SIZE_MEM);
     if (!gpio_base) {
        printk(KERN_ERR "Failed to map GPIO memory\n");
        return -ENOMEM;
    }

    reg_val = ioread32(gpio_base);
    reg_val &= ~(7 << (PIN * 3));
    reg_val |=  (1 << (PIN * 3));
    iowrite32(reg_val, gpio_base);

    toggle_thread = kthread_run(toggle_gpio, NULL, "gpio_toggle_thread");
    if (IS_ERR(toggle_thread)) {
        printk(KERN_ERR "Failed to create gpio toggle thread\n");
        iounmap(gpio_base);
        return PTR_ERR(toggle_thread);
    }

    return 0;
}

static void __exit gpio_toggle_exit(void)
{
    printk(KERN_INFO "GPIO toggle module exit\n");

    if (toggle_thread)
        kthread_stop(toggle_thread);

    if (gpio_base)
        iounmap(gpio_base);
}

module_init(gpio_toggle_init);
module_exit(gpio_toggle_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Code Embedded");
MODULE_DESCRIPTION("A kernel module to toggle GPIO PIN 3 on Raspberry Pi 4 ");

Here is the result captured using Logic Analyzer:

Logic 4

Tags: #raspberry #pi #gpio #RPi


DISCLAIMER: This kernel modules are provided for educational and demonstration purposes only.

USE AT YOUR OWN RISK.

The authors code disclaim any and all liability for any direct, indirect, incidental, special, or consequential damages arising out of or in connection with the use, modification, or distribution of this software. This module is provided “AS IS” without any express or implied warranties, including, but not limited to, the implied warranties of merchantability, fitness for a particular purpose, or non-infringement. By using this kernel module[s], you agree that you are solely responsible for ensuring that its use complies with all applicable laws, regulations, and licensing requirements. It is your responsibility to thoroughly test and verify the module in your environment prior to any production use. The author assumes no responsibility for any damage to your systems, data loss, or other adverse consequences that may result from the use or misuse of this module. If you do not agree with the terms of this disclaimer, do not use this kernel module provided above in any form.