Linux 驱动实战:SR501 人体红外传感器驱动开发与调试全记录
Linux 驱动实战:SR501 人体红外传感器驱动开发与调试全记录
摘要:本文详细记录了在 IMX6ULL 开发板上,将一份 GPIO 按键输入驱动改造成 SR501 人体红外传感器驱动的完整过程。内容涵盖硬件连接、驱动代码逐行讲解、关键函数剖析、中断配置、常见错误排查(如 GPIO 占用、模块版本不匹配、设备节点未创建等)以及最终成功运行的测试结果。文中提供了完整可编译的驱动代码与测试程序,并配有流程图和知识巩固问答,适合嵌入式 Linux 驱动初学者参考。
目录
1. 项目背景与目标
在嵌入式 Linux 系统中,GPIO 是最基础的外设之一。SR501 是一款常用的人体红外传感器,当检测到人体移动时,其 OUT 引脚会输出高电平,无人时恢复低电平。本项目旨在编写一个字符设备驱动,实现以下功能:
- 将 SR501 的 OUT 引脚连接到 IMX6ULL 的 GPIO4_19(编号 115)。
- 驱动能够捕获引脚电平变化(上升沿和下降沿),并通过中断方式通知应用程序。
- 提供标准的文件操作接口:
read、poll、fasync。 - 自动创建设备节点
/dev/sr501。 - 编写测试程序,实时打印传感器状态(有人/无人)。
通过本项目,你将掌握:
- GPIO 的申请、方向设置、电平读取
- GPIO 中断的注册与处理
- 环形缓冲区、等待队列、异步通知等内核机制
- 字符设备驱动的完整框架
- 常见编译、加载错误的排查方法
2. 硬件连接与 GPIO 编号计算

根据开发板接口图,SR501 与 IMX6ULL 的连接如下:
| SR501 引脚 | 开发板引脚 | 说明 |
|---|---|---|
| VCC | 5V | 供电正极 |
| GND | GND | 供电负极 |
| OUT | GPIO4_19 | 信号输出 |
GPIO 编号计算公式:
对于 i.MX6ULL,GPIO 编号 = 组号 × 32 + 组内偏移。
- GPIO4 对应组号 3(因为从 0 开始计数:GPIO1=0,GPIO2=1,GPIO3=2,GPIO4=3)
- GPIO4_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.h:GPIO API(申请、设置方向、读写)interrupt.h、irq.h:中断注册wait.h、poll.h:等待队列与 poll 机制uaccess.h:copy_to_user/copy_from_user
自定义结构体:
c
struct 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) ((x+1) % 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/fasync)
read 函数
c
static 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 函数
c
static 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 函数
c
static 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 中断处理函数
c
static 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 模块初始化函数
c
static 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_create+device_create:创建类与设备节点,触发 udev/mdev 自动创建设备文件。
3.6 模块退出函数
c
static 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,打印对应数据。
输出示例:
text
get button: 0x100 // 有人,高电平
get button: 0x0 // 无人,低电平
5. 编译与加载流程
5.1 Makefile 编写
makefile
KERNEL_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_test
obj-m:指定内核模块目标。make modules:在内核源码树下编译外部模块。- 测试程序使用交叉工具链编译。
5.2 编译驱动与测试程序
在驱动源码目录执行:
bash
make clean
make
生成 gpio_drv.ko 和 button_test。
5.3 加载模块与设备节点创建
将文件传输到开发板后:
bash
insmod gpio_drv.ko # 加载模块
dmesg | tail -20 # 查看初始化日志
ls /dev/sr501 # 检查设备节点
./button_test /dev/sr501 # 运行测试
如果节点未自动创建,手动创建:
bash
cat /proc/devices | grep gpio # 获取主设备号,如 248
mknod /dev/sr501 c 248 0
6. 实战中遇到的错误及解决方案
6.1 模块版本不匹配
错误信息:
text
led_drv: disagrees about version of symbol module_layout
原因:编译模块的内核源码版本与开发板运行的内核版本不一致。
解决:确保 KERNEL_DIR 指向与开发板内核完全匹配的源码树,重新编译。
6.2 GPIO 被配置为输出无法申请中断
错误信息:
text
gpiochip_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 类目录重复导致加载失败
错误信息:
text
sysfs: 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 道简答题及答案
Q1:gpio_request 和 gpio_direction_input 的作用分别是什么?
A1: gpio_request 向内核申请独占使用某个 GPIO,防止冲突;gpio_direction_input 将 GPIO 设置为输入模式,只有输入模式才能正确读取外部电平并产生中断。
Q2:为什么中断处理函数中要调用 wake_up_interruptible 和 kill_fasync?
A2: wake_up_interruptible 唤醒因 wait_event_interruptible 睡眠的进程,使其能继续执行 read 读取数据;kill_fasync 向设置了异步通知的进程发送 SIGIO 信号,告知数据可读。
Q3:环形缓冲区的作用是什么?
A3: 当传感器事件产生速度快于应用程序读取速度时,环形缓冲区可以暂存多个事件,避免数据丢失。它通过读写指针实现先进先出。
Q4:register_chrdev 第一个参数传 0 的含义是什么?
A4: 表示由内核动态分配一个未使用的主设备号,返回值即为主设备号。
Q5:设备节点 /dev/sr501 是如何自动创建的?
A5: 驱动中调用 class_create 创建类,再调用 device_create 创建设备并发送 uevent 事件;用户空间的 mdev 或 udev 监听该事件,自动在 /dev 下创建节点。
Q6:中断触发方式 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING 表示什么?
A6: 表示上升沿和下降沿都会触发中断。对于 SR501,有人时输出上升沿,无人时输出下降沿,使用双边沿触发可以捕获所有状态变化。
Q7:copy_to_user 和 copy_from_user 为什么不能省略?
A7: 用户空间和内核空间内存隔离,直接访问用户指针可能导致内核崩溃。这两个函数会进行地址合法性检查,确保数据安全传输。
Q8:驱动退出时释放资源的顺序为什么必须与初始化相反?
A8: 因为资源间存在依赖关系。例如,先销毁设备节点再销毁类,否则可能出现空指针;先释放中断再释放 GPIO,防止正在处理的中断访问已释放的 GPIO。
Q9:测试程序中 read(fd, &val, 4) 返回 4 表示什么?
A9: 表示成功读取了 4 字节数据,即驱动打包的 key 值。若返回值小于 4,则可能是被信号中断或发生错误。
Q10:SR501 上电后为什么需要等待一段时间才能稳定输出?
A10: 传感器内部电路需要预热和自校准,通常需要 30~60 秒才能进入稳定工作状态。在此期间输出可能不稳定或不响应。
9. 总结与展望
通过本次项目,我们成功将一份复杂的按键驱动精简并改造成了适配 SR501 传感器的输入驱动,完整实现了 GPIO 中断、环形缓冲、等待队列、异步通知等内核机制。在调试过程中,我们排查了模块版本不匹配、GPIO 方向错误、类目录残留、传感器预热等实际问题,积累了宝贵的实战经验。
最终成果:
- 驱动代码稳定运行,中断响应及时。
- 测试程序能准确打印传感器状态。
- 硬件连接正确,传感器触发正常。
后续可扩展方向:
- 使用设备树描述硬件,避免硬编码 GPIO 编号。
- 实现
sysfs属性接口,方便用户空间配置触发边沿、消抖时间等。 - 支持多个传感器同时工作(如 GPIO116、GPIO117 等)。
希望本文能为 Linux 驱动初学者提供清晰的指引,助力大家快速上手嵌入式开发!
更多推荐
所有评论(0)