资讯动态

STM32红外解码实战:NEC协议、电平极性和定时器时间戳全解析

发布时间:2026/9/1 0:19:26 来源:尧图企业网站定制
简介基于STM32的红外代码程序是一套可直接运行的红外解码实验源码包主要面向嵌入式初学者、电子竞赛备赛者及需快速实现红外遥控功能的开发者帮助理解红外信号时序解析、协议格式及按键映射流程。压缩包共156个文件、约7.84MB以C/H源码为主体含64个C文件与63个H文件同时包含Keil工程文件、可直接烧录的hex固件、脚本文件及PDF/文本说明目录按功能模块划分便于在STM32标准库工程中直接打开、编译与二次开发。资源附带LCD驱动、触摸控制与USMART调试组件可在开发板上直观查看解码结果另有批量清理脚本与文档资料方便工程维护与原理学习。目前已有1921人学习下载适合对照代码进行红外协议学习、功能移植或作为课程设计、毕业设计的基础框架。 前阵子有朋友在群里发问题STM32红外接收头输出已经有波形变化了但程序就是解不出正确键值。这问题我当年也卡了整整一个周末后来发现根本不是代码逻辑问题而是没搞懂NEC协议里接收头的电平极性和定时器的时间戳测量方式。这篇文章就基于我自己在STM32上做红外解码、编码项目的实践经验从协议参数、硬件接线、代码实现到踩坑记录一次性写清楚希望能让准备做红外遥控、智能家居控制或者红外学习项目的朋友少走点弯路。1. 认识NEC协议先弄清接收头输出的是什么在动手写STM32的代码之前得先把红外协议本身说清楚。我见过很多人的第一步就错了把红外接收头当成普通光电传感器以为有光的时候引脚就拉高没有光就拉低。实际上NEC协议用的载波是38kHz接收头内部集成了解调电路它输出的TTL信号已经和原始载波无关了只代表“有没有收到38kHz的调制脉冲”。NEC的一个完整帧包括9ms的引导码载波 4.5ms的空闲 地址码8位 地址反码8位 命令码8位 命令反码8位。这里的“载波”指的是38kHz的高频方波而接收头输出后会反相有载波时输出低电平空闲时输出高电平。这个反相关系一开始很容易漏掉导致代码里判断高低电平全部反着来。很多例程的引脚定义看着都差不多其实方向完全看接收头型号。逻辑0和逻辑1怎么区分NEC用低脉冲宽度相同、高电平间隔不同的方式每个数据位都会先有一个560us的低电平脉冲随后如果是逻辑0跟着另一个560us的高电平如果是逻辑1就跟着1690us的高电平。也就是说两个数据位的边界都从同一个560us低脉冲开始真正决定0还是1的是后面的“间隔”长度。理解了这一点解码程序就可以设计成先测低脉冲长度再测后面高电平长度。我把常用脉宽列成表方便对照调参。接收头输出的电平持续时间含义低电平约9000us引导码 / 重复码起始高电平约4500us引导码后的停顿即将开始数据位低电平约560us每个数据位的固定起始脉冲高电平约560us逻辑0高电平约1690us逻辑1高电平约2250us重复码后的停顿需要提醒的是这组数据是理论值实际遥控器的晶振偏差会导致时间差有正负10%左右的变化所以比较代码里要留出判断窗口例如判断逻辑1时可以设为大于1200us且小于2500us千万不要用“大于某个值就判1”这种一刀切的方式。后面第4节我会给出具体的窗口写法。2. 硬件注意点和开发环境准备讲完协议接下来是硬件。最常见的接收头是HS0038B和VS1838B它们的引脚定义都是OUT/GND/VCC但如果你买的是散装件最好自己对着数据手册确认一遍。选接收头还有一个小讲究有的型号适合5V供电有的3.3V即可。STM32一般使用3.3V供电如果你强行给接收头5V输出高电平就可能超过GPIO的耐压。虽然多数STM32引脚是容忍5V的但不是所有引脚都能容忍尤其是直接输入到定时器输入捕获通道时还是乖乖按3.3V来比较稳。接收头的输出脚一般是集电极开路结构模块上通常会带上拉电阻。如果你买的是裸接收头一定要在OUT和VCC之间接一个10k左右的上拉电阻或者在STM32内部把GPIO配置为上拉输入。我早期在面包板上试板子时漏了这部分输出波形处于悬浮状态抓出来的数据完全是乱的。软件环境我多说一句。经常有人在Keil MDK里新建工程后编译报错提示找不到stm32f1xx.h这多半是没装设备支持包。STM32CubeMX、Keil MDK、ST-Link Utility这三样配合使用是当前比较顺手的组合先用CubeMX选好芯片并生成初始化代码再用Keil打开编译最后用ST-Link Utility烧录。如果你在STM32F103系列上开发记得从Pack Installer里安装STM32F1xx系列的器件包。这个步骤和我早年用标准外设库的年代完全不同新手卡在环境上的时间往往比写代码还多值得先梳理好。在CubeMX里配置解码引脚时我建议把对应GPIO配置为外部中断模式EXTI同时触发沿选择“上升沿下降沿”这样在中断回调里就能同时拿到高电平和低电平时长。接下来再开一个自由运行的定时器作为微秒时间戳。这里有个最容易踩的隐藏点如果你的板子默认用SWD下载调试就不要把PA13/PA14这些SWD引脚拿去接红外模块哪怕强行复用也会把调试器搞掉线。还有PA15/PB3/PB4这几个JTAG引脚虽然可以做普通GPIO但默认情况下不是需要先关闭JTAG复用改完配置后如果调试器连不上一般是这个原因。解决办法是用ST-Link Utility的“connect under reset”模式重新连上再写个恢复程序。3. 解码方案选型为什么我用“定时器时间戳”而不是延时轮询有关STM32红外解码的例程一搜一大把代码风格大致分两类一类在while循环里反复读引脚电平并用Delay_us累积测量另一类用定时器输入捕获。第一类代码最容易看懂但问题很明显你在解码的时候主循环被占死了如果按键信息要同时去驱动OLED、控制电机就会互相卡顿。更麻烦的是如果在读电平的过程中来了一个串口中断延时时间就不准了解出的键值偶尔会错。我的固定做法是定时器只负责产生固定频率的时基通过GPIO外部中断在边沿到来时记录当前CNT值然后两个边沿的CNT相减就是脉冲的持续时间。有些读者可能会觉得外部中断不是也会被其他更高优先级的中断打断吗实际上我们在中断里只是读取一个寄存器并计算差值操作时间极短微秒级的误差对这个协议来说可以接受。相比之下用延时函数去等误差是累积的并且在一次帧期间可能多次被高优先级中断打断。具体配置上我用TIM2作为32位计数器PSC设为71让计数周期等于1us这样TIM2-CNT直接就是微秒级时间戳连换算都不用。之所以优先选32位定时器是因为16位定时器在1MHz计数频率下约65.5ms就会溢出一次而NEC的一次完整帧加上重复码间隔可能超过100ms如果不做溢出补偿时间差就会变成负值或极小的值。虽然可以用溢出中断扩展位数但完全没必要为了省一个定时器给自己增加代码复杂度。如果你手头只剩TIM3这样的16位定时器那就务必记住在中断里检查计数器的更新事件每溢出一次给高位加1最后把时间拼出来。GPIO边沿中断的回调函数怎么写我下面给一段核心示意。需要注意我把“当前引脚电平”作为判断依据进入中断时先读一次引脚就能知道刚才是上升沿还是下降沿然后用当前CNT减去上一次记录的CNT得到上一段电平的持续时间。这个“上一段电平长度”拿到手后就送去状态机判断是引导码、逻辑0还是逻辑1。/* TIM2: 72MHz/72 1MHz, CNT单位us */ volatile uint32_t last_ticks; volatile uint32_t last_high_intv; volatile uint32_t last_low_intv; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin ! IR_IN_Pin) return; uint32_t now TIM2-CNT; uint32_t delta now - last_ticks; last_ticks now; if (HAL_GPIO_ReadPin(IR_IN_GPIO_Port, IR_IN_Pin) GPIO_PIN_RESET) { /* 下降沿刚结束的是高电平高电平长度delta */ last_high_intv delta; ir_on_falling_edge(); } else { /* 上升沿刚结束的是低电平低电平长度delta */ last_low_intv delta; ir_on_rising_edge(); } }优先级方面建议把EXTI中断优先级设为比普通外设中断更高避免读取CNT时被其他事情拖住。如果解出来偶尔错一位多半是供电纹波或接收头输出上拉的路径上有干扰可以给接收头供电脚加一个0.1uF和10uF的电容组合。这里我并不否认输入捕获方案的合理性定时器输入捕获通道本身可以在边沿到来时由硬件锁存CNT不占用CPU响应时间这对一些高速场景更合适。但在NEC这种毫秒级协议上用EXTI加时间戳的方式已经足够稳定而且代码好调。真正的瓶颈往往在状态机逻辑而不在获取时间戳的方式。4. 解码状态机的完整拆解从边沿到键值现在进入重点中的重点如何根据一组脉冲时间还原出某个按键的32位数据。我在第3节代码里已经把last_low_intv和last_high_intv分别拿到手了接下来分两种情况处理。先看上升沿上升沿代表低电平刚结束如果这个低电平大概9ms基本可以确定是引导码如果大概是2.25ms就是重复码如果是560us左右那就是某个数据位的起始低脉冲但此时还不知道这个位是0还是1要等下一个下降沿拿高电平时长。看下降沿下降沿代表高电平刚结束如果此时已经进入数据接收状态那么这个高电平长度如果接近560us就是逻辑0接近1690us就是逻辑14.5ms一般是引导码后面的帧起始间隔出现在第一个数据位之前。为了不让状态机乱掉我把它拆成三个状态IDLE、WAIT_GAP、RECV_BITS。IDLE表示等待引导码检测到9ms低电平后进入WAIT_GAP等待下一个4.5ms高电平等到了就进入RECV_BITS开始累积32位数据。在RECV_BITS状态里每一个560us左右的数据位低脉冲到来时我先把当前的位计数位置准备好然后在下降沿去判断高电平时长给key_code左移填充0或1。积累满32位后做一次反码校验通过后置位frame_ready。#define T_HEADER_LOW_MIN 8000 #define T_HEADER_LOW_MAX 11000 #define T_GAP_HIGH_MIN 3500 #define T_GAP_HIGH_MAX 5600 #define T_BIT1_HIGH_MIN 1200 #define T_BIT1_HIGH_MAX 2500 #define T_BIT0_HIGH_MIN 300 #define T_BIT0_HIGH_MAX 900 enum { IR_IDLE, IR_WAIT_GAP, IR_RECV_BITS } ir_state; volatile uint32_t key_code; volatile uint8_t bit_cnt; volatile uint8_t frame_ready; volatile uint8_t repeat_flag; void ir_on_rising_edge(void) { if ((last_low_intv T_HEADER_LOW_MIN) (last_low_intv T_HEADER_LOW_MAX)) { /* 9ms低电平引导码 */ ir_state IR_WAIT_GAP; bit_cnt 0; key_code 0; repeat_flag 0; } else if ((last_low_intv 1500) (last_low_intv 3200)) { /* 2.25ms低电平重复码 */ repeat_flag 1; } } void ir_on_falling_edge(void) { if (ir_state p a hrefhttps://download.csdn.net/download/wangshuo624668529/9699525 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价