Proteus+51单片机实现红外遥控NEC协议仿真与解码

发布时间:2026/9/16 3:21:18
Proteus+51单片机实现红外遥控NEC协议仿真与解码 简介一套基于89C51/89C52单片机的Proteus红外遥控模拟项目面向单片机初学者、课程设计者及51单片机实战爱好者适用于学习红外遥控信号收发原理、C语言模块化编程及Proteus 7.8环境下的仿真调试。资源包含Keil5工程文件、C语言源码、HEX编译文件和DSN仿真工程共34个文件其中解码、遥控、解码LCD等C程序模块与可运行的DSN仿真图是核心内容可加载后直接查看红外解码及LCD显示效果HEX文件还可快速烧录验证uv2/opt工程配置与lst/plg/obj中间产物则便于理解Keil编译过程。压缩包仅98KB轻便易取已有2698人学习浏览其完整工程结构与模块划分对入门单片机红外应用与Proteus联合调试有不错的参考价值。1. 红外遥控仿真比你想的更值得先搭一遍红外遥控仿真的难点从来不在码表而在对完时序之后不确定代码到底能不能跑。很多人直接焊实物手边没有示波器最后只能靠逻辑分析仪盲猜。Proteus 先把这笔意外账单省掉了搭好一个 DSN 工程写两个 51 程序一个模拟遥控器发射 NEC 波形一个做外部中断解码全程在 Proteus 8 Professional 里跑通。这套方案只依赖元件库里的 AT89C51 和红外接收头不需要额外的红外模块和真实遥控器。对于刚入门 51 和红外解码的初学者或者在做毕业设计、智能家居项目预研的工程师它能用一个下午把 NEC 协议、定时器测脉宽、DSN 工程组织整条链路验证完。2. 红外遥控仿真的 NEC 协议与 DSN 电路元件选型2.1 NEC 协议里最容易被仿真忽略的时序边界NEC 协议看起来只有三组时间9ms 引导码、560us 载波脉冲、以及 1.69ms 和 560us 两种空闲段。实际写代码时大多数人栽在“边界”上引导码之后紧跟的第一个数据位两个下降沿之间的间隔是 13.5ms 附近还是 2.25ms 附近状态机处理稍有不慎就会把引导码当成数据位。仿真的价值就在于这些边界是可以用逻辑分析仪直接看出来的不用等真机。协议本身很好记。引导码是 9ms 载波加 4.5ms 空闲逻辑 1 是 560us 载波加 1.69ms 空闲逻辑 0 是 560us 载波加 560us 空闲。数据帧由地址码、地址反码、命令码、命令反码组成每字节 8 位低位先发。真正影响解码准确性的不是这些数字本身而是接收端对时间间隔的容差判断。提示NEC 协议本身没有强制规定载波频率误差范围但接收头模型和真实 TSOP 系列通常按 38kHz 设计。仿真阶段载波频率不必精确到 38.0kHz解码端只关心包络的边沿间隔。2.2 Proteus 元件库选型AT89C51 与 TSOP4838元件选型直接决定仿真能不能跑起来以及后续移植代码时改动多少。这里用 AT89C51 而不是 STM32原因很实际Proteus 里 AT89C51 是原生模型程序加载稳定外部中断 INT0 和定时器 0 的行为和真实芯片一致而 STM32 在 Proteus 里的模型往往需要额外配置时钟树和外设对红外遥控仿真来说属于不必要的复杂度。红外接收头选 TSOP4838元件库搜索时输入 TSOP 就能看到常用型号。它的输出是集电极开路结构有载波时输出低电平无载波时输出高电平这个反相特性直接决定了外部中断的触发边沿选择。很多教程里把接收头输出接 P3.2同时把单片机 P3.2 设置为下降沿触发的外部中断 0正好匹配。这里有一个容易踩的选型误区不要选那种自带载波解调功能的“红外一体接收头”符号因为 Proteus 里有些红外发射管符号只是发光二极管不会产生 NEC 协议波形。我们的做法是让第二块 AT89C51 用普通 IO 口模拟整个红外发射过程然后把输出信号直接送进 TSOP4838 的输入端。2.3 在 Proteus 8 Professional 里搭最小 DSN 电路完整的最小系统由两块 AT89C51 组成第一块做遥控发射第二块做解码和显示。如果只验证解码逻辑可以先只放接收端但为了让整个链路能反复发送建议两块芯片都放上。元件清单如下元件数量作用AT89C512一块模拟遥控器一块做解码TSOP48381红外接收头输出接 INT012MHz 晶振2两个单片机各用一颗30pF 电容4晶振负载电容10k 电阻2复位上拉10uF 电解电容2复位电路共阴数码管1显示命令码220 电阻8数码管段选限流连线时注意几点第一块单片机的 P1.0 作为红外发射输出直接连到 TSOP4838 的输入脚第二块单片机的 P3.2 接 TSOP4838 输出脚P0 口接数码管的 a 到 dp 段且要接上拉或限流电阻数码管的位选接 P2.0。两个晶振电路各自独立不要把复位电路共用。保存时把工程文件命名为 IR_REMOTE.DSN。Proteus 8 的工程文件由 .pdsprj 和 .DSN 共同组成但单独复制 .DSN 出来也能被老版本打开这也是“代码 DSN”打包交付时最常见的文件组织方式。2.4 用第二块 AT89C51 当遥控器发射代码生成 NEC 波形发射端的做法是用普通 IO 口直接输出“高电平代表红外 LED 亮”的波形。在仿真里这个波形被 TSOP4838 模型当作遥控信号处理省去了真实红外发射管的驱动电路。代码的核心是定时器或延时函数控制载波的持续时长。#include AT89X51.H #include INTRINS.H #define IR_OUT P1_0 void delay_us(unsigned int n) { unsigned int i; for (i 0; i n; i) { _nop_(); _nop_(); } } void send_carrier(unsigned int us) { unsigned int t us / 26; // 38kHz 载波半周期约 13us bit level 0; while (t--) { IR_OUT level; level ~level; delay_us(13); } } void send_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { if (dat 0x01) { send_carrier(560); // 逻辑 1: 560us 载波 delay_us(1690); // 空闲 1.69ms } else { send_carrier(560); // 逻辑 0: 560us 载波 delay_us(560); // 空闲 560us } dat 1; // LSB first } } void send_frame(unsigned char addr, unsigned char cmd) { send_carrier(9000); // 引导码 9ms delay_us(4500); // 4.5ms 空闲 send_byte(addr); send_byte(~addr); send_byte(cmd); send_byte(~cmd); send_carrier(560); // 停止位 IR_OUT 0; } void main(void) { while (1) { send_frame(0x00, 0x45); // 每次发送后间隔一会儿再发下一条 delay_us(20000); } }这段代码里最关键的是 send_carrier 和 send_byte 两个函数。send_carrier 用翻转 IO 的方式产生近似 38kHz 的载波周期约 26ussend_byte 按 LSB first 顺序逐位发送每一位的前半段都是 560us 载波后半段由数据决定是 560us 还是 1690us。注意 delay_us 在 Keil 编译后实际延时不一定精确等于参数值因为循环体里nop的开销会叠加所以参数需要按逻辑分析仪实测结果微调。发射端代码放到第一块 AT89C51 里第二块则跑解码程序。两块芯片之间只有一根信号线方便定位问题发射端有输出但解码端没反应就查这一根线发射端输出本身没有就查发送函数。3. 红外解码代码实现外部中断 定时器测脉宽3.1 为什么选外部中断 INT0 而不是轮询 IO最常见的错误做法是在主循环里不停地读 P3.2然后通过延时函数判断高电平持续时间。这种轮询方式在真实系统中容易漏数据因为红外帧只有 20 多毫秒期间如果主循环里出现 LCD 刷新或按键扫描就会错过边沿。正确做法是让外部中断 0 捕捉下降沿进入中断后立刻读取定时器 0 的计数值用相邻两次下降沿之间的时间差来判断数据位。这样即使主循环在做别的事情也不影响边沿采集。定时器 0 采用方式 116 位计数12MHz 晶振下每个计数脉冲是 1us最大可测量 65535us足够覆盖 13.5ms 的引导码。3.2 Keil C51 完整解码代码接收端完整代码分成三部分初始化函数、外部中断 0 服务函数、主循环显示函数。中断服务里只做边沿测量和数据位判断显示放在主循环避免中断占用过长。#include AT89X51.H #define IR_IN_PIN P3_2 static unsigned int last_time 0; static unsigned long recv_buf 0; static unsigned char bit_cnt 0; static unsigned char new_flag 0; static unsigned char cmd_code 0; void init_timer0(void) { TMOD 0xF0; TMOD | 0x01; // 定时器 0 方式 116 位 TH0 0; TL0 0; TR0 1; // 启动定时器 ET0 0; // 不用定时器中断 } void decode_nec_data(void) { // 接收缓冲区从低位开始存放 // 第 16 位到第 23 位是命令码 if ((recv_buf 0xFF) 0x0D) // 反码检查可简化为地址反码 { cmd_code (unsigned char)((recv_buf 8) 0xFF); new_flag 1; } } void ext0_isr(void) interrupt 0 { unsigned char th, tl; unsigned int now, diff; TR0 0; // 读取前先停表 th TH0; tl TL0; now (th 8) | tl; TH0 0; TL0 0; TR0 1; diff now - last_time; last_time now; if (diff 10000) { // 引导码间隔约 13.5ms bit_cnt 0; recv_buf 0; return; } if (diff 600 || diff 3000) { // 无效间隔丢弃 return; } if (diff 1700) { recv_buf (recv_buf 1) | 1; // 逻辑 1 } else { recv_buf (recv_buf 1) | 0; // 逻辑 0 } bit_cnt; if (bit_cnt 32) { decode_nec_data(); bit_cnt 0; } } void init_display(void) { P0 0xFF; P2_0 1; } void display_hex(unsigned char val) { unsigned char code tab[] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E }; P0 tab[val 4]; P2_0 1; // 简单静态显示高四位和低四位交替无法体现这里只显示高四位 } void main(void) { init_timer0(); init_display(); EX0 1; // 使能外部中断 0 IT0 1; // 下降沿触发 EA 1; while (1) { if (new_flag) { new_flag 0; display_hex(cmd_code); } } }代码里的核心逻辑在中断服务函数。TSOP4838 输出低电平对应有载波因此每次下降沿表示载波开始。两次下降沿之间的间隔就是“载波时长 空闲时长”引导码约 13.5ms逻辑 1 约 2.25ms逻辑 0 约 1.12ms。diff 大于 10000 判定为引导码清空接收缓冲diff 在 600 到 3000 之间才处理数据位大于 1700 判定为逻辑 1否则为逻辑 0。注意这种方式利用的是边沿间隔和图里的电平极性无关。所以即使把接收头换成输出反相的型号只要仍用下降沿触发解码逻辑不用改。3.3 定时器初值与阈值参数是怎么确定的定时器采用 16 位方式初值清 0 后直接开始计数。12MHz 晶振下机器周期是 12 除以 12MHz等于 1us所以定时器每加 1 就是 1us。13.5ms 引导码对应的计数值约 13500逻辑 0 约 1125逻辑 1 约 2250。时间间隔12MHz 计数值判定逻辑引导码 13.5ms约 13500diff 10000逻辑 0 1.12ms约 1125600 diff 1700逻辑 1 2.25ms约 22501700 diff 3000停止位后长时间空闲110msdiff 10000 复位状态阈值定在 1700us 而不是死守 1690us是因为编译器的延时函数本身有误差Proteus 的逻辑分析仪显示的结果和理论值之间会有几十微秒偏差。1700 区分逻辑 0 和逻辑 110000 区分引导码和数据位这两个阈值留出了足够容差。3.4 编译 hex 并加载到 DSN最小可运行流程在 Keil C51 里新建工程时选择 AT89C51 芯片不一定要完全匹配只要内核是 8051 即可。编译前把晶振频率设置成 12MHz因为定时器计数公式依赖这个参数。生成 hex 文件的步骤是点击 Target Options在 Output 选项卡勾选 Create HEX File重新 Build。打开 Proteus 后双击第二块 AT89C51在 Program File 一栏选择刚才生成的 hex 文件。第一块发射芯片同样加载发射程序。加载完成后点击左下角的运行按钮如果一切正常数码管会显示发射端发送的命令码。注意新建 Proteus 工程时如果选择了第三方编译器程序文件加载位置可能会被自动清空每次打开 DSN 前检查一下两处 Program File 设置。4. 仿真调试的 3 个必调参数与常见坑位4.1 晶振频率12MHz 与 11.0592MHz 的定时器阈值换算很多人把开发板上的 11.0592MHz 晶振直接搬进 Proteus结果发现定时器计数值全部变了。11.0592MHz 下机器周期是 12 除以 11.0592MHz约 1.085us同样的 2.25ms 间隔计数值变成约 2074而不是 2250。如果按 12MHz 的阈值判断逻辑 1 仍能落在区间内但边界余量变小。时间间隔12MHz 阈值11.0592MHz 阈值引导码判断 10000 10000逻辑 1 下限 1700 1700逻辑 0 下限 600 600实际上只要阈值保持 600、1700、10000 这三个值11.0592MHz 下也能工作。怕的是把阈值卡死在 2250us 附近11.0592MHz 时逻辑 1 的计数值 2074 就会判断失败。所以建议仿真统一用 12MHz逻辑分析仪看到的数字更好换算也方便跟真实模块对接时反推时序。4.2 TSOP4838 输出反相为什么编码会判反TSOP4838 的数据手册明确写了有载波时输出低无载波时输出高。这个反相会让不少人在看波形时产生误解发射端明明是 9ms 高电平载波接收头输出却是 9ms 低电平于是下意识想改成上升沿触发。改成上升沿触发不是不行但要注意上升沿和下降沿测得的边沿间隔是一样的。比如逻辑 1 的“560us 载波 1690us 空闲”下降沿之间是 2.25ms上升沿之间也是 2.25ms。如果改成上升沿触发只是把起点换到空闲段结束的位置间隔不变。所以不要因为波形反相就改动中断触发边沿出问题时先确认是“间隔判断”的问题还是“电平判断”的问题。4.3 发射端载波与发送间隔的参数设置发射代码里最值得调的两个参数是 send_carrier 的循环次数和发送帧之间的间隔。前者决定载波持续时长后者决定遥控器按下时是否会重复发送。真实遥控器在按键按住时每隔 110ms 会发一次重复码但仿真时不需要实现完整重复码只要在两次 send_frame 之间加一个 20ms 以上的延时避免解码端连续收到多帧。如果发送间隔太短比如小于解码端处理 32 位数据所需的时间会导致位计数错位。解码端收到半帧数据后再次收到引导码时diff 超过 10000 会强行复位但上一次的半截数据会造成一次错误的显示。表现为数码管偶尔跳一个无关值。遇到这种情况把发送循环里的 delay_us(20000) 增大到 30000 或更长。4.4 数码管不亮、连续跳键、无反应三个常见现象排查现象可能原因处理方式数码管完全不亮P0 未加上拉或数码管共阴共阳选错P0 口每个引脚接 220 欧电阻到数码管段位按键按住时连续跳变发送间隔太短解码端收到连续帧把发射端的帧间隔加到 30ms 以上无反应但复位正常第一块芯片未加载 hex 或信号线没连到 TSOP4838 输入双击两块芯片确认 Program File检查 P1.0 到接收头输入端的连线排查时效率最高的做法是先在发射端发一个固定码比如 0x45然后解码端收到后点亮一个 LED。这个最小化联调程序能排除显示部分的问题把故障范围缩小到中断测脉宽这一步。// 最小联调发射端循环发 0x00 0x45 帧 // 接收端主循环里收到命令码 0x45 后点亮 P1.7 LED void main(void) { unsigned char last 0x00; init_timer0(); EX0 1; IT0 1; EA 1; P1_7 1; while (1) { if (new_flag) { if (cmd_code 0x45) { P1_7 0; // 点亮 LED } new_flag 0; } } }这段代码只保留了解码和三行判断逻辑如果 LED 能点亮说明中断测量和阈值判断都没问题故障一定在后面的显示代码里。如果 LED 不亮再用逻辑分析仪看 INT0 引脚有没有波形有波形就查阈值参数没波形就查发射端是否在正常工作。5. 用逻辑分析仪验证 NEC 时序把 DSN 调成可交付状态5.1 挂探针看 INT0 波形并比照边沿间隔表Proteus 8 Professional 运行仿真后界面底部有逻辑分析仪入口。打开后直接把 INT0 引脚拖进通道就能看到实时波形。读波形时不要盯着高低电平看只看下降沿之间的间距。一个完整数据帧里应该先看到约 13.5ms 的引导码间隔然后连续 8 个约 2.25ms 或 1.12ms 的数据位间隔最后是停止位。如果发现引导码间隔只有 9ms 左右说明测量的是“载波开始到载波结束”的宽度而不是下降沿到下降沿的间隔。这种情况通常是因为中断服务里读取定时器的顺序有问题先清零后读取导致把低电平时间当成了间隔。正确顺序是先把 TH0、TL0 读出来再清零。验证通过后阈值参数的调整就有了依据。逻辑分析仪显示实测间隔为 1.15ms 的位较多可以把逻辑 0 的上限从 1700 微调到 1600如果引导码间隔实测只有 12.8ms百分百是发射端延时函数偏短去调 send_frame 里的 4500 微秒参数。提示验证时可以把发射端代码临时改成只循环发引导码和 0x55 数据这样波形上会看到规律的 1.12ms、2.25ms 交替最容易暴露阈值区间没对齐的问题。5.2 用“实测值反推参数”而不是“理论值硬套”Keil C51 的空循环延时函数经过优化后实际延时往往比计算值小。我第一次搭这套仿真时send_carrier(9000) 实际只输出了约 7.2ms逻辑分析仪一眼就能看出来。修改方式很简单把 send_carrier 里的循环次数加大 20%再看波形直到引导码间隔落在 13.5ms 附近。这个“看波形、调参数、再看波形”的循环在 Proteus 里比真实硬件快得多因为不需要反复烧录和接线。调好发射端后解码端阈值可以继续沿用 600、1700、10000不需要为了迁就发射端的误差而改数字因为这三个阈值有足够的容差覆盖 10% 左右的偏差。最后确认波形合格后hex 文件和 DSN 工程放同一目录把发射代码、解码代码、DSN 三个文件一起归档这套红外遥控仿真就能直接给别人复现了。本文还有配套的精品资源点击获取