前言
本文带你无前提条件,从零模仿破解美的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、逻辑分析仪的使用以及驱动的编写。希望不拘泥于一个知识点而是有一个整体的大局观。