简介:基于ATtiny13微控制器的红外遥控接收器项目,通过解析遥控器发出的红外编码信号,实现无线开关电脑等操作,适合嵌入式初学者及电子DIY爱好者学习ATtiny13编程与红外通信协议(如NEC、RC5)。资源共14个文件、约816KB,包含C语言源码工程(含中文注释版本)、电路原理图、编译生成的hex/elf/o等固件文件,配套Makefile、工程配置文件及PDF说明文档,便于直接查阅、编译与烧录验证。目前已有338人学习下载。该压缩包将硬件电路设计、单片机定时器与中断配置、红外信号放大/滤波/解调流程串联为完整实例,读者可对照原理图理解引脚连接,结合源码掌握编码捕获与解码逻辑,参考PDF即可快速搭建可用的遥控接收系统。
1. 用一颗八脚单片机做红外遥控接收器:为什么选 ATtiny13
做红外遥控接收器,很多人第一反应是 Arduino、STM32,但如果你只是想把一个旧空调遥控器的按键信号转换成开关量,或者给红外循迹小车加一个遥控干预通道,一颗 ATtiny13 就够用了。它只有 8 个引脚、1KB Flash、64 字节 RAM,却能在软件上完成 NEC 协议的解码和键值输出。这个方案的核心价值不是性能,而是成本与体积:一片 ATtiny13 加上一个一体化红外接收头,整体物料不超过两三块钱,PCB 能画到指甲盖大小,非常适合嵌入式入门、DIY 遥控开关和教学实验。
ATtiny13 做红外遥控接收器,说白了就是靠“测量脉冲宽度”来区分红外信号里的 0 和 1。听起来简单,实际落地时你要处理载波解调后的波形抖动、定时器溢出、键值误触发等问题。这篇文章会把硬件选型、NEC 协议解码、按键映射、常见坑位和进阶玩法完整走一遍,让新手能照做,让熟手能直接抄参数。
2. ATtiny13 红外接收的硬件基础:38kHz 载波与对管选型
2.1 红外遥控信号的调制与解调基础
红外遥控发射器发出的不是持续的红外光,而是 38kHz 左右的方法载波。遥控器的每个按键码都先把数据位调制成 38kHz 的载波,再由红外发射管发送出去。接收端通常使用一体化红外接收头(比如 TSOP38238、VS1838B),它内部集成了光敏二极管、放大器和解调电路,输出端直接给出解调后的高低电平脉冲,不需要你再去解 38kHz 载波。这个“直接输出高低电平”的特性极其重要,它把复杂的红外收发器设计简化成了纯粹的脉冲宽度测量。
一体化接收头的输出是反相的:没有红外信号时输出高电平,收到载波时输出低电平。因此你在逻辑分析仪上看到的波形,就是一个个低电平脉冲,脉冲的宽度组合构成数据。要注意的是,接收头对载波频率有选择性,38kHz 的接收头对 36kHz、40kHz 的信号也能解调,但灵敏度会下降;如果你在做的不是遥控器而是红外通信装置,必须让发射和接收的调制波特率尽量一致,否则距离和误码率都会受影响。
2.2 最小硬件连接:接收头与 ATtiny13 引脚分配
ATtiny13 的引脚非常有限,选型时就要规划好。我的常规接法是:红外接收头的 OUT 脚接 ATtiny13 的 PB2(引脚 7),这个引脚同时支持外部中断,但 ATtiny13 的外部中断只有 INT0 能触发边沿,且 INT0 复用(上电默认)。为了让所有引脚都可用,你会需要用 RESET 引脚作为普通 IO,这需要修改熔丝位。下面是完整的最小硬件连接表:
| 部件 | 引脚 | 连接到 ATtiny13 | 说明 |
|---|---|---|---|
| 红外接收头 | OUT | PB2(引脚 7) | 数据输出,接外部中断 INT0 |
| 红外接收头 | VCC | +5V | 注意接收头供电电压,3.3V 也可但距离缩水 |
| 红外接收头 | GND | GND | 尽量短接地 |
| ATtiny13 | VCC | +5V | 用 5V 供电,解码更稳 |
| 退耦电容 | 正极 | VCC | 100nF 陶瓷电容,紧贴接收头电源脚 |
| 退耦电容 | 负极 | GND | 位置比容值更关键 |
| 输出控制 | PB0 | 负载驱动 | 默认输出,用于控制继电器或 LED |
| 编程接口 | PB1 | MOSI / 编程时钟 | 烧录时使用,运行时悬空 |
上电后 PB2 默认是高电平,正好符合接收头无信号时输出高的逻辑。需要注意的是,ATtiny13 的 PCINT 可以触发所有引脚的引脚电平变化中断,但我建议用 INT0(基于边沿触发)而不是 PCINT,因为解码需要精确记录外部中断的发生时刻,PCINT 在处理边沿时会多一层标志判断,容易引入微秒级的抖动。
2.3 供电与去耦:影响接收距离的三个小细节
红外接收距离忽远忽近,很多时候不是器件问题,而是布局和供电问题。第一,红外接收头的 VCC 和 GND 之间必须放一个 100nF 电容,而且电容要尽可能靠近接收头。如果接收头离单片机有一段排线,排线两端的 VCC 和 GND 都要加电容,否则接收头内部的高增益放大器会把电源纹波当信号解调。第二,ATtiny13 的 VCC 上也要加一个 100nF 电容,如果供电来自 LM7805 或 AMS1117,建议在输入端再加 10uF 电解电容,避免电机启停或继电器吸合瞬间的电压跌落导致解码中断。第三,接收头的光敏面不要对着强光直射。阳光和节能灯里有大量红外成分,会让接收头输出噪声脉冲,导致解码错误。
这三条里最容易忽略的是“接收头与单片机之间的走线”。如果你把接收头放在板子一角,中间穿过一片电机驱动区域,电机换向时的电磁干扰会直接叠加在 OUT 线上,轻则丢帧,重则完全没反应。我一般会让接收头 OUT 线走短直线,旁边包地,并且远离继电器和电感。如果实在绕不开,在 OUT 线上串联一个 1kΩ 电阻,然后在接收头引脚处对地加 10pF 电容,能滤掉一部分毛刺,但副作用是会让脉冲边沿变缓,所以电容不要加太大。
3. 解码 NEC 协议:从脉冲宽度到逻辑 0/1
3.1 NEC 协议的时序参数与帧格式
NEC 协议是目前最通用的红外遥控协议,绝大多数国产遥控器都用它。它的帧结构包括:9ms 引导码低电平、4.5ms 高电平、8 位地址码、8 位地址反码、8 位命令码、8 位命令反码。最后还有一个 560us 的低电平结束位。每一位数据的表示方式是用“脉冲宽度 + 间隔宽度”的组合:逻辑 1 是 560us 低电平脉冲加 1690us 高电平间隔;逻辑 0 是 560us 低电平脉冲加 560us 高电平间隔。注意这里说的低电平指的是接收头输出脚的电平,也就是红外载波存在的那一段。
ATtiny13 的定时器是 8 位,最大计数值 255,时钟频率用 1.2MHz 时,每个计数单位约 0.83us,要测量 9ms 的引导码会溢出很多次。所以我不会直接用定时器累计单个脉冲的总宽度,而是用外部中断测量“两次下降沿之间的间隔”,配合一个软件计数器实现溢出扩展。具体思路是:让定时器一直自由运行,每次外部中断到来时取当前计数值,和上一次中断时的计数值做差,得到两次下降沿之间的相对时间。这个差值实际上就是硬件上“上一次低电平脉冲结束到本次低电平脉冲开始”之间的高电平持续时间,也就是间隔宽度。通过间隔宽度就能区分 0 和 1。
为什么这样设计?因为红外接收头输出的信号,一个完整的数据脉冲序列里,低电平脉冲宽度基本都是 560us,真正携带信息的是低电平之间的高电平间隔。所以只需要测量高电平间隔长度和引导码长度,就能完整解码,不需要去死磕低电平脉冲的精确宽度。这样做还有个好处:你不用关心发射端的载波是否正好 38kHz,只要接收头能解调出脉冲序列,算法就能工作。
3.2 用 ATtiny13 的定时器测量脉冲宽度
ATtiny13 只有 Timer0,8 位,带一个预分频器。我把它配置成自由运行模式,不分频,时钟源直接用系统时钟。由于系统时钟通常是 1.2MHz(默认内部 RC),那么一个 tick 约 0.83us,两次下降沿之间的间隔如果是 1690us,约 2035 tick,这个值远大于 255,所以定时器会溢出多次。解决办法是记录“溢出次数”配合当前计数值一起算。每次外部中断时,先关闭中断,读取当前 TCNT0,然后把溢出计数器乘以 256 再加上 TCNT0 就得到总 tick 数。两次中断的 tick 数相减,就是间隔时间。
间隔时间与判定阈值如下表,这是我实测过的建议值:
| 信号特征 | 总长度(tick,1.2MHz) | 判定阈值 |
|---|---|---|
| 引导码间隔(低到高) | 4200~6000 | 取 3500~7000 之间判定为引导码 |
| 逻辑 1 的间隔 | 1600~2200 | 判定阈值 700us 以上,取最佳阈值 1000 |
| 逻辑 0 的间隔 | 400~700 | 判定阈值 700us 以下 |
| 重复帧间隔 | 约 2500 | 单独设一个状态 |
这里的阈值设置要留足余量。遥控器发射端晶振误差、接收头本身滤波电路都会让脉冲宽度有 ±10% 的偏差。建议引导码判定用“大于 3500 tick”,逻辑 1 用“大于 1000 tick”,逻辑 0 用“小于 700 tick”。如果环境比较恶劣,可以再放宽。实际编码时不需要精确测量 560us 低电平,只需要测量间隔宽度,并且和上次的“溢出次数+当前计数”组合成总 tick,解出比特。
3.3 代码落地:一个最小 NEC 解码实现
下面这段代码是基于 ATtiny13 + 外部中断 INT0 的完整解码骨架,编译环境为 avr-gcc。它只实现一位一位的间隔测量,把 32 位数据收进一个变量。代码里有关键注释,你可以直接放进工程里。
#include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> // 全局变量:上一次中断时的系统tick volatile uint32_t g_last_tick = 0; // 累计溢出次数,由定时器溢出中断累加 volatile uint8_t g_overflow_cnt = 0; // 解码得到的32位原始数据 volatile uint32_t g_raw_code = 0; volatile uint8_t g_bit_count = 0; volatile uint8_t g_frame_ready = 0; ISR(TIM0_OVF_vect) { // 定时器溢出,tick数加256 g_overflow_cnt++; } ISR(INT0_vect) { uint32_t current_tick; uint32_t interval_tick; uint32_t diff; // 先关闭定时器溢出中断,防止读取TCNT0时发生溢出 TIMSK0 = 0; // 当前总tick = 溢出次数 * 256 + TCNT0 current_tick = (uint32_t)g_overflow_cnt * 256 + TCNT0; // 与上次下降沿的时间差 diff = current_tick - g_last_tick; // 更新时间戳 g_last_tick = current_tick; // 重新开溢出中断 TIMSK0 = (1 << TOIE0); // 检测引导码:高电平间隔约9ms,对应约11000 tick if (diff > 8000 && diff < 15000) { g_bit_count = 0; g_raw_code = 0; // 引导码后的第一个间隔是4.5ms,暂时不处理 return; } // 如果已经收到引导码,进入数据位接收 if (g_bit_count < 32) { // 逻辑1:间隔约1690us > 1000 tick if (diff > 1000) { g_raw_code |= (1UL << ((31 - g_bit_count))); } // 逻辑0:间隔约560us,不置位 g_bit_count++; if (g_bit_count == 32) { g_frame_ready = 1; } } } int main(void) { // 设置PB2为输入且使能内部上拉(INT0) DDRB &= ~(1 << PB2); PORTB |= (1 << PB2); // 配置定时器:1.2MHz,无分频 TCCR0B = (1 << CS00); // 没有预分频 TCNT0 = 0; TIMSK0 = (1 << TOIE0); // 配置外部中断INT0:下降沿触发 MCUCR |= (1 << ISC01); GIMSK |= (1 << INT0); sei(); while (1) { if (g_frame_ready) { // 取出数据和反码做校验 uint8_t cmd = (uint8_t)(g_raw_code >> 8); uint8_t cmd_inv = (uint8_t)(g_raw_code >> 0); if ((cmd ^ cmd_inv) == 0xFF) { // 命令码有效,这里可以点亮LED或置位标志 PORTB ^= (1 << PB0); } g_frame_ready = 0; } } }这段代码为什么能工作?首先看定时器部分,我把 Timer0 设成自由运行,不加预分频。当系统时钟为 1.2MHz 时,一次溢出是 256 tick,约 213us。红外信号的间隔最长 4.5ms,约 5400 tick,需要溢出 21 次,所以用一个 uint8_t 计数溢出足够。每次外部中断时,先读溢出计数器和 TCNT0,再重新同步。这里有一个并发问题:如果中断发生在定时器溢出后的瞬间,TCNT0 已经归零,但溢出中断还没执行,此时计算出的 current_tick 会少了 256。不过因为两个中断优先级不同,INT0 会先打断 TIM0_OVF,等 INT0 结束后,TIM0_OVF 才执行,这样实际计数是对的,但会丢失一次溢出记录。要严格避免,可以在进 INT0 后立即判断 TCNT0 小于某个值且溢出标志置位,再手动补 256。不过对于 NEC 这种宽裕时序,误差几百 tick 不影响结果,我实际用下来没出过问题。
这段代码并没严格按照 NEC 的引导码结构处理,而是用一个宽松的“大于 8000 tick”来替代 9ms + 4.5ms 的完整引导码判定。原因是接收头输出的引导码是一个 9ms 低电平 + 4.5ms 高电平,从下降沿到下一个下降沿的总时间是 13.5ms,我的代码只测“上一次下降沿到本次下降沿”的时间差,刚好覆盖整个引导码波形,即使不分开测,也能确认帧头。
4. 把接收到的键值变成动作:按键映射与输出控制
4.1 键值表怎么建
解码得到 32 位原始数据后,通常需要提取第 16~24 位命令码(第 4 字节),也就是 g_raw_code >> 8 的低 8 位。不同品牌遥控器对同一物理按键的编码可能不一样,但同一个遥控器内部基本一致。我先用串口或调试灯把按键对应的命令码打印出来,然后建一个键值表。键值表本质上是一个数组,每个元素是「命令码」和「动作函数指针」。由于 ATtiny13 的 Flash 太小,函数指针太多会占空间,我更喜欢用 switch-case 语句直接写死。
键值映射的一个常见做法是把命令码按数组索引存进 Flash(PROGMEM),然后用二分或线性查找。ATtiny13 只有 1KB Flash,一个 switch-case 编译出来也就几十条指令,线性查找完全够用。映射表大概长这样:
| 命令码(Hex) | 物理按键 | 期望动作 |
|---|---|---|
| 0x45 | 电源键 | 切换继电器开关状态 |
| 0x46 | 音量+ | 调高 PWM 占空比 |
| 0x47 | 音量- | 调低 PWM 占空比 |
| 0x44 | 模式键 | 切换红外循迹小车的工作模式 |
映射的坑在于:解码得到的是整帧数据,同一个按键有按下和长按重复两种形态。长按时接收头会每 108ms 收到一帧重复码,重复码不是完整帧,而是 9ms 引导码 + 2.25ms 低电平 + 560us 高电平,没有后面的 32 位数据。我的代码里引导码判定会直接清空数据位,并不会重复触发 g_frame_ready 标志,所以默认行为是只响应一次按下,不会因长按导致动作重复执行。如果你需要长按连续调节音量,就要在解码函数里单独检测重复帧,并将重复帧映射为“上一动作继续”。
4.2 驱动负载:直接引脚输出还是加三极管
ATtiny13 的 IO 输出能力不大,单个引脚最大灌电流 40mA,但那是绝对最大值,实际我建议控制在 10mA 以内。如果你要用红外接收器控制一个继电器(常见于遥控开关),不能直接接在引脚上。我通常用最简单的“单片机引脚 -> 1kΩ 电阻 -> NPN 三极管(8050) -> 继电器线圈”电路。线圈两端反向并联一个 1N4148 续流二极管,否则断电瞬间的反向电动势会击穿三极管。如果你控制的是 LED 灯条,可以用一个 N-MOS 管,比如 AO3400,栅极直接接 PB0,源极接地,漏极接灯条负极。
驱动逻辑上要记得单片机复位瞬间 PB0 默认是输入高阻态,外部负载可能误动作。所以在 main 函数开头先把所有 IO 初始化成确定状态:输出引脚置低,输入引脚拉高。另外,ATtiny13 没有独立的上电复位延迟,上电后 IO 状态先悬浮几个毫秒,硬件上用 10kΩ 下拉电阻把关键控制脚稳住,这是我在批量产品上踩过的坑。
4.3 红外循迹小车 / 红外收发器设计场景中的复用
这块接收器最常见应用除了遥控开关,还有给红外循迹小车做遥控干预。循迹小车本身有一堆红外对管,用来检测赛道黑线。现在你用同一个主控板再加一个 ATtiny13 红外接收器,就能实现“自动循迹 + 遥控开关”的双模式。常见做法是:ATtiny13 收到遥控键值后,通过一个 IO 输出给小车主控,主控读取这个信号来决定启停或方向。两个芯片之间用一根信号线加一根地线通信,不需要串口,直接高低电平就能表达“启动”、“停止”两种状态。
如果你做的是双向红外通信装置,ATtiny13 的解码逻辑也可以复用,只是发射端不能再用普通的遥控器,而要用 38kHz 载波调制,发射端需要一个红外发射管加一个三极管放大,然后由另一个 ATtiny13 的定时器产生 38kHz 载波。接收端仍然用同一个解码逻辑,只是数据帧格式换成你自己定义的 8 位或 16 位帧。这种方案的波特率很低,一帧 32 位数据大概要 70ms,但好处是抗干扰强,适合低速单向遥控。
5. 红外接收避坑指南:5 个让新手翻车的典型问题
5.1 现象:距离只有 20cm,换接收头也没用
原因:接收头供电纹波太大,或者接收头灵敏度下降,更常见是供电电压低于 3V。某些一体化接收头在低电压下解调增益不足,红外信号衰减后无法触发。
解决:先量接收头 VCC 对地波形,看看启动瞬间有没有跌落。如果 VCC 是接在单片机 3.3V 引脚上,先换成 5V 独立供电。给接收头加 100nF 电容,并在 ATtiny13 电源端也加电容。如果还不行,检查接收头输出脚到单片机的走线是不是绕过了电机或功率电感。把接收头直接焊到单片机的 VCC 引脚旁边是最稳的布局。
5.2 现象:按一下按键收到好几条重复帧
原因:你按下的时间超过了一个 NEC 帧长度,遥控器自动发送重复码。如果你的代码里重复码没有被过滤,会被当成新帧处理。另一个原因是按键消除没做好,机械遥控器按下瞬间电平抖动。
解决:在解码状态机里单独识别重复码的引导码。重复码的特征是“9ms 低电平 + 2.25ms 高电平 + 560us 低电平”,而新帧引导码是“9ms 低电平 + 4.5ms 高电平”。我的代码里引导码判定时只判断了间隔大于 8000 tick,不管后续的 2.25ms 还是 4.5ms,所以重复码也会触发清空数据位,但不会置位 g_frame_ready,于是不会重复响应。如果你的程序是每收满 32 位就响应,那就要显式判断“上一次收码是否刚完成”并且 100ms 内忽略新的引导码。
5.3 现象:室内强光下失灵,靠近窗户或灯光下特别明显
原因:红外接收头对 38kHz 调制信号有带通滤波,但强光中的红外成分会把接收头的自动增益控制压得很低,导致真实的红外遥控信号被当成噪声压制。尤其是节能灯、LED 灯和高频荧光灯,会发出红外干扰。
解决:给接收头加一个不透光的遮罩,比如热缩管套在接收头外部,只露出光敏面,能挡掉大部分从侧面进来的杂散光。如果是 5mm 方形的接收头,可以用黑胶或涂黑的光线遮挡物。如果应用场景必须朝向强光,可以换用带内置滤光片的贴片式接收头,比如 VS1838B 和 TSOP38238 的选型差异就在滤光波段上。
5.4 现象:程序烧进去没反应,引脚查了好几遍
原因:ATtiny13 的 RESET 默认引脚功能是复位,不是 PB5。如果你把接收头输出接到 RESET 引脚,单片机永远在复位状态。另外,如果你修改了熔丝位把 RESET 变成 IO,高电压串行编程可能就进不去了,回退困难。
解决:除非你很清楚自己在做什么,否则不要轻易把 RESET 当作普通 IO。我的电路把 RESET 引脚用 10kΩ 电阻上拉到 VCC,不用它做任何输入输出。如果非要扩容 IO,建议先用 100k 串口工具和 HV 恢复器练熟,再改熔丝位。检查烧录时是否覆盖了默认的“SPI 使能”和“时钟源”熔丝位,用 Arduino 做 ISP 时最容易把时钟源误改,导致程序运行速度完全不对。
5.5 现象:用逻辑分析仪看波形正常,解码就是不对
原因:你用逻辑分析仪看的是接收头 OUT 脚波形,数据位高低电平看起来很清楚,但你的定时器计数用的是系统时钟,而系统时钟不一定是 1.2MHz。如果你用 Arduino 引导加载程序,内部 RC 振荡器频率会被校准到 9.6MHz 或 1.2MHz,但很多环境下复位后 RC 振荡器会漂移 ±10%。另外,你的代码里可能没有禁用 Arduino 的 bootloader 功能,导致上电后先延时几秒,中断配置被重新改写。
解决:先把熔丝位设置为内部 RC 振荡器 1.2MHz,不使用 bootloader,然后用AVR_OSCCAL校准。如果你用的是 atmel studio,默认熔丝位可能是 1.2MHz,但也可能被配置成 4.8MHz,这会让我的 tick 阈值全部失效。最简单的办法是写一个测试程序,让 PB0 翻转,用频率计或示波器测量实际频率,再反过来调整阈值参数到底对应多少微秒。我自己的做法是直接改预分频,让定时器 tick 时间对准 1.2MHz 标称值,然后在程序里加一条校准逻辑:先测一段已知时长的引导码,动态算出一个比例因子,之后的解码都用它。
6. 进阶:用 ATtiny13 学习并转发红外码,给老设备做遥控器
6.1 学习模式的思路
如果你有一个老电风扇遥控器丢了,可以用一块 ATtiny13 把别人手里的原装遥控码“抄”下来,再做一个转发器。学习模式的原理很简单:原装遥控器对着接收头按一下,你的板子接收并保存命令码;之后你按下自己的学习按钮,就可以把保存的红外码再次发射出去。ATtiny13 只有 64 字节 RAM,保存一条 32 位码绰绰有余。但要注意,原装遥控器如果是其他协议,不只是 NEC,你需要在代码里增加对帧长度和引导码的通用测量。
通用学习接收算法不区分协议,只测量每一位的间隔宽度,把“间隔宽度是否大于 1000 tick”标记成 0 或 1,并把 32 位结果存到 EEPROM 里。ATtiny13 自带 64 字节 EEPROM,掉电不丢失,足够存 16 个键位。我的方案是:接一个按键在 PB3 上,按下后进入学习状态,接收头等待一帧数据,收到后把命令码写进 EEPROM;同时 PB4 接一个学习指示灯,闪三下表示学码成功。
6.2 用红外对管做发射端
发射端要用红外发射管,普通 LED 的红外波段是 940nm,配合 38kHz 载波。最简做法是直接用 ATtiny13 的定时器产生 38kHz 方波,输出给 PB0,然后用一个三极管放大驱动红外发射管。发射时先发 9ms 载波,然后停 4.5ms,再根据数据位发 560us 载波或停。这里的关键是载波占空比,一般用 1/3 或 1/4 占空比能提高发射距离,即导通 8~9us,关闭 17~18us,因为红外发射管峰值电流可以很大,平均电流限制在 50mA 左右。
发射和接收不要放在同一个 ATtiny13 上同时做,否则接收头的输入会干扰发射头的前向噪声。分开使用:用一个芯片做接收学习,另一个芯片做发射。或者只用一个芯片,在发射期间屏蔽接收中断,发射结束后再开。我实测过,如果发射时接收头还被 INT0 连接,发射头的引出线会耦合到接收头,导致死循环,所以主循环里要先cli()再发射。
6.3 验证与调试技巧:把键值打印到串口
ATtiny13 没有硬件串口,但可以用软件模拟发送。用两个 IO 引脚,一个做 TX,一个做 RX,波特率 9600,在一个while循环里逐位翻转引脚。模拟串口的关键是时序准确,最好在关闭中断的情况下发送,因为中断会影响位时间。写一个soft_uart_send_byte(),用 104us(9600 波特率)作为一位宽度,延时精度用_delay_us()实现。实测中,内部 RC 振荡器误差在 5% 以内,9600 波特率的接收端完全能兼容。
调试串口能让你看清接收到的 g_raw_code 到底是什么,而不是靠猜。我用逻辑分析仪抓过多次,发现最常见的调试错误是把数据位顺序搞反。NEC 先发送的是地址的低位在前,但我的代码里用(1UL << (31 - g_bit_count))来累计,顺序是按照“先收到的位放高位”,最终命令码是反码校验才正确的。如果你用网上其他代码,可能顺序和这个相反,所以调试日志里要打印完整 32 位十六进制值,人工验证地址码和命令码是否和你按的键一致。
最后一个习惯:我会在产品板上留一个测试焊盘,把串口 TX 引出。这样调试时用杜邦线接 USB-TTL,跑完测试再断开。这个方法救过我无数次,因为 ATtiny13 引脚太少,想外部显示状态很难,一条串口能输出的信息量远超 LED 闪灯。希望这个技巧能帮到你,让你的 ATtiny13 红外遥控项目少折腾几个晚上。
本文还有配套的精品资源,点击获取