拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ESP32 RMT外设详解:从NEC协议到红外学习发射器实战

看看家里的电器遥控器十个里面有八个是红外电视、空调、机顶盒、风扇全是那一枚小灯珠在前头闪。但你要是真拿示波器去量它的输出会发现这玩意儿一点都不简单——一串宽度各异的脉冲有的几百微秒有的几毫秒高低电平来回跳背后是一套完整的通信协议。更麻烦的是各家厂商的协议还不一样同一个牌子的空调和电视编码规则都能差出十万八千里。这篇文章想聊透一个东西ESP32 系列芯片上专门为这类脉冲信号设计的硬件外设 RMTRemote 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 不是用高电平或低电平来表示的而是用高电平持续时间相同、低电平持续时间不同来区分的。数据位高电平载波突发低电平空闲总周期逻辑 0562.5us562.5us1.125ms逻辑 1562.5us1687.5us2.25ms引导码9000us4500us13.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 GPIOVS1838B VCC3.3V3V3VS1838B GNDGNDGNDVS1838B OUT信号GPIO5需外部上拉 10k 到 3V3实际多数模块板载已带S8050 基极通过 510 欧姆电阻GPIO4S8050 集电极红外 LED 负极 限流电阻—S8050 发射极GNDGND红外 LED 正极3.3V3V3接线图我不画了强调两个关键点。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, copy_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_inrmt_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_tNEC 最长的学习波形包含重复帧也才 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 通道的专门对付这类看起来是代码问题实际上是时序问题的疑难杂症。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门