公司动态
基于STM32与HXD019的万能红外遥控器DIY全解析
简介本资源是一套基于STM32F103C8单片机与HXD019红外收发芯片实现的万能遥控器完整开发方案面向嵌入式初学者及红外协议应用开发者解决红外学习、编码识别与自定义发射等核心问题。压缩包含1010个文件总计3.45MB涵盖351个C源码含主控逻辑与IRDA驱动、279个头文件定义寄存器映射与协议结构、114个文本说明文档含通讯协议详解、引脚功能标注及操作流程以及IAR 5.3/5.4工程配置文件icf、ewp、project等和启动汇编代码cstart_thumb2.asm等支持开箱即编译调试。已有42人下载学习代码结构清晰主循环中通过PB14触发红外学习、PB12执行发射PA5/PA6/PA4分别指示学习、发送与系统状态配套HXD019驱动与STM32标准外设库V3.5具备完整硬件原理图、协议解析逻辑与可实测运行的工程框架适合红外遥控二次开发与嵌入式协议实践。1. 项目缘起从“一堆遥控器”到“一个盒子”的折腾不知道你有没有和我一样的烦恼客厅的茶几上常年堆着电视、机顶盒、空调、风扇、音响甚至氛围灯的遥控器找起来麻烦不说偶尔电池没电了更是让人抓狂。几年前我就琢磨着能不能自己动手做一个“万能遥控器”把所有红外设备都管起来。市面上成品虽然多但要么学习功能不好用要么不支持一些冷门协议要么就是可玩性太低没法按自己想法定制。作为一个嵌入式开发的老兵我决定用STM32单片机作为大脑再搭配一款高性能的红外收发芯片从头到尾自己设计一个。经过一番选型和折腾我最终敲定了以STM32F103C8T6也就是大家常说的“蓝桥杯”最小系统板核心作为主控搭配HXD019这颗红外编解码芯片的方案。这个组合的好处是STM32负责逻辑控制和用户交互比如按键、屏幕显示而HXD019则专精于红外信号的发射与接收它能硬件解析市面上绝大多数红外协议大大减轻了MCU的负担也让代码写起来更清爽。整个项目从画原理图、设计通讯协议到编写底层驱动和上层应用逻辑前前后后花了小半年踩了不少坑也积累了很多实战经验。今天我就把这个项目的完整设计思路、硬件原理、通讯协议解析以及软件源代码的核心逻辑毫无保留地分享出来。无论你是想复现一个万能遥控器还是想深入学习STM32与专用外设芯片的协同工作方式这篇文章都能给你提供一条清晰的路径。2. 核心器件选型与硬件设计思路做硬件项目第一步永远是选型。选型决定了项目的性能天花板、开发难度和最终成本。我这个万能遥控器的核心就两个主控MCU和红外收发芯片。2.1 为什么是STM32F103C8T6在众多单片机中选中STM32F103C8T6主要是基于以下几点考虑性能与资源平衡它基于ARM Cortex-M3内核主频72MHz对于处理红外协议编码、管理按键扫描和驱动一个小型OLED屏来说绰绰有余。64KB的Flash和20KB的RAM足以容纳复杂的协议库和用户数据。极高的普及度与生态这可能是国内最知名的STM32型号之一资料浩如烟海。无论是标准库还是HAL库都有成熟的工程模板。遇到任何问题几乎都能在网上找到解决方案或讨论极大降低了开发风险。丰富的外设它拥有多个USART、SPI、I2C接口这为连接HXD019通常用UART或I2C、OLED显示屏I2C/SPI、按键矩阵等提供了硬件基础。特别是其DMA功能在高效处理红外数据流时非常有用。成本与获取便利性核心板价格极其低廉且引脚全部引出方便焊接和调试。对于个人DIY和小批量制作它是性价比之王。当然它也有缺点比如核心板上的Flash和RAM对于要存储海量红外码库的情况可能稍显紧张但通过外挂SPI Flash或者对码库进行高效编码压缩完全可以解决。2.2 HXD019红外协议的“瑞士军刀”红外遥控看似简单就是发一串脉冲但其实里面的协议五花八门比如NEC、RC5、RC6、Sony SIRC、Sharp等每种协议的载波频率通常是38kHz、数据格式、头码、逻辑定义都不同。如果让STM32用IO口定时器去模拟实现所有协议的收发和解析软件复杂度会呈指数级上升且非常消耗CPU资源。HXD019就是为了解决这个问题而生的专用芯片。它的核心能力在于硬件级编解码芯片内部集成了红外调制解调器支持载波频率可调常见38kHz能自动完成信号的调制发射时和解调接收时STM32只需要通过串口发送/接收已经编解码好的数据帧即可。多协议兼容芯片固件通常内置了数十种常见红外协议。你只需要通过命令告诉它“现在用NEC协议发射这段数据”或者“自动识别刚才接收到的信号是哪种协议”它就能帮你搞定底层繁杂的时序问题。简化电路它内部集成了驱动电路可以直接连接红外发射管LED和接收头省去了外部三极管放大等电路让硬件设计更简洁可靠。在我的设计中HXD019通过**UART异步串口**与STM32通信。选择UART而非I2C主要是因为红外数据包长度不定UART的流式传输特性更合适且速率足够通常波特率设为9600或115200。STM32作为主机向HXD019发送命令和数据HXD019作为从机执行命令并返回状态或数据。2.3 硬件原理图核心部分详解完整的原理图包括电源、STM32最小系统、HXD019接口、红外收发电路、用户输入按键、输出显示OLED等部分。这里挑几个关键电路讲讲。电源部分 整个系统采用5V USB供电。通过一颗AMS1117-3.3稳压芯片将5V转换为稳定的3.3V为STM32、HXD019和OLED屏供电。在电源入口处一定要加一个100μF的电解电容和一个0.1μF的瓷片电容并联用于滤除低频和高频噪声这是系统稳定工作的基础。STM32与HXD019连接 这是核心通信链路。连接非常简单STM32的USART1_TX(PA9) 接 HXD019的RXD接收脚。STM32的USART1_RX(PA10) 接 HXD019的TXD发送脚。HXD019的VCC接3.3VGND接地。注意有些HXD019模块需要将SET引脚拉高或拉低来选择通信模式比如UART或I2C具体要参照你购买模块的数据手册。在我的设计中通过一个10k电阻将SET引脚上拉到3.3V将其配置为UART模式。红外发射电路 HXD019的IR_OUT引脚输出的是已经调制好的38kHz载波信号但驱动能力有限不能直接驱动红外发射管IRED。这里需要一个简单的三极管放大电路。我选用了一个通用的NPN三极管如S8050HXD019的IR_OUT通过一个100Ω的限流电阻连接到三极管的基极。三极管的集电极接红外发射管的负极发射管的正极通过一个100Ω的电阻接到5V电源注意这里发射管工作电压是5V以获得更强的发射功率。三极管的发射极接地。在红外发射管两端反向并联一个1N4148开关二极管用于在关断瞬间释放线圈发射管等效电感产生的反向电动势保护三极管。注意红外发射管的指向性和距离有关为了效果更好可以使用2-3个发射管并联但每个管子仍需串联独立的限流电阻计算总电流不要超过三极管和电源的承载能力。红外接收电路 这部分更简单。通用的38kHz红外接收头如HS0038B有三只脚VCC接3.3V或5V依型号而定、GND、OUT数据输出。将OUT脚直接连接到HXD019的IR_IN引脚即可。接收头内部已经完成了光电转换、放大、解调输出的是解调后的数字信号。按键与显示按键我设计了一个4x4的矩阵键盘用于输入数字、选择设备、调节参数等。行线4根接STM32的4个IO口配置为推挽输出列线4根接另外4个IO口配置为上拉输入。通过扫描法检测按键。OLED显示选用0.96寸的I2C接口OLED屏SSD1306驱动。仅需连接4根线VCC、GND、SCL接STM32的PB6、SDA接PB7。I2C总线上需要加上拉电阻通常OLED模块内部已集成。3. HXD019与STM32的通讯协议深度解析硬件是躯体通讯协议就是神经。STM32要想指挥HXD019干活必须遵循它规定的“语言”。HXD019的UART协议帧格式非常典型理解它对于调试和编程至关重要。3.1 数据帧通用格式HXD019的通信数据帧通常遵循以下结构[帧头] [长度] [命令字] [数据域] [校验和]帧头固定为0xAA或0x55用于标识一帧数据的开始。有些版本可能用0xAA作为主机发送帧头0x55作为从机回复帧头具体需查手册。我使用的版本统一用0xAA。长度指示从“命令字”开始到“校验和”之前的所有字节数。注意这个长度值本身也是一个字节。命令字指明要执行的操作例如0x01代表发射红外信号0x02代表学习红外信号0x03代表读取版本号等。数据域可变长度内容根据命令字不同而不同。对于发射命令这里存放的是红外协议类型和用户码、键值码等数据对于学习命令这里可能是空或包含参数。校验和通常是从“长度”字节开始到“数据域”最后一个字节为止所有字节的累加和可能取低8位。用于验证数据传输的正确性。3.2 关键命令详解与实战数据流我们以最常用的发射红外信号和学习红外信号两个命令为例拆解整个通信过程。命令一发射红外信号 (CMD0x01)假设我们要用NEC协议发射一个用户码为0x00FF键值码为0x15的信号。STM32构建命令帧协议类型NEC协议假设对应代码为0x00。用户码0x000xFF注意NEC协议的用户码是16位先发低位还是高位取决于芯片规定通常先发低位字节0x00。键值码0x15。那么数据域可能就是[0x00, 0x00, 0xFF, 0x15]协议类型用户码低用户码高键值码。长度 数据域长度 4字节。校验和 长度(0x04) 命令(0x01) 数据域(0x000x000xFF0x15) 的累加和假设计算后为0x1D。最终帧AA 04 01 00 00 FF 15 1DSTM32通过UART发送将上述字节流通过USART1发送给HXD019。HXD019响应如果芯片接收帧正确且校验通过它会执行发射操作并通过红外发射管发出对应的38kHz调制红外信号。同时它通常会返回一个应答帧例如AA 01 01 00 AC表示操作成功其中0x00可能是成功状态码。命令二学习红外信号 (CMD0x02)这个命令稍微复杂因为涉及等待和接收不定长的红外数据。STM32发送学习命令帧可能很简单如AA 01 02 03长度1命令2无数据域校验和0x03。这个命令是告诉HXD019“请进入学习模式准备好接收红外信号。”HXD019进入学习状态芯片会点亮一个指示灯如果模块有的话并开始监听红外接收头。用户操作用原始遥控器对准红外接收头按下按键。HXD019返回学习结果芯片成功捕获并解析红外信号后会通过UART主动上报一帧数据。这帧数据的数据域会包含学习到的协议类型、用户码、键值码甚至可能是原始的脉宽时序数据。例如返回帧AA 06 02 00 00 FF 15 40 1D这里0x02是命令字表示这是对学习命令的回复0x00是协议类型NEC0x00和0xFF是用户码0x15是键值码0x40可能是信号强度或其它信息最后0x1D是校验和。STM32处理STM32收到此帧后解析数据并将协议类型、用户码、键值码这三个关键信息存储到自己的Flash或外置EEPROM中形成一个“遥控码库”。3.3 协议实现的软件关键点在STM32的代码中实现与HXD019的稳定通信有以下几个核心环节1. UART的配置与中断处理初始化UART1波特率设为9600兼容性最好或115200速度更快8位数据位1位停止位无校验。必须开启UART接收中断。因为HXD019可能会在任何时候主动上报数据如学习结果。在中断服务函数中将收到的每一个字节存入一个环形缓冲区Ring Buffer。在主循环或一个专用的协议解析任务中不断从环形缓冲区中取出数据进行帧的拼接和解析。使用状态机State Machine来解析帧是最可靠的方法依次判断帧头、长度、收集数据、验证校验和。2. 发送函数的封装 编写一个HXD019_SendCommand()函数输入命令字和数据指针自动计算长度和校验和拼接成完整帧并通过UART发送。这个函数应处理好总线忙状态。3. 超时与重发机制 对于“学习”这类需要等待响应的命令必须加入超时机制。发送学习命令后启动一个定时器如500ms。如果在超时前收到正确的回复帧则关闭定时器处理数据如果超时则判定学习失败可能需重新发送命令或提示用户重试。4. 数据存储结构设计 在STM32的Flash中或外置AT24Cxx EEPROM需要设计一个表格来存储学习到的红外码。每条记录至少应包含typedef struct { uint8_t protocol; // 协议类型如NEC0 uint16_t user_code; // 用户码 uint8_t key_code; // 键值码 char name[16]; // 自定义名称如“电视-开机” } IrCode_TypeDef;通过按键选择或OLED菜单用户可以调用对应的记录然后STM32组织发射命令帧发送给HXD019。4. 软件架构与核心代码逻辑剖析有了清晰的硬件和协议认知我们就可以搭建软件的骨架了。我的软件整体采用“前后台”架构即中断服务程序作为“后台”处理紧急事务如UART数据接收主循环作为“前台”轮询处理各种任务。4.1 系统初始化与任务调度main()函数的主体结构如下int main(void) { // 1. 初始化系统时钟、延时函数 SystemClock_Config(); Delay_Init(); // 2. 初始化硬件外设 UART1_Init(9600); // 与HXD019通信 I2C1_Init(); // 连接OLED KEY_Matrix_Init(); // 矩阵键盘初始化 OLED_Init(); // OLED初始化 OLED_Clear(); // 3. 初始化红外码库从Flash加载 IrCodeLib_Init(); // 4. 显示主界面 OLED_ShowMainMenu(); // 5. 主循环 - 任务调度器 while (1) { Task_KeyScan(); // 任务1扫描按键 Task_IrParse(); // 任务2解析HXD019数据 Task_UIUpdate(); // 任务3更新用户界面 // 其他任务... Delay_ms(10); // 简单的延时调度也可用RTOS } }这里没有使用实时操作系统RTOS因为对于这个项目任务简单轮询足够。如果后续要增加复杂的网络功能或文件系统移植FreeRTOS是更好的选择。4.2 按键扫描与菜单逻辑实现矩阵键盘的扫描函数Task_KeyScan()需要实现消抖和状态机。一旦检测到有效按键就根据当前系统所处的“模式”或“菜单层级”来执行相应动作。void Task_KeyScan(void) { static uint8_t last_key KEY_NONE; uint8_t current_key KEY_GetValue(); // 获取当前物理按键值 if (current_key ! KEY_NONE) { if (last_key current_key) { key_press_cnt; if (key_press_cnt DEBOUNCE_CNT) { // 消抖确认 key_press_cnt DEBOUNCE_CNT; if (key_state KEY_IDLE) { key_state KEY_PRESSED; // 触发按键事件放入事件队列或直接处理 ProcessKeyEvent(current_key); } } } else { key_press_cnt 0; } last_key current_key; } else { last_key KEY_NONE; key_press_cnt 0; key_state KEY_IDLE; } }ProcessKeyEvent()函数是核心它根据一个全局的sys_mode变量如MODE_MAIN_MENU,MODE_LEARN,MODE_SEND来决定按键的意义。例如在MODE_MAIN_MENU下上下键移动光标OK键进入子菜单在MODE_LEARN下OK键触发发送学习命令给HXD019。4.3 HXD019数据解析状态机这是项目中最精妙的部分之一。UART接收中断只管存数据解析工作在主循环的Task_IrParse()中完成。我们用一个状态机来对付可能出现的帧不完整、数据错误等情况。typedef enum { FRAME_STATE_IDLE, FRAME_STATE_HEADER, FRAME_STATE_LENGTH, FRAME_STATE_CMD, FRAME_STATE_DATA, FRAME_STATE_CHECKSUM } FrameState_TypeDef; void Task_IrParse(void) { uint8_t byte; static FrameState_TypeDef state FRAME_STATE_IDLE; static uint8_t data_len 0; static uint8_t data_index 0; static uint8_t cmd 0; static uint8_t checksum_calc 0; static uint8_t rx_buffer[32]; if (RingBuffer_GetByte(uart1_rx_buf, byte)) { // 从环形缓冲区取一个字节 switch (state) { case FRAME_STATE_IDLE: if (byte 0xAA) { // 找到帧头 state FRAME_STATE_HEADER; checksum_calc 0; // 开始计算校验和 } break; case FRAME_STATE_HEADER: // 有些协议可能帧头后还有一个固定字节这里我们直接进入长度状态 data_len byte; checksum_calc byte; // 长度计入校验和 state FRAME_STATE_LENGTH; break; case FRAME_STATE_LENGTH: cmd byte; checksum_calc byte; // 命令字计入校验和 data_index 0; if (data_len 0) { state FRAME_STATE_DATA; } else { state FRAME_STATE_CHECKSUM; // 无数据域 } break; case FRAME_STATE_DATA: rx_buffer[data_index] byte; checksum_calc byte; if (data_index data_len) { state FRAME_STATE_CHECKSUM; } break; case FRAME_STATE_CHECKSUM: if (checksum_calc byte) { // 校验和正确 // 解析成功根据cmd处理rx_buffer中的数据 HandleHXD019Frame(cmd, rx_buffer, data_len); } else { // 校验错误丢弃本帧 } // 无论对错解析完一帧后回到空闲状态 state FRAME_STATE_IDLE; break; } } }HandleHXD019Frame()函数会根据cmd调用不同的处理函数。例如如果是0x02学习回复就提取协议和码值保存到码库并在OLED上显示“学习成功”。4.4 红外码库的存储与管理STM32F103C8T6的内部Flash有64KB用户程序通常用不完。我们可以利用最后的若干页每页1KB或2KB来存储红外码库。重要操作Flash前必须解锁写操作必须以半字16位或字32位为单位且写入前必须先擦除整页擦除。// 假设我们将码库存储在Flash的最后一页起始地址0x0800F800 #define IR_CODE_FLASH_ADDR 0x0800F800 #define IR_CODE_MAX_NUM 50 // 最多存储50条码 void IrCodeLib_SaveToFlash(IrCode_TypeDef *pCode, uint16_t index) { uint32_t write_addr IR_CODE_FLASH_ADDR index * sizeof(IrCode_TypeDef); // 1. 检查地址是否越界 // 2. 解锁Flash FLASH_Unlock(); // 3. 如果要写的页不是空的需要先整页擦除通常第一次使用时统一擦除 // 4. 按字32位写入数据 uint32_t *pSrc (uint32_t*)pCode; uint32_t *pDst (uint32_t*)write_addr; for(int i0; isizeof(IrCode_TypeDef)/4; i) { FLASH_ProgramWord((uint32_t)pDst, *pSrc); pDst; pSrc; } // 5. 锁定Flash FLASH_Lock(); }读取就简单了直接内存映射读取即可。为了管理方便我还会在Flash中固定位置存储一个“码库信息头”记录当前存储的码数量、版本等信息。5. 开发调试过程中的“坑”与解决之道这个项目看起来不复杂但实际动手时从硬件到软件坑是一个接一个。我把几个印象最深的分享出来希望能帮你省点时间。坑一红外发射距离极短或不稳定现象电路焊接无误代码也正常发送但遥控距离只有十几厘米或者时灵时不灵。排查首先用手机摄像头普通相机模式对准红外发射管。正常发射时在手机屏幕上应该能看到发射管发出明显的白紫色光点。如果看不到或很暗说明驱动电路有问题。检查三极管基极限流电阻。电阻太大基极电流太小三极管未饱和导通导致发射管电流不足。我最初用了1kΩ后来换成100Ω效果立竿见影。检查发射管供电电压。红外发射管需要一定的工作电压通常2V左右和电流20-100mA。确保你的限流电阻计算正确R (5V - Vf_led - Vce_sat) / I_led。其中Vf_led约1.2VVce_sat约0.2V若想要50mA电流R (5-1.2-0.2)/0.05 72Ω选择68Ω或100Ω。终极武器用逻辑分析仪或示波器抓取HXD019的IR_OUT引脚波形。你应该能看到一个被38kHz方波调制的脉冲串。如果这里波形正常但发射距离仍不够问题一定在发射管驱动电路上。坑二HXD019学习功能时好时坏现象按下学习键有时能成功有时失败毫无规律。排查环境光干扰强烈的日光灯、太阳光都含有红外成分会干扰接收头。确保学习时在室内避开强光直射甚至可以用手稍微遮挡一下接收头周围。供电噪声HXD019和红外接收头对电源噪声比较敏感。确保电源滤波电容特别是0.1μF的瓷片电容紧贴芯片的VCC和GND引脚焊接。原始遥控器电量不足旧遥控器的电池电压下降发射功率减弱导致学习失败。换新电池试试。协议兼容性虽然HXD019支持很多协议但并非全部。某些品牌的空调、风扇遥控器可能使用非常规的编码方式。如果始终无法学习某个设备很可能就是协议不支持。这时可以尝试让HXD019进入“原始码学习模式”如果支持它会记录下高低电平的原始时长虽然数据量大但可以兼容所有协议。我的代码中为此专门增加了一个“Raw Mode”学习选项。坑三STM32程序跑飞或HardFault现象在操作Flash保存码库或者进行大量字符串处理时系统突然死机。排查栈溢出增加了OLED显示和码库管理后局部变量和函数调用层级变深。我一开始没有调整栈大小在startup_stm32f10x_md.s启动文件中默认的栈大小可能只有0x4001KB。将其改大到0x800或0x1000问题解决。Flash操作中断在擦写Flash期间必须禁止所有中断。因为Flash控制器在工作时CPU是暂停的。我的擦写函数中忘了关中断。void FLASH_PageErase(uint32_t page_addr) { __disable_irq(); // 关键禁止中断 // ... Flash解锁、擦除操作 ... __enable_irq(); // 操作完成开启中断 }数组越界或指针错误在解析UART数据帧或处理码库索引时一定要做严格的边界检查。if (index IR_CODE_MAX_NUM) return;这样的防护语句能避免灾难性的内存覆盖。坑四OLED显示乱码或闪烁现象屏幕显示字符错乱或者刷新时明显闪烁。排查I2C时序问题STM32的I2C硬件有时在标准模式下不太稳定特别是在有中断干扰时。我最终放弃了硬件I2C改用GPIO模拟I2C软件I2C时序完全可控问题迎刃而解。刷新策略不要每次更新都全屏刷新OLED_Clear() 重绘所有内容。这样会导致严重的闪烁。应该采用局部更新策略只更新需要改变的区域。或者使用双缓冲机制在内存中准备好一整帧图像然后一次性发送给OLED。电源干扰确保OLED模块的电源稳定。可以在其VCC和GND之间再加一个10μF的钽电容。6. 功能扩展与进阶玩法基础功能实现后这个万能遥控器平台还有很大的扩展空间这里提几个我实践过或设想过的方向1. 增加无线控制Wi-Fi/蓝牙通过ESP-01SESP8266或HC-05蓝牙模块让STM32与之通过UART通信。这样你就可以在手机上开发一个App通过Wi-Fi或蓝牙发送指令给遥控器再由遥控器发出红外信号控制家电。实现“人在卧室关客厅空调”的场景。2. 集成RTC与定时任务给STM32加上一颗DS1302或DS3231时钟芯片。就可以实现定时开关功能。比如设定每天上午10点自动打开空调晚上12点自动关闭电视插座。代码上需要增加一个定时任务调度器不断比较当前时间与预设任务时间。3. 语音控制接入一个简单的离线语音识别模块如LD3320。虽然识别率有限但训练几个特定指令如“打开电视”、“空调二十六度”是可行的。实现“说句话就控制”的体验。4. 码库共享与云端备份如果设备接入了网络可以将学习到的红外码库上传到云端服务器如通过HTTP POST到自己的服务器。这样即使设备复位也可以从云端下载恢复。更进一步可以建立一个共享码库用户贡献不同设备的红外码造福大家。5. 使用更强大的主控如果觉得STM32F103资源紧张可以升级到STM32F4系列甚至使用ESP32。ESP32自带Wi-Fi和蓝牙双核处理能力更强可以直接用Arduino框架或ESP-IDF开发能实现更复杂的网络服务和用户交互界面。回过头看这个项目最大的收获不是做出了一个能用的遥控器而是完整地走了一遍嵌入式产品开发的全流程需求分析、器件选型、原理图设计、PCB绘制后续可以升级、固件开发、调试排错、功能迭代。每一个环节踩的坑都是宝贵的经验。硬件设计让我更深刻地理解了去耦电容、信号完整性、驱动能力这些概念软件调试则巩固了状态机、环形缓冲区、中断与主循环协作等核心编程思想。希望这份超详细的总结能为你点亮自己DIY道路上的那盏灯。本文还有配套的精品资源点击获取