简介面向嵌入式初学者与智能家居开发者的STM32红外模块空调控制工程包解决怎样用STM32模拟格力空调遥控器发射指令的问题适合用作红外通信、定时器PWM和家电控制方向的实战参考。压缩包内含226个文件以.c源码、.h头文件、Keil工程文件.uvprojx、交叉引用与目标文件.crf/.o、编译链接生成的.hex/.axf以及调试辅助文件为主整体约8.35MB目录保留完整Keil工程结构便于直接编译烧录。该工程已有890人学习下载在同类STM32入门资源中热度不错。重点价值在于完整展示了从GPIO推挽输出配置、定时器产生PWM波形、红外编码发送到接收信号解析的代码流程并保留了STM32标准外设库中定时器、RCC时钟、ADC等驱动源文件的调用写法。对照格力空调的NEC协议适配过程可以快速理解遥控器指令格式适合对照开发板调试也可作为其他红外遥控项目的改版基底。1. STM32红外遥控格力空调一套能学码、能重放、还能联动温湿度的完整方案夏天最难受的事莫过于空调遥控器按键失灵或者直接丢了。用一块 STM32 单片机加红外接收头和红外发射管把格力空调遥控器的原始红外码学下来存进 Flash再拿按键把码重放出去这套方案我已经完整跑通好几轮。它可以解决的不只是「遥控器丢了」的尴尬还能把红外码用起来做温湿度联动、定时开关整套硬件成本不到二十块钱。新手照着做就能得到一台「能学能发」的红外遥控器熟手可以直接把解码和发射代码移植到自己的智能家居项目里。下面从 NEC 协议讲起到解码、发射、存储、避坑最后给验证手段和一个温度联动例子。2. NEC协议与格力码先搞懂38kHz载波和数据帧格式2.1 红外通信链路与最关键的几段时间红外遥控通信链路是单向的遥控器里的红外发射管以 38kHz 载波去“点亮”和“熄灭”接收头比如 HS0038B把载波信号还原成高低电平。所谓 38kHz 载波就是 26.3us 一个周期高电平约占三分之一这样可以有效区分环境光和遥控信号。NEC 协议里每一帧以引导码开头9ms 载波加 4.5ms 空闲。逻辑 0 是 560us 载波加 560us 空闲逻辑 1 是 560us 载波加 1690us 空闲。这里说的载波和空闲是发射端电平接收端输出恰好反相有载波时输出低电平无载波时输出高电平。很多人一上来对着接收头的波形看会以为数据位反了其实没有只是反相输出导致观察习惯要调整。信号段载波时间空闲时间引导码9ms4.5ms逻辑0560us560us逻辑1560us1690us重复码9ms2.25ms 560us 载波还有一个关键点是重复码。如果你一直按着遥控器按键NEC 协议并不是把同一帧反复发而是发一次完整帧之后每隔 110ms 左右发一个 9ms 载波加 2.25ms 空闲加 560us 载波的重复码。做空调控制时重复码不能省略很多空调要收到两个以上重复码才执行动作。不同品牌的空调虽然都走 NEC 变体但地址码、命令码的组织方式差别很大。格力空调的遥控码通常是 16 位地址加 8 位命令加 8 位命令反码也可能扩展到 32 位数据。动手前最好先把原遥控器发的波形完整捕捉一遍看引导码和位数不要直接套用网上抄来的码表。2.2 格力空调数据帧特点地址码和反码校验格力空调的红外协议基于 NEC但很多型号用的是 16 位地址码。一帧里包含厂商地址、操作码和操作码反码接收端通过“命令码与反码相加等于 0xFF”来校验数据是否正确。比如 16 位地址里高 8 位是厂商标识低 8 位是机器类型不同批次空调可能存在差异。所以在做项目时最稳妥的方式是“学习模式”让 STM32 接收原装遥控器的码存下原始脉宽序列而不是硬编码一套码表。这样哪怕换一台空调只要把原遥控器对着接收头按一次就能重新学习不用重新编译固件。还有一个容易忽略的点格力部分机型的码长不是标准 32 位。有些高端机型一帧有 64 位甚至 96 位里面包含温度、模式、风速、扫风等档位状态。这种情况下如果按标准 NEC 的“8 位地址8 位地址反码8 位命令8 位命令反码”去硬解析大概率解析错。所以解码程序里要设计成“先捕获完整脉冲序列再按 560us 基准做位宽分类”而不是直接按固定字节数读取。2.3 硬件选型STM32F103C8T6、HS0038B、IR333C选 STM32F103C8T6 而不是 8 位单片机或 Arduino主要是图它的定时器和中断资源。解码需要外部中断加定时器输入捕获发射需要一路定时器做 38kHz 载波另一路定时器做脉宽调度。F103 有多个 TIM主频 72MHz处理这些绰绰有余而且库函数资料多踩坑容易查。接收头用 HS0038B注意它的输出是反相输出空闲时输出高电平收到载波时输出低电平。数据手册上标注载波频率 38kHz实际容限大约在 3640kHz 之间所以载波频率偏差控制在 5% 以内问题不大。发射管常用 IR333C峰值波长 940nm配合 100Ω 限流电阻和一颗 NPN 三极管比如 S8050做开关驱动。直接用 GPIO 推挽输出驱动红外管虽然能亮但电流不够距离最多几十厘米加上三极管之后距离能到 58 米。接线也固定HS0038B 的 OUT 接 PA0定时器输入捕获通道IR333C 正极接 3.3V负极接三极管集电极三极管基极通过 1kΩ 电阻接 PB0。这套接法里 PB0 输出高电平时三极管导通发射管点亮输出低电平时截止载波中断。3. 解码实现定时器输入捕获与脉宽测量3.1 解码思路用定时器捕获两段边沿之间的宽度红外解码最常用的方案是外部中断加定时器计数接收头输出引脚产生下降沿收到载波开始时进入中断读取当前定时器计数值然后把相邻两次中断的计数值相减得到脉冲宽度。这样不用开定时器中断也不会因为中断频繁而丢数据。我一般把 PA0 配置成定时器 TIM2 的输入捕获通道 1工作在上升沿和下降沿都捕获的模式。因为 HS0038B 空闲时输出高电平收到载波拉低所以双边沿捕获拿到的是完整的高低电平宽度序列。// 定时器2初始化输入捕获 PA0分频72-1计数频率1MHz void TIM2_Cap_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_BothEdge; // 双边沿捕获 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0f; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE); }这段配置最关键的是TIM_ICPolarity_BothEdge它让每个边沿都触发捕获相当于一次中断记一次沿。TIM_ICFilter设为 0x0f 可以过滤掉高频毛刺避免把干扰当数据。计数频率 1MHz 下一个计数值代表 1us脉宽测量分辨率足够区分 560us 和 1690us。中断服务函数里要做的事是读取捕获寄存器算出这次边沿距上次的间隔并把间隔值存进一个全局数组。按最多 512 个沿来设计超出就认为帧异常防止数组越界。volatile uint32_t nec_edge[512]; volatile uint16_t nec_edge_cnt 0; uint32_t last_cap 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { uint32_t cur TIM_GetCapture1(TIM2); uint32_t gap (cur - last_cap) 0xFFFFFFFF; last_cap cur; if (nec_edge_cnt 511) { nec_edge[nec_edge_cnt] gap; } else { nec_edge_cnt 0; // 溢出则重新开始 } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }这里有一个小技巧用差值而不是绝对值这样即使定时器溢出回绕只要每帧时间不超过定时器溢出周期差值依然正确。TIM2 是 16 位定时器1MHz 计数下每 65.5ms 溢出一次NEC 一帧最长不超过 70ms差值在单帧内是可靠的。3.2 帧解析把脉宽序列换算成位和字节捕获到完整的引导码加数据段后程序要判断这是不是有效 NEC 帧。判定依据就是引导码对应的边沿间隔正常 NEC 引导码 9ms 载波加 4.5ms 空闲下降沿间隔大约是 13500us。实测值可能在 12500 到 14500 之间所以判定阈值取 11000 到 17000 比较稳。解析时把引导码之后的每一对边沿看成一个数据位。发射端先发 560us 载波再根据 0 或 1 发 560us 或 1690us 空闲接收端看到的第一个沿是收到载波的下降沿第二个沿是载波结束的上升沿这两个沿的间隔就是 560us。空闲时长体现在下一个下降沿到来之前的间隔上所以解析函数拿“每隔一个沿”的间隔来判断逻辑位。uint8_t nec_buf[16] {0}; uint8_t nec_bit_cnt 0; uint8_t nec_parse_frame(void) { // 检查引导码第0个间隔应约为9ms 4.5ms if (nec_edge_cnt 6) return 0; if (nec_edge[0] 11000 || nec_edge[0] 18000) return 0; nec_bit_cnt 0; for (uint16_t i 1; i 1 nec_edge_cnt; i 2) { if (nec_bit_cnt 64) break; uint32_t low_wid nec_edge[i]; // 载波段宽度 uint32_t high_wid nec_edge[i 1]; // 空闲段宽度 if (low_wid 300 || low_wid 800) return 0; uint8_t bit (high_wid 1000) ? 1 : 0; nec_buf[nec_bit_cnt 3] | (bit (nec_bit_cnt 7)); nec_bit_cnt; } return 1; }这里容易误解的是nec_buf按 NEC 协议的 LSB first 存放第 0 个数据位是地址码的最低位。bit (nec_bit_cnt 7)把先收到的位放到位 0再依次放到位 1、位 2……最后得到的字节就是原始码。解析完之后可以做反码校验把命令码和命令反码相加如果等于 0xFF 就认为帧合法。但格力这类空调不一定都是标准 8 位反码结构所以工程里把校验做成可配置项默认开启校验失败时也能在串口打印原始脉宽序列方便人工核对。3.3 用串口打印脉宽序列把黑匣子打开解码最常见的翻车点是程序跑起来串口打印一堆 0xFF。这时候不要急着改算法先把原始脉宽序列打印出来看。我习惯在解析失败时把nec_edge[0]到nec_edge[nec_edge_cnt-1]全部通过串口发出去间隔用逗号分隔然后在 PC 端把数据保存成 CSV导入表格就能看到每段的微秒数。void nec_dump_raw(void) { printf([RAW] cnt%d\r\n, nec_edge_cnt); for (uint16_t i 0; i nec_edge_cnt; i) { printf(%ld,, nec_edge[i]); } printf(\r\n); }这段代码只是把数组打印出来但对排查特别有用。比如你发现第一段间隔不是 13000us 而是 26000us说明接收头钳位或者数据帧中间夹了识别码如果第一段是 9000us说明捕获到的不是引导码前沿而是引导码内部的某个沿。打印出来的数据能一眼看出协议边界在哪。串口波特率用 115200printf 重定向到 USART1调试时记得关掉比 TIM2 优先级更高的中断否则打印过程会干扰后续边沿捕获。4. 发射链路三极管驱动、38kHz载波与位宽调度4.1 发射电路三极管驱动而不是 GPIO 直推红外发射管 IR333C 在 20mA 电流下才能有足够的辐射功率STM32 的 GPIO 在 3.3V 供电下推挽输出最多能提供十几毫安而且驱动能力随负载升高下降很快。不加三极管时实测发射距离通常不超过 50cm还是正对接收头的情况。加上三极管之后发射距离可以稳定到 5 米以上。我用 NPN 三极管 S8050 做低边驱动PB0 通过 1kΩ 电阻接三极管基极发射管正极接 3.3V发射管负极接三极管集电极三极管发射极接地。这样 PB0 输出高电平时三极管导通发射管点亮PB0 输出低电平时三极管截止载波中断。如果发射管正极接 5V 而 STM32 是 3.3V 逻辑基极电阻阻值要重新算。一般基极电流取 2mA 左右就能让三极管饱和1kΩ 电阻在 3.3V 下基极电流约 2.3mA正常够用。集电极电流 100mA 时如果电源是 5V限流电阻约 3347Ω。4.2 定时器 PWM 生成 38kHz 载波载波产生有两种方式一种是用定时器输出 PWM 波形另一种是在发射函数里用 GPIO 翻转加延时。前者波形稳定但占用 PWM 通道后者实现简单但阻塞时间长而且容易被中断干扰。推荐用 TIM3 产生 38kHz 的 PWM占空比 1/3输出引脚 PB0在需要发送某一位时开启 TIM3 的 PWM 输出载波出现在空闲时间关闭 PWM 输出载波消失。这样载波每个周期都精准位宽调度只控制 PWM 的开关时刻。// TIM3 PWM初始化输出约38kHz占空比33%CH1 - PB0 void TIM3_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, ENABLE); 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); TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 1MHz计数 TIM_TimeBaseStructure.TIM_Period 26 - 1; // 26us约38.4kHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 8; // 8us高电平约33%占空比 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); // 先不使能TIM3发射时才开 }TIM_Period 26 - 1得到 26us 周期约 38.4kHz在接收头容限范围内。TIM_Pulse 8让高电平占 8us剩下 18us 是低电平占空比约 33%。IR333C 在 33% 占空比下平均功耗不大同时峰值电流足够保证辐射强度。PWM 通道初始化后不要立即使能 TIM3否则载波会一直输出。发射时才TIM_Cmd(TIM3, ENABLE)位与位之间的空闲时间则TIM_Cmd(TIM3, DISABLE)。关闭 PWM 后加一个很短的高电平间隙约 16us避免接收头把相邻载波段误合并。4.3 发射函数按 NEC 时序控制 PWM 开关有了 PWM 开关发射函数就变成纯粹的延时调度。发射一帧的流程是先开 9ms 载波关 4.5ms然后对每一位先开 560us、再根据 bit 关 560us 或 1690us。延时用定时器查询方式实现避免 for 循环延时被系统滴答中断干扰太多。// 发送一位先载波560us再按bit决定空闲时间 void nec_send_bit(uint8_t bit) { TIM_Cmd(TIM3, ENABLE); delay_ticks_us(560); TIM_Cmd(TIM3, DISABLE); if (bit) { delay_ticks_us(1690); } else { delay_ticks_us(560); } }// 发送完整帧引导码 32位数据LSB first void nec_send_frame(uint32_t frame) { TIM_Cmd(TIM3, ENABLE); delay_ticks_us(9000); TIM_Cmd(TIM3, DISABLE); delay_ticks_us(4500); for (int i 0; i 32; i) { nec_send_bit((frame i) 0x01); } // 帧结束加一个短的载波脉冲标志停止位 TIM_Cmd(TIM3, ENABLE); delay_ticks_us(560); TIM_Cmd(TIM3, DISABLE); }frame i从最低位开始取与解码时bit (nec_bit_cnt 7)的存放顺序正好对应。delay_ticks_us用 TIM4 的微秒延时函数查询 CNT 寄存器而不是 SysTick因为 SysTick 可能被系统中断或其他任务占用。末尾那个 560us 的停止载波脉冲不是 NEC 标准必需的但加上它之后接收端收到的波形更接近原装遥控器某些空调对帧结束边界比较敏感。发射的时候要特别注意中断优先级。如果在发射过程中来了串口中断或者定时器中断延时时长就会拉长580us 变成 800us接收端就会误判。我的做法是发射前把 TIM2 的输入捕获中断挂起发射完再恢复如果项目里需要边接收边发送就把发射放在最高中断优先级或者用定时器触发 DMA。整个发射流程测试时先不接空调用接收头回环验证把 HS0038B 的 OUT 接到另一个 GPIO 输入在 STM32 上同时开接收和发送让单片机自己发一帧再自己解码对比原始码和重放码是否一致。这是最快验证链路通不通的办法。5. 避坑与排查格力红外遥控最常见的五个坑5.1 解码结果全是 0xFF甚至 0x00 和 0xFF 交替出现现象接收头接上之后串口打印出来的解码结果要么全是 0xFF要么是规律性的交替值完全看不出正确命令。原因HS0038B 输出反相空闲时输出高电平有载波时输出低电平。如果外部中断配置成只在其中一个边沿捕获测到的间隔就会变成“空闲时间载波时间”的组合解析出来的 bit 自然不对。解决把捕获边沿配置成TIM_ICPolarity_BothEdge双边沿捕获解析时只取成对间隔中的第二个作为数据位宽度。这是最直接的方案不用在软件里做电平反转。5.2 现象发射管发光但空调离 3 米就没反应现象用手机摄像头能看到红外发射管亮但把发射器放到离空调 3 米的位置空调没反应凑到 50cm 以内才有反应。原因发射电流不够或者载波频率偏差超出接收头容限。直接用 GPIO 推挽驱动最常遇到IR333C 在 3.3V 下没有三极管时峰值电流可能只有 5mA 左右辐射强度不够。解决加 NPN 三极管低边驱动按目标电流重算限流电阻假设 VCC 3.3V发射管压降 1.2V集电极电流 100mA 时限流电阻约 21Ω取 22Ω 或 33Ω 都行。还要用示波器或逻辑分析仪量一下 PWM 周期如果TIM_Period设错载波变成 30kHz 或 45kHz接收头灵敏度会大幅下降。5.3 现象按标准 NEC 协议发了完整帧空调完全不响应现象程序里发了一帧标准的 32 位 NEC 数据对着空调按下去空调没有任何反应用接收头回环解码又完全正确。原因格力空调的遥控命令不是“按一下按键就发一帧”这么简单很多型号在收到完整帧之后还要收到几个重复码才执行。另外部分机型把温度、模式、风速打包在一个长帧里只发一个命令码会被当成无效数据。解决在发射函数里发完完整帧后再发 23 个重复码间隔取 110ms。重复码格式是 9ms 载波加 2.25ms 空闲加 560us 载波。实现时就是发完主帧后调用重复码发送函数三次每次之间延时约 108ms。5.4 现象手机摄像头能看到发射管发光接收头解码却偶尔丢帧现象发射链路看起来正常但接收头回环时偶尔丢帧或者解出错误码有时连续几次都失败重新上电又好了。原因手机摄像头看到的红外光只能说明载波存在不代表脉宽准确。如果发射函数里的延时被高优先级中断打断9800us 引导码可能变成 11000us 或 8500us接收端解出来的脉宽就不稳定。解决在发射函数开头做一次全局关中断发射完再开。一帧 67ms 以内关中断对系统影响几乎可以忽略。如果项目里有必须跑的中断就把它们的优先级调到低于发射期间的任务优先级或者用 DMA 定时器做发射调度。5.5 现象学习到的码保存到内部 Flash断电再上电就读不出来现象第一次学习能解码也能发射断电再上电后从 Flash 读出来的码变成 0xFFFFFFFF重新学习一次又正常。原因STM32F103 内部 Flash 不能直接覆盖写写入前必须先按扇区擦除而且最小擦除单位是 1KB。如果代码里只做了写操作没做擦除写进去的数据就是错的。解决先读目标扇区的状态地址非 0xFF 时整扇区擦除再把 32 位数据按字写入。另一个办法是把码存到外部 I2C EEPROM 如 AT24C02避开 Flash 擦写寿命问题。工程里默认用外部 EEPROM主程序只保留最近一次的缓冲副本掉电后从 EEPROM 恢复。6. 验证与进阶逻辑分析仪校验 温湿度自动联动6.1 用逻辑分析仪把波形拉出来看调试到最后比起盲调我建议把接收头的输出接到逻辑分析仪用 1MHz 采样率抓一段 200ms 波形。你马上能看到引导码是不是 9ms 加 4.5ms每个 bit 的脉宽是不是 560us 和 1690us。如果波形高低电平时间和理论值偏差超过 5%先查载波频率和延时函数。比如你看到引导码宽度 9520us其他 bit 宽度也成比例偏大 6%说明定时器预分频算错了。如果只是个别 bit 长度异常那就是中断打断。逻辑分析仪上还能直接数出每帧的 bit 数格力码是 32 位还是 64 位一眼确认不用猜。// 伪代码每30秒读一次温湿度判断是否达到阈值 while (1) { dht11_read(temp, humi); if ((temp 28) (!ac_on)) { nec_send_frame(ac_cool_on_cmd); ac_on 1; } else if ((temp 25) (ac_on)) { nec_send_frame(ac_power_off_cmd); ac_on 0; } delay_sec(30); }6.2 温湿度联动让空调自己决定开还是关把解码能力复用起来加一个 DHT11 温湿度传感器就能做成最简单的空调自动控制温度高于 28℃ 时发制冷开机命令低于 25℃ 时发关机命令。因为已经把原始码学下来存好了联动逻辑里只需要调用发射函数。这套联动逻辑里要注意不能只用温度跳变判断否则 27.5℃ 反复横跳时空调会频繁开关。加上回差之后28℃ 开启、25℃ 关闭中间 3℃ 的回差区就是天然的保护带。发射命令前最好把轮询周期从 5 秒改成 30 秒给空调压缩机留出启动和停止的时间。这个联动只是把解码和发射能力用起来的方式之一配合定时器还能做到“每天 8 点自动开、18 点自动关”。说起来也是吃过亏才长记性曾经在校验时图省事没有拿原遥控器做波形对比直接按网上抄的码去发射结果那台空调换了校验规则白折腾了一晚上。从那以后我每次做红外控制项目都强制走一遍“学习→解析→回环验证→逻辑分析仪对比”的流程再也没翻过车。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?