破解红外发射-美的空调实战篇

本文详细介绍了如何使用STM32通过HAL库和定时器模拟美的RN02D/BG空调遥控器的红外信号发送,包括解析遥控器波形、配置定时器PWM模式、编写延时函数以及驱动代码实现。通过逻辑分析仪抓取波形,分析协议格式,最终成功实现空调的远程控制。
摘要由CSDN通过智能技术生成

前言

本文带你无前提条件,从零模仿破解美的RN02D/BG型号空调遥控器发送原理。

1.教程的直接目的:使用stm32+远红外发射管开启空调。
2.包含学习知识:通过本文你可以了解stm32hal库使用定时器提供us级延迟方法,使用定时器pwm功能方法,美的空调驱动的编写思想。以及一些硬件相关的介绍。

开始!

1基础入手

首先我们要清晰我们的最终目的是模仿空调的红外头,发出一样的波形来骗过空调接收,空调只是忠实的执行监测程序,一样的波形没有不接受的道理。如果没反应说明你波形还是有问题。

作者拆开了遥控器,简单查看了一下,如图。
明显看到红外发射管的位置

背板布线还是挺漂亮的,mcu部分点了较,也可以做到防护作用。直接掏出逻辑分析仪!简单试了试发现应该是红外二极管的阴极电平会变化,阳极直接接到vcc了。这样我们就可以抓到一些波形了。
通过查找网上信息,这种红外的编码一般都有帧头帧尾和中间的数据位等部分。但实际网上的协议时序与本品牌的遥控器并不能对上,只是类似。所以只能自己观察,扒一下协议。
在这里插入图片描述

2详细分析&注意事项

2.1软件部分

这里主要参考这个博主的文章,可以说协议格式是相同的,但是细节是不同的,没关系,逻辑分析仪抓好自己慢慢数一数。
通过波形可以看出,如果我们想模拟遥控器,我们应当模拟一个载波,然后再想办法根据协议控制载波的开关时间。

额外插一句:载波是固定评率的波形,然后他的开启与关闭时间不同构成了01的区别,这其实是编码方式,对编码方式感兴趣的同学可以搜索m2编码或者米勒编码学习。顺便博主最近在研究计网。这些都是属于物理层的协议哦!不是简单的高电平表示1 低电平表示0哦!

这里要注意的是,没有必要开启定时器频繁进入中断开关io电平,以达到模拟载波的目的。因为stm32已经给我们提供了定时器的pwm模式。这里通过cubemx开启一个定时器,设置好他的pwm模式如图。这里使用的定时器16只有一个通道ch1 可以复用a6引脚,目前我们的小熊派上a6并没有使用,那就用他作为输出吧。本文我是用的是基于stm32l431rct6芯片的小熊派开发板。cubemx开启tim16,参数的设置如图,注意!我的定时器总线时钟设置为最高的80Mhz,这里预分频系数选择80-1 = 79,这样tim16的时钟频率就是1m,也就是每1us一个时钟。这样方便计算。下面两个箭头指示的值设置为26与8,
这样我们就可以获得一个周期是26us,其中9us是高电平的pwm载波波形了!
在这里插入图片描述
其实cubemx帮我们借助上面的图形化界面生成了三个主要函数:
void MX_TIM16_Init(void);
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle);
void HAL_TIM_MspPostInit(TIM_HandleTypeDef* timHandle);

注意事项1:tim使用时,或者说cubemx使用时,我们必须清醒的认识到它帮助我们生成的函数其实包括两种:一种是MX_xxinit这种函数,是他帮你整理好的函数,你可以随意改动,因为只是里面调用的hal库函数,名字无所谓。另外一种是如msp函数。此类函数名字是不可以修改的,因为他是一个_weak 修饰的函数,hal规定了必须叫这个名字。cubemx帮我们强实现了,这个回调函数乱改名字hal是不承认的,他会继续掉_weak 函数用来避免编译错误,但其实什么也不做。

补充一点基础知识,回到注意事项,cubemx只会帮你做好一定的初始化任务,他不会帮你跑起来,所以如果你的定时器没有跑起来,请检查你是否使用了start函数。不同的功能需要采用不同的start函数
注意事项2:tim的pwm功能使用时要注意配置好正确的参数。请务必深刻理解预分频系数,重载值等概念。否则你得不到合适的pwm波。

下一步我们需要在开一个定时器用来实现us级别的延时。这里开启tim2定时器。同样给出cubemx的配置截图:
在这里插入图片描述
此外我们要在tim.c文件中实现一个us级别的延时供上层使用。这里一并贴出tim.c文件全部代码。
注意事项3:别忘记在tim头文件中声明延时函数哦!

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    tim.c
  * @brief   This file provides code for the configuration
  *          of the TIM instances.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "tim.h"

/* USER CODE BEGIN 0 */
#include <stdio.h>
int timecnt = 0;
/* USER CODE END 0 */

TIM_HandleTypeDef htim2;
TIM_HandleTypeDef htim16;

/* TIM2 init function */
void MX_TIM2_Init(void)
{

  /* USER CODE BEGIN TIM2_Init 0 */

  /* USER CODE END TIM2_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM2_Init 1 */

  /* USER CODE END TIM2_Init 1 */
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 79;
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 65535;
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM2_Init 2 */
	HAL_TIM_Base_Start_IT(&htim2);                     //重点关注 这块不加使能中断就不会跑起来
  /* USER CODE END TIM2_Init 2 */

}
/* TIM16 init function */
void MX_TIM16_Init(void)
{

  /* USER CODE BEGIN TIM16_Init 0 */

  /* USER CODE END TIM16_Init 0 */

  TIM_OC_InitTypeDef sConfigOC = {0};
  TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};

  /* USER CODE BEGIN TIM16_Init 1 */

  /* USER CODE END TIM16_Init 1 */
  htim16.Instance = TIM16;
  htim16.Init.Prescaler = 79;
  htim16.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim16.Init.Period = 25;
  htim16.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim16.Init.RepetitionCounter = 0;
  htim16.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim16) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_Init(&htim16) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 8;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
  sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
  if (HAL_TIM_PWM_ConfigChannel(&htim16, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
  sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
  sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
  sBreakDeadTimeConfig.DeadTime = 0;
  sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
  sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
  sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
  if (HAL_TIMEx_ConfigBreakDeadTime(&htim16, &sBreakDeadTimeConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM16_Init 2 */
	
  /* USER CODE END TIM16_Init 2 */
  HAL_TIM_MspPostInit(&htim16);

}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{

  if(tim_baseHandle->Instance==TIM2)
  {
  /* USER CODE BEGIN TIM2_MspInit 0 */

  /* USER CODE END TIM2_MspInit 0 */
    /* TIM2 clock enable */
    __HAL_RCC_TIM2_CLK_ENABLE();

    /* TIM2 interrupt Init */
    //HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
    //HAL_NVIC_EnableIRQ(TIM2_IRQn);
  /* USER CODE BEGIN TIM2_MspInit 1 */

  /* USER CODE END TIM2_MspInit 1 */
  }
  else if(tim_baseHandle->Instance==TIM16)
  {
  /* USER CODE BEGIN TIM16_MspInit 0 */

  /* USER CODE END TIM16_MspInit 0 */
    /* TIM16 clock enable */
    __HAL_RCC_TIM16_CLK_ENABLE();
  /* USER CODE BEGIN TIM16_MspInit 1 */

  /* USER CODE END TIM16_MspInit 1 */
  }
}
void HAL_TIM_MspPostInit(TIM_HandleTypeDef* timHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(timHandle->Instance==TIM16)
  {
  /* USER CODE BEGIN TIM16_MspPostInit 0 */

  /* USER CODE END TIM16_MspPostInit 0 */

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**TIM16 GPIO Configuration
    PA6     ------> TIM16_CH1
    */
    GPIO_InitStruct.Pin = GPIO_PIN_6;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    GPIO_InitStruct.Alternate = GPIO_AF14_TIM16;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* USER CODE BEGIN TIM16_MspPostInit 1 */

  /* USER CODE END TIM16_MspPostInit 1 */
  }

}

void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
{

  if(tim_baseHandle->Instance==TIM2)
  {
  /* USER CODE BEGIN TIM2_MspDeInit 0 */

  /* USER CODE END TIM2_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_TIM2_CLK_DISABLE();

    /* TIM2 interrupt Deinit */
    HAL_NVIC_DisableIRQ(TIM2_IRQn);
  /* USER CODE BEGIN TIM2_MspDeInit 1 */

  /* USER CODE END TIM2_MspDeInit 1 */
  }
  else if(tim_baseHandle->Instance==TIM16)
  {
  /* USER CODE BEGIN TIM16_MspDeInit 0 */

  /* USER CODE END TIM16_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_TIM16_CLK_DISABLE();
  /* USER CODE BEGIN TIM16_MspDeInit 1 */

  /* USER CODE END TIM16_MspDeInit 1 */
  }
}

/* USER CODE BEGIN 1 */
void delayXus(uint16_t us)

{

	uint16_t differ=0xffff-us-5;					//设定定时器计数器起始值

	__HAL_TIM_SET_COUNTER(&htim2,differ);

	HAL_TIM_Base_Start(&htim2);					//启动定时器

  while(differ<0xffff-6)							//补偿,判断

  {

    differ=__HAL_TIM_GET_COUNTER(&htim2);			//查询计数器的计数值

  }

  HAL_TIM_Base_Stop(&htim2);


}
/* USER CODE END 1 */

	

好了!现在我们的基础已经搭好了,我们开始进入驱动编写过程。首先,我们可以把一次要发出的波形分成帧头 帧尾 帧分割 数据位四种,每种其实都是一段载波加一段低电平构成。
所以可以先把他们先写出来。另外三个只需要写死时间即可。但是数据位要区分写1还是0,所以writebit应该具有一个参数,我们得到了一个写bit位的函数以后肯定要继续封装,用来写1byte。其实整个协议就是帧头 六个字节 帧尾 分割帧 帧头 六个重复的字节 帧尾构成。所以我们可以继续封装一层,这样只需要提供一个数组就可以执行一次按键动作了。
talk is cheap show my code
ir_air.c文件代码


#include <stdio.h>
#include "tim.h"
#include "main.h"
#include "ir_air.h"

//发送一段载波
void ir_send_single(uint16_t hightime,uint16_t lowtime)
{
	//这里默认你的对应tim16是可用的,所以请先调试好对应定时器的pwm功能
	HAL_TIM_PWM_Start(&htim16,TIM_CHANNEL_1);
	delayXus(hightime);
	HAL_TIM_PWM_Stop(&htim16,TIM_CHANNEL_1);
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_6,GPIO_PIN_RESET);
	delayXus(lowtime);
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_6,GPIO_PIN_SET);
}
//发送消息头命令
void ir_send_head(void)
{		
	uint16_t hightime = 4360;
	uint16_t lowtime = 4460;
	ir_send_single(hightime,lowtime);
}
//发送消息尾部命令
void ir_send_tail(void)
{		
	uint16_t hightime = 477;
	uint16_t lowtime = 0;
	ir_send_single(hightime,lowtime);
}
//发送重复消息分割符号命令
void ir_send_cut(void)
{		
	uint16_t hightime = 477;
	uint16_t lowtime = 5277;
	ir_send_single(hightime,lowtime);
}
//发送1bit
uint8_t ir_send_bit(uint16_t value)
{	
	uint8_t ret = 0;
	uint16_t hightime;
	uint16_t lowtime;
	if(0x00 == value)
	{		
		hightime =478;//写0,载波478us,低电平600us
		lowtime = 600;
	}
	else if(0x01 == value)
	{			
		hightime =478;//写1,载波478us,低电平1700us
		lowtime = 1700;
	}else
	{
		printf("ir_send_bit error!\r\n");
		ret  = 1;
		return ret;
	}
		
	ir_send_single(hightime,lowtime);
	return ret;
}
//发送1字节
uint8_t ir_send_byte(uint8_t value)
{	
	uint8_t temp;
	uint8_t ret = 0;
	temp = value;
	for (int i=0;i<8;i++)
	{
		temp = (value>>i)&0x01;
		ret = ir_send_bit(temp);
		if(ret != 0)
		{
			return ret;
		}
	}
	return ret;
}
//发送打开数据 25° 二级风 制冷模式
uint8_t ir_send_open(void)
{
	ir_send_head();
	ir_send_byte(0x4d);
	ir_send_byte(0xb2);
	ir_send_byte(0xf9);
	ir_send_byte(0x06);
	ir_send_byte(0x03);
	ir_send_byte(0xfc);
	ir_send_cut();
	
	ir_send_head();//协议规定要重复发一次
	ir_send_byte(0x4d);
	ir_send_byte(0xb2);
	ir_send_byte(0xf9);
	ir_send_byte(0x06);
	ir_send_byte(0x03);
	ir_send_byte(0xfc);
	ir_send_tail();
	return 0;

}


ir _air.h代码块

#ifndef __IR_AIR_H__
#define __IR_AIR_H__
#include "tim.h"
#include "main.h"


//发送一段载波
void ir_send_single(uint16_t hightime,uint16_t lowtime);
void ir_send_head(void);
void ir_send_tail(void);
uint8_t ir_send_bit(uint16_t value);
uint8_t ir_send_byte(uint8_t value);
uint8_t ir_send_open(void);
#endif  /* __IR_AIR_H__ */


注意事项4:
上面的代码在使用定时器的过程中:使用tim2存在一句:HAL_TIM_Base_Start(&htim2);
使用tim16存在一句:HAL_TIM_PWM_Start(&htim16,TIM_CHANNEL_1);
这一点很关键,我们设置好tim不代表他会跑起来,需要使用对应的模式的start语句开始定时器。更多信息请查看hal库代码和其他教程。这里只是提示大家注意检查。因为如果出现定时器跑的不对的情况,萌新可能不敢确定是调用错误还是设置错误。

软件部分就大体如此,作者追求简单的验证效果,并没有详细的探索全部的数据位代表的含义
抓取分析的过程文本如图
一帧数据包括两次传输,是重复传输。所以一次按键实际传送了6个有效字节。其中246字节是135字节的反码。包括信息的只有3字节。并且第一字节是固定的头。所以其实1 2字节是不变的。初步的结论是,第二字节代表不同风速,第五个字节前四位代表工作模式,后四位代表温度。
注意 这里的bit顺序是逻辑分析仪上的时间顺序!与代码中有效位顺序正好相反!

2.2硬件部分

必须承认,作者硬件资源及其有限,甚至没有一个红外发射管,只好将hellobug开发板上的红外管拆了下来,他的原理图如图
在这里插入图片描述

一个非常简单的电路,没什么好说的。不过作者手里的8050和电阻都是贴片封装的,非常小,于是决定借鸡生蛋,使用历史遗留pcb承载三极管和电阻。
在这里插入图片描述

左侧的两根排针要链接到红外发射管上,两个挨着的排针是GND,R13是10k的电阻,D1是100R。右上方的针脚连接到a6引脚即可,条件就是这么的艰苦,没办法:)

总结

其实模拟遥控器只是玩票之作。主要练习使用hal库、cubemx、逻辑分析仪的使用以及驱动的编写。希望不拘泥于一个知识点而是有一个整体的大局观。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值