ESP32 实战教程:用 RMT 学习并重放红外遥控器信号
本文记录一个很适合边玩边学的项目:让 ESP32 先“偷听”遥控器,再把听到的信号保存下来,最后通过网页把它重新发出去。它不是魔法,也不是把“开空调”四个字变成红外线,而是认真记录一串高低电平持续时间,然后尽可能原样重播。

一、为什么红外遥控值得单独做一个项目
家里的电视、风扇、机顶盒、灯带,大多都还在使用红外遥控。红外的好处是便宜、低功耗、互相不需要配网;缺点也很明显:必须对着设备,沙发缝里找不到遥控器时,科技感会迅速退化成“谁看见我的遥控器了”。
ESP32 很适合做红外中继器。一方面,RMT(Remote Control Transceiver)外设可以以稳定的时间分辨率采集和输出脉冲;另一方面,ESP32 自带 Wi-Fi,学习一次后,就可以做一个简易网页,手机点一下按钮即可发送“开机、音量加、风速低”等命令。这个项目同时练习了 GPIO、时序、载波、存储、WebServer 和边界检查,知识密度很高。
这里先建立一个关键认识:遥控器发出的不是“数字 0 和 1”这么简单,而是一段带有载波的脉冲序列。接收头通常已经帮我们完成了 38 kHz 载波解调,ESP32 收到的是一串高低电平及其持续时间。发送端则需要重新打开 38 kHz 载波,并在正确的时间让红外 LED 闪烁。
二、硬件准备与接线思路
建议准备:
- ESP32 DevKitC 或同类开发板一块;
- VS1838B、HX1838 等 38 kHz 红外接收头;
- 940 nm 红外发光二极管一只;
- NPN 三极管(S8050、2N2222 均可)、1 kΩ 基极电阻、100 Ω 左右 LED 限流电阻;
- 面包板和杜邦线。
接收头的 OUT 接 ESP32 的一个普通 GPIO,VCC 接 3.3 V,GND 共地。发射部分不要把大功率红外 LED 直接挂在 GPIO 上,GPIO 负责驱动三极管,LED 电流由三极管承担。这样既能提高距离,也不会让 GPIO 变成“电子蚊香”。
红外接收头的引脚顺序并不统一,买到模块后先看丝印和数据手册。接反电源通常不会有戏剧性的爆炸,但会让排查过程变成一场漫长的猜谜。建议先用手机摄像头观察红外 LED 是否在发送,再接收实际设备信号。
三、RMT 的两个重要概念
Arduino-ESP32 当前文档中的 RMT API 以 rmt_data_t 表示一个符号,每个符号包含两段电平和持续时间。我们可以把持续时间理解成“高电平多少个 tick,低电平多少个 tick”。如果把 RMT 初始化为 1 MHz,那么 1 tick 就约等于 1 微秒,阅读 NEC、Sony 等协议会非常直观。
第二个概念是载波。接收阶段通常使用接收头的解调输出,不需要自己产生 38 kHz;发送阶段则要用 rmtSetCarrier() 打开载波,例如 38.5 kHz、约 33% 占空比。载波频率写错时,示波器看起来可能很热闹,但家电就是不理你。
下面是一个精简的接收框架。它故意没有把任意遥控协议硬编码成 NEC,因为“学习型”项目的价值就在于先保存原始时序,后面再根据设备逐步做协议识别。
#include <Arduino.h>
#include "rmt.h"
const int IR_RX_PIN = 27;
rmt_data_t symbols[128];
void setup() {
Serial.begin(115200);
// 1 MHz:一个 tick 约等于 1 us,便于观察原始时序
if (!rmtInit(IR_RX_PIN, RMT_RX_MODE, RMT_MEM_NUM_BLOCKS_2, 1000000)) {
Serial.println("RMT RX init failed");
while (true) delay(1000);
}
rmtSetRxMinThreshold(IR_RX_PIN, 80); // 过滤过短的毛刺
Serial.println("Press a remote key...");
}
void loop() {
size_t count = RMT_SYMBOLS_OF(symbols);
if (!rmtRead(IR_RX_PIN, symbols, &count, 5000)) return;
Serial.printf("received %u symbols\\n", (unsigned)count);
for (size_t i = 0; i < count; ++i) {
Serial.printf("[%u,%u] [%u,%u]\\n",
symbols[i].duration0, symbols[i].level0,
symbols[i].duration1, symbols[i].level1);
}
}
Arduino Core 3.x 的 RMT API 与早期示例会有差异,尤其是 rmtSetCarrier() 参数从 tick 语义变成了载波频率和占空比。复制网上旧代码前,先确认自己的核心版本和头文件;否则最容易出现“能编译但发不出去”或者结构体字段对不上。
四、学习一颗按键:不要急着存垃圾数据
第一次收到数据时,建议把它分成三步处理。第一步去掉开头和结尾特别短的毛刺;第二步记录总符号数和最大持续时间,防止异常数据撑爆数组;第三步给这颗按键一个人类能看懂的名字,例如 tv_power,再写入 LittleFS 或 NVS。
学习流程如下图所示:

可以采用这样的数据结构:
struct IrKey {
char name[16];
uint16_t count;
uint16_t duration[256];
uint8_t level[256];
};
真正落盘时,不建议每按一次就马上写几十次 NVS。更稳妥的方式是先在 RAM 中学习,网页确认“保存”后再写入文件;保存前还要做长度检查。对连续学习的用户操作,可以把同一遥控器的按键存成一个 JSON 或二进制文件,启动时一次性读入索引。
五、重放时的 38 kHz 载波
发送前先用 rmtInit() 初始化 TX,再设置载波:
const int IR_TX_PIN = 25;
bool setupIrTx() {
if (!rmtInit(IR_TX_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_2, 1000000)) {
return false;
}
// 38.5 kHz、约 33% 占空比,适合大多数家电接收头
return rmtSetCarrier(IR_TX_PIN, true, true, 38500, 0.33);
}
void sendPowerKey(rmt_data_t* data, size_t count) {
// 最后一段结束后保持低电平,避免 LED 持续发光
rmtSetEOT(IR_TX_PIN, 0);
rmtWrite(IR_TX_PIN, data, count, RMT_WAIT_FOR_EVER);
}
原始时序的电平极性需要根据接收头输出确认。有些模块空闲时为高,收到红外时拉低;直接把接收数据照搬到发射端可能会反相。遇到“串口看着一模一样但设备不响应”,优先检查极性、载波、发光管方向和发射电流,而不是先怀疑宇宙规律。
六、加一个手机可用的网页
网页端不需要复杂框架,返回一个按钮列表即可。ESP32 连接家中 Wi-Fi 后,访问它的 IP,按钮点击时请求 /ir/tv_power。服务端只允许访问白名单按键,不把用户传入的任意路径直接拼到文件系统,这样做既安全也方便排错。
WebServer server(80);
void handleKey(const char* key) {
if (strcmp(key, "tv_power") == 0) {
sendPowerKey(powerSymbols, powerCount);
server.send(200, "text/plain; charset=utf-8", "已发送电视电源键");
return;
}
server.send(404, "text/plain; charset=utf-8", "未知按键");
}
正式使用时建议增加登录口令,至少不要把带有“开门、开锁”含义的红外设备暴露给访客网络。红外本身没有加密,能看到设备的人就有机会重放信号;它适合控制电视和灯,不适合拿来替代门禁的安全认证。
七、常见问题排查
1. 能学到数据,但发射无效
检查是否启用了 38 kHz 载波、占空比是否合理;确认发射 LED 是红外型号而不是普通红色 LED;用手机摄像头看 LED 是否闪烁;最后再检查接收数据是否被反相。
2. 每次学习得到的长度都不一样
这很常见。按键按下时间、接收头 AGC、环境光都会影响首尾间隔。不要直接比较整串数据是否逐字节相等,可以允许 10% 左右的时间误差,并对头码和重复码分别处理。
3. 空调遥控器比电视复杂
电视常见的是“按一次发送一个命令”,空调遥控器经常发送完整状态帧:温度、模式、风速都在一帧里。学习“开机”后重放可能只改变部分状态,甚至因为温度不同而表现不同。空调项目更适合先识别协议,再自己构造状态帧。
八、这次实验真正学到了什么
这个项目表面是做一个遥控器,实际上练习了嵌入式工程里很重要的三个习惯:先测量再猜协议;先限制边界再写入存储;把硬件差异明确写在配置里。RMT 只是工具,真正可迁移的能力是“把现实世界的一段时序数据,可靠地变成程序能保存、验证和重放的结构”。
下一步可以继续加:自动识别 NEC/RC5 协议、学习结果导出为 JSON、网页里显示原始波形、为不同房间建立按键分组,以及在发送前加入冷却时间,避免用户连续点击导致家电收到一串“我到底按了几次”的命令。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号