目录
1. 红外遥控简介
2. NEC协议
3. 硬件设计
4. 实验程序详解
4.1 main.c
4.2 Remote.c
4.3 Remote.h
1. 红外遥控简介
红外遥控是一种无线、非接触的控制技术。具有抗干扰能力强,信息传输可靠,功耗低,成本低,易实现等优点。被广泛的应用于家用电器,越来越多的被使用到计算机系统中。
红外遥控不像无限电遥控那样,可以穿过障碍物去控制被控的对象。所以,在设计红外遥控时,不需要每套(发射器和接收器)有不同的遥控频率或编码(防止遥控隔墙控制邻居的家用电器),因此,同类的红外遥控设备,可以使用相同的遥控频率和编码,不会出现遥控信号 “串门” 的情况。并且红外光为不可见光,对环境影响很小。红外光的波长远小于无线电波的波长,所以红外遥控不会影响家中其他电器。
红外遥控的编码目前广泛使用的是:NEC Protocol 的 PWM 脉冲宽度调制和 philips RC-5 Protocol 的 PPM 脉冲位置调制
STM32F4的开发板使用的是 NEC 协议。
2. NEC协议
基本发送和接收:
空闲状态:红外LED不亮,接收头输出高电平
发送低电平:红外LED以 38 KHz频率闪烁发光,接收头输出低电平
发送高电平:红外LED不亮,接收头输出高电平
注:38KHz是最底层的频率,在NEC协议编码过程中不会体现;
NEC协议产生的波形(也可以说是我们遥控器按下时,发出的波形),如下图:
在遥控器没有按下时,处于空闲状态,默认为高电平。
下图中红色的波形,表示Start起始信号。Start信号由9ms的下降沿和4.5ms的上升沿组成。
下图中蓝色的波形,表示遥控按键发送的数据Data。Data信号由560us的下降沿和560us的上升沿或者560us的下降沿和1690us的上升沿组成。其中560us的下降沿和560us的上升沿表示逻辑0;560us的下降沿和1690us的上升沿表示逻辑1;
Data一般是32位的。
Data的格式是:地址码Address+地址反码+Command命令+命令反码。其中每部分都是8位,总共32位。
其中地址码Address是遥控器的标识符(防止不同品牌的遥控器互相用);反码是用来验证的,判断有没有发错。命令码就是界码,表示遥控器按下哪个按键。
下图中绿色的波形,表示按下不放时(类似于电视遥控换台,换台键按住不放,则电视频道一直++)。Repeat信号由9ms下降沿和2.25ms上升沿组成。
NEC协议的特点:
1. 8位地址和8位指令长度
2. 地址和命令2次传输(确保可靠性)
3. PWM脉冲宽度调制,以发射红外载波的占空比代表 “0” 和 “1”
4. 载波频率为38KHz
5. 位时间为1.125ms或2.25ms
遥控接收头在收到脉冲的时候为低电平,在没有脉冲的时候为高电平。
前面已经提及了NEC的数据格式,这里总结一下:
起始码、地址码、地址反码、控制码、控制反码。起始码由一个 9ms 的低电平和一个 4.5ms 的高电平组成,地址码、地址反码、控制码、控制反码均是 8 位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可用于校验)。
NEC规定的连发码:
上图中绿色部分Repeat由9ms低电平+2.5ms高电平+0.56ms低电平+97.94高电平(这里的0.56ms低电平+97.94高电平对应于上图总的连发码110ms)组成。如果一帧数据发送完毕以后,按键仍然没有放开,则发送重复码,也就是连发码。
我们可以通过统计连发码的次数来标记按键按下的长短/次数。
3. 硬件设计
红外接收头连接在MCU引脚的PA8上。
注意:REMOTE_IN 和 DCMI_XCLK共用了PA8,所以他们不可以同时使用。
4. 实验程序详解
实验现象:
本实验采用定时器的输入捕获功能实现红外解码。
首先开机在LCD上显示一些信息之后,等待红外触发,如果接收正确的红外信号,解码。并且在LCD上显示键值和所代表的意义,以及按键的次数(主要是按键不放导致Repeat)
4.1 main.c
#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "MyI2C.h"
#include "AT24C02.h"
#include "Remote.h"
//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
led_set(sta);
}
int main(void)
{
u8 key;
u8 t=0;
u8 *str=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168);
uart_init(115200);
LED_Init();
LCD_Init();
Remote_Init();
POINT_COLOR=RED;
LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
LCD_ShowString(30,70,200,16,16,"REMOTE Test");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2023/06/21");
LCD_ShowString(30,130,200,16,16,"KEYVAL:");
LCD_ShowString(30,150,200,16,16,"KEYCNT:");
LCD_ShowString(30,170,200,16,16,"SYMBOL:");
while(1)
{
key=Remote_Scan(); //获得按键返回值,Remote_Scan()错误的情况下返回0,其余情况返回键值
if(key)
{
LCD_ShowNum(30+7*8,130,key,3,16); //显示键值
LCD_ShowNum(30+7*8,150,RemoteCNT,3,16); //显示按键次数
//RemoteCNT记录按键按下的次数,如果按键一直按下,那么发完一个命令以后,DownTimerCount>2200&&DownTimerCount<2600
//在绿色区域内重复,所以对应的 RemoteCNT++;
switch(key)
{
case 0:str="ERROR";break;
case 162:str="POWER";break;
case 98:str="UP";break;
case 2:str="PLAY";break;
case 226:str="ALIENTEK";break;
case 194:str="RIGHT";break;
case 34:str="LEFT";break;
case 224:str="VOL-";break;
case 168:str="DOWN";break;
case 144:str="VOL+";break;
case 104:str="1";break;
case 152:str="2";break;
case 176:str="3";break;
case 48:str="4";break;
case 24:str="5";break;
case 122:str="6";break;
case 16:str="7";break;
case 56:str="8";break;
case 90:str="9";break;
case 66:str="0";break;
case 82:str="DELETE";break;
}
LCD_Fill(86,170,116+8*8,170+16,WHITE); //清除范围内的数据,x 86~116+8*8 ; y 170~170+16
LCD_ShowString(30+7*8,170,200,16,16,str);
}
else
{
delay_ms(10);
}
t++;
if(t==20)
{
t=0;
LED0=!LED0;
}
}
}
4.2 Remote.c
#include "stm32f4xx.h"
#include "Remote.h"
#include "usart.h"
//红外遥控初始化
//设置IO引脚及TIM1_CH1的输入捕获
//红外接收头引脚接到PA8,PA8引脚复用TIM1_CH1,定时器1通道1
void Remote_Init(void)
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE); //使能TIM1时钟
//引脚初始化
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF; //引脚复用为定时器1
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8; //PA8
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource8,GPIO_AF_TIM1); //PA8复用为TIM1
//定时器1初始化
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period=10000; //设定计数器自动重装载值,10ms溢出,计数器从0开始计数,1ms计数1000,那么10ms计数值到10000,也就是自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=167; //预分频值,167+1/168=1M的计数频率,1us+1
TIM_TimeBaseInit(TIM1,&TIM_TimeBaseInitStructure);
//初始化定时器输入捕获
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1; //定时器1
TIM_ICInitStructure.TIM_ICFilter=0x03; //是否滤波
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising; //上升沿捕获
TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1; //配置输入不分频
TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI; //映射到TI1上
TIM_ICInit(TIM1,&TIM_ICInitStructure);
TIM_ITConfig(TIM1,TIM_IT_Update|TIM_IT_CC1,ENABLE); //使能更新和捕获
TIM_Cmd(TIM1,ENABLE); //开启定时器1中断
NVIC_InitTypeDef NVIC_InitStructure; //TIM1是高级定时器,有两个中断服务函数
NVIC_InitStructure.NVIC_IRQChannel=TIM1_UP_TIM10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority=2; //子优先级2
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel=TIM1_CC_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=3;
NVIC_Init(&NVIC_InitStructure);
//开启定时器1输入捕获中断和更新中断。捕获到上升沿,产生捕获中断;当定时器计数溢出时,产生更新中断;
}
//在写利用定时器TIM1的通道1输入捕获遥控器的信号时,首先需要了解NEC协议到底是怎么传输的,NEC协议和IIC、SPI是完全不同的
//这里我说的不同是针对于最显著的特点不同:IIC、SPI时序均是通过时序的上升沿、下降沿来发送数据的,只需要特定时间设置上升沿、下降沿就可以写时序图对应的程序,更精确地说高低电平就可以表示逻辑上的1和0
//NEC协议却不是这样,NEC协议的逻辑1和0是通过一段时间的下降沿 + 一段时间的上升沿来描述的,不单单是如此,NEC协议的起始信号和数据信号以及重复信号Repeat都是通过一段时间的上升沿 + 一段时间的下降沿来描述的
//所以首先需要明确如何判断这段信号表示逻辑0还是逻辑1;也就是如何判断上升沿和下降分别持续的时间,进而判断到底代表逻辑1还是逻辑0
//*******************************************************************************
//定时器的输入捕获功能是可以得到时序高低电平的持续时间的,这个我们在之前已经学习过
//这里就是通过定时器的输入捕获功能获取高低电平的持续时间,通过电平持续的时间来判断到底是逻辑1还是逻辑0;NEC协议最根本的思路就是如此
//*******************************************************************************
//通过观察NEC协议的编码说明图可以发现:逻辑1和逻辑0的低电平持续时间都是560us;而高电平的持续时间是不同的,因此可以输入捕获计算高电平的持续时间,进而判断遥控发出的信号数据代表逻辑1还是逻辑0
//遥控器接收的状态
//[7]:收到了引导码标志,引导码其实就是NEC协议上的起始码
//[6]:得到了一个按键的所有信息
//[5]:保留
//[4]:标记上升沿是否已经被捕获
//[3:0]:溢出计时器
u8 RemoteReceiveState=0; //定义的变量RemoteReceiveState实际上是一个8位寄存器,其各个位如上所示,该变量代表遥控器接收的状态
u16 DownTimerCount; //下降沿时计数器的值; 因为要计算高电平的持续时间,所以下降沿一定是在上升沿之后(当然,是在提前已经捕获,就意味着上一个一定是上升沿)
//上升沿计数器开始计时,下降沿得到计数器的值,就意味着得到了高电平的持续时间
u32 RemoteReceiveData=0; //红外接收到的数据
u8 RemoteCNT=0; //按键按下的次数
//定时器TIM1溢出中断
void TIM1_UP_TIM10_IRQHandler(void)
{
//首先,进入该中断还是先判断中断状态位,然后清除中断状态位,为下一次判断状态位做准备
//接着,明确溢出中断的思路:
//之所以进入溢出中断,是因为初始化中断时,设置的自动重装载值是10000,设置的预分频值是167,可以计算出时钟频率为1M,那么溢出的周期就是10ms
//进入中断就意味着计数值++,来到了10000,也可以说整个时序持续了10us,进入了溢出中断
//这里首先需要知道:定时器溢出的周期是10ms,输入捕获设置的是上升沿捕获,一旦捕获上升沿就会进入输入捕获中断服务函数,判断是上升沿就会清空计数器
//清空计数器之后,计数器自然不会++,来到10000。
//说这么多想表达的意思就是:只要进入溢出中断,那么就意味着10ms内都没有输入捕获来清空计数器的值,也就是输入信号没有发生变化,说明10ms没有接收红外信号了,因此可以判断为接收结束
if(TIM_GetITStatus(TIM1,TIM_IT_Update)==SET) //判断溢出中断标志位
{
if(RemoteReceiveState&0X80) //表示收到了引导码,也就是收到了起始码
//这里需要明白,起始码之后的时序(不算上连发码)加在一起也没有10ms,是不会进入溢出中断的
//一旦进入了溢出中断,表示一个时序一定是进入了连发阶段,数据时序+连发阶段时间是远远大于10ms的
//也就表示已经有数据被接收到了,现在正在执行该数据的连发
{
RemoteReceiveState&=~0X10; //0001 0000根据优先级先取反,1110 1111按位与表示将RemoteReceiveState的第4位置0
//取消上升沿已经被捕获的标志,为下一次捕获上升沿做准备
if((RemoteReceiveState&0X0F)==0X00) //低四位全为0,表示刚刚溢出,也就是计数周期刚好到10ms的临界值,这个时候一个按键的所有信息肯定已经被接收完了
{
RemoteReceiveState|=1<<6; //标记已经得到了一个按键的所有信息
}
if((RemoteReceiveState&0X0F)<14) //取出溢出计数器的值,离开循环的条件是寄存器低4位为13
{
RemoteReceiveState++;
}
else //寄存器低4位值大于13,发一个数据近似于10ms(一定是不到10ms的),连发13次数据一定是超过了连发码的时序周期,所以清空相关参数,为下次做准备
{
//收到的连发码已经结束,清空相关参数
RemoteReceiveState&=~(1<<7); //清空引导标识
RemoteReceiveState&=0XF0; //清空计数器
}
}
}
TIM_ClearITPendingBit(TIM1,TIM_IT_Update); //清除中断标志位,为下次中断判断做准备
}
//定时器TIM1输入捕获中断服务函数
void TIM1_CC_IRQHandler(void)
{
if(TIM_GetITStatus(TIM1,TIM_IT_CC1)==SET) //获取中断输入捕获的状态位
{
//进入中断服务函数的思路是:获得高电平的时间,进行比较,究竟是代表逻辑1还是代表逻辑0
//获得的思路是:初始化设置上升沿捕获,上升沿捕获进入中断服务函数,也就是进入该函数,立刻设置下降沿捕获,一方面捕获本次高电平,另一方面为下次捕获下降沿做准备
//之所以准备捕获下降沿是因为:上升沿设置计时,下降沿的时候得到计数器的值,也就意味着得到了整个高电平的持续时间,期间的状态位设置均通过RemoteReceiveState设置相关位即可
if(Remode_DATA) //红外输入引脚为高电平,上升沿捕获
{
TIM_OC1PolarityConfig(TIM1,TIM_ICPolarity_Falling); //调用配置TIM1通道1极性函数,在收到上升沿捕获的同时,立刻设置下降沿捕获,为下次捕获下降沿做准备,同时也是为了捕获本次的高电平
TIM_SetCounter(TIM1,0); //定时器清0,表示计数器开始计数
RemoteReceiveState|=0x10;//设置RemoteReceiveState的第4位为1,表示上升沿已经被捕获
}
else //否则红外输入引脚为低电平,下升沿捕获
{
DownTimerCount=TIM_GetCapture1(TIM1); //获得TIM1计数器的值给到变量,表示下降沿时计数器的值,也就意味着得到了高电平的时间
TIM_OC1PolarityConfig(TIM1,TIM_ICPolarity_Rising); //设置上升沿捕获,为下次捕获上升沿做准备
//接下来就需要对得到的计数器值进行逻辑判断
if(RemoteReceiveState&0x10) //得到计数器的第四位,也就是高电平是否已经被捕获,这里是进行判断的逻辑,就是捕获下降沿得到计数器值的同时,判断上一个时序是不是上升沿
//如果上一个时序不是上升沿,那么两个下降沿之间的计数器值也就不能代表高电平的持续时间
//如果上一个时序是上升沿,那么得到的计数器值DownTimerCount才是有效的
{
if(RemoteReceiveState&0x80)//得到最高位,表示收到了引导码,已经完成了NEC的Start步骤
{
//接下来根据高电平的时间判断究竟是逻辑1还是逻辑0
//该判断语句需要结合NEC协议的时序图上的上升沿和下降沿的时间
if(DownTimerCount>300&&DownTimerCount<800) //逻辑0的高电平持续时间为560us,表示逻辑0
{
//该程序左移的操作建立在C语言补码的形式之上
RemoteReceiveData=RemoteReceiveData<<1;//NEC协议发送数据是低位在前,高位在后;
//左移一位,右侧低位补0
RemoteReceiveData=RemoteReceiveData|0;//将数据的右侧最低位置0
//上述两句程序的意思是倘若检测出为低电平0,则将RemoteReceiveData左移一位,将最低位置0
//因为是低位在前,所以如此循环,就能保证第一次检测到的一直左移到最高位;
}
else if(DownTimerCount>1400&&DownTimerCount<1800) //逻辑1的高电平持续时间为1690us,表示逻辑1
{
RemoteReceiveData=RemoteReceiveData<<1;
RemoteReceiveData=RemoteReceiveData|1; //同理还是左移一位,右侧补0,补0的那一位置1
}
else if(DownTimerCount>2200&&DownTimerCount<2600) //绿色重复的高电平时间2.25ms=2250us
{
RemoteCNT++; //进入重复区间,进来一次,按键++
RemoteReceiveState&=0xF0; //清空计数器,因为进入该Repeat区间也就象征着NEC协议结束了,清空计数器,为下位数据传输做准备
}
}
else if(DownTimerCount>4200&&DownTimerCount<4700) //表示来到起始信号的高电平持续时间 4.5ms=4500us
{
RemoteReceiveState|=1<<7;//进入NEC协议的起始阶段,意味着收到了引导码,所以需要将状态位设置为1
//理解这个代码首先需要知道C语言中操作符的优先级
//在C语言中,取反操作符>左移/右移>与>或
//代码中先计算1<<7位,再进行或操作运算
RemoteCNT=0; //起始阶段一定没有按键按下,将上一次传输时的按键值清0
}
}
RemoteReceiveState&=~(1<<4); //该代码的意义是:设置状态位4=0;
//因为上一个高电平已经被处理了,紧接着设置高电平还没有被捕获,为下一次捕获的状态位判断做准备
}
}
TIM_ClearITPendingBit(TIM1,TIM_IT_CC1); //清除中断捕获状态标志位,为下一次判断中断标志位做准备
}
//处理红外键盘
//返回值:
// 0,没有任何按键按下
//其他,按下的按键键值.
u8 Remote_Scan(void)
{
u8 sta=0; //记录键值
u8 t1,t2;
if(RemoteReceiveState&(1<<6)) //判断状态位,如果已经得到一个按键的所有信息了
{
//数据格式是:地址码+地址反码+命令码+命令反码
//RemoteReceiveData是红外接收到的数据,32位
t1=RemoteReceiveData>>24; //得到地址码 高8位
t2=(RemoteReceiveData>>16)&0xFF; //得到地址反码 17~24位,右移16位得到高16位,和0xFF按位与,表示取出低8位
if((t1==(u8)~t2)&&t1==REMOTE_ID) //校验遥控识别码及地址
{
//地址码校验没问题后,取出命令码和命令反码,进行校验;
//其中命令码就是键值对应的信息
t1=RemoteReceiveData>>8; //t1定义的是8位,RemoteReceiveData>>8表示的是第9位~32位,t1是8位的变量,所以赋值的时候只能得到低8位,也就是第9~16位,也就得到了命令码
t2=RemoteReceiveData; //直接赋值,得到的就是低8位,命令反码
if(t1==(u8)~t2)
{
sta=t1; //如果校验没问题,就将命令码t1赋值给sta,以便于返回该值,得到键值
}
}
if((sta==0)||((RemoteReceiveState&0X80)==0)) // 按键数据错误/遥控已经没有按下了
{
RemoteReceiveState&=~(1<<6); //清除接收到有效按键标识
RemoteCNT=0; //清除按键次数计数器
}
}
return sta;
}
4.3 Remote.h
#ifndef _INFRAREDREMOTECONTROL__H_
#define _INFRAREDREMOTECONTROL__H_
#include "sys.h"
#define Remode_DATA PAin(8) //红外数据输入脚
#define REMOTE_ID 0 //红外遥控识别码(ID),每款遥控器的该值基本都不一样,但也有一样的.
//我们选用的遥控器识别码为0
extern u8 RemoteCNT; //按键按下的次数
void Remote_Init(void);
void TIM1_UP_TIM10_IRQHandler(void);
void TIM1_CC_IRQHandler(void);
u8 Remote_Scan(void);
#endif
该实验代码的每一步都进行了详细的解释。如果对代码还有疑问或者哪里解释的不对,欢迎评论改正!!!