ATtiny13红外遥控接收器:8脚单片机解码NEC协议完整工程 简介基于ATtiny13微控制器设计的一款红外遥控接收器项目资料面向嵌入式初学者、电子DIY爱好者及单片机课程设计学生解决如何通过红外遥控器无线控制电脑开机/关机等场景。资料共14个文件压缩包约816KB包含C语言源代码、Makefile构建脚本、电路原理图、项目PDF说明文档以及编译生成的hex固件、elf调试文件、map地址映射和lss汇编列表覆盖从源码到烧录验证全流程。目前已有三百三十八人学习下载。项目完整覆盖了ATtiny13的引脚配置、红外编码协议如NEC解码流程、定时器中断处理、串口通信等关键知识模块配合原理图可快速搭建硬件电路源码层次分明适合结合PDF指南逐行理解红外信号从接收到解码的实现细节也可作为基于单片机实现红外遥控开关的模块化参考设计助你掌握完整的嵌入式软硬件协作方法。1. ATtiny13 红外遥控接收器:8 脚单片机解 NEC 协议,一份能直接抄的工程这份 ATtiny13 红外遥控接收器资源,核心只有一件事:用一个 8 引脚 AVR 单片机把 38kHz 红外遥控器的 NEC 协议完整解出来,再拿解码结果去驱动继电器、点灯或者做任何你需要的开关逻辑。很多人第一反应是:ATtiny13 才 4KB Flash、64 字节 SRAM,能跑红外解码?实际上解码状态机、按键映射、输出控制整套固件压到 2KB 以内毫无压力,还能给业务逻辑留出空间。这个资源包适合三类人:一是手头正好有 ATtiny13 库存、想省掉 ESP 或 STM32 的低成本方案党;二是刚接触 AVR 中断系统、想抄一套能用的中断状态机模板的入门者;三是被 51 单片机外部中断的麻烦搞烦了、想看看更干净的中断处理流程的工程师。下文按硬件选型、协议解码、资源优化、常见问题这条线展开,所有代码都基于 ATtiny13 的数据手册特性来写。2. 硬件搭建与选型:接收头、引脚分配、最小电路2.1 接收头选型:TSOP38238 和 VS1838B 差在哪红外遥控的载波频率不是统一的。NEC 协议标准是 38kHz,但也有不少遥控器用 36kHz 或 40kHz,接收头的选型必须跟发射端载波匹配。选错频率最直观的结果就是接收距离骤降——同一个遥控器,配 38kHz 接收头能 8 米触发,配 36kHz 的可能缩到 1 米内,甚至完全不响应。所以选型第一步不是看价格,而是确认你手里遥控器的载波频率。没有示波器的话,一个笨办法是拿手机摄像头对着接收头发射管看:按下按键时,屏幕上能看到红外发射管亮起的频率条纹,38kHz 和 40kHz 的条纹密度有明显差异。TSOP38238 是 Vishay 的经典 38kHz 接收头,工作电压 2.5~5.5V,静态电流典型值在 0.45mA 左右,输出为推挽式、空闲拉高、收到载波时拉低。它的内部集成了 AGC、带通滤波器和整形电路,输出波形已经很接近理想的数字信号,直接接 MCU 引脚就行。VS1838B 是国产兼容型号,引脚定义和工作原理基本一致,价格能做到几毛钱一颗,但有个明显的差异:VS1838B 对电源噪声更敏感,尤其在 3.3V 供电时输出边沿会变缓,偶尔出现低电平拉不到 0V 的情况。我在 5V 系统里用 VS1838B 居多,3.3V 系统里更倾向 TSOP38238。接收头引脚排列在不同品牌之间是有区别的。TSOP38238 正面看从左到右是 OUT、GND、VCC,而 VS1838B 常见排列是 OUT、GND、VCC,但同样叫 VS1838B 的某些批次会把电源和地反过来。所以拿到新的接收头,第一件事是翻数据手册确认引脚顺序,不要靠颜色或者直觉去接线,这个错误我至少见人翻车过三次。接收头输出端不需要外接上拉电阻,它的推挽输出足够驱动 ATtiny13 的输入引脚,反倒是乱加上拉会略微抬高低电平阈值,造成解码不稳定。供电上去耦电容不能省。红外接收头虽然静态电流很小,但它在收到载波脉冲的瞬间会有明显的电流跳变,如果 ATtiny13 和接收头共用一根细长的 USB 供电线,这个跳变会直接串到电源纹波里,导致解码阈值判断抖动。常见做法是在接收头 VCC 和 GND 之间放一个 100nF 陶瓷电容,靠近接收头引脚;如果发现距离不稳定,再在电源入口并联一个 10μF~47μF 的电解电容做储能。这个细节在 5V 供电时还能忍,换到 3.3V 稳压输出时影响会被放大,因为 LDO 的动态响应普遍不如开关电源。2.2 最小电路与引脚分配:PB0 是唯一合理入口ATtiny13 有 6 个可用 I/O,PB0 到 PB5,其中只有 PB0 带硬件外部中断 INT0。红外解码对时序极其敏感,用轮询去读 PB0 不是不行,但主循环任何一条耗时指令都会导致位宽测量误差,所以 PB0 作为接收头输出入口是唯一合理的选择。剩余的引脚做输出和配置:ATtiny13 引脚复用功能在接收器中的用途PB0 (引脚5)INT0/PCINT0红外接收头 OUT 输入PB1 (引脚6)PCINT1状态 LED,解码成功翻转PB2 (引脚7)T0/PCINT2可接拨码开关,控制运行模式PB3 (引脚2)ADC3/PCINT3预留,可做模拟量采集PB4 (引脚3)ADC2/PCINT4预留,或做第二路输出PB5 (引脚1)RESET/ADC0默认做复位,不建议复用为 I/OPB5 要单独说。它的复用引脚是 RST,默认功能是复位,如果把熔丝位里的 RSTDISBL 置位,才能把它当普通 I/O 用。但 ATtiny13 一旦禁用复位引脚,就只能靠高压串行编程器才能把熔丝刷回来,普通 USBasp 和 Arduino ISP 都没办法。这个资源里的工程没有用 PB5 做 I/O,保持默认复位功能,省得烧录时给自己挖坑。PB2 我留给了模式选择:接一个拨码开关到 GND,高电平走学习模式,低电平走直接映射模式,这样同一份固件不用重新编译就能适配两种遥控器。整个最小系统只需要四样东西:ATtiny13 一片、红外接收头一个、LED 加 470Ω 限流电阻、以及给接收头的一颗 100nF 去耦电容。供电我用 5V 稳压,ATtiny13 内部 9.6MHz 时钟在 5V 下跑得很稳。烧录接口用标准 6 针 ISP:VCC、GND、MISO(MOSI 在 ATtiny13 上复用为 PB3,实际上 ISP 用的是 PB4/MISO、PB3/MOSI,具体看编程器定义)、SCK、PB5/RST。接线顺序是:接收头 OUT 接 ATtiny13 的 PB0接收头 GND 接公共地,接收头 VCC 接 5VLED 正极串 470Ω 电阻接 PB1,负极接地100nF 电容跨接在接收头 VCC 和 GND 之间,尽量靠近引脚这种接法直接支持后续代码里的中断触发方式。需要强调一点:接收头输出空闲是高,收到载波时是低,所以 PB0 的默认外部中断配置应该是上升沿触发——也就是从低回高的那个时刻,正好对应每一位载波结束的位置。理解了这一点,后面看代码里的时间间隔计算就不会懵。3. NEC 协议解码实现:Timer0 脉冲测量与中断状态机3.1 NEC 协议帧结构:引导码、地址码、反码校验NEC 协议的一帧由引导码、16 位地址、8 位命令、8 位命令反码组成,总共 32 位数据。引导码是 9ms 的载波脉冲加 4.5ms 的空闲,这组长度远超后续任何一位的脉宽,所以解码器可以靠它来同步帧头。地址和命令都按 LSB 在前的方式逐位发送,反码的作用是接收端校验,防止把噪声当成有效按键。单个数据位的定义很简单:先是一个 560μs 的载波脉冲,接着是空闲间隔。如果间隔是 560μs,这一位是逻辑 0;如果间隔是 1.69ms,这一位是逻辑 1。从接收头的输出角度看,载波对应低电平、空闲对应高电平,所以每个数据位从上升沿到下一个上升沿的完整周期是 1.125ms(0)或者 2.25ms(1)。这就是中断状态机能区分 0 和 1 的物理基础。手里遥控器如果一直按住某个键,发射端不会重复发完整帧,而是每 108ms 发一次重复码。重复码由 9ms 载波加 2.25ms 空闲加一个 560μs 的载波脉冲组成,没有后续数据位。解码器如果不处理重复码,按住按键时主循环看到的是一次帧、一次重复码、一次帧这样的混乱序列,轻则重复触发,重则把重复码当成错误帧丢弃。后面避坑章节会具体说怎么处理。3.2 中断状态机:一个 ISR 把 32 位帧收完ATtiny13 的 Timer0 是 8 位计数器,直接拿 TCNT0 来测 2.25ms 的位周期会溢出,所以需要配合溢出中断组成一个 16 位时间基准。时钟配置为内部 9.6MHz,Timer0 预分频 64,计数器频率是 150kHz,每个计数对应约 6.67μs。下面这个代码是资源包的核心文件,我把它精简成 80 行以内的可复现版本:/* * ATtiny13 红外遥控接收器 — NEC 协议解码 * * 硬件连接: * PB0 — VS1838B / TSOP38238 OUT * PB1 — 状态 LED,解码成功后翻转 * * 时钟: 内部 9.6MHz,CKDIV8 熔丝必须清零 * Timer0: 64 预分频,溢出中断扩展为 16 位时间戳 */ #include avr/io.h #include avr/interrupt.h #define IR_PIN PB0 #define LED_PIN PB1 #define ST_WAIT_LEAD 0 // 等待引导码 #define ST_MID_LEAD 1 // 已捕获引导码上升沿 #define ST_DATA 2 // 数据位接收中 volatile uint8_t decode_state ST_WAIT_LEAD; volatile uint8_t timer_ovf 0; volatile uint16_t prev_edge 0; volatile uint8_t ir_bytes[4]; volatile uint8_t ir_bit_cnt 0; volatile uint8_t ir_ready 0; /* Timer0 溢出中断: 每 256 个计数累加一次 */ ISR(TIM0_OVF_vect) { timer_ovf; } /* PB0 上升沿中断: 测量两次上升沿之间的时间差 */ ISR(INT0_vect) { uint8_t ovf; uint8_t tcnt; uint16_t now; uint16_t diff; cli(); ovf timer_ovf; tcnt TCNT0; sei(); now ((uint16_t)ovf 8) | tcnt; diff now - prev_edge; prev_edge now; if (decode_state ST_WAIT_LEAD) { ir_bit_cnt 0; decode_state ST_MID_LEAD; } else if (decode_state ST_MID_LEAD) { /* 引导码 9ms 空闲 4.5ms 第一位载波 0.56ms 5.06ms */ /* 150kHz 采样率下,对应计数 700~850 */ if (diff 700 diff 850) { decode_state ST_DATA; } else { decode_state ST_WAIT_LEAD; } } else { /* 数据位: diff 对应上一位的空闲时间 本位载波时间 */ if (diff 250) { ir_bytes[ir_bit_cnt 3] (ir_bytes[ir_bit_cnt 3] 1); } else if (diff 450) { ir_bytes[ir_bit_cnt 3] (ir_bytes[ir_bit_cnt 3] 1) | 1; } else { decode_state ST_WAIT_LEAD; ir_bit_cnt 0; return; } ir_bit_cnt; if (ir_bit_cnt 32) { ir_ready 1; decode_state ST_WAIT_LEAD; } } }这段代码的逻辑关键是 diff 的定义:从上一个上升沿到当前上升沿的计数差值。引导码结束时的上升沿和第一位数据载波结束时的上升沿之间,隔着 4.5ms 空闲加 0.56ms 载波,合计 5.06ms,在 150kHz 计数频率下就是约 759 个计数,所以阈值取 700~850。数据位阶段,一个 0 位从上升沿到下一个上升沿是 1.125ms,约 168 个计数;一个 1 位是 2.25ms,约 337 个计数。用 250 和 450 作为分界,把 0、1 和异常间隔区分开。这里有个细节:进入 ST_DATA 后,第一次有效的数据判断发生在第三个上升沿,对应的是第一位。ir_bit_cnt 从 0 开始,每处理一个上升沿加 1,达到 32 就置位 ir_ready。ir_bytes 按字节推进,每 8 位进一个字节,最后 ir_bytes[2] 是命令码,ir_bytes[3] 是命令反码,取的时候直接读就行。中断里cli()和sei()包住读取 TCNT0 的操作,是防止 8 位计数器刚好在读取瞬间溢出导致时间戳错乱,这个坑稍后在避坑章节展开。3.3 主循环消费数据:反码校验与按键映射中断负责采集,主循环负责消费,两者通过 ir_ready 标志配合。主循环轮询到 ir_ready 置位,就说明一帧完整的 32 位数据已经躺在 ir_bytes 里了。下面是资源包中配套的主循环逻辑:int main(void) { DDRB (1 LED_PIN); MCUCR (1 ISC01) | (1 ISC00); /* INT0 上升沿触发 */ GIMSK (1 INT0); TCCR0B (1 CS01) | (1 CS00); /* Timer0 64 预分频 */ TIMSK0 (1 TOIE0); sei(); while (1) { if (ir_ready) { ir_ready 0; uint8_t addr ir_bytes[0]; uint8_t addr_inv ir_bytes[1]; uint8_t cmd ir_bytes[2]; uint8_t cmd_inv ir_bytes[3]; /* 反码校验 */ if (addr ! (uint8_t)~addr_inv) { continue; } if (cmd ! (uint8_t)~cmd_inv) { continue; } /* 按命令码分发 */ if (cmd 0x45) { PORTB ^ (1 LED_PIN); } } } }反码校验的写法要特别注意:对 uint8_t 取反后结果是 int 类型,直接比较会带上高 8 位的 1,所以必须用(uint8_t)~强转回 8 位。这个细节我在第一次写的时候漏掉,导致明明命令码是对的,校验却总是不通过,排查了大半天。主循环里没有做任何延时,轮询空转马上进入下一轮。这样做的好处是保留最低的响应延迟,坏处是 CPU 空载功耗略高。如果只是做遥控开关,这个功耗差异无所谓;如果要做电池供电,那就得看第 4 章的睡眠方案。命令码 0x45 对应 NEC 协议里遥控器数字 1 的常见编码,不同品牌的遥控器这个值会有差异,实际使用时把资源包里的按键映射表改成自己遥控器的键值就行。4. 资源极限优化:4KB Flash 和 64B SRAM 怎么分配4.1 内存占用分配:表放 Flash,变量别堆 SRAMATtiny13 的 4KB Flash 对纯解码来说绰绰有余,但 64 字节 SRAM 才是真正的紧箍咒。中断里用的 ir_bytes、ir_bit_cnt、ir_ready 这些全局变量加上栈空间,大概要占掉十几字节,剩下的还要给主循环。如果一开始就想着把所有按键映射做成 volatile 数组,SRAM 立刻见底。AVR GCC 默认把所有非 const 全局变量放进 SRAM,const 数组如果没有加 PROGMEM 也会被复制到 SRAM,这是新手最容易踩的内存陷阱。资源包里的按键映射表用的是 PROGMEM,把表固定在 Flash 地址空间,运行时按需读取:#include avr/pgmspace.h #include avr/eeprom.h /* 遥控器命令码 → 逻辑键号映射表,存放在 Flash */ const uint8_t key_map[8] PROGMEM { 0x45, 0x46, 0x47, 0x44, 0x40, 0x43, 0x07, 0x15 }; /* 用户自定义映射,可写入 EEPROM */ uint8_t user_map[4] EEMEM { 0xFF, 0xFF, 0xFF, 0xFF }; uint8_t lookup_key(uint8_t cmd) { for (uint8_t i 0; i 8; i) { if (pgm_read_byte(key_map[i]) cmd) { return i; } } return 0xFF; }pgm_read_byte是 AVR Libc 提供的宏,它在读 Flash 时会把临时数据放进一个即时释放的临时寄存器,RAM 占用不会增长。EEPROM 这边,ATtiny13 有 64 字节,写坏不心疼,用来存用户学习到的键值很划算。两者的配合方式是:上电先读 EEPROM,EEPROM 里的值都是 0xFF 就落到 PROGMEM 默认表,否则优先用 EEPROM 的映射。这套做法在同一个固件上实现学习型遥控开关,不需要重新编译就能换遥控器。编译优化选项也有讲究。Makefile 里-Os是默认,但如果你的代码里混了大块 switch-case 分支表,-Os可能生成查表跳转指令,一个表占 2 字节还好;真正吃 Flash 的是_delay_ms(),一个延时就吃进去一两百字节。资源包里所有延时改成了基于 Timer0 溢出的非阻塞计时,主循环只判断时间差,一套逻辑同时解决去抖和重复码节流。4.2 低功耗再进一步:PWR_DOWN 睡眠与唤醒方式如果这个接收器要放电池供电,MCU 的空转轮询会白白烧掉 1mA 量级的电流。ATtiny13 的 PWR_DOWN 模式能把自身功耗压到 1μA 以下,但前提是唤醒方式选对。ATtiny13 在睡眠状态下的硬件外部中断 INT0 只支持电平触发唤醒,边沿触发在 deep sleep 里不生效,这是一个数据手册里写得很清楚但很多人会忽略的边界条件。所以睡眠唤醒的正确姿势是用引脚变化中断 PCINT0,而不是 INT0。把红外接收头输出接到 PB0 后,PCINT0 寄存器使能 PB0 的引脚变化中断,这时候无论上升沿还是下降沿都能把芯片从 PWR_DOWN 拉醒。唤醒后先做一次引脚状态判断:如果当前是高电平,说明这里只是一次噪声抖动,重新睡回去;如果当前是低电平,说明载波正在发送,马上切换到 INT0 上升沿模式开始解码。代码如下:#include avr/sleep.h void enter_sleep(void) { /* 关闭 INT0,开 PCINT0,两个中断共用 PB0 */ GIMSK (1 PCIE); PCMSK (1 PCINT0); set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); sei(); sleep_cpu(); sleep_disable(); /* 唤醒后恢复 INT0 上升沿触发 */ MCUCR (1 ISC01) | (1 ISC00); GIMSK (1 INT0); PCMSK 0; }注意sleep_enable和sei的顺序:先使能睡眠,再开全局中断,进sleep_cpu后中断一来立刻唤醒;如果反了,中断会在进入睡眠前被吃掉,芯片就永远睡过去了。唤醒后 GIMSK 要从 PCINT 切换回 INT0,解锁后面的解码流程。这套切换逻辑在资源包的 low_power.c 里,直接抄就能用。对功耗的预期要说清楚:ATtiny13 睡下去是微安级,但红外接收头本身工作就要 0.5~1mA,所以整个系统的底噪电流主要来自接收头,不是一个完全零功耗的方案。想再往下压,只能从接收头的供电下手,比如用 ATtiny13 的一个 I/O 引脚串 MOS 管给接收头断电,平时接收头完全不供电,定时醒来给它上电然后扫描一遍。这个做法我已经在另一套低功耗遥控开关上验证过,平均电流能做到 20μA 左右,但那是另一套工程了。5. 避坑指南:ATtiny13 红外遥控接收器常见翻车现场5.1 现象:接收距离只有 10 厘米,一米外完全没反应原因分两类。第一是载波频率不匹配,遥控器发射的是 40kHz,接收头是 38kHz 的,虽然 38kHz 的带通滤波器有一定的带宽,但增益会大幅下降,距离被压缩到脚底下。第二是电源纹波,接收头对 VCC 上的毛刺极敏感,劣质 USB 供电或者 LDO 布局不当,都会让接收头内部的比较器产生误翻转。解决:先用手机摄像头确认发射管载波频率,判断是不是选型问题;再在接收头 VCC 与 GND 之间就近焊 100nF 陶瓷电容,必要时在电源入口并 47μF 电解。如果两个都做了还是近,拿示波器看 PB0 波形,确认空闲电平是不是稳定高电平、载波来的时候是不是干净的低脉冲。5.2 现象:按一下遥控器,输出连续跳好几档,像是按键粘住了原因是 NEC 协议的重复码。按住按键不松时,发射端每 108ms 发一次重复码,而重复码的第一段也是一个 9ms 载波加 2.25ms 空闲,如果你的解码状态机把重复码当成一帧新数据的引导码,主循环就会一次次触发业务逻辑。解决:在主循环消费完一次帧数据后,记录当前时间,下一次 ir_ready 置位时先判断间隔。如果两次有效帧之间只有约 108ms,说明是对同一按键的重复,直接丢弃。更进一步,可以专门识别重复码的特征时长,连续收到三次重复码就主动进入待机等待释放。资源包的去抖逻辑用的就是第一种方案,一个全局变量存上次触发时间,间隔小于 150ms 的后续帧一律忽略。5.3 现象:程序烧不进去,AVRDude 报 device signature 读取失败这通常是熔丝位惹的祸。ATtiny13 出厂默认 CKDIV8 是编程的,实际运行频率是 9.6MHz 除以 8,也就是 1.2MHz,SPI 编程时钟如果还是按默认的 100kHz 以上,低速目标还能忍;但如果你用的是外部晶振配置,或者不小心把 RSTDISBL 熔丝置位,那普通 ISP 编程器就彻底连不上了。解决:先用低速模式烧录,avrdude -c usbasp -p t13 -B 125kHz,确认能读到 signature 后立刻重写熔丝。CKDIV8 必须清零,SPIEN 保持默认。最危险的操作是置位 RSTDISBL,一旦置位,PB5 变成普通 I/O,编程器失去复位控制,只能上高压编程器救回来。我的习惯是熔丝只改高低位的 CKDIV8 和 BOD 相关位,扩展位 RESET 那个绝对不碰。5.4 现象:大部分按键能解出来,偶尔一个键出来是乱码优先怀疑中断里读取 TCNT0 的竞争条件。TCNT0 是 8 位计数器,从 0xFF 到 0x00 的溢出瞬间,你读到的值可能已经归零,而 timer_ovf 还没来得及加 1,这会导致 now 比真实时间小了 256 个计数,换算成时间就是 1.7ms 的偏差,一个本应是 1 的位被判成 0。解决:用cli()和sei()把timer_ovf和TCNT0的读取包成临界区,就是我在 3.2 节代码里写的那样。另一个来源是发射端 AGC 导致尾边沿提前结束,如果乱码集中在某个按键上,多半是那颗键的遥控器编码用了不同的协议变种,需要上逻辑分析仪抓真实波形才能定论。5.5 现象:遥控器换了个品牌,键值对不上或者完全不响应NEC 协议有扩展模式,地址码可能不是 8 位而是 16 位,厂商自定义的命令码排列也不一样。还有厂商用的是 NEC 的 32 位变体,地址部分固定为 0x00FF,命令码和反码位置不变,但判断逻辑如果写死了地址等于 0x00 就会误判。解决:不要假设地址一定是 0x00,解码后同时校验地址反码和命令反码,都通过就把命令码交给映射表。资源包里的 lookup_key 函数就是先查映射表,映射表里没有的键值直接忽略,这样即使协议地址有差异,只要命令码匹配就能用。遇到完全解不开的遥控器,先用示波器或逻辑分析仪看引导码和位宽是不是标准 NEC,不是的话就得改协议参数了。6. 验证技巧与进阶:逻辑分析仪抓波形,确认中断路径解码程序写完之后,第一件事不是直接焊到产品上,而是先用逻辑分析仪验证波形和代码的判断逻辑。把探头夹在 ATtiny13 的 PB0 上,采样率设到 16MHz 以上,按下遥控器按键,在软件里标出两个上升沿之间的时间间隔。对照引导码 5.06ms、数据位 1.125ms 和 2.25ms 这几组特征值,一眼就能看出阈值区间设置是否合理。资源包解码代码里的 700~850、250、450 这几个阈值,就是基于 9.6MHz 时钟和 64 预分频算出来的,如果你改了时钟频率,这些数必须按比例重算。进阶一点的验证方法是测中断处理的耗时。在 INT0 中断服务程序开头翻转一个测试引脚,逻辑分析仪上看这个引脚的脉冲宽度,就是实际中断处理时间。ATtiny13 在 9.6MHz 下处理一次中断大概几十微秒,距离数据位的最小间隔 560μs 还有很大余量,但如果你的中断里塞进了长长的乘除法或者函数调用,这个耗时会被明显放大,严重时下一个沿到来时中断还没退出,直接丢位。我一般会把中断处理代码控制在纯移位和比较操作,任何可能的复杂计算都丢到主循环去。还有一个实用技巧是可以将重复码识别放进主循环的超时判断,而不是在中断里加状态,这样中断代码保持最短。资源包里还附带了一个验证用的小工具逻辑:用 PB2 输出一个 1kHz 方波,接一个红外 IR LED 对准接收头,板上自测解码链路。这样烧完程序不用拿实体遥控器做回归测试,每次改代码后先跑一轮自测,省去大量重复按遥控器的时间。从那以后我每次拿到一个新遥控器,都是先抓引导码和最小位宽的波形,再改代码里的阈值,然后才谈按键映射。希望帮到你。本文还有配套的精品资源点击获取