【STM32】NEC协议红外连续收发硬件电路设计与实现基于STM32平台

基于STM32的NEC协议红外连续接收和发送的硬件电路及其实现

博主使用的芯片是STM32L431,但本文内容适用于STM32全系列芯片。本帖中的代码原理在任何MCU上都是通用的。
本帖介绍的IR红外通信主要用于超近距离通信,例如外设设备的出厂设置。不过,该代码经过适当调整后,也可用于任何距离的红外通信场景。

NEC协议

  • 逻辑1为560us高电平 + 1690us低电平, 总时长为2.25ms
  • 逻辑0为560us高电平 + 560us低电平,总时长为1.12ms
  • 启动信号为9ms高电平 + 4.5ms低电平
  • 重复信号为9ms高电平 + 2.25ms低电平 + 560us高电平,并且是以110ms为周期发送的

    NEC协议发送数据的数据帧格式:引导码+地址+地址取反+数据+数据取反,其中取反用于验证数据是否正确
    ![在这里插入图片描述](https://i3.wp.com/i-blog.csdnimg.cn/direct/83669f1a1ba64884b34f24822fe9ae64.png
    除了发出一帧的数据以外,还可能会有按着发送键不放的情况,这种情况就要使用重复码,重复码即重复信号,如下图
    重复信号的周期是110ms,由9ms高电平 + 2.25ms低电平 + 560us高电平组成。
  • 硬件电路图

    红外电路的硬件电路实现
    PA8 需要可以开启定时器的设备,本文使用定时器1的通道1
    PA0 需要可以开启定时器PWM的设备,本文使用定时器2的通道1
    值得注意的是,在本文的设计过程中,采用了两个定时器来完成收发功能,但是原理上是可以使用一个定时器来完成的。
    只不过,该项目在设计过程中资源不紧张,故开启了两个定时器。

    如下图实物图:
    中间的两个排针一个是红外发送一个是红外接收
    实物图

    实现原理

  • 接收红外信号使用定时器1的输入捕获功能,默认接收上升沿信号,当接收到信号后,进入接收捕获中断,在中断中保存信号。
  • 配置如下图
    TIM1配置

  • 发送信号使用定时器2的PWM输出功能,按照协议开关PWM输出以控制红外发射头闪烁。
  • 本文硬件结构是当PWM输出低电平的时候,红外发射管发射红外信号,而输出高电平的时候,红外发射管是熄灭状态。

    配置如下图

    上图中,

  • Prescaler为79,是因为本文使用的stm32L4的时钟频率为80MHz,故Pre为80-1,得到1Mhz的TIM2频率,即周期为1us
  • Counter Period 为26-1,原因是1us * 26 = 26us(约为38kHz),38kHZ是NEC协议的载波频率
  • Pulse为17,是因为NEC协议中,高电平的占空比在1/3较为合适,也是手册推荐占空比,故17/26代表2/3的低电平和1/3的高电平(需要理解CH Polarity 为 Low的含义)
  • CH Polarity设置为Low,代表低电平有效,也就是PWM信号的初始电平为低电平
  • 代码部分

    接收部分

    /* 使用HAL库 如果使用标准库,请自行更改 */
    #include "main.h"
    #include <stdint.h>
    #include <stdbool.h>
    #include "tim.h"
    
    // IR接收状态结构体
    typedef struct {
        uint16_t valueUp;        	 	// 上升沿对应的计数器值
        uint16_t valueDown;       		// 下降沿对应的计数器值
        bool isUpCapt;            		// 是否为上升沿捕获
        int width;           			// 下降沿与上升沿之间的差值(us单位)
        uint16_t buffer[128];     		// 存储数据
        uint16_t bufferId;        		// 存储数据数组下标
        bool rcvFlag;             		// 接收标志位
        uint8_t hexbuf[4];        		// 存储转换的16进制数据
    } IR_HandleTypeDef;
    
    IR_HandleTypeDef hIR;
    
    /* 
    *	微秒级延时函数
    *	该函数使用systick定时,在使用该函数前应该初始化systick 
    *	systick初始化函数详见本贴附录
    */
    static void delay_us(uint32_t us) {
        SysTick->LOAD = (SystemCoreClock / 1000000) * us - 1;
        SysTick->VAL = 0;
        while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0);
    }
    
    
    /** 
    * @biref 初始化红外
    * @note 初始化函数中对结构体进行赋初值,并开启TIM1的中断
    */
    void IR_Init(void) {
        hIR.valueUp = 0;
        hIR.valueDown = 0;
        hIR.isUpCapt = true;
        hIR.width = 0;
        hIR.bufferId = 0;
        hIR.rcvFlag = false;
    
    	/*开启定时器1中断*/
        HAL_TIM_Base_Start_IT(&htim1);
        HAL_TIM_IC_Start_IT(&htim1, TIM_CHANNEL_1);
    }
    
    /** 
    * @biref 红外接收函数
    * @note 接收红外信号并解析
    */
    uint8_t *IR_Receive(void) {
        uint8_t rcvData[32] = {0};
        hIR.rcvFlag = false;
    
        // 数据解码,低电平大于1000us的为逻辑1,小于1000us的为逻辑0
    	// 定时器10us捕获一次,故为100
        for (int i = 0; i < 32; i++) {
            rcvData[i] = hIR.buffer[hIR.bufferId + 1 + i] > 100 ? 1 : 0;
        }
    
        // 变换成4位16进制,并直接进行位序颠倒
        for (int i = 8; i <= 32; i += 8) {
            uint8_t temp = 0;
            // 将二进制数据转换为字节
            for (int j = 0; j < 8; j++) {
                temp |= (rcvData[i - 8 + j] << j);
            }
            hIR.hexbuf[i / 8 - 1] = temp;
        }
    
        //===== 打印解析后的数据  DEBUG ====//
        printf("IR:");
        for (int i = 0; i < 4; i++) {
            printf("%02X ", hIR.hexbuf[i]);
        }
        printf("\r\n");
    
        return hIR.hexbuf;
    }
    
    /**	输入捕获中断回调函数
    * @note 该函数用于在触发输入捕获中断后对数据进行解析
    * @note	使用该函数前,请确保定时器的中断捕获中断配置完成
    */ 
    void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
        if (hIR.isUpCapt) {
    		hIR.isUpCapt = false;
    		/*上升沿捕获时间点*/
            hIR.valueUp = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_ICPOLARITY_FALLING);
        } else {
    		hIR.isUpCapt = true;
    		/*下降沿捕获时间点*/
            hIR.valueDown = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
            __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);
    				
    		/* 低电平时长 */
            hIR.width = hIR.valueDown - hIR.valueUp;
    				
    		/* 阻塞中断等待高电平发送完成 */
    		delay_us(577);
    		
    		/*根据NEC协议,引导区为9ms高电平 + 4.5ms低电平,判断4.5ms低电平即可*/
            if (hIR.width > 440 && hIR.width < 460) {
                hIR.bufferId = 0;
                hIR.buffer[hIR.bufferId++] = hIR.width;
            } else if (hIR.bufferId && hIR.bufferId <= 32) {
    			//保存32位数据
                hIR.buffer[hIR.bufferId++] = hIR.width;
            }else if(hIR.width > 9000)
    		{	 // 重复接收,根据NEC协议计算,重复码末尾的低电平约为98.19ms,而前面的高电平为560us
    			// 所以可以判断是否接收到90ms以上的低电平来实现重复接收
    			hIR.rcvFlag = true;
    			hIR.bufferId = 33;
    		}
    		
    		// 数据接收完成
    		if (hIR.bufferId == 33) {
    			hIR.rcvFlag = true;
    			hIR.bufferId = 0;
             }
        }
    }
    
    // 使能红外接收
    void IR_Enable(void) {
        if (hIR.rcvFlag) {
            uint8_t *buf = IR_Receive();
            uint8_t data = 0;
    
            if (buf != NULL && IR_CheckData(buf, &data) == HAL_OK) {
                IR_TableCMD(data);
            }
        }
    }
    
    

    发送部分

    /*提醒: 如IR_HandleTypeDef结构体、延时函数等基本代码已在接收部分完成,故不在发送部分复写*/
    #define IR_SendBitHigh  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
    #define IR_SendBitLow  	HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
    /*
    * 红外发送
    *	引导码:9ms的高电平 + 4.5ms的低电平。
    *	用户码:8位数据,通常为0x00。
    *	数据码:8位数据,表示要发送的命令。
    *	反码:数据码的反码,用于校验。
    *	结束码:560µs的高电平。
    */
    
    
    // 发送NEC引导码
    void IR_SendLeader(void) {
        IR_SendBitHigh;       // 产生9ms的高电平
        delay_us(9000);
        IR_SendBitLow;       // 产生4.5ms的低电平
        delay_us(4500);
    }
    
    // 发送NEC数据位0/1
    void IR_SendBit(uint8_t bit) {
        if (bit) {                       //数据位1
            IR_SendBitHigh;     // 产生560us的高电平
            delay_us(560);
            IR_SendBitLow;     // 产生1680us的低电平
            delay_us(1680);
        } else {                         //数据位0
            IR_SendBitHigh;     // 产生560us的高电平
            delay_us(560);
            IR_SendBitLow;     // 产生560us的低电平
            delay_us(560);
        }
    }
    
    // NEC数据发送一个字节,从低位开始
    void IR_WriteByte(uint8_t byte) {
     
        for (uint8_t i = 0; i < 8; i++) {
            // 提取当前位的值
            uint8_t bit = (byte >> i) & 0x01;
            // 写入这一位
            IR_SendBit(bit);		
        }
    }
    
    // 发送NEC数据包
    void IR_Send(uint8_t data) {
    			/*打开PWM发送*/
    		HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);
        	IR_SendLeader(); 				// 发送引导码
    		IR_WriteByte(0x00);			// 发送地址码
    		IR_WriteByte(0xFF);			// 发送地址反码
    		IR_WriteByte(data);			// 发送命令码
    		IR_WriteByte(~data);		// 发送命令反码
    	
    		IR_SendBitHigh; 		// 发送结束码
    		delay_us(560);
    		IR_SendBitLow;
    		/*关闭PWM发送*/
    		HAL_TIM_PWM_Stop(&htim2,TIM_CHANNEL_1);
    }
    

    函数使用

    void mian()
    {
    	IR_Init(); /*初始化*/
    	/*发送红外数据 0x44*/
    	IR_Send(0x44);
    	while(1)
    	{
    		IR_Enable(); /*开启红外连续接收功能*/
    	}
    }
    

    现象

    接收现象

    随意找一个可以发送红外信号的遥控器或者装置,对准红外接收头(由于本文做的是近距离的红外通信,所以需要很久,但如果你的红外接收头能够接收较远距离的红外信号,可以适当拉远距离)

    串口助手获取信息
    这里的数据是十六进制的数据,其中,前两位是地址位(0x00)和地址反码(0xFF),后两位是数据位(0x44)和数据反码(0xBB)
    注意,此处的反码与数电中的原码反码不是一个意思,只是代表按位取反的意思

    发送现象

    拿一个红外转串口的模块来接收发送的红外信号

    串口调试助手现象

    根据该红外转串口模块商家描述,当该模块接收到信息后,只会通过串口输出 地址位+地址反码+数据位的信息帧。
    不过,具体的反馈信息看具体的模块商家给的模块说明。
    以上信息显示是正确的。

    以上便是红外发送和接收的硬件电路和实现方法,如有缺漏,请指出。

    参考文献

    NEC格式详解
    基于STM32f103c8t6的红外接收发送

    附录

    systick初始化

    /**
      * @brief 初始化SysTick定时器
      * @param 无
      * @retval 无
      */
    void SysTick_Init(void)
    {
      // 配置SysTick重装载值为系统时钟频率的1/1000000(1us)
      SysTick->LOAD = (SystemCoreClock / 1000000) - 1;
      // 清空当前计数器值
      SysTick->VAL = 0;
      // 配置SysTick时钟源为系统时钟,并启动计数器
      SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
    }
    

    作者:Asialing

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【STM32】NEC协议红外连续收发硬件电路设计与实现基于STM32平台

    发表回复