资讯详情

资讯详情

STM32自制万能红外遥控器:NEC协议解码与发射实战

简介基于STM32的万能红外遥控器设计文档面向嵌入式爱好者、电子竞赛队伍及智能家居开发者。文档完整记录一款集红外控制、红外学习、语音控制、Wi-Fi远程控制于一体的遥控器项目从需求分析、硬件选型到STM32程序逻辑、Qt上位机开发均有覆盖并给出ESP8266模块调试、语音识别固件烧录等关键环节的实操细节。功能上涵盖100组红外指令学习与发送、4x4电容矩阵键盘操作、1.44寸LCD状态显示、蜂鸣器反馈以及Android/Windows上位机APP远程控制语音指令支持小爱同学、小度、天猫精灵等常见助手。技术栈涉及STM32、ESP8266、Qt、海凌科V20语音识别模块等包体为单个PDF文件大小46.43MB内容按项目实现顺序编排含硬件原理图、实物图、核心代码注释、调试流程与注意事项。已有79人学习适合作为毕业设计、课程设计或智能家居产品预研的参考资料。1. 方案是怎么定下来的STM32做万能遥控器的核心优势茶几上堆着四五个遥控器电视一个、机顶盒一个、空调一个、风扇一个客厅里找不到哪个对应哪个这大概是每个家庭都经历过的事。“万能红外遥控器”这个思路其实很早就有了市面上几十块钱的红外学习遥控器也不少但做嵌入式开发的人都会有一个念头与其买一个功能固定的不如自己用单片机做一个。既能学习原装遥控器的码又能自定义按键布局还能加屏幕显示当前状态比买来的产品自由太多。选STM32而不是其他平台主要是三个方面考虑。第一是片内外设足够丰富定时器的输入捕获和PWM输出恰好可以覆盖红外的接收解码和发射调制两条链路不需要额外芯片。第二是Flash空间够大STM32F103C8T6有64KB Flash除了固件之外还能划出一块区域存码库不像51单片机那么紧张。第三是生态成熟标准外设库和HAL库的资料都非常多尤其是后面要说的NEC红外协议几乎每个做红外遥控的人都是从NEC开始入门的。整套系统的架构并不复杂红外接收头负责捕获原装遥控器的信号STM32通过定时器输入捕获把脉冲宽度解析成数据帧然后按协议存入内部Flash需要使用时再通过定时器PWM产生38kHz载波由IO口控制红外发射管把编码数据发出去。用户交互端加一个OLED屏幕和几个按键就能做到不依赖上位机独立操作。整机成本算下来核心板的十几块钱加上接收头、发射管、屏幕、按键总共不到三十块比起直接买一个学习型遥控器的价格也没差太多但多出来的可玩性是成品设备给不了的。我用的时候遇到过一个问题很多便宜的“万能遥控器”其实只是内置了码库对老型号设备经常匹配不上而自学习方案完全没有这个问题——只要原装遥控器还能按出信号就能学进来。这也是我坚持做学习型而不是纯码库型的原因。2. 红外解码原理先看懂NEC协议再谈采集与存储2.1 NEC协议的时序拆解红外遥控的起点是红外编码协议。市面上主流家电用的协议有好几种NEC、RC5、RC6、Sony SIRC、飞利浦的RCS等等。这些协议各不相同但基本逻辑是一致的用一定频率的载波通常是38kHz承载数据脉冲接收端通过高低电平的持续时间来区分逻辑0和逻辑1。NEC协议是其中最典型也最容易理解的大部分国产家电遥控器都用它所以它天然适合作为自学习红外遥控的第一步。NEC一帧数据的结构是引导码9ms高电平 4.5ms低电平16位地址码通常8位地址 8位反码8位数据码 8位反码算上结束位约560us的高电平一帧总时长约70ms逻辑0的表示是560us载波 560us空闲逻辑1是560us载波 1690us空闲。注意NEC还有扩展协议地址码可能达到16位但解码原理不变。接收端的红外接收头如HS0038B或VS1838B负责了一部分解调工作其内部集成放大、限幅、带通滤波和检波电路输出端直接把38kHz载波解调为数字电平。也就是说接收头输出的就是上图这些脉冲的包络线高电平对应当前载波存在实际输出是反相的低电平表示空闲。对于MCU来说解码的核心任务就变成记录每一次电平跳变的时间戳计算脉冲宽度再按照协议规则去解释这些宽度。2.2 用定时器输入捕获记录脉冲宽度STM32定时器的输入捕获功能在这里派上了大用场。思路很简单把红外接收头的输出脚接到定时器的捕获通道上配置上升沿和下降沿都触发捕获在每个跳变沿读取当前计数器的值前后相减就得到了脉冲宽度。以STM32F103C8T6为例TIM2挂载在APB1总线上主频72MHz配置输入捕获为双沿触发计数器工作在最大值为0xFFFF的16位模式下。72MHz下计数周期约13.9nsNEC协议最短的560us对应约40320个计数远小于65535的上限不会溢出。即使遇到极端的长时间无信号也不用担心只会进入空闲状态。解码部分可以用一个简单的状态机写逻辑。核心代码如下typedef struct { uint16_t address; uint8_t command; uint8_t command_inv; uint8_t is_repeat; uint8_t frame_ok; } NEC_Frame_t; void IR_Decode(uint32_t pulse_us, NEC_Frame_t *frame) { static uint8_t bit_count 0; static uint32_t code 0; static uint8_t state 0; static uint16_t addr 0; // 判断引导码9ms 高电平 4.5ms 低电平 if (state 0) { if (pulse_us 8500 pulse_us 9500) { state 1; } return; } if (state 1) { if (pulse_us 4300 pulse_us 4700) { state 2; // 正常引导码之后的数据位 bit_count 0; code 0; } else if (pulse_us 1900 pulse_us 2300) { // 重复帧引导码9ms 2.25ms对应按键长按 frame-is_repeat 1; frame-frame_ok 1; state 0; } else { state 0; } return; } // 数据位先判断低电平维持区间即数据位的空闲段 if (pulse_us 100 || pulse_us 2000) { state 0; return; } if (bit_count 32) { code 1; if (pulse_us 1450) code | 1; // 1.69ms 空闲 逻辑1 bit_count; } if (bit_count 32) { frame-address (code 16) 0xFFFF; frame-command (code 8) 0xFF; frame-command_inv code 0xFF; frame-frame_ok 1; frame-is_repeat 0; bit_count 0; state 0; } }代码里用了脉冲宽度阈值的方式判断逻辑1实际使用中560us和1690us之间的裕量很大只要接收头质量正常、距离不要太远判断基本不会出错。需要特别说明的是接收头输出的电平是反相的——有载波时输出低电平无载波时输出高电平。捕获时间戳时不需要关心电平本身只要在翻转沿记录时间差即可但如果你要自己实现判断高电平还是低电平这一点千万别搞反网上好多初学者在这里栽了跟头。2.3 自学习模式下接收质量的坑自学习模式对接收质量的要求比对码库模式更高因为码库是“识别”自学习是“记录”。识别只要读出数据就行记录则要把数据能完整还原成发射时序。实测下来接收距离、环境光干扰、接收头的摆放角度都会影响学习成功率。最好的做法是学习时把遥控器对准接收头距离控制在5~15cm环境光不要太强尤其要避开阳光直射和节能灯节能灯的红外干扰比较明显。如果按了学习键但捕获到的脉冲宽度毫无规律先别急着怀疑代码用示波器或逻辑分析仪看接收头输出。没有仪器的话可以用串口把每次捕获的脉宽时间戳打印出来肉眼观察有没有9ms的引导码这比瞎猜快得多。3. 码库存储与数据结构不是所有数据都值得存成原始时间戳3.1 两种存储策略的取舍自学习型遥控器需要存数据。怎么存演化的路上有两条分支方案A直接把捕获到的高低电平时间戳按顺序存成数组。优点是适配所有红外协议不用解析缺点是极占空间。一个NEC帧约20多个脉冲每个脉冲存uint16_t的时间戳需要40多字节这还好但像空调遥控器这种复杂的协议一帧可能有上百个脉冲存储开销就比较难看。方案B先解析成协议数据地址码、数据码发射时再按协议规则重建波形。优点是省空间、便于管理和匹配缺点是只能支持已解析的固定协议遇到未知协议就无能为力了。对于NEC协议方案B非常划算一个完整按键的命令只占2字节地址 2字节数据的空间。但作为万能学习型遥控器你永远不知道用户会拿什么遥控器来学所以更好的架构是把两种方案做混合已知协议走解析通道未知协议按原始脉冲时间戳保存。我在这个项目里把两种都做了。对于NEC协议存储结构可以压到最小一个按键的码库记录是这样typedef struct { uint8_t protocol; // 协议类型0NEC1原始脉冲 uint8_t is_ext; // 是否扩展地址 uint16_t address; uint8_t cmd; uint8_t cmd_inv; uint16_t raw_len; // 原始脉冲模式下有效 uint16_t raw_data[200];// 原始脉冲模式下的时间戳数组 } CodeEntry_t;NEC协议下raw_data不启用一条记录只占约7字节。对于原始脉冲模式raw_data保存每个脉冲的时间长度发射时按1:1还原。空调遥控器一帧的脉冲数比较多加上引导码可能上百个200个uint16_t的数组能覆盖绝大多数场景。3.2 Flash分区与磨损均衡STM32F103C8T6的Flash是64KB按1KB一页划分。项目固件编译后一般不到30KB所以还有相当一部分空间可以规划成码库区。我习惯把码库放在最后一个扇区附近比如地址0x0800F000开始的8页8KB能存几百条NEC码对于家用场景完全够用。但这里有个必须提醒的坑STM32 Flash擦写有寿命限制F103的Flash擦写次数约为1万次。如果每学习一个按键就写一次Flash用久了会出问题。做自学习遥控器时学习操作不可能像读取操作那么频繁但也要做好保护。我加了一个简单的磨损均衡机制码库区不固定写同一页而是按顺序往后面页写写完一轮再从头覆盖。这样能延长Flash寿命。更简单粗暴的做法是学习前把新数据写入RAM等用户确认保存后再统一写入Flash避免学习过程中反复擦写。还要注意Flash按页擦除的特性。如果想更新码库里的某一条记录不能只给它擦掉重写通常做法是把整页读出来在RAM里修改目标条目然后整页擦除再写回。STM32F103的单页是1KB读出来改掉再写回的时间大约几十毫秒用户完全无感。代码里用HAL库的HAL_FLASH_Erase和HAL_FLASH_Program就能完成注意操作前必须解锁FlashHAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase_cfg { .TypeErase FLASH_TYPEERASE_PAGES, .PageAddress CODE_BANK_ADDR, .NbPages 1 }; uint32_t page_err 0; HAL_FLASHEx_Erase(erase_cfg, page_err); // 逐字节写入... HAL_FLASH_Lock();3.3 码库的索引管理数据存进去了怎么快速找到我用了一个非常简单的两层索引第一层是码库条目的线性表每条记录包含协议类型、地址、命令和存储位置等元数据存于Flash末尾使用时一次性读入RAM第二层是用户自定义的按键映射表指定“按键1对应哪条码库记录”。有了这两层按遥控器上的数字1时先查按键映射表得到码库索引再根据索引找到具体码库记录解析发射。整个查找过程在几十条记录里做线性扫描耗时几乎可以忽略。4. 发射电路与38kHz调制为什么同一个定时器能干两件事解码用定时器输入捕获发射又要用定时器PWM来产生38kHz载波这时候有一个问题同一个定时器还能同时用来捕获吗答案是看资源分配。F103C8T6有TIM1到TIM4共四个定时器外加一个基本定时器TIM6。通常的解码用TIM2的CH1发射用TIM3的CH1两者互不干扰。发射电路的原理很简单STM32的IO口输出高低电平控制红外发射管发光但IO口直接驱动能力有限而且红外发射管的压降和电流需求通常需要外接三极管来放大。典型电路是PA6输出PWM波经过一个NPN三极管比如S8050或MOS管当作开关管驱动红外发射管。LED串一个几十欧姆的限流电阻接到VCC。发射时序的控制方式有两种。一种是用定时器PWM输出38kHz载波需要发射载波时开PWM不需要就关掉码元时间靠延时函数或阻塞计数控制。另一种是用GPIO翻转产生载波对时序精度要求高而且浪费CPU不推荐。先说载波频率的计算。STM32的定时器时钟来自APB1倍频后的72MHz设置TIM3的PSC分频系数和ARR自动重装值就能得到指定频率的PWM。38kHz对应的ARR计算如下定时器时钟 72MHz需要PWM频率 38kHzARR 1 72MHz / (PSC 1) / 38kHz取PSC 0则ARR ≈ 1894。为了保证更精细的频率调节和占空比控制实际工程里我习惯先分频PSC 1ARR 72MHz / 2 / 38kHz ≈ 947。占空比设置在1/4到1/3之间比较合适红外发射管峰值电流大、平均电流低载波导通时间太短发射距离会缩小太长会发热甚至烧管。发射NEC一帧数据时只要按协议要求的时间控制PWM的开关即可。伪代码void NEC_SendByte(uint8_t data) { for (int i 0; i 8; i) { IR_PWM_ON(); DelayUs(560); IR_PWM_OFF(); if (data 0x01) { DelayUs(1690); } else { DelayUs(560); } data 1; } }注意一个细节NEC协议在数据位之间的空闲期是不发载波的所以发射完一个位后要完全关闭PWM。如果你用的是片上定时器加DMA或中断方式产生精确时序那更优雅对于学习型遥控器除了空调这类长帧协议外NEC的发送用阻塞延时就能有很好的效果没必要上DMA。STM32主频72MHz每条延时指令误差在微秒级以内一个70ms的帧累计误差小于1%家电设备完全识别得了。发射管的选择也有讲究。市场上常见的红外发射管有两类一类是850nm波长的小功率管适合短距离3米内另一类是940nm的中功率管加上透镜能打8米以上。我实际测试过用普通Φ5发射管100欧姆限流电阻3.3V供电直射距离能达到4~5米转弯或隔着玻璃会衰减要在客厅场景全覆盖的话建议用两个发射管并联或者选峰值电流更大的管子。再提一个容易踩坑的地方38kHz是NEC的标准载波但有些空调遥控器用38.5kHz或37.9kHz区别很小接收头带宽通常能容忍而Sony的SIRC协议用40kHz如果你要兼容可以把定时器ARR改成固定值来调整载波频率。万能红外遥控器的“万能”很大程度上体现在这里——不是把一种协议做得多完美而是能覆盖不同设备的载波差异。5. 按键映射与用户界面从“能发射”到“好用的遥控器”之间差了什么自学习搞定、发射搞定这个项目其实已经能用了。但从能用到好用还得解决一个实际问题用户怎么知道按哪个键对应哪个设备总不能每次发射前都拿串口看调试信息。我的做法是加了一个0.96英寸OLED屏幕用I2C接口连接通过四个按键操作界面。开机后屏幕显示当前设备通道比如“TV”或“AC”上下键切换通道左右键选择要执行的按键功能。每个通道对应一套码库映射表按下“学习”功能键后屏幕提示“Learning...”此时把原装遥控器对准接收头按对应按键学习完成后屏幕显示“OK”这套交互流程和商品遥控器非常接近。代码结构上我用了简单的菜单状态机不引入RTOS裸机轮询按键加屏幕刷新。OLED显示用软件I2C成本低接线也方便标准库的I2C外设在这类低速率场景下不如软件模拟顺手。按键处理必须做消抖。红外接收头对杂散信号很敏感如果按键消抖做不好按一下可能触发两次学习存储的数据就乱了。我用最简单的20ms延时消抖10ms读一次电平连续两次一致才认为按键生效实测误触率降到可以忽略。还有一个从实际使用中总结出来的体验优化空调遥控器因为有“开机/关机”状态差异、风速档位切换、制热制冷切换等它的每个按键对应的红外命令不是固定不变的而是“以当前状态为输入、生成不同的数据帧”。如果用原始脉冲记录方式要原样存储空调遥控器的所有按键状态组合非常占空间用协议解析方式则至少要知道该空调协议的类型。对于这部分我提供了一种笨办法就是“多帧连续学习”——长按某个自定义键时连续学习多帧发射时按顺序播放模拟空调遥控器按键时连续发多帧的行为。实测控制格力和美的的老款空调基本靠谱新款变频空调协议复杂只能靠大码库覆盖。6. 实测中的坑与解决载波、距离、数据错乱的真实教训6.1 载波频率不对导致的学习成功但发射失败第一次做完发射功能时我遇到一个非常迷惑的现象用原装遥控器对着接收头学习学习成功但把学习到的数据发射出去电视毫无反应。用手机摄像头看发射管能看到红外光在闪说明确实在发但设备不认。排查到最后发现是载波频率偏差太大。我配置的PWM频率计算是38kHz但忽略了一点STM32的APB1定时器时钟不是直接等于72MHz的。F103的APB1最大频率只有36MHz定时器如果要达到72MHz必须使能定时器时钟的2倍频。在标准库的SystemInit配置里默认情况下RCC_PCLK1_Div2会把APB1设为36MHz但定时器外设的时钟源在APB1分频系数大于1时会自动翻倍。用CubeMX配置的工程一般会自动处理好但手动配置标准库工程时漏了这个细节PWM频率就会变成19kHz而不是38kHz差了一倍接收头根本没反应。解决方法就是在初始化代码里检查RCC时钟树确认TIM3的时钟确实为72MHz。用CubeMX重新生成一次时钟配置或者直接拿HAL_RCC_GetPCLK1Freq()打印出来验证。6.2 发射距离受占空比和电流的双重影响红外发射管驱动的限流电阻值直接决定了发射功率。100欧姆电阻在3.3V下峰值电流约30mA忽略三极管压降这个电流对于普通Φ5发射管来说距离有限。实测在原装遥控器电池满电的情况下原装发射管峰值电流能到100mA量级单管发射距离能到10米以上我的电路只有其1/3。后来我把驱动改成了两个发射管串联再配一个三极管限流电阻换成47欧姆峰值电流到50mA左右发射距离提升到约7米。但这还没完——载波占空比也会影响发射管平均功耗如果占空比设到50%连续发射一帧70ms发射管的平均电流会更高时间久了有烧管风险。实测用1/3占空比既能保证距离又比较安全。6.3 学习时收到的数据为什么有时候会出现随机跳变一种典型的“幽灵bug”是同一把原装遥控器同一个按键学习两次得到的数据不一样。排查后发现是接收头在逻辑1和逻辑0的边界处产生了一个额外脉冲也就是硬件消抖电路在某些信号摆率下产生了振铃。接收头输出的脉冲本身不够干净再加上MCU捕获的边沿有些是“假边沿”导致脉冲宽度识别出现偏差。解决办法是在解码状态机里加一个最小脉冲宽度过滤小于100us的脉冲直接丢弃。同时自学习之前连续读取三帧三帧数据完全一致才认为学习成功。如果三帧有差异提示用户重新操作。这个策略非常有效把学习成功率从90%左右拉到了接近100%。7. 已知协议之外的扩展方向把这套方案做成通用红外平台NEC协议只是红外遥控协议的一份子。如果你想兼容更多设备需要了解其他协议的差异。RC5协议是曼彻斯特编码中间跳变表示逻辑SIRC协议用脉冲间隙的个数表示数值空调类设备多是自定义协议。不同协议之间的共性是“载波频率 脉冲时序”只要你的采集和发射链路能适配足够宽的频率范围方案就能继续扩展。我在设计时保留了两个通用接口typedef struct { uint32_t carrier_hz; void (*send_bit)(uint8_t bit); void (*send_frame)(const uint8_t *data, uint8_t len); int (*decode_frame)(const uint16_t *pulses, uint16_t len, uint8_t *out, uint8_t *out_len); } IR_Protocol_t;每增加一种协议就实现一套send/decode回调注册进来。UI上的设备类型选择本质就是在切换这份协议表。这样一来万能红外遥控器本身就成了一个红外协议研究平台后续加RF射频遥控315MHz/433MHz模块也只是复用同一套按键映射和界面逻辑只不过把红外收发头换成射频模组。我自己后续又在这套硬件上跑过LVGL的简单移植用带SPI接口的大尺寸屏替换了OLED操作界面从三行文字变成了图形化图标整体体验提升明显。前提是MCU从F103C8T6换成了F429或F407这类Flash和RAM更大的型号说明了这套方案的可扩展空间确实不小。对于刚入门的同学我建议先把NEC协议吃透把学习、存储、发射整条链路跑通再考虑扩展其他协议——因为底层逻辑通了往上加东西只是时间问题。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →