STM32红外接收与发送实战系列一:基于中断与定时器的精确数据解析实战指南
在学习STM32的过程中,学习到了关于红外接收与发送的内容,也是一个不断摸索的过程。
(一) 首先在学习红外接收之前,需要先了解一下红外信号
红外通信利用的是红外光(不可见光)来传输信号。发射端通过发光二极管 (IR LED) 发射特定频率的红外光,接收端使用红外接收头 接收光信号,并将其转化为电信号。由于是不可见光,所以如果要是想看的话,需要用手机摄像头对着看。 我认为红外信号的本质就是通过红外发射管以特定的频率快速闪烁,形成“亮-灭-亮-灭”的信号模式。
最常见的红外通讯协议——–NEC协议
NEC红外线协议使用脉冲宽度编码(Pulse Width Encoding)来表示二进制数据。每个数据位由一系列的脉冲组成,逻辑0和逻辑1分别由不同的脉冲宽度表示。
NEC 协议的数据格式包括了引导码、用户码、用户码(或者用户码反码)、按键键码和键码反码。(也就是说,当接收到一段红外信号时,他会由引导码、用户码、或者用户码反码、按键键码和键码反码这五种来组成)。NEC的数据格式如下图所示:
引导码:高电平9ms,低电平4.5ms
发射数据0用:0.56ms高电平 + 0.565ms低电平来表示
发射数据1用:0.56ms高电平 + 1.69ms低电平来表示
红外是怎样通过NEC协议发送的:红外发射是通过搭载在38KHZ载波(占空比为3:1)发射出去的,也就是说当发送数据为高电平时,红外发射管发送的是38KHZ的载波信号,当发送的数据为低电平时,红外发射管什么都不发射。
38KHZ的载波信号怎样产生:频率是38KHZ,那么周期也就是1/38KHZ=26.3us(微秒)。如果STM32的时钟频率为72HZ,那么要产生一个38KHZ(占空比为3:1)的载波信号,就要产生预分频器(PSC)为71,重装载值(ARR)为26,输出比较寄存器(CCR)为8的PWM信号(约值)。
举个例子:如果通过红外想要发送的数据为5(转换成2进制为0101),那么红外发送的数据就应该为00000000 11111111 00000101 11111010 因为发射数据0用:0.56ms高电平 + 0.565ms低电平来表示 。发射数据1用:0.56ms高电平 + 1.69ms低电平来表示。并且在发送数据之前还要发送引导码(高电平9ms,低电平4.5ms)所以发送内容如下所示:高电平发送的是38KHZ(占空比为1:3)的载波信号,低电平不发送。 (二)下面就要说一下怎样接收的,这就要看STM32的原理图了(IR接的是PB7引脚)
IR口外接了一个上拉电阻,IR口默认是高电平。当红外发射管亮时,接收管导通,给CPU一个低电平,(即OUT引脚变成低电平),所以IR引脚变成(低电平)。由此可得红外发射的信号与红外接收的信号相反。所以IR口接收到的信号为(与发送的信号相反):
数据1为:0.56ms低电平 + 1.69ms高电平
发射数据0用:0.56ms高电平 + 0.565ms低电平
引导码: 低电平9ms,高电平4.5ms
(三)解析信号
解析信号有两种方式,第一种利用定时器中的输入捕获来获取红外信号,第二种也就是下文讲到的利用端口中断和定时器来获取红外信号并解析。
解析思路:不管是数据1还是数据0,低电平的时间都是0.56ms,不同的是高电平的时间,所以我们只要测量出高电平的时间,就可以判断出数据是0还是1。在判断数据前,还需要判断引导码,只有引导码符合要求才会解析下面的数据。
首先,我们要开启IR口(PB7)的外部中断,用于检测红外接收模块输出信号的上升沿和下降沿;
//PB7---开启时钟GPIOB和AFIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE);
//配置GPIOB7为输入模式,浮空
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;//引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOB, &GPIO_InitStructure);
//选择 GPIO 管脚用作外部中断线路
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource7);
//配置EXTI
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line7;
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising_Falling;//上升沿和下降沿
EXTI_Init(&EXTI_InitStructure);
其次,我们要开启定时器:定时器在STM32中用于精确测量时间。红外信号是以脉冲宽度的方式来区分逻辑“0”和“1”,因此定时器用于测量从一个中断触发到下一个中断触发之间的时间差(脉冲宽度)。
预分频器 (Prescaler):STM32的时钟频率通常较高,不能直接用于测量微秒级的脉冲宽度。因此,需要通过预分频器将时钟频率降低到合适的范围。例如,如果STM32的主时钟频率为72MHz,设置预分频器为71后,定时器的计数频率将变为1MHz,即每计数一个单位表示1微秒。
自动重载寄存器 (ARR):该寄存器定义了定时器计数的最大值。当计数器达到这个值时,定时器会溢出并产生中断。在红外接收中,ARR的值应该足够大,以避免在处理脉冲时溢出。
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Period = 0xFFFF; // 最大计数
TIM_TimeBaseInitStructure.TIM_Prescaler = 71; // 1us 计时单位
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM7, &TIM_TimeBaseInitStructure);
配置完外部中断和定时器后,我们还要宏定义开启定时器,关闭定时器,和获取定时器CNT的值
#define TIM_start TIM_Cmd(TIM7,ENABLE)//开启定时器
#define TIM_stop TIM_Cmd(TIM7,DISABLE)//关闭定时器
#define TIM_get TIM_GetCounter(TIM7)//获取当前CNT的值
接下来,就要写外部中断服务函数。利用外部中断检测红外信号的上升沿和下降沿,使用定时器测量信号脉冲的长度,根据脉冲宽度确定数据的“0”或“1”。PB7端口由于外接上拉电阻的原因默认处于高电平。
uint16_t number_red=1;//中断函数的switch判断位
uint16_t date_red[32]={0};//存储数据0或者1
uint16_t i_red=0;//数据长度
void EXTI9_5_IRQHandler(void){
//判断中断位
if(EXTI_GetITStatus(EXTI_Line7)==SET){ //判断中断
switch (number_red)
{
case 1: //红外信号来时第一吃进中断,开启定时器
TIM_SetCounter(TIM7, 0); // 将定时器7的计数器清零
TIM_start;
number_red++;
break;
case 2: //第二次进中断,判断低电平的时间是否在9ms附近
if (8000<TIM_get&&TIM_get< 10000){
number_red++;
TIM_SetCounter(TIM7, 0); // 将定时器7的计数器清零
}
else{
number_red=1;
}
break;
case 3: //第三次进中断,判断高电平的时间是否在4.5ms左右
if(3000<TIM_get && TIM_get<5500){
number_red++;
led_control(1,1);
}
break;
default: //接下来进终端都是通过判断高电平的时间来确认数据为1还是0
//判断上升沿还是下降沿,如果是上升沿清零计数器的值,
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7)==SET){
TIM_SetCounter(TIM7, 0); // 将定时器7的计数器清零
}
else{ //下降沿,判断数据是1还是0,并将数据0或1写进date_red数组中
uint16_t timer_value = TIM_get;
if(465<timer_value && timer_value<665){
//红外发射为0;
printf("0");
date_red[i_red]=0;
i_red++;
}
else if(1490<timer_value&& timer_value<1890){
//红外发射为1;
printf("1");
date_red[i_red]=1;
i_red++;
}
}
break;
}
//清楚中断标志位
EXTI_ClearITPendingBit(EXTI_Line7);
}
}
数据最后一次上升沿触发后,将没有下降沿中断来关闭定时器,为了不影响下一次接收红外信号,我们需要利用定时器溢出中断来结束本次信号接收,并将标志位设为初始数据。并且通过handle_received_data函数将接收到的二进制数转变为十进制数存储到data_red_date数组中
//配置定时器向上溢出中断
TIM_ITConfig(TIM7,TIM_IT_Update,ENABLE);
NVIC_EnableIRQ(TIM7_IRQn);
//定时器溢出中断函数
void TIM7_IRQHandler(void){
TIM_ClearITPendingBit(TIM7,TIM_IT_Update);
if(number_red==4){
TIM_stop;
handle_received_data(date_red,i_red);// 在主程序中处理接收到的数据
i_red = 0;
number_red = 1;
}
}
//将接收到的二进制数,转变为十进制存储到data_red_date数组中
uint8_t data_red_date[4]={0};
void handle_received_data(uint16_t* data,uint16_t len) {
uint16_t i=len;
memset(data_red_date,0,sizeof(data_red_date)); //清楚数组中的内容
while(i>0){
data_red_date[(len-i)/8] |= (data[(len-i)] << ((len-i) % 8));
i--;
}
}
作者:愤怒的小王¥