IMX6ULL Linux Key Driver Practice: A Complete Analysis from GPIO Polling to Device Tree Interrupts + Wait Queues
In embedded Linux development, key drivers are a classic case for understanding character devices, Platform bus, interrupt mechanisms, and blocking I/O. Based on the IMX6ULL development board, this article integrates the core highlights of three blog posts, using “driver version evolution” as the main thread to comprehensively dissect the optimization path of key drivers—from basic GPIO polling to standard device tree interrupts + wait queues. It balances theoretical depth, code details, and practical implementation, helping developers master the core design philosophy of Linux drivers.
I. Core Technology Stack and Design Philosophy
1.1 Core Technology System
| Device Tree (DTS/DTB) | Uniformly describes hardware resources like key GPIOs and interrupts, decoupling drivers from hardware via the compatible property. |
| GPIO Subsystem | Encapsulates low-level register operations, providing standardized APIs like gpio_request and gpio_get_value to simplify hardware operations. |
| Interrupt Handling | Uses “event triggering” instead of polling, triggering interrupts on key press/release to reduce CPU usage (request_irq + ISR). |
| Wait Queue | Implements “sleep-wake” mechanism, releasing CPU when the application waits for key events and waking up after interrupts, achieving efficient blocking I/O. |
| Platform Bus | Automatically matches device tree nodes with drivers via the compatible property, standardizing driver initialization (probe function). |
1.2 Driver Evolution Design Philosophy
From “functional implementation” to “high performance + high portability,” the evolution of the three versions perfectly illustrates the Linux driver design principle of “low coupling, high cohesion”:
| V1 | Pure GPIO Polling | High | Medium | Minimal logic, ideal for understanding GPIO operations. |
| V2 | GPIO-to-Interrupt + Wait Queue | Low | Medium | Introduces blocking I/O, solving CPU usage issues. |
| V3 | Device Tree Interrupt + Wait Queue | Low | High | Fully decouples hardware details, standard Linux practice. |
II. Device Tree (DTS) Writing: Standardizing Hardware Description
The device tree serves as the “bridge” between drivers and hardware, uniformly describing key GPIO and interrupt resources without hardcoding hardware details in the driver.
2.1 DTS Node Example (Added to IMX6ULL Device Tree File)
ptkey {
compatible = "pt-key"; // Matches driver's `of_device_id`, the core matching property.
ptkey-gpio = <&gpio1 4 GPIO_ACTIVE_LOW>; // Key GPIO (GPIO1_IO04, active low).
interrupt-parent = <&gpio1>; // Interrupt parent controller (GPIO1).
interrupts = <4 IRQ_TYPE_EDGE_FALLING>; // Interrupt number 4, falling-edge triggered (key press).
};
- &gpio1: References the IMX6ULL’s GPIO1 controller node.
- GPIO_ACTIVE_LOW: Indicates the GPIO is active low (key press pulls GPIO low).
- interrupts: Defines interrupt trigger mode; IRQ_TYPE_EDGE_FALLING means falling-edge triggered.
2.2 Device Tree Matching Mechanism
III. Driver Code Analysis: Detailed Evolution of Three Versions
3.1 V1: Pure GPIO Polling Driver (key.c)
The most basic implementation, polling GPIO levels to determine key status, ideal for beginners to understand the GPIO subsystem.
Core Code
#define DEV_NAME "key"
static int key_gpio;
// Read key status (1=not pressed, 0=pressed).
static inline int get_key_status(void) {
return gpio_get_value(key_gpio); // GPIO subsystem API, reads level.
}
// `read` function: Returns key status when the application calls `read`.
static ssize_t read(struct file *file, char __user *buf, size_t size, loff_t *loff) {
int status = get_key_status();
// Copies data from kernel space to user space.
if (copy_to_user(buf, &status, sizeof(status)))
return –EFAULT;
return sizeof(status);
}
// `probe` function: Core driver initialization flow.
static int probe(struct platform_device *pdev) {
struct device_node *pdts;
int ret = misc_register(&misc_dev); // Registers a miscellaneous device, auto-creates `/dev/key`.
if (ret) goto err_misc_register;
pdts = of_find_node_by_path("/ptkey"); // Finds the device tree node.
key_gpio = of_get_named_gpio(pdts, "ptkey-gpio", 0); // Gets GPIO number from the node.
ret = gpio_request(key_gpio, "key"); // Requests GPIO resources to avoid conflicts.
gpio_direction_input(key_gpio); // Configures GPIO as input mode.
printk("V1: GPIO polling driver initialized.\\n");
return 0;
// Error handling: Releases allocated resources to avoid memory leaks.
err_misc_register:
misc_deregister(&misc_dev);
printk("Driver initialization failed, ret=%d\\n", ret);
return ret;
}
Workflow and Drawbacks
- Workflow: Application calls read in a while(1) loop → driver reads GPIO level → returns status.
- Drawbacks: High CPU usage (up to 99%), inability to capture transient key actions, poor real-time performance.
3.2 V2: Interrupt + Wait Queue (key_irq.c)
Core optimization: Replaces polling with interrupts and implements “sleep-wake” via wait queues, solving CPU usage issues.
3.2.1 Core Mechanism: Wait Queue
Wait queues are the core of Linux blocking I/O, acting as a “process waiting room”:
- When no key event occurs, the application sleeps, releasing CPU.
- When a key triggers an interrupt, the driver wakes the sleeping process for efficient event response.
3.2.2 Core Code Modifications
// Defines wait queue and condition variable.
static wait_queue_head_t wq; // Wait queue head.
static int condition = 0; // Event readiness flag (0=not ready, 1=ready).
static int key_irq; // Interrupt number.
// Interrupt service function (top half): Triggered on key press.
static irqreturn_t key_irq_handler(int irq, void *dev) {
int arg = *(int *)dev;
if (100 != arg) return IRQ_NONE; // Validates private data to avoid false triggers.
condition = 1; // Marks event as ready.
wake_up_interruptible(&wq); // Wakes up the waiting application process.
return IRQ_HANDLED; // Informs the kernel the interrupt is handled.
}
// Modified `read` function: Implements blocking I/O.
static ssize_t read(struct file *file, char __user *buf, size_t size, loff_t *loff) {
condition = 0;
// Sleeps if condition is unmet (0% CPU usage).
wait_event_interruptible(wq, condition);
int status = 1; // Marks key press event.
copy_to_user(buf, &status, sizeof(status));
return sizeof(status);
}
// `probe` function: Adds interrupt initialization.
static int probe(struct platform_device *pdev) {
// (GPIO initialization omitted, same as V1.)
key_irq = gpio_to_irq(key_gpio); // Converts GPIO to interrupt number (depends on GPIO-IRQ binding).
// Registers interrupt: interrupt number, ISR, trigger mode, name, private data.
ret = request_irq(key_irq, key_irq_handler,
IRQF_TRIGGER_FALLING, "key0_irq", &arg);
init_waitqueue_head(&wq); // Initializes wait queue.
printk("V2: Interrupt + wait queue driver initialized.\\n");
return 0;
}
Core Advantages
- 0% CPU usage when the application is blocked, responding only on key press.
- Timely interrupt response, capturing transient key actions with improved real-time performance.
- Retains miscellaneous device features, auto-creating /dev/key without manual mknod.
3.3 V3: Interrupt Parsing via Device Tree (key_irq_sub.c)
The gpio_to_irq approach in V2 still relies on “fixed GPIO-interrupt number binding.” V3 resolves interrupt numbers directly from the device tree, fully decoupling hardware details—the standard Linux implementation.
Key Improvement: Interrupt Number Resolution
// V2 approach: Depends on GPIO-interrupt binding
// key_irq = gpio_to_irq(key_gpio);
// V3 approach: Parses interrupt number from device tree (recommended)
key_irq = irq_of_parse_and_map(pdts, 0); // pdts: device tree node pointer, 0: interrupt index
if (key_irq < 0) {
printk("Failed to parse interrupt number\\n");
return –EINVAL;
}
Core Advantages
- No dependency on fixed GPIO-interrupt binding; hardware changes only require device tree updates.
- Driver code is generic and portable to other Linux platforms supporting device trees.
- Fully aligns with Linux device driver models, adhering to the “hardware-description-and-driver-logic-separation” philosophy.
3.4 Unified Platform Driver Framework
All three versions are built on the standard Platform driver framework, ensuring scalability and compatibility:
// Device tree match table: Matches DTS node's 'compatible' property
static struct of_device_id key_table[] = {
{.compatible = "pt-key"},
{} // Sentinel
};
// Platform driver structure
static struct platform_driver pdrv = {
.probe = probe, // Executed on successful match
.remove = remove, // Executed on driver unload
.driver = {
.name = DEV_NAME,
.of_match_table = key_table, // Device tree match table
}
};
// Driver entry/exit (simplified via macros)
module_platform_driver(pdrv);
MODULE_LICENSE("GPL"); // Open-source license declaration
IV. Application: Efficient Blocking I/O
The application avoids polling by using read to block until key events occur, ensuring simplicity and efficiency.
4.1 Application Code
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, const char *argv[]) {
int fd = open("/dev/key", O_RDWR);
if (fd < 0) {
perror("Failed to open /dev/key");
return 1;
}
int status = 0;
while (1) {
// Blocks until key event; process sleeps when idle
int ret = read(fd, &status, sizeof(status));
printf("Key pressed! ret=%d, status=%d\\n", ret, status);
}
close(fd);
return 0;
}
4.2 Performance Comparison
| V1 | ~99% | Polling, potential missed events |
| V2/V3 | 0% (sleep) | Interrupt-triggered, real-time |
V. Practical Verification: Compilation to Execution
5.1 Compilation Setup
(1) Driver Module Compilation (Makefile)
# Select driver version (choose one)
obj-m += key.o # V1: GPIO polling
# obj-m += key_irq.o # V2: Interrupt + wait queue
# obj-m += key_irq_sub.o # V3: Device tree interrupt parsing
KERNELDIR ?= /home/linux/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux-gnueabihf- ARCH=arm
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
Run: make to generate .ko driver module.
(2) Application Compilation
arm-linux-gnueabihf-gcc key_app.c -o key_app
(3) Device Tree Compilation
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
Generates updated device tree (e.g., imx6ull-14×14-evk.dtb).
5.2 Driver Loading and Validation (V3 Recommended)
ls /dev/key # Verify device node creation
Expected output:key platform_driver_register success
######################### key_driver probe
irq = 123 dev = 100 # Interrupt number and private data
VI. Troubleshooting Guide
6.1 Driver Match Failure (probe Not Executed)
- compatible mismatch: Ensure of_device_id in the driver exactly matches the DTS node.
- Incorrect DTS node path: Verify of_find_node_by_path("/ptkey") matches the DTS path.
- Device tree not updated: Confirm the compiled DTB file is flashed.
6.2 Interrupt Registration Failure (request_irq Returns Negative)
- Incorrect interrupt number: Check irq_of_parse_and_map return value; validate DTS interrupt configuration.
- Interrupt conflict: Use cat /proc/interrupts to check occupied interrupts.
- Trigger mode error: Ensure the interrupt trigger matches hardware (e.g., falling edge for keys).
6.3 Application Not Sleeping (100% CPU Usage)
- Uninitialized wait queue: Call init_waitqueue_head(&wq).
- ISR not waking queue: Ensure condition=1 and wake_up_interruptible(&wq) are called.
- Condition variable not reset: Set condition=0 in read before waiting.
6.4 Unresponsive Key
- GPIO misconfiguration: Confirm gpio_direction_input sets GPIO as input.
- Hardware wiring error: GPIO should be high when idle, low when pressed.
- Incorrect interrupt trigger: For key-release events, use IRQF_TRIGGER_RISING.
VII. Key Takeaways
7.1 Essential API Reference
| of_find_node_by_path | Locates device tree node by path |
| of_get_named_gpio | Retrieves GPIO number from DTS node |
| gpio_request | Reserves GPIO resources |
| gpio_to_irq | Converts GPIO to interrupt (V2) |
| irq_of_parse_and_map | Parses interrupt from DTS (V3 recommended) |
| request_irq | Registers interrupt handler |
| init_waitqueue_head | Initializes wait queue |
| wait_event_interruptible | Puts process to sleep until condition |
| wake_up_interruptible | Wakes sleeping process |
7.2 Design Principles
VIII. Conclusion
This guide demonstrates the evolution of a Linux key driver from “basic functionality” to “high-performance + high portability”:
- V1: Introduces GPIO subsystem basics.
- V2: Core interrupt and wait queue mechanisms to eliminate CPU waste.
- V3: Device tree standardization for full hardware-driver decoupling.
This methodology applies to all Linux input devices (e.g., touch keys, encoders, IR receivers) and forms the foundation for advanced driver development (e.g., input subsystems, touchscreen drivers).
网硕互联帮助中心




评论前必须登录!
注册