简介:这份资源面向嵌入式开发初学者与智能家居爱好者,提供基于STM32微控制器驱动红外模块控制格力空调的完整工程实践。内容围绕STM32的GPIO推挽输出、定时器PWM波形生成、NEC红外编码与解码、格力遥控协议适配等核心知识点展开,帮助读者理解从信号编码到设备响应的完整链路。压缩包共226个文件,约8.35MB,以c源码、h头文件、o目标文件、crf交叉引用文件及d依赖文件为主,另含uvprojx工程文件、sct分散加载脚本、hex烧录文件与map映射文件,构成可直接编译调试的Keil工程结构。目前已有890人学习下载。读者可从中获取红外发射与接收的驱动代码、定时器配置范例、协议解析思路及工程目录组织方式,适合作为智能家居红外控制项目的参考模板,也可用于嵌入式通信与PWM应用的练手素材。
1. 空调遥控器丢了别急着买新的:STM32 加红外模块自己做一个
夏天最崩溃的事,不是空调坏了,是遥控器找不着了。翻遍沙发缝、抽屉底,最后发现被猫扒拉到床底下,电池还漏液了。买一个原装遥控器动辄几十上百,通用遥控器码库又对不上,按键布局还反人类。其实空调遥控的本质就是红外编码发射,格力空调的红外协议早就被逆向得明明白白,拿一块 STM32 最小系统板加一个几块钱的红外发射模块,自己写几十行代码就能替代原装遥控器,还能顺手做成定时开关、温度联动、手机蓝牙控制。这个项目适合手上有 STM32 开发板、想入门嵌入式红外通信的开发者,也适合家里空调遥控器已经阵亡、想顺便学点硬件的朋友。整条链路不复杂:STM32 产生 38kHz 载波调制信号,通过红外 LED 发射,空调接收后解码执行。难点不在代码量,在于时序精度和协议帧格式,下面把踩过的坑和能直接抄的代码都摊开讲。
2. 硬件选型与红外协议拆解:38kHz 载波和格力帧格式
2.1 为什么选 STM32 而不是 51 或 Arduino
红外发射对时序的要求比想象中苛刻。格力空调的红外协议采用脉冲位置调制,一个逻辑位的宽度在 0.56ms 到 1.69ms 之间,载波频率 38kHz,周期约 26.3μs。如果用软件延时来翻转 IO 口产生载波,主频低了根本来不及。51 单片机跑 12MHz 时一个机器周期 1μs,勉强能凑,但中断一打断时序就飘。Arduino 的tone()函数虽然能产生 38kHz,但没法精确控制每个脉冲的启停,发出来的帧空调经常不认。
STM32 的优势在于硬件定时器。用 TIM 的 PWM 输出模式,可以直接在引脚上产生占空比 1/3、频率 38kHz 的方波,CPU 只需要控制 PWM 的开启和关闭时间,就能精确调制出协议要求的脉冲序列。我一般用 TIM2 或 TIM3 的通道,预分频器和自动重装载值算好,输出比较模式配成 PWM1,占空比设 33% 左右。这样即使主循环里有其他任务,红外时序也不会被干扰。
选型上,STM32F103C8T6 最小系统板足够,红外发射模块用普通的 IR LED 加一个 NPN 三极管驱动,或者直接买带驱动电路的发射模块。接收端如果想做学习功能,加一个 VS1838B 一体化接收头,接在另一个定时器的输入捕获通道上。
2.2 格力红外协议帧结构
格力空调的红外编码格式属于自家变种,不是标准的 NEC 协议。一帧完整的指令由引导码、数据码和结束码组成。引导码是 9ms 的低电平加 4.5ms 的高电平,这个和 NEC 类似。数据码共 35 位,前 3 位是固定的 001,后面 32 位分成 4 个字节,依次是:模式与温度、风速与扫风、定时与睡眠、校验和。
每个逻辑位的表示方式:逻辑 0 是 0.56ms 低电平加 0.56ms 高电平,逻辑 1 是 0.56ms 低电平加 1.69ms 高电平。注意这里的低电平指的是红外 LED 导通发射载波的时间段,高电平是 LED 熄灭的时间段。接收头输出端会把这个极性反过来,所以写发射代码时别搞混。
校验和的计算方式是前三个字节相加取低 8 位。这个校验和如果算错,空调会直接忽略整帧,没有任何提示,调试时容易怀疑人生。
下面这张表是模式字节的高 3 位编码,温度占低 5 位,温度值 = 实际温度 - 16。
| 模式 | 编码(高3位) | 温度范围 |
|---|---|---|
| 自动 | 000 | 16~30 |
| 制冷 | 001 | 16~30 |
| 除湿 | 010 | 16~30 |
| 送风 | 011 | 不可调 |
| 制热 | 100 | 16~30 |
风速字节的高 2 位:自动 00、低速 01、中速 10、高速 11。扫风位在风速字节的 bit6,睡眠位在第三个字节的 bit7。这些位拼起来就是完整的控制指令。
2.3 硬件连接与供电注意
接线本身不复杂,但有几个地方容易翻车。IR LED 的正极接 3.3V 或 5V,负极接三极管集电极,三极管基极通过 1kΩ 电阻接 STM32 的 PWM 输出引脚,发射极接地。如果直接用 IO 口驱动 LED,电流一般不够,发射距离可能只有几厘米,空调根本收不到。用三极管驱动后,LED 峰值电流可以到 100mA 以上,发射距离能到 5 到 8 米。
VS1838B 接收头的输出脚接 STM32 的另一个定时器输入捕获通道,供电 3.3V,输出端加一个 10kΩ 上拉电阻。接收头解调后输出的是基带信号,和发射端的调制信号极性相反,写解码代码时要注意。
注意:IR LED 和接收头不要接在同一个电源轨上直接并联,发射时的电流突变会干扰接收头,导致解码误码。分开供电或者加 LC 滤波。
3. 核心代码实现:PWM 载波生成与帧发送
3.1 定时器 PWM 配置
先看载波部分的初始化。以 TIM3 通道 1 为例,系统时钟 72MHz,目标 38kHz,占空比 1/3。
// TIM3 PWM 初始化,输出 38kHz 载波 void IR_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // PB0 作为 TIM3_CH3 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 72MHz / 38kHz ≈ 1894.7,取预分频 0,重装载 1894 TIM_TimeBaseStructure.TIM_Period = 1894; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // PWM1 模式,占空比 1/3,比较值约 631 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 631; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC3Init(TIM3, &TIM_OCInitStructure); TIM_Cmd(TIM3, DISABLE); // 先不启动,发送时再开 }这段代码的关键参数是TIM_Period和TIM_Pulse。72MHz 除以 1894 约等于 38.01kHz,误差在接收头容忍范围内。TIM_Pulse设为 631 时占空比约 33%,这个占空比下 LED 发射功率和发热比较平衡。如果发现发射距离不够,可以适当加大占空比到 50%,但不要超过 50%,否则接收头可能解调异常。
3.2 微秒级延时与载波开关
发送帧的时候需要精确控制每个脉冲的持续时间,所以需要一个微秒级延时函数。用 SysTick 或者 DWT 都能做,这里用简单的循环延时,配合示波器校准。
// 粗略微秒延时,72MHz 下每次循环约 1us void delay_us(uint32_t us) { uint32_t i; for(i = 0; i < us * 8; i++) // 系数根据编译器优化级别调整 { __NOP(); } } // 打开 38kHz 载波 void IR_Carrier_On(void) { TIM_Cmd(TIM3, ENABLE); } // 关闭载波 void IR_Carrier_Off(void) { TIM_Cmd(TIM3, DISABLE); GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 确保引脚拉低,LED 不导通 }delay_us里的系数 8 不是固定的,跟编译器优化等级和主频有关。我一般用示波器抓一个 560μs 的脉冲,看实际宽度偏差多少,然后微调这个系数。如果手边没有示波器,可以用逻辑分析仪,或者拿接收头解调后的波形反推。
IR_Carrier_Off里把引脚拉低这一步不能省。TIM_Cmd 关闭后,引脚会保持关闭前的电平状态,如果刚好停在高电平,LED 会一直导通,不仅浪费电,还可能烧 LED。
3.3 发送一个逻辑位和完整帧
逻辑位的发送函数把载波开关和延时组合起来。
// 发送一个逻辑位 // bit: 0 或 1 void IR_Send_Bit(uint8_t bit) { IR_Carrier_On(); delay_us(560); // 所有位都以 560us 载波开头 IR_Carrier_Off(); if(bit) delay_us(1690); // 逻辑 1:熄灭 1.69ms else delay_us(560); // 逻辑 0:熄灭 0.56ms } // 发送完整帧 // data: 4 字节数据,依次为模式温度、风速扫风、定时睡眠、校验和 void IR_Send_Frame(uint8_t *data) { uint8_t i, j; // 引导码:9ms 载波 + 4.5ms 空闲 IR_Carrier_On(); delay_us(9000); IR_Carrier_Off(); delay_us(4500); // 固定 3 位 001 IR_Send_Bit(0); IR_Send_Bit(0); IR_Send_Bit(1); // 发送 4 个字节,每字节低位在前 for(i = 0; i < 4; i++) { for(j = 0; j < 8; j++) { IR_Send_Bit((data[i] >> j) & 0x01); } } // 结束码:560us 载波后拉低 IR_Carrier_On(); delay_us(560); IR_Carrier_Off(); }这里有个细节:格力协议的数据位是低位在前,和 NEC 一样。如果你按高位在前发,空调收到的字节是反的,模式温度全乱。我一开始就是这里搞反了,对着空调按了半天没反应,后来用接收头抓了一帧原装遥控器的波形,对比才发现位序错了。
校验和的计算放在调用层做:
// 计算校验和并填充到 data[3] void IR_Fill_Checksum(uint8_t *data) { data[3] = (data[0] + data[1] + data[2]) & 0xFF; }3.4 组装一个制冷 26 度的指令
把上面的函数串起来,发一个制冷、26 度、自动风速、不扫风的指令。
void IR_Send_Cool_26(void) { uint8_t cmd[4]; // 字节 0:模式 001 + 温度 (26-16=10) = 0b00101010 = 0x2A cmd[0] = 0x2A; // 字节 1:风速自动 00,扫风关,其余位按默认 cmd[1] = 0x00; // 字节 2:定时和睡眠都关 cmd[2] = 0x00; // 字节 3:校验和 IR_Fill_Checksum(cmd); IR_Send_Frame(cmd); }cmd[0]的 0x2A 拆开看:高 3 位 001 是制冷模式,低 5 位 01010 是十进制 10,加上 16 就是 26 度。cmd[1]和cmd[2]全零表示风速自动、不扫风、无定时、不睡眠。实际使用中如果空调没反应,先检查校验和,再检查位序,最后用手机摄像头对着 IR LED 看有没有紫白色闪烁,肉眼看不到红外光,但摄像头能拍到。
4. 避坑与排查:空调不响应、发射距离短、误触发
4.1 空调完全没反应
现象:代码烧进去,IR LED 用手机摄像头看有闪烁,但空调纹丝不动。
原因:最常见的是校验和算错,其次是位序反了,还有可能是引导码时长偏差太大。格力空调对帧格式的容忍度比想象中低,校验和错一个位就直接丢弃。
解决:先用 VS1838B 接收头抓一帧原装遥控器的数据,把 4 个字节打印出来,和自己的代码对比。如果手边没有接收头,用逻辑分析仪抓 IR LED 驱动引脚的波形,量引导码是不是 9ms 和 4.5ms,逻辑 1 的高电平是不是 1.69ms。偏差超过 10% 就要调延时系数。
4.2 发射距离只有十几厘米
现象:必须把发射管怼到空调接收窗跟前才有反应。
原因:IO 口直接驱动 LED,电流只有几毫安,红外辐射功率不够。或者三极管基极电阻太大,饱和深度不够。
解决:加三极管驱动,基极电阻降到 1kΩ 甚至 470Ω,集电极串一个 10Ω 到 22Ω 的限流电阻。IR LED 的峰值电流可以到 100mA 以上,但平均电流要控制在 20mA 以内,否则发热严重。占空比 1/3 的情况下,峰值 100mA 对应的平均电流约 33mA,短时间发射没问题,长时间连续发要加散热或降低占空比。
4.3 偶尔能触发但经常失灵
现象:同一个指令,按十次能成功两三次,其他时候空调没反应或者执行了错误的模式。
原因:延时函数被中断打断,导致脉冲宽度抖动。或者电源纹波太大,接收头输出误码。
解决:发送红外帧的时候关掉全局中断,发完再开。delay_us用 DWT 的周期计数器实现,比循环延时更稳定。电源端在接收头和发射管附近各加一个 100μF 电解电容并联 0.1μF 瓷片电容,滤掉发射时的电流突变。
// 发送前关中断,发送后恢复 __disable_irq(); IR_Send_Frame(cmd); __enable_irq();提示:关中断的时间要控制在 100ms 以内,否则会影响其他实时任务。一帧格力指令的发送时间大约 60ms,在安全范围内。
4.4 接收头解码时好时坏
现象:做学习功能时,接收头输出的数据偶尔多一位或少一位。
原因:接收头的 AGC 自动增益控制需要时间稳定,连续两帧之间间隔太短会导致第一帧被吞掉。另外,接收头输出端没有上拉电阻,开漏输出悬空时电平不确定。
解决:两帧之间至少间隔 100ms,给 AGC 留恢复时间。输出端加 10kΩ 上拉电阻到 3.3V。输入捕获的定时器预分频器设小一点,保证 560μs 的脉冲能测到足够精度,一般 1MHz 计数频率对应 1μs 分辨率就够了。
5. 二次开发扩展:从固定指令到智能控制
5.1 把指令表做成可配置的结构体
上面写的IR_Send_Cool_26是硬编码的,改个温度就要重新编译。实际用的时候我一般把模式、温度、风速做成结构体,运行时动态组装。
typedef struct { uint8_t mode; // 0 自动,1 制冷,2 除湿,3 送风,4 制热 uint8_t temp; // 16~30 uint8_t fan; // 0 自动,1 低,2 中,3 高 uint8_t swing; // 0 关,1 开 uint8_t sleep; // 0 关,1 开 } AC_Command_t; void IR_Send_AC(AC_Command_t *ac) { uint8_t cmd[4] = {0}; cmd[0] = (ac->mode << 5) | ((ac->temp - 16) & 0x1F); cmd[1] = (ac->fan << 6) | (ac->swing << 5); cmd[2] = (ac->sleep << 7); IR_Fill_Checksum(cmd); IR_Send_Frame(cmd); }这样上层只需要填结构体,不用关心位操作。模式编码和温度偏移都封装在函数里,改协议或者加新功能时只动这一处。
5.2 加一个串口或蓝牙指令接口
把IR_Send_AC挂到串口接收中断里,就能用电脑或手机发指令。比如收到字符串C26A表示制冷 26 度自动风。
// 串口接收回调示例 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { char ch = USART_ReceiveData(USART1); // 解析协议,填充 AC_Command_t,调用 IR_Send_AC // 具体解析逻辑根据自定义协议实现 } }蓝牙模块用 HC-05 或类似型号,接在串口上,手机端发同样的字符串就能控制。如果想接 Home Assistant 或米家,加一个 ESP8266 做透传,STM32 只负责红外发射,逻辑判断放在上位机。
5.3 用接收头做红外学习
如果手上有原装遥控器但协议对不上,可以用 VS1838B 接收头做学习。接收头输出接定时器输入捕获,测量每个脉冲的高电平和低电平宽度,存到数组里,回放时按同样的宽度发射。
// 输入捕获记录脉冲宽度(简化示意) void TIM4_IRQHandler(void) { if(TIM_GetITStatus(TIM4, TIM_IT_CC4) != RESET) { uint16_t capture = TIM_GetCapture4(TIM4); // 计算与上一次捕获的差值,存入 pulse_buf // 根据高低电平判断是载波段还是空闲段 TIM_ClearITPendingBit(TIM4, TIM_IT_CC4); } }学习模式的关键是存储空间。一帧格力指令大约 70 个脉冲,每个脉冲用 16 位存宽度,需要 140 字节。STM32F103C8T6 有 64KB Flash,存几十组指令绰绰有余。回放时把存储的宽度数组按顺序延时输出,载波段开 PWM,空闲段关 PWM。
5.4 定时与温度联动
加一个 RTC 或者用 SysTick 做软定时,到点自动发开关指令。温度联动需要外接传感器,比如 DS18B20 或者 DHT11,读到的温度高于阈值就发制冷指令,低于阈值就发制热或关机。
// 简单的定时发送逻辑 if(rtc_hour == 22 && rtc_minute == 0 && !sent_today) { AC_Command_t ac = {.mode = 1, .temp = 26, .fan = 0}; IR_Send_AC(&ac); sent_today = 1; }这套代码我前后改了三四版,最开始用循环延时,空调十次有八次不认;后来换成定时器 PWM,一次就通了。最深的教训是:红外发射看着简单,但时序精度和电源质量是两条命根子,省什么都不能省示波器或者逻辑分析仪。从那以后我每次调红外协议,都强制先用接收头抓一帧原装遥控器的波形存下来当基准,再对比自己的发射波形,偏差超过 5% 就停下来查,绝不靠猜。希望这个项目能帮你把家里的空调管起来,也帮你在嵌入式红外通信上少走点弯路。
本文还有配套的精品资源,点击获取