Linux 驱动实战:SR501 人体红外传感器驱动开发与调试全记录

张开发
2026/4/21 12:56:47 15 分钟阅读

分享文章

Linux 驱动实战:SR501 人体红外传感器驱动开发与调试全记录
Linux 驱动实战SR501 人体红外传感器驱动开发与调试全记录摘要本文详细记录了在 IMX6ULL 开发板上将一份 GPIO 按键输入驱动改造成 SR501 人体红外传感器驱动的完整过程。内容涵盖硬件连接、驱动代码逐行讲解、关键函数剖析、中断配置、常见错误排查如 GPIO 占用、模块版本不匹配、设备节点未创建等以及最终成功运行的测试结果。文中提供了完整可编译的驱动代码与测试程序并配有流程图和知识巩固问答适合嵌入式 Linux 驱动初学者参考。目录文章目录Linux 驱动实战SR501 人体红外传感器驱动开发与调试全记录目录[TOC]1. 项目背景与目标2. 硬件连接与 GPIO 编号计算3. 驱动代码逐行详解3.1 头文件与全局结构3.2 环形缓冲区与等待队列3.3 文件操作函数read/poll/fasyncread 函数poll 函数fasync 函数3.4 中断处理函数3.5 模块初始化函数3.6 模块退出函数4. 测试程序代码及说明5. 编译与加载流程5.1 Makefile 编写5.2 编译驱动与测试程序5.3 加载模块与设备节点创建6. 实战中遇到的错误及解决方案6.1 模块版本不匹配6.2 GPIO 被配置为输出无法申请中断6.3 sysfs 类目录重复导致加载失败6.4 传感器预热与硬件接触问题7. 驱动与应用程序流程图驱动初始化流程中断处理与数据流应用程序执行流程8. 知识巩固10 道简答题及答案9. 总结与展望1. 项目背景与目标在嵌入式 Linux 系统中GPIO 是最基础的外设之一。SR501 是一款常用的人体红外传感器当检测到人体移动时其 OUT 引脚会输出高电平无人时恢复低电平。本项目旨在编写一个字符设备驱动实现以下功能将 SR501 的 OUT 引脚连接到 IMX6ULL 的 GPIO4_19编号 115。驱动能够捕获引脚电平变化上升沿和下降沿并通过中断方式通知应用程序。提供标准的文件操作接口read、poll、fasync。自动创建设备节点/dev/sr501。编写测试程序实时打印传感器状态有人/无人。通过本项目你将掌握GPIO 的申请、方向设置、电平读取GPIO 中断的注册与处理环形缓冲区、等待队列、异步通知等内核机制字符设备驱动的完整框架常见编译、加载错误的排查方法2. 硬件连接与 GPIO 编号计算根据开发板接口图SR501 与 IMX6ULL 的连接如下SR501 引脚开发板引脚说明VCC5V供电正极GNDGND供电负极OUTGPIO4_19信号输出GPIO 编号计算公式对于 i.MX6ULLGPIO 编号 组号 × 32 组内偏移。GPIO4 对应组号 3因为从 0 开始计数GPIO10GPIO21GPIO32GPIO43GPIO4_19 的编号 3 × 32 19 115因此驱动代码中使用 GPIO 编号115。注意该引脚在设备树中默认可能被复用为 CSI 摄像头接口功能需要确保硬件连接无误并在驱动中正确申请 GPIO 资源。3. 驱动代码逐行详解3.1 头文件与全局结构c#include linux/module.h #include linux/fs.h #include linux/errno.h #include linux/kernel.h #include linux/init.h #include linux/device.h #include linux/gpio.h #include linux/interrupt.h #include linux/irq.h #include linux/wait.h #include linux/poll.h #include linux/slab.h #include linux/fcntl.h #include linux/uaccess.h头文件作用module.h内核模块必需fs.h文件操作结构体gpio.hGPIO API申请、设置方向、读写interrupt.h、irq.h中断注册wait.h、poll.h等待队列与 poll 机制uaccess.hcopy_to_user/copy_from_user自定义结构体cstruct gpio_desc { int gpio; // GPIO 编号 int irq; // 中断号 char *name; // 标签名称 int key; // 传感器编号本例未使用 struct timer_list key_timer; // 定时器本例未使用 }; static struct gpio_desc gpios[1] { {115, 0, sr501, }, };虽然结构体包含了key和key_timer字段但这是从按键驱动改造过程中保留的SR501 驱动并未使用它们。保留是为了代码修改的连续性实际可精简。3.2 环形缓冲区与等待队列c#define BUF_LEN 128 static int g_keys[BUF_LEN]; static int r, w; #define NEXT_POS(x) ((x1) % BUF_LEN) static int is_key_buf_empty(void) { return (r w); } static int is_key_buf_full(void) { return (r NEXT_POS(w)); } static void put_key(int key) { if (!is_key_buf_full()) { g_keys[w] key; w NEXT_POS(w); } } static int get_key(void) { int key 0; if (!is_key_buf_empty()) { key g_keys[r]; r NEXT_POS(r); } return key; } static DECLARE_WAIT_QUEUE_HEAD(gpio_wait); struct fasync_struct *button_fasync;环形缓冲区用于缓存传感器事件防止应用程序来不及读取时丢失数据。put_key由中断处理函数调用将打包的事件数据存入缓冲区get_key由read函数调用取出最早的事件。等待队列当缓冲区为空时read调用wait_event_interruptible使进程睡眠当中断产生并放入数据后调用wake_up_interruptible唤醒睡眠进程。异步通知结构体button_fasync用于支持fcntl(fd, F_SETFL, O_ASYNC)当有数据可读时内核向应用进程发送SIGIO信号。3.3 文件操作函数read/poll/fasyncread 函数cstatic ssize_t gpio_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset) { int err; int key; if (is_key_buf_empty() (file-f_flags O_NONBLOCK)) return -EAGAIN; wait_event_interruptible(gpio_wait, !is_key_buf_empty()); key get_key(); err copy_to_user(buf, key, 4); return 4; }逻辑若文件打开时设置了非阻塞标志且缓冲区为空立即返回-EAGAIN。否则调用wait_event_interruptible睡眠直到缓冲区非空。被唤醒后从缓冲区取出一个key值4 字节整数。通过copy_to_user将数据拷贝到用户空间。数据格式key的低 8 位为传感器编号本例中为 0高 8 位为电平值1 表示高电平有人0 表示低电平无人。应用层打印0x%x即可看到类似0x100有人和0x0无人的输出。poll 函数cstatic unsigned int gpio_drv_poll(struct file *fp, poll_table *wait) { poll_wait(fp, gpio_wait, wait); return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM; }支持select/poll多路复用。将等待队列添加到poll_table并返回可读状态掩码。fasync 函数cstatic int gpio_drv_fasync(int fd, struct file *file, int on) { if (fasync_helper(fd, file, on, button_fasync) 0) return 0; else return -EIO; }当应用程序设置异步标志时调用内核负责管理异步通知链表。中断处理函数中通过kill_fasync发送信号。3.4 中断处理函数cstatic irqreturn_t gpio_key_isr(int irq, void *dev_id) { struct gpio_desc *gpio_desc dev_id; int val; int key; printk(gpio_key_isr key %d irq happened\n, gpio_desc-gpio); val gpio_get_value(gpio_desc-gpio); key (gpio_desc-key) | (val 8); put_key(key); wake_up_interruptible(gpio_wait); kill_fasync(button_fasync, SIGIO, POLL_IN); return IRQ_HANDLED; }执行流程从dev_id获取 GPIO 描述结构体。读取当前引脚电平val。打包key值低 8 位为gpio_desc-key此处为 0高 8 位为val。将key存入环形缓冲区。唤醒等待队列上的睡眠进程。向设置了异步通知的进程发送SIGIO信号。为什么在中断中做这么多事因为 SR501 输出的是稳定数字信号无需软件消抖直接在中断里完成数据采集和通知可保证最佳实时性。3.5 模块初始化函数cstatic int __init gpio_drv_init(void) { int err; int i; int count sizeof(gpios) / sizeof(gpios[0]); printk(%s %s line %d\n, __FILE__, __FUNCTION__, __LINE__); for (i 0; i count; i) { // 1. 申请 GPIO err gpio_request(gpios[i].gpio, gpios[i].name); if (err) { printk(KERN_ERR can not request gpio %s %d\n, gpios[i].name, gpios[i].gpio); return -ENODEV; } // 2. 设置为输入 gpio_direction_input(gpios[i].gpio); // 3. 获取中断号 gpios[i].irq gpio_to_irq(gpios[i].gpio); // 4. 注册中断 err request_irq(gpios[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpios[i].name, gpios[i]); if (err) { printk(KERN_ERR request_irq failed for %s\n, gpios[i].name); gpio_free(gpios[i].gpio); return err; } } // 注册字符设备 major register_chrdev(0, 100ask_gpio_key, gpio_key_drv); // 创建类 gpio_class class_create(THIS_MODULE, 100ask_gpio_key_class); if (IS_ERR(gpio_class)) { unregister_chrdev(major, 100ask_gpio_key); return PTR_ERR(gpio_class); } // 创建设备节点 device_create(gpio_class, NULL, MKDEV(major, 0), NULL, sr501); return 0; }关键步骤说明gpio_request向内核申请 GPIO 资源防止冲突。gpio_direction_input将引脚配置为输入模式必须否则无法作为中断源。gpio_to_irq将 GPIO 编号转换为中断号。request_irq注册中断处理函数触发方式为双边沿上升沿和下降沿都触发。register_chrdev注册字符设备动态分配主设备号。class_createdevice_create创建类与设备节点触发 udev/mdev 自动创建设备文件。3.6 模块退出函数cstatic void __exit gpio_drv_exit(void) { int i; int count sizeof(gpios) / sizeof(gpios[0]); device_destroy(gpio_class, MKDEV(major, 0)); class_destroy(gpio_class); unregister_chrdev(major, 100ask_gpio_key); for (i 0; i count; i) { free_irq(gpios[i].irq, gpios[i]); gpio_free(gpios[i].gpio); } }逆序释放资源先销毁设备节点再销毁类然后注销字符设备最后释放中断和 GPIO 资源。4. 测试程序代码及说明c#include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h #include string.h static int fd; int main(int argc, char **argv) { int val; int i; if (argc ! 2) { printf(Usage: %s dev\n, argv[0]); return -1; } fd open(argv[1], O_RDWR); if (fd -1) { printf(can not open file %s\n, argv[1]); return -1; } while (1) { if (read(fd, val, 4) 4) printf(get button: 0x%x\n, val); else printf(get button: -1\n); } close(fd); return 0; }程序逻辑接收设备路径参数如/dev/sr501。以阻塞方式打开设备文件。循环调用read读取 4 字节数据打印十六进制值。当传感器状态变化时驱动唤醒read打印对应数据。输出示例textget button: 0x100 // 有人高电平 get button: 0x0 // 无人低电平5. 编译与加载流程5.1 Makefile 编写makefileKERNEL_DIR : /home/book/100ask_imx6ull-sdk/Linux-4.9.88 CURRENT_DIR : $(shell pwd) obj-m : gpio_drv.o all: $(MAKE) -C $(KERNEL_DIR) M$(CURRENT_DIR) modules arm-buildroot-linux-gnueabihf-gcc -o button_test button_test.c clean: $(MAKE) -C $(KERNEL_DIR) M$(CURRENT_DIR) clean rm -rf modules.order button_testobj-m指定内核模块目标。make modules在内核源码树下编译外部模块。测试程序使用交叉工具链编译。5.2 编译驱动与测试程序在驱动源码目录执行bashmake clean make生成gpio_drv.ko和button_test。5.3 加载模块与设备节点创建将文件传输到开发板后bashinsmod gpio_drv.ko # 加载模块 dmesg | tail -20 # 查看初始化日志 ls /dev/sr501 # 检查设备节点 ./button_test /dev/sr501 # 运行测试如果节点未自动创建手动创建bashcat /proc/devices | grep gpio # 获取主设备号如 248 mknod /dev/sr501 c 248 06. 实战中遇到的错误及解决方案6.1 模块版本不匹配错误信息textled_drv: disagrees about version of symbol module_layout原因编译模块的内核源码版本与开发板运行的内核版本不一致。解决确保KERNEL_DIR指向与开发板内核完全匹配的源码树重新编译。6.2 GPIO 被配置为输出无法申请中断错误信息textgpiochip_lock_as_irq: tried to flag a GPIO set as output for IRQ genirq: Failed to request resources for sr501原因驱动在request_irq之前没有调用gpio_direction_input将引脚设为输入模式或者该引脚在设备树中被复用为其他功能如 CSI导致 GPIO 子系统无法将其用于中断。解决在gpio_request后立即调用gpio_direction_input。如果问题依旧换一个空闲 GPIO如 GPIO116测试确认驱动代码正确后再研究设备树修改。6.3 sysfs 类目录重复导致加载失败错误信息textsysfs: cannot create duplicate filename /class/100ask_gpio_key_class原因之前加载驱动时创建了类目录但因中途失败未能正确销毁再次加载时重复创建导致冲突。解决重启开发板sysfs 是内存文件系统重启后清空。或手动删除残留目录rm -rf /sys/class/100ask_gpio_key_class需要 root 权限。6.4 传感器预热与硬件接触问题现象驱动加载成功中断注册成功但测试程序无输出中断计数为 0。排查过程通过 sysfs 读取 GPIO 电平cat /sys/class/gpio/gpio115/value发现电平会变化说明传感器工作正常。用杜邦线手动触碰 3.3V 和 GND测试程序立即有输出证明中断功能正常。重新插拔传感器接线等待一段时间SR501 需预热再次测试成功。结论SR501 上电后有初始化时间约 30~60 秒期间输出不稳定同时杜邦线可能存在接触不良重新插拔后恢复正常。7. 驱动与应用程序流程图驱动初始化流程text模块加载 │ ▼ gpio_drv_init() │ ├─► gpio_request() 申请 GPIO ├─► gpio_direction_input() 配置输入 ├─► gpio_to_irq() 获取中断号 ├─► request_irq() 注册中断 │ ├─► register_chrdev() 注册字符设备 ├─► class_create() 创建类 └─► device_create() 创建设备节点 /dev/sr501中断处理与数据流text硬件电平跳变 │ ▼ gpio_key_isr() │ ├─► gpio_get_value() 读取电平 ├─► 打包 key 值 ├─► put_key() 存入环形缓冲区 ├─► wake_up_interruptible() 唤醒睡眠进程 └─► kill_fasync() 发送异步信号应用程序执行流程text开始 │ ├─► open(/dev/sr501, O_RDWR) │ └─► while (1) │ ├─► read(fd, val, 4) 阻塞等待数据 │ └─► printf(get button: 0x%x\n, val)8. 知识巩固10 道简答题及答案Q1gpio_request和gpio_direction_input的作用分别是什么A1gpio_request向内核申请独占使用某个 GPIO防止冲突gpio_direction_input将 GPIO 设置为输入模式只有输入模式才能正确读取外部电平并产生中断。Q2为什么中断处理函数中要调用wake_up_interruptible和kill_fasyncA2wake_up_interruptible唤醒因wait_event_interruptible睡眠的进程使其能继续执行read读取数据kill_fasync向设置了异步通知的进程发送SIGIO信号告知数据可读。Q3环形缓冲区的作用是什么A3当传感器事件产生速度快于应用程序读取速度时环形缓冲区可以暂存多个事件避免数据丢失。它通过读写指针实现先进先出。Q4register_chrdev第一个参数传 0 的含义是什么A4表示由内核动态分配一个未使用的主设备号返回值即为主设备号。Q5设备节点/dev/sr501是如何自动创建的A5驱动中调用class_create创建类再调用device_create创建设备并发送 uevent 事件用户空间的mdev或udev监听该事件自动在/dev下创建节点。Q6中断触发方式IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING表示什么A6表示上升沿和下降沿都会触发中断。对于 SR501有人时输出上升沿无人时输出下降沿使用双边沿触发可以捕获所有状态变化。Q7copy_to_user和copy_from_user为什么不能省略A7用户空间和内核空间内存隔离直接访问用户指针可能导致内核崩溃。这两个函数会进行地址合法性检查确保数据安全传输。Q8驱动退出时释放资源的顺序为什么必须与初始化相反A8因为资源间存在依赖关系。例如先销毁设备节点再销毁类否则可能出现空指针先释放中断再释放 GPIO防止正在处理的中断访问已释放的 GPIO。Q9测试程序中read(fd, val, 4)返回 4 表示什么A9表示成功读取了 4 字节数据即驱动打包的key值。若返回值小于 4则可能是被信号中断或发生错误。Q10SR501 上电后为什么需要等待一段时间才能稳定输出A10传感器内部电路需要预热和自校准通常需要 30~60 秒才能进入稳定工作状态。在此期间输出可能不稳定或不响应。9. 总结与展望通过本次项目我们成功将一份复杂的按键驱动精简并改造成了适配 SR501 传感器的输入驱动完整实现了 GPIO 中断、环形缓冲、等待队列、异步通知等内核机制。在调试过程中我们排查了模块版本不匹配、GPIO 方向错误、类目录残留、传感器预热等实际问题积累了宝贵的实战经验。最终成果驱动代码稳定运行中断响应及时。测试程序能准确打印传感器状态。硬件连接正确传感器触发正常。后续可扩展方向使用设备树描述硬件避免硬编码 GPIO 编号。实现sysfs属性接口方便用户空间配置触发边沿、消抖时间等。支持多个传感器同时工作如 GPIO116、GPIO117 等。希望本文能为 Linux 驱动初学者提供清晰的指引助力大家快速上手嵌入式开发

更多文章