NEC协议
红外遥控的编码目前广泛使用的是:NEC Protocol
的 PWM
(脉冲宽度调制)和 PhilipsRC-5 Protocol
的 PPM
(脉冲位置调制),下面是 NEC
协议的特征:
- 8 位地址和 8 位指令长度(控制码);
- 地址和命令 2 次传输(确保可靠性)
- PWM 脉冲位置调制,以发射红外载波的占空比代表 “0” 和 “1” ;
- 载波频率为
38Khz
; - 位时间为
1.125ms
或2.25ms
;
NEC码的位定义:
- 一个脉冲对应
560us
的连续载波 - 一个逻辑
1
传输需要2.25ms
(560us
脉冲+1680us
低电平=2240us) - 一个逻辑
0
的传输需要1.125ms
(560us
脉冲+560us
低电平=1120us)
红外接收头端的信号为:
- 收到脉冲的时候为低电平,在没有脉冲的时候为高电平(即与发出的NEC码反向)
- 逻辑
1
( 560us 低+1680us 高) - 逻辑
0
( 560us 低+560us 高)
NEC 遥控指令的数据格式为:同步码头、地址码、地址反码、控制码、控制反码。从红外接收头端的角度来看:
- 同步码:由一个
9ms
的低电平和一个4.5ms
的高电平组成 - 地址码、地址反码:8位数据,遥控器的地址码要与红外接收头端的地址码要对应?
- 控制码、控制反码:8位数据,键值相关
- 按照低位在前,高位在后的顺序发送
- 采用反码是为了增加传输的可靠性(可用于校验)
以正点原子的STM32开发板为例,配带的遥控器的地址码为0x00:
- 地址码为0
- 控制码为0x15(从接收端来看)
- 连发码(由
9ms
低电平+2.5m
高电平+0.56ms
低电平+97.94ms
高电平组成=110ms
),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码,即连发码,可以通过统计连发码的次数来标记按键按下的长短/次数。
项目实践中的NEC码
一般给出的码值表是从发送端的角度来看的,而客户码实际上就是地址码+地址反码这一项(不一定构成反码),也就是说如果从接收端接收到的波形来看,83F4对应的是C12F(先发低位,再发高位);OK键15对应A8
对应原理图
对应的原理图如下:
软件设计思路一
仿正点原子的做法,该方式不适合要做低功耗模式的需求:
- 开启定时器对应通道输入捕获功能,默认上升沿捕获。定时器的计数频率为
1MHz
,自动装载值为10000
,也就是溢出时间为10ms
。 - 开启定时器 输入捕获中断 和 溢出/更新中断,当捕获到上升沿产生捕获中断,当定时器计数溢出,产生更新中断 。
- 当捕获到上升沿的时候,设置捕获极性为下降沿捕获(为下次捕获下降沿做准备),然后设置定时器计数值为0(清空定时器),同时设置变量RmtSta的为4值为1,标记已经捕获到上升沿
- 当捕获到下降沿的时候,读取定时器的值赋给变量Dval,然后设置捕获极性为上升沿捕获(为下次捕获上升沿做准备),同时对变量RmtSta的位4进行判断:如果RmtSta位4为1,说明之前已经捕获到过上升沿,那么对Dval进行判断,
300-800(560)
之间,说明接收到的是数据0;1400-1800(1680)
之间说明接收到的数据为1;2200-2600(2500)
,说明是连发码;4200-4700(4500)
说明为同步码;分析后甚至相应的标志位 - 如果是定时器发生溢出中断,那么分析,如果之前接收到了同步码,并且是第一次溢出,标记完成一次按键信息采集(有两种情况,情况1:接收完码后一直处于高电平后产生溢出;情况2:处于连发码中
97.94ms
高电平这段时间产生溢出,正点原子例程用是否>130ms
来判断是处于两种情况中的哪一种) - 接收码值的时候采用的是左移的方式,跟接收端看到的波形数值是一致的,所以是遥控器码值的反码
用 定时器捕获 + 外部中断(低功耗) 的方式时,会产生问题,猜测是在软件优先级一样的情况下,硬件优先级(根据中断向量表判断)高的外部中断常常打断了定时器的捕获,所以造成了有时解不了码的问题。
remote.h代码
#ifndef _REMOTE_H_
#define _REMOTE_H_
#include "stm8s.h"
#define REMOTE_IDH 0x0C
#define REMOTE_IDL 0xF3
extern u8 RmtCnt;//按键按下的次数
void Clk_Init(void);
void Remote_Init(void);
u8 Remote_Scan(void);
#endif
remote.c代码
#include "remote.h"
//选用16MHz的HSI作为主时钟源
void Clk_Init(void){
CLK_DeInit();//设置为默认值
CLK_HSICmd(ENABLE);//启用HSI
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);//HSI分频
CLK_SYSCLKConfig(CLK_PRESCALER_CPUDIV1);//CPU分频
}
//初始化PD3为浮空输入
void IR_GPIO_Init(void){
GPIO_DeInit(GPIOD);//恢复指定端口
GPIO_Init(GPIOD, GPIO_PIN_3, GPIO_MODE_IN_FL_NO_IT);//设置PD3为浮空输入无中断
}
void TIM2_Init(void){
TIM2_DeInit();
TIM2_TimeBaseInit(TIM2_PRESCALER_16, 10000); //最大10ms溢出,1us加1,只有向上计数
TIM2_ARRPreloadConfig(ENABLE);
//TIM2_ClearFlag(TIM2_FLAG_UPDATE);//清除更新标志
TIM2_ITConfig(TIM2_IT_UPDATE, ENABLE);//允许更新/溢出中断
//Send_Str("TIM2_Cmd before \n");
TIM2_Cmd(ENABLE);//开启定时器
//Send_Str("TIM2_Cmd after \n");
//TIM2_UpdateRequestConfig(TIM2_UPDATESOURCE_REGULAR);//中断源选择为只有溢出才能触发
//TIM2_GenerateEvent(TIM2_EVENTSOURCE_UPDATE);//产生更新事件,不触发中断(此处即更新了预分频器)
enableInterrupts(); //开启中断
//CH2、上升沿捕获、IC2映射在TI2FP2上、捕获预分频为1、无滤波器且采样率=主时钟频率
//TIM2_PWMIConfig(TIM2_CHANNEL_2, TIM2_ICPOLARITY_RISING, TIM2_ICSELECTION_DIRECTTI, TIM2_ICPSC_DIV1, 0x00);
//CH2、上升沿捕获、IC2映射在TI2FP2上、捕获预分频为1、无滤波器且采样率=主时钟频率
TIM2_ICInit(TIM2_CHANNEL_2, TIM2_ICPOLARITY_RISING, TIM2_ICSELECTION_DIRECTTI, TIM2_ICPSC_DIV1, 0x00);
TIM2_ITConfig(TIM2_IT_CC2, ENABLE);//捕捉比较2中断使能
TIM2_ClearITPendingBit(TIM2_IT_CC1);
TIM2_ClearFlag(TIM2_FLAG_CC1);
//TIM2_CCxCmd(TIM2_CHANNEL_2, ENABLE);//输入捕获/比较输出使能
}
//TIM2为输入捕获
void Remote_Init(){
IR_GPIO_Init();
TIM2_Init();
//printf("Remote Init \n");
}
//RmtSta来标识遥控器接收状态
//[7]:收到了引导码标志
//[6]:得到了一个按键的所有信息
//[5]:保留
//[4]:标记上升沿是否已经被捕获
//[3:0]:溢出计时器
u8 RmtSta=0;
u16 Dval; //下降沿时计数器的值
u32 RmtRec=0; //红外接收到的数据
u8 RmtCnt=0; //按键按下的次数
/**
* @brief Timer2 更新/上溢出/刹车 中断程序.
* @param None
* @retval None
*/
INTERRUPT_HANDLER(TIM2_UPD_OVF_BRK_IRQHandler, 13){
if(RmtSta&0x80){//RmtSta[7]是否置1,则上次有数据(引导码)被接收到了
RmtSta&=~0X10;//取消上升沿已经被捕获标记(将RmtSta[4]置0)
if((RmtSta&0X0F)==0X00)//对RtmSta[3:0]进行判断,为0代表第一次溢出
RmtSta|=1<<6; //标记已经完成一次按键的键值信息采集(将RmtSta[6]位置1)
if((RmtSta&0X0F)<14){//高电平持续时间<130ms,可能还处于连发码中的97.94ms高电平阶段
RmtSta++;
}else{//如果超过130ms,就表示遥控器上按键已经松开了
RmtSta&=~(1<<7); //清空引导标识
RmtSta&=0XF0; //清空计数器
}
}
TIM2_ClearFlag(TIM2_FLAG_UPDATE);//清除更新标志
TIM2_ClearITPendingBit(TIM2_IT_UPDATE);
}
/**
* @brief Timer2 捕获/比较 中断程序.
* @param None
* @retval None
*/
INTERRUPT_HANDLER(TIM2_CAP_COM_IRQHandler, 14){
BitStatus bs;
bs = GPIO_ReadInputPin(GPIOD, GPIO_PIN_3);//读PD3的输入电平状态
if (bs){//上升沿捕获
TIM2_OC2PolarityConfig(TIM2_OCPOLARITY_LOW);//改为下降沿捕获
TIM2_SetCounter(0x0000);//清空定时器的值
RmtSta|=0X10;//RmtSta[4]位置1 标记上升沿已经被捕获
}else{//下降沿捕获
Dval = TIM2_GetCounter();//读取CCR2也可以清CC2IF捕获标志位
TIM2_OC2PolarityConfig(TIM2_OCPOLARITY_HIGH);//改为上升沿捕获
if(RmtSta&0x10){//完成一次高电平捕获(如果RmtSta[4]为1)
if(RmtSta&0X80){//接收到了引导码(如果RmtSta[7]为1)
if(Dval>300&&Dval<800){ //560为标准值,560us(NEC中的0位)
RmtRec<<=1; //将RmtRec这个数左移一位.这里采用的是低位在前,高位在后的算法
RmtRec|=0; //接收到0
}else if(Dval>1400&&Dval<1800){ //1680为标准值,1680us(NEC中的1位)
RmtRec<<=1; //左移一位.
RmtRec|=1; //接收到1
}else if(Dval>2200&&Dval<2600){ //2500为标准值2.5ms,得到按键键值增加的信息(连发码)
RmtCnt++; //按键次数增加1次
RmtSta&=0XF0; //清空计时器
}
}else if(Dval>4200&&Dval<4700){ //4500为标准值4.5ms(引导码)
RmtSta|=1<<7; //RmtSta[7]位置1,标记成功接收到了引导码
RmtCnt=0; //清除按键次数计数器
}
}
RmtSta&=~(1<<4);//RmtSta[4]位置0,即清除上升沿捕获标志
}
TIM2_ClearITPendingBit(TIM2_IT_CC2);
}
//处理红外键盘
//返回值:
//0,没有任何按键按下
//其他,按下的按键键值.
u8 Remote_Scan(void){
u8 sta=0;
u8 t1,t2;
if(RmtSta&(1<<6))//得到一个按键的所有信息了(RmtSta[7]位是否为1)
{
t1=RmtRec>>24; //得到地址码(u32的RmtRec码右移24位,只保留地址码的8位)无符号
t2=(RmtRec>>16)&0xff; //得到地址反码(右移16位后只保留低8位)
if((t1==REMOTE_IDH)&&t2==REMOTE_IDL)//检验遥控识别码(ID)及地址
{
t1=RmtRec>>8;//得到控制码
t2=RmtRec; //得到控制反码
if(t1==(u8)~t2)sta=t1;//键值正确
}
if((sta==0)||((RmtSta&0X80)==0))//按键数据错误/遥控已经没有按下了
{
RmtSta&=~(1<<6);//清除接收到有效按键标识(RmtSta[7]位设为1)
RmtCnt=0; //清除按键次数计数器
}
}
return sta;
}
main.c代码
#include "stm8s.h"
#include "stdio.h"
#include "remote.h"
#include "uart.h"
#include "led.h"
#include "delay.h"
#include "gpio_init.h"
void main(void)
{
u8 key;
//u8 *str=0;
Clk_Init();//主时钟初始化
//UART_Init();//初始化串口设置
//LED_init();
init_TIM1();
Remote_Init();//IR解码涉及相关的初始化
Default_GPIO_Init();
//printf("all init \n");
while(1){
if(!GPIO_ReadInputPin(GPIOC, GPIO_PIN_6)){
key=Remote_Scan();
if(key&&RmtCnt==1){
//printf("Number of key repeats:%d \n",RmtCnt);
switch(key){
case 0:
//printf("str=ERROR \n");
break;
case 0x8A:
//printf("Press Power \n");
GPIO_WriteReverse(GPIOD, GPIO_PIN_5);
//LED_Reverse();
Delay100ms(20);//这里是delay.c中的函数
//LED_Reverse();
GPIO_WriteReverse(GPIOD, GPIO_PIN_5);
break;
}
}
}
}
}
软件设计思路二
采用外部中断的方式来做,适合做低功耗模式,但没做对连发码处理。
- 具体思路:将IR接收脚设置为下降沿触发,解码的整个过程都在中断服务函数里搞,高低电平的持续时间是在中断函数中通过等待IR接收脚的电平状态翻转得到的。
- 接收码值的时候采用的是右移的方式,所以得到和遥控器的码值一致
- stm8在进入中断服务函数时,不用对标志位进行清零,会自动进行清零
remote.h代码
#ifndef _REMOTE_H_
#define _REMOTE_H_
#include "stm8s.h"
#define REMOTE_IDH 0x30
#define REMOTE_IDL 0xCF
extern u8 IR_Data[4];//存放红外线解码接收的数据
extern bool IR_State;//FALSE表示未接收到数据,TRUE表示接收到数据
void Clk_Init(void);
void Remote_Init(void);
u8 getKey(void);
#endif
remote.c代码
#include "remote.h"
u8 IR_Data[4];//记录地址码、地址反码、控制码、控制反码
bool IR_State;//TRUE表示接收完一个码值,FALSE表示没接收完
BitStatus NEC_IR;//对应IR接收脚的电平状态,后面没用上
u16 x;//保存定时器的数值
//选用1MHz的HSI作为主时钟源
void Clk_Init(void){
CLK_DeInit();//设置为默认值
CLK_HSICmd(ENABLE);//启用HSI
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);//HSI分频 fMASTER
CLK_SYSCLKConfig(CLK_PRESCALER_CPUDIV16);//CPU分频 fCPU
}
//初始化PD3为浮空输入
void IR_GPIO_Init(void){
EXTI_DeInit();
GPIO_DeInit(GPIOD);//恢复指定端口
//GPIO_Init(GPIOD, GPIO_PIN_3, GPIO_MODE_IN_FL_NO_IT);//设置PD3为浮空输入无中断
GPIO_Init(GPIOD, GPIO_PIN_3, GPIO_MODE_IN_FL_IT);//设置PD3为浮空输入开启中断
EXTI_SetExtIntSensitivity(EXTI_PORT_GPIOD, EXTI_SENSITIVITY_FALL_ONLY);//仅下降沿触发
}
void TIM2_Init(void){
TIM2_DeInit();
TIM2_TimeBaseInit(TIM2_PRESCALER_16, 65535); //最大10ms溢出,1us加1,只有向上计数;计数最大值对应自动装载值
TIM2_ARRPreloadConfig(ENABLE);//直到更新事件后,ARR预装载寄存器的值才会被拷贝到影子寄存器中
//TIM2_ITConfig(TIM2_IT_UPDATE, ENABLE);//允许更新/溢出中断
//TIM2_Cmd(ENABLE);//开启定时器
//enableInterrupts(); //开启中断
}
//TIM2为输入捕获
void Remote_Init(){
IR_GPIO_Init();
TIM2_Init();
//printf("Remote Init \n");
}
//函数功能:测量高电平时间
u16 IR_GetTime_H(void){
TIM2_SetCounter(0);//清空定时器的值
TIM2_Cmd(ENABLE);//开启定时器
while(GPIO_ReadInputPin(GPIOD, GPIO_PIN_3)){}//等待高电平结束
x=TIM2_GetCounter();
TIM2_Cmd(DISABLE);//关闭定时器
return x;
}
//函数功能:测量低电平时间
u16 IR_GetTime_L(void){
TIM2_SetCounter(0);//清空定时器的值
TIM2_Cmd(ENABLE);//开启定时器
while(!GPIO_ReadInputPin(GPIOD, GPIO_PIN_3)){}//等待低电平结束
x=TIM2_GetCounter();
TIM2_Cmd(DISABLE);//关闭定时器
return x;
}
u8 getKey(void){
u8 key=0;
if((IR_Data[0]==REMOTE_IDH)&&IR_Data[1]==REMOTE_IDL){
if(IR_Data[2]==(u8)~IR_Data[3]) key=IR_Data[2];//键值正确
}
return key;
}
//外部中断PD中断服务函数
INTERRUPT_HANDLER(EXTI_PORTD_IRQHandler, 6){
u16 time;
u8 i,j,data=0;
//stm8自动清除中断标志位,所以此处没写
//NEC_IR = GPIO_ReadInputPin(GPIOD, GPIO_PIN_3);
time=IR_GetTime_L();//得到低电平时间
if(time<7000||time>10000)return; //标准时间: 9000us
time=IR_GetTime_H();得到高电平时间
if(time<3000||time>5500)return; //标准时间4500us
//正式解码NEC协议
for(i=0;i<4;i++)
{
for(j=0;j<8;j++)
{
time=IR_GetTime_L(); //得到低电平时间
if(time<400||time>700)return; //标准时间: 560us
time=IR_GetTime_H(); //得到高电平时间
if(time>1400&&time<1800) //数据1 1680us
{
data>>=1;
data|=0x80;
}
else if(time>400&&time<700) //数据0 560us
{
data>>=1;
}
else return;
}
IR_Data[i]=data; //存放解码成功的值
}
//解码成功
IR_State=TRUE;
}
main.c代码
#include "stm8s.h"
#include "stdio.h"
#include "remote.h"
#include "uart.h"
#include "led.h"
#include "delay.h"
#include "gpio_init.h"
void main(void)
{
u8 key;
//bool Power_Over_Flag=FALSE;
//u8 *str=0;
Clk_Init();//主时钟初始化
//LED_init();
init_TIM1();
Remote_Init();//IR解码涉及相关的初始化
UART_Init();//初始化串口设置
enableInterrupts(); //开启中断
Default_GPIO_Init();
printf("all init \n");
while(1){
if(!GPIO_ReadInputPin(GPIOC, GPIO_PIN_6)){
if(IR_State){
IR_State=FALSE;
key=getKey();
switch(key){
case 0:
printf("other remote \n");
break;
case 0x51:
printf("Press Power \n");
GPIO_WriteReverse(GPIOD, GPIO_PIN_5);
//LED_Reverse();
Delay100ms(20);
//LED_Reverse();
GPIO_WriteReverse(GPIOD, GPIO_PIN_5);
//Power_Over_Flag=TRUE;
break;
}
}
}
halt();
}
}