news 2026/9/9 11:06:47

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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接GND,OUT接一个带外部中断功能的GPIO,比如PA0或者PB0。我习惯在OUT和VCC之间加一个10kΩ上拉电阻,虽然接收头内部有上拉,但外加一个能让波形边沿更干净,尤其是杜邦线连接的时候抗干扰能力明显好一些。

发射端方案就多样了:最简单的玩法是直接用单片机的定时器输出38kHz的PWM载波,用一个GPIO控制三极管驱动红外发射管,通过控制PWM的开启和关闭来模拟NEC时序。也可以用现成的红外发射模块,模块内部已经集成好了驱动电路,STM32只要给数据引脚送电平就行。我测试的时候更喜欢用模块,体积小、接线少,验证逻辑够用了。

2. NEC协议原理与解码实操

2.1 波形时序和位定义拆解

NEC协议的关键时序参数就几个数字,但每个数字都有讲究。先把它们刻在脑子里:

  • 引导码:9ms载波 + 4.5ms空闲
  • 逻辑"0":560μs载波 + 560μs空闲,总时长1.12ms
  • 逻辑"1":560μ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米,后来在窗口位置开了个孔才恢复正常。这种跟光学相关的坑,代码层面完全发现不了,只能靠实际测试才暴露出来。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 11:03:36

Abaqus二次开发全解析:从UMAT到Python脚本的实战指南

简介&#xff1a;面向需要进行ABAQUS二次开发的工程师与研究人员&#xff0c;这份完整代码资料包以Python语言为主线&#xff0c;系统覆盖API调用、自动化建模、自定义材料与载荷等开发场景&#xff0c;既能用于功能验证&#xff0c;也能作为二次开发模板。包内共343个文件&…

作者头像 李华
网站建设 2026/9/9 11:03:31

从BSP到系统架构师:思维方式与能力模型的关键跃迁

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 11:01:20

opencode开源AI编程助手实战指南:从安装配置到Skills扩展

最近 opencode 这个开源 AI 编程助手在开发者圈子里讨论度一下子起来了。经常能看到“opencode 安装”“opencode 使用教程”“opencode 免费模型”这些热搜词挂在首页&#xff0c;还有人问它到底是哪家公司的、和 Claude Code、Codex 有什么区别。简单说&#xff0c;opencode …

作者头像 李华
网站建设 2026/9/9 11:01:17

杭州燃气灶维修附近师傅哪里找?欧米到家快速响应家庭厨房设备维修需求

文章简介燃气灶作为家庭厨房每天使用频率较高的设备&#xff0c;长期受到油污、高温、频繁点火影响&#xff0c;容易出现打不着火、点火困难、火焰异常、自动熄火、火力变小、旋钮失灵、燃烧不充分等问题。燃气灶虽然结构相比大型家电简单&#xff0c;但涉及燃气供应、点火装置…

作者头像 李华
网站建设 2026/9/9 10:59:06

2026年AI工具选型实战指南:从需求定位到落地避坑全解析

做AI生产力工具选型这件事&#xff0c;这两年越来越像买相机——参数表上写满了“更强、更快、更便宜”&#xff0c;但真到要掏钱下单的时候&#xff0c;大多数人的第一反应还是“我到底需要哪个”。 这份指南就是来解决这个问题的。我结合2026年Q3这个时间点上的主流产品动态…

作者头像 李华
网站建设 2026/9/9 10:58:35

ECC内存错误全解析:从uncorrectable报错到MBIST测试实践

一开机就被一条告警糊脸&#xff1a;“Uncorrectable ECC Errors: 2”。旁边还跟着一条不知道什么版本的提示&#xff0c;内存槽位一会儿显示A2&#xff0c;一会儿又显示CPU0_DIMM_C2。电脑还没进系统&#xff0c;心里先凉了半截——内存坏了&#xff1f;机器要报废&#xff1f…

作者头像 李华