嵌入式linux学习记录六、读取按键值的方式 一、查询方式原理用户空间不断调用 read() 驱动直接读取 GPIO 电平返回 没有按键也立即返回实现步骤驱动层// 1. read 函数直接读取返回不等待 static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off) { int val; // 直接读 GPIO不管有没有事件 val gpio_get_value(gpio_num); copy_to_user(buf, val, sizeof(val)); return sizeof(val); }用户层// 2. 用户空间不断轮询 while (1) { read(fd, val, sizeof(val)); if (val 0) printf(按键按下\n); usleep(10000); // 延时10ms }缺点CPU 一直轮询浪费资源 实时性差 不推荐实际使用二、休眠-唤醒方式原理没有按键事件 → 进程休眠让出 CPU 按键按下中断 → 唤醒进程 进程读取按键值返回实现步骤驱动层static wait_queue_head_t wq; static bool has_event false; static int key_val; static int gpio_num; // 1. 中断回调函数 static irqreturn_t key_handler(int irq, void *dev_id) { key_val gpio_get_value(gpio_num); has_event true; wake_up_interruptible(wq); // 唤醒休眠进程 return IRQ_HANDLED; } // 2. probe 里完整初始化 static int key_probe(struct platform_device *pdev) { struct device_node *node pdev-dev.of_node; int irq, ret; // 初始化等待队列 init_waitqueue_head(wq); // 获取 gpio 编号 gpio_num of_get_named_gpio(node, key-gpios, 0); // 申请 gpio gpio_request(gpio_num, key); // 设置为输入 gpio_direction_input(gpio_num); // 获取中断号 irq gpio_to_irq(gpio_num); // 注册中断 ← 这步之前漏掉了 ret request_irq(irq, key_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, key, NULL); if (ret) { dev_err(pdev-dev, 注册中断失败\n); return ret; } return 0; } // 3. read 函数 static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off) { // 没有事件则休眠等待中断唤醒 wait_event_interruptible(wq, has_event); // 被唤醒拷贝数据 copy_to_user(buf, key_val, sizeof(key_val)); has_event false; return sizeof(key_val); }用户层// 5. 用户层直接 read没事件会阻塞 while (1) { read(fd, val, sizeof(val)); // 阻塞在这里 printf(按键值%d\n, val); // 有事件才继续 }特点进程阻塞在 read()CPU 可做其他事 实时性好资源占用低 但只能等一个 fd无法同时监听多个三、poll/select 方式原理用户层用 poll/select 同时监听多个 fd 没有事件 → 进程休眠 任意 fd 有事件 → 唤醒进程 用户再去 read 读取数据实现步骤驱动层// 1. 实现 poll 函数 static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait) { unsigned int mask 0; // 2. 把等待队列注册到 poll 机制 poll_wait(filp, wq, wait); // 3. 有事件则返回可读标志 if (has_event) mask | POLLIN | POLLRDNORM; return mask; } // 4. file_operations 注册 poll static struct file_operations key_fops { .poll key_poll, .read key_read, // 同休眠唤醒方式 }; // 5. 中断里唤醒同休眠唤醒方式 static irqreturn_t key_handler(int irq, void *dev_id) { key_val gpio_get_value(gpio_num); has_event true; wake_up_interruptible(wq); return IRQ_HANDLED; }用户层// 6. 用户层用 poll 监听 struct pollfd fds[1]; fds[0].fd fd; fds[0].events POLLIN; while (1) { // 超时 2000ms ret poll(fds, 1, 2000); if (ret 0) { printf(超时无按键\n); } else if (ret 0) { if (fds[0].revents POLLIN) { read(fd, val, sizeof(val)); printf(按键值%d\n, val); } } }特点可以同时监听多个 fd按键、串口、网络... 有超时机制不会永久阻塞 是多路复用 IO 的标准做法四、异步通知方式原理驱动主动给进程发信号SIGIO 进程注册信号处理函数 按键按下 → 驱动发信号 → 进程处理函数被调用 进程不需要主动等待完全异步实现步骤驱动层// 1. 定义异步队列 static struct fasync_struct *async_queue; // 2. 实现 fasync 函数 static int key_fasync(int fd, struct file *filp, int on) { // 注册/注销异步通知 return fasync_helper(fd, filp, on, async_queue); } // 3. 中断里发送信号 static irqreturn_t key_handler(int irq, void *dev_id) { key_val gpio_get_value(gpio_num); has_event true; // 发送 SIGIO 信号给用户进程 if (async_queue) kill_fasync(async_queue, SIGIO, POLL_IN); return IRQ_HANDLED; } // 4. release 里清理 static int key_release(struct inode *inode, struct file *filp) { key_fasync(-1, filp, 0); // 删除异步队列 return 0; } // 5. file_operations 注册 static struct file_operations key_fops { .fasync key_fasync, .read key_read, .release key_release, };用户层// 6. 注册信号处理函数 static void sig_handler(int sig) { int val; read(fd, val, sizeof(val)); printf(异步通知按键值%d\n, val); } int main() { fd open(/dev/key, O_RDWR); // 7. 注册 SIGIO 信号处理函数 signal(SIGIO, sig_handler); // 8. 设置进程为 fd 的属主 fcntl(fd, F_SETOWN, getpid()); // 9. 使能异步通知 int flags fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | FASYNC); // 10. 主循环做其他事按键靠信号通知 while (1) { printf(进程做其他事情...\n); sleep(1); } }---------------------------------------------------------------------------------------------------------------------------------**************************************************************************************************************---------------------------------------------------------------------------------------------------------------------------------gpio是不用在设备树中指定中断号。因为GPIO 控制器本身就是一个“二级中断控制器”。在硬件上一个 GPIO Bank比如 GPIO1通常有 32 个引脚。这 32 个引脚的内部中断信号在芯片内部会被汇聚级联到一两根总中断线上然后再连到 GIC或 GPC。在设备树中芯片厂商已经在imx6ull.dtsi中把 GPIO1 连接到 GIC 的那两根总中断号写好了。作为一个普通外设驱动你只需要告诉内核“我用的是 GPIO1 控制器的第 18 号引脚”。至于这个引脚对应系统底层的哪个虚拟中断号Linux 内核的GPIOLIB 框架和irq_domain 机制会在系统启动时自动动态计算并建立映射。正确做法指定 GPIO 控制器与引脚号只需要将父节点指向对应的gpioX然后填写引脚索引Pin IndexDTS中my_interrupt_device { compatible custom,plat-irq-demo; pinctrl-names default; pinctrl-0 pinctrl_instance; /* 显式声明中断父控制器和对应的引脚第18号引脚下降沿触发 */ interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; };在驱动程序中你只需要通过以下两行代码就能直接在代码里把这个 GPIO 转换成可用的虚拟中断号#include linux/module.h #include linux/platform_device.h #include linux/interrupt.h #include linux/of_irq.h static irqreturn_t my_plat_irq_handler(int irq, void *dev_id) { /* 中断处理上半部逻辑 */ pr_info(GPIO Interrupt triggered!\n); return IRQ_HANDLED; } static int my_plat_probe(struct platform_device *pdev) { int irq; int ret; /* * 1. 直接获取虚拟中断号 * 参数 0 表示获取该设备节点下的第 0 个中断资源对应的 interrupts 属性第一组值 * 内核在此处会自动完成底层 GPIO 到虚拟 IRQ 号的映射 */ irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(pdev-dev, Failed to get IRQ from platform device: %d\n, irq); return irq; } dev_info(pdev-dev, Successfully mapped to Virtual IRQ: %d\n, irq); /* 2. 注册中断服务程序 (ISR) */ /* 注意这里的触发标志通常填 0因为内核会默认采用设备树DTS中指定的 IRQ_TYPE_EDGE_FALLING */ ret devm_request_irq(pdev-dev, irq, my_plat_irq_handler, 0, plat_irq_demo, NULL); if (ret) { dev_err(pdev-dev, Failed to request irq %d\n, irq); return ret; } return 0; } static int my_plat_remove(struct platform_device *pdev) { /* 使用了 devm_ 自动释放机制此处无需手动 free_irq */ return 0; } static const struct of_device_id my_plat_of_match[] { { .compatible custom,plat-irq-demo }, { /* 哨兵 */ } }; MODULE_DEVICE_TABLE(of, my_plat_of_match); static struct platform_driver my_plat_driver { .probe my_plat_probe, .remove my_plat_remove, .driver { .name plat_irq_demo, .of_match_table my_plat_of_match, }, }; module_platform_driver(my_plat_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Driver Developer);