Skip to content

Instantly share code, notes, and snippets.

@rday
Created December 14, 2015 15:42
Show Gist options
  • Save rday/c796e3307c4087ae3539 to your computer and use it in GitHub Desktop.
Save rday/c796e3307c4087ae3539 to your computer and use it in GitHub Desktop.
Kernel module to detect button presses and record the duration
/*
* Monitor GPIO for button press
* https://www.kernel.org/doc/Documentation/gpio/gpio.txt
* drivers/gpio/gpiolib.c
*
* This is a simple kernel module to detect the duration of a button press on a Raspberry Pi 2.
* The button press durations are stored in a kfifo. A character device can be read, and the
* next available button press will be sent to user space. Only one process can read from
* the character device at a time.
*
* The BCM2709 doesn't recognize HIGH and LOW GPIO interrupts, only RISING and FALLING.
* This is problematic since we will get several interrupts per button press and release.
* We decide to only recognize the initial RISING interrupt and FALLING interrupt. The
* subsequent interrupts are ignored.
*
* Todo:
* - In a real world scenario, an interrupt should be triggered from servo feedback or some
* other hardware mechanism indicating a pour, not just a button press.
* - We'd also want to make this much more configurable and build a proper interface from
* userspace.
* - The character device should support polling.
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>
#include <linux/ktime.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/kfifo.h>
#include <linux/mutex.h>
#define DEVICE_NAME "gpio_monitor"
#define CLASS_NAME "gpiomonitor"
static unsigned int gpio_btn = 18;
static unsigned int irq_number = 0;
static spinlock_t gpio_press_time_lock;
static ktime_t gpio_press_time;
static struct class* char_class = NULL;
static struct device* char_device = NULL;
static int major_number;
static struct kfifo gpio_press_fifo;
static DEFINE_MUTEX(gpio_monitor_mutex);
/*
* Validate the access mode of the open, and lock the device so only
* one process can get data at a time.
*/
static int dev_open(struct inode *i, struct file *f)
{
/* Our sample device does not allow write access */
if ( ((f->f_flags & O_ACCMODE) == O_WRONLY)
|| ((f->f_flags & O_ACCMODE) == O_RDWR) ) {
printk(KERN_INFO "GPIO Monitor: Write access not supported\n");
return -EACCES;
}
// We only allow one user at a time
if (!mutex_trylock(&gpio_monitor_mutex)) {
printk(KERN_INFO "GPIO Monitor: Device already in use\n");
return -EBUSY;
}
printk(KERN_INFO "GPIO Monitor: device opened\n");
return 0;
}
/*
* Now that the user process has released the device, others can use it.
*/
static int dev_release(struct inode *i, struct file *f)
{
mutex_unlock(&gpio_monitor_mutex);
printk(KERN_INFO "GPIO Monitor: device released\n");
return 0;
}
/*
* We don't support writes
*/
static ssize_t dev_write(struct file *f, const char *buf, size_t len, loff_t *off)
{
return -EINVAL;
}
/*
* This is a very basic read function. We should be able to poll this device and
* copy more than one button press at a time, but this is just an example
*/
static ssize_t dev_read(struct file *filep, char *buf, size_t len, loff_t *offset)
{
int err = 0;
unsigned int copied;
if (kfifo_is_empty(&gpio_press_fifo)) {
return 0;
}
err = kfifo_to_user(&gpio_press_fifo, buf, sizeof(unsigned int), &copied);
return copied;
}
/**
* This handler will be called every time we detect a rise or fall on the GPIO
*/
static irq_handler_t gpio_pressed(unsigned int irq, void *dev_id, struct pt_regs *regs){
uint8_t value;
unsigned int duration;
spin_lock(&gpio_press_time_lock);
value = gpio_get_value(gpio_btn);
if (value==0) {
// If no time has elapsed, we probably already cleared on a FALLING
// interrupt. So finish the handler.
if (ktime_to_ms(gpio_press_time) == 0) {
goto finished;
}
duration = ktime_to_ms(ktime_sub(ktime_get(), gpio_press_time));
if (duration==0) {
goto finished;
}
gpio_press_time = ktime_set(0, 0);
kfifo_in(&gpio_press_fifo, &duration, sizeof(unsigned int));
printk(KERN_INFO "GPIO_MONITOR: Detected button release, duration of %u\n", duration);
} else {
// If a time is already set, we already received a RISING interrupt.
// So we can finish the handler.
if (ktime_to_ms(gpio_press_time) > 0) {
goto finished;
}
printk(KERN_INFO "GPIO_MONITOR: Detected button press\n");
gpio_press_time = ktime_get();
}
finished:
spin_unlock(&gpio_press_time_lock);
return (irq_handler_t) IRQ_HANDLED; // Announce that the IRQ has been handled correctly
}
static struct file_operations fops =
{
.open = dev_open,
.read = dev_read,
.write = dev_write,
.release = dev_release,
};
int register_chardev(void)
{
major_number = register_chrdev(0, DEVICE_NAME, &fops);
if (major_number<0){
printk(KERN_ALERT "GPIO Monitor: failed to register a major number\n");
return major_number;
}
char_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(char_class)){ // Check for error and clean up if there is
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_ALERT "Failed to register device class\n");
return PTR_ERR(char_class); // Correct way to return an error on a pointer
}
char_device = device_create(char_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
if (IS_ERR(char_device)){ // Clean up if there is an error
class_destroy(char_class); // Repeated code but the alternative is goto statements
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_ALERT "Failed to create the device\n");
return PTR_ERR(char_device);
}
return 0;
}
int init_module(void)
{
int result = 0;
printk(KERN_INFO "GPIO Monitor loading...\n");
if (register_chardev() < 0) {
return -1;
}
if (kfifo_alloc(&gpio_press_fifo, 128, GFP_KERNEL)) {
printk(KERN_ERR "Could not allocate FIFO\n");
return -ENOMEM;
}
mutex_init(&gpio_monitor_mutex);
printk(KERN_INFO "GPIO Monitor: Major number %d\n", major_number);
if (!gpio_is_valid(gpio_btn)) {
printk(KERN_INFO "Invalid GPIO %d\n", gpio_btn);
return -ENODEV;
}
gpio_direction_input(gpio_btn);
gpio_press_time = ktime_set(0, 0);
printk(KERN_INFO "GPIO Monitor loaded.\n");
if (gpio_get_value(gpio_btn) == 1) {
printk(KERN_INFO "Initialized in middle of button press");
}
irq_number = gpio_to_irq(gpio_btn);
// BCM2709 doesn't support high and low gpio interrupts, only rising and falling
result = request_irq(irq_number,
(irq_handler_t) gpio_pressed,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"gpio_pressed",
NULL);
printk(KERN_INFO "Pressed interrupt request resulted in %d\n", result);
if (result<0) {
return result;
}
return 0;
}
void cleanup_module(void)
{
free_irq(irq_number, NULL);
gpio_free(gpio_btn);
kfifo_free(&gpio_press_fifo);
device_destroy(char_class, MKDEV(major_number, 0));
class_unregister(char_class);
class_destroy(char_class);
unregister_chrdev(major_number, DEVICE_NAME);
printk(KERN_INFO "GPIO Monitor unloaded.\n");
}
MODULE_LICENSE("GPL");
/*****************************
// This is the user space program which will read the character
// device to find new button presses.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
int main(){
int ret, fd;
unsigned int press_time;
fd = open("/dev/gpio_monitor", O_RDONLY); // Open the device with read/write access
if (fd < 0){
perror("Failed to open the device...");
return errno;
}
ret = read(fd, &press_time, sizeof(unsigned int));
if (ret <= 0) {
perror("No data available for reading");
return errno;
}
printf("Button press of duration: %d %d\n", press_time, ret);
return 0;
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment