DIY:制作一个语音识别的空调遥控器

  夏天到了,空调简直是我们的救命神器,但是对于丢三落四的我,总是因为找不到遥控器而烦恼,所以花了一天一夜的时间动手用单片机做了一个语音识别空调遥控器,把它24小时放在空调底下,从此再也不怕遥控器找不到了~由于除了遥控器最基本的功能以外,甚至还可以加上自己定义的功能,比如循环开半小时再关半小时,用来省电;或者根据环境温度调节空调温度等等等等~
效果图:
Alt

1 红外遥控器原理

  红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计算机系统中。
   红外遥控的发射电路是采用红外发光二极管来发出经过调制的红外光波;红外接收电路由红外接收二极管、三极管或硅光电池组成,它们将红外发射器发射的红外光转换为相应的电信号,再送后置放大器。
  发射机一般由指令键(或操作杆)、指令编码系统、调制电路、驱动电路、发射电路等几部分组成。当按下指令键或推动操作杆时,指令编码电路产生所需的指令编码信号,指令编码信号对载波进行调制,再由驱动电路进行功率放大后由发射电路向外发射经调制定的指令编码信号。
接收电路一般由接收电路、放大电路、调制电路、指令译码电路、驱动电路、执行电路(机构)等几部分组成。接收电路将发射器发出的已调制的编码指令信号接收下来,并进行放大后送解调电路,解调电路将已调制的指令编码信号解调出来,即还原为编码信号。指令译码器将编码指令信号进行译码,最后由驱动电路来驱动执行电路实现各种指令的操作控制(机构)。
  红外遥控的编码目前广泛使用的是:NEC Protocol 的 PWM(脉冲宽度调制)和 Philips RC-5 Protocol 的 PPM(脉冲位置调制)。这里我抓取了我自己奥克斯空调遥控器的指令波形,发现使用的是NEC协议脉宽调制。
Alt
  NEC 码的位定义:一个脉冲对应 560us 的连续载波,一个逻辑 1 传输需要 2.25ms(560us脉冲+1680us 低电平),一个逻辑 0 的传输需要 1.125ms(560us 脉冲+560us 低电平)。而遥控接收头在收到脉冲的时候为低电平,在没有脉冲的时候为高电平,这样,我们在接收头端收到的信号为:逻辑 1 应该是 560us 低+1680us 高,逻辑 0 应该是 560us 低+560us 高。数据均采用8bit传输方式,低位在前高位在后。
Alt
  需要注意的是,这里接收器解码出的高电平,实际上发射器是灭掉状态;低电平才是发射器亮起状态。而且发射器端是要用38KHz的载波驱动的,并不是简单的高电平驱动。这一点一定要注意。

2 红外遥控器的制作流程

2.1 遥控器指令解码

  解码遥控器指令不是必须的,但这一步可以加深对协议的理解,方便后续代码的编写。如果你在网上能找到你所复制的遥控器的协议编码方式,可以省略这一步。
  首先,用逻辑分析仪抓取红外接收器解调出的信号,大体了解一下协议格式。
Alt
  比如上图中红色长方形内的数据为0b11110110,因为是低位在前,所以这个数据为0x6F,同理黄色长方形内的数据为0xE0。这个步骤主要是为了后面用单片机进行解码时,对结果正确性进行校验。
  根据不同功能下的指令差异,可以分析出每个字节的每一位代表的含义。比如我的AUX空调遥控器的命令分析如下。主流的空调品牌的协议应该在网上可以找得到。
Alt

2.2 单片机解码代码编写

  我的红外接收器和发射器是淘宝随便买的,单片机用的是华大的HC32L130,当然这些都是可以自己选择的,成本越低越好。因为驱动红外发射器用的载波只需要38KHz,对单片机的时钟频率要求也不是很高。
  考虑到由于协议为PWM调制,所以需要分析占空比来确定每一位代表的含义,但实际上只需要看高电平的时间就可以确定这一位的含义,所以采用PWM捕获来捕获上升沿,捕获到之后马上把捕获类型改为下降沿捕获。这样两次捕获之间的计数器的值除以时钟频率就是高电平的时间。除了SYNC信号外,每8个bit为一个字节数据,数据存放在一个数组buffer中。由于AUX遥控器为13byte一条指令,所以接收到13byte之后在主函数里把接收到的数据打印出来并重置buffer。代码示例如下:

初始化函数:

void PwmCaptureInit(void)
{
	stc_pcacfg_t  PcaInitStruct;
    stc_gpio_cfg_t GpioInitStruct;

    Sysctrl_SetPeripheralGate(SysctrlPeripheralGpio, TRUE);
    Sysctrl_SetPeripheralGate(SysctrlPeripheralPca, TRUE);  
	
    //PA07设置为PCA_CH1
    GpioInitStruct.enDrv  = GpioDrvH;
    GpioInitStruct.enDir  = GpioDirIn;
    Gpio_Init(GpioPortA, GpioPin7, &GpioInitStruct);
    Gpio_SetAfMode(GpioPortA,GpioPin7,GpioAf2);
      
    PcaInitStruct.pca_clksrc = PcaPclkdiv32;	//32MHz / 32 = 1MHz
    PcaInitStruct.pca_cidl   = FALSE;
    PcaInitStruct.pca_ecom   = PcaEcomDisable;   
    PcaInitStruct.pca_capp   = PcaCappEnable;        //上升沿捕获
    PcaInitStruct.pca_capn   = PcaCapnDisable;        //禁止下降沿捕获
    PcaInitStruct.pca_mat    = PcaMatDisable; 
    PcaInitStruct.pca_tog    = PcaTogDisable;  
    PcaInitStruct.pca_pwm    = PcaPwm8bitDisable;   
    PcaInitStruct.pca_epwm   = PcaEpwmDisable;   
    Pca_M1Init(&PcaInitStruct);    
	Pca_ClrItStatus(PcaCcf1);
    Pca_ConfModulexIt(PcaModule1, TRUE);
    EnableNvic(PCA_IRQn, IrqLevel3, TRUE);
	Pca_StartPca(TRUE);
}

中断服务函数:

uint8_t  remoteStatus = 0;
uint16_t captureValue = 0;		//下降沿时计数器的值
uint8_t  captureCount = 0;	//按键按下的次数
uint8_t cmdBuf[13] = { 0 };//每条命令有13bytes
uint8_t cmdNum = 0;//当前是第几条命令


uint8_t bitCnt = 0;
void Pca_IRQHandler(void)
{
	M0P_TIM0_MODE0->M0CR &= ~1;	//停止并清零定时器
	M0P_TIM0_MODE0->CNT = 0;
	
    if(Pca_GetItStatus(PcaCcf1) != FALSE)
    {
		if(Gpio_GetInputIO(GpioPortA, GpioPin7) == TRUE)//捕获到上升沿后,标记捕获状态并切换为下降沿捕获
		{
			M0P_PCA->CCAPM1_f.CAPP = 0;
			M0P_PCA->CCAPM1_f.CAPN = 1;	//切换为下降沿捕获
			Pca_SetCnt(0);
			remoteStatus |= 0X10;
		}
		else	//捕获到下降沿,判断当前接收到的是什么指令码,并回到上升沿捕获
		{
			captureValue = Pca_GetCcap(PcaModule1);
			M0P_PCA->CCAPM1_f.CAPP = 1;
			M0P_PCA->CCAPM1_f.CAPN = 0;	//切换为上升沿捕获
			if(remoteStatus & 0X10)				//前面捕获到上升沿
            {
                if(remoteStatus & 0X80) //当前命令存在合法的SYNC码头
                {
                    if(captureValue > 300 && captureValue < 800)	//560us
                    {
                        cmdBuf[cmdNum] >>= 1;					//接收到0,右移一位
						bitCnt++;
                    }
                    else if(captureValue > 1400 && captureValue < 1900)	//1680us
                    {
                        cmdBuf[cmdNum]  >>= 1;					//接收到1,左移一位并存在最高位
                        cmdBuf[cmdNum]  |= 0x80;			
						bitCnt++;
                    }
                    else if(captureValue > 2200 && captureValue < 2700)	//2.5ms,按键次数增加
                    {
                        captureCount++; 		
                        remoteStatus &= 0XF0;				//清空计时器
                    }
					if(bitCnt==8)
					{
						bitCnt = 0;
						cmdNum++;
					}
                }
                else if(captureValue > 4200 && captureValue < 4700)		//4.5ms SYNC
                {
                    remoteStatus |= 0X80;				//SYNC状态位置1
                    captureCount = 0;					//清除按键次数计数器
                }
            }
            remoteStatus &= ~0x10;	//等待下一次上升沿捕获
		}
    }
	Pca_ClrItStatus(PcaCcf1);
	Pca_ClrItStatus(PcaCf);
	M0P_TIM0_MODE0->M0CR |= 1;	//定时器重新开始计数
}

2.3 单片机编码发送代码编写

  那么到这里就可以按照协议来发送我们的命令了。注意驱动红外二极管要用38KHz的载波驱动。因为这个载波频率不需要特别精准,所以我只是用软件模拟了一个方波。需要精准的PWM驱动可以用定时器/硬件PWM实现。
驱动代码:

void RemoteGpioInit(void)
{
	stc_gpio_cfg_t    GpioInitStruct;
    DDL_ZERO_STRUCT(GpioInitStruct);
    
    Sysctrl_SetPeripheralGate(SysctrlPeripheralGpio, TRUE);
    
    //PB9作为红外输出data脚
    GpioInitStruct.enDrv  = GpioDrvH;
    GpioInitStruct.enDir  = GpioDirOut;
    Gpio_Init(GpioPortB, GpioPin9, &GpioInitStruct);
}


void IRSend_1(void)
{
	uint32_t cnt = 0x2C;
	while(cnt--)	//38KHz,560us
	{
		M0P_GPIO->PBOUT ^= 0x200;
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
		__nop();__nop();
	}
}

void IRSend_0(void)
{
	M0P_GPIO->PBOUT &= ~0x200;
	delay10us(30);//延时函数不准,这里应该是560us
}

发送代码:

void RemoteSend(uint8_t* data,uint16_t len)
{
	REMOTE_SEND_SYNC;
	while(len--)
	{
		RemoteSendByte(*data);
		data++;
	}
	REMOTE_SEND_END;
}
//低位先发
void RemoteSendByte(uint8_t data)
{
	int i=0;
	for(;i<8;i++)
	{
		if(data & 0x01)
		{
			REMOTE_SEND_1;
		}
		else
		{
			REMOTE_SEND_0;
		}
		data >>= 1;
	}
}

  到此为止,红外线收发环境已经搭建完成。

2.4 语音识别模块配置

  这里我选择的语音识别模块为天问TW-ASR,这个模块我用起来感觉还是很容易上手的。可以图形化编程,语音识别也比较准。在模块定制的编程工具上写好你要识别的语音,并把语音匹配成功的行为设置好(比如通过串口发送数据)。
在这里插入图片描述

2.5 单片机串口模块配置+红外发射

  语音模块识别成功以后,通过串口将指令发送给单片机。我们用单片机接收到其发送的指令,每接收到13字节就判断接受到的指令含义,并把对应的命令通过红外二极管发送给空调,这样一套完整的语音识别遥控器方案就大功告成了!
串口初始化:

void UartInit(int baud)
{
	stc_uart_cfg_t  stcCfg;
    stc_uart_baud_t stcBaud;
	stc_gpio_cfg_t stcGpioCfg;

    DDL_ZERO_STRUCT(stcGpioCfg);
    DDL_ZERO_STRUCT(stcCfg);
    DDL_ZERO_STRUCT(stcBaud);

    Sysctrl_SetPeripheralGate(SysctrlPeripheralUart1,TRUE); ///<使能UART1外设时钟门控开关

    ///<UART Init
    stcCfg.enRunMode        = UartMskMode1;                 ///<模式3
    stcCfg.enStopBit        = UartMsk1bit;                  ///<1bit停止位
    stcCfg.enMmdorCk        = UartMskDataOrAddr;            ///<多机模式时
    stcCfg.stcBaud.u32Baud  = baud;                         ///<波特率9600
    stcCfg.stcBaud.enClkDiv = UartMsk8Or16Div;              ///<通道采样分频配置
    stcCfg.stcBaud.u32Pclk  = Sysctrl_GetPClkFreq();        ///</<获得外设时钟(PCLK)频率值
    Uart_Init(M0P_UART1, &stcCfg);                          ///<串口初始化

    ///<UART中断使能
    Uart_ClrStatus(M0P_UART1,UartRC);                       ///<清接收请求
    Uart_ClrStatus(M0P_UART1,UartTC);                       ///<清接收请求
    Uart_EnableIrq(M0P_UART1,UartRxIrq);                    ///<使能串口接收中断
    EnableNvic(UART1_IRQn, IrqLevel2, TRUE);              ///<系统中断使能
	
	Sysctrl_SetPeripheralGate(SysctrlPeripheralGpio,TRUE);
	stcGpioCfg.enDir = GpioDirOut;
	Gpio_Init(GpioPortA,GpioPin2,&stcGpioCfg);
    Gpio_SetAfMode(GpioPortA,GpioPin2,GpioAf1);  
	
    stcGpioCfg.enDir = GpioDirIn;
    Gpio_Init(GpioPortA,GpioPin3,&stcGpioCfg);
    Gpio_SetAfMode(GpioPortA,GpioPin3,GpioAf1);    
}

串口中断:

char uartRecvDataBuf[20];
uint16_t uartRecvDataNum;
uint8_t uartRxFlg=0;
void Uart1_IRQHandler(void)
{
	if(Uart_GetStatus(M0P_UART1, UartRC))
    {
		uartRecvDataBuf[uartRecvDataNum++] = Uart_ReceiveData(M0P_UART1);
		uartRxFlg = 1;
    }
	Uart_ClrStatus(M0P_UART1, UartRC);
}

数据解析:

#define COMMAND_AMOUNT	36
char* commands[COMMAND_AMOUNT] = 
{
	"TURN ON\r\n","TURN OFF\r\n",	//0 1
	"TEMP -1\r\n","TEMP +1\r\n",	//2 3
	"SPEED 1\r\n","SPEED 2\r\n","SPEED 3\r\n","SPEED 4\r\n","SPEED 5\r\n","SPEED AUTO\r\n",	//4 5 6 7 8 9
	"MODE HEAT\r\n","MODE COLD\r\n","MODE AUTO\r\n",	//10 11 12
	"WINDLR ON\r\n","WINDLR OFF\r\n","WINDUD ON\r\n","WINDUD OFF\r\n",	//13 14 15 16
	"TIMEON 1\r\n","TIMEON 2\r\n","TIMEON 3\r\n","TIMEON 4\r\n",	//17 18 19 20
	"TIMEOFF 0.5\r\n","TIMEOFF 1\r\n","TIMEOFF 1.5\r\n","TIMEOFF 2\r\n","TIMEOFF 2.5\r\n",	//21 22 23 24 25
	"TIMEOFF 3\r\n","TIMEOFF 3.5\r\n","TIMEOFF 4\r\n","TIMEOFF 4.5\r\n","TIMEOFF 5\r\n",	//26 27 28 29 30
	"TIMEOFF 5.5\r\n","TIMEOFF 6\r\n",	//31 32
	"TIMEONOFF  1 1\r\n","TIMEONOFF  0.5 0.5\r\n","TIMEONOFF  1 0.5\r\n",	//33 34 35
};
void AnalyzeCommand(void)
{
	int i=0;
	
	for(i=0;i<COMMAND_AMOUNT;i++)
	{
		if(strcmp(uartRecvDataBuf,commands[i]) == 0)//匹配
		{
			SendCommand(i);
		}
	}
}

指令发送:

uint8_t RemoteCommand[13] = {0xc3,0x8f,0xe0,0x00,0xe0,0x00,0x20,0x00,0x80,0x00,0x00,0x45,0x41};

void SendCommand(uint16_t cmdNum)
{
	uint16_t i = 0;
	uint8_t tmp = 0;
	
	switch(cmdNum)
	{
		case 0:	//TURN ON
			RemoteCommand[9] |= 1<<5;
			RemoteCommand[11] = 0x45;
			break;
		case 1://TURN OFF
			RemoteCommand[9] &= ~(1<<5);
			RemoteCommand[11] = 0x45;
			break;
		case 2://TEMP -1
			tmp = (RemoteCommand[1]>>3) - 1;
			RemoteCommand[1] &= ~(0xf8);
			RemoteCommand[1] |= tmp<<3;
			RemoteCommand[11] = 0x41;
			break;
		case 3://TEMP +1
			tmp = (RemoteCommand[1]>>3) + 1;
			RemoteCommand[1] &= ~(0xf8);
			RemoteCommand[1] |= tmp<<3;
			RemoteCommand[11] = 0x40;
			break;
		case 4:	//SPEED 1
			RemoteCommand[4] &= ~(0xe0);
			RemoteCommand[4] |= 0x3 << 5;
			RemoteCommand[11] = 0x44;
			break;
		case 5:
			RemoteCommand[4] &= ~(0xe0);
			RemoteCommand[4] |= 0x2 << 5;
			RemoteCommand[11] = 0x44;
			break;
		case 6:
			RemoteCommand[4] &= ~(0xe0);
			RemoteCommand[4] |= 0x1 << 5;
			RemoteCommand[11] = 0x44;
			break;
		case 7:
			RemoteCommand[4] &= ~(0xe0);
			RemoteCommand[4] |= 0x6 << 5;
			RemoteCommand[11] = 0x44;
			break;
		case 8:
			RemoteCommand[4] &= ~(0xe0);
			RemoteCommand[4] |= 0x5 << 5;
			RemoteCommand[11] = 0x44;
			break;
		case 9: //SPEED AUTO
			RemoteCommand[4] &= ~(0xe0);
			RemoteCommand[4] |= 0x7 << 5;
			RemoteCommand[11] = 0x44;
			break;
		case 10: //MODE HEAT
			RemoteCommand[6] &= ~(0xe0);
			RemoteCommand[6] |= 0x80;
			RemoteCommand[11] = 0x46;
			break;
		case 11: //MODE COLD
			RemoteCommand[6] &= ~(0xe0);
			RemoteCommand[6] |= 0x20;
			RemoteCommand[11] = 0x46;
			break;
		case 12: //MODE AUTO
			RemoteCommand[6] &= ~0xe0;
			RemoteCommand[6] |= 0x00;
			RemoteCommand[11] = 0x46;
			break;
		case 13: //WINDLR ON
			RemoteCommand[2] &= ~0xe0;
			RemoteCommand[11] = 0x43;
			break;
		case 14:
			RemoteCommand[2] |= 0xe0;
			RemoteCommand[11] = 0x43;
			break;
		case 15://WINDUD ON
			RemoteCommand[1] &= ~0x3;
			RemoteCommand[11] = 0x42;
			break;
		case 16:
			RemoteCommand[1] |= 0x3;
			RemoteCommand[11] = 0x42;
			break;
		case 17:	//TIMEON 1 2 3 4
		case 18:
		case 19:
		case 20:
			RemoteCommand[5] &= ~0x1F;
			RemoteCommand[4] &= ~0x1F;
			RemoteCommand[4] |= cmdNum - 16;
			RemoteCommand[11] = 0x4d;
			break;
		case 21: case 23: case 25: case 27: case 29: case 31:	//TIMEOFF 0.5 1.5 ...5.5
			RemoteCommand[5] |= 0x1F;	//0.5h
			RemoteCommand[4] &= ~0x1F;
			RemoteCommand[4] |= (cmdNum - 21) / 2;
			RemoteCommand[11] = 0x4d;
			break;
		case 22: case 24: case 26: case 28: case 30: case 32:	//TIMEOFF 1 2 3 4 5 6
			RemoteCommand[5] &= ~0x1F;
			RemoteCommand[4] &= ~0x1F;
			RemoteCommand[4] |= (cmdNum - 20) / 2;
			RemoteCommand[11] = 0x4d;
			break;
		case 33:
		case 34:
		case 35:
		default:
			return;
	}
	RemoteCommand[12] = 0;
	for(i = 0; i < 12; i++)
		RemoteCommand[12] += RemoteCommand[i];
	RemoteSend(RemoteCommand, 13);
}

  到此为止,一个完整的语音识别空调遥控器就完成了!需要更多资料可以关注公众号:嵌入式付呱呱,后台留言即可!

  • 7
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

emXiaoMing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值