看看家里的电器遥控器,十个里面有八个是红外,电视、空调、机顶盒、风扇,全是那一枚小灯珠在前头闪。但你要是真拿示波器去量它的输出,会发现这玩意儿一点都不简单——一串宽度各异的脉冲,有的几百微秒,有的几毫秒,高低电平来回跳,背后是一套完整的通信协议。更麻烦的是,各家厂商的协议还不一样,同一个牌子的空调和电视,编码规则都能差出十万八千里。
这篇文章想聊透一个东西:ESP32 系列芯片上专门为这类脉冲信号设计的硬件外设 RMT(Remote Control)。我会从它和 GPIO 模拟、UART 这类常规方案的本质区别讲起,再把红外遥控里最主流的 NEC 协议逐 bit 拆开,最后给一套完整的实战项目——用 ESP32-C3 做一台能学习、能发射的红外遥控装置。适合刚开始接触嵌入式通信协议、想搞懂红外遥控底层原理、或者想自己做万能遥控器的朋友,照着抄就能跑通。
1. 遥控器背后的RMT外设,到底解决了什么问题
1.1 RMT的本质:一套给脉冲信号准备的高速量尺和信号发生器
RMT 这个名字容易让人误会,它跟射频、无线没有半点关系。在 ESP-IDF 的文档里,RMT 的定位是红外遥控收发外设,但说白了,它就是一组硬件计时器加电平检测器。每个 RMT 通道内部有独立的时钟计数器,输入引脚上只要发生电平跳变,硬件就会立刻把当前计数器的值记下来,存进一个 FIFO 缓冲区。发射方向反过来,硬件按照你准备好的时间表,在指定电平上停留指定的时钟周期,自动产生一串脉冲。
我打个比方。用普通 GPIO 模拟红外信号,好比一个人拿着秒表手动记跑步成绩,反应再快也有误差;RMT 则是跑道边摆了一排光电感应门,人一跑过去,时间自动就记下来了,精确到微秒级别。红外遥控信号的每一位宽度通常只有几百微秒,这个量级对硬件计时来说毫无压力,但对软件响应来说就非常紧张了。
1.2 为什么不能用GPIO翻转或者UART来模拟红外遥控
很多人第一反应是:红外遥控不就是高低电平吗?我用 GPIO 拉高拉低不就行了。理论上行,实际上非常痛苦。NEC 协议里最短的一个 bit 周期只有 1.125ms,但这不是均匀方波,而是"载波突发 + 空闲"的组合。你用 GPIO 模拟,就得在中断里精确延时,主循环一旦被其他任务抢占几百微秒,时序就偏了,接收端解码立刻失败。
我还见过有人试图用 UART 发红外——毕竟 UART 起始位、数据位、停止位也是一串高低电平。但问题是 UART 帧格式固定,每一位宽度相同,而红外协议需要的是任意宽度的脉冲组合,UART 根本表达不了"9000 微秒高电平、4500 微秒低电平"这种自定义时长。SPI、I2C、CAN 这些标准总线也一样,它们解决的是"怎么按既定格式传数据",红外需要的是"先量出来这段电平到底多宽",方向完全不同。
RMT 把这个问题彻底解决了。接收时,硬件自动记录每个电平宽度,你只需要在帧结束时读缓冲区;发送时,把符号(symbol)数组交给硬件,它自己按节奏输出。整个过程 CPU 几乎不参与,时序精确度取决于时钟源,轻松做到微秒级甚至更高精度。
1.3 RMT收发链路在工作时CPU在干什么
我用 ESP-IDF 写过完整的红外收发代码,这里说下实际硬件行为。接收状态下,RMT 通道的输入引脚配置为边沿中断,每次引脚电平跳变,硬件记录跳变方向和时间戳,存入内部的 symbol RAM。当总线空闲超过你设定的阈值,硬件触发 DMA 中断,把整段数据交给 CPU。CPU 拿到的是已经整理好的脉冲序列,而不是一条一条去数边沿。
发射状态下更省心。你把一段rmt_symbol_word_t数组交给 DMA,硬件按照每个 symbol 的duration值逐段输出,电平由level字段决定。如果启用了载波功能,高电平期间还会自动叠加 38kHz 的载波脉冲,CPU 发完指令后直接干别的去了。这也是为什么 RMT 能做到同时收发多路信号——通道之间互相独立,互不干扰。
2. NEC红外协议逐bit拆解:一帧数据是怎么排列的
2.1 红外遥控协议全家桶:NEC为什么在中国市场最常见
红外遥控协议不是一个标准,而是一堆标准。日本厂商喜欢用 NEC 和 Sony SIRC,欧洲家电大量用 Philips RC-5,空调厂商还喜欢自定义变体。但如果你拆开国内的电视、机顶盒、风扇遥控器,十有八九是 NEC 协议或者 NEC 的变种。
原因很现实:NEC 协议帧结构紧凑,一条指令只有 32 位数据加引导码,大约 67ms 就能发完一帧,配合重复码机制,长按按键的响应也很稳定。更重要的是,NEC 协议是 1980 年代 NEC 公司(现在的瑞萨)提出的,早期日本家电大量采用,国内厂商做遥控器时直接抄了这套设计,久而久之就成了事实标准。我后面实战项目里的解析代码,就是按 NEC 帧格式来写的。
2.2 NEC一帧的完整时间轴:引导码、数据位、停止位
NEC 协议一帧数据的完整结构是这样的:引导码 + 8 位地址码 + 8 位地址反码 + 8 位命令码 + 8 位命令反码 + 停止位,总共 68 个时间单位。
引导码是整帧的起始标记,固定为 9ms 高电平(载波突发)加 4.5ms 低电平(空闲)。接收端靠它来判断"一帧数据开始了"。跟在后面的 32 位数据,全部按 LSB 优先顺序发送。地址码和命令码各带一个反码,用来做校验——接收端把数据和反码异或一下,结果必须是 0xFF,否则就认为这帧数据有错误。
数据发完之后还有一个 562.5us 的高电平停止位,表示"帧到这里结束"。整个一帧从上电开始算,如果不再发下一帧,总线就回到静默状态。
2.3 用一张表看懂逻辑0与逻辑1的本质区别
NEC 的巧妙之处在于,逻辑 0 和逻辑 1 不是用高电平或低电平来表示的,而是用高电平持续时间相同、低电平持续时间不同来区分的。
| 数据位 | 高电平(载波突发) | 低电平(空闲) | 总周期 |
|---|---|---|---|
| 逻辑 0 | 562.5us | 562.5us | 1.125ms |
| 逻辑 1 | 562.5us | 1687.5us | 2.25ms |
| 引导码 | 9000us | 4500us | 13.5ms |
| 停止位 | 562.5us | — | — |
我最初学的时候绕了很久:为什么逻辑 1 不是靠"高电平更长"来区分?后来想明白了——红外接收头输出的信号是反相的,载波突发对应低电平,空闲对应高电平。如果协议用高电平宽度来编码,载波时长受信号强度影响会波动,判断容易出错。NEC 方案把高电平固定住,低电平的宽度由接收端定时器精确测量,抗干扰能力反而更好。
你写解析代码时,只需要对每个数据位测量它的低电平时间:小于 1ms 判为 0,在 1.2ms 到 2ms 之间判为 1,超过 2ms 直接判定帧异常。
2.4 重复帧和NEC延伸:容易被忽略的两个变体
如果你按住遥控器按键不放,会发现它不会每 67ms 重发一遍完整帧,而是发一个引导码后只跟一个 2.25ms 空闲,再发 562.5us 停止位就结束了。这叫重复码,也叫连续码。接收端检测到重复码时,知道"上一个按键还在被按住",于是继续执行上次的命令。
实际写代码时要特别处理这个分支——重复码没有数据段,如果按完整帧去解析,会把重复码当成异常帧丢掉,长按音量加就会一顿一顿的。
另一个变体是 NEC 延伸协议,也叫 NEC Extended。经典 NEC 的地址段是"8 位地址 + 8 位地址反码",总能用反码校验;而 NEC Extended 把这 16 位改成了 16 位地址,不再区分反码。这导致地址空间从 256 扩展到 65536。很多空调遥控器用的就是这种模式,因为空调功能多,256 个地址根本不够分。解析时不能永远期待"地址异或地址反码等于 0xFF",否则遇到延伸协议会全部判错。
3. 38kHz载波才是红外通信的隐藏主角
3.1 为什么发数据之前还要“背”一个38kHz的包
你拿手机摄像头对着遥控器按键,能看到发射管发出紫色光斑闪烁,那些闪烁其实不是数据本身,而是 38kHz 的载波在打拍子。为什么要这样?两个原因。
第一,抗干扰。家里到处是红外线:太阳光、白炽灯、节能灯都有很宽的红外成分。如果遥控器直接按 0/1 电平发射,接收端分不清哪个是信号哪个是环境光。给信号加上 38kHz 的"节奏"后,接收端只认这个频率的闪烁,其他频率一律视为噪声。
第二,降低功耗。红外 LED 的瞬时电流可以到 100mA,如果一直亮着,电池撑不了几天。用占空比 1/3 的载波驱动,相当于 LED 实际只有三分之一的时间在亮,但接收端通过带通滤波依然能清晰识别信号,平均电流大幅下降。这也是为什么遥控器两节七号电池能用一年。
3.2 一体化接收头做了什么:光电检测、带通滤波、解调
市面上常见的 VS1838B、HS0038B、TSOP38238 这些接收头,内部不是一个简单的光敏二极管,而是一个完整的光电解调模块:光敏二极管检测红外光,AGC 自动增益控制电路根据环境光强度调整灵敏度,带通滤波器只让 38kHz 附近的频率通过,最后解调输出端直接给出脉冲信号。
关键点来了:接收头输出的是集电极开路信号,无载波时输出高电平,检测到 38kHz 载波突发时输出低电平。它已经把载波剥掉了,你拿到的就是纯数据脉冲。也就说,RMT 接收引脚上见到的不是 38kHz 的方波,而是反相后的 NEC 数据帧。很多新手第一次量接收头输出时一脸懵,以为收到了噪声,其实那正是解调后的正常信号。
3.3 发射电路怎么搭:三极管开关和限流电阻的计算
红外发射不能直接用 GPIO 引脚驱动 LED。单个 GPIO 最大输出电流有限,带不动大电流红外管,而且 38kHz 方波驱动下,峰值电流不够会严重缩短发射距离。我常用的方案是一颗 NPN 三极管(S8050 就行)做开关,GPIO 控制基极,集电极接红外 LED 和限流电阻。
参数怎么算?假设 VCC 是 3.3V,红外 LED(比如 940nm 的 IR333)正向压降约 1.2V,三极管饱和压降约 0.2V。想让 LED 峰值电流到 80mA,限流电阻 R = (3.3 - 1.2 - 0.2) / 0.08 ≈ 24 欧姆,取 22 欧姆标准值。基极电阻让 GPIO 输出大约 5mA 驱动电流,R = (3.3 - 0.7) / 0.005 ≈ 520 欧姆,取 510 欧姆。注意这里用的是脉冲峰值电流,不是持续电流,所以 80mA 在 1/3 占空比下平均只有 27mA,三极管和 LED 都能扛住。
3.4 RMT的载波合成:硬件替你把PWM干了
如果用 GPIO 方案,光做一个 38kHz 的 PWM 就得占用一个定时器,还得保证和数据脉冲相位同步,代码复杂得很。RMT 在硬件层面直接集成了载波合成功能——配置好载波频率和占空比后,发射通道在高电平期间会自动输出载波脉冲,低电平期间完全静默。
以 ESP-IDF 为例,配置载波就是几行代码的事:
rmt_carrier_config_t carrier_cfg = { .frequency_hz = 38000, // 38kHz .duty_cycle = 0.3333, // 占空比 1/3 .polarity_active_low = false // 高电平时输出载波 }; rmt_apply_carrier(tx_channel, &carrier_cfg);这里有个容易理解错的地方:duty_cycle是载波本身的高电平占空比,不是整个发射周期。占空比 1/3 意味着 38kHz 方波里高电平只占约 8.8us 中的 2.9us,这样既保证接收端能稳定解调,又控制了平均功耗。polarity_active_low默认是false,含义是当 symbol 电平为高时叠加载波,低电平时不输出。这与红外 LED 驱动电路正好匹配。
4. 实战:用一块ESP32-C3做红外学习和发射装置
4.1 项目目标与硬件选型
这节我直接给出一个能抄作业的完整项目:红外学习发射器。它有两个功能:第一,对着电视机遥控器按一下,它能学习这条指令;第二,按键触发,它把学习到的指令原样发射出去,控制电视。扩展到空调、机顶盒也同理,关键在于"学习模式保存原始波形"这一设计思路。
硬件清单如下:
- 一块 ESP32-C3 开发板(ESP32-S3、ESP32 也可以,代码逻辑一样)
- VS1838B 红外一体化接收头
- 一个 940nm 红外发射 LED
- 一颗 S8050 NPN 三极管
- 22 欧姆、510 欧姆电阻各一个
- 面包板和杜邦线若干
选择 ESP32-C3 的原因很实在:它便宜、引脚够用,RMT 有 4 个发射通道和 4 个接收通道,做红外学习器绰绰有余。VS1838B 是 38kHz 中心频率,和 NEC 载波匹配度最高。收发一体的话,注意发射 LED 和接收头别面对面放,距离拉开 5cm 以上,否则发射瞬间会把自己的接收通道打爆。
4.2 接线:一共就三根信号线
| 模块 | 引脚 | ESP32-C3 GPIO |
|---|---|---|
| VS1838B VCC | 3.3V | 3V3 |
| VS1838B GND | GND | GND |
| VS1838B OUT | 信号 | GPIO5(需外部上拉 10k 到 3V3,实际多数模块板载已带) |
| S8050 基极 | 通过 510 欧姆电阻 | GPIO4 |
| S8050 集电极 | 红外 LED 负极 + 限流电阻 | — |
| S8050 发射极 | GND | GND |
| 红外 LED 正极 | 3.3V | 3V3 |
接线图我不画了,强调两个关键点。VS1838B 的输出是集电极开路,模块上已经带 10k 上拉电阻的就不用外接,裸芯片必须自己加,不然输出高电平飘忽不定。发射电路里红外 LED 放在集电极和 VCC 之间、限流电阻放在集电极和 LED 负极之间即可,千万别把 LED 正极直接接 GPIO。
4.3 RMT发送通道配置与NEC指令发射
做发射端之前,先定义 NEC 协议的符号数组结构。RMT 的rmt_symbol_word_t表示一个完整的高低电平周期:
#include "driver/rmt_tx.h" #include "driver/rmt_rx.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #define RMT_TX_GPIO 4 #define RMT_RX_GPIO 5 #define NEC_CARRIER_HZ 38000 #define RMT_RESOLUTION_HZ 1000000 // 1MHz,每个 tick 为 1us static rmt_channel_handle_t tx_channel = NULL; static rmt_channel_handle_t rx_channel = NULL; static rmt_encoder_handle_t copy_encoder = NULL;发送一条 NEC 指令,核心是把地址和命令组包成一个符号数组:
#define NEC_LEADING_CODE_DUR_0 9000 #define NEC_LEADING_CODE_DUR_1 4500 #define NEC_BIT_ZERO_DUR_0 560 #define NEC_BIT_ZERO_DUR_1 560 #define NEC_BIT_ONE_DUR_0 560 #define NEC_BIT_ONE_DUR_1 1690 #define NEC_STOP_BIT_DUR_0 560 rmt_symbol_word_t nec_build_frame(uint16_t addr, uint16_t cmd, int extended) { rmt_symbol_word_t syms[34]; int idx = 0; // 引导码 syms[idx++] = (rmt_symbol_word_t){ .duration0 = NEC_LEADING_CODE_DUR_0, .level0 = 1, .duration1 = NEC_LEADING_CODE_DUR_1, .level1 = 0 }; // 32bit 数据,LSB first uint32_t data = 0; if (extended) { data = (addr & 0xFFFF) | ((cmd & 0xFF) << 16) | ((~cmd & 0xFF) << 24); } else { data = (addr & 0xFF) | ((~addr & 0xFF) << 8) | ((cmd & 0xFF) << 16) | ((~cmd & 0xFF) << 24); } for (int i = 0; i < 32; i++) { bool bit = (data >> i) & 1; if (bit) { syms[idx++] = (rmt_symbol_word_t){ .duration0 = NEC_BIT_ONE_DUR_0, .level0 = 1, .duration1 = NEC_BIT_ONE_DUR_1, .level1 = 0 }; } else { syms[idx++] = (rmt_symbol_word_t){ .duration0 = NEC_BIT_ZERO_DUR_0, .level0 = 1, .duration1 = NEC_BIT_ZERO_DUR_1, .level1 = 0 }; } } // 停止位 syms[idx++] = (rmt_symbol_word_t){ .duration0 = NEC_STOP_BIT_DUR_0, .level0 = 1, .duration1 = 0, .level1 = 0 }; return *syms; }初始化发射通道并发送:
void rmt_tx_init(void) { rmt_tx_channel_config_t tx_cfg = { .gpio_num = RMT_TX_GPIO, .clk_src = RMT_CLK_SRC_DEFAULT, .resolution_hz = RMT_RESOLUTION_HZ, .mem_block_symbols = 128, }; rmt_new_tx_channel(&tx_cfg, &tx_channel); rmt_carrier_config_t carrier_cfg = { .frequency_hz = NEC_CARRIER_HZ, .duty_cycle = 0.3333, .polarity_active_low = false, }; rmt_apply_carrier(tx_channel, &carrier_cfg); rmt_copy_encoder_config_t enc_cfg = {}; rmt_new_copy_encoder(&enc_cfg, ©_encoder); rmt_enable(tx_channel); } void nec_send_frame(uint16_t addr, uint16_t cmd, int extended) { rmt_symbol_word_t symbols[34]; // 组装 symbols(和上面的 nec_build_frame 一致) // ... rmt_transmit_config_t tx_opt = { .loop_count = 0, }; rmt_transmit(tx_channel, copy_encoder, symbols, sizeof(symbols), &tx_opt); }注意rmt_transmit是异步的,调用后立即返回。如果需要连续发送两帧,要等上一帧发送完成,可以用rmt_tx_wait_all_done(tx_channel, portMAX_DELAY)等待。
4.4 RMT接收通道配置与NEC信号解析
接收端初始化时有两个关键参数:signal_range_min_ns和signal_range_max_ns。前者用于滤波,短于该值的毛刺会被硬件直接丢弃;后者是帧结束判定条件,总线空闲超过该时间就认为一帧结束。
#define RMT_RX_MIN_NS 1250 // 过滤掉小于 1.25us 的噪声尖峰 #define RMT_RX_MAX_NS 20000000 // 20ms 无信号视为帧结束 static rmt_symbol_word_t rx_symbols[256]; void rmt_rx_init(void) { rmt_rx_channel_config_t rx_cfg = { .gpio_num = RMT_RX_GPIO, .clk_src = RMT_CLK_SRC_DEFAULT, .resolution_hz = RMT_RESOLUTION_HZ, .mem_block_symbols = 128, }; rmt_new_rx_channel(&rx_cfg, &rx_channel); rmt_rx_event_callbacks_t cbs = { .on_recv_done = rx_done_cb, }; rmt_rx_register_event_callbacks(rx_channel, &cbs, NULL); rmt_enable(rx_channel); rmt_receive_config_t recv_cfg = { .signal_range_min_ns = RMT_RX_MIN_NS, .signal_range_max_ns = RMT_RX_MAX_NS, }; rmt_receive(rx_channel, rx_symbols, sizeof(rx_symbols), &recv_cfg); }接收完成回调里做 NEC 解析:
bool nec_parse_symbols(rmt_symbol_word_t *syms, size_t n, uint16_t *addr, uint16_t *cmd) { if (n < 35) return false; // 引导码 + 32bit + 停止位 + 结束符 // 引导码检查 if (syms[0].duration0 < 8000 || syms[0].duration0 > 10000) return false; if (syms[0].duration1 < 4000 || syms[0].duration1 > 5000) return false; uint32_t data = 0; for (int i = 0; i < 32; i++) { rmt_symbol_word_t *s = &syms[1 + i]; if (s->duration1 > 1300 && s->duration1 < 2200) { data |= (1 << i); } else if (s->duration1 > 300 && s->duration1 < 900) { // 逻辑 0 } else { return false; } } uint8_t a = data & 0xFF; uint8_t ai = (data >> 8) & 0xFF; uint8_t c = (data >> 16) & 0xFF; uint8_t ci = (data >> 24) & 0xFF; if ((a ^ ai) == 0xFF && (c ^ ci) == 0xFF) { *addr = a; *cmd = c; return true; } // 尝试 NEC Extended *addr = data & 0xFFFF; *cmd = c; return true; } bool IRAM_ATTR rx_done_cb(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data) { // 注意中断上下文,这里只拷贝数据 memcpy(rx_symbols, edata->received_symbols, edata->num_symbols * sizeof(rmt_symbol_word_t)); rx_num_symbols = edata->num_symbols; xSemaphoreGiveFromISR(rx_done_sem, NULL); return true; }主循环里收到信号量后,调用nec_parse_symbols解析。这里有个不能跳过的细节:每次接收完成,必须重新调用rmt_receive,让接收通道重新进入等待状态,否则后续信号不会触发中断。我最早写的时候漏了这一步,结果是第一帧能收到,第二帧开始就完全没反应了。
4.5 学习模式为什么保存原始波形而不是解析结果
文章开头提到空调遥控器协议复杂,这里展开讲透。如果你做的只是控制自家电视,硬编码 NEC 地址命令就够了。但要做"万能学习遥控器",就绝不能依赖解析出的地址命令——很多空调协议本身就不符合标准 NEC,解析可能失败,或者你解析出来的地址命令在发射时因为少了反码、缺少某些帧头标记而导致设备拒收。
我的做法是:学习阶段把 RMT 收到的原始 symbol 数组原封不动保存下来,发射阶段原样发送。这样不管对方是什么协议、多少位数据、什么校验规则,对学习器来说只是一段脉冲波形,照葫芦画瓢就行。
但这里有一个关键坑:VS1838B 接收头输出的是反相信号,载波突发对应低电平。如果直接把接收到的 symbol 数组交给发射通道,RMT 会认为高电平期间叠加载波,结果发射出去的是原信号的"反码",设备当然不认。解决方法是在保存前做电平反转,或者直接在接收初始化时配置flags.invert_in:
rmt_rx_channel_config_t rx_cfg = { .gpio_num = RMT_RX_GPIO, .clk_src = RMT_CLK_SRC_DEFAULT, .resolution_hz = RMT_RESOLUTION_HZ, .mem_block_symbols = 128, .flags.invert_in = true, // 反相输入,让接收到的 symbol 与发射信号同相 };这样保存下来的波形就是正逻辑:高电平表示"有载波",低电平表示"无载波"。发射时直接用 copy encoder 发出去,配合载波叠加,完整还原原始遥控器的红外输出。
5. 实测中避开的坑:从半米距离到空调拒收
5.1 症状一:RMT接收只能跑一次,第二次就没有中断
典型的"第一次能收到数据,第二次就哑火"问题。原因我先直接说:rmt_receive是一次性配置,回调返回后接收通道处于停止状态。必须在接收完成回调里再次调用rmt_receive重新武装,或者在主循环处理完数据后调用。
排查链路是这样的:先看日志里第一次接收完有没有打印什么异常;再确认回调里是不是只做了解析,没有重新调用rmt_receive;最后确认rmt_receive传入的缓冲区是否足够大,如果缓冲区被填满也会导致提前触发回调。我的习惯是把缓冲区设成 256 个rmt_symbol_word_t,NEC 最长的学习波形(包含重复帧)也才 70 个左右符号,留足余量。
5.2 症状二:距离近得感人,只有几十厘米
发射距离不足,多数不是代码问题,而是驱动电路电流不够。我见过有人直接用 GPIO 接 LED,电流只有十几毫安,距离自然上不去。把 22 欧姆限流电阻换成 10 欧姆,峰值电流可以拉到 100mA 以上,距离能翻好几倍。但注意不能无限制加大电流,超过 LED 的峰值耐受值会烧管子。
另一个影响距离的细节是载波占空比。之前我把duty_cycle配成了 0.5,结果发射距离比 0.33 短了不少。原因不是平均功率不够,而是接收头的 AGC 电路对载波占空比有要求,过高的占空比会被误判为连续光。保持 1/3 左右最稳妥。
5.3 症状三:学习后发射,电视能响应空调却不行
电视能响应说明发射链路基本没问题,空调不行就要怀疑协议变体了。我排查过一台格力的遥控器,用逻辑分析仪抓它的波形,发现引导码后面不是标准的 4.5ms 空闲,而是 5.6ms;数据位的低电平宽度也比标准 NEC 长。用我上面的nec_parse_symbols解析,虽然能勉强解析出数据,但发射时把引导码缩回标准值,空调就不认了。
解决办法就是文章前面说的学习模式。空调协议再奇葩,只要你是原样记录波形再原样发射,就不可能出错。这也是为什么我强烈建议做学习遥控器时别偷懒做协议解析,直接存波形。
5.4 症状四:日志里出现大量乱码一样的窄脉冲
这种情况多发生在白天靠窗的位置。太阳光里有丰富的红外成分,接收头会输出一些随机窄脉冲。RMT 的滤波参数signal_range_min_ns就是干这个用的——把小于该时长的毛刺直接滤掉。NEC 最短的有效脉冲大约是 560us,所以滤波阈值设在 100us 到 200us 都不会误伤有效信号。我建议设成 1250ns 卡掉所有不足 1.25us 的尖峰,效果很好。
另外一个容易被忽略的干扰源是 LED 照明。某些廉价 LED 灯的高频驱动电路会辐射出红外噪声,靠近时尤其明显。给接收头加一个滤光罩,或者把 VS1838B 的引脚剪短减少天线效应,都能改善。
5.5 给新手的排查清单
如果你按照本文的代码搭完,测试时还是有各种怪问题,按这个顺序排查能省不少时间:
- 电压是否正常?VS1838B 供电 3.3V 或 5V 都行,但输出上拉必须接同一个电源域,否则电平判断会出错。
- 示波器或逻辑分析仪接接收头输出,按遥控器按键时是否能看到脉冲?看不到就是接收头接线或型号问题。
- 发射时用手机摄像头对准 LED,能看到紫色闪烁吗?看不到说明三极管驱动没生效。
- 把你学习到的 symbol 数量打印出来,比对一下是否和原始遥控器帧长度一致。数量偏少说明
signal_range_max_ns设小了,帧被提前截断。
最后再分享一个我在实际项目里学到的教训:做这类嵌入式协议调试时,逻辑分析仪比示波器好用得多。红外信号速度不快,普通的 24MHz 采样逻辑分析仪完全够用,还能自动解码 NEC,抓一帧波形出来和代码里的 symbol 数组一对比,问题出在哪一层立刻就能定位。我桌上常备一个 8 通道的,专门对付这类"看起来是代码问题,实际上是时序问题"的疑难杂症。