4

【Linux 中断】红外接收器设备驱动 - 浇筑菜鸟

 1 year ago
source link: https://www.cnblogs.com/jzcn/p/17197007.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

现在很多家电都使用了红外,而智能家居的诞生,连音响都带了红外遥控功能。为了解决家里遥控器比较多的情况,多数手机都支持了红外功能,这和以前的万能遥控器一样。这里主要记录红外接收的驱动,当然明白怎么接收的,对于遥控的发射就比较简单了。

二、红外接收器

  1. 外观

    2406897-20230309084918870-487061474.png
  2. 接收的工作原理
    红外探头应该也是光敏电阻的一种,当接收到波长在750-1150NM的光时,OUT 引脚就会产生一个 38kHz 的 PWM 波。一般在电路中都会给 OUT 引脚进行一个上拉,所以没有检测到红外光时,OUT 引脚是稳定的高电平。通过这个现象我们就可以进行无线通信。
    注意:750-1150NM的光时是肉眼不可见的,不过可以通过手机摄像头进行查看

  3. 通信协议
    了解完原理后,只需要配上相应的通信协议就可以使用红外进行无线通信了。常用的红外线信号传输协议有ITT协议、NEC协议、NokiaNRC协议、Sharp协议、SonySIRC协议、PhilipSRC-5协议、PhilipsRC-6协议,以及PhilipsRECS-80协议等。

    需要了解不同协议区别的可以参考:几种常用的红外线信号传输协议,红外的协议种类比较多,部分公司也会自己指定不同的协议,比如小米公司的遥控器,见小米红外遥控器如何适配到其他应用设备之上

    此笔记主要使用 NEC 协议完成驱动的编写,其他的协议驱动也可以参考完成。

三、 NEC协议

  1. 数据帧格式

    引导码 地址码0 地址码1 命令码 命令反码 引导码(重复)
    LSB-MSB(0-7) LSB-MSB(8-15) LSB-MSB(16-23) LSB-MSB(24-31)

    注意:在标准的NEC协议中,地址码1为地址码0的反码,而在许多遥控器中,地址码0和地址码1共同作为红外遥控器的编码值。

  2. PPM(脉冲位置调制)
    2406897-20230309093043009-443944366.png

  3. 接收波形
    2406897-20230309093245853-499060339.png
    注意:实际波形在低电平期间是一个 38kHz 的 PWM 波。

  4. 数据解析
    在接收数据时需要过滤 38kHz 的波形,如下所示:

    /** * @brief 红外中断响应函数 * * @param irq * @param dev_id * @return 0,成功;其他负值,失败*/static irqreturn_t infrared_interrupt(int irq, void *dev_id){ unsigned previous_offset; // 上一次的时间 unsigned start_offset; // 波型的起始时间差 long long now = ktime_to_us(ktime_get()); /* 由于红外接收传感器在接收到红外信号时产生一个38KHz的信号,所以接收时需要过滤,使信号变为一个低电平信号 */ /*-------------------------------- 滤波 --------------------------------*/ /* 从当前时刻开始接收一个下降沿开始的方波周期 */ if (0 == infrared_pwm.flag ) { infrared_pwm.start_time = now; infrared_pwm.flag = 1; } /* 计算两次下降沿的时差 */ previous_offset = now - infrared_pwm.previous; infrared_pwm.previous = now; /* 过滤红外接收器自生产生38KHz的信号,周期大约是 26us */ if (previous_offset < 60) { return IRQ_HANDLED; } /* 下降沿开始的时差,也就是一个周期的时间 */ start_offset = now - infrared_pwm.start_time; /* 消除上次持续的信号 */ if (start_offset == 0) { return IRQ_HANDLED; } /* 完成一个周期的数据采集 */ infrared_pwm.flag = 0; // infrared_pwm.low_time = start_offset - previous_offset + 52; // 低电平时间 // infrared_pwm.high_time = previous_offset - 52; // 高电平时间 /* NEC 解码 */ infrared_nec_decode(start_offset); return IRQ_HANDLED;}

四、linux 中断驱动

在中断驱动中我使用了异步通知的方式,与应用程序进行通信

/** * @brief 红外接收器初始化函数 * * @return 0,成功;其他负值,失败*/static int infrared_init(void){ int res; /* 申请 GPIO 资源 */ infrared_dev.gpio = INFRARED_GPIO; res = gpio_request(infrared_dev.gpio, "infrared"); if (res) { pr_err("infrared dev: Failed to request gpio\n"); return res; } /* 将 GPIO 设置为输入模式 */ gpio_direction_input(infrared_dev.gpio); /* 申请中断 */ infrared_dev.irq_num = gpio_to_irq(infrared_dev.gpio); res = request_irq(infrared_dev.irq_num, infrared_interrupt, IRQF_TRIGGER_FALLING, "infrared", NULL); if (res) { gpio_free(infrared_dev.gpio); return res; } return 0;} /** * @brief 打开设备 * * @param inode 传递给驱动的 inode * @param filp 设备文件,file 结构体有个叫做 private_data 的成员变量 * 一般在 open 的时候将 private_data 指向设备结构体。 * @return 0 成功;其他 失败 */static int infrared_open(struct inode *inode, struct file *filp){ /* 将设备数据设置为私有数据 */ filp->private_data = &infrared_dev; printk(PRINTK_GRADE "infrared_open\n"); return 0;} /** * @brief 从设备读取数据 * * @param filp 要打开的设备文件(文件描述符) * @param buf 返回给用户空间的数据缓冲区 * @param count 要读取的数据长度 * @param offt 相对于文件首地址的偏移 * @return 0 成功;其他 失败 */static ssize_t infrared_read(struct file *filp, char __user *buf, size_t count, loff_t *offt){ int res = 0; // struct infrared_dev_t *infrared_dev = filp->private_data; res = copy_to_user(buf, infrared_receive_data, count); if(res != 0) { printk(PRINTK_GRADE "111111111111111\n"); return -1; } // printk(PRINTK_GRADE "infrared_read\n"); return 0;} /** * @brief 向设备写数据 * @param filp 设备文件,表示打开的文件描述符 * @param buf 要写给设备写入的数据 * @param count 要写入的数据长度 * @param offt 相对于文件首地址的偏移 * @return 写入的字节数,如果为负值,表示写入失败*/static ssize_t infrared_write(struct file *filp, const char __user *buf, size_t count, loff_t *offt){ int res = 0; // struct infrared_dev_t *infrared_dev = filp->private_data; char write_buf[1024] = {"0"}; res = copy_from_user(write_buf, buf, count); if(res != 0) { return -1; } printk("kernel recevdata:%s\r\n", write_buf); return 0;} static int infrared_fasync(int fd, struct file *filp, int on){ struct infrared_dev_t *infrared_dev = filp->private_data; printk(PRINTK_GRADE "infrared_fasync\n"); /* 异步通知初始化 */ return fasync_helper(fd, filp, on, &infrared_dev->fasync_queue);} /** * @brief 关闭/释放设备 * @param filp 要关闭的设备文件(文件描述符) * @return 0 成功;其他 失败*/static int infrared_release(struct inode *inode, struct file *filp){ int res = 0; printk(PRINTK_GRADE "infrared_release\n"); /* 删除异步通知 */ infrared_fasync(-1, filp, 0); return res;} /* 设备操作函数结构体 */static struct file_operations infrared_ops = { .owner = THIS_MODULE, .open = infrared_open, .read = infrared_read, .write = infrared_write, .release = infrared_release, .fasync = infrared_fasync,}; /** * @brief 注册字符设备驱动 * * @return 0,成功;其他负值,失败*/static int infrared_register(void){ int ret = -1; // 保存错误状态码 /* GPIO 中断初始化 */ ret = infrared_init(); /* 1、创建设备号 */ /* 采用动态分配的方式,获取设备编号,次设备号为0 */ /* 设备名称为 infrared_NAME,可通过命令 cat /proc/devices 查看 */ /* INFRARED_CNT 为1,只申请一个设备编号 */ ret = alloc_chrdev_region(&infrared_dev.devid, 0, INFRARED_CNT, INFRARED_NAME); if (ret < 0) { pr_err("%s Couldn't alloc_chrdev_region, ret = %d \r\n", INFRARED_NAME, ret); goto fail_region; } /* 2、初始化 cdev */ /* 关联字符设备结构体 cdev 与文件操作结构体 file_operations */ infrared_dev.cdev.owner = THIS_MODULE; cdev_init(&infrared_dev.cdev, &infrared_ops); /* 3、添加一个 cdev */ /* 添加设备至cdev_map散列表中 */ ret = cdev_add(&infrared_dev.cdev, infrared_dev.devid, INFRARED_CNT); if (ret < 0) { pr_err("fail to add cdev \r\n"); goto del_unregister; } /* 4、创建类 */ infrared_dev.class = class_create(THIS_MODULE, INFRARED_NAME); if (IS_ERR(infrared_dev.class)) { pr_err("Failed to create device class \r\n"); goto del_cdev; } /* 5、创建设备,设备名是 INFRARED_NAME */ /*创建设备 INFRARED_NAME 指定设备名,*/ infrared_dev.device = device_create(infrared_dev.class, NULL, infrared_dev.devid, NULL, INFRARED_NAME); if (IS_ERR(infrared_dev.device)) { goto destroy_class; } return 0; destroy_class: device_destroy(infrared_dev.class, infrared_dev.devid);del_cdev: cdev_del(&infrared_dev.cdev);del_unregister: unregister_chrdev_region(infrared_dev.devid, INFRARED_CNT);fail_region: /* 释放个人初始化申请的资源,如del_init(); */ free_irq(infrared_dev.irq_num, NULL); gpio_free(infrared_dev.gpio); return -EIO;} /** * @brief 注销字符设备驱动 * * @return 0,成功;其他负值,失败*/static void infrared_unregister(void){ /* 1、删除 cdev */ cdev_del(&infrared_dev.cdev); /* 2、注销设备号 */ unregister_chrdev_region(infrared_dev.devid, INFRARED_CNT); /* 3、注销设备 */ device_destroy(infrared_dev.class, infrared_dev.devid); /* 4、注销类 */ class_destroy(infrared_dev.class); /* 释放中断 */ free_irq(infrared_dev.irq_num, NULL); /* 释放 IO */ gpio_free(infrared_dev.gpio);} /** * @brief 驱动入口函数 * * @return 0,成功;其他负值,失败*/static int __init infrared_driver_init(void){ pr_info("infrared_driver_init\n"); return infrared_register();} /** * @brief 驱动出口函数 * * @return 0,成功;其他负值,失败*/static void __exit infrared_driver_exit(void){ pr_info("infrared_driver_exit\n"); infrared_unregister();} /* 将上面两个函数指定为驱动的入口和出口函数 */module_init(infrared_driver_init);module_exit(infrared_driver_exit); /* LICENSE 和作者信息 */MODULE_LICENSE("GPL");MODULE_AUTHOR("JIAOZHU");MODULE_INFO(intree, "Y");

五、完整的驱动程序

#include <linux/init.h>#include <linux/module.h>#include <linux/fs.h>#include <linux/cdev.h>#include <linux/uaccess.h>#include <linux/types.h>#include <linux/kernel.h>#include <linux/delay.h>#include <linux/ide.h>#include <linux/errno.h>#include <linux/gpio.h>#include <asm/mach/map.h>#include <linux/of.h>#include <linux/of_address.h>#include <linux/of_gpio.h>#include <asm/io.h>#include <linux/device.h>#include <linux/semaphore.h>#include <linux/of_irq.h>#include <linux/irq.h> /***************************************************************文件名 : infrared.c作者 : jiaozhu版本 : V1.0描述 : 红外接收器驱动其他 : 无日志 : 初版 V1.0 2023/3/3***************************************************************/ /* 红外接收器的数据引脚 59 */#define INFRARED_GPIO 59 #define PRINTK_GRADE KERN_INFO /*------------------ 字符设备内容 ----------------------*/#define INFRARED_NAME "infrared"#define INFRARED_CNT (1) static unsigned char infrared_receive_data[4]; /*------------------ 设备数据结构体 ----------------------*/struct infrared_dev_t { dev_t devid; // 设备号 struct cdev cdev; // cdev struct class *class; // 类 struct device *device; // 设备 struct device_node *nd; // 设备节点 int irq_num; // 中断号 int gpio; // 数据接收引脚 struct fasync_struct *fasync_queue; // 异步相关结构体}; struct infrared_dev_t infrared_dev; // 设备数据结构体 /*------------------ 红外波形过滤结构体 ----------------------*/struct infrared_pwm_t{ long long previous; // 记录上一次的时间,64bit int flag; // 表示每个方波周期的开始 long long start_time; // 周期的起始时间 int low_time; // 低电平时间 int high_time; // 高电平时间}; struct infrared_pwm_t infrared_pwm = // 红外波形采集{ .flag = 0, .previous = 0, .start_time = 0, .low_time = 0, .high_time = 0,}; /*------------------ 红外 NEC 数据解析结构体 ------------------*/struct nec_decode_buf_t{ int flag; // 表示 NEC 数据开始 unsigned times[128]; // 记录每帧的时间 int num; // 表示第几帧}; struct nec_decode_buf_t nec_buf ={ .flag = 0, .num = 0,}; /** * @brief 红外 NEC 数据解析 * * @param period 一个方波周期*/static void infrared_nec_decode(int period){ int i, j; unsigned char temp; if ((period > 13000) && (period < 14000)) { nec_buf.flag = 1; nec_buf.num = 0; return; } if (nec_buf.num < 32) { nec_buf.times[nec_buf.num ++] = period; } if ((period > 10500) && (period < 13500)) { if (nec_buf.flag) { for(i = 0; i < 4; i++) // 一共4个字节 { temp = 0; for(j = 0; j < 8; j++) { if ((nec_buf.times[i * 8 + j] > 2100) && (nec_buf.times[i * 8 + j] < 2400) ) { temp |= 1 << j; } } // printk("%02x ", temp); infrared_receive_data[i] = temp; } // printk("\n"); nec_buf.flag = 0; } else { // printk(PRINTK_GRADE "Repetitive signal\n"); memset(infrared_receive_data, 0xFF, sizeof(infrared_receive_data)); } /* 发送异步通知 */ kill_fasync(&infrared_dev.fasync_queue, SIGIO, POLL_IN); } } /** * @brief 红外中断响应函数 * * @param irq * @param dev_id * @return 0,成功;其他负值,失败*/static irqreturn_t infrared_interrupt(int irq, void *dev_id){ unsigned previous_offset; // 上一次的时间 unsigned start_offset; // 波型的起始时间差 long long now = ktime_to_us(ktime_get()); /* 由于红外接收传感器在接收到红外信号时产生一个38KHz的信号,所以接收时需要过滤,使信号变为一个低电平信号 */ /*-------------------------------- 滤波 --------------------------------*/ /* 从当前时刻开始接收一个下降沿开始的方波周期 */ if (0 == infrared_pwm.flag ) { infrared_pwm.start_time = now; infrared_pwm.flag = 1; } /* 计算两次下降沿的时差 */ previous_offset = now - infrared_pwm.previous; infrared_pwm.previous = now; /* 过滤红外接收器自生产生38KHz的信号,周期大约是 26us */ if (previous_offset < 60) { return IRQ_HANDLED; } /* 下降沿开始的时差,也就是一个周期的时间 */ start_offset = now - infrared_pwm.start_time; /* 消除上次持续的信号 */ if (start_offset == 0) { return IRQ_HANDLED; } /* 完成一个周期的数据采集 */ infrared_pwm.flag = 0; // infrared_pwm.low_time = start_offset - previous_offset + 52; // 低电平时间 // infrared_pwm.high_time = previous_offset - 52; // 高电平时间 /* NEC 解码 */ infrared_nec_decode(start_offset); return IRQ_HANDLED;} /** * @brief 红外接收器初始化函数 * * @return 0,成功;其他负值,失败*/static int infrared_init(void){ int res; /* 申请 GPIO 资源 */ infrared_dev.gpio = INFRARED_GPIO; res = gpio_request(infrared_dev.gpio, "infrared"); if (res) { pr_err("infrared dev: Failed to request gpio\n"); return res; } /* 将 GPIO 设置为输入模式 */ gpio_direction_input(infrared_dev.gpio); /* 申请中断 */ infrared_dev.irq_num = gpio_to_irq(infrared_dev.gpio); res = request_irq(infrared_dev.irq_num, infrared_interrupt, IRQF_TRIGGER_FALLING, "infrared", NULL); if (res) { gpio_free(infrared_dev.gpio); return res; } return 0;} /** * @brief 打开设备 * * @param inode 传递给驱动的 inode * @param filp 设备文件,file 结构体有个叫做 private_data 的成员变量 * 一般在 open 的时候将 private_data 指向设备结构体。 * @return 0 成功;其他 失败 */static int infrared_open(struct inode *inode, struct file *filp){ /* 将设备数据设置为私有数据 */ filp->private_data = &infrared_dev; printk(PRINTK_GRADE "infrared_open\n"); return 0;} /** * @brief 从设备读取数据 * * @param filp 要打开的设备文件(文件描述符) * @param buf 返回给用户空间的数据缓冲区 * @param count 要读取的数据长度 * @param offt 相对于文件首地址的偏移 * @return 0 成功;其他 失败 */static ssize_t infrared_read(struct file *filp, char __user *buf, size_t count, loff_t *offt){ int res = 0; // struct infrared_dev_t *infrared_dev = filp->private_data; res = copy_to_user(buf, infrared_receive_data, count); if(res != 0) { printk(PRINTK_GRADE "111111111111111\n"); return -1; } // printk(PRINTK_GRADE "infrared_read\n"); return 0;} /** * @brief 向设备写数据 * @param filp 设备文件,表示打开的文件描述符 * @param buf 要写给设备写入的数据 * @param count 要写入的数据长度 * @param offt 相对于文件首地址的偏移 * @return 写入的字节数,如果为负值,表示写入失败*/static ssize_t infrared_write(struct file *filp, const char __user *buf, size_t count, loff_t *offt){ int res = 0; // struct infrared_dev_t *infrared_dev = filp->private_data; char write_buf[1024] = {"0"}; res = copy_from_user(write_buf, buf, count); if(res != 0) { return -1; } printk("kernel recevdata:%s\r\n", write_buf); return 0;} static int infrared_fasync(int fd, struct file *filp, int on){ struct infrared_dev_t *infrared_dev = filp->private_data; printk(PRINTK_GRADE "infrared_fasync\n"); /* 异步通知初始化 */ return fasync_helper(fd, filp, on, &infrared_dev->fasync_queue);} /** * @brief 关闭/释放设备 * @param filp 要关闭的设备文件(文件描述符) * @return 0 成功;其他 失败*/static int infrared_release(struct inode *inode, struct file *filp){ int res = 0; printk(PRINTK_GRADE "infrared_release\n"); /* 删除异步通知 */ infrared_fasync(-1, filp, 0); return res;} /* 设备操作函数结构体 */static struct file_operations infrared_ops = { .owner = THIS_MODULE, .open = infrared_open, .read = infrared_read, .write = infrared_write, .release = infrared_release, .fasync = infrared_fasync,}; /** * @brief 注册字符设备驱动 * * @return 0,成功;其他负值,失败*/static int infrared_register(void){ int ret = -1; // 保存错误状态码 /* GPIO 中断初始化 */ ret = infrared_init(); /* 1、创建设备号 */ /* 采用动态分配的方式,获取设备编号,次设备号为0 */ /* 设备名称为 infrared_NAME,可通过命令 cat /proc/devices 查看 */ /* INFRARED_CNT 为1,只申请一个设备编号 */ ret = alloc_chrdev_region(&infrared_dev.devid, 0, INFRARED_CNT, INFRARED_NAME); if (ret < 0) { pr_err("%s Couldn't alloc_chrdev_region, ret = %d \r\n", INFRARED_NAME, ret); goto fail_region; } /* 2、初始化 cdev */ /* 关联字符设备结构体 cdev 与文件操作结构体 file_operations */ infrared_dev.cdev.owner = THIS_MODULE; cdev_init(&infrared_dev.cdev, &infrared_ops); /* 3、添加一个 cdev */ /* 添加设备至cdev_map散列表中 */ ret = cdev_add(&infrared_dev.cdev, infrared_dev.devid, INFRARED_CNT); if (ret < 0) { pr_err("fail to add cdev \r\n"); goto del_unregister; } /* 4、创建类 */ infrared_dev.class = class_create(THIS_MODULE, INFRARED_NAME); if (IS_ERR(infrared_dev.class)) { pr_err("Failed to create device class \r\n"); goto del_cdev; } /* 5、创建设备,设备名是 INFRARED_NAME */ /*创建设备 INFRARED_NAME 指定设备名,*/ infrared_dev.device = device_create(infrared_dev.class, NULL, infrared_dev.devid, NULL, INFRARED_NAME); if (IS_ERR(infrared_dev.device)) { goto destroy_class; } return 0; destroy_class: device_destroy(infrared_dev.class, infrared_dev.devid);del_cdev: cdev_del(&infrared_dev.cdev);del_unregister: unregister_chrdev_region(infrared_dev.devid, INFRARED_CNT);fail_region: /* 释放个人初始化申请的资源,如del_init(); */ free_irq(infrared_dev.irq_num, NULL); gpio_free(infrared_dev.gpio); return -EIO;} /** * @brief 注销字符设备驱动 * * @return 0,成功;其他负值,失败*/static void infrared_unregister(void){ /* 1、删除 cdev */ cdev_del(&infrared_dev.cdev); /* 2、注销设备号 */ unregister_chrdev_region(infrared_dev.devid, INFRARED_CNT); /* 3、注销设备 */ device_destroy(infrared_dev.class, infrared_dev.devid); /* 4、注销类 */ class_destroy(infrared_dev.class); /* 释放中断 */ free_irq(infrared_dev.irq_num, NULL); /* 释放 IO */ gpio_free(infrared_dev.gpio);} /** * @brief 驱动入口函数 * * @return 0,成功;其他负值,失败*/static int __init infrared_driver_init(void){ pr_info("infrared_driver_init\n"); return infrared_register();} /** * @brief 驱动出口函数 * * @return 0,成功;其他负值,失败*/static void __exit infrared_driver_exit(void){ pr_info("infrared_driver_exit\n"); infrared_unregister();} /* 将上面两个函数指定为驱动的入口和出口函数 */module_init(infrared_driver_init);module_exit(infrared_driver_exit); /* LICENSE 和作者信息 */MODULE_LICENSE("GPL");MODULE_AUTHOR("JIAOZHU");MODULE_INFO(intree, "Y");

六、测试程序

#include <fcntl.h>#include <stdio.h>#include <unistd.h> #include <sys/types.h>#include <sys/stat.h>#include <stdlib.h>#include <sys/ioctl.h>#include <signal.h> /***************************************************************文件名 : drive_read_app.c作者 : jiaozhu版本 : V1.0描述 : 驱动读取测试其他 : 使用方法:./drive_read_app [/dev/xxx]argv[1] 需要读取的驱动日志 : 初版 V1.0 2023/3/3***************************************************************/ int fd;char *filename; /** * @brief 信号响应函数,用于读取红外接收的数据 * * @param num 信号量*/void infrared_handler(int num){ int res; unsigned char data_buf[4]; /* 从驱动文件读取数据 */ res = read(fd, data_buf, sizeof(data_buf)); if (res == 0) { printf("infrared data: %02x %02x %02x %02x\n", data_buf[0], data_buf[1], data_buf[2], data_buf[3]); } else { printf("read file %s failed!\r\n", filename); }} /** * @brief main 主程序 * @param argc argv 数组元素个数 * @param argv 具体参数 * @return 0 成功;其他 失败*/int main(int argc, char *argv[]){ int flags = 0; if(argc != 2){ printf("Error Usage!\r\n"); return -1; } filename = argv[1]; /* 打开驱动文件 */ fd = open(filename, O_RDWR); if(!fd){ printf("Can't open file %s\r\n", filename); return -1; } signal(SIGIO, infrared_handler); /* 设置当前进程接收信号 */ fcntl(fd, F_SETOWN, getpid()); flags =fcntl(fd, F_GETFL); /* 开启异步通知 */ fcntl(fd, F_SETFL, flags | FASYNC); while (1); close(fd); return 0;}

几种常用的红外线信号传输协议:https://tech.hqew.com/news_1050217
小米红外遥控器如何适配到其他应用设备之上:<https://blog.csdn.net/qq_40001346/article/details/108639243


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK