开发板上有个红外接收器,是和F103的PB9相连,也配了一个遥控器,来学习红外遥控的原理,并且实际操作一下。
HS0038是一款红外接收头,当收到遥控器发过来的信号后,通过REMOTE_IN传输到PB9口,我们做一个输入捕获,把波形还原,即可解码遥控器的信号了,然后用串口将遥控器对应信号的按键信息打印出来。原理如此,下面实操。
NEC协议红外遥控器的通信采用了38KHz的载波信号,位时间为1.125ms或2.25ms:发射时逻辑1为0.56ms脉冲(即38KHz的方波)+1.68ms的低电平
,逻辑0为0.56ms脉冲+0.56ms低电平
。而遥控接收头在收到脉冲时为低电平,在没有脉冲的时候为高电平,所以接收头端收到的信号为:逻辑1=0.56ms低+1.68ms高;逻辑0=0.56ms低+0.56ms高
NEC协议的数据格式为:同步码头、地址码、地址反码(即按位取反)、控制码、控制反码。其中同步码为一个9ms的低电平和一个4.5ms的高电平组成。地址和控制码都是8位。
PB9对应的是TIM4-CH4
,把它设置为Input Capture direct mode
,并且开启中断,选择prescaler位71,其他默认选择,generate code。
根据上面NEC的数据格式,我们可以只计算捕获高电平的时间,即560us高电平代表0,1680高电平代表1,4.5ms高电平代表同步码(也叫引导码)。
STM32CubeMX中设置TIM4的prescaler
为71,则计数1代表1us,接下去就可以利用计数器来计时。并且一定要开启TIM4的中断。Generate Code之后参考了正点原子的代码
首先是两个中断回调函数:
uint8_t IRSta = 0; //接受状态,[0:3]溢出计数器,[4]标记上升沿是否已经被捕获,[5]保留,[6]得到了一个按键的全部信息,[7]收到了引导码标志
uint16_t Dval; //高电平计数器的值,根据此判断高低电平的时间
uint32_t IRData = 0; //红外接收的数据
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时器更新中断回调
{
if(htim->Instance == TIM4)
{
if(IRSta & 0x80) //[7]=1,即收到了引导码标志
{
IRSta &=~0x10; //取消上升沿被捕获标记[4]
if((IRSta&0x0F) == 0x00) //即低4位均为0
IRSta |=1<<6; //将[6]置1,标记已经得到一次按键的全部信息
if((IRSta & 0x0F)<14) //还未溢出
IRSta ++;
else
{
IRSta &=~(1<<7); //引导标志置0
IRSta &= 0xF0; //溢出计数器清空
}
}
}
}
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM4)
{
if(IR_IN) //capture了上升沿
{
TIM_RESET_CAPTUREPOLARITY(&htim4, TIM_CHANNEL_4); //清除捕获上升沿
TIM_SET_CAPTUREPOLARITY(&htim4, TIM_CHANNEL_4, TIM_ICPOLARITY_FALLING); //开始捕获下降沿
__HAL_TIM_SET_COUNTER(&htim4, 0); //清空定时器值
IRSta |= 0x10; //[4]置1,即标志上升沿已捕获
}
else //捕获下降沿
{
Dval = HAL_TIM_ReadCapturedValue(&htim4, TIM_CHANNEL_4); //下降沿计数
TIM_RESET_CAPTUREPOLARITY(&htim4,TIM_CHANNEL_4); //清除捕获下降沿
TIM_SET_CAPTUREPOLARITY(&htim4,TIM_CHANNEL_4,TIM_ICPOLARITY_RISING); //开始捕获上升沿
if(IRSta & 0x10) //如果完成了一次高电平捕获,接下来看是否有引导码
{
if(IRSta & 0x80) //接收了引导码
{
if(Dval>300 && Dval<800) //560us
{
IRData <<= 1; //左移一位
IRData |= 0; //高电平560us,表示0
}
else if(Dval>1400 && Dval<1800) //1680us
{
IRData <<=1;
IRData |= 1; //高电平1680us,表示1
}
else if(Dval>2200 && Dval<2600) //2.5ms
{
keyCount ++;
IRSta &= 0xF0;
}
}
else if(Dval>4200 && Dval<4700) //4.5ms高电平,引导码
{
IRSta |=1<<7; //[7]置1,引导码
keyCount = 0;
}
}
IRSta &=~(1<<4); //清空[4],即高电平计数结束
}
}
}
接下去定义一个Remote_Scan()
函数,即根据扫描得到的时长获取地址码、控制码,并计算反码和真实扫描的反码进行对比,还有一个遥控器ID(即地址码)的校验,这里正点原子的REMOTE_ID
为0,即地址码为0。具体代码如下:
uint8_t Remote_Scan(void)
{
uint8_t sta=0;
uint8_t t1, t2;
if(IRSta &(1<<6)) //[6]=1,得到一个按键的信息
{
t1 = IRData >> 24; //地址码
t2 = (IRData >> 16) & 0xFF; //地址反码
if((t1==(uint8_t)~t2) && t1 == REMOTE_ID) //校验t1是否为t2的反码
{
t1 = IRData >> 8; //控制码
t2 = IRData; //控制码反码
if(t1==(uint8_t)~t2) //再次校验是否反码
sta = t1; //t1即收到的控制码
}
if((sta==0)||((IRSta&0X80)==0))//按键数据错误/遥控已经没有按下了
{
IRSta&=~(1<<6);//清除接收到有效按键标识
}
}
return sta;
}
其他的代码基本无需更改,**但需要在MX_TIM4_Init(void)
最后添加两个语句
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_4); //开启通道4的捕获(中断方式)
__HAL_TIM_ENABLE_IT(&htim4, TIM_IT_UPDATE); //更新使能中断
main函数的while(1)循环里使用switch case
语句来判断读到的控制码是多少,根据控制码显示出按的遥控器哪个键。正点原子的实例是通过LCD显示屏来显示,而我改写了串口输出。
测试的时候发现按一次按键会输出很多次同样的key
和str
,猜想是按键的时间有点长,在正点原子的例子上体现不出来,所以我在获取key之后再延时了400ms(这个值可以去多次试)再进入判断语句,可以按一次键,串口输出一次结果。
while (1)
{
key=Remote_Scan();
HAL_Delay(400); //这个Delay很有必要
if(key)
{
printf("Code of the IR is: %0.8X \r\n", key);
switch(key)
{
case 0:str="ERROR";break; //其实不可能为0,为0进不来
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;
}
printf("func of the code is: %s\r\n", str);
}
else
{
HAL_Delay(10);
}
或者也可以调整时钟频率,改小一点,再试试结果。