和红外有关的模块有很多,比如红外循迹,红外感应,红外发射,红外接收,红外对射,红外编解码等等。
今天我们要介绍的是红外编解码模块,它最常见的应用就是我们家里的电视、空调,当我们按下遥控器上的按钮时,红外信号从遥控器上的红外编解码模块发射,操作电视音量增大,空调温度降低等等。
本文首发 良许嵌入式网 :https://www.lxlinux.net/e/ ,欢迎关注!
本文所涉及的源码及安装包如下(由于平台限制,请点击以下链接阅读原文下载):
https://www.lxlinux.net/e/stm32/infrared-sensor-tutorial.html
如果你是嵌入式开发小白,那么建议你先读读下面几篇文章。
前期教程,没看过的小伙伴可以先看下。
红外编解码模块使用特定的红外协议来确保设备之间的通信准确性和兼容性。常见的红外编解码协议包括 NEC、RC5、RC6 等。
我们今天介绍的是 NEC 红外编解码模块,型号是 YS-IRTM。
红外发射头: 用于发射红外信号,波长为 940nm,频率为 38k,协议为 NEC 编码的红外信号。
红外接收头:用于接收 NEC 红外信号,进而单片机进行分析解码操作。
红外头扩展:该接口为红外发射头的扩展,可以连接多个红外发射头(常称红外发射模块),用于安放到不同的位置,实现多方位控制。
默认波特率是 9600。
YS-IRTM | STM32 |
---|---|
GND | GND |
RXD | A2(串口2)/B10(串口3) |
TXD | A3(串口2)/B11(串口3) |
5V | 5V |
我们今天介绍的红外编解码模块采用 NEC 编码,由引导码、用户码高位、用户码低位、数据码、数据反码五部分组成。
NEC 编码格式如下:
使用 38kHz 的载波频率。
引导码间隔为 9ms+4.5ms,用于同步发送方和接收方的时钟。
用户编码用于识别设备类型,比如识别不同的遥控器。
通过脉冲串之间的时间间隔来实现信号的调制(PWM)。
逻辑「0」由 0.56ms 的 38kHz 载波和 0.565ms 的无载波间隔组成,周期1.125ms。
逻辑「1」由 0.56ms 的 38kHz 载波和 1.69ms 的无载波间隔组成,周期2.25ms。
结束位由 0.56ms 的 38kHz 载波组成。
学习完原理,就进行我们的实践吧。
实现目标是我们有一个三色 LED 灯,三个灯各自有特定的信号,遥控器/手机发送红外信号,红外编解码模块收到数据,若含绿灯信号,绿灯亮;再次发送绿灯信号,绿灯灭,黄灯和红灯设定和效果一样。
我们知道,NEC 红外信号编码由 1 个 16 位用户码(分为高、低 8 位)、1 个 8 位数据码和 1 个 8 位数据码的反码组成。格式如下:
「用户码高位、用户码低位、数据码、数据反码」
我们在做解码操作时,只需要将遥控器对准红外接收头,按下需要解码的按键,即可通过串口调试助手查看到解码的结果,结果输出为「用户码高位+用户码低位+命令码」三位。
在做编码发送时发送「地址+操作位+数据位1+数据位2+数据位3」即可。
所以在正式开始前,我们需要知道我们的遥控器/手机会发出怎样的红外信号。
准备所需要的硬件如下:
我红外遥控器用的是正点原子的,不一定要用同款,甚至有的手机也可以当作红外遥控器用。
先将红外编解码模块与 USB 转 TTL 模块连接,插到电脑,用串口看看遥控器会发出怎样的编码。
接线如下:
YS-IRTM | USB 转 TTL |
---|---|
VCC | 5V |
RXD | TX |
TXD | RX |
GND | GND |
接好效果如下:
打开串口助手,选择你的串口号,波特率选择 9600;勾选显示接收时间,将换行输出,看的更清楚;勾选十六进制显示。
然后就可以按遥控器查看编码啦,以下是我的遥控器 1~9 的编码。我们选择 1 的 00 FF 16 为绿灯码,2 的 00 FF 19 为黄灯码,3 的 00 FF 10D 为红灯码。
红外的发射指令格式如下:
地址 | 操作位 | 数据位1 | 数据位2 | 数据位3 |
---|---|---|---|---|
A1(FA) | XX | XX | XX | XX |
地址:A1为默认地址(可改),FA 为通用地址 (不可改)。
操作位:该位的数据用于代表当前的工作状态。
数据位:不同的操作位(工作状态)有不同的数据内容,具体可看下表。
操作位 | 数据位1 | 数据位2 | 数据位3 | 说明 |
---|---|---|---|---|
F1 | 用户码高位 | 用户码低位 | 命令码 | |
F2 | 1-FF | 00 | 00 | 数据位1代表需要修改的地址值 |
F3 | 1-4 | 00 | 00 | 01 - 4800bps 02 - 9600bps 03 - 19200bps 04 - 57600bps |
比如:
目的 | 编码 |
---|---|
发射 NEC 信号编码为 1C 2F 33 | A1 F1 1C 2F 33 |
修改串口通信地址为 0xA5 | A1 F2 A5 00 00 |
修改波特率为9600bps(对应序号2) | A1 F3 02 00 00 |
我们发射信号后会收到如下结果:
编码 | 意义 |
---|---|
F1 | 发射成功 |
F2 | 串口地址修改成功 |
F3 | 波特率设置成功 |
无返回 | 指令接收错误、操作不成功、重启才有效 |
串口效果如下:
A1是串口通信默认地址,修改串口通信地址为A5后,再发送「A1 F1 00 FF 16」就收不到了,要发送「A5 F1 00 FF 16」才可以得到发射成功的「F1」。
我们来试试红外对射,两个红外编解码模块发送、接收。
本实验使用的硬件如下:
两对接线如下:
YS-IRTM | USB 转 TTL |
---|---|
5V | 5V |
RXD | TXD |
TXD | RXD |
GND | GND |
接好效果如下:
电脑打开两个串口调试助手,发送编码效果如下,红框和绿框各是一次发送结果。
红外对射的交互方式虽然简单,但是有很多应用场景。例如利用红外对射进行无线控制和交互,实现遥控车辆、飞行器、电子游戏等的操作和反馈。
本教程使用的硬件如下:
接线如下:
YS-IRTM | LED | STM32 | USB 转 TTL |
---|---|---|---|
5V | 5V | ||
RXD | A2 | ||
TXD | A3 | ||
GND | G | ||
R | A5 | ||
Y | A6 | ||
G | A7 | ||
GND | G | ||
A10 | TX | ||
A9 | RX | ||
G | GND |
烧录的时候接线如下表,如果不会烧录的话可以看我之前的文章【STM32下载程序的五种方法:https://www.lxlinux.net/e/stm32/five-ways-to-flash-program-to-stm32.html】。
ST-Link V2 | STM32 |
---|---|
SWCLK | SWCLK |
SWDIO | SWDIO |
GND | GND |
3.3V | 3V3 |
接好如下图:
串口接收数据在【STM32串口接收不定长数据(接收中断+超时判断):https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-rxne-time-out.html】有详细介绍,没看过的小伙伴可以看看。
UART_HandleTypeDef ys_uart_handle; uint8_t ys_uart_rx_buf[YS_RX_BUF_SIZE]; uint8_t ys_uart_tx_buf[YS_TX_BUF_SIZE]; uint16_t ys_uart_rx_len = 0; void ys_init(uint32_t baudrate) { ys_uart_handle.Instance = YS_INTERFACE; /* BT */ ys_uart_handle.Init.BaudRate = baudrate; /* 波特率 */ ys_uart_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 数据位 */ ys_uart_handle.Init.StopBits = UART_STOPBITS_1; /* 停止位 */ ys_uart_handle.Init.Parity = UART_PARITY_NONE; /* 校验位 */ ys_uart_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */ ys_uart_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */ ys_uart_handle.Init.OverSampling = UART_OVERSAMPLING_16; /* 过采样 */ HAL_UART_Init(&ys_uart_handle); /* 使能BT */ } void ys_rx_clear(void) { memset(ys_uart_rx_buf, 0, sizeof(ys_uart_rx_buf)); //清空接收缓冲区 ys_uart_rx_len = 0; //接收计数器清零 } void YS_IRQHandler(void) { uint8_t receive_data = 0; if(__HAL_UART_GET_FLAG(&ys_uart_handle, UART_FLAG_RXNE) != RESET){ //获取接收RXNE标志位是否被置位 if(ys_uart_rx_len >= sizeof(ys_uart_rx_buf)) //如果接收的字符数大于接收缓冲区大小, ys_uart_rx_len = 0; //则将接收计数器清零 HAL_UART_Receive(&ys_uart_handle, &receive_data, 1, 1000); //接收一个字符 ys_uart_rx_buf[ys_uart_rx_len++] = receive_data; //将接收到的字符保存在接收缓冲区 } if (__HAL_UART_GET_FLAG(&ys_uart_handle, UART_FLAG_IDLE) != RESET) //获取接收空闲中断标志位是否被置位 { int i = 0; printf("receive: \r\n"); for(i = 0; i < ys_uart_rx_len; i++ ) printf("%02X ", ys_uart_rx_buf[i]); //将接收到的数据打印出来 printf("\r\n"); control_led(); ys_rx_clear(); __HAL_UART_CLEAR_IDLEFLAG(&ys_uart_handle); //清除UART总线空闲中断 } }
LED 灯的代码简简单单,只要进行一下三个灯的初始化就行。
void led_init(void) { GPIO_InitTypeDef gpio_init_struct; LED1_GPIO_CLK_ENABLE(); /* LED1时钟使能 */ LED2_GPIO_CLK_ENABLE(); /* LED2时钟使能 */ LED3_GPIO_CLK_ENABLE(); /* LED3时钟使能 */ gpio_init_struct.Pin = LED1_GPIO_PIN; /* LED1引脚 */ gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */ gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */ gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(LED1_GPIO_PORT, &gpio_init_struct); /* 初始化LED1引脚 */ gpio_init_struct.Pin = LED2_GPIO_PIN; /* LED2引脚 */ HAL_GPIO_Init(LED2_GPIO_PORT, &gpio_init_struct); /* 初始化LED2引脚 */ gpio_init_struct.Pin = LED3_GPIO_PIN; /* LED3引脚 */ HAL_GPIO_Init(LED3_GPIO_PORT, &gpio_init_struct); /* 初始化LED3引脚 */ LED1(0); /* 关闭 LED1 */ LED2(0); /* 关闭 LED2 */ LED3(0); /* 关闭 LED3 */ }
LED 的 .h文件:
#ifndef _LED_H #define _LED_H #include "sys.h" /******************************************************************************************/ /* 引脚 定义 */ #define LED1_GPIO_PORT GPIOA #define LED1_GPIO_PIN GPIO_PIN_7 #define LED1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define LED2_GPIO_PORT GPIOA #define LED2_GPIO_PIN GPIO_PIN_6 #define LED2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define LED3_GPIO_PORT GPIOA #define LED3_GPIO_PIN GPIO_PIN_5 #define LED3_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PB口时钟使能 */ /******************************************************************************************/ /* LED端口定义 */ #define LED1(x) do{ x ? \ HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) #define LED2(x) do{ x ? \ HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) #define LED3(x) do{ x ? \ HAL_GPIO_WritePin(LED3_GPIO_PORT, LED3_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(LED3_GPIO_PORT, LED3_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) /* LED取反定义 */ #define LED1_TOGGLE() do{ HAL_GPIO_TogglePin(LED1_GPIO_PORT, LED1_GPIO_PIN); }while(0) /* 翻转LED1 */ #define LED2_TOGGLE() do{ HAL_GPIO_TogglePin(LED2_GPIO_PORT, LED2_GPIO_PIN); }while(0) /* 翻转LED2 */ #define LED3_TOGGLE() do{ HAL_GPIO_TogglePin(LED3_GPIO_PORT, LED3_GPIO_PIN); }while(0) /* 翻转LED3 */ /******************************************************************************************/ /* 外部接口函数*/ void led_init(void); /* LED初始化 */ #endif
我的遥控器前两位都一样,只需要判断第三位是不是为绿/黄/红灯码即可。若前两位都不正确,那就不是我的遥控器发出的红外信号,不用再往下判断了。
void control_led(void) { if(ys_uart_rx_buf[0] == 0x00 && ys_uart_rx_buf[1] == 0xFF) //地址码正确 { switch(ys_uart_rx_buf[2]) //判断数据码 { case 0x16: //绿灯码 LED1_TOGGLE(); //翻转LED1 break; case 0x19: //黄灯码 LED2_TOGGLE(); //翻转LED2 break; case 0x0D: //红灯码 LED3_TOGGLE(); //翻转LED3 break; } } }
主函数如下:
int main(void) { HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟,72M */ delay_init(72); /* 初始化延时函数 */ usart_init(115200); /* 串口1波特率设为115200 */ ys_init(9600); /* 串口2波特率设为9600 */ led_init(); printf("红外控制灯……\r\n"); while(1) { delay_ms(1000); } }
烧录后,打开串口,按下遥控器1、2、3,效果如下。
红外编解码模块(串口2)波特率是9600,串口调试助手接收的是单片机(串口1)的数据,波特率115200,大家不要弄混啦。
我们的三个小灯也打开了。(我的小绿灯不是很亮,用旧了,嘻嘻)
另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
推荐阅读:
欢迎关注我的博客:良许嵌入式教程网,满满都是干货!