前言

上一篇介绍了使用GPIO模拟时序实现I2C协议的功能,本文继续使用GPIO模拟的方式来实现一个平时生活中常用的模块——红外发射管的驱动,红外在家电的应用中十分广泛,空调、电视、投影等都是通过红外遥控来实现的。

红外发射管简介

红外发射管实物就是下图中左边的红框内的器件,右侧的是红外接收管。


大家拿到红外发射管后,第一反应可能就是这玩儿意和LED灯长得一模一样啊,其实,不光长得一样,其的工作原理也是一样,就是通过一定的规律给电平,点“亮”和熄灭红外发射管来产生红外光信号,这个红外光信号被接收管接受处理后,就会产生对应的电信号,然后用户接收和处理这个电信号用来控制不同的外设即可。只不过红外发射管的波长超过了我们肉眼可见的波段,我们平常事无法观察到的。如下图:我们肉眼可见的光波段就是380nm----760nm,而这个红外发射管的波长是940nm。


当然,如果实在是想看见点亮效果,也可以把周围的灯都关了,找一个比较黑的环境,用手机摄像头就可以看见红光。

NEC协议

上面提到了,红外发射管用的是一种特殊的方式进行数据发送,具体的发送格式需要参考红外接收管的芯片手册以及使用到的具体通信协议了,这里接收管使用的是HS0038,通信协议是NEC。

HS0038

先来看看HS0038的手册是怎么描述的,如下图所示,可以知道,当接收到的输入红外光是一个频率为38khz,占空比为50%,持续时间为500us-700us的PWM波形时,HS0038的输出端就会产生一个低电平,而当输入的红外光不是此规格的信号时,HS0038就会输出一个高电平。


来个实际波形分析一下:
如下图所示:D1是红外发射管发射出的光信号,D0是红外接收管根据输入信号产生的输出信号,可以看出,当D1的波形变成了PWM时,D0输出低电平,而D1输出不是PWM时,D0输出就是高电平。

NEC 的逻辑“1”与逻辑“0”

为啥我上面一直说的HS0038的输出是高低电平而不是逻辑0和逻辑1呢?
还记得之前驱动WS2812B与DHT11的时候,他们的逻辑0与逻辑1并不是简单的对应高低电平,这里也是如此,NEC协议中的逻辑0与逻辑1也不是简单的高低电平,而是有不同时间段的高低电平组合而成,其中:
逻辑“0”是由560us的低电平加560us的高电平组成,
逻辑“1”是由560us的低电平加1690us的高电平组成,
再结合下面的这个逻辑分析仪的图大家就可以看懂了,注意最下面绿色的解码,第一段低电平与第一段高电平组成了第一个逻辑0,
第二段低电平与第二段高电平又组成了一个逻辑0,
而第三段的低电平与第三段高电平组成的是逻辑1,


因为红外传输的工程中大部分设备都是支持NEC格式的,所以我们发射管端也要对应这个NEC码的时序做出调整,当需要输出逻辑1时,就要使用到下图的前半段时序,而输出逻辑“0”时,就需要使用到下图的后半段时序:
原文链接——http://t.csdn.cn/LoC2r

整个数据传输动态过程可以看下面的动图:
单片机控制开关管按照逻辑“0”与逻辑“1”的时序来控制红外发射管的点亮与关闭,而红外接收管接收到红外发射管的信号后就会输出对应的高低电平信息,使用MCU进行捕获处理这部分信号就可以了。

NEC的数据帧格式

再弄清楚了NEC协议的逻辑“0”与逻辑“1”后,就要知道其数据帧的具体格式了,为了进一步提高数据的稳定性,NEC码在传输的过程中对于数据帧也有着很高的要求,具体帧格式如下:


需要注意一点,前面的数据通信都是高位在前,高位先发,此处的NEC是低位在前,低位先发。

同步码头(引导码/起始码)、地址码(遥控ID)、地址反码、控制码(键值)、控制反码。
同步码、地址码(遥控ID)、地址反码、控制码(键值)、控制反码
同步码由一个 9ms 的低电平和一个 4.5ms 的高电平组成
如果接收到同步,说明有红外数据进来

同步码 + 8位地址码+ 8位地址码反码+ 8位控制码 + 8位控制码反码
红外接收每一次接收到数据都是32位

实际的数据:

帧头 地址码 地址码反码 命令 命令反码
9ms 的低电平+ 4.5ms 的高电平 8位 8位 8位 8位
9ms 的低电平+ 4.5ms 的高电平 1011 0111 0100 1000 0110 0110 1001 1001


除此之外还有一个连续发送的数据帧格式,这里就不做介绍了,有需要的自己去研究一下吧。

编程思路

搞清楚了红外发射管与接收管的通信过程以及其数据帧的格式后,接下来就可以开始编程了,本文先解决红外发射管的代码,接收管的使用留到下一篇介绍GPIO复用功能的时候再说。

1. GPIO管脚

首先第一步,二话不说,直接看原理图,找到GPIO是PB7,前面分析了,在发送红外光的时候,需要发送一段频率为38khz,占空比位50%,持续时间长度为560us的PWM,来使得红外接收管输出低电平,一般来说最好的解决方案就是使用定时器来产生PWM来实现低电平,当时间到了后再将GPIO口拉低不输出PWM即可实现
这里选择PB7管脚是可以作为定时器4的通道2来使用的,但是有一个问题,在实际使用过程中定时器4另有它用;所以没法了,只能使用GPIO模拟的办法来实现红外发射管的功能了,这里就需要模拟出一段PWM波形,当然是使用推挽模式配合延时来实现。


对于PWM实现的方案,这里给大吉留几个链接,供大家参考。
STM32 NEC红外遥控器解码——
基于STM32f103c8t6的红外接收发送
除此之外还有几篇写的很好地博客也给大家贴在这
STM32入门开发: 制作红外线遥控器(智能居家-万能遥控器)


GPIO初始化代码:

/************************************************
函数功能:红外发送控制码
函数名:Infrared_Send_Init
函数形参:None
函数返回值:
备注:  PB7
**************************************************/
void Infrared_Send_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;                       // 定义一个GPIO_InitTypeDef类型的变量
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);   // 允许GPIOB时钟
	
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;                  // GPIO_Pin_7
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;           // 通用推挽输出
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;          // 50MHz速度
	GPIO_Init(GPIOB, &GPIO_InitStruct);
	
	GPIO_ResetBits(GPIOB, GPIO_Pin_7);                      //PB7拉低
}
 

找到实现方案后,就该具体执行了,对于这类数据帧比较复杂的通信方式,在模拟其时序的时候一定记得从下往上来写,先搞定各个部分,然后再拼接起来组成一整个时序。
整个数据帧无非就是以下几个内容组成,
1.同步头,2.逻辑“0”与逻辑“1”;而且逻辑0与逻辑1还可以进一步细分为低电平和高电平,也就是一个PWM,一个非PWM。那么接下来就一个一个的实现。

2. 模拟同步头

由于接收管的同步头是9ms的低电平加上4.5ms的高电平,那么根据转换方式可以知道,发射管的同步头是9ms的38khz、50%占空比的PWM加上4.5ms的低电平。


38khz的PWM,一个脉冲的时长为1/38 000=26.3us;
占空比50%,则输出高电平的时间:26.3/2=13.15us由于STM32F103C8T6的系统滴答延时最多支持到us级别,所以这里把小数点后的给省略,直接高低电平各自延时13us。
而9ms的PWM一共有9ms/26=346个。
于是代码就有了:

/************************************************
函数功能:红外发送同步头
函数名:NEC_IE_Start
函数形参:None
函数返回值:
备注:  
// ①引导码:载波发射9ms,不发射4.5ms
**************************************************/
//------------------------------------------------------------
void NEC_IE_Start(void)
{
	u16 i;
// 载波发射 9ms ≈ 26.3us * 346
	for(i=0;i<346;i++) 
	{
		PBout(7)=1;	// IE抬高,发射红外光
		Systick_Delay_us(13);	// 延时13us
		PBout(7)=0;;	// IE拉低,不发射红外光
		Systick_Delay_us(13);	// 延时13us
	}
	// 载波不发射 4.5ms ≈ 26.3us * 171
	for(i=0;i<171;i++)
	{
		PBout(7)=0;	// IE拉低,不发射红外光
		Systick_Delay_us(26);	// 延时26us
	}
}
 

3.发送逻辑“0”与逻辑“1”

逻辑“0”与逻辑“1”的共有部分是560us的PWM波形,不同之处在于后面的部分,


先看逻辑“1”,除了560us的PWM,它还需要2.25ms-560us=1690us的低电平,PWM还是参考上面的思路,于是发送逻辑“1”的代码就有了:

/************************************************
函数功能:红外发送逻辑“1”
函数名:NEC_IE_Send_one
函数形参:None
函数返回值:
备注:  
// NEC协议数据"1" = 载波发射0.56ms + 载波不发射1.68ms
**************************************************/
//-------------------------------------------------
void NEC_IE_Send_one(void)
{
	u8 i;
	// 载波发射0.56ms ≈ 26.3us * 21
	//-------------------------------
	for(i=0;i<22;i++)
	{
		//26.3us(载波发射周期)占空比50%
		//------------------------------------
		PBout(7)=1;    // IE抬高,发射红外光
		Systick_Delay_us(13);	// 延时13us
		
		PBout(7)=0;  // IE拉低,不发射红外光
		Systick_Delay_us(13);	// 延时13us
		//------------------------------------
	}
	
	// 载波不发射1.69ms ≈ 26us * 65
	//--------------------------------
	for(i=0;i<65;i++)
	{
		//26us(载波不发射周期)
		//------------------------------------
		PBout(7)=0;	// IE拉低,不发射红外光
		Systick_Delay_us(26);	// 延时26us
	}
}

同样的逻辑“0”的思路也差不多,直接给出代码:


/************************************************
函数功能:红外发送逻辑“0”
函数名:NEC_IE_Send_zero
函数形参:None
函数返回值:
备注:  
// NEC协议数据"0"= 载波发射0.56ms + 载波不发射0.56ms
**************************************************/
//-------------------------------------------------
void NEC_IE_Send_zero(void)     
{
	u8 i;
	
	// 载波发射0.56ms ≈ 26.3us * 21
	//-------------------------------
	for(i=0;i<22;i++)
	{
        //26.3us(载波发射周期)
		//------------------------------------
		PBout(7)=1;	// IE抬高,发射红外光
		Systick_Delay_us(13);	// 延时13us                       
											
		PBout(7)=0;	// IE拉低,不发射红外光
		Systick_Delay_us(13);	// 延时13us
		//------------------------------------
	}

	
	// 载波不发射0.56ms ≈ 26.3us * 21
	//-------------------------------
	for(i=0;i<21;i++)
	{
        //26.3us(载波不发射周期)
		//------------------------------------
		PBout(7)=0;	// IE拉低,不发射红外光
		Systick_Delay_us(26);	// 延时26us
		//------------------------------------
	}
}
 

发送一个字节数据

搞定了逻辑“0”与逻辑“1”之后,下一步自然是封装一个可以一次发送8个位也就是一字节的函数。这里需要注意的是低位先发。


于是代码如下:

/************************************************
函数功能:红外发送一个字节
函数名:NEC_IE_One_Data
函数形参:u8 IE_One_Data
函数返回值:
备注:  发送一个字节的数据,一次发8位,低位在前。
**************************************************/
//----------------------------------------------------------------------------------------------
void NEC_IE_One_Data(u8 IE_One_Data)
{
	u8 i;
	
	for(i=0;i<8;i++)
	{
		if( IE_One_Data & 0x01 )
			NEC_IE_Send_one();
		else
			NEC_IE_Send_zero();
			
		IE_One_Data>>=1;
	}
}
 

发送一帧数据

一帧数据格式在前面已经介绍了,这里根据帧格式配合上面的函数做一个组合就可以了。
代码如下:


/************************************************
函数功能:发送NEC 一帧数据
函数名:NEC_IE_code_message
函数形参:u8 IE_One_Data
函数返回值:
备注: 
// 将一帧数据调制为NEC协议规定的红外载波发射出去
// 一帧数据格式:低位在前,由低到高发送8位数据
// 这帧数据是:②8位用户码字节(8位) / ③8位用户码反码(8位) / ④8位数据码 / ⑤8位数据码的反码
**************************************************/
void NEC_IE_code_message(u8 user_code_8bit, u8 data_code_8bit)
{
	// ①引导码:载波发射9ms,不发射4.5ms
	NEC_IE_Start();
	//------------------------------------------------------------
	//------------------------------------------------------------
	// ②8位用户码	:8位数据
	NEC_IE_One_Data(user_code_8bit);
	
	// ③8位用户码的反码:8位数据
	NEC_IE_One_Data(~user_code_8bit);
	
	// ④8位数据码 :8位数据
	NEC_IE_One_Data(data_code_8bit);
	
	// ⑤8位数据码的反码:8位数据
	NEC_IE_One_Data(~data_code_8bit);
	//------------------------------------------------------------
	// ⑥结束码‘0’
	//--------------
	NEC_IE_Send_zero();
	//--------------
}
 

结束码

关于结束码,由于32位数据发送完毕后,不管最后一位的逻辑是0还是1,末尾都是PBout(7)=0; // IE拉低,不发射红外光;理论来说,这种情况不额外添加结束码是可行的;但是笔者尝试了一下,不再末尾加一个发送逻辑“0”的结束码的话,逻辑分析仪无法解析出数据,为了能够正常解析出数据还是在发送外32位数据后加上一句NEC_IE_Send_zero()。
需要注意,实际上的遥控器大部分是没有这个结束信号的。


而对于连续码的时序,如下图,有兴趣的话可以自己尝试,调通后可以投食一下,我想白嫖。

既然搞定了红外发送的基本用法,最大的想法自然是控制空调之类的设备了,但是需要注意,各个厂家的空调的协议不太一样,需要在上面代码的基础上进一步修改才行。
具体的介绍大家参考这些大佬的——
STM32入门开发: 制作红外线遥控器(智能居家-万能遥控器)
格力空调红外编码解析
STM32解析美的空调红外遥控器
格力空调红外编码

现象

这里笔者还没去倒腾空调的编码,只是实现了简单的功能,最后捕捉到的实际波形和解析出来数据如下:

总结

关于使用GPIO模拟时序驱动红外发射管的介绍就先记录到这,至于空调的控制,这个先鸽了,或者有做出来的可以踢我一下。文中如有不足欢迎批评指正。