This post is part of the series about my
personal
BeagleBone Black project. The
Go REST API and the
Rust HAL both access
GPIO/I2C from user space (/sys/class/gpio, /dev/i2c-*). That’s plenty
for most use cases. This post shows the deeper level: a kernel module of
your own, for when user-space access no longer suffices.
When is user space no longer enough?
| Requirement | User space (sysfs//dev/i2c-*) |
|---|---|
Kernel module | Occasional read/write |
Sufficient | Unnecessary overhead |
Event-driven reaction to a signal (e.g. a button interrupt) | Only possible via polling — costs CPU time and latency |
| Hard timing requirements |
Cannot be guaranteed (user space can be preempted at any time) | Considerably better, though still not hard, real-time guarantees |
A kernel module is no silver bullet — it comes with its own risks (a crash brings down the whole system, not just one process). Rule of thumb: try user space first, reach for a kernel module only once polling or latency actually become a problem. |
Basic structure of an out-of-tree module
// gpio_irq_demo.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init gpio_irq_demo_init(void)
{
pr_info("gpio_irq_demo: loaded\n");
return 0;
}
static void __exit gpio_irq_demo_exit(void)
{
pr_info("gpio_irq_demo: unloaded\n");
}
module_init(gpio_irq_demo_init);
module_exit(gpio_irq_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Paul Fleischmann");
MODULE_DESCRIPTION("GPIO interrupt demo character device");The matching Makefile for an out-of-tree module:
obj-m += gpio_irq_demo.o
KDIR ?= /lib/modules/$(shell uname -r)/build
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) cleanLocally, against a kernel with matching headers, plain make is enough.
For the BeagleBone Black, it’s cross-compiled against the Yocto kernel
instead (see below).
Registering a character-device interface
A kernel module alone doesn’t do anything yet — it needs an interface that user space can interact with. For a simple device, a character device fits well:
// gpio_irq_demo.c (continued)
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "gpio_irq_demo"
static dev_t dev_num;
static struct cdev my_cdev;
static int irq_count = 0;
static ssize_t demo_read(struct file *file, char __user *buf,
size_t len, loff_t *offset)
{
char msg[16];
int msg_len = snprintf(msg, sizeof(msg), "%d\n", irq_count);
if (*offset > 0)
return 0;
if (copy_to_user(buf, msg, msg_len))
return -EFAULT;
*offset += msg_len;
return msg_len;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.read = demo_read,
};demo_read returns the number of interrupts seen so far to user space —
a simple cat /dev/gpio_irq_demo shows the current count.
GPIO interrupt handling
The real payoff over user-space polling: the kernel only becomes active when an event actually occurs.
// gpio_irq_demo.c (continued)
#include <linux/gpio.h>
#include <linux/interrupt.h>
#define GPIO_PIN 60 /* e.g. P9_12 on the BeagleBone Black */
static int irq_number;
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
irq_count++;
pr_info("gpio_irq_demo: interrupt #%d on GPIO %d\n", irq_count, GPIO_PIN);
return IRQ_HANDLED;
}
static int setup_gpio_irq(void)
{
int ret;
ret = gpio_request(GPIO_PIN, "gpio_irq_demo");
if (ret)
return ret;
gpio_direction_input(GPIO_PIN);
irq_number = gpio_to_irq(GPIO_PIN);
ret = request_irq(irq_number, gpio_irq_handler,
IRQF_TRIGGER_RISING, "gpio_irq_demo", NULL);
if (ret) {
gpio_free(GPIO_PIN);
return ret;
}
return 0;
}
|
For comparison, the equivalent user-space solution via sysfs looks like this — with the crucial difference that it has to actively poll:
echo 60 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio60/direction
echo rising > /sys/class/gpio/gpio60/edge
# Polling loop instead of a real interrupt:
while true; do
cat /sys/class/gpio/gpio60/value
sleep 0.1
doneCross-compiling against the Yocto kernel
Unlike the local make against the host kernel headers, the BeagleBone
Black needs the kernel headers and toolchain from the
Yocto
build:
source /opt/poky/*/environment-setup-cortexa8hf-neon-poky-linux-gnueabi
make -C $KERNEL_SRC M=$(pwd) modules$KERNEL_SRC points to the configured kernel source tree of the Yocto
build (tmp/work-shared/beaglebone-yocto/kernel-source), not a generic
ARM kernel — the module must be built against exactly the kernel version
and configuration running on the target, otherwise loading fails with a
version mismatch.
Loading, unloading, diagnostics
sudo insmod gpio_irq_demo.ko
dmesg | tail -5
cat /dev/gpio_irq_demo # current interrupt count
sudo rmmod gpio_irq_demo
dmesg | tail -5
|
Summary
| Aspect | Key takeaway |
|---|---|
When a kernel module is needed | When user-space (sysfs) polling fails on CPU load or latency |
Interrupt handler | Keep it short and non-blocking — move long work into a bottom half |
Cross-compilation | Must happen against the exact kernel version/configuration of the Yocto build |
Diagnostics |
|