基本原理
本遥控器的编码是NEC编码,为PWM(脉冲宽度调制)。
发射红外载波的时间固定,通过改变不发射载波的时间来改变占空比。
- 逻辑“0”是由0.56ms的38KHZ载波和0.560ms的无载波间隔组成;
- 逻辑“1”是由0.56ms的38KHZ载波和1.685m 的无载波间隔组成;
- 结束位是0.56ms的38K载波。
- 重复码由9ms红外脉冲和2.25ms的无红外脉冲以及560us的红外脉冲组成。
若发了一次命令码之后,一直按住遥控器按键并未松手,遥控器发射端将不会再发送命令码,而是每隔 110ms 时间,发送一段重复码。
过程分析
初始化部分
通用配置过程:
- 将相关的gpio和硬件挂载到RCC
- 初始化GPIO
- 初始化TIM时基单元
- 初始化IC输入捕获
- 初始化NVIC中断管理
- 实现中断处理函数
- 启用中断
- 启动定时器
TIM3的CH1通道位于PA6引脚。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed =GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
配置时基单元。arr和psc作为函数参数传入,支持动态调整。
在本文中,分频系数为72,这样标准周期就是1us,方便后续计算。
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = arr - 1;
TIM_TimeBaseStructure.TIM_Prescaler = psc - 1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);
配置输入捕获。将TIM_ICInitStructure.TIM_ICFilter设置为0xf是为了对输入捕获信号进行滤波,滤波时间为8个时钟周期。
- 对应16个采样点的平均,意味着最终捕获值是16个相邻采样值的平均值。
- 这相当于对输入信号进行8个时钟周期的滤波,因为每个采样周期占用一个时钟周期。
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICFilter = 0xf;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_CKD_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInit(TIM3,&TIM_ICInitStructure);
配置中断优先级。中断优先级的配置方案需要在主函数中设置:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_Init(&NVIC_InitStructure);
在之前的《PWM输入/输出》中,输入捕获采用的是事件方式:
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
当时没有设置中断,设置了事件。事件由硬件自动完成,不需要我们实现中断处理函数,只需要指定处理方式即可。
我们这次用不到事件。
涉及各种码的判断,以及信号处理阶段的判断,硬件事件难以处理这种复杂任务,需要通过中断实现。
启用中断标志位,在TIM3的CC1通道出现下降沿时,自动触发TIM_IT_CC1中断:
TIM_ITConfig(TIM3,TIM_IT_CC1,ENABLE);
配置完毕,启动TIM定时器:
TIM_Cmd(TIM3,ENABLE);
中断处理函数
通过输入捕获,判断两个下降沿之间的时间间隔:
- 引导码:13.5ms
- 重复码:11.25ms
- 逻辑0:1.12ms
- 逻辑1:2.245ms
将输入捕获通道的GPIO设置为上拉输入。在没有外界信号时会自动上拉到高电平。
时间间隔通过TIM_GetCounter(TIM3)
获取。TIM定时器设置的标准周期为1us,返回值即为下降沿之间的时间间隔,单位为us。
获取之后通过TIM_SetCounter(TIM3,0);
设置寄存器值为0,相当于重新开始计数。由于一个周期的开始和结束都是下降沿,所以上一个周期的结束接着就是下一个周期的开始,计数值误差可以接受。
操作类似于一个状态机:
- 初始处于空闲状态,TIM定时器的值是无意义的。在第一个下降沿到来时,设置TIM寄存器值为0,开始计数,进入准备状态。
- 准备状态下,第二个下降沿到来时,根据时长判断是引导码还是重复码。重复码则回到空闲状态。引导码还需要读取具体的指令,进入接受状态。
- 接收状态下,逐位接收,共32位。接收结束后回到空闲状态。等待下一个响应。
为什么引导码和重复码在同一个状态下判断?
对于上面四种码的时间间隔,很明显:引导码与重复码相近,逻辑0和逻辑1相近。
如果同一个阶段去判断时间间隔差距过大的码,可能造成数据遗漏。
如何按位接收?
需要用到指针,通过模运算、位运算实现。
指令一共有32位,需要一个char[4]
类型的数组。
创建一个指针,其实是整型,
- 对8做除法,用于对指定下标的char赋值
- 对8取模之后,值介于
[0,7]
,为指定位赋值
这种做法在存储器扩展、cache映射中都有应用,映射的常用方式就是取模。
如果学过,应该不会陌生。
数据校验
NEC编码的数据包括:
8位地址码+8位地址码的反码+8位命令码+8位命令码的反码
验证操作就是将反码部分按位取反,判断是否相等。
Addr_Temp = ~IR_DATA[1];
Cmd_Temp = ~IR_DATA[3];
if (IR_DATA[0] == Addr_Temp && IR_DATA[2] == Cmd_Temp) //数据验证
{
IR_Address = IR_DATA[0];
IR_Command = IR_DATA[2];
IR_DataFlag = 1;
}
代码实现
到目前,需要创建如下变量:
uint8_t IR_State
:记录当前状态:空闲/准备/接收uint16_t IR_Time
:下降沿间隔时间:引导/重复/0/1uint8_t IR_RepeatFlag,IR_DataFlag
:数据状态:重复/有数据uint8_t IR_DATA[4]
:数据:地址码+地址码反码+命令码+命令码反码uint8_t IR_pData
:指针:对IR_DATA进行段选和位选
void TIM3_IRQHandler()
{
uint8_t Addr_Temp,Cmd_Temp;
if (TIM_GetITStatus(TIM3,TIM_FLAG_CC1) != RESET)
{
if (IR_State == 0) //空闲态
{
TIM_SetCounter(TIM3,0);
IR_State = 1;
}
else if (IR_State == 1) //准备态
{
IR_Time = TIM_GetCounter(TIM3);
if (IR_Time > 13500 - 500 && IR_Time < 13500+500) //Start信号
{
IR_State = 2;
}
else if (IR_Time > 11250 - 500 && IR_Time < 11250+500) //Repeat信号
{
IR_State = 0;
IR_RepeatFlag = 1;
}
else
{
}
TIM_SetCounter(TIM3,0);
}
else if (IR_State == 2) //接收态
{
IR_Time = TIM_GetCounter(TIM3);
if (IR_Time > 1120 -500 && IR_Time < 1120+500) //逻辑0
{
IR_DATA[IR_pData/8] &= ~(0x01 << (IR_pData %8));
IR_pData++;
}
else if (IR_Time > 2250 -500 && IR_Time < 2250 + 500) //逻辑1
{
IR_DATA[IR_pData/8] |= (0x01 << (IR_pData %8));
IR_pData++;
}
else
{
IR_pData = 0;
IR_State = 1;
}
if (IR_pData>=32) //如果接收完32位数据
{
IR_pData = 0;
Addr_Temp = ~IR_DATA[1];
Cmd_Temp = ~IR_DATA[3];
if (IR_DATA[0] == Addr_Temp && IR_DATA[2] == Cmd_Temp) //数据验证
{
IR_Address = IR_DATA[0];
IR_Command = IR_DATA[2];
IR_DataFlag = 1;
}
IR_State = 0;
}
TIM_SetCounter(TIM3,0);
}
else
{
}
TIM_ClearITPendingBit(TIM3,TIM_IT_CC1);
}
}
实验效果
飞线
红外部分并不直接与PA6相连,需要飞线。
用杜邦线连接IF和TIM3的CH1所在的PA6即可。
在主函数中调用
uint8_t Address;
uint8_t Command;
uint8_t Num;
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Remote_Init(30000,72);
OLED_Init();
OLED_ShowString(1,1,"ADDR CMD NUM");
OLED_ShowString(2,1,"00 00 000");
while(1)
{
if (IR_GetDataFlag() || IR_GetRepeatFlag())
{
Address = IR_GetAddress();
Command = IR_GetCommand();
OLED_ShowHexNum(2,1,Address,2);
OLED_ShowHexNum(2,6,Command,2);
if (Command == IR_VOL_ADD)
{
Num++;
}
if(Command == IR_VOL_MINUS)
{
Num--;
}
OLED_ShowNum(2,10,Num,3);
}
}
}
VID_20240215_014048
参考
- STM32F103C8T6引脚定义.xlsx
- stm32 使用说明+笔记(必读).pdf
- 32版开发板原理图.pdf