尧图精选

STM32红外解码实战:用定时器输入捕获解析NEC协议

🕒 发布时间:2026/9/16 3:10:57 📁 来源:尧图网络
简介基于STM32F103的红外解码程序包面向嵌入式初学者与开发者解决红外遥控信号接收与协议解析问题。资源涵盖NEC、RC5等常见PWM编码方式的解码思路通过定时器捕获脉宽和中断状态机还原指令码适用于智能家居、消费电子等场景。压缩包共247个文件约4.12MB以C源码43个.c、头文件44个.h、汇编启动文件33个.s为主同时包含编译生成的.o/.axf/.hex固件和工程配置文件便于直接编译烧录与二次移植。已有354人学习下载适合用来理解STM32标准库外设操作、学习定时器输入捕获和中断服务程序设计。包内还附有工程备份、链接脚本、文档等可对照源码了解从底层初始化到解码状态机的完整实现并能根据注释快速修改适配不同遥控器协议是入门STM32红外应用的实用参考。1. 红外解码在解什么把『STM32(F103)红外解码.rar』解压后工程结构基本大同小异标准库、main.c、一段定时器中断printf 出来一串十六进制键值。真正拿到自己板子上烧一遍就会发现解码本身不是难点难点在几个看起来不起眼的地方GPIO 配浮空好还是上拉好定时器输入捕获测周期还是测脉宽引导码 9ms 和 4.5ms 怎么区分为什么长按遥控器时程序会以为收到了新按键。说到底红外解码不复杂它是在量一串电平宽度把 NEC 协议里 560us、1690us 这些时间量准协议自然成立。下面从 NEC 时序开始用 F103 的定时器输入捕获把这套逻辑完整落地。2. NEC 协议时序与捕获方案选型2.1 NEC 帧结构引导码、数据位与重复码NEC 协议用 38kHz 载波、约 1/3 占空比调制。一体化接收头对载波解调后输出的是反相逻辑电平发射端有载波时输出低电平载波停止时输出高电平。于是接收头输出脚上看到的一帧 NEC 数据是引导码9ms 低电平 4.5ms 高电平逻辑 1560us 低电平 1690us 高电平周期 2.25ms逻辑 0560us 低电平 560us 高电平周期 1.12ms重复码9ms 低电平 2.25ms 高电平一帧数据共 32 位发送顺序为地址码 8 位、地址反码 8 位、命令码 8 位、命令反码 8 位全部按 LSB first 发送。反码的作用是校验addr_inv 必须等于 ~addrcmd_inv 必须等于 ~cmd否则帧损坏直接丢弃。很多“解码乱跳”的工程就是少了这步校验把无线干扰收到的错误帧也当成有效按键。参数接收头输出视角典型值判定范围引导码9ms 低 4.5ms 高13.5ms12.0 ~ 15.0ms逻辑 0低 高1.12ms0.9 ~ 1.5ms逻辑 1低 高2.25ms1.8 ~ 2.8ms重复码9ms 低 2.25ms 高11.25ms10.0 ~ 12.5ms帧间隔高电平空闲≥ 40ms不处理由于接收头空闲输出高电平上升沿到上升沿的间隔恰好等于“低电平 高电平”的完整周期。上面表格里的判定范围就是围绕周期值取的这也是后面状态机判断的依据。2.2 测周期、测脉宽还是外部中断常见实现有三种。第一种是 GPIO 外部中断 EXTI 加定时器计数在边沿中断里读 CNT 算时间差。这种方式代码直观但外部中断没有硬件滤波能力38kHz 载波的残余毛刺容易造成边沿误触发必须自己写软件消抖中断里的边沿切换逻辑一多就容易漏事件。第二种是 GPIO 轮询只在主循环里读引脚按下遥控器的瞬间不一定能被扫到长按或快速连按会丢帧只适合做教学演示。推荐的做法是定时器输入捕获配置为只在上升沿捕获硬件自动把 CNT 值锁存到 CCRx每捕获一次产生一个 CC1 中断读差值即可。F103 的输入捕获通道自带数字滤波器能滤掉纳秒级毛刺省掉软件消抖逻辑。三种方案的对比很直观方案实时性抗毛刺代码量适用场景定时器输入捕获高好ICFilter 硬件滤波少F103 常规做法EXTI 定时器高一般需软件消抖较多老工程或特殊引脚GPIO 轮询低中最少教学演示后面的实现以“上升沿周期测量 TIM2 输入捕获”为准。这套方案在 F103 上不需要改硬件纯软件就能读完 NEC 整帧。2.3 16 位计数器溢出与时间基准F103 的 TIM2/3/4/5 计数器是 16 位。若直接按 72MHz 计数65535 个 tick 只有 910us连一个逻辑 0 的 1.12ms 周期都包不住必须预分频。常见的做法是 PSC 设为 71让计数频率降到 1MHz1 个 tick 对应 1us。此时 16 位计数器最大量程是 65.535ms远大于 NEC 最长 13.5ms 的引导码周期也大于 11.25ms 的重复码周期。所以两次相邻上升沿之间的差值必然小于 65536us用 uint16_t 做无符号减法得到的就是正确时间差不需要额外处理计数器回绕。这里要澄清一个时钟树细节标准库默认 APB1 预分频为 2即 PCLK1 36MHz但定时器时钟被倍频到 72MHz即便有人把 APB1 改为不分频PCLK1 72MHzTIM2 时钟仍是 72MHz。所以 PSC 71 在两种情况都成立。调工程时用逻辑分析仪核对一下 1ms 脉宽对应计数 1000就能确认时间基准没跑偏。3. 用 TIM2 输入捕获在 F103 上搭解码通道3.1 一体化接收头接线与电平关系DIY 常用的 VS1838B、HX1838B 一体化接收头3.3V 和 5V 供电都能工作。它们输出脚是开集电极结构空闲时必须被上拉到高电平。市面上的接收头模块一般自带 10k 上拉电阻直接连杜邦线就能用如果买的是裸接收头输出脚到 VCC 之间要补一个 10k 电阻。引脚标识典型符号接法OUT / S输出接 STM32 PA0GND / G地接 GNDVCC / V电源接 3.3V 或 5V供电脚 VCC 和 GND 之间建议并联 4.7uF 电解电容和 0.1uF 陶瓷电容。38kHz 载波解调瞬间会有脉冲电流电源纹波一大解码距离会缩短到一米以内。PA0 是 TIM2_CH1 的默认映射不需要重映射。在 Keil 里装好 STM32F1 芯片包、用标准库新建工程后把初始化函数放进单独的文件即可。3.2 GPIO、TIM2 时钟与捕获中断初始化标准库下面初始化分三块开启 GPIOA 和 TIM2 时钟、配置 PA0 为上拉输入、配置 TIM2_CH1 上升沿捕获。完整函数如下void IR_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim_base; TIM_ICInitTypeDef tim_ic; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); /* PA0 复用为 TIM2_CH1上拉输入空闲时读到高电平 */ gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_IPU; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); /* 定时器时钟 72MHzPSC71 - 1MHz1 tick 1us */ tim_base.TIM_Period 0xFFFF; /* 最大量程 65.535ms */ tim_base.TIM_Prescaler 72 - 1; tim_base.TIM_ClockDivision TIM_CKD_DIV1; tim_base.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tim_base); /* 上升沿捕获直接从 TI1 输入不分频 */ tim_ic.TIM_Channel TIM_Channel_1; tim_ic.TIM_ICPolarity TIM_ICPolarity_Rising; tim_ic.TIM_ICSelection TIM_ICSelection_DirectTI; tim_ic.TIM_ICPrescaler TIM_ICPSC_DIV1; tim_ic.TIM_ICFilter 0x08; /* 8 个采样周期滤波 */ TIM_ICInit(TIM2, tim_ic); /* 开捕获中断抢占优先级设为 1避免和延时函数互相挤占 */ nvic.NVIC_IRQChannel TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }逻辑说明GPIO 不需要调用 PinRemap 函数PA0 就是 TIM2_CH1 的默认引脚。PSC 为 71 时计数频率是 72MHz 除以 72得到 1MHz 计数时钟。TIM_ICFilter 0x08表示连续采样 8 个定时器时钟周期只有电平稳定才认为边沿有效用来挡掉接收头输出脚上的短暂毛刺。TIM_ITConfig在TIM_Cmd之前执行避免定时器启动瞬间产生一次空捕获中断。捕获中断里只需要做三件事读 CNT、算差值、把差值喂给状态机。不要在中断里调用任何 delay 函数也不要在中断里直接 printf。优先级是否比 SysTick 高取决于工程里 delay 的实现方式最稳妥的方法是中断里只做状态机耗时控制在几十微秒。3.3 为什么只捕获上升沿接收头空闲输出高一帧数据从引导码低电平开始所以接触到的第一个跳变沿是上升沿。每来一个上升沿计算它与上一次上升沿的间隔得到的就是完整的“低 高”周期。若选择下降沿捕获引导码之前那段 40ms 以上的空闲高电平会产生一个无意义的下降沿初始状态处理会多出不少分支。另一个选上升沿的理由是逻辑 0 和逻辑 1 的区分只看总周期长度不需要关心当前引脚是高还是低也就不需要在中断里额外读取 GPIO 电平。网上不少 .rar 工程用的是“先捕获上升沿再切换下降沿”的脉宽测量法也能解码但每次中断多几条边沿切换语句状态一多就容易漏边沿。改成周期法后中断里没有边沿切换逻辑明显更简单也更容易移植。4. 状态机解析 NEC 帧并提取键值4.1 解码状态机与判据区间解码逻辑适合用状态机实现而不是把一堆脉宽存进数组再事后分析。状态机的好处是边接收边判断收到干扰位立刻放弃当前帧不需要分配大缓冲区。三个状态分别是IR_WAIT_LEAD等待引导码识别 12 ~ 15ms 的周期IR_READ_BITS连续读取 32 个数据位IR_WAIT_LEAD一帧结束回到初始状态每个状态迁移只依赖当前测得的时间差 diff不依赖历史序列。判定区间取自第 2 章的表格引导码 12 ~ 15ms逻辑 1 为 1.8 ~ 2.8ms逻辑 0 为 0.9 ~ 1.5ms重复码为 10 ~ 12.5ms。区间之间留出 1.5 ~ 1.8ms 的空档这样遥控器晶振偏差和接收头响应误差都不会导致临界抖动。4.2 中断处理与 32 位数据拼装捕获中断里用无符号减法算时间差这是最不容易出错的写法void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); uint16_t now TIM_GetCounter(TIM2); uint16_t diff (uint16_t)(now - ir_last_tick); /* 无符号回绕减法 */ ir_last_tick now; ir_decode(diff); } }now - ir_last_tick是 uint16_t 减法在底层天然按 65536 取模只要两次捕获间隔小于 65.5ms结果就是准确微秒数。NEC 最长一档周期是 13.5ms这个条件在 F103 上总是成立所以不需要处理定时器溢出中断。状态机函数如下volatile uint8_t ir_state; /* 0等待引导码 1读取数据位 */ volatile uint32_t ir_code; volatile uint8_t ir_bit_cnt; volatile uint8_t ir_frame_ready; volatile uint8_t ir_key; volatile uint8_t ir_rep; /* 重复码标志 */ static void ir_decode(uint16_t diff) { if (ir_state 0) { if (diff 12000 diff 15000) { ir_state 1; ir_bit_cnt 0; ir_code 0; } return; } /* 读取数据位 */ if (diff 1800 diff 2800) { ir_code (ir_code 1) | 1; /* 逻辑 12.25ms */ ir_bit_cnt; } else if (diff 900 diff 1500) { ir_code (ir_code 1); /* 逻辑 01.12ms */ ir_bit_cnt; } else if (diff 10000 diff 12500) { ir_rep 1; /* 重复码键值不变 */ ir_state 0; return; } else { ir_state 0; /* 干扰放弃本帧 */ return; } if (ir_bit_cnt 32) { /* NEC 发送顺序地址、地址反码、命令、命令反码 */ uint8_t addr ir_code 0xFF; uint8_t addr_inv (ir_code 8) 0xFF; uint8_t cmd (ir_code 16) 0xFF; uint8_t cmd_inv (ir_code 24) 0xFF; if (addr_inv (uint8_t)~addr cmd_inv (uint8_t)~cmd) { ir_key cmd; /* 校验通过才更新键值 */ ir_frame_ready 1; } ir_state 0; } }参数说明整个状态机没有数组、没有 malloc全部是全局变量中断里直接读写主循环只消费ir_frame_ready和ir_key。校验放在 32 位全部收齐之后反码对不上就静默丢弃。ir_rep用于区分长按主循环里看到ir_rep后执行连击逻辑但不要更新ir_key。4.3 位序、反码校验与按键映射NEC 是 LSB first 发送上面的代码用左移累加第一个收到的位进入 bit0后续位依次左移最后ir_code的字节分布正好是地址码、地址反码、命令码、命令反码不需要做字节反转。很多“键值对不上”的问题就是用了“先右移再写最高位”的写法把位序搞反了。如果从别的工程抄来的代码解出的键值和遥控器按键对不上先检查它的位拼装顺序。反码校验通过后ir_key就是命令码。多数 NEC 遥控器地址固定为 0x00但也有用 0x80、0xFF 甚至自定义地址的遥控器调试时把addr一起打印不要只盯着键值。主循环里用查表把命令码映射成按键名const char *ir_key_name(uint8_t key) { static const struct { uint8_t code; const char *name; } map[] { {0x45, CH-}, {0x46, CH}, {0x47, CH}, {0x44, PREV},{0x40, NEXT}, {0x43, PLAY}, {0x07, VOL-}, {0x15, VOL}, {0x09, EQ}, {0x16, 0}, {0x19, 100}, {0x0D, 200}, {0x0C, 1}, {0x18, 2}, {0x5E, 3}, }; for (int i 0; i (int)(sizeof(map) / sizeof(map[0])); i) { if (map[i].code key) return map[i].name; } return ?; }主循环消费数据的写法if (ir_frame_ready) { ir_frame_ready 0; printf(key: %s (0x%02X)\r\n, ir_key_name(ir_key), ir_key); }打印放在主循环而不是中断里既避免阻塞中断也方便后续接 OLED 或串口屏。按键响应逻辑可以做成 switch 分支也可以维护一个函数指针数组按ir_key分发到不同业务函数。5. 波形核对、抗干扰与串口调参技巧5.1 先用逻辑分析仪确认接收头波形解码不对先别动代码把逻辑分析仪接到接收头 OUT 引脚按住遥控器按键。健康波形应该是9ms 低电平 4.5ms 高电平后面跟着 32 段宽度在 1.1ms 和 2.2ms 两档跳变的脉冲然后是一段至少 40ms 的高电平。如果完全没有波形检查接收头供电和上拉电阻如果只有持续低电平多半是接收头型号不匹配或电源纹波过大。5.2 载波毛刺、时钟偏移与中断卡死TIM_ICFilter设为 0 时遥控器离得很近的情况下强信号可能在上升沿附近产生振铃一次真实边沿会被当成两次捕获diff值变成一个异常小的数。把ICFilter调到 0x08 或 0x0A 能滤掉这类毛刺。另一个典型的故障是系统时钟配置错误比如 HSE_VALUE 与实际晶振不符所有时间测量整体偏移逻辑 1 周期可能测成 1700us正好落在判定空档里导致整帧丢弃。如果程序在按键后进了 HardFault优先查中断里有没有调用不可重入的库函数以及有没有数组越界写入。解码状态的全局变量都应在初始化时清零特别是刚上电时ir_last_tick为 0第一次捕获算出的diff可能很大状态机要能容忍这个值直接把它当作噪声处理。5.3 打印脉宽序列快速调整阈值调一个陌生遥控器时与其盲猜阈值不如把捕获到的脉宽序列缓存起来收满一帧后一次性通过串口打印人工核对。在中断里加一个轻量缓存volatile uint16_t ir_diff_buf[40]; volatile uint8_t ir_diff_cnt; volatile uint8_t ir_diff_ready; /* 在 ir_decode(diff) 之前缓存 */ if (ir_diff_cnt 40) ir_diff_buf[ir_diff_cnt] diff; /* 收到完整帧后在 ir_decode 里置 ir_diff_ready 1 */主循环里打印if (ir_diff_ready) { for (uint8_t i 0; i ir_diff_cnt; i) printf(%d , ir_diff_buf[i]); printf(\r\n); ir_diff_ready 0; ir_diff_cnt 0; }打印出来的序列第一个值应在 13500 附近表示引导码周期后面 32 个值只应该出现 1100 和 2250 两档。如果出现第三档数值对照状态机的判定区间调整即可。这个缓存打印的思路不只适用于 NEC换 RC5 或 Sony SIRC 协议时只要改协议参数表解码框架可以原样复用。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →