一、概述
现在很多家电都使用了红外,而智能家居的诞生,连音响都带了红外遥控功能。为了解决家里遥控器比较多的情况,多数手机都支持了红外功能,这和以前的万能遥控器一样。这里主要记录红外接收的驱动,当然明白怎么接收的,对于遥控的发射就比较简单了。
二、红外接收器
-
外观
-
接收的工作原理
红外探头应该也是光敏电阻的一种,当接收到波长在750-1150NM的光时,OUT 引脚就会产生一个 38kHz 的 PWM 波。一般在电路中都会给 OUT 引脚进行一个上拉,所以没有检测到红外光时,OUT 引脚是稳定的高电平。通过这个现象我们就可以进行无线通信。
注意:750-1150NM的光时是肉眼不可见的,不过可以通过手机摄像头进行查看 -
通信协议
了解完原理后,只需要配上相应的通信协议就可以使用红外进行无线通信了。常用的红外线信号传输协议有ITT协议、NEC协议、NokiaNRC协议、Sharp协议、SonySIRC协议、PhilipSRC-5协议、PhilipsRC-6协议,以及PhilipsRECS-80协议等。需要了解不同协议区别的可以参考:几种常用的红外线信号传输协议,红外的协议种类比较多,部分公司也会自己指定不同的协议,比如小米公司的遥控器,见小米红外遥控器如何适配到其他应用设备之上。
此笔记主要使用 NEC 协议完成驱动的编写,其他的协议驱动也可以参考完成。
三、 NEC协议
-
数据帧格式
引导码 地址码0 地址码1 命令码 命令反码 引导码(重复) LSB-MSB(0-7) LSB-MSB(8-15) LSB-MSB(16-23) LSB-MSB(24-31) 注意:在标准的NEC协议中,地址码1为地址码0的反码,而在许多遥控器中,地址码0和地址码1共同作为红外遥控器的编码值。
-
PPM(脉冲位置调制)
-
接收波形
注意:实际波形在低电平期间是一个 38kHz 的 PWM 波。 -
数据解析
在接收数据时需要过滤 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*************************************************************** 文件名 : 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 #include #include #include #include #include #include #include /*************************************************************** 文件名 : 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