hx1838 红外遥控(1):接收时序的解码

本文开发环境:

  • MCU型号:STM32F103C8T6
  • IDE环境: MDK 5.27
  • 代码生成工具:STM32CubeMx 5.6.1
  • HAL库版本:STM32Cube_FW_F1_V1.8.0

本文内容:

  • STM32 使用 定时器捕获功能捕获红外时序
  • 解码 hx1838 时序

附件:

  • MDK5 示例工程

红外遥控器 + 红外接收头 :
在这里插入图片描述

测试过程请注意遥控和接收头的距离不要过远,本文遥控在1m以上会有不稳定现象,实际操作可以使用示波器或逻辑分析仪捕获波形,保证接收头收到的遥控数据是完整的。

一、接收头的滤波输出

hx1838 红外接收头自带了滤波的功能,本文使用的接收头中,当接收到38Khz的PWM 时,输出低电平,否则输出高电平:
在这里插入图片描述
由于接收头的滤波功能,所以程序只需要检测高低电平的时长既可。

二、NEC编码

本文使用的 红外遥控器采用了NEC编码规则:

  • NEC 载波频率为 38Khz
    • 引导码:9ms 高电平 + 4.5ms 低电平
    • 1 码 :0.56 ms 高电平 + 0.56 ms 低电平
    • 0 码 : 0.56ms 高电平 + 1.68 ms 低电平
    • 结束码 :0.56ms 高电平
  • 数据帧格式:引导码 + 地址 + 地址反码 + 键值 + 键值反码 + 结束码
  • 重复帧格式:9ms 高电平 + 2.25ms低电平 + 结束位 + 结束码
  • 高位在前,即首先收到的是高位的数据

:本文使用的接收头,电平极性与协议相反。所以,当捕获到一个 9ms 低电平 + 4.5ms 高电平时,即收到一个引导码。

三、时序图

这是逻辑分析仪捕获到当遥控上按键 [1] 被按下一次的时序:
在这里插入图片描述
根据 NEC 的编码格式,可以实际看出这一帧数据的含义:
在这里插入图片描述
由于红外接收头输出的极性相反,所以高电平宽的波形为1码,高电平窄的波形为0码。

四、解码

只需要获取每个波形高低电平时长,通过然后将对应的数据置1或清0,最后读取整个数据即可。本文将时序的解码分为三个部分:

  • 时序捕获
    这部分程序负责捕获高低电平的之间变化,并记录其持续的值记录到数组中,初始化时,将定时器通道配置为下降沿捕获,当捕获到一个下降沿,就将捕获极性改为上升沿,并记录此时时间,下一次捕获时,说明获得了一个低电平,将两次时间作差,既可以得到低电平的时间。
  • 时序解码
    这部分程序负责读取时序捕获的值,并根据时长判断码值,最后合并成数据,如读取数组第一个值和第二个值分为为 9ms 和 4.5 ms,那么将认为捕获到一个引导码,读取到一个0.56ms低电平和0.56ms高电平,则认为读取到一个 0 码。
  • 执行程序
    这部分根据解析到的键值执行相应的代码

五、STM32CubeMx 输入捕获配置

这里只列出关键配置,关于 Mx 配置定时器捕获原理及完整的示例,详见:Mx 配置定时器 的输入捕获部分

  • 本文将 TIM1 的 CH1 设置为输入捕获通道:
    在这里插入图片描述
  • 并打开了定时器溢出中断和捕获中断:
    在这里插入图片描述
    将定时器配置为72分频,即计数频率为 1M (TIM1 时钟源为 72M),可以捕获us级别的时间间隔。

六、程序

程序主要分为4个部分:

  • 调试语句
  • 波形捕获
  • 波形解码
  • 程序执行
  • 使能捕获

空闲状态:本文使用变量sta_idle来标志空闲状态,空闲状态只能是没有数据正在捕获中。

1. 调试部分

当 宏定义 RX_DBG_EN 为 1 时,RX_DGB 等效于 printf ,可以正常的打印数据,当 RX_DBG_EN 为0 时 RX_DGB 为空语句,程序不打印数据。

#define  RX_DBG_EN   0

#if RX_DBG_EN
#define RX_DBG(format, ...) printf(format, ##__VA_ARGS__)
#else
#define RX_DBG(format, ...) ;
#endif

这样,可通过RX_DBG_EN 宏,来控制程序是否打印调试信息。
本文示例工程中,宏被关闭,如果需要查看调试信息,将值改为1即可。由于打印数据较多,有一定概率打印不完整,一般重复3-4次即可获取一次完整数据。打开调试宏请避免使用连按功能。

2. 波形捕获

本文设置了TIM1的CH1为捕获通道,波形的捕获需要2个回调函数:

  • void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
    极性捕获函数用来捕获 IO 口的电平跳变,并记录当时计时的值
  • void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    因为定时器溢出以后,计数会清0(本文配置决定),所以当捕获的电平在计数周期末时比如(9999),下一个电平来时候,可能捕获的值为550,所以计算时间差需要 550 + 10000(1次溢出)- 9999

2.1 捕获中断回调函数

rx_rcv_init()HAL_TIM_IC_CaptureCallback() 子函数:

/* 电平捕获中断回调 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    static uint16_t tmp_cnt_l,tmp_cnt_h;
	if(TIM1 == htim->Instance)
	{
        switch(cap_pol)                                                                                         //根据极性标志位判断捕获是低电平还是高电平
        {   
            /* 捕获到下降沿 */
            case 0:
                tmp_cnt_l = HAL_TIM_ReadCapturedValue(&htim1,TIM_CHANNEL_1);                                    //记录当前时刻
                TIM_RESET_CAPTUREPOLARITY(&htim1, TIM_CHANNEL_1);                                               //复位极性配置
                TIM_SET_CAPTUREPOLARITY(&htim1, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);                //改变极性
                
                cap_pol = 1;                                                                                    //极性标志位改为上升沿
                
                if(sta_idle)                                                                                    //如果当前为空闲状态,空闲捕获到的时序,为第一个下降沿
                {
                    rx_rcv_init();
                    break;                                                                                      //返回
                }
                rx_frame[cap_pulse_cnt] = tim_udt_cnt * 10000 + tmp_cnt_l - tmp_cnt_h;                          //与上次捕获的计时作差,记录值
                tim_udt_cnt = 0;                                                                                //溢出次数清0
                RX_DBG("(%2d)%4d us:H\r\n",cap_pulse_cnt,rx_frame[cap_pulse_cnt]);                              //DBG:打印捕获到的电平及其时长
                cap_pulse_cnt++;                                                                                //计数++
                break;
            
            /* 捕获到上升沿 */
            case 1:
                tmp_cnt_h = HAL_TIM_ReadCapturedValue(&htim1,TIM_CHANNEL_1);
                TIM_RESET_CAPTUREPOLARITY(&htim1, TIM_CHANNEL_1);               
                TIM_SET_CAPTUREPOLARITY(&htim1, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING);
                
                cap_pol = 0;   
                if(sta_idle)
                {
                    rx_rcv_init();
                    break;
                }
                rx_frame[cap_pulse_cnt] = tim_udt_cnt * 10000 + tmp_cnt_h - tmp_cnt_l;
                tim_udt_cnt = 0;
                RX_DBG("(%2d)%4d us:L\r\n",cap_pulse_cnt,rx_frame[cap_pulse_cnt]);
                cap_pulse_cnt++;
                break;
            
            default:
                break;
        }
    }
}

第38行:这里需要判断一次,是否为第一个边缘,如果是就执行初始化相关内容,然后退出,第一个边缘是没有上一次边缘可以与之作差的,此后的每一个边缘触发,都是一个电平时长的捕获。

cap_pulse_cnt,是方波个数的统计,也用来做方波数组的下标值,因为他们存在一一对应的关系。

tim_udt_cnt在非空闲状态定时器溢出一次,将累加1,每一次捕获到电平,计算时长以后,都需要将变量清零。所以,当这个值在一定未被清零时候,说明一定时间没有数据产生,可借助它来判断MCU是否进入空闲状态。

2.2 定时器1 溢出回调函数

/* 溢出中断回调函数 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{   
	if(TIM1 == htim->Instance)
	{
        if(sta_idle)                                          //空闲状态,不作任何处理
        {
            return;
        }
        
        tim_udt_cnt++;                                         //溢出一次
        if(tim_udt_cnt == 3)                                   //溢出3次
        {
            tim_udt_cnt = 0;                                   //溢出次数清零
            sta_idle    = 1;                                   //这是为空闲状态
            cap_frame   = 1;                                   //标记捕获到新的数据
        }
	} 
}

tim_udt_cnt 变量累计到3时候,证明至少20ms,至多30ms未捕获到新的电平,固判断为空闲状态

3. 时序解码

波形捕获部分,已经将捕获到的电平宽度一一保存在数组rx_frame中,所以这部分程序需要分析数组的各个值。这部分需要一个数据结构:

#define  RX_SEQ_NUM  33

struct {
    uint16_t  src_data[RX_SEQ_NUM*2];
    uint16_t  repet_cnt;
    union{
        uint32_t rev;
        struct
        {
            uint32_t key_val_n:8;
            uint32_t key_val  :8;
            uint32_t addr_n   :8;
            uint32_t addr     :8;
        }_rev;
    }data;
}rx;

RX_SEQ_NUM :定义了波形(1高电平+1低电平的组合)的个数,此处不考虑结束码,所以是 33 个 波形,
src_data :用存储捕获到的值,由于一个波形有一个高电平和一个低电平,所以数组的长度需要 RX_SEQ_NUM * 2
data:是一个共同体,其中变量 rev 和 结构体_rev 处于同一内存中,程序可以直接操作rev,达到改变 结构体_rev 成员值的方法。

uint8_t appro(int num1,int num2)
{
    return (abs(num1-num2) < 300);
}

uint8_t hx1838_data_decode(void)
{
    memcpy(rx.src_data,rx_frame,RX_SEQ_NUM*4);
    memset(rx_frame,0x00,RX_SEQ_NUM*4);   
    RX_DBG("========= rx.src[] =================\r\n");
    for(uint8_t i = 0;i<=(RX_SEQ_NUM*2);i++)
    {
        RX_DBG("[%d]%d\r\n",i,rx.src_data[i]);
    }
    RX_DBG("========= rx.rec =================\r\n");
    if(appro(rx.src_data[0],9000) && appro(rx.src_data[1],4500))                 //#1. 检测前导码
    {
        uint8_t tmp_idx = 0;
        rx.repet_cnt  = 0;                                                       //新按键开始,按键重复个数清0
        for(uint8_t i = 2;i<(RX_SEQ_NUM*2);i++)                                  //#2. 检测数据
        {
            if(!appro(rx.src_data[i],560))
            {
                RX_DBG("%d,err:%d != 560\r\n",i,rx.src_data[i]);
                return 0;
            }
            i++;
            if(appro(rx.src_data[i],1680))
            {
                rx.data.rev |= (0x80000000 >> tmp_idx);                          //第 tmp_idx 为置1
                tmp_idx++;
            }
            else if(appro(rx.src_data[i],560))
            {
                rx.data.rev &= ~(0x80000000 >> tmp_idx);                         //第 tmp_idx 位清0
                tmp_idx++;
            }
            else
            {
                RX_DBG("%d,err:%d != 560||1680\r\n",i,rx.src_data[i+1]);
                return 0;
            }
        }
    }
    else if(appro(rx.src_data[0],9000) && appro(rx.src_data[1],2250) && appro(rx.src_data[2],560))
    {
        rx.repet_cnt++;
        return 2;
    }
    else
    {
        RX_DBG("前导码检测错误\r\n");
        return 0;
    }
    return 1;
}

第3,4行:拷贝捕获电平的数组到新数组中,并将捕获电平的数组清零,由于数组元素为2个字节所以长度需要 x2,即为波形个数的4倍。
注意到,由于捕获到的时序是有误差的,所以程序不能直接判断值是否相等,所以appro(int num1,int num2)用来判断两个值是否接近,只需要接近标准值即可,本文设置的范围为 ± 300 us。

程序首先是判断前2个值,若为 9500us 和 4500 us,就是前导码,可以进一步判断后续的值,若前3个值是 9500us + 2250us + 560us 那就是重复码,rx.repet_cnt ++累加1,若都不是,则数据出错。

4. 执行部分

当解码程序将捕获的电平解析以后,就可以根据键值来执行对应的程序了:

void hx1838_proc(uint8_t res)
{
    if(res == 0)
    {
        printf("error \r\n");
        return;
    }
    
    if(res == 2)
    {
        printf("repet(%d)\r\n",rx.repet_cnt);
        return;
    }      
    switch(rx.data._rev.key_val)
    {
        case 162:
            printf("detected code [%s] \r\n","1");
            break;
        
        case 98:
            printf("detected code [%s] \r\n","2");
            break;
        
        case 226:
            printf("detected code [%s] \r\n","3");
            break;
        
        case 34:
            printf("detected code [%s] \r\n","4");
            break;
        
        case 2:
            printf("detected code [%s] \r\n","5");
            break;
        
        case 194:
            printf("detected code [%s] \r\n","6");
            break;
        
        case 224:
            printf("detected code [%s] \r\n","7");
            break;
        
        case 168:
            printf("detected code [%s] \r\n","8");
            break;
        
        case 144:
            printf("detected code [%s] \r\n","9");
            break;
        
        case 152:
            printf("detected code [%s] \r\n","0");
            break;
        
        case 104:
            printf("detected code [%s] \r\n","*");
            break;
        
        case 176:
            printf("detected code [%s] \r\n","#");
            break;
                
        case 24:
            printf("detected code [%s] \r\n","↑");
            break;
                
        case 16:
            printf("detected code [%s] \r\n","←");
            break;
        
        case 74:
            printf("detected code [%s] \r\n","↓");
            break;
        
        case 90:
            printf("detected code [%s] \r\n","→");
            break;
        
        case 56:
            printf("detected code [%s] \r\n","OK");
            break;
        
        default:
            printf("detected unknow code \r\n");
            break;
        
    }
}

程序直接判断了rx.data._rev.key_val的值,这是因为在对rev操作时,已经改变了它的值,这体现了共同体的便利性,否则需要程序进行拆解。这里执行程序比较简单,只是简单把值打印出来。其中,参数为 解码函数的返回值,0表示数据错误,1表示新的按键值,2表示重复码。

5. 使能捕获

最后需要注意,HAL库默认是不开启捕获的,所以需要用户使能:

void hx1838_cap_start(void)
{
    HAL_TIM_Base_Start_IT(&htim1);
    HAL_TIM_IC_Start_IT(&htim1,TIM_CHANNEL_1);
}

hx1838_cap_start()分别打开了溢出中断和捕获中断

6. 测试

void HX1838_demo(void)
{
    hx1838_cap_start();                          //使能捕获
    while(1)  
    { 
        if(cap_frame)                            //如果捕获到新的数据
        {   
            hx1838_proc(hx1838_data_decode());   //处理解码后的数据
            cap_frame = 0;                       //标记未捕获到数据
        }
    };
}

上文代码写在新建立文件HX1838.c中,在main函数调用需要声明外部函数:

int main(void)
{
...
  HAL_Init();
...
  while (1)
  {
  ... ...
      extern void HX1838_demo(void);
      HX1838_demo();
  }
  /* USER CODE END 3 */
}

本文使用ST-LINk的调试打印功能,运行程序后MDK的调试窗口中可以观察到数据情况:
在这里插入图片描述

七、附件

IR 接收 示例工程
提取码:1234

### 使用 STM32HX1838 实现红外遥控控制 LED 灯 #### 准备工作 为了实现这一目标,需要准备如下硬件组件: - STM32 开发板一块 - HX1838 红外接收头一个 - WS2812B 或其他类型的可编程 RGB LED 模块若干 - 连接线若干根 确保开发环境已经配置好,并安装了必要的库文件用于处理红外信号和驱动LED。 #### 接口连接说明 HX1838 的 VCC 引脚接到 5V 电源上;GND 引脚接地;OUT 输出引脚则需连至 STM32 上的一个 GPIO 输入捕获端口。对于WS2812B来说,则将其 DIN 数据输入引脚与另一个GPIO相连作为PWM输出来发送数据给LED条带[^1]。 #### 软件设计思路 程序主要分为两大部分:一是对接收到的红外编码进行解码分析得到按键信息;二是依据解析后的命令去操作相应的LED状态变化逻辑。 针对上述需求编写C语言源代码如下所示: ```c #include "stm32f1xx_hal.h" #include "irremote.h" #define LED_PIN GPIO_PIN_13 #define LED_PORT GPIOD // 初始化函数 void SystemClock_Config(void); static void MX_GPIO_Init(void); int main(void){ HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); IRrecv irrecv(GPIO_PIN_IR); // 假设IR传感器连接到了定义好的PIN decode_results results; while (true) { if(irrecv.decode(&results)){ switch(results.value){ // 对应不同的键值执行不同动作 case KEY_ONOFF:// 关机/开机键 Toggle_LED(LED_PIN, LED_PORT); break; default: break; } irrecv.resume(); // Continue receiving } } } // 切换LED开关状态 void Toggle_LED(uint16_t pin, GPIO_TypeDef* port){ HAL_GPIO_TogglePin(port,pin); } ``` 这段代码实现了基本的功能框架,在`main()`循环里不断监听来自HX1838传来的脉冲序列,并尝试匹配预存于内部表中的标准协议模板。一旦成功识别出特定模式下的按压事件就会触发相应分支内的指令集完成实际的任务——这里是改变指定IO管脚上的高低电平从而达到点亮或熄灭外部负载的目的[^2]。 #### 注意事项 由于某些品牌的产品可能采用了私有的加密算法或者其他复杂机制使得其发出的信息难以被第三方解读模仿,所以在实践中可能会遇到兼容性方面的问题。因此建议先从小型家电如风扇、简易电视机等开始测试再逐步扩大适用范围。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值