嵌入式通信协议解析(基于红外NEC通信协议)

发布于:2025-08-04 ⋅ 阅读:(17) ⋅ 点赞:(0)



摘要

对于红外按键数据的接受

一、基于红外NEC通信协议

![[Pasted image 20250523190241.png]]

使用定时器的输入捕获来实现,以NEC通信协议为基础进行分析,是一种调制信号,载波频率是38KHZ。

该数据包含:引导码、用户码、用户码、数据码、数据反码、停止位。一共32位,也就是4个字节。
用户码表示设备是什么厂商的。
数据码表示的是按键信息。

并且发送0和发送1的区别是:

如果是发送的数据是0,那么就需要表示0.56ms的高电平和周期是1.125ms。

如果是发送的数据是1,那么就表示需要0.56ms的高电平和周期是2.25ms。

正是基于上述的数据0和数据1的区别,所以必须要进行捕获该IO口的高低电平信号的时间,从而分辨出是数据0和数据1。

红外接收头内部的三极管电路具有信号反向的功能,也就是将1变为0,0变为1,因此:

数据0:是0.56ms的低电平和0.56ms的高电平,

数据1:是0.56ms的低电平和1.69ms的高电平,

引导码:的9ms是高电平变为低电平,4.5ms的低电平。

![[Pasted image 20250523190256.png]]

并且表示0和1电平,并不是简单的持续高低电平,是有一定的要求的,例如:0.56ms的低电平和0.56ms的高电平表示二进制0
0.56ms的低电平和1.69ms的高电平表示二进制1
这两个注意都是从低电平为基准,也就是说可以注意下降沿
因为这里面的信号都是连续的,那么假如现在发送的是二进制0,那么高电平结束以后,我肯定是低电平,因为高电平也是有时间限制的,如果超过这个时间,可能就不是二进制0了。同理在高电平的时间内,也是应该是多少就是多少,不然就不是对应的二进制,因此不存在低电平前面是低电平,除非这个通信发生错误,不然低电平前面一定是高电平,那么就一定有下降沿开始计时,这样这一次下降沿到下一次下降沿就是一个周期,但是我们还没有计时脉宽的时间,这个也需要记录,不然会导致内部的高低电平时序不标准也可能导致错误。

从数据0和数据1的信号格式可以看出,都是从低电平开始然后到高电平,只不过区别0和1的根本原因是高电平的持续时间不一样,因此我们在分析的时候,一定使用的是下降沿捕获, 因为高电平到低电平是结束的标志,也是通过高电平的时间来区分数据0和数据1,因此下降沿是合理且必须的

此外还有一个问题,我们如果把数据的解析不放在中断里面,使用主循环,那么假设主循环的时间是在ms级,由于红外的数据0和数据1解析也是在ms级可能会出现数据丢失的问题,因此我们需要使用FIFO缓冲区来处理采集到的时间基准点,从而避免数据丢失。

![[Pasted image 20250523193039.png]]

这个电路怎么理解呐,就是我通过单片机PC6连接这个1838红外传感头,这个红外接收头接收的是一系列高低电平,传感器发送的是有规律的发送(基于一定的协议,就是NEC协议),那么我怎么判断这些内容是什么,那就是根据协议,利用端口的输入捕获进行分析,通过计算高低电平时间已经下降沿的时候,进行判断。

为什么需要使用定时器,因为定时器可以提供一个时间概念,不然你怎么判断这些信号是什么样子,并且定时器配合中断,就可以给很多内容定制了基准。

因此我们需要利用定时器和中断来判断输入捕获:
大致可以理解为,TIME7是高级定时器,与我们的需求的功能在芯片内部已经实现产生联系。

首先我们通过基础配置,设置捕获的是下降沿,那么在开始的时候,我们首先进行计时,在第一次的下降沿进行一次清零操作(为什么是下降沿,很简单,我们的红外接收头的内部电路将红外发过来的信号进行了一次反转,因此只能以下降沿进行周期性性判断。)的时候开始计数,那么到第二次下降沿的时候,我们这个计数器已经计数了很多,那么我们就可以判断出这两次下降沿产生的间隔是多久。

这个地方深思的是,单片机IO端口收到红外接收头发送过来的下降沿,单片机内部产生一个中断标志位,读出计数器的值是多少,同时清空计数器。(读取通道捕获值) 为什么定时器7能实现这一段的描述,主要是因为芯片内部实现了这些电路。也就是IC工程师的能力。

关于数据解析的思路:
解析数据传入,读出数据

读数据使用循环队列。

一帧数据一般都是有头码的,
假设一帧数据的头码是错的,那么这一帧数据是没有意义的,但是对于发送方来说,他只会发,不管是否正确。而对于接收方来说需要保证这一帧数据是准确的,如果是错误的,可以不处理该帧。两种情况:
一种是没有解析到引导码,那么就直接不处理,那么后续还在发送,并且发送的时间不不满足帧头时间的要求。
一种是解析的引导码不对,那么对于接收方来说和第一种判断的本质是一样的,因为接收方只需要按照正确的方法解析,除开这种都是错的。

使用一个static bool HeadCodeFlag = false;静态变量,目的是为了引导解析正确以后,这一帧后续的过程都是正常解析的。(并且在解析完一帧(32个字节)后,我们还需要将这个标志位设置为默认错误状态,目的是继续下一帧判断。)

然后开始解析0和1,先进行数据1判断,这是因为1需要的时间长,所以我们先判断是否满足1,因为我们使用的是整个周期进行判断的,满足数据0的时间肯定是满足数据1的时间。所以先判断数据1,。(这里其实准确来说应该是使用高点平的时间进行判断。)

关于数据的存储,
红外在传输数据的时候是低位在前,高位在后。

在接受的时候我们是先接收低位,这是因为接收以后,直接就显式的数据,避免了先接收高位数据还需要颠倒。

低位优先,这是因为我们传进来一个低位,假如这个数据1是在最低位,那么经过8次右移最终就会移到最低位,这个地方就相当于是一个守恒,怎么理解呐?假如我现在是倒数第三位是1,那么此时就是还剩下五次填充,那么这个数据仍需要右移五次,而此时倒数第三位的1是在最高位,而右移五次就是到了他应该在的位置。这也因为3+5=8。右移的总次数是确定的,第几次进来的数据,最终确定的位置就是第几位(从最低位算),因为第几次进来的就意味着后续还需要进来(8-当前第几次),也就是右移多少次。
不管是数据0还是数据1的判断,都是需要进行右移的。

并且是先进行移位,为什么?肯定是为了腾位置啊,不然如果先赋值,在移位就相当于是第一次的值直接就放到了第二个位置,直接就导致数据错位了。

0x55
1 0 1 1 0 0 0 0

接收的时候我们先接受的是低位,然后是高位,因

原本是 0 0 0 0 0 0 0 0 

需要接收的数据是: 1 0 1 1 0 0 0 0
但是我们接收的顺序是: 0 0 0 0 1 1 0 1

前面的是四个0,因此接受完毕还是 0 0 0 0  0 0 0 0

>>1 || 0x80, 此时的数据变成 1 0 0 0  0 0 0 0 
又是一个1 因此又需要>>1 || 0x80, 此时变成 1 1 0 0  0 0 0 0 
此时是0,则变成 0 1 1 0  0 0 0 0
最后是一个1去,则变成>>1 || 0x80,  因此最终是  1 0 1 1   0 0 0 0

0单纯的右移,
1右移以后还需要和 0x80 进行或。

g_irCode[s_index / 8]   针对的是存在数组的哪一个位置,可以想象成这个数组需要存32位,那么一个位置就是8位,那么就转换为对应的数组的索引值。这些都是技巧啊。变成的技巧。

g_irCode[s_index / 8] 针对的是存在数组的哪一个位置,可以想象成这个数组需要存32位,那么一个位置就是8位,那么就转换为对应的数组的索引值。这些都是技巧啊。变成的技巧。

static void ParseIrFrame(uint32_t tickNum)
{
	//再一次体现了static关键字的伟大之处。2025年5月23日20:37:09
	static bool s_headFlag = false;    //是否解析到引导码
	static uint8_t s_index = 0;
	
	if(tickNum > TICK_HEAD_MIN && tickNum < TICK_HEAD_MAX)
	{
		s_headFlag = true;
		return;
	}
	else{}
	
	if(!s_headFlag)
	{
		return;
	}
	if(tickNum > TICK_1_MIN && tickNum < TICK_1_MAX)
	{
		g_irCode[s_index / 8] >>= 1;
		g_irCode[s_index / 8] |= 0x80;
		s_index++;
	}
	if(tickNum > TICK_0_MIN && tickNum < TICK_0_MAX)
	{
		g_irCode[s_index / 8] >>= 1;
		s_index++;
	}
	
	if(s_index == 32)
	{
		if ((g_irCode[2] & g_irCode[3]) == 0)   //(g_irCode[2] == (uint8_t)~g_irCode[3])
		{
			g_irCodeFlag = true;
		}
		else
		{
			g_irCodeFlag = false;
		}
		s_headFlag = false;
		s_index = 0;
	}
}

上述代码的感悟:
主要分为
1、针对引导码的判断
2、针对二进制1的判断
3、针对二进制0的判断
4、如何将位转换为数组的索引值,
5、如何进行接收二进制数的表达,以上是针对先接收低位在接收高位的情况。
6、数据校验,

		if ((g_irCode[2] & g_irCode[3]) == 0)   //(g_irCode[2] == (uint8_t)~g_irCode[3])
		{
			g_irCodeFlag = true;
		}
		else
		{
			g_irCodeFlag = false;
		}

也就是红外传递的数据,一个是数据码,一个是数据反码,用于校验的。直接当场校验,其实不建议在中断里面这么操作,容易产生中断不准确。

还有一个需要注意的是

虽然 g_irCode[3] 是unit8位,但是取反以后变成了32位,这个地方容易是一个陷阱,一定要注意。不要轻易的使用取反,要慎重,慎之又慎。这个地方是跟MDK有关系。

7、在进行初始化,迎接下一次的接收。

		s_headFlag = false;
		s_index = 0;

以上这些思想,一定要深深体会,只有体会了,才能融会贯通。

一个重要的地方思想:就是一个.c文件一定有一个对外暴露的接口函数,

对多字节键值数组采用双缓冲区或关中断保护,对于红外按键,测试一下是否有问题,如果有问题再优化。

![[Pasted image 20250803162806.png]]

当一个键按下超过36ms,振荡器使芯片激活,将发射一组108ms 的编码脉冲,这 108ms发射代码由一个引导码(9ms),一个结果码(4.5ms),低8位地址码(9ms~18ms), 高8 位地址码9ms~18ms),8 位数据码(9ms~18ms)和这8 位数据的反码(9ms~18ms)组成。如果键按下超过108ms 仍未松开,接下来发射的代码(连发码) 将仅由起始码(9ms)和结束码(2.25ms)组成。

并且触发连按的键值是108ms。

![[Pasted image 20250803164255.png]]

这是单机发送的数据,

并且可以看到在这个里面已经进行了消抖处理,我们在接受以后,不需要消抖处理。只需要根据收到的数据确定是连续按下,还是单击按键

主循环一直读,写的太慢了。
并且不是一直写,而是有中断才会写数据,程序就会认为是空的。

/******************** (C) COPYRIGHT 2025 jimmy ***********************************
  * @file      
  * @describe  
  * @platform  
  * @version   V1.0.0
  * @author    
  * @date      
**********************************************************************************/
#include "stdlib.h"
#include "stdint.h"
#include <string.h>
#include <stdbool.h>
#include "math.h"

#include "bsp_gpio.h"
#include "HDL_IR_Ctrl.h"

#if IRCTRL_FIFO_EN == 1
	volatile static IRCTRL_T  g_IRCtrl;
	volatile static uint32_t  g_TxBuf[IRCTRL_TX_BUF_SIZE];		/* 发送缓冲区 */
	volatile static uint32_t  g_RxBuf[IRCTRL_RX_BUF_SIZE];		/* 接收缓冲区 */
#endif
	
static uint8_t g_IRCode[4]; 																/* 将获取的一帧红外数据存入该数据 */
static bool g_IRCodeFlag = false;														/* 读取红外键值错误 */
static uint8_t IRCtrl_Value = 0;

/**************************************************
*函数名称:
*函数功能:初始化串口相关的变量
*入口参数:
*出口参数:
*运行时基:
**************************************************/
static void IRCtrl_Init(void)
{
#if(IRCTRL_FIFO_EN == 1)
	g_IRCtrl.com = COM0;					 									/* STM32 串口设备 */
	g_IRCtrl.rxwrite = 0;														/* 接收FIFO写索引 */
	g_IRCtrl.rxread = 0;														/* 接收FIFO读索引 */
	g_IRCtrl.rxcount = 0;														/* 接收到的新数据个数 */
	g_IRCtrl.rxbuf = g_RxBuf;												/* 接收缓冲区指针 */	
	g_IRCtrl.rxsize = IRCTRL_RX_BUF_SIZE;						/* 接收缓冲区大小 */	

	g_IRCtrl.txwrite = 0;														/* 发送FIFO写索引 */
	g_IRCtrl.txread = 0;														/* 发送FIFO读索引 */	
	g_IRCtrl.txBuf = g_TxBuf;												/* 发送缓冲区指针 */
	g_IRCtrl.txsize = IRCTRL_TX_BUF_SIZE;						/* 发送缓冲区大小 */
	g_IRCtrl.txcount = 0;														/* 待发送的数据个数 */
	
	g_IRCtrl.SendBefor = 0;													/* 发送数据前的回调函数 */
	g_IRCtrl.SendOver = 0;													/* 发送完毕后的回调函数 */
	g_IRCtrl.ReciveNew = 0;													/* 接收到新数据后的回调函数 */
#endif
}

/**************************************************
*函数名称:
*函数功能:将数据放入缓冲区
*入口参数:
*出口参数:
*运行时基:
**************************************************/
static void IRCtrlFIFO_Push(volatile IRCTRL_T *pIRCtrl, uint32_t data)
{
	uint32_t index = (pIRCtrl->rxwrite + 1) % pIRCtrl->rxsize;
	if(index == pIRCtrl->rxread){
		return;
	}																	
	pIRCtrl->rxbuf[pIRCtrl->rxwrite] = data;   			/* 从串口接收数据寄存器读取数据存放到接收FIFO */
	pIRCtrl->rxwrite = index;
}

/**************************************************
*函数名称:
*函数功能:从缓冲区读取数据
*入口参数:
*出口参数:
*运行时基:
**************************************************/
static void IRCtrlFIFO_Pop(volatile IRCTRL_T *pIRCtrl, uint32_t *pdata)
{
	if(pIRCtrl->rxread == pIRCtrl->rxwrite){
		return;
	}																	
	*pdata = pIRCtrl->rxbuf[pIRCtrl->rxread];   			/* 从串口接收数据寄存器读取数据存放到接收FIFO */
	pIRCtrl->rxread = (pIRCtrl->rxread + 1) % pIRCtrl->rxsize;
}

/**************************************************
*函数名称:
*函数功能:解析数据,被主循环调用
*入口参数:
*出口参数:
*运行时基:
**************************************************/
static void IRCtrl_Analyse(void)
{
	static bool HeadCodeFlag = false;
	static uint8_t index = 0;
	uint32_t TickBuf = 0;
	IRCtrlFIFO_Pop(&g_IRCtrl, &TickBuf);
	if(TickBuf > TICK_HEAD_MIN && TickBuf < TICK_HEAD_MAX){
		HeadCodeFlag = true;
		return;
	}
	if(!HeadCodeFlag){
		return;
	}	
	if(TickBuf > TICK_1_MIN && TickBuf < TICK_1_MAX){
		g_IRCode[index/8] >>= 1;
		g_IRCode[index/8] |= 0x80;
		index++;
	}
	if(TickBuf > TICK_0_MIN && TickBuf < TICK_0_MAX){
		g_IRCode[index/8] >>=1;
		index++;
	}
	if(index == IRCTRL_DATA_SIZE){
		if((g_IRCode[2] & g_IRCode[3]) == 0){
			g_IRCodeFlag = true;
		} else {
			g_IRCodeFlag = false;
		}
		HeadCodeFlag = false;
		index = 0;
	}
	Get_IRCtrl_Value(&g_IRCode[2]);	
}
/**************************************************
*函数名称:
*函数功能:将键值转换为我们想要的键值
*入口参数:
*出口参数:
*运行时基:
**************************************************/
void Get_IRCtrl_Value(uint8_t* pdata)
{
	uint8_t	Value =0;
	if(!g_IRCodeFlag){
		return;
	}		
	if(*(pdata) == 0x0C){
		Value |= 0x01;
	}	
	if(*(pdata) == 0x18){
		Value |= 0x02;   
	}	
	if(*(pdata) == 0x5E){
		Value |= 0x04;   
	}
	g_IRCodeFlag = false;
	IRCtrl_Value = Value;
}

/**************************************************
*函数名称:
*函数功能:将红外按键的键值传递给FML按键处理文件
*入口参数:
*出口参数:
*运行时基:
**************************************************/
uint8_t Read_IRCtrl_Value(void)
{
	return IRCtrl_Value;
}

/**************************************************
*函数名称:
*函数功能:接受红外传感器数据
*入口参数:
*出口参数:
*运行时基:
**************************************************/
void IRCtrl_Rec(uint32_t data)
{
	IRCtrlFIFO_Push(&g_IRCtrl, data);
}

/**************************************************
*函数名称:
*函数功能:初始化FIFO数据
*入口参数:
*出口参数:
*运行时基:
**************************************************/
void HDL_RICtrl_Init(void)
{
	IRCtrl_Init();																	/* 必须先初始化全局变量 */
}

/**************************************************
*函数名称:
*函数功能:主循环调用,根据脉宽长度分析红外按键键值信息
*入口参数:
*出口参数:
*运行时基:
**************************************************/
void HDL_IRCtrl_Process(void)
{
	IRCtrl_Analyse();
}


如果觉得我的内容对您有帮助,希望不要吝啬您的赞和关注,您的赞和关注是我更新优质内容的最大动力。



专栏介绍

《嵌入式通信协议解析专栏》
《PID算法专栏》
《C语言指针专栏》
《单片机嵌入式软件相关知识》
《FreeRTOS源码理解专栏》
《嵌入式软件分层架构的设计原理与实践验证》



文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。


网站公告

今日签到

点亮在社区的每一天
去签到