单片机学习小组
直播中

程成

12年用户 502经验值
私信 关注

STM32如何使用定时器捕获功能捕获红外时序?

STM32如何使用定时器捕获功能捕获红外时序?

回帖(1)

王晶

2022-2-9 15:22:28
  本文开发环境:
  

  • 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:Hrn",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:Lrn",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[] =================rn");
    for(uint8_t i = 0;i<=(RX_SEQ_NUM*2);i++)
    {
        RX_DBG("[%d]%drn",i,rx.src_data);
    }
    RX_DBG("========= rx.rec =================rn");
    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,560))
            {
                RX_DBG("%d,err:%d != 560rn",i,rx.src_data);
                return 0;
            }
            i++;
            if(appro(rx.src_data,1680))
            {
                rx.data.rev |= (0x80000000 >> tmp_idx);                          //第 tmp_idx 为置1
                tmp_idx++;
            }
            else if(appro(rx.src_data,560))
            {
                rx.data.rev &= ~(0x80000000 >> tmp_idx);                         //第 tmp_idx 位清0
                tmp_idx++;
            }
            else
            {
                RX_DBG("%d,err:%d != 560||1680rn",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("前导码检测错误rn");
        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 rn");
        return;
    }
   
    if(res == 2)
    {
        printf("repet(%d)rn",rx.repet_cnt);
        return;
    }      
    switch(rx.data._rev.key_val)
    {
        case 162:
            printf("detected code [%s] rn","1");
            break;
        
        case 98:
            printf("detected code [%s] rn","2");
            break;
        
        case 226:
            printf("detected code [%s] rn","3");
            break;
        
        case 34:
            printf("detected code [%s] rn","4");
            break;
        
        case 2:
            printf("detected code [%s] rn","5");
            break;
        
        case 194:
            printf("detected code [%s] rn","6");
            break;
        
        case 224:
            printf("detected code [%s] rn","7");
            break;
        
        case 168:
            printf("detected code [%s] rn","8");
            break;
        
        case 144:
            printf("detected code [%s] rn","9");
            break;
        
        case 152:
            printf("detected code [%s] rn","0");
            break;
        
        case 104:
            printf("detected code [%s] rn","*");
            break;
        
        case 176:
            printf("detected code [%s] rn","#");
            break;
               
        case 24:
            printf("detected code [%s] rn","↑");
            break;
               
        case 16:
            printf("detected code [%s] rn","←");
            break;
        
        case 74:
            printf("detected code [%s] rn","↓");
            break;
        
        case 90:
            printf("detected code [%s] rn","→");
            break;
        
        case 56:
            printf("detected code [%s] rn","OK");
            break;
        
        default:
            printf("detected unknow code rn");
            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的调试窗口中可以观察到数据情况:



举报

更多回帖

发帖
×
20
完善资料,
赚取积分