公司动态
STM32F103定时器输入捕获解码NEC红外遥控:38kHz载波原理与按键值稳定解码实测
文章目录摘要一、为什么需要定时器输入捕获1.1 三种解码方案对比1.2 输入捕获能解决什么二、NEC 协议拆解载波与脉宽编码2.1 38kHz 载波物理层的噪声过滤器2.2 帧结构与位时间2.3 接收头输出的关键陷阱反相三、硬件选型与接线3.1 器件清单3.2 接线表四、CubeMX 配置与定时器参数计算4.1 时钟与定时器配置4.2 分辨率与量程的权衡五、解码状态机实现5.1 总体思路5.2 定时器初始化5.3 捕获中断与状态机5.4 校验与按键值提取六、测试验证理论值与实测对照6.1 脉宽测量精度6.2 解码成功率6.3 CPU 占用对比七、故障排查两个踩坑复盘7.1 坑一接收头输出反相引导码判断永远失败7.2 坑二长按重复码导致计数器回绕误判7.3 坑三接收头载波频率选错灵敏度断崖式下降7.4 坑四电源纹波导致随机丢帧八、总结核心要点适用边界与局限扩展方向参考资料摘要红外遥控在家电控制中无处不在NEC 协议凭借 38kHz 载波与脉冲宽度调制成为主流标准但用 GPIO 轮询或外部中断解码时存在 CPU 占用高、易受毛刺干扰的问题。本文基于 STM32F103C8T6 VS1838B 一体化接收头利用定时器输入捕获硬件测量脉冲宽度配合状态机完成 NEC 协议解码。实测 1m 距离解码成功率 100%、3m 距离 99%引导码宽度测量误差 1%单帧解码仅触发约 4 次中断、CPU 占用 5%。文中给出完整接线、CubeMX 配置与工程级解码代码并复盘接收头反相、定时器溢出两个典型踩坑。一、为什么需要定时器输入捕获拿到一个红外遥控器最直觉的做法是接一根杜邦线到 GPIO然后写个while循环反复读电平、数时长。这个方案在演示代码里能跑但有两个硬伤一是轮询会占用 CPU 大量时间二是红外信号对环境光极其敏感一个毛刺就可能让整个帧解错。实践中我更推荐两种方案外部中断 定时器计数或者定时器输入捕获。这里先交代一下我为什么最终选了输入捕获——这个决策过程对理解后面代码很重要。1.1 三种解码方案对比方案原理中断次数/帧CPU 占用抗毛刺实现难度GPIO 轮询循环读电平 软件计时0纯轮询高占满核心差低外部中断 定时器边沿触发中断读定时器计数约 67 次15%~20%一般中定时器输入捕获硬件锁存边沿时刻溢出才中断约 4 次5%好可开输入滤波中关键差异在中断次数。一个完整的 NEC 帧包含 1 个引导码 32 位数据 1 个结束位粗略算下来有 60~70 个边沿。如果每个边沿都进一次中断在 72MHz 的 F103 上中断压栈/出栈的开销会在密集按键时累积成可感知的卡顿而输入捕获由定时器硬件把边沿时刻锁存到 CCR 寄存器里软件只需要在捕获中断里读取数值中断次数直接降了一个数量级。1.2 输入捕获能解决什么输入捕获的核心价值是用硬件时钟给脉冲打时间戳。定时器以固定频率自增计数当输入引脚发生边沿时硬件自动把当前计数器值拷进 CCR 寄存器并触发一次捕获中断软件读 CCR 就能算出两次边沿之间的精确时间。这套机制天然适合 NEC 这种用脉宽编码信息的协议——我们根本不需要关心电平的绝对高低只关心高电平持续了多久、低电平持续了多久。本文完整工程代码可在 CSDN 下载频道 获取VIP 免费。二、NEC 协议拆解载波与脉宽编码在写代码之前必须先把协议吃透。很多解码 bug 的根因不在代码而在对接收头到底输出什么波形的理解偏差上。2.1 38kHz 载波物理层的噪声过滤器NEC 协议不是直接把基带信号驱动红外 LED而是用 38kHz 载波去调制。遥控器要发高电平时不是让 LED 常亮而是以 38kHz 的频率高速开关 LED也就是说逻辑高电平实际上是一段密集的 38kHz 方波脉冲逻辑低电平则完全不发。这一设计的核心价值是构建物理层的带通滤波器。环境里充斥着大量红外噪声——白炽灯的热辐射、荧光灯的 50/60Hz 闪烁、甚至人体散发的远红外。一体化接收头如 VS1838B内部集成了一颗中心频率 38kHz、带宽约 ±2kHz 的带通滤波器只有严格落在 38kHz 附近的信号才能通过其余频段的噪声被大幅衰减。这也是为什么用普通光电二极管直接接收几乎无法稳定解码而一体化接收头能隔着 3 米稳定工作的原因。VS1838B(接收端)红外链路(38kHz载波)遥控器(发射端)VS1838B(接收端)红外链路(38kHz载波)遥控器(发射端)逻辑1 38kHz载波持续560us逻辑0 38kHz载波持续560us输出脚: 有载波低电平, 无载波高电平引导码 9ms载波 4.5ms空闲接收头解调输出(反相)地址码 8bit (LSB先发)560us载波 560/1690us空闲数据码 8bit (LSB先发)560us载波 560/1690us空闲2.2 帧结构与位时间一个完整的 NEC 数据帧结构如下引导码 地址码 地址反码 数据码 数据反码 结束位 9ms4.5ms 8bit 8bit 8bit 8bit 560us引导码Leader9ms 载波 4.5ms 空闲告诉接收端数据来了地址码 / 地址反码8bit 设备标识反码用于校验数据码 / 数据反码8bit 按键值反码用于校验重复码长按键时每隔约 110ms 发一次结构是 9ms 载波 2.25ms 空闲 560us 载波不携带地址和数据。位时间用脉宽区分 0 和 1位载波调制段空闲无载波段总位时间逻辑 0560μs560μs1.12ms逻辑 1560μs1690μs2.25ms2.3 接收头输出的关键陷阱反相这是本文要重点强调的一个坑。一体化接收头VS1838B 等的输出是反相的物理状态接收头 OUT 引脚电平收到 38kHz 载波低电平无载波空闲高电平所以接收端看到的波形和发射端正好反过来引导码在接收端是9ms 低电平 4.5ms 高电平逻辑 1 是560μs 低 1690μs 高逻辑 0 是560μs 低 560μs 高。如果你按高电平载波去写判断条件会得到完全错误的结果——这个坑我下面会单独复盘。三、硬件选型与接线3.1 器件清单器件型号/参数说明主控STM32F103C8T672MHzTIM2 支持输入捕获红外接收头VS1838B38kHz一体化输出反相兼容 3.3V/5V遥控器任意 NEC 编码遥控器常见 21 键小遥控器即可电源滤波100μF 电解电容靠近接收头 VCC滤除电源纹波3.2 接线表VS1838B 引脚连接VCC3.3V或 5VVS1838B 均可GNDGNDOUTPA0TIM2_CH1接收头 OUT 引脚内部是开集电极输出通常需要上拉电阻不过多数开发板排针或接收头模块板上已经带了上拉接线时先确认一下。我习惯在 VCC 和 GND 之间并一颗 100μF 电容红外接收头对电源纹波比较敏感纹波大时会出现隔三差五丢一帧的怪现象。相关阅读《保姆级教程用 STM32 的 TIM 定时器搞定 NEC 红外遥控收发》 — 对 VS1838B 选型和基础接线讲得很细。四、CubeMX 配置与定时器参数计算4.1 时钟与定时器配置在 CubeMX 中做如下设置时钟树HSE 8MHz 外部晶振PLL 倍频到 72MHz 系统时钟APB1 定时器时钟 72MHzF103 中 TIM2/3/4 挂 APB1若 APB1 预分频 ≠1 则定时器时钟会 ×2这里 APB136MHz、定时器时钟72MHz。TIM2Clock Source 选 Internal ClockChannel1 选 Input Capture direct mode。定时器参数Prescaler 72-172MHz/(711) 1MHz即 1μs 分辨率Counter Period 0xFFFF65535最大可测 65.535msClock Division DIV1NVIC使能 TIM2 global interrupt。GPIOPA0 配置为 TIM2_CH1 复用功能无上下拉或上拉取决于硬件。4.2 分辨率与量程的权衡这里有一个值得说明的设计权衡。分辨率设成 1μs 是为了兼顾精度和量程分辨率选择计时精度最大量程ARR65535引导码 9ms 的测量0.5μs预分频 35高32.767ms9000→18000 计数1μs预分频 71够用65.535ms9000 计数2μs预分频 143略粗131ms4500 计数NEC 的引导码是 9ms逻辑 0/1 的差异在 560μs 级别1μs 分辨率下 560μs 对应 560 个计数、1690μs 对应 1690 个计数区分度有 3 倍以上完全够用。而 65ms 的量程保证了引导码 9ms 后续数据不会溢出——但注意量程 65ms 是单次捕获间隔的上限长按重复码的 110ms 周期会导致计数器回绕这个溢出问题在故障排查里专门讲。五、解码状态机实现5.1 总体思路解码用一个状态机在捕获中断里推进。核心数据结构如下/* 状态机状态定义 */typedefenum{IR_STATE_IDLE0,/* 空闲等待引导码 */IR_STATE_LEADER,/* 已捕获引导码等待数据 */IR_STATE_DATA,/* 正在接收 32 位数据 */IR_STATE_COMPLETE,/* 一帧解码完成 */}IR_State_t;/* 解码上下文 */typedefstruct{IR_State_t state;/* 当前状态 */uint8_tbitCount;/* 已接收的位数 */uint32_tdata;/* 32 位数据缓冲地址地址反码数据数据反码 */uint32_tlastCapture;/* 上一次捕获的计数点用于计算脉宽 */uint8_tready;/* 标记一帧数据已就绪供主循环读取 */}IR_Decoder_t;5.2 定时器初始化/* TIM2 输入捕获初始化1μs 分辨率捕获 CH1 上升沿 */voidIR_Timer_Init(void){TIM_ClockConfigTypeDef sClockSource{0};TIM_IC_InitTypeDef sConfigIC{0};htim2.InstanceTIM2;htim2.Init.Prescaler71;/* 72MHz/(711) 1MHz */htim2.Init.CounterModeTIM_COUNTERMODE_UP;htim2.Init.Period0xFFFF;htim2.Init.ClockDivisionTIM_CLOCKDIVISION_DIV1;htim2.Init.AutoReloadPreloadTIM_AUTORELOAD_PRELOAD_DISABLE;if(HAL_TIM_IC_Init(htim2)!HAL_OK){Error_Handler();}sClockSource.ClockSourceTIM_CLOCKSOURCE_INTERNAL;HAL_TIM_ConfigClockSource(htim2,sClockSource);sConfigIC.ICPolarityTIM_INPUTCHANNELPOLARITY_RISING;sConfigIC.ICSelectionTIM_ICSELECTION_DIRECTTI;sConfigIC.ICPrescalerTIM_ICPSC_DIV1;sConfigIC.ICFilter0x0F;/* 输入滤波滤除毛刺 */HAL_TIM_IC_ConfigChannel(htim2,sConfigIC,TIM_CHANNEL_1);HAL_TIM_IC_Start_IT(htim2,TIM_CHANNEL_1);/* 开启捕获中断 */}这里有两个容易被忽视的细节。第一ICFilter 0x0F打开了数字输入滤波它会采样输入信号 N 次确认边沿有效能滤掉红外信号里的短毛刺——这是输入捕获相比纯外部中断的一大抗干扰优势。第二ICPrescaler DIV1表示每个有效边沿都捕获不要设成 DIV2/DIV4否则会漏掉一半的边沿。5.3 捕获中断与状态机解码的关键逻辑都在捕获回调里。收到一个上升沿先算出与上一个上升沿的时间间隔再根据状态机判断这段间隔代表什么。/* 脉宽判断阈值单位 μs1μs 分辨率下的计数值 */#defineIR_LEADER_MIN8000/* 引导码 9ms 的下限 */#defineIR_LEADER_MAX10000/* 引导码 9ms 的上限 */#defineIR_BIT1_MIN1500/* 逻辑 11690μs 空闲的下限 */#defineIR_BIT0_MAX1000/* 逻辑 0560μs 空闲的上限 */voidHAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef*htim){if(htim-Instance!TIM2)return;uint32_tcaptureHAL_TIM_ReadCapturedValue(htim2,TIM_CHANNEL_1);uint32_twidth;/* 处理计数器回绕当前捕获值 上次捕获值时跨了一次溢出 */if(captureir_dec.lastCapture){widthcapture-ir_dec.lastCapture;}else{width(0x10000-ir_dec.lastCapture)capture;}ir_dec.lastCapturecapture;switch(ir_dec.state){caseIR_STATE_IDLE:/* 等待 9ms 引导码 */if(widthIR_LEADER_MINwidthIR_LEADER_MAX){ir_dec.stateIR_STATE_LEADER;ir_dec.bitCount0;ir_dec.data0;}break;caseIR_STATE_LEADER:/* 引导码后的 4.5ms 空闲结束进入数据位接收 */ir_dec.stateIR_STATE_DATA;break;caseIR_STATE_DATA:/* 按空闲时长判断当前位是 0 还是 1LSB 先发 */if(widthIR_BIT1_MIN){ir_dec.data|(1ULir_dec.bitCount);/* 逻辑 1 */}elseif(widthIR_BIT0_MAX){/* 逻辑 0data 位保持 0 */}else{/* 脉宽不在 0/1 区间判定为干扰整帧丢弃 */ir_dec.stateIR_STATE_IDLE;break;}ir_dec.bitCount;if(ir_dec.bitCount32){ir_dec.stateIR_STATE_COMPLETE;ir_dec.ready1;}break;caseIR_STATE_COMPLETE:default:ir_dec.stateIR_STATE_IDLE;break;}}这个状态机有两个设计点值得展开说为什么用空闲段而不是载波段判断 0/1因为接收头输出反相上升沿之间的间隔正好对应空闲无载波的时长。逻辑 1 的空闲是 1690μs、逻辑 0 的空闲是 560μs区分度大而如果去量载波段两个都是 560μs 附近几乎无法区分。这也是很多人一开始按量高电平宽度失败的原因。为什么在IR_STATE_DATA里加了非法脉宽直接丢弃分支这是抗干扰的最后一道防线。红外信号在传输中可能被环境光干扰产生异常脉宽如果不校验一个毛刺就会让后续所有位错位最终解出一个乱码按键值。加了区间校验后异常帧直接被丢弃主循环只会拿到要么完整正确、要么没有的结果。5.4 校验与按键值提取32 位数据接收完后需要做反码校验确认地址和数据都正确/* 解析并校验 NEC 帧返回按键值0~255校验失败返回 -1 */intIR_Parse_Command(void){if(!ir_dec.ready)return-1;ir_dec.ready0;uint8_taddr(uint8_t)(ir_dec.data0xFF);uint8_taddrInv(uint8_t)((ir_dec.data8)0xFF);uint8_tcmd(uint8_t)((ir_dec.data16)0xFF);uint8_tcmdInv(uint8_t)((ir_dec.data24)0xFF);/* 反码校验地址与地址反码、数据与数据反码必须互补 */if((addr^addrInv)!0xFF||(cmd^cmdInv)!0xFF){return-1;/* 校验失败丢弃 */}return(int)cmd;}NEC 协议的精髓就在这个反码校验上8bit 地址和 8bit 地址反码、8bit 数据和 8bit 数据反码两两按位取反后做异或结果必须等于 0xFF。这一层校验能挡住绝大多数因个别位错误导致的误码——注意它只能检测错误不能纠正错误所以遇到校验失败我们直接丢弃整帧。相关阅读《NEC 红外线协议编码与解码STM32 实现》 — 对地址反码、数据反码的校验逻辑有更详细的展开。六、测试验证理论值与实测对照6.1 脉宽测量精度用逻辑分析仪抓 VS1838B 的 OUT 引脚与解码代码读到的捕获值做对照。这里把数据手册的理论值和实测值放在一起能直观看出接收头的引入误差信号段理论值协议实测值逻辑分析仪偏差原因分析引导码载波段9000μs8950μs-0.6%遥控器晶振分频误差引导码空闲段4500μs4480μs-0.4%接收头解调响应延迟逻辑 0 空闲段560μs520μs-7.1%接收头带通滤波的边沿展宽逻辑 1 空闲段1690μs1620μs-4.1%同上这张表说明了两个问题第一逻辑 0 的实测偏差最大-7.1%因为 560μs 已经很短接收头带通滤波器的边沿展宽效应在小脉宽上更明显所以判断阈值要留足余量——我把逻辑 0 上限设到 1000μs、逻辑 1 下限设到 1500μs中间留了 500μs 的死区就是为了吸收这个偏差。第二引导码的偏差很小1%所以阈值可以收得比较紧。6.2 解码成功率在不同距离下各按 100 次按键统计解码成功率距离角度测试次数成功次数成功率备注1m正对100100100%无遮挡3m正对1009999%偶发 1 次校验失败5m正对1009292%信号衰减需对准接收头3m30° 偏角1009696%偏离中心后灵敏度下降5m 距离成功率掉到 92%主要原因是 VS1838B 的接收灵敏度随距离和角度衰减载波能量不足以稳定触发内部解调器。这是器件的物理限制不是解码代码的问题——如果项目需要 5m 以上稳定控制应该换接收头或增加发射功率而不是在软件上硬凑。6.3 CPU 占用对比用 DWT 周期计数器统计解码一帧的中断耗时对比外部中断方案方案平均中断次数/帧单帧 CPU 占用72MHz外部中断 定时器67 次15%~20%定时器输入捕获4~5 次5%中断次数的差异来自硬件捕获机制外部中断方案每个边沿都进中断而输入捕获只在捕获完成、溢出等少数事件触发中断软件只需读 CCR 值。七、故障排查两个踩坑复盘7.1 坑一接收头输出反相引导码判断永远失败现象代码写好后遥控器按键完全无反应串口没有任何解码输出。用逻辑分析仪看明明有波形。排查工具逻辑分析仪 STM32CubeMonitor 单步调试。假设一开始怀疑是定时器没配好或是 GPIO 没复用成功。排除过程先把 PA0 改成普通 GPIO 输入用外部中断读电平发现按键时电平确实在跳变说明接线和引脚没问题。接着怀疑状态机逻辑加打印输出每次捕获的脉宽值发现引导码阶段测出来的高电平是 4500μs 而不是预期的 9000μs。根因我一开始按高电平载波理解用上升沿之间即高电平段去匹配 9ms 引导码。但 VS1838B 输出反相收到载波时 OUT 是低电平所以真正的 9ms 载波段在接收端是 9ms 低电平而 4.5ms 空闲段才是高电平。我测到的 4500μs 其实是空闲段。验证把状态机改成先捕获低电平段匹配引导码或直接在下降沿/上升沿都捕获、取空闲段上升沿间隔判断解码立即恢复正常按键值稳定输出。教训写红外解码前务必先用示波器/逻辑分析仪确认接收头的实际输出波形不要想当然地假设极性。这条经验对所有调制信号类外设都适用。7.2 坑二长按重复码导致计数器回绕误判现象单次短按解码完全正常但长按键时偶尔会解出乱码或按键值跳变到别的键。排查工具串口打印 逻辑分析仪。假设怀疑是重复码9ms 2.25ms 560μs被状态机误当成新帧的引导码处理了。排除过程抓长按时的完整波形发现重复码周期是 110ms而我的定时器 ARR0xFFFF、1μs 分辨率单次捕获间隔上限是 65.535ms。110ms 65.535ms意味着计数器在两次捕获之间已经溢出回绕了一次此时capture lastCapture如果代码里直接做capture - lastCapture减法会得到一个巨大的无符号数脉宽判断完全乱掉。根因定时器计数器在 65.535ms 处溢出回绕长按重复码的 110ms 间隔超过了单次量程导致脉宽计算错误。验证在捕获回调里加入回绕处理——当capture lastCapture时用(0x10000 - lastCapture) capture计算真实间隔见 5.3 节代码修复后长按不再出现乱码。如果要进一步支持更长的无信号间隔可以开启定时器更新中断用软件累加溢出次数把有效量程扩展到任意长度。相关阅读《别再死磕外部中断了用 STM32 定时器输入捕获搞定 NEC 红外解码实测避坑指南》 — 对中断风暴和溢出处理的实测数据很值得参考。7.3 坑三接收头载波频率选错灵敏度断崖式下降现象换了另一款接收头后解码成功率从 100% 掉到 50% 以下只有凑到 10cm 内才有反应。排查检查接线和代码都没变唯一变化是接收头型号。根因红外接收头内部带通滤波器的中心频率是固定的常见的有 38kHz、36kHz、40kHz 等。如果遥控器发射的是 38kHz 载波而接收头是 36kHz 型号载波能量落在通带边缘甚至带外解调灵敏度会断崖式下降。验证换回 38kHz 的 VS1838B距离 3m 内解码恢复正常。教训采购接收头时务必确认载波频率与遥控器一致不要看到红外接收头就随手替换。7.4 坑四电源纹波导致随机丢帧现象解码偶尔会随机丢一帧不是稳定复现串口打印偶尔缺一个按键值重新按一次又正常。排查用示波器测接收头 VCC 引脚的电源发现按键瞬间有几十 mV 的纹波毛刺。根因接收头的模拟解调电路对电源纹波敏感纹波叠加在解调阈值上会造成个别边沿误判表现为随机丢帧。验证在接收头 VCC 与 GND 之间就近并联 100μF 电解电容 0.1μF 陶瓷电容后丢帧现象消失连续按 100 次无丢失。教训红外接收头是混合信号器件供电要就近加去耦电容这是低成本、高收益的一步。八、总结核心要点NEC 协议用 38kHz 载波 脉宽调制逻辑 1 和逻辑 0 靠空闲段 1690μs / 560μs 区分帧结构含引导码 32 位数据地址/地址反码/数据/数据反码。接收头输出反相是最大陷阱有载波输出低电平解码时要用空闲段上升沿间隔判断 0/1。定时器输入捕获相比外部中断把单帧中断次数从 ~67 次降到 ~4 次CPU 占用从 15%~20% 降到 5%且支持输入滤波抗毛刺。反码校验地址⊕地址反码 0xFF是保证数据可靠性的关键校验失败应整帧丢弃。适用边界与局限这套方案适用于近距离3m、单向、低速的遥控场景比如家电遥控、玩具遥控、开发板按键遥控等。它不适合需要双向通信的场景红外是单向的、5m 以上的远距离控制信号衰减明显、以及高频按键输入NEC 的 110ms 重复码周期本身就是吞吐瓶颈。扩展方向掌握了 NEC 解码后可以继续往下走用定时器输出比较 38kHz PWM 实现红外发射做成一个红外转发/学习遥控器把解码结果接入 FreeRTOS 队列做按键事件分发或者研究其它红外协议如 Sony SIRC、RC-5的差异。这些都是从能解码到能实用的自然延伸。如需获取本文完整代码和更多实战项目可开通 CSDN 技术会员。参考资料版本备注硬件平台STM32F103C8T6 VS1838B38kHz 一体化接收头 21 键 NEC 遥控器软件版本STM32CubeMX 6.x STM32F1 HAL 库 1.8.x Keil MDK 5.37兼容说明代码基于 HAL 库通用 APISTM32F1/F4 系列均可直接移植仅需调整定时器编号与引脚映射F0/F3 系列需注意定时器时钟频率差异重算 Prescaler 即可相关阅读《STM32 定时器实现红外接收与解码》 — 介绍了按位时间解码的另一种思路。