ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32红外遥控器实战:NEC协议解码与发射完整指南

STM32红外遥控器实战:NEC协议解码与发射完整指南 简介STM32 红外遥控器程序是一份面向嵌入式初学者及课程设计/毕设学生的完整工程源码包。项目以 STM32 为控制核心涵盖红外通信、PWM 脉冲编解码、GPIO 与定时器中断等硬件接口处理并给出基于 NEC、RC5 等标准的信号解码与校验实现能直接帮助读者理解红外遥控收发流程。资源包共 95 个文件其中包含 43 个头文件和 39 个 C 源文件另附有链接脚本、编译映射及调试备份文件压缩包仅 384KB。工程按 SYSTEM、HARDWARE、IR、KEY、TFT、FLASH、PWM、IRSend、USART1 等模块清晰组织便于按需查阅和学习。目前已由 4863 人浏览学习。下载后可直接用 Keil 打开工程对照源码掌握定时器输入捕获、状态机解码、红外发送时序设计及调试技巧也可在此基础上扩展更多遥控协议或显示反馈功能。 做了这么多年嵌入式要说哪个外设模块最亲民红外遥控器绝对排得上号。很多朋友入手STM32的第一个实战项目不是点灯就是红外遥控毕竟家里空调、电视、机顶盒一抓一大把拿手机对着录一段波形就能玩起来。我之前帮人调过好几个基于STM32的红外遥控器程序从最初的NEC协议硬啃到后面用逻辑分析仪一次性抓完整个帧踩了不少坑也攒了不少经验。这篇文章就把STM32 红外遥控器这套完整玩法拆开揉碎讲清楚从协议原理、解码实操到发射实现再到排查技巧尽量做到拿过去就能用。适合刚学完GPIO和定时器、想做点实际东西的新手也适合项目里要快速集成红外的工程师参考。1. 方案选型与整体设计思路1.1 为什么红外遥控首选NEC协议红外遥控的编码协议市面上有好几种NEC、RC5、RC6、Sony SIRC、Philips RC-MM等等。做项目选型的时候我几乎无脑推荐NEC协议原因很实在家电覆盖率最高。你家里随便翻出几个遥控器大概率有一半以上是NEC协议这意味着你不需要专门的发射器拿现成遥控器就能做解码验证。其次是资料极其丰富网上随便一搜就是一堆现成代码出了问题也好排查。NEC协议还有个特点是帧结构规整、容错性好。它用脉冲宽度来区分0和1配合引导码和停止位解码器的逻辑非常清晰。相比之下RC5协议用了曼彻斯特编码对时序要求更苛刻新手上手容易因为中断响应不及时导致解码失败。所以我的建议是除非产品明确要求兼容某个特定遥控器否则直接按NEC协议来做。1.2 硬件选型与接线方案解码端的核心器件是红外接收头市面上最常见的是HS0038B和VS1838B两者引脚兼容、用法几乎一样。接收头内部集成了光电二极管、AGC放大器和解调电路输出的是已经去掉38kHz载波后的基带信号也就是说它直接把原始码型还原成高低电平STM32只需要采集电平宽度就能解出数据。这里有个特别容易踩的坑接收头输出的是反相后的波形。空闲时输出高电平收到载波时输出低电平也就是说引导码那段9ms的载波在接收头输出端表现为9ms的低电平。初次做这个项目的人对着逻辑分析仪看半天怎么都跟协议文档对不上就是因为没意识到反相问题。接线很简单以STM32F103为例接收头VCC接3.3V有些型号可以接5V但3.3V最稳GND接GNDOUT接一个带外部中断功能的GPIO比如PA0或者PB0。我习惯在OUT和VCC之间加一个10kΩ上拉电阻虽然接收头内部有上拉但外加一个能让波形边沿更干净尤其是杜邦线连接的时候抗干扰能力明显好一些。发射端方案就多样了最简单的玩法是直接用单片机的定时器输出38kHz的PWM载波用一个GPIO控制三极管驱动红外发射管通过控制PWM的开启和关闭来模拟NEC时序。也可以用现成的红外发射模块模块内部已经集成好了驱动电路STM32只要给数据引脚送电平就行。我测试的时候更喜欢用模块体积小、接线少验证逻辑够用了。2. NEC协议原理与解码实操2.1 波形时序和位定义拆解NEC协议的关键时序参数就几个数字但每个数字都有讲究。先把它们刻在脑子里引导码9ms载波 4.5ms空闲逻辑0560μs载波 560μs空闲总时长1.12ms逻辑1560μs载波 1.69ms空闲总时长2.25ms结束位最后一个数据位之后有一个560μs的载波脉冲之后空闲重复码9ms载波 2.25ms空闲 560μs载波一个完整的NEC帧由引导码、8位地址码、8位地址反码、8位命令码、8位命令反码组成总共32位数据加上引导码和结束位。如果按住遥控器按键不放发射端会每隔110ms左右发一帧重复码接收端可以根据重复码来判断按键一直按着这个状态典型应用就是音量连续调节。为什么要有反码这是为了校验。接收端把地址码和地址反码相加、命令码和命令反码相加如果都等于0xFF就认为数据有效。实际项目里这个校验逻辑别偷懒省掉因为红外信号太容易受环境干扰不加校验的后果就是偶尔会解出错误的键值在关键控制场景里可能引发误动作。2.2 用输入捕获精准测量脉宽解码的本质就是测量每个脉冲的宽度然后根据宽度判定是0还是1。测量方式有两种一种是用外部中断翻转触发在中断里读定时器计数器的值来计算时间差另一种是用定时器的输入捕获功能。我强烈推荐输入捕获原因只有一个稳定性。外部中断方式受中断响应延迟影响很大尤其是系统里还有别的中断在跑的时候每次进入中断到读出计数器数值之间的延迟不确定会导致脉宽测量有几十微秒的误差。输入捕获是硬件完成的脉冲边沿一到计数器值瞬间被锁存到捕获寄存器精度高得多。以STM32F103为例我常用的配置是// 定时器2通道1输入捕获配置PA0 TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 先上升沿捕获 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; // 开启输入滤波抗毛刺 TIM_ICInit(TIM2, TIM_ICInitStructure); // 定时器时基配置72MHz主频72分频得到1MHz计数频率即1us计数一次 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_TimeBaseInitStructure); TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_ClearFlag(TIM2, TIM_FLAG_CC1); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE);这里有几个细节需要重点说。第一分频系数设成72减1得到1MHz的计数时钟也就是计数器每1微秒加1这样脉宽测量直接读差值就是微秒数方便后续判断。第二TIM_ICFilter这个参数我设成0x0F也就是开启了最强的输入滤波可以滤掉小于约220ns的毛刺。但要注意滤波太强会让边沿延迟几十纳秒对红外解码来说完全不影响却能显著提升抗干扰能力。2.3 解码状态机从毛刺中提取有效帧输入捕获中断里每捕获到一个边沿我们拿到的是相邻两次捕获的间隔硬件上需要做边沿翻转。我习惯用一个变量记录上一次捕获的电平方向每次进中断后改变捕获极性这样两次捕获之间的差值就是一个完整的脉冲宽度。解码的核心逻辑可以抽象成一个小状态机// 伪代码输入捕获中断服务函数 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); uint16_t capture TIM_GetCapture1(TIM2); uint16_t diff capture - lastCapture; // 计算时间差 lastCapture capture; // 根据当前状态处理diff switch(decodeState) { case STATE_IDLE: // 等待引导码9ms低电平接收头反相后 if (diff 8000 diff 10000) decodeState STATE_GUIDE; else if (diff 15000) // 超时复位状态 bitCount 0; break; case STATE_GUIDE: // 引导码后的4.5ms高电平 if (diff 4000 diff 5000) { decodeState STATE_DATA; dataValue 0; bitCount 0; } else decodeState STATE_IDLE; break; case STATE_DATA: // 数据位560us低电平 560us或1690us高电平 if (diff 400 diff 700) { decodeState STATE_DATA_SPACE; // 等待决定0或1的间隔 } else { decodeState STATE_IDLE; } break; case STATE_DATA_SPACE: if (diff 400 diff 700) { dataValue (dataValue 1) | 0; // 逻辑0 bitCount; } else if (diff 1500 diff 2000) { dataValue (dataValue 1) | 1; // 逻辑1 bitCount; } else { decodeState STATE_IDLE; break; } if (bitCount 32) { decodeState STATE_IDLE; frameReady 1; // 解码完成标志 } break; } } }上面这个状态机配合4字节变量存储数据每一bit往左移一位32个bit收完就是一整个帧。收完帧之后在主循环里做校验地址加反码等于0xFF命令加反码等于0xFF两个都满足才认为数据有效。我实际调试的时候发现状态机的超时处理和复位逻辑非常关键。如果长时间没有新边沿进来状态机必须能自动回到空闲状态否则下一帧数据来了还在上一个状态里打转解码必然错乱。解决办法是在定时器更新中断里做一个超时判断超过20ms没有捕获事件就把状态机复位到STATE_IDLE。这个处理我当时写漏了结果就是遥控器按得稍微快一点第二帧就解不出来。3. 发射端实现从0开始构造NEC波形3.1 PWM载波生成与时序控制解码能做通之后很多人自然会想能不能让STM32自己发射红外信号去控制别家的设备答案当然可以。发射端的核心也是两点载波生成和码型调制。红外遥控的标准载波频率是38kHz少数设备用36kHz或40kHz接收头对频率有一定容忍度但最好还是把频率调到最准。用STM32的定时器输出PWM非常容易以F103为例// 使用TIM3通道2输出38kHz PWM占空比1/3 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 1MHz计数频率 TIM_TimeBaseStructure.TIM_Period 26 - 1; // 1MHz/26 38.46kHz接近38kHz TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseInitStructure); TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 8; // 约1/3占空比 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInit(TIM3, TIM_OCInitStructure);需要注意的是载波占空比不是越大越好。红外发射管有最大平均电流限制占空比太大容易烧管子太小则发射距离不够。实践中1/3占空比是个很好的折中既能保证接收头可靠解调又不会让发射管过热。有些方案直接用50%占空比也能工作但长时间连续发射的时候发射管发热明显寿命会打折扣。发射时序控制有两种做法一种是在发送码元期间持续开启PWM空闲时关闭PWM另一种是用GPIO直接控制发射管在需要发射载波的时段用软件翻转GPIO来模拟38kHz方波。第二种做法简单但占CPU且频率不稳我强烈不推荐。正确的做法是PWM始终开启用定时器中断或者延时函数精确控制PWM的输出使能这样载波的频率和相位始终稳定接收端解调更可靠。3.2 发送一帧NEC数据的完整流程发送一帧NEC数据的基本流程是先发9ms载波再停4.5ms然后逐个bit发送最后发结束位。时序控制用阻塞延时就能做关键是要精确我用的是SysTick做的微秒级延时函数。void IR_SendNEC(uint8_t addr, uint8_t cmd) { // 1. 引导码9ms载波 4.5ms空闲 IR_TX_ENABLE(); delay_us(9000); IR_TX_DISABLE(); delay_us(4500); // 2. 地址码和地址反码 for (int i 0; i 8; i) { IR_SendBit((addr i) 0x01); } for (int i 0; i 8; i) { IR_SendBit((~addr i) 0x01); } // 3. 命令码和命令反码 for (int i 0; i 8; i) { IR_SendBit((cmd i) 0x01); } for (int i 0; i 8; i) { IR_SendBit((~cmd i) 0x01); } // 4. 结束位 IR_TX_ENABLE(); delay_us(560); IR_TX_DISABLE(); } void IR_SendBit(uint8_t bit) { IR_TX_ENABLE(); delay_us(560); IR_TX_DISABLE(); if (bit) { delay_us(1690); // 逻辑1 } else { delay_us(560); // 逻辑0 } }这个发送函数看起来简单但实际使用中有一个明显的缺陷阻塞延时期间CPU什么都干不了。如果在主循环里需要频繁发送遥控码同时又想处理按键扫描、OLED刷新就会互相卡顿。更好的方案是把发送过程改造成基于定时器中断的状态机发送在中断里每到一个时间点切换一次电平状态主循环只管设置要发送的数据剩下的事交给硬件。这个优化做下来发射功能就变成了一个后台任务体验提升非常明显。需要补充的是红外发射管的驱动电路。GPIO直接驱动发射管电流不够一般需要加一个NPN三极管或者MOS管。典型电路是GPIO通过1kΩ电阻接三极管基极发射极接地集电极串一个红外发射管加一个10Ω~20Ω限流电阻到3.3V或5V电源。PWM信号接到GPIO上发射管就跟着PWM频率闪烁发出38kHz的红外光。这里注意发射管的压降和限流电阻的选型1.2V~1.5V的正向压降、20mA~50mA的驱动电流是常见工作点限流电阻用欧姆定律算一下就能确定。4. 常见问题与排查技巧实录4.1 解码失败和误码的排查思路做红外遥控项目最容易遇到的就是遥控器按了程序没反应。我总结了排查顺序按这个顺序查基本能覆盖90%的问题第一确认硬件接线和供电。接收头VCC和GND别接反OUT引脚一定要接到有外部中断能力的引脚上F103的PA0、PB0、PC0这些都可以。有些新手把接收头接到ADC引脚上那当然进不了中断。第二看波形。这一步我强烈建议用逻辑分析仪或者示波器看一下接收头OUT引脚的波形按遥控器的时候应该能看到一串脉冲。如果完全没有波形检查遥控器电池和接收头型号。如果波形有但很乱比如高低电平毛刺特别多优先检查接线是不是太长、有没有和电源线绑在一起另外给接收头电源引脚加一个0.1μF去耦电容也能改善。第三验证定时器配置。用调试器在输入捕获中断里打断点看每次捕获的差值是否在合理范围。如果差值永远是一个固定值比如永远等于0那多半是捕获通道配置错了或者计数频率不对。我遇到过最诡异的案例是同一个遥控器在开发板上解码完全正常集成到产品里之后偶发误码。排查到最后发现是板子上的DC-DC电源模块在负载变化时产生了几百毫伏的纹波红外接收头的供电被干扰了。解决方法是把接收头供电从DC-DC输出改成了LDO输出同时在接收头VCC引脚就近加10μF电解电容和0.1μF陶瓷电容。这个案例告诉大家硬件抗干扰能力才是解码稳定性的根基代码写得再花哨也弥补不了电源脏的问题。4.2 发射距离短和遥控不灵光的处理红外发射做出来之后常见故障是对着接收头能控制稍微远一点就不行或者角度偏一点就没反应。红外是直线传播且方向性很强的信号发射管和接收头之间必须近似直线对准中间不能有遮挡。如果距离确实很近不到1米优先检查发射管的驱动电流是否足够用万用表量一下发射管两端有没有6V到5V的压差、工作电流是否在15mA以上。另外容易被忽略的是红外发射管的波长。遥控器用的红外发射管波长是940nm可见光摄像头拍不到但用手机摄像头对着发射管能看到紫色光点。如果买错了波长比如850nm的虽然肉眼看可能更亮但接收头的光谱响应不匹配距离会大打折扣。这个坑我栽过一次花了半天时间怀疑电路最后换回940nm的管子立刻好了。还有一个可以优化的细节接收头的AGC时间常数会影响连续帧的接收。有些接收头在收到一帧数据后需要一小段时间恢复如果程序在收到帧之后立即开始等待下一帧可能因为接收头还在恢复期而丢掉重复码。实测下来在解完一帧之后加2~4ms的延时再重新进入等待状态重复码的接收成功率会明显提升。4.3 几个实用调试小技巧最后分享几个我自己常用的调试手段都是花时间换来的经验。用手机摄像头辅助检查发射环节。这个方法非常实用手机摄像头对红外光敏感按下发射键的时候从手机屏幕里能看到发射管点亮。这能帮你区分程序没发送和发送了但接收端不行两类问题。手机屏幕里看得到闪光的脉冲节奏甚至能粗略判断载波是否在正常调制。解码数据通过串口打印出来看原始值。我习惯在解码完成之后不仅打印最终的命令码还打印32位的原始数据以及每一位的脉宽时间。这样一旦出现误码能直接从数据上看出是哪一位跳变导致的快速定位是时序问题还是干扰问题。调试接收灵敏度时可以用示波器监控接收头输出端的脉冲宽度。如果按下遥控器后输出的引导码脉宽不是在9ms附近比如变成了7ms或者11ms说明遥控器本身有偏差或者电池电压偏低。这时候如果程序里的容错范围写得太死比如只接受8.5ms到9.5ms就会解码失败。所以解码程序里对引导码和数据位的判断范围要留出足够的容差我一般给20%左右的余量太低容易漏码太高则容易把干扰噪声误判成有效数据。5. 扩展玩法与个人心得红外遥控程序跑通之后往上扩展的方向非常多。我见过有人把它做成万能红外遥控器用一个红外发射管加一个按键学习空调、电视的原始码存到Flash里之后一键发射也有人把红外接收头装在鱼缸灯上用遥控器控制灯光亮度和颜色搜STM32鱼缸能看到不少类似案例还有人结合ESP8266模块做了一个手机APP远程控制红外家电的网关这就是物联网场景里常见的红外桥接器了。从技术上说经典红外遥控项目真正有价值的不是解码这个动作本身而是它对嵌入式时序处理能力的锻炼。做这个项目你会接触到定时器输入捕获、输出比较、外部中断、状态机设计、精准延时这几个核心技能这几样几乎在后续任何一个稍微复杂一点的ST芯片项目里都会用到。我见过不少工程师简历上写着熟悉STM32外设开发但问到底层定时器原理的时候答不上来红外遥控项目恰好能把这些基础打扎实。最后提醒一句实验的时候注意红外LED的发射角度和接收头的安装位置如果是在产品外壳里做嵌入式设计要给红外窗口留出足够的透光区域很多外壳材质对红外光有很强的遮挡装上之后遥控距离骤减。我当时做的一个小盒子外壳用的黑色亚克力装上之后遥控距离从8米掉到不到1米后来在窗口位置开了个孔才恢复正常。这种跟光学相关的坑代码层面完全发现不了只能靠实际测试才暴露出来。本文还有配套的精品资源点击获取
返回列表