简介:本资源是一套基于STM32F103C8单片机与HXD019红外收发芯片实现的万能遥控器完整开发方案,面向嵌入式初学者、电子设计爱好者及STM32课程实践者,解决红外学习与复现遥控指令的核心技术难点。压缩包含1010个文件,以351个C源码、279个头文件(.h)构成主程序框架,辅以114个文本协议说明、44个汇编启动文件(如cstart_thumb2.asm)及IAR工程配置文件(.icf/.ewp/.project等),全面支撑硬件驱动、红外协议解析与按键交互逻辑;整体体积3.45MB,结构清晰,便于模块化理解与二次开发。已有42人学习下载,提供已验证的IAR 5.3/5.4工程环境、STM32标准外设库V3.5支持、完整红外学习/发送流程代码(含PB14学习键、PB12发射键及PA4–PA6状态指示灯控制)、HXD019专用驱动与通讯协议文档,可直接编译烧录运行,是掌握红外遥控底层通信与STM32外设协同开发的实用参考项目。
1. 项目概述:从零打造一个万能遥控器
最近在整理工作室的旧设备,发现各种遥控器堆了满满一抽屉,电视、空调、机顶盒、风扇……每个都得单独找,麻烦不说,还容易丢。这让我想起了几年前用STM32和红外编解码芯片HXD019做过的一个万能遥控器项目,当时纯粹是出于兴趣,想把手头闲置的开发板利用起来,没想到做出来的成品还挺好用,不仅整合了家里大部分电器的遥控功能,还能通过手机APP学习新码型,彻底告别了“遥控器海”。
这个项目本质上是一个基于STM32微控制器的红外信号学习与发射装置。它的核心功能有两个:一是“学习”,即通过红外接收头捕获并解析市面上各种遥控器发出的红外编码信号,将其存储起来;二是“发射”,在需要时,由STM32控制HXD019芯片,精准地复现出学习到的红外编码信号,从而控制对应的电器。整个过程涉及硬件电路设计、红外通讯协议解析、嵌入式软件编程等多个环节,是一个综合性很强的嵌入式系统开发实践。
对于电子爱好者、嵌入式初学者或者有类似需求的开发者来说,这个项目非常有价值。它不仅能让你深入理解红外遥控的工作原理、掌握STM32的GPIO、定时器、外部中断等外设的灵活应用,还能接触到实际产品开发中常见的信号编解码、数据存储、用户交互等完整流程。接下来,我就把这个项目的硬件原理、通讯协议和软件设计的核心细节,结合我踩过的坑和积累的经验,毫无保留地分享出来。
2. 硬件系统设计与核心芯片选型
一个万能遥控器的硬件系统,可以拆解为几个核心模块:主控单元、红外学习模块、红外发射模块、电源管理模块以及用户交互模块。每个模块的选型和设计都直接关系到最终产品的稳定性、易用性和成本。
2.1 主控芯片:为什么是STM32?
在这个项目中,我选择了意法半导体的STM32F103C8T6,也就是大家常说的“蓝桥杯”或“最小系统板”上常用的那款芯片。选择它主要基于以下几点考量:
- 性能与资源平衡:STM32F103C8T6基于ARM Cortex-M3内核,主频72MHz,完全足以应对红外信号编解码这种对时序要求苛刻的任务。它拥有64KB Flash和20KB RAM,对于存储大量的红外编码数据(通常每条指令几十到几百字节)以及运行相对复杂的用户逻辑(如菜单、协议判断)绰绰有余。
- 丰富的外设:这是关键。我们需要至少一个高精度定时器(TIM)来捕获和生成红外载波(通常是38kHz),需要外部中断(EXTI)来精确检测红外接收头输出的下降沿以解码,需要通用输入输出口(GPIO)控制发射电路和按键/LED等。STM32F103的定时器功能强大,支持输入捕获和PWM输出模式,正好完美匹配我们的需求。
- 开发生态与成本:STM32拥有极其庞大的社区和丰富的资料,HAL库、标准库让开发变得便捷。这款芯片价格亲民,货源充足,非常适合个人项目和产品原型开发。
注意:虽然STM32F103系列目前仍是主流选择,但在新设计中也可以考虑STM32G0或F0系列,它们性价比更高,功耗更低。但对于首次接触的朋友,F103的资料最全,踩坑时更容易找到解决方案。
2.2 红外编解码核心:HXD019详解
红外遥控信号的发射,并不是直接把数字信号“亮灭”发射出去,而是需要将其调制在一个高频载波上(通常是38kHz),以提高抗干扰能力和发射距离。HXD019正是一款集成了载波调制功能的红外发射驱动芯片。
HXD019的工作原理:它本质上是一个受控的38kHz方波发生器。芯片有三个关键引脚:VCC(电源)、GND(地)和I/O(信号输入)。当I/O引脚为高电平时,HXD019内部振荡器停止,输出端(通常接红外发射二极管)关闭;当I/O引脚为低电平时,芯片内部产生38kHz的方波并从输出端驱动红外二极管发光。因此,我们只需要让STM32的某个GPIO引脚输出我们想要发射的、已经包含编码信息的数字波形(即高低电平序列),然后把这个波形接到HXD019的I/O脚,HXD019就会自动帮我们把“低电平”时段转换成“38kHz红外闪光”,而“高电平”时段则保持熄灭。这大大简化了硬件设计,我们无需STM32自己用PWM生成38kHz载波,节省了宝贵的定时器资源。
电路设计要点:
- 限流电阻:红外发射二极管(IRED)的驱动电流需要计算。假设IRED正向压降Vf约为1.2V,HXD019输出端饱和压降Vsat约为0.3V,系统电压VCC为3.3V。那么限流电阻R = (VCC - Vf - Vsat) / If。如果希望发射电流If为100mA,则 R = (3.3 - 1.2 - 0.3) / 0.1 = 18Ω。考虑到发射是间歇性的,可以选择一个1/4W的20Ω电阻。电流大小直接决定发射距离和角度,需要根据实际壳体透光性和使用场景调整。
- PCB布局:红外发射管应靠近板边,并确保前方无遮挡。最好在PCB上为发射管设计一个凸起的“炮筒”结构,或使用独立的透镜来聚焦红外光,以增强指向性和距离。
2.3 红外学习模块设计
学习功能依靠一个红外接收头实现,如常见的VS1838B、HS0038等。这类接收头是三引脚器件(VCC, GND, OUT),内部集成了光电二极管、前置放大器、带通滤波器和解调电路。它只对38kHz左右的载波敏感,当接收到被38kHz调制的红外信号时,它会进行解调,并在OUT引脚输出解调后的原始数字编码波形。当没有有效信号时,OUT引脚保持高电平。
电路连接极其简单:接收头的VCC和GND接系统电源,OUT引脚连接到STM32的一个GPIO口,并且这个GPIO必须配置为外部中断下降沿触发模式。因为绝大多数红外编码协议(如NEC、RC5)都以一个下降沿作为一位数据的开始。通过捕获下降沿之间的时间间隔,我们就能解码出是逻辑‘0’还是逻辑‘1’。
一个关键的硬件细节:红外接收头在工作时会产生一定的电流波动,为了电源稳定,务必在接收头的VCC和GND引脚之间就近放置一个10uF~47uF的电解电容和一个0.1uF的陶瓷电容进行去耦。否则,在接收强信号时可能会导致电源毛刺,甚至引起单片机复位。
2.4 电源与用户交互设计
- 电源管理:项目通常由USB供电或两节AAA电池供电。如果使用电池,需要考虑低压检测(LVD)和休眠功能以省电。STM32内部有掉电检测(PVD),可以配置在电压低于某阈值(如2.9V)时产生中断,提示用户更换电池。在等待用户按键的长时间空闲状态下,应将STM32进入STOP模式,并关闭红外接收头的电源(可通过一个MOS管控制),此时整机电流可降至微安级。
- 用户交互:至少需要几个实体按键(学习键、发射键、模式切换键)、一个LED指示灯(指示学习/发射状态)和一个显示屏。显示屏的选择很多,从简单的OLED(SSD1306驱动,I2C接口)到稍复杂的LCD都可以。我推荐使用0.96寸OLED,它对比度高,功耗低,接口简单,显示菜单和编码信息非常直观。
3. 红外通讯协议深度解析与解码实现
万能遥控器的“万能”二字,关键在于它能理解和模拟多种红外通讯协议。市面上主流的协议有十几种,但掌握了核心原理和几种典型协议后,其余的都是触类旁通。
3.1 红外协议通用框架
几乎所有红外协议都遵循一个基本框架:引导码 + 用户码 + 数据码 + 结束码。数据通过脉冲宽度(Pulse Width)或脉冲间隔(Space Width)来表示‘0’和‘1’。
- 引导码:一个独特的、较长的脉冲组合,用于通知接收设备“有数据来了”,同时让接收端的自动增益控制(AGC)电路稳定下来。它是区分不同协议的首要特征。
- 用户码/地址码:用来区分不同厂家或不同设备类型,防止你家的遥控器控制到邻居家的电视。
- 数据码/命令码:具体的控制指令,如“音量加”、“电源开关”。
- 数据反码:很多协议会发送数据码的反码,用于接收端校验,提高可靠性。
- 重复码:当按键持续按下时,为了节能和减少无线干扰,不会一直发送完整指令,而是发送一个简短的“重复码”。
解码的本质,就是测量红外接收头输出波形中,高电平和低电平的持续时间,然后根据目标协议的时序规定,判断出每一位是0还是1,最后拼接出用户码和数据码。
3.2 NEC协议解码实战(以STM32外部中断为例)
NEC协议是应用最广泛的协议之一,其时序规定非常经典:
- 引导码:9ms的低电平脉冲 + 4.5ms的高电平。
- 逻辑‘0’:560us低电平 + 560us高电平。
- 逻辑‘1’:560us低电平 + 1.68ms高电平。
- 数据格式:32位数据 = 16位用户码(地址) + 8位命令码 + 8位命令反码。
在STM32上的解码实现步骤:
- 初始化:将连接红外接收头的GPIO配置为外部中断,下降沿触发。使能一个高精度定时器(如TIM2),将其时钟源设置为内部时钟,不分频,这样计数器每计数一次代表一个固定的极短时间(例如,72MHz主频下,1us计数72次)。
- 中断服务程序(ISR)逻辑:
- 进入外部中断后,首先关闭外部中断(防止在解码过程中被干扰),并清除中断标志。
- 记录时间戳:立即读取定时器(TIM2->CNT)的当前值,存入变量
falling_edge_time。 - 判断引导码:这是关键。我们需要计算本次下降沿与上一次下降沿之间的时间间隔(即高电平持续时间)。用本次的
falling_edge_time减去上一次存储的last_falling_edge_time,再换算成微秒。如果这个时间大约在4.5ms左右(考虑误差,比如4ms-5ms),并且我们知道前一个低电平持续时间是9ms(这需要在代码中记录低电平时间),那么我们就判定收到了有效的NEC引导码,开始正式解码。 - 解码数据位:在判定引导码后,定时器继续运行。我们切换思路,改为在GPIO的上升沿中断中测量高电平时间,或者在下降沿中断中测量两个下降沿之间的总周期时间。更稳健的做法是:在下降沿中断中,记录时间戳并切换中断触发边沿为上升沿;在上升沿中断中,记录时间戳并计算刚刚结束的低电平持续时间,然后再次切换为下降沿触发,等待下一个下降沿来计算高电平持续时间。通过交替测量,我们可以得到每个脉冲(低电平)和每个间隔(高电平)的精确时间。
- 逻辑判定:对于NEC协议,我们主要关心高电平时间。如果测得的高电平时间在560us左右,则判定为逻辑‘0’;如果在1.68ms左右,则判定为逻辑‘1’。将判定结果移位存入一个32位变量。
- 接收完成:接收满32位后,验证后8位是否是前8位命令码的反码。如果是,则解码成功,保存数据并置位一个标志位;如果超时(例如超过100ms没有新下降沿),则判定本次接收失败,重置所有状态。
// 伪代码逻辑示例 volatile uint32_t ir_raw_data = 0; volatile uint8_t ir_bit_index = 0; volatile uint8_t ir_received_flag = 0; void EXTI_IR_Handler(void) { static uint32_t last_time = 0; uint32_t current_time = TIM2->CNT; uint32_t pulse_width = (current_time - last_time) * TIME_PER_TICK; // 计算时间间隔 if(pulse_width > 8000 && pulse_width < 10000) { // 检测到约9ms的低电平? // 可能是引导码的开始,等待后续高电平判断 ir_bit_index = 0; ir_raw_data = 0; } else if(pulse_width > 4000 && pulse_width < 5000) { // 检测到约4.5ms的高电平? // 确认是NEC引导码,开始准备接收数据位 // 切换中断边沿或启动状态机 } else if(pulse_width > 400 && pulse_width < 1800) { // 数据位的高电平部分 if(pulse_width > 1200) { // 大于1.2ms判为1 ir_raw_data |= (1UL << ir_bit_index); } // 否则为0,不需要操作,因为之前已清零 ir_bit_index++; if(ir_bit_index >= 32) { // 接收完成,进行反码校验 if( ((ir_raw_data>>16)&0xFF) == (uint8_t)(~((ir_raw_data>>24)&0xFF)) ) { ir_received_flag = 1; } ir_bit_index = 0; } } last_time = current_time; EXTI_ClearITPendingBit(...); }实操心得:红外接收头输出的信号在起始阶段可能会有小幅抖动,在判断引导码和数据位时,一定要留出足够的误差容限(比如±20%)。另外,环境光(特别是日光灯)可能含有红外成分,会造成干扰,因此在软件上可以增加“连续收到多个完整帧才确认”的简单滤波逻辑。
3.3 学习模式的通用化设计
我们的目标是“万能”,所以不能只支持NEC。学习模式的设计思路是“盲收+分析”。
- 原始波形录制:当用户按下“学习”键后,单片机开启定时器输入捕获功能,或者使用高频率的GPIO轮询(不推荐,消耗CPU),记录下红外接收头引脚上一段时间内(如500ms)所有的电平变化(上升沿和下降沿)及其精确时刻(定时器计数器值)。这得到的是一个原始的时间戳序列。
- 协议分析与参数提取:录制完成后,分析这个时间戳序列。
- 首先找出最长的低电平脉冲,它很可能是引导码的一部分。
- 计算所有高电平、低电平脉宽的平均值、最小值,找出可能代表‘0’和‘1’的基准时间单位。
- 尝试匹配已知的协议特征(如NEC的9ms/4.5ms引导码,RC5的恒定位长等)。
- 如果匹配成功,则按该协议规则解析出用户码和数据码,并以该协议的格式存储(存储协议类型、用户码、数据码)。
- 如果无法匹配任何已知协议,则进入“原始码学习”模式,直接存储整个时间戳序列(压缩存储,如存储脉宽数组)。发射时,则严格按照这个时间序列来重现波形。这种方式可以兼容几乎所有未知协议。
4. 软件架构与核心代码实现
软件部分需要实现用户界面管理、红外编解码驱动、数据存储管理三大功能模块。一个好的架构能让代码清晰、易于维护和扩展。
4.1 主程序状态机设计
嵌入式系统非常适合用状态机(State Machine)来组织逻辑。主程序可以设计为以下几个状态:
- IDLE状态:显示主菜单,扫描按键。
- LEARN_WAIT状态:等待用户按下待学习遥控器的按键。一旦检测到红外信号,进入LEARN_RECORD状态。
- LEARN_RECORD状态:录制原始红外信号并分析,成功后保存到指定位置,然后返回IDLE。
- EMIT_SELECT状态:用户选择要发射的遥控指令。
- EMIT_SEND状态:根据选择的指令和其协议类型,调用对应的发射函数,驱动HXD019发出红外信号。
typedef enum { SYS_IDLE, SYS_LEARN_WAIT, SYS_LEARN_RECORD, SYS_EMIT_SELECT, SYS_EMIT_SEND, SYS_SETTINGS } SysState_t; volatile SysState_t g_sys_state = SYS_IDLE; void Main_Loop(void) { switch(g_sys_state) { case SYS_IDLE: Display_MainMenu(); key = Key_Scan(); if(key == KEY_LEARN) { g_sys_state = SYS_LEARN_WAIT; Display_Prompt(“请按下原遥控器按键...”); } else if(key == KEY_EMIT) { g_sys_state = SYS_EMIT_SELECT; } break; case SYS_LEARN_WAIT: if(IR_ReceivedFlag_Check()) { // 检测到有红外信号 g_sys_state = SYS_LEARN_RECORD; } if(Key_Scan() == KEY_CANCEL) { g_sys_state = SYS_IDLE; } break; case SYS_LEARN_RECORD: if(IR_Learn_Process()) { // 执行学习流程 Display_Success(“学习成功!”); Save_To_Flash(); } else { Display_Error(“学习失败!”); } g_sys_state = SYS_IDLE; break; // ... 其他状态处理 } }4.2 红外发射驱动:精准的时序控制
发射是学习的逆过程。对于已知协议(如NEC),我们需要根据协议规范,用STM32的GPIO模拟出特定的波形给HXD019。
以发射NEC码为例:
- 将连接HXD019 I/O引脚的GPIO配置为推挽输出。
- 发射函数需要接收用户码和命令码作为参数。
- 使用一个高精度定时器(如TIM3)的PWM输出模式来产生38kHz载波?不,这里不需要。因为我们有HXD019。我们只需要用另一个定时器(如TIM4)或者简单的延时函数(不推荐,因中断可能影响精度)来控制I/O引脚的高低电平持续时间。
- 更精确的做法:使用一个定时器(TIM4)在更新中断中,根据预置的“时间线”来翻转GPIO。我们预先计算好整个NEC波形(引导码、32位数据、结束码)中每个电平跳变点相对于起始点的时间(单位us),并将其存入一个数组。然后启动TIM4,将其周期设置为总时间,开启更新中断。在中断服务程序里,维护一个索引,根据当前时间判断是否需要翻转GPIO输出电平。这种方法时序最精准。
// 定义NEC波形时间线 (单位:微秒) // 数组内容表示从开始,到该时间点,GPIO电平需要改变。 // 例如: {9000, 1, 13500, 0, ...} 表示0-9000us输出0,9000-13500us输出1... const uint16_t nec_waveform_timeline[] = { 9000, // 引导码 9ms低电平结束, 之后变高 13500, // 引导码 4.5ms高电平结束,之后变低(开始数据位) 14120, // 第一位‘0’或‘1’的560us低电平结束,变高 // ... 后续根据具体数据码计算每个跳变点 }; void TIM4_IRQHandler(void) { // TIM4 更新中断 static uint16_t idx = 0; if(TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) { uint32_t current_time = get_current_time_us(); // 获取从发射开始经过的时间 if(idx < TIMELINE_SIZE && current_time >= nec_waveform_timeline[idx]) { GPIO_ToggleBits(IR_EMIT_GPIO_Port, IR_EMIT_Pin); // 翻转发射引脚电平 idx++; } if(current_time >= total_waveform_time) { TIM_Cmd(TIM4, DISABLE); // 发射结束,关闭定时器 idx = 0; } TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } }对于“原始码”发射,流程类似,只是时间线数组直接从学习时存储的原始脉宽数组中获取。
4.3 数据存储与管理
学习到的遥控码需要非易失性存储。STM32F103C8T6内部有64KB Flash,我们可以划出最后几页(如Page 60-63,每页1KB或2KB)用于存储数据。
存储结构设计: 每个存储条目可以包含:协议类型(1字节)、自定义名称(字符串)、用户码(2-4字节,取决于协议)、命令码(1-2字节)、原始码数据长度(如果原始码)、原始码数据(可变长数组)。
由于Flash写入前必须先擦除(按页擦除),且寿命有限(约1万次),我们需要设计一个简单的磨损均衡或队列管理。例如,使用一个结构体数组在RAM中管理所有条目,只有添加或删除操作时,才将整个数组更新到Flash。或者,将Flash模拟成EEPROM,使用两个页面进行交替擦写。
重要注意事项:在对内部Flash进行写操作时,必须禁止所有中断,因为写操作期间CPU会暂停,任何中断都可能导致不可预知的行为。同时,要确保写操作的地址在正确的页内,并且已经擦除。
5. 调试技巧、常见问题与优化方案
实际制作过程中,肯定会遇到各种问题。这里分享一些我踩过的坑和解决办法。
5.1 硬件调试:示波器是你的眼睛
没有示波器,调试红外项目就像盲人摸象。一个几十兆带宽的二手示波器就足够。
- 检查发射波形:将探头点在HXD019的I/O输入脚和红外发射二极管阴极。你应该能看到STM32输出的、规整的数字波形(高低电平)。再点在红外发射二极管阳极,应该能看到被38kHz调制的波形(低电平时是一簇密集的方波,高电平时是平坦的)。
- 检查接收波形:用另一个已知好的遥控器对准接收头按下按键,在接收头的OUT引脚应该能看到解调后的数字波形。对比这个波形和标准协议时序图,就能判断接收头是否工作正常,以及环境干扰大小。
- 常见硬件问题:
- 发射距离短:检查红外发射管的限流电阻是否太小(电流不足),或太大(电流太小)。检查发射管是否安装到位,前方透镜是否干净。尝试更换不同型号的发射管(注意波长一般是940nm)。
- 接收不灵敏或误触发:检查接收头电源的去耦电容。尝试给接收头加上一个金属屏蔽罩(可以是剪开的易拉罐),防止环境光干扰。确认接收头中心频率是否为38kHz(有些是36kHz或40kHz)。
- 单片机无故复位:很可能是电源问题。红外发射瞬间电流较大,可能造成电源电压跌落。确保电源有足够容量,并在STM32的电源入口处增加一个大电容(如100uF)。
5.2 软件调试与问题排查
解码不稳定,时好时坏:
- 首要原因:中断干扰。确保红外解码的外部中断优先级设置合理,且中断服务函数执行时间尽可能短。如果中断函数里做了浮点运算或复杂处理,很容易丢失后续边沿。把解码的状态判断和数据处理放到主循环中,中断只负责记录时间戳。
- 其次:时序容错设置过小。严格按照协议规定的9ms±10%来判断引导码,在实际中可能因为遥控器个体差异或电池电压不足而不准。将误差范围放宽到±30%甚至更多。
- 使用“脉宽测量”而非“周期测量”:对于像NEC这样用高电平宽度区分0和1的协议,在下降沿中断中,我们更容易准确测量前一个高电平的宽度(即本次下降沿与上次上升沿的时间差),而不是测量整个周期。这需要你在上升沿和下降沿中断中协作。
发射无法控制设备:
- 确认协议和码值:先用学习功能学习原装遥控器的码,看看解码出来的用户码和命令码是否合理。然后用这个码去发射,看能否控制。这是验证发射链路是否正常的最好方法。
- 检查载波频率:HXD019产生的38kHz可能不准。可以用示波器测量发射时红外二极管两端的波形,计算一下载波频率。偏差太大(如超过±1kHz)可能导致设备不识别。HXD019的精度由外部电阻决定,可以查阅其数据手册调整。
- 发射波形畸变:如果STM32的I/O引脚驱动能力不足,或者连接到HXD019的走线过长,可能导致波形边沿变缓。可以在GPIO输出后串联一个几十欧姆的小电阻,并在HXD019的I/O脚对地加一个几十皮法的小电容,起到整形作用。
5.3 功能扩展与优化方向
基础功能实现后,可以考虑以下扩展,让项目更有趣:
- 增加射频(RF)学习与发射:有些高级设备(如窗帘电机、车库门)使用315MHz或433MHz的射频遥控。可以增加一个射频学习发射模块(如SYN470R/ SYN480R + PT2262解码),让遥控器真正“全屋通用”。
- 接入物联网:通过ESP8266或ESP32模块,让STM32连接Wi-Fi。开发一个简单的手机APP或微信小程序,通过MQTT或HTTP协议向设备发送指令,实现手机远程控制。你甚至可以用它来配合智能家居平台,实现场景联动。
- 语音控制:接入一个离线语音识别模块(如LD3320),通过训练几个简单的口令(如“打开电视”、“空调二十六度”),实现语音控制,体验更科幻。
- 低功耗优化:对于电池供电,优化是关键。除了使用STOP休眠模式,还可以选择更低功耗的主控(如STM32L系列),并采用中断唤醒设计。只有按键按下或收到特定唤醒信号(如红外)时才启动系统,平时保持极低功耗。
这个项目从硬件焊接、软件调试到功能完善,每一步都需要耐心和细致的排查。当最终用一个自己制作的小设备,成功控制客厅里所有电器的那一刻,那种成就感是无可替代的。它不仅仅是一个遥控器,更是一个涵盖了数字电路、单片机编程、信号处理、协议分析等多个领域的综合实践平台。希望我的这些经验,能帮你少走些弯路,更快地享受到创造的乐趣。
本文还有配套的精品资源,点击获取