定时器_输入捕获(初稿,标题要修改)
零、 最终效果(虽然简单,但是添加功能)
方便了解整体的效果
视频链接:
代码仓库:
涉及点
1、
2、
3、
使用的材料
材料 | 参考链接 |
---|---|
stm32f103c8t6 | 某宝 |
一、实现功能(三个)
1.通过输入捕获来解码 NEC 红外遥控器键值
2. 使用 OLED 和串口来显示和打印解码后的键值
3. 通过 EQ 键(键值 0x09)控制 LED 的翻转
二、原理分析(是要另外一章,不要放这里)
1、NEC 红外遥控器
红外遥控器广泛应用于家用电器的控制,
比如电视机,空调都是使用的红外编码来进行信息传输的,
红外遥控器由两部分组成,一个是发射端(遥控器),一个是接收端(家用电器),
发射端是通过人眼不可见的红外光来对信息进行编码的,
一般使用 38KHz 1/3 占空比的载波进行信息的调制,
是载波示意图,有载波时表示逻辑 1,无载波表示逻辑 0,
然后通过逻辑 0 和 1 的不同组合就衍生出了不同的红外协议,
比如上图举例的是 NEC 红外编码协议
在硬件上,使用下面的驱动电路来产生红外载波
即是红外发射驱动电路原理图
这里有两个要注意的点,一是为什么使用载波,二是载波频率为什么是 38KHz
使用载波的目的有两个,第一个是通过载波可以降低外界干扰。
如果我们直接通过LED 通断来作为逻辑 0 和 1 的话, 外界存在的其他红外干扰源会将传输的信息打乱
这将会传输一个无意义或者违背使用者控制意图的信息。
而用载波调制之后传输就会变得相对安全,因为红外接收部分有带通滤波器,
它只会让和载波频率相近的红外信号通过,因此其他频率的干扰源就被过滤掉了,
而不加调制的话,接收端是无法做到 0Hz 的带通滤波器的,
另外接收头其实就是一个使用运放搭建的反馈电路,无论载波的信号强弱,到最后输
出都会把它变为恒定的电压, 因此使用载波时,传输距离也会变远。
第二个好处就是低功耗,因为使用 1/3 占空比的载波和不是用载波在工作相同时间时,有载波的会更省电。
除此之外红外遥控器的好处是不同房间的同一个家用电器不会受到干扰。
选择38KHz 是因为红外遥控的载波不止有 38KHz 一种,只不过 38KHz 是最常用的,
因为发射电路使用的晶振是 455KHz 的,经过 12 分频之后得到 37.9KHz 约等于 38KHz,
另外红外发射频率和红外接收头频率要保持一致。
NEC 红外协议的时序
NEC 协议时序图
这是人为规定好的
时序的开头由 9ms 的逻辑 1 和 4.5ms 的逻辑 0 组成, 该开头不代表任何数据,
只是告诉接收端我要开始传输了。
接下来的部分由“一字节地址码+一字节地址反码+一字节命令码+一字节命令反码” 组成,
反码的作用是用于校验数据是否正确, 如果想要表达更多的信息,可以不使用反码。
地址码也可以叫用户编码,它类似于设备配对的指纹, 通过它可以单独控制某个家用电器,而其他电器不受影响。
命令码就是遥控器的键值,受控设备通过键值来执行对应动作。
二进制的 0 和 1 是通过逻辑 0 和 1 的不同组合时间来区分的,是因为采用了载波
另外,NEC 红外协议最先传输的是字节的低位,
比如上面例子中的地址: 10011010,其真实值时: 01011001。
那么,长时间按住遥控器的音量键不松开的话,音量会递增或者递减,
这是因为在发送一个完整数据帧之后,如果按键还在按下,那么遥控器会每隔 110ms 发送一个重复码信号
重复码时序由 9ms 逻辑 1+2.25ms 逻辑 0+0.56ms 逻辑 1 组成
这里要注意,在接收端接收的电平和发送端是相反的,
比如起始信号,发送端是 9ms 逻辑 1 加 4.5ms 逻辑 0,
对应接收端的输出电压是 9ms 逻辑 0 加 4.5ms 逻辑 1
下面是红外接收电路:
,当然NEC只是属于红外协议之一,红外的协议还有很多,比如 RC5, RC6, ITT, JVC 等,大家可以查看我们提供的红外相关资料进行理解
2、定时器输入捕获
使用定时器的输入捕获功能可以侦测外部数字信号的周期,占空比,
如果再加上某些协议时序的判断,既可以作为一个逻辑分析仪来用,
这里是使用输入捕获来解析 NEC红外协议逻辑,进而解码出遥控器键值。
作为定时器计数器基础单元的计数器,重装载寄存器,预分频寄存器,中断使能寄存器等,这里不再说明
因为主角是
捕获/比较模式寄存器n(TIMx_CCMRn),n = 1,2
捕获/比较使能寄存器(TIMx_CCER)
捕获/比较寄存器(TIMx_CCRn) ,n = 1,2,3,4
配置输入捕获的步骤
1.配置定时计数器初始化结构体
2. 配置对应捕获引脚为浮空输入
3. 配置输入捕获初始化结构体,包括使用的通道,触发信号沿的类型,分频值,是否滤波等
4. 使能捕获比较中断和定时器更新中断(更新非必须, 在本项目中需要更新中断)
三、程序分析
将程序主要分为五个部分,我的习惯是从局部到整体,如果喜欢从整体到局部的朋友,倒过来看就行了
1、输入捕获初始化
也就是定时器 4 输入捕获 3 通道初始化函数
void initTIM4IC3(u16 ICPolarity)
{
TIM_ICInitTypeDef TIM_ICInitStructure; //定义输入捕获初始化结构体
TIM_ICInitStructure.TIM_Channel = TIM_Channel_3; //配置输入捕获通道3为输入通道
TIM_ICInitStructure.TIM_ICPolarity = ICPolarity; //设置触发极性
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //将通道3连接到TI3
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //无预分频
TIM_ICInitStructure.TIM_ICFilter = 0x00; //无滤波
TIM_ICInit(TIM4, &TIM_ICInitStructure); //生效输入捕获设置
}
2、NEC 初始化函数
该函数用于初始化红外接收头的 IO 口,将 IO 口设置为浮空输入
void initNEC(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/****************************红外接收IO初始化****************************/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //配置PB8
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //配置为浮空输入
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
3、NEC 码长采集以及解码
根据NEC协议来进行的,位于定时器的服务函数
void TIM4_IRQHandler(void)
{
static u8 times = 0; //捕获次数,用于标记是否是第一次捕获,0是第一次,非0不是
static u8 rep_times = 0; //用于第一次接收重复码时,将上一次重复码次数清除掉
static u8 i = 0; //数组NEC_Buffer的索引值
u16 cache = 0; //用于临时存放输入捕获时定时器的值
if(TIM_GetITStatus(TIM4, TIM_IT_CC3) == SET) //发生捕获
{
TIM_ClearITPendingBit(TIM4, TIM_IT_CC3); //清除捕获挂起位(标志位)
if(times == 0) //第一次捕获
{
++times; //给times赋一个非0值即可
TIM_SetCounter(TIM4,0); //清零计数器,让其从0开始计数
}else //非第一次捕获
{
cache = TIM_GetCapture3(TIM4); //核算NEC码的时长
if(cache > 1230 && cache < 1500) //起始码 在12.3ms和15ms之间
{
i= 0;
NEC_DataStructure.NEC_Buffer[i++] = cache;
TIM_SetCounter(TIM4,0);
}else if(cache > 70 && cache < 270) //二进制1或0 在0.7ms和2.7ms之间
{
if(NEC_DataStructure.NEC_Buffer[0] != 0) //说明成功接收到起始码
{
NEC_DataStructure.NEC_Buffer[i++] = cache;
TIM_SetCounter(TIM4,0);
}else //没有接收到起始码接收到了数据码,说明是干扰
{
times = 0;
i = 0;
}
}else if(cache > 9000 && cache < 13000) //重复码 在100ms和120ms之间
{
if(NEC_DataStructure.NEC_Buffer[0] != 0) //接收到起始码重复 码才有效
{
if(rep_times==0)
{
NEC_DataStructure.NEC_Buffer[34] = 0;
++rep_times;
}
++NEC_DataStructure.NEC_Buffer[34]; //重复码数+1
TIM_SetCounter(TIM4,0);
}else
{
times = 0;
i = 0;
}
}else //无意义的码长,从零重新接
{
times = 0;
i = 0;
}
}
}
if(TIM_GetITStatus(TIM4, TIM_IT_Update) == SET) //定时器溢出中断,说明已经停止接收红外码(溢出需要200ms)
{
TIM_ClearITPendingBit(TIM4, TIM_IT_Update); //软件清除溢出挂起位
decodeNEC(&NEC_DataStructure,0x00); //解码
rep_times = 0;
times = 0;
i = 0;
}
}
4、红外解码函数
void decodeNEC(NEC_DataTypeDef* pNECData,u8 Addr)
{
u8 i,j;
u8* pdata = (u8*)pNECData; //将结构体指针强制转换为u8指针,方便存放码值
for(i=0;i<4;++i) //循环读取4个码值
{
for(j=0;j<8;++j) //读取单个码值
{
if(pNECData->NEC_Buffer[j+i*8+1] <270 && pNECData->NEC_Buffer[j+i*8+1] > 170) //二进制1
{
*(pdata+i) |= 0x80>>(7-j);
}else if(pNECData->NEC_Buffer[j+i*8+1] <170 && pNECData->NEC_Buffer[j+i*8+1] > 70) //二进制0
{
*(pdata+i) &= ~(0x80>>(7-j));
}else //无效值
{
memset(NEC_DataStructure.NEC_Buffer,0,35);
return;
}
if(*pdata != Addr) //防止数据错误
{
*pdata = 0;
memset(NEC_DataStructure.NEC_Buffer,0,35);
return ;
}
}
}
memset(NEC_DataStructure.NEC_Buffer,0,35);
pNECData->RepeatCode = pNECData->NEC_Buffer[34]; //重复码次数赋值
}
5、主函数
int main(void)
{
/*初始化各外设*/
initSysTick();
/*波特率115200*/
initUART();
initLED();
initNVIC(NVIC_PriorityGroup_2);
initNEC();
/*定时器4时钟周期10us,中断周期200ms,使能更新中断和捕获通道3中断*/
initTIMx(TIM4,719,19999,TIM_IT_Update|TIM_IT_CC3,ENABLE);
/*设置下降沿触发捕获*/
initTIM4IC3(TIM_ICPolarity_Falling);
initIIC();
initOLED();
/*使用ADC初始化函数只是为了将连接PA15的LED灯关掉*/
initADC();
formatScreen(0x00);
showCNString(32,0,"风媒电子",FONT_16_CN);
showString(0,2,"ADDR:",FONT_16_EN);
showString(0,4,"CMD :",FONT_16_EN);
while (1)
{
showNumber(40,2,NEC_DataStructure.AddrCode_H,HEX,2,FONT_16_EN);
showNumber(40,4,NEC_DataStructure.CmdCode_H,HEX,2,FONT_16_EN);
printf("AddrCode is :%02X\n",NEC_DataStructure.AddrCode_H);
printf("CmdCode is :%02X\n",NEC_DataStructure.CmdCode_H);
if(NEC_DataStructure.CmdCode_H == 0x09) //如果是EQ键按下
{
NEC_DataStructure.CmdCode_H = 0; //清除EQ键值,防止LED重复闪烁
toggleLED();
}
Delay_ms(100);
}
}