本文章将介绍HX1838模块基本信息、NEC红外协议简介和基本实现方法、STM32控制该模块的简易代码,使用的MCU是STM32F103C8T6。后续代码升级请持续关注博主更新。
目录
模块简介
NEC协议简介
硬件原理概述
引导码
逻辑0和逻辑1的表示
地址码、数据码及其反码
停止位
低位优先
重发码
代码实现思路
STM32CubeMX配置
硬件连线
示例代码
代码问题分析
模块简介
HX1838红外接收头模块是一款遵循NEC红外协议的模块,通常配合商家赠送的红外遥控器使用,除此之外,也可以使用红外灯珠设计红外遥控器进行控制(商家可能会赠送)。
接收头只负责接收遥控器或红外线发射二极管发送的信息,信息的解析由硬件电路和单片机进行处理。
遥控器发送的数据包遵循NEC红外协议,如果采用自己设计的红外遥控器进行控制,发送数据的格式和协议可以自定义,以下是对NEC协议的介绍。
NEC协议简介
硬件原理概述
下图为遥控器发送的信号,黄色部分并不是高电平,而是38kHz的连续脉冲信号。
图片来自up主 爱上半导体
一帧红外遥控信号,竟如此复杂,超乎你的想象!红外遥控的工作原理!_哔哩哔哩_bilibili
经由红外接收管内置的芯片处理,芯片收到高频脉冲就会输出低电平,收不到则输出高电平,故单片机引脚接收到的信号如下图:
中的图片进行修改
一帧数据包含:
引导码,地址码,地址反码,数据码,数据反码,停止位(按照发送顺序排列)
引导码
由上文知,信号线空闲的时候,红外模块内置的芯片输出的是高电平,当一帧信号传输到单片机引脚,单片机最先收到的就是引导码,产生9ms的低电平,接下来再产生4.5ms的高电平。
引导码作为帧头,负责引出后续的有效信息,单片机通过检测引导码来确认是否开始接收一帧数据。
逻辑0和逻辑1的表示
涉及到通信协议,就有必要思考,如何表示数据位的某一位是0还是1?由于NEC是单线通信协议,我们通过检测高电平时长和低电平时长确定。NEC使用这种编码方式,我们不需要设置接收位的接收周期,也可以检测数据中的多个逻辑0和1,较为方便和节省资源。
地址码、数据码及其反码
单片机通过检测地址码,来识别遥控器,不同的遥控器可以设置不同的地址码,地址共8位(bit),通过上图中的“逻辑1”和“逻辑0”来表示每1位。HX1838红外模块赠送的遥控器,地址码一般是0x00,不同产品可能存在差异。
数据码就是需要传输的有效数据,比如红外遥控器的键值,共8位。下表是HX1838模块赠送的遥控器的键值表,不同遥控器键值表可能不同,建议到手后检查核对。
键值 | 实际数据 | 数据反码 |
1 | 0x45 | 0xba |
2 | 0x46 | 0xb9 |
3 | 0x47 | 0xb8 |
4 | 0x44 | 0xbb |
5 | 0x40 | 0xbf |
6 | 0x43 | 0xbc |
7 | 0x07 | 0xf8 |
8 | 0x15 | 0xea |
9 | 0x09 | 0xf6 |
* | 0x16 | 0xe9 |
0 | 0x19 | 0xe6 |
# | 0x0d | 0xf2 |
上箭头 | 0x18 | 0xe7 |
左箭头 | 0x08 | 0xf7 |
右箭头 | 0x5a | 0xa5 |
下箭头 | 0x52 | 0xad |
OK | 0x1c | 0xe3 |
地址反码和数据反码就是对前面的地址码、数据码进行按位取反,是为了对数据进行校验而存在的。在收到的一帧数据中,如果~(xx反码)≠ xx码,那么这帧数据就是无效的,需要丢弃。
停止位
至于停止位,往往是持续0.56ms的低电平,一般在程序中无需判断停止位,收到引导码后,接收四个字节(地址码、数据码及其反码)即可选择结束本轮接收。
重发码
在发送完一帧数据后,如果红外遥控器的按键依旧按下,就会每110ms发送一次重发码。用于检测遥控器按键是否持续按下。如果我们不设计持续按下对应的相关功能,通常需要识别出重发码即可,重点在于区分重发码和引导码。
低位优先
在数据位接收顺序方面,NEC协议遵循低位优先原则,发送地址码、数据码及其反码时,先发较低位再发较高位。
代码实现思路
单片机没有NEC通信接口,我们需要自己编写代码去识别每一个码元。如何识别?通过测量波形来识别,单片机与测量波形相关的功能有输入捕获、PWM输入模式,考虑到复杂度与资源占用,本项目采用较简单轻便的输入捕获模式。
图片摘自江协科技STM32教程
上图展示了输入捕获的基本原理,以上升沿触发为例,当外输入信号产生一个上升沿,TIM定时器外设就会产生一个捕获事件,单片机内部会产生以下变化:
1.TIMx会将CNT当前的值写入CCR1寄存器;
2.TIMx产生一个捕获/比较中断(如果我们使能了该中断);
3.如果我们配置了reset从模式,CNT寄存器的值在被写入CCR1寄存器后,会被清零;
在reset从模式下,我们直接读取CCR1寄存器,就可以获知两个上升沿之间的时长,不必再作加减运算。
确定了使用的外设以后,我们来对NEC协议进行简单分析和拆解:
中的图片进行修改
由上述分析,引导码、逻辑0、逻辑1、重复码(地址码、数据码及其反码由若干逻辑0和逻辑1组合而成),这些码元都是以下降沿开始,下降沿结束的。我们可以将TIMx触发捕获事件的边沿设置为下降沿。下降沿意味着当前码元的结束,恰好此时捕获中断产生,我们可以在中断函数内处理该码元;同时也意味着下一个码元的开始。
这些码元的长度也是各不相同的,我们可以通过输入捕获测量它们的总时长,根据时长来区别不同的码元。当然,判断条件应适当留有余量,具体可参照下文的代码。
STM32CubeMX配置
以STM32F103C8T6为例进行配置。如果你使用的是标准库,可以依照下图来使用库函数配置必要项。
配置时钟树:
配置定时器:
硬件连线
如图,S为信号线,连接到我们刚刚配置的TIM1 CH1即可,与单片机共VCC共地即可。本项目中使用的MCU是STM32,VCC接+3.3V即可。S引脚和负极的引脚应该会在模块上标出。
示例代码
hx1838.c
#include "hx1838.h" volatile uint8_t IR_buf[4]; volatile uint8_t IR_Update_flag; /** * @brief 对红外模块初始化,打开中断 * @param None * @retval None */ void IR_Init(void) { HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1); } /** * @brief 将NEC协议的数据位映射到对应的业务上 * @param None * @retval None */ void IR_DataBitTranslate(void) { //判断并更新键值 if ((IR_buf[0] == 0x00) && ((IR_buf[0] ^ IR_buf[1]) == 0xFF) && ((IR_buf[2] ^ IR_buf[3]) == 0xFF)) { // 校验通过,处理这帧数据 switch(IR_buf[2]) { case IR_KEY_LEFT: IR_AniMode = (IR_AniMode_t)((IR_AniMode + 1) % MODE_TOTAL); IR_AnimationInit(); break; case IR_KEY_RIGHT: IR_AniMode = (IR_AniMode_t)((IR_AniMode + 3 - 1) % MODE_TOTAL); IR_AnimationInit(); break; default:; } } } /** * @brief 将接收到的字节收入环形缓冲区 * @param byte 填入收到的字节 * @retval None */ void IR_InQuene(uint8_t byte, uint8_t indexx) { IR_buf[indexx] = byte; } /** * @brief 检测到对应边沿后,IRQ函数会调取该函数进行必要的解码操作 * @note 此工程中,检测到下降沿,然后测量电平持续时间 * @param htim TIM的句柄 * @retval None */ void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static volatile uint8_t OneByte = 0; static volatile uint8_t byte_sum = 0; static volatile uint8_t poi = 0; static volatile uint8_t LeaderCopy_Flag = 0; static volatile uint32_t period = 0; if(htim == &htim1) { period = HAL_TIM_ReadCapturedValue(&htim1, TIM_CHANNEL_1); //接收1 Byte地址/数据,建立在引导码发送的前提 if(LeaderCopy_Flag == 1) { if(period >= 1800 && period <= 2900) { //逻辑1 OneByte = OneByte | (0x01 << poi); poi++; } else if(period >= 800 && period <= 1400) { //逻辑0 OneByte = OneByte & ~(0x01 << poi); poi++; } else { //信号异常,直接重启 poi = 0; OneByte = 0; byte_sum = 0; LeaderCopy_Flag = 0; return; } //接收完OneByte if(poi == 8) { IR_InQuene(OneByte, byte_sum); //后续会改成置标志位,中断外入队 byte_sum++; poi = 0; OneByte = 0; if(byte_sum == 4) { byte_sum = 0; LeaderCopy_Flag = 0; IR_Update_flag = 1; } } } else { if(period >= 10000 && period <= 11500) //单位是us { //重复码 return; } else if(period > 11500 && period <= 15000) { //引导码 LeaderCopy_Flag = 1; } } } }hx1838.h
#ifndef __HX1838_H__ #define __HX1838_H__ #include "main.h" #include "stm32f1xx_hal.h" #include "tim.h" #include "ws2812.h" void IR_Init(void); void IR_DataBitTranslate(void); #endif代码问题分析
1.灵敏度有时较低,有时按下按键后可能无法执行相关业务,具体问题有待排查;
2.回调函数较臃肿,只需留下必要的接收语句,即period = ...;解码操作可在主函数执行;
3.仅采用裸一维数组当缓冲区,数据极容易被覆盖,后续会使用环形缓冲区;
4.未设计重发码相关程序。
后续会针对这些问题优化更新