51单片机红外解码数码管显示:NEC协议与定时器中断实战解析
简介面向51单片机学习者和电子工程入门者这套红外解码数码管显示实例聚焦遥控信号的接收、解码与可视化显示适合在智能家居、遥控器控制等场景中理解单片机中断、定时器与I/O端口的实际应用。资源共11个文件涵盖C语言源码.c、Keil工程文件.uv2/.plg/.opt、编译生成文件.hex/.m51/.obj/.lst及备份文件压缩包整体仅15KB内容精简、便于直接查看和烧录验证。已有291人学习对初学者具有较好的实践参考价值。实例完整展示了红外遥控协议中脉冲宽度测量与解码算法帮助理解NEC等常见编码的数据格式和帧结构同时讲解定时器与中断协作捕获短长脉冲并识别逻辑0/1再通过数码管动态扫描实现数值显示在节省I/O资源的同时完成直观反馈。项目从电路连接、C语言程序设计到仿真调试均有覆盖包含实验电路接线要点与代码注释适合对照源码逐步理解红外解码、显示驱动和单片机外设协作的完整思路为后续嵌入式开发打下坚实基础。1. 红外解码数码管显示先弄清红外遥控器在发什么很多人拿到“51单片机教程实例117-红外解码数码管显示”这个题目第一反应是赶紧抄一段延时解码代码通电后对着接收头按遥控器期待数码管直接跳出键值。但实际做的时候会发现要么一个键都不亮要么显示乱码。原因很简单红外接收头输出的不是现成的二进制数据而是一串宽度不同的低电平脉冲真正的信息藏在每个脉冲的时间长度里。解码的本质就是测量这些时间宽度再把宽度映射成 0 和 1。这篇文章会从 NEC 协议的时间参数讲起用外部中断加定时器的方式在 51 单片机上实现解码最后把数据码和用户码显示到数码管上。整个过程不依赖复杂的外设一个接收头、一个共阴数码管、一块 51 开发板就能跑通。适合刚学完定时器和中断的入门者也适合在 Proteus 仿真正常但实物不通的时候回头看时序的人。先把协议层看明白再写代码就不会被各种“差 100us”的问题牵着走。2. NEC协议与51单片机最短解码路径定时器加外部中断2.1 为什么选 NEC 协议而不是 RC5 或 Sony SIRC市面上绝大多数家电遥控器走的是 NEC 协议少量设备用 RC5、Sony SIRC 或 Sharp 协议。NEC 被用得最多是因为它的编码方式对单片机非常友好每个数据位要么是 1.12ms 的高电平时序要么是 2.25ms 的高电平时序中间足够容错。常见的 NEC 数据帧包含起始码、8 位用户码、用户反码、8 位数据码、数据反码共 32 位数据。接收头解调出来以后空闲是高电平有信号时拉低所以所有信息都体现在“低电平多久、高电平多久”上。如果选 RC5它是曼彻斯特编码每一位中间有一次跳变要求单片机在位的中心点精确采样51 的普通定时器也能做但代码复杂度高不少。Sony SIRC 协议则是变长帧有 12、15、20 位三种格式解码逻辑要额外判断位数。做“红外解码数码管显示”这类入门实例NEC 是最短路径只需要测量相邻下降沿之间的时间间隔就能区分起始码、数据 0 和数据 1。下表是 NEC 协议在 38kHz 载波下的典型时间参数后面写代码判断阈值全靠这张表。信号段低电平时间高电平时间总周期说明起始码9ms4.5ms13.5ms每一帧的开始数据 0560us560us1.12ms逻辑 0数据 1560us1.69ms2.25ms逻辑 1重复码9ms2.25ms11.25ms长按按键时连续发送这里有个容易踩的坑接收头输出的低电平是载波检波后的结果不是原始 38kHz 方波。所以示波器或者逻辑分析仪看到的是以上表格里的低/高电平而不是 38kHz 正弦波。2.2 用 INT0 加定时器 0 测量相邻下降沿间隔2.2.1 外部中断下降沿捕获接收头输出脚接在单片机的 P3.2 也就是 INT0 上。空闲时引脚为高收到起始码后先拉低 9ms然后抬高 4.5ms接着再拉低进入第一个数据位。只要配置 INT0 为下降沿触发那么每一个“高到低”的跳变都会进中断。在中断里读取定时器当前值减去上一次进中断时读到的值就能得到两次下降沿之间的时间。这个时间本质上是“上一段低电平 上一段高电平”的总和正好可以直接和表格中的总周期比对。这种测量方式比“先测低电平再测高电平”简单因为它不需要在中断里反复切换触发边沿也不需要在主循环里轮询引脚。代价是它无法区分连续两个同向变化但 NEC 协议本身就是电平交替变化的所以没有这个问题。2.2.2 定时器 0 的微秒计时配置51 单片机标准 12T 模式下机器周期等于 12 除以晶振频率。如果晶振是 12MHz那么一个机器周期正好是 1us。11.0592MHz 晶振下一个机器周期约 1.085us。为了方便计算实例代码通常默认用 12MHz因为定时器计数器的累加值可以直接当作微秒数来用。配置定时器 0 为模式 1也就是 16 位计时器不开启任何定时器中断void Timer0_Init(void) { TMOD 0xF0; // 清空定时器 0 的模式位 TMOD | 0x01; // 定时器 0 模式 116 位不自动重装 TH0 0; TL0 0; // 从 0 开始计数 ET0 0; // 不使用定时器 0 中断 TR0 1; // 启动定时器 0 }参数说明ET0 0很关键。我们不需要定时器溢出时做任何事只把它当作一个自由运行的计数器。TMOD 0xF0是为了不影响定时器 1 的配置TMOD | 0x01只把低四位设置成 0001。每次外部中断里把TH0和TL0清零让计时从当前下降沿重新开始下一次下降沿再读回数值这样得到的就是间隔时间。2.3 解码状态机的四种状态有了时间间隔后需要维护一个简单状态机。初始状态叫“等待起始码”。当检测到间隔大于 10ms典型起始码 13.5ms 或重复码 11.25ms时进入“接收数据位”状态同时把位计数器清零。之后每来一次下降沿就根据间隔大小判断当前 bit 是 0 还是 1送入移位寄存器。收集满 32 位后进入“校验”状态。状态切换可以用一个bit_count变量完成不需要复杂的状态枚举但逻辑上要明确这四种状态空闲、发现起始码、收数据、校验完成。校验完成后如果用户码反码和用户码按位取反一致、数据反码也一致就认为这一帧有效。如果中间出现大于 10ms 的间隔则重新回到等待起始码状态。需要提醒的是起始码和重复码都满足“间隔大于 10ms”的条件。重复码后面没有 32 位数据所以当它出现时状态机会在收数据位阶段因为再次收到大间隔而复位这并不会破坏已经解码完成的旧数据数码管可以继续显示上一次的值。这正好符合大多数红外遥控产品的行为按键长按时数据不重复刷新。3. 用 Keil C51 写解码和数码管显示可复制的核心代码3.1 硬件连接与引脚分配常见做法是开发板上已经焊好红外接收头和四位一体数码管。如果没有按下面表格接线即可。模块引脚连接到红外接收头OUTP3.2 / INT0红外接收头VCC5V红外接收头GNDGND共阴数码管A-G、DPP0.0-P0.7 通过 220 欧电阻数码管位选 1十位P2.0数码管位选 2个位P2.1注意如果开发板的数码管是共阴且已经配上三极管位选驱动那么位选引脚逻辑可能是低电平有效需要根据原理图调整。实例代码里默认高电平选中。3.2 红外解码中断函数与参数说明下面这段代码是解码核心。晶振按 12MHz 计算定时器计数值直接当作微秒使用。中断每次由下降沿触发读取TH0和TL0与上次记录值相减得到间隔时间然后按照阈值判断 bit。#define IR_START_TH 10000 // 间隔大于 10ms 认为是起始码或重复码 #define IR_BIT1_TH 1600 // 间隔大于 1.6ms 认为是数据 1 #define IR_BIT_MIN 600 // 小于 0.6ms 直接丢弃按噪声处理 volatile unsigned char ir_addr; volatile unsigned char ir_data; volatile bit ir_ok 0; void Int0_IR(void) interrupt 0 { static unsigned int last_time 0; static unsigned char bit_cnt 0; static unsigned long irdat 0; unsigned int now; unsigned int diff; now (TH0 8) | TL0; TH0 0; TL0 0; diff now - last_time; last_time now; if (diff IR_START_TH) { // 起始码或重复码重新累积数据 bit_cnt 0; irdat 0; return; } if (bit_cnt 32 diff IR_BIT_MIN) { irdat 1; if (diff IR_BIT1_TH) { irdat | 1; } bit_cnt; if (bit_cnt 32) { // 校验用户码反码和数据反码 unsigned char uc irdat 24; unsigned char uc_ irdat 16; unsigned char dc irdat 8; unsigned char dc_ irdat; if (uc (unsigned char)~uc_ dc (unsigned char)~dc_) { ir_addr uc; ir_data dc; ir_ok 1; } } } }逻辑说明第一次检测到间隔大于 10ms就判定为起始码然后清除位计数器。接下来每个下降沿会读到一个 1.12ms 或 2.25ms 的间隔对应数据 0 和数据 1。irdat不断左移高位先接收符合 NEC 协议先发用户码低字节的顺序。32 位收满后把字节拆出来做反码校验。参数说明IR_BIT1_TH取 1.6ms 是数据 0 总周期 1.12ms 和数据 1 总周期 2.25ms 的中间值。IR_BIT_MIN是噪声滤除阈值实际测试中可能因为信号抖动出现很短的间隔直接忽略。TH0和TL0清零语句要放在读取now之后否则读取到的值已经是清零后的。last_time在空闲时也需要更新否则起始码前的噪声会干扰第一次间隔计算。3.3 数码管显示函数与动态刷新共阴数码管段码表按0-F排列。显示两位数时最简单的是动态扫描。下面的函数接受一个八位数据高四位和低四位分别作为十位和个位显示code unsigned char seg_code[16] { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71 }; void DisplayByte(unsigned char dat) { P2 0x01; P0 seg_code[dat 4]; DelayMs(5); P2 0x02; P0 seg_code[dat 0x0F]; DelayMs(5); }参数说明dat 4是高 4 位用作十位dat 0x0F是低 4 位用作个位。P2 0x01打开十位位选P2 0x02打开个位位选。DelayMs(5)表示每个位停留约 5ms两个位加起来 10ms 刷新一轮肉眼不会看到闪烁。如果位数增加到四位就把位选口扩展成四个每轮停留时间缩短到 2ms 左右。3.4 主循环里把解码结果搬到数码管主程序只需要初始化定时器、配置 INT0、然后循环刷新数码管。解码完成后置位ir_ok主循环检测到后把ir_data保存到显示缓冲区并清除ir_ok避免同一帧数据被重复处理。void main(void) { unsigned char showed 0; Timer0_Init(); IT0 1; // INT0 下降沿触发 EX0 1; // 打开外部中断 0 EA 1; // 打开总中断 while (1) { if (ir_ok) { showed ir_data; ir_ok 0; } DisplayByte(showed); } }这里主循环每时每刻都在扫描数码管。中断发生时会打断扫描但因为动态扫描周期只有 10ms短暂打断不会造成明显暗影。showed初始化为 0上电后数码管显示 00收到遥控指令后显示数据码。4. Proteus 仿真和实物调试量出来的才是真信号4.1 在 Proteus 里搭红外遥控解码仿真图如果要先做仿真常见做法是从元件库里放置IRREMOTE和TSOP1738。IRREMOTE是一个带按键的遥控器模型点击按键就会输出调制波形TSOP1738是红外接收头模型解调后输出到单片机。连线时把TSOP1738的 OUT 脚接 P3.2VCC 和 GND 接好。要注意仿真模型有时默认接收头输出极性相反如果程序收不到有效数据可以先在接收头输出脚接一个上拉电阻到 VCC再把程序里的last_time更新逻辑检查一遍。Proteus 仿真的好处是可以直接放一个示波器在接收头输出端观察每次按键后的波形。很多同学仿真失败是因为晶振频率设置成了 11.0592MHz但代码里的阈值是按 12MHz 算的。仿真模型里双击单片机把晶振频率改成 12MHz重启仿真后再试。4.2 仿真时解码失败的常见原因仿真失败通常集中在三个地方。一是遥控器模型没有和接收头对准Proteus 里有些版本要求IRREMOTE的发射角度朝向接收头否则信号无法到达。二是接收头输出脚缺少上拉电阻仿真模型里有些输出脚是弱上拉实测波形会变形。三是按键按下的时间太短仿真中IRREMOTE默认按一次只发一帧如果程序里的中断还没来得及收完所有位就会因为下一个间隔超时而丢弃。一个实用的检查方法是临时把diff变量的高字节和低字节分别送到 P1 口和 P2 口跑仿真时通过虚拟终端或者逻辑探针观察数值。比如在中断里加入P1 (unsigned char)(diff 8); P2 (unsigned char)(diff);这样每次下降沿后 P1/P2 就会更新可以直观看到起始码间隔附近的数据是不是 0x35 左右。确认间隔准了再改回数码管显示。4.3 实物调试时用串口辅助打印在实物上调试时只靠数码管很难判断中断到底有没有进去。这时串口是最好用的调试工具。用 USB-TTL 模块连接开发板的串口初始化串口 1然后把ir_data和diff发到电脑串口助手。初始化串口的代码void UART_Init(void) { SCON 0x50; // 串口模式 1允许接收 TMOD 0x0F; TMOD | 0x20; // 定时器 1 模式 28 位自动重装 TH1 0xFD; // 11.0592MHz 波特率 9600 的初值 TL1 0xFD; TR1 1; } void SendByte(unsigned char dat) { SBUF dat; while (!TI); TI 0; }注意这里晶振是 11.0592MHz而解码阈值是按 12MHz 算的。两种频率同时出现在一个项目里没问题但串口波特率必须按 11.0592MHz 计算否则乱码。如果开发板用的是 12MHz 晶振串口 9600 波特率会有误差可以把TH1改成 0xF3 附近但最好用 11.0592MHz 晶振。调试时在ir_ok置位后调用SendByte(ir_data)然后用串口助手接收和数码管显示值一比能快速确定是解码问题还是显示问题。5. 验证红外解码是否准确的三个技巧逻辑分析仪、重复码和显示校验5.1 用逻辑分析仪抓 NEC 波形如果手头有逻辑分析仪把它接到接收头 OUT 引脚和 GND采样率设成 1MHz 就足够。按下遥控器后逻辑分析仪上能清楚看到 9ms 的低电平、4.5ms 的高电平以及后面一串 1.12ms 或 2.25ms 的周期。这里最容易犯的错误是拿着逻辑分析仪去量接收头之前的光信号那个位置有 38kHz 载波必须量解调后的 OUT 输出。抓到波形后从第一个下降沿开始数 32 位把每一位的宽度和 NEC 参数表对照如果每一位都能对得上解码程序基本不会有问题。如果波形宽度偏差超过 20%先检查接收头供电电压和滤波电容很多实物问题都出在电源纹波上。5.2 处理重复码和按键保持长按遥控器按键时NEC 协议会发送重复码也就是 9ms 低电平加 2.25ms 高电平后面没有数据位。上面第 2 章的状态机在遇到重复码时会回到起始状态但不会清除已经解码成功的ir_data所以数码管一直保持上次的值。如果你希望长按能触发连续动作比如音量持续增加那就需要单独识别重复码。可以在中断里加一个重复码标志bit ir_repeat 0; // 在 diff IR_START_TH 的判断分支里 if (diff 10000 diff 12000) { ir_repeat 1; } else { ir_repeat 0; }主循环检测ir_repeat后可以配合定时器判断按键按住了多长时间超过 500ms 就每秒执行一次比如让显示的数字加一。不过要注意ir_repeat必须在主循环处理后手动清掉否则下一帧数据到来时还会误判。5.3 用数据反查键值表最后一个是所有红外项目都要做的事建立键值表。不同品牌遥控器的数据码完全不同同一个遥控器上每个按键的数据码也不同。把红外接收头对准遥控器按一个键记录数码管显示的值做成一张表然后再在程序里用 switch 语句把数据码映射成功能。比如某款遥控器的实测数据按键数据码(Hex)10x4520x4630x47POWER0x45注意如果不同按键显示相同说明解码可能只解到了低 8 位而高 8 位用户码被忽略了。检查时把ir_addr也显示出来确认地址码稳定。正常 NEC 遥控器在同一品牌下地址码固定如果每次按同一个键地址码都在变大概率是中断里last_time没有更新或者位序反了导致校验失败。用“按两次看数据是否一致”这一招能快速把问题定位到协议层还是电路层。本文还有配套的精品资源点击获取