1)实验平台:【正点原子】 NANO STM32F103 开发板
2)摘自《正点原子STM32 F1 开发指南(NANO 板-HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

第二十五章 红外遥控实验
本章,我们将向大家介绍如何通过 STM32 来解码红外遥控器的信号。ALIENTK NANO
STM32F103 标配了红外接收头和一个很小巧的红外遥控器。在本章中,我们将利用 STM32F1
的输入捕获功能,解码开发板标配的这个红外遥控器的编码信号,并将解码后的键值显示在数
码管上。本章分为如下几个部分:
25.1 红外遥控简介
25.2 硬件设计
25.3 软件设计
25.4 下载验证
25.1 红外遥控简介
红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成
本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计
算机系统中。
由于红外线遥控不具有像无线电遥控那样穿过障碍物去控制被控对象的能力,所以,在设
计红外线遥控器时,不必要像无线电遥控器那样,每套(发射器和接收器)要有不同的遥控频率
或编码(否则,就会隔墙控制或干扰邻居的家用电器),所以同类产品的红外线遥控器,可以有
相同的遥控频率或编码,而不会出现遥控信号“串门”的情况。这对于大批量生产以及在家用
电器上普及红外线遥控提供了极大的方面。由于红外线为不可见光,因此对环境影响很小,再
由红外光波动波长远小于无线电波的波长,所以红外线遥控不会影响其他家用电器,也不会影
响临近的无线电设备。
红外遥控的编码目前广泛使用的是:NEC Protocol 的 PWM(脉冲宽度调制)和 Philips
RC-5 Protocol 的 PPM(脉冲位置调制)。ALIENTEK 战舰 STM32 开发板配套的遥控器使用
的是 NEC 协议,其特征如下:
1、8 位地址和 8 位指令长度;
2、地址和命令 2 次传输(确保可靠性)
3、PWM 脉冲位置调制,以发射红外载波的占空比代表“0”和“1”;
4、载波频率为 38Khz;
5、位时间为 1.125ms 或 2.25ms;
NEC 码的位定义:一个脉冲对应 560us 的连续载波,一个逻辑 1 传输需要 2.25ms(560us
脉冲+1680us 低电平),一个逻辑 0 的传输需要 1.125ms(560us 脉冲+560us 低电平)。而遥控
接收头在收到脉冲的时候为低电平,在没有脉冲的时候为高电平,这样,我们在接收头端收到
的信号为:逻辑 1 应该是 560us 低+1680us 高,逻辑 0 应该是 560us 低+560us 高。
NEC 遥控指令的数据格式为:同步码头、地址码、地址反码、控制码、控制反码。同步码
由一个 9ms 的低电平和一个 4.5ms 的高电平组成,地址码、地址反码、控制码、控制反码均是
8 位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可
用于校验)。
我们遥控器的按键“▽”按下时,从红外接收头端收到的波形如图 25.1.1 所示:
图 25.1.1 按键“▽”所对应的红外波形
从图 33.1.1 中可以看到,其地址码为 0,控制码为 168。可以看到在 100ms 之后,我们还
收到了几个脉冲,这是 NEC 码规定的连发码(由 9ms 低电平+2.5m 高电平+0.56ms 低电平
+97.94ms 高电平组成),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码,
即连发码,可以通过统计连发码的次数来标记按键按下的长短/次数。
第十五章我们曾经介绍过利用输入捕获来测量高电平的脉宽,本章解码红外遥控信号,刚
好可以利用输入捕获的这个功能来实现遥控解码。关于输入捕获的介绍,请参考第十五章的内
容。
25.2 硬件设计
本实验采用定时器的输入捕获功能实现红外解码,本章实验功能简介:开机在初始化用到
的外设之后,即进入等待红外触发,如过接收到正确的红外信号,则解码,并在数码管上显示
键值(由于数码管显示字符有限,我们用显示数字来代表键值)。同样我们也是用 DS0 来指示
程序正在运行。
所要用到的硬件资源如下:
1) 指示灯 DS0
2) 数码管
3) 蜂鸣器
4) 红外接收头
5) 红外遥控器
前三个,在之前的实例已经介绍过了,遥控器属于外部器件,遥控接收头在板子上,与
MCU 的连接原理图如 25.2.1 所示:
图 25.2.1 红外遥控接收头与 STM32 的连接电路图
红外遥控接收头连接在 STM32 的 PB0(TIM3_CH3)上。硬件上不需要变动,只要程序将
TIM3_CH3 设计为输入捕获,然后将收到的脉冲信号解码就可以了。开发板配套的红外遥控器
外观如图 25.2.2 所示:
图 25.2.2 红外遥控器
25.3 软件设计
打开我们光盘的红外遥控器实验工程,可以看到我们添加了 remote.c 和 remote.h 两个文件,
同时因为我们使用的是输入捕获,所以还用到库函数 stm32f1xx_hal_tim.c 和 头文 件
stm32f1xx_hal_tim.h。
打开 remote.c 文件,代码如下:
TIM_HandleTypeDef TIM3_Handler; //定时器 3 句柄
//红外遥控初始化
//设置 IO 以及 TIM3_CH3 的输入捕获
void Remote_Init(void)
{
TIM_IC_InitTypeDef TIM3_CH3Config;
TIM3_Handler.Instance=TIM3; //通用定时器 3
TIM3_Handler.Init.Prescaler=(72-1); //预分频器,1M 的计数频率,1us 加 1.
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period=10000; //自动装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&TIM3_Handler);
//初始化 TIM3 输入捕获参数
TIM3_CH3Config.ICPolarity=TIM_ICPOLARITY_RISING; //上升沿捕获
TIM3_CH3Config.ICSelection=TIM_ICSELECTION_DIRECTTI;//映射到 TI3 上
TIM3_CH3Config.ICPrescaler=TIM_ICPSC_DIV1; //配置输入分频,不分频
TIM3_CH3Config.ICFilter=0x03; //IC4F=0003 8 个定时器时钟周期滤波
HAL_TIM_IC_ConfigChannel(&TIM3_Handler,&TIM3_CH3Config,
TIM_CHANNEL_3);//配置 TIM3 通道 3
HAL_TIM_IC_Start_IT(&TIM3_Handler,TIM_CHANNEL_3); //开始捕获 TIM3 的通道 3
__HAL_TIM_ENABLE_IT(&TIM3_Handler,TIM_IT_UPDATE); //使能更新中断
}
//定时器 3 底层驱动,时钟使能,引脚配置
//此函数会被 HAL_TIM_IC_Init()调用
//htim:定时器 3 句柄
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_TIM3_CLK_ENABLE(); //使能 TIM3 时钟
__HAL_RCC_GPIOB_CLK_ENABLE(); //开启 GPIOB 时钟
GPIO_Initure.Pin=GPIO_PIN_0; //PB0
GPIO_Initure.Mode=GPIO_MODE_AF_INPUT; //复用输入
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
HAL_NVIC_SetPriority(TIM3_IRQn,1,3);//设置中断优先级,抢占优先级 1,子优先级 3
HAL_NVIC_EnableIRQ(TIM3_IRQn); //开启 ITM3 中断
}
//遥控器接收状态
//[7]:收到了引导码标志
//[6]:得到了一个按键的所有信息
//[5]:保留
//[4]:标记上升沿是否已经被捕获
//[3:0]:溢出计时器
u8 RmtSta=0;
u16 Dval;
//下降沿时计数器的值
u32 RmtRec=0;
//红外接收到的数据
u8 RmtCnt=0;
//按键按下的次数
//定时器 3 中端服务函数
void TIM3_IRQHandler(void)
{
HAL_TIM_IRQHandler(&TIM3_Handler);//定时器共用处理函数
}
//定时器更新(溢出)中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
if(RmtSta&0x80)//上次有数据被接收到了
{
RmtSta&=~0X10;
//取消上升沿已经被捕获标记
if((RmtSta&0X0F)==0X00)RmtSta|=1<<6;
//标记已经完成一次按键的键值信息采集
if((RmtSta&0X0F)<14)RmtSta++;
else
{
RmtSta&=~(1<<7);//清空引导标识
RmtSta&=0XF0; //清空计数器
}
}
}
}
//定时器输入捕获中断回调函数
void
HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)//捕获中断发生时执行
{
if(htim->Instance==TIM3)
{
if(RDATA)//上升沿捕获
{
TIM_RESET_CAPTUREPOLARITY(&TIM3_Handler,TIM_CHANNEL_3);
// 一 定 要 先 清 除 原 来 的 设
置!!
TIM_SET_CAPTUREPOLARITY(&TIM3_Handler,TIM_CHANNEL_3,
TIM_ICPOLARITY_FALLING);//CC1P=1 设置为下降沿捕获
__HAL_TIM_SET_COUNTER(&TIM3_Handler,0); //清空定时器值
RmtSta|=0X10;
//标记上升沿已经被捕获
}else //下降沿捕获
{
Dval=HAL_TIM_ReadCapturedValue(&TIM3_Handler,
TIM_CHANNEL_3);//读取 CCR2 也可以清 CC2IF 标志位
TIM_RESET_CAPTUREPOLARITY(&TIM3_Handler,TIM_CHANNEL_3);
//一定要先清除原来的设置!!
TIM_SET_CAPTUREPOLARITY(&TIM3_Handler,TIM_CHANNEL_3,
TIM_ICPOLARITY_RISING);//配置 TIM3 通道 3 上升沿捕获
if(RmtSta&0X10)//完成一次高电平捕获
{
if(RmtSta&0X80)//接收到了引导码
{
if(Dval>300&&Dval<800)
//560 为标准值,560us
{
RmtRec<<=1;
//左移一位.
RmtRec|=0;
//接收到 0
}else if(Dval>1400&&Dval<1800)
//1680 为标准值,1680us
{
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;
//标记成功接收到了引导码
RmtCnt=0;
//清除按键次数计数器
}
}
RmtSta&=~(1<<4);
}
}
}
//处理红外键盘
//返回值:
//
0,没有任何按键按下
//其他,按下的按键键值.
u8 Remote_Scan(void)
{
u8 sta=0;
u8 t1,t2;
if(RmtSta&(1<<6))//得到一个按键的所有信息了
{
t1=RmtRec>>24;
安博体育全站官网//得到地址码
t2=(RmtRec>>16)&0xff;
//得到地址反码
if((t1==(u8)~t2)&&t1==REMOTE_ID)//检验遥控识别码(ID)及地址
{
t1=RmtRec>>8;
t2=RmtRec;
if(t1==(u8)~t2)sta=t1;//键值正确
}
if((sta==0)||((RmtSta&0X80)==0))//按键数据错误/遥控已经没有按下了
{
RmtSta&=~(1<<6);//清除接收到有效按键标识
RmtCnt=0;
//清除按键次数计数器
}
}
return sta;
}
该部分代码包含 6 个函数,首先是 Remote_Init 函数,该函数和 MSP 回调函数
HAL_TIM_IC_MspInit 共同完成 TIM1 的时基参数初始化,TIM3_CH3 的输入捕获配置,IO 口
初始化配置,IO 口复用映射配置以及 NVIC 配置。具体内容和输入捕获实验章节基本一致,不
同的是换了定时器而已。请参考输入捕获实验章节讲解。
溢出(更新)中断回调函数为
HAL_TIM_PeriodElapsedCallback,在本例程里面,该函数来
处理 TIM3 溢出,并表用于标记键值获取完成,每一次红外按键解码,都必须通过定时器溢出
时间来标记完成。输入捕获中断回调函数
HAL_TIM_IC_CaptureCallback,在本例程里面,该函
数实现对红外信号的高电平脉冲的捕获,同时根据我们之前简介的协议内容来解码。这两个中
断处理回调函数用到几个全局变量,用于辅助解码,并存储解码结果。
这里简单介绍一下高电平捕获思路:首先输入捕获设置的是捕获上升沿,在上升沿捕获到
以后,立即设置输入捕获模式为捕获下降沿(以便捕获本次高电平),然后,清零定时器的计
数器值,并标记捕获到上升沿。当下降沿到来时,再次进入捕获中断服务函数,立即更改输入
捕获模式为捕获上升沿(以便捕获下一次高电平),然后处理此次捕获到的高电平。
最后是 Remote_Scan 函数,该函用来扫描解码结果,相当于我们的按键扫描,输入捕获解
码的红外数据,通过该函数传送给其他程序。
接下来打开 remote.h,可以看到下面一行代码:
#define REMOTE_ID 0
这里的 REMOTE_ID 就是我们开发板配套的遥控器的识别码,对于其他遥控器可能不一样,
只要修改这个为你所使用的遥控器的一致就可以了。其他是一些函数的声明,我们不做讲解。
下面我们看看 main.c 里面主函数代码:
// 共阴数字数组
// 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F, .,全灭
u8 smg_num[]={0xfc,0x60,0xda,0xf2,0x66,0xb6,0xbe,0xe0,0xfe,
0xf6,0xee,0x3e,0x9c,0x7a,0x9e,0x8e,0x01,0x00};
u8 key=0; //按键值
u8 num=0x00;//数值
u8 num1=0x00;//数值
u8 smg_wei=6;//数码管位选
u8 smg_duan=0;//数码管段选
u8 smg_flag=0;//数码管显示标志 0:正常显示 1:消除鬼影
u8 t=0;
int main(void)
{
HAL_Init(); //初始化 HAL 库
Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M
delay_init(72); //初始化延时函数
uart_init(115200);
//串口初始化为 115200
LED_Init();
//初始化与 LED 连接的硬件接口
BEEP_Init(); //蜂鸣器初始化
LED_Init();
//初始化与 LED 连接的硬件接口
LED_SMG_Init(); //数码管初始化
TIM4_Init(19,7199); //数码管 2ms 定时显示
Remote_Init();
//红外接收初始化
while(1)
{
}
}
//定时器 4 中断服务函数调用
void TIM4_IRQHandler(void)
{
if(__HAL_TIM_GET_IT_SOURCE(&TIM4_Handler, TIM_IT_UPDATE) !=RESET)
{
__HAL_TIM_CLEAR_IT(&TIM4_Handler, TIM_IT_UPDATE);
key = Remote_Scan();
if(key)
{
switch(key)
{
case 104:num1=0x00; num = smg_num[1]; BEEP=0;break; //按键'1'
case 152:num1=0x00;num=smg_num[2];BEEP=0;break; //按键'2'
case 176:num1=0x00;num=smg_num[3];BEEP=0;break; //按键'3'
case 48:num1=0x00;num=smg_num[4];BEEP=0;break; //按键'4'
case 24:num1=0x00;num=smg_num[5];BEEP=0;break; //按键'5'
case 122:num1=0x00;num=smg_num[6];BEEP=0;break; //按键'6'
case 16:num1=0x00;num=smg_num[7];BEEP=0;break; //按键'7'
case 56:num1=0x00;num=smg_num[8];BEEP=0;break; //按键'8'
case 90:num1=0x00;num=smg_num[9];BEEP=0;break; //按键'9'
case 66:num1=0x00;num=smg_num[0];BEEP=0;break; //按键'0'
case 82:num1=0x00;num=smg_num[17];BEEP=0;break; //按键'DELETE'
case 162:num1=smg_num[1];num=smg_num[0];BEEP=0; break;
//按键'POWER'
case 98:num1=smg_num[1];num=smg_num[1];BEEP=0; break;
//按键'UP'
case 226:num1=smg_num[1];num=smg_num[2];BEEP=0; break;
//按键'ALIENTEK'
case 34:num1=smg_num[1];num=smg_num[3];BEEP=0;break;
//按键'LEFT'
case 2:num1=smg_num[1];num=smg_num[4];BEEP=0; break;
//按键'PLAY'
case 194:num1=smg_num[1];num=smg_num[5];BEEP=0; break;
//按键'RIGHT'
case 224:num1=smg_num[1];num=smg_num[6];BEEP=0; break;
//按键'VOL-'
case 168:num1=smg_num[1];num=smg_num[7];BEEP=0; break;
//按键'DOWN'
case 144:num1=smg_num[1];num=smg_num[8];BEEP=0; break;
//按键'VOL+'
}
}else
{
BEEP=1;
}
if(smg_wei==6)//数码管位
{
smg_duan = num1;
}
else if(smg_wei==7)
{
smg_duan = num;
}
if(smg_flag) LED_Write_Data(0x00,smg_wei);//消除鬼影(段码不显示)
else
LED_Write_Data(smg_duan,smg_wei);//正常显示
LED_Refresh();//数码管数据更新
smg_flag=!smg_flag;
if(smg_flag==0)//正常显示才更新位码
{
smg_wei++;
if(smg_wei==8) smg_wei=6;
}
t++;
if(t==250)//LED0 每 500MS 闪烁
{
t=0;
LED0=!LED0;
}
}
}
main 函数代码比较简单,主要是外设初始化,在定时器 4 中断里(没有用回调函数形式),
通过 Remote_Scan 函数获得红外遥控输入的数据(键值),然后以键值对应的数字,动态扫描
方式显示在数码管上面,同时数码管做了消除鬼影的操作。
至此,我们的软件设计部分就结束了。
25.4 下载验证
在代码编译成功之后,我们通过下载代码到 ALIENTEK NANO STM32F103 上,没有按下
遥控器时数码管无任何显示,DS0 闪烁表示程序正在运行,此时我们通过遥控器按下不同的按
键,则可以看到数码管上显示了不同数字,同时蜂鸣器鸣叫一声,显示如图 25.4.1 所示:
图 25.4.1 解码成功
从图可看到,数码管显示“5”,即代表我们按下红外遥控器的是按键“5”。
发表评论