☰
基于STM32的学习型万能红外遥控器设计与实现
2026/10/28 1:12:56 网站建设 项目流程

简介:基于STM32微控制器搭建的学习型红外遥控器源码包,面向嵌入式爱好者、STM32入门开发者以及需要自制万能遥控器的工程师,重点解决多台红外设备遥控器繁杂、难以统一管理的问题。项目实现了红外接收解码、学习存储与模拟发送的完整流程,矩阵键盘数字键可保存并触发10组控制码,适合客厅设备整合、课程设计或创客DIY。压缩包共122个文件,整体约33.45MB,以C/H源码和Keil工程文件(uvprojx/uvoptx)为主,同时包含PDF设计文档、原理图、实物图、烧录hex文件以及接线说明,结构清晰便于对照学习;源码中还涉及红外协议编解码、矩阵键盘扫描、OLED显示驱动、串口调试等模块,可作为入门练手项目逐段研读。已有946人学习下载,配套项目设计文档与功能演示视频入口能帮助降低门槛。拿到资料后可参照文档采购硬件、完成接线并编译烧录,快速复刻出带OLED显示、支持学习/发送模式切换的成品遥控器。

1. 万能红外遥控器为什么需要“学习型”:一个 38kHz 载波背后的黑匣子

电视、机顶盒、空调、风扇、投影仪,每家每户至少有四五台带红外遥控的设备,遥控器一多,找不着、按错、电池漏液是常事。有人把遥控器全收进抽屉,有人下单“万能遥控器”,结果发现万能遥控器只内置了品牌码库,老空调、杂牌机顶盒根本匹配不上——这就是非学习型方案的边界。基于 STM32 设计的万能红外遥控器(学习型)源码包,解决的问题就是这最后一步:把任意红外遥控器的按键码“学”进设备里,之后按一个键就能替代原装遥控器。它不识别协议、不依赖码库,只要遥控器能发,它就能学;对接收头采样到的一串高低电平时间序列做存储和重放,本质是一个“波形黑匣子”。适合正在做毕业设计、嵌入式入门,或者想给家里做一台实用红外网关的开发者。

2. 先立住原理:红外遥控的载波、数据帧与“学习”到底在学什么

2.1 38kHz 载波与接收头输出:为什么接收头输出的是反相波形

红外遥控不是直接把数据比特变成电平发出去,而是先把数据调制到载波上。家用电器最常用的载波频率是 38kHz,也有 36kHz、40kHz、56kHz 的,但 38kHz 覆盖了绝大多数电视、机顶盒、空调和风扇。载波的用途是让接收端能区分环境光和遥控信号——接收头前端有滤光片和 AGC 电路,只对 38kHz 附近的脉冲串敏感,阳光、白炽灯里的红外成分大多是连续的,会被自动增益电路当成直流背景滤掉。

STM32 学习红外遥控,第一步不是用 ADC 去采载波,而是用一个一体化接收头(TSOP4838、VS1838B 这类)把载波信号解调成基带电平。注意这类接收头输出是反相的:空闲时输出高电平,检测到 38kHz 载波时输出被拉低。也就是说,“有载波”对应低电平,“无载波”对应高电平。这块如果没先想清楚,后面学习的脉宽序列全是反的,重放出去家电根本不认。

所以学习型遥控器采样到的原始波形,是遥控器发出的载波包络:一串高高低低的电平,高电平宽度代表静默时间,低电平宽度代表载波持续时间。把这个电平序列记下来,就等价于把整个遥控信号记下来了。接收头的输出直接接 STM32 的定时器输入捕获引脚,用定时器捕获每个跳变沿的时间,两个相邻沿的时间差就是一个脉宽。

2.2 学习型与协议解耦:用时间序列替代 NEC/RC5 解码

做过红外解码的人都知道 NEC 协议:引导码 9ms 载波加 4.5ms 静默,数据位用 560us 载波加 560us 或 1690us 静默区分 0 和 1。问题是家电不止 NEC 一种,还有 SONY 的 12/15/20 位可变长度协议、飞利浦的 RC5、空调用的各种私有长码。如果做一个依赖协议识别的万能遥控器,每种协议都要单独写解码器,遇到冷门协议直接抓瞎。

学习型的思路是从根上绕开协议识别。既不解码,也不判断这是 NEC 还是 RC5,只做一件事:把接收头输出的每个高电平宽度、低电平宽度按顺序存成时间戳数组。比如 NEC 的引导码,在学习型设备里就是“低电平 9000us,高电平 4500us”两个时间戳;逻辑 1 就是“低电平 560us,高电平 1690us”。至于这段序列在协议层面叫什么,不重要,重放时原样输出即可。

时间戳数组的容量按最坏情况算。以 512 个 uint16_t 元素为例,每个脉宽最大能表示 65535us,一帧常见的 NEC 按键码大约产生 64~68 个跳变沿,512 的上限足够覆盖绝大多数遥控帧。空调的私有协议偶有超过 200 个沿的长帧,也装得下。采样时用定时器输入捕获产生中断,每次捕获记录当前计数值与前一次计数值的差值。相邻两次计数差就是脉宽,单位看预分频配置,一般做到 1us 分辨率就够。

2.3 发射路径:PWM 载波与码流的“点乘”

学习记录的是基带电平序列,重放时要把这段序列重新“搬运”到 38kHz 载波上发出去。发射路径的本质是载波和码流相乘:码流是低电平(对应有载波)时输出 38kHz 脉冲串,码流是高电平(对应静默)时输出保持低电平。

最朴素的做法是用 GPIO 翻转产生 38kHz:进入低电平区间后,在中断里定时翻转引脚。但这样做的载波频率完全依赖中断的实时性,任何更高优先级的中断插入都会让载波抖动,发射距离和成功率都会明显下降。更稳的方案是硬件 PWM 产生 38kHz,另外用一个定时器控制 PWM 的输出使能,STM32 的 TIM 主输出使能位(MOE)正好能当这个闸门。两个定时器配合,一个管“什么时候响”,一个管“响多久”,载波波形完全由硬件产生,软件只管按时间戳切换 MOE 的开关。

如果想让波形更干净,还可以用定时器级联:主定时器输出比较通道产生脉宽窗口,通过 TRGO 触发从定时器的 PWM 使能。但这种做法调试门槛高,我一般先用 MOE 开关方案跑通,再视情况优化。38kHz 载波的占空比推荐 1/3:红外发射管峰值电流可以给大,平均电流又不至于过热,发射距离比 1/2 占空比明显更远。

3. 硬件搭建与最小系统:接收头、发射管和 STM32 的引脚分配

3.1 器件选型:TSOP4838、940nm 发射管与 S8050 驱动电路

我一般选 TSOP4838 或 VS1838B 做接收头,两者引脚兼容,都是三脚封装:OUT、GND、VS。TSOP4838 的带宽窄一点,抗干扰更好;VS1838B 便宜,用在室内环境没问题。发射管选 940nm 的,直径 5mm 的直插型号,市面上绝大多数遥控器用的就是 940nm 波长,部分老设备可能用 850nm,但 940nm 管也能覆盖大部分场景。

STM32 的 GPIO 推挽输出能力有限,直接驱动红外发射管,峰值电流只有十几毫安,发射距离很难超过 1 米。正确做法是加一级三极管驱动。常见电路是:发射管阳极接 3.3V,阴极串一个 20~30Ω 限流电阻接到 S8050 集电极,S8050 发射极接地,基极串 1kΩ 电阻接 STM32 的 PWM 输出脚。这样 PWM 高电平时三极管导通,发射管流过脉冲电流。限流电阻的取值要在峰值电流和平均功耗之间取平衡:38kHz 占空比 1/3 时,峰值电流 300mA 对应的平均电流只有 100mA,S8050 的 Icmax 是 500mA,留有余量。

供电方面,接收头的 VS 脚要并联一个 100nF 陶瓷电容和一个 10~47uF 电解电容,滤掉 3.3V 电源上的纹波。红外接收对电源纹波敏感,纹波大会让接收头内部 AGC 误判,学习到的脉宽序列里多出很多毛刺。这批电容不能省,是血泪经验。

3.2 引脚分配与定时器资源:TIM1 做发射载波、TIM2 做输入捕获

STM32F103 系列有 TIM1 高级定时器和 TIM2~TIM4 通用定时器,资源上完全够用。我常用的分配如下,如果你的板子引脚冲突,按相同功能换引脚即可:

功能引脚定时器/外设说明
红外接收输入PB0TIM3_CH3输入捕获,采样脉宽序列
红外发射输出PA8TIM1_CH1PWM 输出 38kHz,由 MOE 控制开关
OLED I2CPB8/PB9I2C1显示学习状态和按键索引
学习按键PA0GPIO 外部中断进入学习模式
发射按键/确认键PA1GPIO触发重放
功能键/翻页键PA2GPIO切换设备类型

TIM1 挂在 APB2 总线上,F103 在 APB2 预分频为 2 时定时器时钟是 72MHz;TIM3 挂在 APB1 总线上,预分频为 2 时定时器时钟也是 72MHz。上电后要先用 CubeMX 的时钟树确认这两个定时器时钟确实是 72MHz,否则后面的 38kHz 和 1us 时基全偏。很多人直接拿 CubeMX 默认配置生成工程,APB1 和 APB2 的预分频不对,TIM3 的时钟变成 36MHz,输入捕获出来的脉宽整体翻倍,这是最常见的“看着逻辑没问题但就是学不对”的坑。

3.3 从 CubeMX 到 Keil:定时器输入捕获初始化与中断回调

在 CubeMX 里把 TIM3_CH3 配成 Input Capture 模式,时钟源选内部时钟,然后在生成的 tim.c 里补参数。下面这段是经过实际调过的配置,注释写在关键位置:

// tim.c 中 TIM3 初始化,重点是预分频和滤波参数 static void MX_TIM3_Init_FOR_IR(void) { TIM_IC_InitTypeDef sConfigIC = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 72 - 1; // APB1 定时器时钟 72MHz,分频后计数频率 1MHz,1 个 tick = 1us htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 0xFFFF; // 16 位自动重载,最大计数 65535us htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; HAL_TIM_IC_Init(&htim3); sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; // 先捕获上升沿 sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 直接映射:TI1 -> IC1 sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 捕获不分频,每个沿都触发 sConfigIC.ICFilter = 0x0F; // 采样滤波,滤掉 <1us 的毛刺 HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_3); HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_3); // 使能捕获中断 }

预分频 72-1 是本段代码的核心:72MHz 除以 72 得到 1MHz 计数频率,计数器每 1us 加一。这样捕获到的数值差直接就是脉宽的微秒数,写代码时不用再做单位换算。ICFilter 设为 0x0F,内部采样时钟会对输入信号连续采样 8 次,8 次电平一致才确认边沿有效,能滤掉接收头输出上的窄毛刺,但也会把小于 1us 的真实脉宽过滤掉——红外遥控协议里没有这么短的脉宽,所以这个取舍是安全的。

中断回调里要做两件事:记录相邻沿的时间差、翻转捕获边沿极性。接收头的信号是高低电平交替,只捕获上升沿只能拿到半个周期,必须在下一次中断前把捕获极性翻到下降沿,下一次再翻回来,这样才能拿到完整的高低电平宽度序列。

// stm32f1xx_it.c 或回调文件中,输入捕获中断处理 #define IR_BUF_MAX 512 volatile uint16_t ir_timestamp[IR_BUF_MAX]; volatile uint16_t ir_timestamp_cnt = 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint16_t last_edge_time = 0; if (htim->Instance == TIM3) { uint16_t now = __HAL_TIM_GET_COUNTER(&htim3); uint16_t diff = now - last_edge_time; // 无符号减法,计数器回绕时结果依然正确 last_edge_time = now; if (ir_timestamp_cnt < IR_BUF_MAX) { ir_timestamp[ir_timestamp_cnt++] = diff; } TIM3->CCER ^= TIM_CCER_CC3P; // 翻转捕获边沿:上升沿 <-> 下降沿 } }

diff 用 uint16_t 无符号减法,利用 C 语言的回绕特性自动处理 65535us 溢出,不用额外判断。TIM3->CCER 的 CC3P 位控制通道 3 的捕获极性,异或翻转就是切换沿方向。这段代码写在中断里,执行时间必须短,不能在这里做 OLED 刷新、Flash 写入这类耗时操作,否则会漏掉后续的捕获边沿。

4. 核心软件设计与状态机:从“记录一段脉冲”到“按一下就能重放”

4.1 学习状态机:待机、学习等待、记录中、校验

学习型遥控器的软件核心是一个状态机。我习惯分成四个状态:空闲态、学习等待态、记录态、校验态。空闲态下设备按普通遥控器使用;按一下学习键进入学习等待态,此时 OLED 提示“请对准原装遥控器并按下一个按键”;接收到第一跳变沿后进入记录态,持续接收脉宽直到判定帧结束;帧结束后进入校验态,把刚学的码与上一次学习结果做对比,通过则存入码库。

判定帧结束的规则要仔细定。红外遥控的码流里,引导码、数据位、结束位之间会有静默间隔,NEC 的逻辑 1 静默 1690us,引导码后的静默最长 4500us,帧与帧之间的静默则在 10ms 以上。所以取 20ms 作为判定阈值比较稳妥:超过 20ms 没有新的边沿,就认为这一帧已经结束。但空调的某些协议在长帧中间有较大间隔,如果碰到学不全的情况,可以把阈值放宽到 50ms,代价是长按遥控器时容易把重复帧也录进来。

学习主函数的流程用代码表达如下:

// ir_learn.c,学习一帧红外码型,返回记录的脉宽数量 uint16_t IR_LearnFrame(void) { uint32_t last_event_tick; uint32_t now; ir_timestamp_cnt = 0; // 清空时间戳缓冲区 last_event_tick = HAL_GetTick(); // 打开输入捕获,开始接收 HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_3); while (1) { now = HAL_GetTick(); if (ir_timestamp_cnt > 0) { last_event_tick = now; // 只要有新脉宽进来,就刷新“最后活动时间” } if ((ir_timestamp_cnt > 0) && (now - last_event_tick > 20)) { break; // 连续 20ms 无新沿,判定帧结束 } if (ir_timestamp_cnt >= IR_BUF_MAX) { break; // 缓冲区满了,强制结束,避免死循环 } } HAL_TIM_IC_Stop_IT(&htim3, TIM_CHANNEL_3); return ir_timestamp_cnt; }

状态机里“20ms 无新沿”这个判断必须在主循环里做,不能在中断里做——中断里做延时判断会阻塞后续边沿捕获。主循环轮询的方式牺牲了一点实时性,但换来的是实现简单和不会漏沿。学到一帧后,进入校验;校验通过存入码库并提示成功,失败则提示用户重新短按一次。

4.2 码库存储:为什么用 W25Q64 而不是内部 Flash

码库存储有两条路:STM32 内部 Flash 和外部 SPI Flash。内部 Flash 的优点是省一颗芯片、电路简单;缺点是擦除按页(F103 是 1KB/2KB),擦除寿命标称 1 万次左右,而且擦除期间 CPU 要等。遥控器码库免不了反复学习、覆盖、测试,按一页存一帧算,每学一次就要擦一次,1 万次寿命看似够用,但做开发调试时一天擦几百次很正常,几个月后就可能把某页擦穿了。

外部 SPI Flash 用 W25Q64 的话,扇区擦除是 4KB,寿命标称 10 万次,余量大了十倍不止。我一般把码库按“索引区 + 数据区”组织,索引区每条目录 32 字节,数据区每条固定 2KB(脉宽数组 + 帧头信息)。学习新码时先往数据区末尾追加写入,写完再更新索引区对应的目录项。这样每次学习只擦一个扇区,而且不会反复擦同一块。

存储帧的格式设计如下:

// ir_store.h,码库帧结构 typedef struct { uint8_t device_type; // 设备类型:0=电视 1=机顶盒 2=空调 3=风扇 uint8_t key_code; // 按键索引:0=电源 1=音量+ 2=音量- ... uint16_t carrier_khz; // 载波频率,默认 38 uint16_t pulse_cnt; // 脉宽数量,最大 512 uint32_t timestamp[512]; // 脉宽序列,单位 us uint16_t crc16; // 校验和,用于读取时快速判断数据是否损坏 } IR_Code_Frame_t;

载波频率这个字段值得多说一句。虽然绝大多数设备用 38kHz,但少数进口设备用 36kHz 或 40kHz。学习时没办法直接从接收头输出判断载波频率(接收头已经把载波吃掉了),所以这个字段默认按 38 存。如果遇到学完但发射无效的设备,可以在设置菜单里手动调载波频率,逐档试出能用的值——这是万能遥控器“万能”的最后一公里,不做这个字段,遇到非标准载波的设备就只能干瞪眼。

4.3 重放定时器:用 MOE 主输出使能控制 PWM 窗口

发射一帧码型的核心是遍历脉宽数组,按时间戳开关 PWM 输出。注意此时 TIM1 已经配置成 38kHz 的 PWM 输出模式,但 PWM 引脚的电平由 MOE 位控制:MOE 置 1 时 PWM 正常输出,MOE 清 0 时输出被强制拉到无效电平。这样软件要做的只是定时开 MOE、定时关 MOE,38kHz 的载波抖动被硬件吸收,软件再慢也不影响载波质量。

// ir_send.c,重放一帧学习到的码型 void IR_SendFrame(IR_Code_Frame_t *frame) { uint16_t *p = frame->timestamp; uint16_t cnt = frame->pulse_cnt; __HAL_TIM_MOE_DISABLE(&htim1); // 先关闭输出,确保起始状态干净 for (uint16_t i = 0; i < cnt; i++) { if (i % 2 == 0) { __HAL_TIM_MOE_ENABLE(&htim1); // 低电平段:输出 38kHz 载波 } else { __HAL_TIM_MOE_DISABLE(&htim1); // 高电平段:静默 } IR_DelayUs(p[i]); // 等待该脉宽结束 } __HAL_TIM_MOE_DISABLE(&htim1); // 帧结束,关闭输出 }

这段代码的时序精度取决于 IR_DelayUs。用 SysTick 的 HAL_Delay 只能精确到 1ms,完全不可用;要用 DWT 的 CYCCNT 寄存器做微秒延时,或者用另一个定时器做忙等。最省事的实现是 DWT:CYCCNT 是 32 位计数器,每个内核时钟加一,72MHz 下 1us 计 72 个数。重放期间要临时屏蔽会被高频触发的中断(比如按键扫描的定时器中断),避免中断插入导致脉宽被拉长。NEC 家电解码器的容差一般在 ±30% 左右,但宁可比解码器更严格,我一般在重放时把中断优先级全部调低,只保留 DWT 延时自身。

载波频率的计算再确认一遍:TIM1 时钟 72MHz,设 ARR=1894、PSC=0、CCR1=631,得到 72000000 / (1894+1) = 37994Hz,约 38kHz,占空比 631/1895 ≈ 1/3。这个频率误差 0.015%,远在接收头带宽范围内。

4.4 UI 与交互:一个 OLED 和三颗按键就够了

学习型遥控器的使用场景是茶几和床头柜,交互不需要复杂。一块 0.96 寸 I2C OLED(SSD1306)显示当前设备类型、按键列表和学习状态,三颗物理按键:学习键、发射键、翻页键。翻页键循环切换设备类型(电视/机顶盒/空调/风扇),选中设备后按发射键重放该设备上次存储的按键码,长按学习键进入学习模式。

OLED 刷新放在主循环里做,不要在中断里刷。SSD1306 的 I2C 一次全屏刷新要几十毫秒,放在中断里直接毁掉输入捕获的时间精度。学习过程中用 OLED 显示“REC...”动效即可,不需要实时波形显示。真要看波形,应该用逻辑分析仪去抓,这是后面第 6 章验证部分的内容。

5. 万能红外遥控器最容易翻车的 5 个地方:现象、原因、解决

5.1 接收头输出反相,重放完全没反应

现象:学习时屏幕显示脉宽数量正常,重放时按了没反应,用手机摄像头看发射管能看到暗红色光,但家电就是不响应。

原因:一体化接收头输出低电平表示“检测到载波”。如果直接按接收头电平记录,序列里高低电平的语义与真实发射波形相反。发射时按“低电平段开启载波”的逻辑工作,等于把静默和载波对调了,家电当然不认。

解决:记录脉宽时不要管语义,只按时间戳数组的先后顺序存。重放时约定偶数索引对应载波段(低电平入),奇数索引对应静默段(高电平入),即 i%2==0 开 MOE。这样学习端和发射端的极性一致,天然抵消了接收头的反相特性。这是最简单稳妥的做法,不要在存储时尝试“纠正”极性,纠正反而容易出错。

5.2 学习时“长按”录到 NEC 重复帧

现象:学习一个按键,录出来的脉宽数量超过 200 个,重放时设备像被“连击”了一样,按一下电源,设备开机又立刻关机。

原因:NEC 协议支持连续码,按键按住不放时,每 110ms 重复发送一次按键帧。如果学习时手指按得稍久,就录到了两帧以上。NEC 一帧是 64~68 个沿,两帧就是 130 多个沿,重放时家电也会按两帧去解析。

解决:学习帧结束的判断用帧间静默时间,NEC 重复帧的帧间距约 110ms,明显大于单帧内部最长的 4.5ms 静默。把“判定帧结束”的阈值设在 20~50ms 之间,就能在一帧结束后立刻截断,不把重复帧录进来。同时 UI 上提示用户“请短按 0.5 秒以内”,双保险。如果录到一半发现脉宽数量异常偏大,直接丢弃并要求重学。

5.3 PWM 载波在中断里翻转,频率漂到 30kHz 以下

现象:近距离对着设备(半米内)能控制,距离超过两米就失效。用示波器或逻辑分析仪看发射引脚,载波频率在 30~40kHz 之间抖动,波形周期肉眼可见不均匀。

原因:用 GPIO 翻转产生 38kHz 的方案里,翻转动作依赖定时器中断。中断响应有延迟,延迟又被其他中断(比如 OLED 刷新、按键消抖)干扰,载波周期的抖动累积到一定程度,接收端 AGC 失锁。

解决:载波必须用硬件 PWM 产生,软件只控制 MOE 的开关。MOE 是定时器硬件层面的输出闸门,开和关都是寄存器操作,一个周期也不差。重放期间把 OLED、按键扫描等中断优先级调到最低,或者干脆在帧发送期间暂时关闭这些中断。经验是:判断一个方案是不是“看起来能用、实际坑人”,就看载波是否由硬件产生,这点必须较真。

5.4 发射距离不到 1 米,贴近设备才能触发

现象:学习正常,重放时遥控器要凑到家电前面 10cm 才有反应,隔一个沙发就不行。

原因:最常见的是 GPIO 直接驱动发射管,驱动能力不足;其次是限流电阻选大了。GPIO 推挽输出一般只能提供 10~20mA 电流,940nm 发射管在这个电流下辐射功率很低。

解决:按第 3 章的电路加 S8050 三极管驱动,把峰值电流拉到 100~300mA。限流电阻 20~30Ω,配合占空比 1/3 的 38kHz 载波,平均电流约在 1/3 峰值,S8050 不会过热。另一个容易忽视的点是发射管的安装方向:5mm 直插发射管顶部有一个圆形透镜,透镜的方向就是主辐射方向,PCB 上如果朝上安装,遥控器就要对准天花板发——把发射管弯脚朝向设备方向,或者用热缩管固定后斜向上安装,距离立刻翻倍。

5.5 输入捕获被环境光干扰,脉宽序列多出一堆毛刺

现象:对着同一个遥控器按键学三次,三次的脉宽数量都不一样,多的时候差 20 多个沿。用逻辑分析仪看接收头输出,波形边缘有大量窄脉冲毛刺。

原因:节能灯、LED 灯、阳光直射都会产生红外干扰。接收头的 AGC 能抑制连续红外,但高频闪烁光源(比如 100Hz 频闪的镇流器)会产生与遥控信号混叠的脉冲。另一个常见来源是 3.3V 电源纹波,纹波大时接收头内部比较器反复翻转,输出端就会有窄毛刺。

解决:接头上加 100nF + 10uF 去耦电容,必要时在电源输入端串一颗 10Ω 电阻组成 RC 滤波。软件上把 TIM3 的 ICFilter 调到 0x0F,这个参数让捕获逻辑连续采样 8 次确认边沿,毛刺宽度不足 1us 的会被直接忽略。学习时避开阳光直射,让遥控器斜对着接收头。如果毛刺还消不掉,可以在校验阶段做“两次学习一致性对比”,脉宽数量不一致或逐项偏差超过 30% 就提示重新学习——这一条是最后的后悔药。

6. 进阶与验证:用逻辑分析仪校准脉宽、把学习型遥控器扩展成红外网关

验证学习型遥控器的唯一可靠手段是逻辑分析仪,不要只靠“家电能不能响应”去判断。用逻辑分析仪的两个通道同时抓接收头输入和发射管驱动输出:第一帧是学习时的原始波形,第二帧是重放时的输出波形。把两段波形放到同一个时间轴上,逐项核对每个高低电平的宽度误差。接收头内部有 AGC 和滤波电容,学习到的脉宽本身有 ±30us 的抖动,所以重放误差在 ±100us 以内,家电基本都能正确解码;但如果某个固定位置的脉宽系统性偏移超过 200us,就要回头查延时函数的精度和中断屏蔽是否彻底。

校准流程通常是:先抓引导码的 9ms 低电平和 4.5ms 高电平,这两个值最容易看;再抓数据位重复出现的最短脉宽 560us,确认没有丢沿。测量点要选在发射管驱动 MOSFET/三极管的集电极,而不是 PWM 定时器的引脚,因为三极管开关速度也会影响脉宽的边缘陡峭度。如果集电极波形上升沿超过 5us,换更快的三极管,或者把基极电阻从 1kΩ 降到 470Ω。

码库管理这块,我习惯在串口上开一个调试协议:用AT+LEARN触发学习,学完通过AT+TIMES把脉宽序列打印到串口,PC 端用 Python 脚本解析。这样不依赖 OLED 也能快速批量学习几十个按键,还能把码库导出成 JSON 备份——换板子、刷固件都不丢码库,比反复抄写 OLED 上的编号实用得多。更进一步,给 STM32 加一颗 ESP8266 或 ESP32 做串口透传,学习型遥控器就变成一台红外网关,手机通过巴法云这类物联网平台下发指令,网关负责把 JSON 里的脉宽序列重放出去。做网关时要特别注意:网络串口数据到达的时机不可控,重放前要先把完整帧拷贝到 RAM 缓冲区,再从缓冲区逐条发送,避免串口中断在帧发送中途插入导致时序破坏。

做完这个项目以后,我养成了一个习惯:任何红外方案上手,先抓一段接收头波形存档,再写一行printf("载波频率=%d\n", freq)打出来。第一次做的时候跳过这步,全链路看起来都是通的,结果载波频率差了 2kHz,折腾了两天才定位到是 CubeMX 时钟树配置的 APB 预分频问题。从那以后,验证的事我从不跳过。希望这些踩坑记录能帮你少走这段弯路,也希望这台学习型遥控器做出来后,能让你家里的旧电器再战几年。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询