前言


前面介绍了GPIO的通用输入输出功能,以及其模拟时序的功能,可以发现,使用模拟时序来驱动外设,代码部分会有些许的麻烦,而且有些时序和功能是用模拟时序无法实现的。这时候就需要使用到外设了,这里的外设不仅仅是指芯片外片外外设,还有一部分是芯片内部的片内外设。
这些片上外设大多数是需要与芯片外进行数据交换的,如定时器输入捕获、定时器比较输出、SPI控制器、I2C控制器。
这些需要数据交换的片上外设都需要使用到前面的GPIO来作为中介。因为GPIO是单片机内部与外部的唯一沟通桥梁,使用这些片上外设时,GPIO就需要根据需要配置为特殊模式,也叫做复用模式。
![在这里插入图片描述](https://img-blog.csdnimg.cn/e7ff1952b78443c09008e92099784d9b.png#pic_center =600*200)
通过芯片手册可以看出STM32F103C8T6的片上外设数量。笔者这个板子使用了四个定时器、一个SPI、三个USART、一个ADC,具体的使用会在后面一一介绍。

定时器输入捕获

为了衔接上一篇的内容,本文先介绍利用定时器的输入捕获来解析红外接收管的数据,并以此来作为一种输入信号进行控制。

红外接收的数据分析

上一篇中我们介绍了HS0038接收的NEC格式的数据帧,截个图过来,如下图所示:


根据这个帧格式,可以找到如下的特征:

1.一帧数据的起始调节有一个同步头,这个同步头有9ms的低电平和4.5ms的高电平组成;
2.逻辑“1”与逻辑“0”的前半段信号都是560us的低电平,只有后半段的高电平时间不同,逻辑“1”的高电平持续时间是1680us,逻辑“0”的高电平持续时间是560us;
根据这些特征,如果想解析出数据,就需要定位到每一个上升沿并且获取到各个部分的高电平持续时间就可以了。根据高电平时间的不同就可以知道此时的信号时同步头还是逻辑“0”、逻辑“1”。
既要准确定位边沿,并且记录下电平持续时间,这个过程是不是瞅着有一些眼熟,在前一个系列介绍输入捕获的时候,我们曾经使用了输入捕获来获取按键按下的时间长度,以及通过超声波模块获取了物体距离,这都是检测边沿加计时。
这里如果不了解输入捕获功能的小伙伴们可以去查看笔者的这篇介绍,在此处不再赘述。
嵌入式学习笔记——输入捕获

捕获思路

根据帧格式的特征,可以捋一下获取红外遥控信号的编程思路:
1.首先需要根据硬件连接初始化对应的定时通道以及GPIO;
2.配置好对应的时基,这里为了便于更加准确的计数,最好是将定时器的计数周期设置为1us一次,这样也便于数据处理。具体的设置方式在后面编程时介绍;
3.设置输入捕获的触发方式以及是否分频的参数,
4.配置捕获中断,进行捕获,
这里由于要获取高电平的持续时间:
首先肯定是需要上升沿触发捕获中断的,上升沿触发后先清空计数值,从0开始计数,且切换触发模式为下降沿触发,当下降沿触发时返回计数值,并再次切换回上升沿触发,判断对比计数值的范围与各个信号的特征就可以知道此时的数据值了。


5.除了捕获中断,还可以开启更新中断作为接收完成的标志,为了进行区分,这里更新中断的时间选择10ms,整个过程中,只有接收完32位信号后,计数值才有可能溢出从而产生更新中断,更新中断一产生就代表可以进行数据处理了。

编程实践

首先根据思路查找对应的GPIO,通过原理图和数据手册可以知道,使用的是GPIOA1,复用的是TIM2的通道2。

1.初始化时钟

再使用任何一个片上外设时,第一件事就是要开启其对应的时钟,这里我们需要开启GPIOA的时钟、TIM2的时钟,由于使用到了复用功能,所以还需要开启复用的时钟。
根据之前的经验的,时钟相关先看数据手册找到片上外设的挂接总线。


可以发现,这俩片上外设分别挂载在APB2与APB1上,所以需要代码如下:

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);	//使能定时器2时钟
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);  //使能GPIOA外设能模块时钟
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);  //使能GPIOA AFIO复用功能模块时钟

可以看到上代码中还有一个是开启了AFIO的时钟,开启他的原因是需要使用到GPIO的复用功能,所以必须开启这个时钟。

2.初始化GPIO

这里需要注意一点,对于想C8T6这种GPIO数量较少的MCU会有一个重映射的设置,重映射的作用就在于可以切换片上外设的复用GPIO端口,但是不是随意更改的,而是根据类似下图的映射表来实现的。此处的TIM2CH2默认就是PA1不需要进行重映射。

void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState)这个函数是专门用来实现重映射设置的。这里暂时用不上,后面用到了再说。

那么问题来了,GPIO的初始化要怎么写呢?
作为输入捕获,模式自然是输入;但是在M3的GPIO输入模式中,没有复用输入模式,这里我们不需要上下拉,也不是模拟输入,所以,直接选择浮空输入即可。

 //设置该引脚为浮空输入功能,
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //TIM2_CH2
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  //浮空输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIO

3.配置定时器的时钟基准

定时器的时钟基准就是设置定时的预分频数、重装载值、计数模式这些定时器的基本参数,关于这些东西的详细介绍,可以参考笔者前一个系列的嵌入式学习笔记——通用定时器
这里使用库函数的思路与之前的GPIO其实是差不多的,也是先在库函数中找到stm32f10x_tim.h,然后找到对应的初始化结构,根据描述以及后面的参数宏或者参数枚举,依次进行结构体成员的赋值。

参数赋值完成后,也是需要调用对应的结构体初始化函数将这些参数写入对应的寄存器中。参数分别是定时器编号,以及上面的结构体的地址。

//初始化TIM2
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;//定义定时器结构体
	TIM_TimeBaseStructure.TIM_Period = arr-1; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
	TIM_TimeBaseStructure.TIM_Prescaler =psc-1; //设置用来作为TIMx时钟频率除数的预分频值 
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位

4.配置输入捕获参数

这里我们需要使用到定时器的输入捕获功能,因此还需要再次基础上进一步进行配置,流程与上面一样,也是找到对应的结构体变量,然后参考描述以及参数宏雨参数枚举进行赋值以及初始化。这里需要配置物理通道、触发信号种类、输入捕获与映射通道。

同样的配置完相关参数后,也需要调用对应的初始化函数进行写入寄存器。


	//输入捕获
	TIM_ICInitTypeDef TIM_ICInitStruct;//定时器输入捕获控制寄存器
	TIM_ICInitStruct.TIM_Channel=TIM_Channel_2;//通道2
	TIM_ICInitStruct.TIM_ICFilter=0x00;//不使用滤波器
	TIM_ICInitStruct.TIM_ICPolarity=TIM_ICPolarity_Rising;//上升沿捕获
	TIM_ICInitStruct.TIM_ICPrescaler=TIM_ICPSC_DIV1;//不分频
	TIM_ICInitStruct.TIM_ICSelection=TIM_ICSelection_DirectTI;// TI2FP2 输入捕获通道选择
	TIM_ICInit(TIM2,&TIM_ICInitStruct);

5.中断配置

然后就是中断的配置了,首先需要调用定时器的函数实现中断信号的配置,这里需要使用到更新中断与捕获中断两种。

上面只是指定了中断信号,还没对中断进行配置,因此还需要找到
misc.h,在这里面也有对应的参数配置结构体。

同样也有对应的初始化函数


于是代码如下:

//中断配置
	TIM_ITConfig(TIM2,TIM_IT_CC2|TIM_IT_Update,ENABLE);//更新中断和捕获中断

	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_InitStruct.NVIC_IRQChannel=TIM2_IRQn;
	NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
	NVIC_Init(&NVIC_InitStruct);
	 //使能放到最后,避免写入失败。
	TIM_Cmd(TIM2, ENABLE);  //使能TIM2

6.中断服务函数

上面的都只是初始化的配置函数,真正的功能实现,还需要使用中断服务函数。首先需要去到启动文件拿到对应的中断服务函数名称。

整段代码的思路就是,
1.利用上升沿触发清空计数器,并将触发模式切换为下降沿触发,计数周期是1us一次;
2.下降沿触发时,判断计数值,并做比较,根据三类特征,判断是同步头、逻辑1还是逻辑0;
3.根据低位在前的顺序,定义一个32位的数据,按位接收数据;
4.当计数器溢出时,也就是计数值到了10000 代表联系10ms的高电平,此时判断为接收完成。

//定时器2中断服务程序	 
void TIM2_IRQHandler(void)
{ 	
	if(TIM_GetITStatus(TIM2,TIM_IT_Update)!=RESET)
	{//10MS产生一次中断,整个传输中最长的高电平时间为同步码 的 4.5ms ;只有释放信号才可能进入更新中断	
		TIM_ClearITPendingBit(TIM2,TIM_IT_Update);	 
		if(Infrared_Receive.start)//接收到过同步头且产生了更新中断说明接收到完毕了
		{
			Infrared_Receive.start=0;
			Infrared_Receive.end =1;//接收完成的标志位
			
		}
	}
	if(TIM_GetITStatus(TIM2,TIM_IT_CC2)!=RESET)
	{	  
		TIM_ClearITPendingBit(TIM2,TIM_IT_CC2);	 
		//进入捕获中断,由于需要捕获比较的是高电平的时间,所以第一次捕获为上升沿,
		//检测是否是上升沿使用IO的输入电平来判断
		if(REMOTE_DATA)					//此时是高电平,说明是上升沿检测,
		{
			//为了统计高电平的时间,进入后需要将计数器的值清零计数一次是1US
			TIM_SetCounter(TIM2,0);  //表示计数值清零
			//切换为下降沿捕获
			TIM_OC2PolarityConfig(TIM2,TIM_ICPolarity_Falling);	
		}
		else       //说明是下降沿的捕获中断,
		{
			//此时需要判断是否为起始(同步头)信号
			//获取此时计数器的值,也就是高电平的持续时间
			Infrared_Receive.ccr = TIM_GetCapture2(TIM2);//获取当前计数值
			TIM_OC2PolarityConfig(TIM2,TIM_ICPolarity_Rising);//切换为上升沿捕获
			
			if(Infrared_Receive.start == 1)     //如果已经接收过同步头
			{
				if(Infrared_Receive.ccr>300 && Infrared_Receive.ccr<800)//560us高电平说明是数据0
				{
						Infrared_Receive.data=Infrared_Receive.data>>1;//低位在前接收后的格式为:8位控制码反码+8位控制码+8位地址码反码+8位地址码
				}
				else if(Infrared_Receive.ccr>1300 && Infrared_Receive.ccr<1900)//1680us说明是数据1
				{
						Infrared_Receive.data=Infrared_Receive.data>>1;
						Infrared_Receive.data |=0x80000000;//接收数据为1时,给对应位补1
				}
			}
			else                           //没有接收到同步头,先判断是否有同步头 
			{
					if(Infrared_Receive.ccr>4000 && Infrared_Receive.ccr<5000)//检测到同步头信号
					{
						Infrared_Receive.start=1;
					}
			}
		}
	}    
}

7.处理数据帧

接收到32bit数据后,根据其帧格式,可以提取出我们需要的数据

/************************************************
函数功能:红外获取控制码
函数名:Infrared_Get_Data
函数形参:None
函数返回值:无控制-1  有控制码则返回控制码
备注:  
**************************************************/
s8 Infrared_Get_Data(void)
{
	u8 data;  //控制码
	
	if(Infrared_Receive.end==1)//检测到数据接收完毕
	{
		Infrared_Receive.end=0;//清除标志位,方便下次接收
		
		data = (Infrared_Receive.data & 0x00ff0000) >> 16;  //获取控制码
		
		return data;	
	}
	return -1;
}

然后就可以调用这个函数根据不同的输入值来执行对应的功能了。

实现效果

总结

关于定时器的输入捕获实现红外输入信号的处理就介绍到这里,文中如有不足之处欢迎批评指正。