简介:本资源面向嵌入式开发初学者与智能家居DIY爱好者,提供一套基于STM32微控制器驱动红外模块控制格力空调的完整工程实践方案,帮助读者理解微控制器原理、红外通信协议与嵌入式编程的融合应用。压缩包共226个文件,约8.35MB,以c源文件、h头文件、o目标文件、crf交叉引用文件及d依赖文件为主,另含uvprojx工程文件、sct分散加载文件、hex烧录文件与xls等辅助资料,覆盖从源码到编译产物的完整开发链路。资源围绕STM32的GPIO推挽输出、定时器PWM波形生成、NEC编码协议解析与格力空调红外指令适配展开,并涉及Keil环境下的固件开发与示波器信号调试思路。目前已有889人学习下载,适合希望掌握红外遥控协议、积累嵌入式项目经验的读者参考借鉴。
1. 拆开空调遥控器的黑匣子:STM32 红外方案到底能解决什么
夏天最让人烦躁的不是热,是找不到空调遥控器。更让做嵌入式的人手痒的是,明明手头有一堆 STM32 开发板,却还要满屋子翻那个塑料壳子。格力空调的红外编码协议早就被扒得底朝天,用 STM32 加一个几毛钱的红外发射管,就能把「找遥控器」这件事从物理世界彻底抹掉。这套方案的核心不是发射红外,而是把格力那套带温度、模式、风速、扫风的 35 位状态码完整复现出来,再通过定时器精确调制到 38kHz 载波上。适合谁?手上有 STM32F103 这类常见板子、会点 C 语言、想给出租屋老空调加个智能控制但不想换空调的人。它不挑空调型号,只要遥控器是格力原厂红外协议,基本都能通吃。
2. 格力红外协议拆解:从 35 位状态码到 38kHz 载波
2.1 格力编码的帧结构与位定义
格力空调红外遥控用的是 NEC 协议的变种,但别被「变种」两个字吓到,它比标准 NEC 还好认。一帧完整数据由引导码、35 位数据码、结束码组成。引导码是 9000μs 低电平加 4500μs 高电平,这个用逻辑分析仪一抓就能看到,非常显眼。35 位数据里,前 3 位是固定标识,中间 5 位是模式(制冷/制热/除湿/送风),接着 5 位温度(16℃到30℃,对应编码 0 到 14),再 5 位风速,然后 5 位扫风状态,最后 12 位是校验和与固定尾码。
位「0」和位「1」的区分靠脉宽:位 0 是 600μs 低电平加 600μs 高电平,位 1 是 600μs 低电平加 1600μs 高电平。注意这里的高低电平是相对红外接收头输出而言的,发射端要反过来。我一般用 TIM 的 PWM 输出 38kHz 载波,再用另一个定时器做微秒级延时来控制载波的通断,这样比用 delay 函数死等要稳得多,也不会因为中断打断导致时序漂移。
2.2 用定时器生成 38kHz 载波与脉宽调制
STM32F103 的 TIM2 挂在 APB1 上,72MHz 主频。要得到 38kHz,周期寄存器 ARR 设为 72M/38k - 1,约等于 1894。占空比设 1/3 比较合适,CCR 设为 632。这样 PWM 通道输出就是标准的红外载波。但光有载波不够,还得控制它什么时候发、什么时候停。我的做法是:PWM 一直配置好但不使能输出,需要发位 0 时,使能 PWM 输出 600μs 然后关闭,再拉高 600μs;发位 1 时,使能 600μs 后关闭,再拉高 1600μs。这个「拉高」阶段可以用 TIM 的另一个通道做单脉冲延时,或者干脆用 DWT 周期计数器做忙等,精度足够。
// TIM2 PWM 初始化,输出 38kHz 载波 void IR_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA1 作为 TIM2_CH2 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 1894; // 72M / 38k - 1 TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 632; // 1/3 占空比 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC2Init(TIM2, &TIM_OCInitStructure); TIM_Cmd(TIM2, DISABLE); // 先不启动,发的时候再开 }这段代码的关键参数就两个:ARR 决定载波频率,CCR 决定占空比。占空比别设 50%,红外接收头对 1/3 占空比的信号最敏感,这是血泪经验,设 50% 也能用但距离会短一截。TIM_Cmd 默认关掉,是为了后面用宏控制通断,避免载波一直往外冒干扰其他中断。
2.3 把温度、模式、风速打包成 35 位
格力协议里温度不是直接写摄氏度,而是写偏移量。16℃对应 0,17℃对应 1,依此类推,30℃对应 14。模式编码:制冷是 0x01,除湿 0x02,送风 0x03,制热 0x04。风速:自动 0x00,一档 0x01,二档 0x02,三档 0x03。扫风开 0x01,关 0x00。这些值要按位拼进一个 64 位变量里,再逐位发送。
// 打包格力 35 位数据,返回一个 64 位整数 uint64_t格力_PackData(uint8_t mode, uint8_t temp, uint8_t fan, uint8_t swing) { uint64_t data = 0; uint8_t temp_code = temp - 16; // 温度偏移 data |= (uint64_t)0x01 << 32; // 固定标识位 data |= (uint64_t)mode << 28; // 模式 5 位 data |= (uint64_t)temp_code << 23; // 温度 5 位 data |= (uint64_t)fan << 18; // 风速 5 位 data |= (uint64_t)swing << 13; // 扫风 5 位 // 后 12 位校验和与尾码,不同批次空调略有差异,建议用逻辑分析仪抓原遥控器 data |= (uint64_t)0x00 << 0; // 占位,实际按抓包结果填 return data; }这里要提醒一句:校验和那 12 位是最大的坑。不同年份出厂的格力空调,尾码和校验算法有细微差别。我见过同一型号不同批次,校验和差一个字节就完全不响应。稳妥做法是拿一个红外接收头接逻辑分析仪,把原遥控器每个按键的码流抓下来,直接比对。没有逻辑分析仪的话,用 STM32 的输入捕获也能凑合,但解析起来费劲。
3. 从代码到空调响应:发射电路与联调步骤
3.1 红外发射管驱动电路怎么搭
STM32 的 GPIO 推挽输出电流最大 20mA,而红外发射管瞬间电流要 100mA 以上才能打远。所以别直接把发射管接 IO 口,中间必须加一级三极管驱动。我一般用 S8050 NPN 三极管,基极串 1kΩ 电阻接 STM32 的 PWM 输出脚,集电极接红外发射管负极,发射管正极串 10Ω 限流电阻接 3.3V。注意发射管引脚有极性,长脚是正极。如果遥控距离要求不高(三米以内),不加三极管也能凑合,但角度稍微偏一点就失灵,体验很差。
接收端调试时,用一体化红外接收头(比如 VS1838B)接示波器或逻辑分析仪,对着发射管按遥控,看解调后的波形。正常应该看到干净的 0/1 脉冲序列。如果波形毛刺多,检查电源退耦电容,接收头 VCC 和 GND 之间并一个 100nF 和 10μF 电容,能解决大部分玄学问题。
3.2 主循环里怎么组织发送逻辑
发送一帧完整数据的流程是:先发 9ms 低电平引导码,再发 4.5ms 高电平,然后循环 35 次逐位发送,最后发一个 560μs 的结束位。注意这里的「低电平」是指红外发射管导通,对应接收头输出低电平。写代码时别搞反了。
// 发送一个位,bit_val 为 0 或 1 void IR_SendBit(uint8_t bit_val) { IR_Carrier_On(); delay_us(600); // 所有位都以 600us 载波开头 IR_Carrier_Off(); if (bit_val) { delay_us(1600); // 位 1 的高电平间隔 } else { delay_us(600); // 位 0 的高电平间隔 } } // 发送完整格力帧 void IR_SendGreeFrame(uint64_t data) { IR_Carrier_On(); delay_us(9000); // 引导码低电平 IR_Carrier_Off(); delay_us(4500); // 引导码高电平 for (int i = 34; i >= 0; i--) { // 35 位,从高位到低位 IR_SendBit((data >> i) & 0x01); } IR_Carrier_On(); delay_us(560); // 结束位 IR_Carrier_Off(); }delay_us 的实现别用 SysTick 中断,中断优先级一高就会打断时序。我一般用 DWT 的 CYCCNT 寄存器做忙等,72MHz 下每个周期约 13.9ns,算好循环次数就行。发送完一帧后至少等 100ms 再发下一帧,否则空调会认为是重复码直接忽略。
3.3 用按键或串口触发不同模式
调试阶段最方便的是用串口发指令。定义一个简单的协议:串口收到「C 26 1 0」表示制冷、26℃、一档风、扫风关。解析后调用打包和发送函数。这样不用反复烧录,用串口助手就能测所有组合。
// 串口命令解析示例,格式:模式 温度 风速 扫风 void Parse_Cmd(char *cmd) { uint8_t mode, temp, fan, swing; char mode_char; sscanf(cmd, "%c %d %d %d", &mode_char, &temp, &fan, &swing); switch (mode_char) { case 'C': mode = 0x01; break; // 制冷 case 'H': mode = 0x04; break; // 制热 case 'D': mode = 0x02; break; // 除湿 case 'F': mode = 0x03; break; // 送风 default: return; } uint64_t frame = 格力_PackData(mode, temp, fan, swing); IR_SendGreeFrame(frame); }联调时先确认空调有反应,再微调校验和。如果按了没反应,先拿手机摄像头对着发射管,按发送时能看到紫白色闪烁,说明载波在发。看不到就是 PWM 没配好或者三极管接反了。能看到闪烁但空调不响应,九成是校验和或位序错了。
4. 避坑与排查:红外发射不响应、距离短、误触发
4.1 空调完全没反应,但发射管在闪
现象:手机摄像头能看到发射管亮,逻辑分析仪抓接收头也有波形,但空调就是不动。原因:校验和或尾码不对,或者位序反了。格力协议是高位先发,但有些资料写的是低位先发,照抄就翻车。解决:用逻辑分析仪抓原遥控器同一按键的码流,逐位比对。重点看第 29 到 35 位,那里是校验和区。如果抓不到原遥控器,试着把校验和字节取反、加一、减一各试一遍,通常能蒙对。
4.2 遥控距离只有半米,角度稍偏就失灵
现象:贴着空调能控制,退后两步就没反应。原因:红外发射管驱动电流不够,或者限流电阻太大。STM32 的 3.3V 直接驱动,串 100Ω 电阻时电流才 20mA 左右,打不远。解决:加 S8050 三极管,基极 1kΩ 到 IO,集电极串 10Ω 到发射管。注意发射管要选 940nm 波长的,和接收头匹配。另外发射管别用普通 LED 代替,波长不对,接收头根本不认。
4.3 按一次键空调连续响应好几次
现象:发一帧指令,空调滴滴响好几声,或者模式乱跳。原因:没有发结束码,或者帧间隔太短被空调当成重复码。格力协议里结束码是 560μs 载波加一段空闲,如果省略结束码,空调会一直等后续位,把噪声当成数据。解决:确保 IR_SendGreeFrame 最后有 560μs 载波,并且发完后延时 100ms 以上再允许下一次发送。如果是按键触发,加一个 200ms 的软件消抖,防止手抖连发。
4.4 制热模式正常但制冷模式不响应
现象:同一个遥控码,制热按了有反应,制冷按了没反应。原因:模式编码搞反了,或者温度偏移算错了。格力制冷模式编码是 0x01,制热是 0x04,但有些老机型制冷是 0x00。解决:把模式编码逐个试,0x00 到 0x04 都发一遍,看哪个能触发制冷。温度偏移也要确认,16℃对应 0 还是 1,拿原遥控器设 16℃抓包看一眼就清楚了。
4.5 上电后空调自己乱动
现象:STM32 刚上电还没发指令,空调就自己响了。原因:GPIO 初始化时电平抖动,或者 PWM 配置过程中输出了一段杂波。解决:在 GPIO 初始化前先把引脚设为下拉输入,等 PWM 配置完成后再切到复用推挽。TIM_Cmd 默认关掉,发的时候再开。另外红外发射管正极串的电阻别省,能限流也能减少上电冲击。
5. 进阶玩法:用 RTC 定时发送与状态记忆
基础功能跑通后,我习惯加两个东西:RTC 定时和状态记忆。RTC 用 STM32 内部的就行,配好闹钟中断,每天早上七点自动发一帧制冷 26℃ 一档风。状态记忆用 Flash 模拟 EEPROM,把最后一次成功的模式、温度、风速存进去,下次上电直接恢复,不用重新配。Flash 写之前先擦除页,写的时候按半字写,注意 STM32F103 的 Flash 写操作要开 HSI 或者确保主频不超过 24MHz 时操作,否则容易写失败。
// 保存空调状态到 Flash 指定地址 void Save_AC_State(uint8_t mode, uint8_t temp, uint8_t fan, uint8_t swing) { uint32_t addr = 0x0801F000; // 最后一页起始地址 uint16_t state[2]; state[0] = (mode << 8) | temp; state[1] = (fan << 8) | swing; FLASH_Unlock(); FLASH_ErasePage(addr); FLASH_ProgramHalfWord(addr, state[0]); FLASH_ProgramHalfWord(addr + 2, state[1]); FLASH_Lock(); }验证方法很简单:断电再上电,串口打印读出来的状态,和断电前一致就算成功。如果读出来全是 0xFF,说明擦除后没写进去,检查 FLASH_ProgramHalfWord 的地址是不是 2 字节对齐。我一般还会在 Flash 里存一个魔数,比如 0x5A5A,读的时候先校验魔数,不对就恢复默认值,避免第一次上电读到空白 Flash 导致乱发指令。
从那以后我每次做红外项目,都强制先用逻辑分析仪抓一遍原遥控器的码流,把引导码、位宽、校验和全部对一遍再写代码。这个习惯帮我省了至少三次通宵排查。希望帮到你。
本文还有配套的精品资源,点击获取