首页 / 资讯中心 / 文章详情

基于ATtiny13的红外遥控接收器设计与NEC协议解码实现

基于ATtiny13的红外遥控接收器设计与NEC协议解码实现 ★ FEATURED ARTICLE
简介基于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说明红外接收头OUTPB2引脚 7数据输出接外部中断 INT0红外接收头VCC5V注意接收头供电电压3.3V 也可但距离缩水红外接收头GNDGND尽量短接地ATtiny13VCC5V用 5V 供电解码更稳退耦电容正极VCC100nF 陶瓷电容紧贴接收头电源脚退耦电容负极GND位置比容值更关键输出控制PB0负载驱动默认输出用于控制继电器或 LED编程接口PB1MOSI / 编程时钟烧录时使用运行时悬空上电后 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/13.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 只有 Timer08 位带一个预分频器。我把它配置成自由运行模式不分频时钟源直接用系统时钟。由于系统时钟通常是 1.2MHz默认内部 RC那么一个 tick 约 0.83us两次下降沿之间的间隔如果是 1690us约 2035 tick这个值远大于 255所以定时器会溢出多次。解决办法是记录“溢出次数”配合当前计数值一起算。每次外部中断时先关闭中断读取当前 TCNT0然后把溢出计数器乘以 256 再加上 TCNT0 就得到总 tick 数。两次中断的 tick 数相减就是间隔时间。间隔时间与判定阈值如下表这是我实测过的建议值信号特征总长度tick1.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 语句直接写死。键值映射的一个常见做法是把命令码按数组索引存进 FlashPROGMEM然后用二分或线性查找。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()用 104us9600 波特率作为一位宽度延时精度用_delay_us()实现。实测中内部 RC 振荡器误差在 5% 以内9600 波特率的接收端完全能兼容。调试串口能让你看清接收到的 g_raw_code 到底是什么而不是靠猜。我用逻辑分析仪抓过多次发现最常见的调试错误是把数据位顺序搞反。NEC 先发送的是地址的低位在前但我的代码里用(1UL (31 - g_bit_count))来累计顺序是按照“先收到的位放高位”最终命令码是反码校验才正确的。如果你用网上其他代码可能顺序和这个相反所以调试日志里要打印完整 32 位十六进制值人工验证地址码和命令码是否和你按的键一致。最后一个习惯我会在产品板上留一个测试焊盘把串口 TX 引出。这样调试时用杜邦线接 USB-TTL跑完测试再断开。这个方法救过我无数次因为 ATtiny13 引脚太少想外部显示状态很难一条串口能输出的信息量远超 LED 闪灯。希望这个技巧能帮到你让你的 ATtiny13 红外遥控项目少折腾几个晚上。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站