Understanding GPIO Access in Linux: Why gpio_set_value_cansleep() Fixed the Warning

Understanding GPIO Access in Linux: Why gpio_set_value_cansleep() Fixed the Warning

When working with GPIOs (General-Purpose Input/Output pins) in Linux, developers often encounter warnings about invalid contexts for sleeping. These warnings can be confusing, especially when using mechanisms like workqueues that are supposed to run in sleepable contexts. In this article, we’ll dissect a real-world example involving a Raspberry Pi GPIO interrupt handler, a workqueue, and a critical warning caused by improper GPIO access. We’ll explain why replacing gpio_set_value() with gpio_set_value_cansleep() resolved the issue and how to avoid similar pitfalls.


The Problem: A Mysterious Kernel Warning

Consider a kernel module that toggles an LED when a GPIO button is pressed. The module uses a workqueue to defer the LED toggling from an interrupt handler (atomic context) to a process context (sleepable). Here’s the key code snippet:

// Original work handler (caused a warning)
static void gpio_work_handler(struct work_struct *work) {
    led_state = !led_state;
    gpio_set_value(GPIO_LED_PIN, led_state); // WARNING HERE
    printk(KERN_INFO "LED toggled to %s\n", led_state ? "ON" : "OFF");
}
        

When the button is pressed, the kernel logs a warning:

[  582.523153] gpiochip pinctrl-rp1 GPIO16 interrupt triggered!
[  582.523180] WARNING: CPU: 0 PID: 11 at drivers/gpio/gpiolib.c:3281 gpiod_set_raw_value+0x94/0xe0
        

The warning points to gpiod_set_raw_value(), which is called by gpio_set_value(). But why does this happen in a workqueue handler, which runs in process context?


Root Cause: Fast vs. Slow GPIO Controllers

The warning arises because the GPIO being accessed is connected to a slow GPIO controller, such as an I2C or SPI-based GPIO expander. Linux categorizes GPIO controllers into two types:

  • Fast GPIO Controllers

Directly memory-mapped (e.g., SoC GPIOs).

Operations are atomic and non-blocking.

Use gpio_set_value().

  • Slow GPIO Controllers

Accessed via buses like I2C or SPI (e.g., PCA953x, MCP230xx).

Operations may require waiting for bus transactions (sleeping).

Use gpio_set_value_cansleep().

Why gpio_set_value() Triggered a Warning

  • The LED’s GPIO (GPIO_LED_PIN) in this example is connected to a slow controller.
  • gpio_set_value() assumes the GPIO can be accessed atomically. For slow controllers, this is unsafe because the operation might sleep.
  • The kernel detects this mismatch and issues a warning to prevent instability.


The Fix: gpio_set_value_cansleep()

The corrected work handler uses gpio_set_value_cansleep():

// Fixed work handler
static void gpio_work_handler(struct work_struct *work) {
    led_state = !led_state;
    gpio_set_value_cansleep(GPIO_LED_PIN, led_state); // SAFE FOR SLOW GPIOs
    printk(KERN_INFO "LED toggled to %s\n", led_state ? "ON" : "OFF");
}
        

How It Works:

  • gpio_set_value_cansleep() explicitly informs the kernel that the GPIO operation might sleep.
  • This function is safe to use in process context (e.g., workqueues, threaded interrupts) where sleeping is allowed.
  • The warning disappears because the kernel no longer detects an invalid context.


Key Takeaways

1. Know Your GPIO Hardware

  • Determine if the GPIO is memory-mapped (fast) or bus-connected (slow).
  • Use gpiod_cansleep() to check if a GPIO can sleep:

if (gpiod_cansleep(gpio_desc)) { 
// Use _cansleep() variant 
}        
struct gpio_chip *gpio =xxx;
gpio->can_sleep = 1;        
/**
 * gpiod_cansleep() - report whether gpio value access may sleep
 * @desc: gpio to check
 *
 */
int gpiod_cansleep(const struct gpio_desc *desc)
{
	VALIDATE_DESC(desc);
	return desc->gdev->chip->can_sleep;
}        

2. Match Functions to Context


Article content

3.Workqueues Are Not Always "Safe" by Default

  • While workqueues run in process context, you must still use the correct GPIO API based on the hardware.
  • A common mistake is assuming all GPIOs are fast and using gpio_set_value() universally.


Debugging Tips

  • Check GPIO Controller Type

Use gpioinfo from the gpiod toolkit:

sudo gpioinfo

gpiochip0 - 54 lines:
  line 0: "ID_SDA" unused input active-high
gpiochip1 - 8 lines:
  line 0: "pca9555" "led0" output active-high [used]        

Here, gpiochip1 is likely an I2C-based expander requiring _cansleep().

  • Inspect Kernel Logs

Warnings like WARNING: CPU: 0 ... gpiod_set_raw_value+0x94/0xe0 indicate a sleep in an invalid context. Search for the GPIO number and controller in the log.


Why This Matters

Using the wrong GPIO API can lead to:

  • Kernel instability: Sleeping in atomic contexts causes hangs or crashes.
  • Missed interrupts: Blocking operations delay interrupt handling.
  • Subtle bugs: Symptoms may appear only under load or with specific hardware.


Conclusion

The transition from gpio_set_value() to gpio_set_value_cansleep() in the example highlights a critical aspect of Linux GPIO programming: hardware awareness. By understanding the underlying GPIO controller type and selecting the appropriate API, developers ensure safe and reliable operation. Whether you’re toggling LEDs or interfacing with complex sensors, always:

  1. Verify if a GPIO might sleep.
  2. Use _cansleep() functions in process context.
  3. Test on actual hardware to catch warnings early.

This approach not only silences kernel warnings but also builds robust drivers ready for real-world deployment

Appendix:

Source Code adds the gpiod_cansleep checking and uses a GPIO descriptor approach rather than referencing the pin directly.

#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/gpio/consumer.h>

#define GPIO_BUTTON_PIN 587
#define GPIO_LED_PIN 625

static int irq_number;
static int led_state = 0;  // Track LED state
static struct workqueue_struct *gpio_wq;
static struct work_struct gpio_work;
static struct gpio_desc *button_desc = NULL;
static struct gpio_desc *led_desc = NULL;

// Work function that will run in process context
static void gpio_work_handler(struct work_struct *work) {
    // Toggle LED state
    led_state = !led_state;
    
    // Check if the LED GPIO is sleepable and use appropriate function
    if (gpiod_cansleep(led_desc)) {
        gpiod_set_value_cansleep(led_desc, led_state);
        printk(KERN_INFO "Used gpiod_set_value_cansleep for LED\n");
    } else {
        gpiod_set_value(led_desc, led_state);
        printk(KERN_INFO "Used gpiod_set_value for LED\n");
    }
    
    printk(KERN_INFO "LED toggled to %s\n", led_state ? "ON" : "OFF");
}

// Interrupt handler (top half)
static irqreturn_t gpio_irq_handler(int irq, void *dev_id) {
    printk(KERN_INFO "gpiochip pinctrl-rp1 GPIO16 interrupt triggered!\n");
    
    // Schedule the work to run in process context
    queue_work(gpio_wq, &gpio_work);
    
    return IRQ_HANDLED;
}

// Module initialization
static int __init gpio_irq_init(void) {
    int ret;

    printk(KERN_INFO "---------------------------------- gpio_irq_init start\n");

    // Create workqueue
    gpio_wq = create_singlethread_workqueue("gpio_workqueue");
    if (!gpio_wq) {
        printk(KERN_ERR "Failed to create workqueue\n");
        return -ENOMEM;
    }
    
    // Initialize work
    INIT_WORK(&gpio_work, gpio_work_handler);

    // Get GPIO descriptors
    button_desc = gpio_to_desc(GPIO_BUTTON_PIN);
    if (!button_desc) {
        printk(KERN_ERR "Failed to get descriptor for GPIO%d\n", GPIO_BUTTON_PIN);
        destroy_workqueue(gpio_wq);
        return -EINVAL;
    }
    printk(KERN_INFO "Got descriptor for GPIO%d\n", GPIO_BUTTON_PIN);
    
    // Check if button GPIO is sleepable
    if (gpiod_cansleep(button_desc)) {
        printk(KERN_INFO "Button GPIO%d is sleepable\n", GPIO_BUTTON_PIN);
    } else {
        printk(KERN_INFO "Button GPIO%d is not sleepable\n", GPIO_BUTTON_PIN);
    }

    // Configure as input
    ret = gpiod_direction_input(button_desc);
    if (ret) {
        printk(KERN_ERR "Failed to set GPIO%d as input\n", GPIO_BUTTON_PIN);
        destroy_workqueue(gpio_wq);
        return ret;
    }
    printk(KERN_INFO "Set GPIO%d as input\n", GPIO_BUTTON_PIN);

    // Get LED GPIO descriptor
    led_desc = gpio_to_desc(GPIO_LED_PIN);
    if (!led_desc) {
        printk(KERN_ERR "Failed to get descriptor for GPIO%d (LED)\n", GPIO_LED_PIN);
        destroy_workqueue(gpio_wq);
        return -EINVAL;
    }
    printk(KERN_INFO "Got descriptor for GPIO%d (LED)\n", GPIO_LED_PIN);
    
    // Check if LED GPIO is sleepable
    if (gpiod_cansleep(led_desc)) {
        printk(KERN_INFO "LED GPIO%d is sleepable\n", GPIO_LED_PIN);
    } else {
        printk(KERN_INFO "LED GPIO%d is not sleepable\n", GPIO_LED_PIN);
    }

    // Configure LED as output
    ret = gpiod_direction_output(led_desc, 0);  // Initialize to OFF
    if (ret) {
        printk(KERN_ERR "Failed to set GPIO%d as output for LED\n", GPIO_LED_PIN);
        destroy_workqueue(gpio_wq);
        return ret;
    }
    printk(KERN_INFO "Set GPIO%d as output for LED\n", GPIO_LED_PIN);

    // Map GPIO to IRQ number
    irq_number = gpiod_to_irq(button_desc);
    if (irq_number < 0) {
        printk(KERN_ERR "Failed to map GPIO to IRQ\n");
        destroy_workqueue(gpio_wq);
        return irq_number;
    }
    printk(KERN_INFO "Map GPIO%d to IRQ%d\n", GPIO_BUTTON_PIN, irq_number);

    // Request IRQ (rising edge trigger)
    ret = request_irq(irq_number, gpio_irq_handler,
                      IRQF_TRIGGER_RISING, "gpio16_irq", NULL);
    if (ret) {
        printk(KERN_ERR "Failed to request IRQ %d\n", irq_number);
        destroy_workqueue(gpio_wq);
        return ret;
    }
    printk(KERN_INFO "Request IRQ%d\n", irq_number);

    printk(KERN_INFO "---------------------------------- gpio_irq_init end\n");
    return 0;
}

// Module cleanup
static void __exit gpio_irq_exit(void) {
    printk(KERN_INFO "---------------------------------- gpio_irq_exit start\n");
    free_irq(irq_number, NULL);
    destroy_workqueue(gpio_wq);
    printk(KERN_INFO "---------------------------------- gpio_irq_exit end\n");
}

module_init(gpio_irq_init);
module_exit(gpio_irq_exit);
MODULE_LICENSE("GPL");        

To view or add a comment, sign in

More articles by David Zhu

Insights from the community

Others also viewed

Explore topics