51单片机基础-红外遥控(NEC协议)

第十八章 红外遥控实验(NEC协议)

1. 导入

红外遥控是家电最常见的人机交互方式。常用接收头(如 VS1838B、HS0038、TSOP1738)将38kHz调制的载波解调为电平脉冲,单片机只需"测时间、看长短"即可解码。本章以最普及的 NEC 协议为例,采用外部中断 + 定时器测时,完成地址/命令码解码与按键响应。

目标:

  • 认识红外接收头与NEC时序;
  • 配置 INT0(P3.2)捕获下降沿;
  • 使用 Timer0 计时并按阈值解码0/1位;
  • 识别引导码、重复码、32位数据,并验证反码;
  • 用LED/串口显示解码结果,或触发功能。

2. 硬件设计

  • 器件:51单片机(STC89C52 等)、红外接收头(VS1838B/HS0038/TSOP1738)、LED若干(或串口)。
  • 连接(典型接法):
    • 接收头 OUTP3.2/INT0
    • 接收头 VCC → +5V
    • 接收头 GND → GND
    • 建议近 VCC/GND 处加 0.1µF 去耦电容。
  • 注意:接收头输出为解调后信号,空闲高,收到载波时输出低电平脉冲。

3. NEC 协议速览

  • 帧结构:
    • 引导码:9ms 低 + 4.5ms 高(两次下降沿间隔约 13.5ms)
    • 数据:32位(LSB先传)= 地址(8) + 地址反码(8) + 命令(8) + 命令反码(8)
    • 每位:0/1 皆为 560µs 低,随后空闲高为
      • "0":约 560µs
      • "1":约 1.69ms
    • 重复码(长按):9ms 低 + 2.25ms 高 + 560µs 低(相邻下降沿约 11.25ms),不再发送32位数据。
  • 实际解码技巧:
    • 仅用"相邻下降沿时间间隔"即可区分:引导(≈13.5ms)、重复(≈11.25ms)、位0(≈1.125ms)、位1(≈2.25ms)。

4. 解码思路

  • 配置 INT0 为下降沿触发。每次下降沿进入中断:

    • 读取 Timer0 计数并清零,得到"与上次下降沿的间隔";
    • 首先判断是否为"引导码间隔"→ 进入接收状态,准备收32位;
    • 接着的每个间隔判断位宽:≈1.1ms 记为 0,≈2.25ms 记为 1(LSB先);
    • 收满32位后按反码校验,得到地址与命令;
    • 若识别到重复码,则认为是"上一个命令的长按重复"。
  • 计时基准:

    • 11.0592MHz 时,定时器每计数≈1.085µs。
    • 经验阈值(单位:计数tick):
      • 引导:1100014000(≈1214ms)
      • 重复: 950012500(≈1012.5ms)
      • 位0: 9001500(≈1.01.5ms)
      • 位1: 17002600(≈1.82.6ms)
    • 不同晶振/接收头可能略有差异,阈值可微调。

5. 完整示例代码(Keil C51,NEC解码 + 串口打印 + LED示例)

c 复制代码
#include <reg52.h>
#include <intrins.h>

/* 硬件定义 */
sbit LED = P1^0;      // 指示灯
// 红外接收头 OUT 接 P3.2(INT0),VCC=5V,GND=GND

/* 串口(用于打印解码结果,可选) */
void uart_init(void) {
    TMOD |= 0x20;     // T1方式2
    TH1  = 0xFD;      // 9600bps @11.0592MHz
    TL1  = 0xFD;
    TR1  = 1;
    SCON = 0x50;      // 8N1,允许接收
    ES   = 0;         // 此处不使用串口中断
    EA   = 1;
}
void uart_send_byte(unsigned char ch) {
    SBUF = ch; while(!TI); TI = 0;
}
void uart_send_str(const char *s){ while(*s) uart_send_byte(*s++); }
void uart_send_hex8(unsigned char v){
    const char hx[]="0123456789ABCDEF";
    uart_send_byte(hx[(v>>4)&0xF]); uart_send_byte(hx[v&0xF]);
}

/* 简单延时(演示用) */
void delay_ms(unsigned int ms) {
    unsigned int i,j;
    for(i=0;i<ms;i++)
        for(j=0;j<125;j++);
}

/* -------- NEC 解码相关 -------- */
/* 阈值(单位:Timer0 tick,~1.085us每tick) */
#define TH_LEAD_MIN  11000u
#define TH_LEAD_MAX  14000u

#define TH_REP_MIN    9500u
#define TH_REP_MAX   12500u

#define TH_BIT0_MIN    900u
#define TH_BIT0_MAX   1500u
#define TH_BIT1_MIN   1700u
#define TH_BIT1_MAX   2600u

#define TH_FRAME_GAP 20000u   // >20ms 视为无效/超时

/* 状态机 */
typedef enum {
    IR_IDLE = 0,
    IR_RECV_BITS
} ir_state_t;

volatile ir_state_t ir_state = IR_IDLE;
volatile unsigned char  ir_bit_index = 0;
volatile unsigned long  ir_shift = 0;   // 按LSB先方式装入
volatile bit            ir_new = 0;     // 获取到完整新码
volatile bit            ir_repeat = 0;  // 重复码标志
volatile unsigned char  ir_addr = 0, ir_addr_n = 0;
volatile unsigned char  ir_cmd  = 0, ir_cmd_n  = 0;
volatile unsigned char  last_cmd = 0, last_addr = 0;

/* 定时器与外部中断初始化:T0计时,INT0测下降沿 */
void ir_hw_init(void) {
    // Timer0: 16位方式1,清零启动作自由计数
    TMOD = (TMOD & 0xF0) | 0x01;
    TH0 = 0; TL0 = 0;
    TR0 = 1;

    // 外部中断0:下降沿触发
    IT0 = 1;    // 1=下降沿触发
    EX0 = 1;    // 允许INT0中断
    EA  = 1;    // 开总中断
}

/* INT0 中断服务:每次下降沿,读取间隔与状态机解码 */
void ext0_isr(void) interrupt 0 {
    unsigned int dur;
    // 读取本次距离上一次下降沿的tick数
    TR0 = 0;
    dur = ((unsigned int)TH0 << 8) | TL0;
    TH0 = 0; TL0 = 0;
    TR0 = 1;

    // 过长间隔,复位状态
    if (dur > TH_FRAME_GAP) {
        ir_state = IR_IDLE;
        ir_bit_index = 0;
        return;
    }

    // 1) 先判断是否引导码
    if (dur >= TH_LEAD_MIN && dur <= TH_LEAD_MAX) {
        ir_state = IR_RECV_BITS;
        ir_bit_index = 0;
        ir_shift = 0;
        return;
    }

    // 2) 判断是否重复码(长按)
    if (dur >= TH_REP_MIN && dur <= TH_REP_MAX) {
        ir_repeat = 1;
        ir_new = 1;      // 交给主循环处理(沿用上次命令)
        return;
    }

    // 3) 接收位数据
    if (ir_state == IR_RECV_BITS) {
        if (dur >= TH_BIT0_MIN && dur <= TH_BIT0_MAX) {
            // 位0:装入0
            // ir_shift |= (0UL << ir_bit_index); // 无需写
            ir_bit_index++;
        } else if (dur >= TH_BIT1_MIN && dur <= TH_BIT1_MAX) {
            // 位1:装入1
            ir_shift |= (1UL << ir_bit_index);
            ir_bit_index++;
        } else {
            // 无效脉宽,丢弃
            ir_state = IR_IDLE;
            ir_bit_index = 0;
            return;
        }

        // 收满32位,拆分与校验
        if (ir_bit_index >= 32) {
            unsigned char a, an, c, cn;
            a  =  (unsigned char)( ir_shift        & 0xFF);
            an =  (unsigned char)((ir_shift >> 8)  & 0xFF);
            c  =  (unsigned char)((ir_shift >> 16) & 0xFF);
            cn =  (unsigned char)((ir_shift >> 24) & 0xFF);

            // 反码校验(NEC:an=~a, cn=~c)
            if ((unsigned char)(a ^ an) == 0xFF && (unsigned char)(c ^ cn) == 0xFF) {
                ir_addr = a; ir_addr_n = an;
                ir_cmd  = c; ir_cmd_n  = cn;
                last_addr = a; last_cmd = c;
                ir_repeat = 0;
                ir_new = 1;
            }

            ir_state = IR_IDLE;
            ir_bit_index = 0;
        }
    }
}

/* -------- 应用层示例 -------- */
void main(void) {
    LED = 1;              // 低有效LED请按实际调整
    uart_init();          // 打开串口查看解码结果(可不用)
    ir_hw_init();

    uart_send_str("NEC IR Decode Start\r\n");

    while (1) {
        if (ir_new) {
            ir_new = 0;

            if (ir_repeat) {
                // 重复码:认为长按上次键
                uart_send_str("REPEAT CMD: 0x"); uart_send_hex8(last_cmd);
                uart_send_str(" ADDR: 0x");       uart_send_hex8(last_addr);
                uart_send_str("\r\n");

                // 示例:长按同样执行上次命令动作
                // 这里简单闪烁LED以示响应
                LED = !LED;
            } else {
                // 新的完整32位码
                uart_send_str("ADDR="); uart_send_hex8(ir_addr);
                uart_send_str(" CMD="); uart_send_hex8(ir_cmd);
                uart_send_str("\r\n");

                // 示例:根据命令码执行
                // 不同遥控器命令码不同,请先观察打印再映射
                // 例如:若某键CMD=0x45,切换LED
                if (ir_cmd == 0x45) {
                    LED = !LED;
                }
            }
        }

        // 其他任务......
    }
}

说明:

  • 将接收头 OUT 接到 P3.2(INT0)。若需改口,请同步修改外部中断配置。
  • 阈值为经验值,若解码不稳,请轻微调整 TH_* 宏。
  • 大多数遥控器不同键的命令码不同,先用串口观察,再按需映射到功能。

6. 调试与验证

  • 步骤:
    • 确认接收头接线正确、5V供电,靠近板上避免环境光干扰;
    • 打开串口助手(9600-8N1),复位单片机;
    • 对准接收头按遥控器按键,应看到形如 "ADDR=xx CMD=yy" 的打印;
    • 记录常用键的 CMD 值,在代码里映射到相应功能(LED/蜂鸣器/电机等)。
  • 常见问题:
    • 无打印:检查 INT0 是否使能,是否设置下降沿;接收头OUT是否接到P3.2。
    • 乱码/不稳定:阈值过松或过紧;接收头附近电源滤波不足;强光干扰。
    • 只有重复码:遥控器按下过快或只识别到重复帧;靠近一些再试,或加大引导阈值容差。
    • 反码校验失败:非NEC遥控或时序判定误差;尝试微调阈值,尤其是位0/1边界。

7. 扩展建议

  • 用定时器中断驱动数码管,实时显示 CMD 十六进制;
  • 支持多协议(如 NEC/RC5/Sony),通过首段时序特征判定选择解码器;
  • 将命令映射为系统菜单控制(模式切换、数值增减);
  • 加入按键学习功能:长按本地键将当前CMD存入 EEPROM,下次上电仍有效。

通过本章,你已经掌握了红外遥控的核心解码技巧:用外部中断采边沿、用定时器测时间、用阈值分辨位型。结合此前的显示与执行部件,你可以快速构建完整的遥控控制系统。


相关推荐
不见长安在3 小时前
Jvm资料整理
jvm·1024程序员节
如果丶可以坑3 小时前
maven无法获取依赖问题
java·maven·1024程序员节
子不语1803 小时前
STM32——按钮实验
stm32·单片机·嵌入式硬件
羊村里的大灰狼3 小时前
Windows下载安装配置rabbitmq
1024程序员节
B站计算机毕业设计之家3 小时前
Python手势识别检测系统 基于MediaPipe的改进SSD算法 opencv+mediapipe 深度学习 大数据 (建议收藏)✅
python·深度学习·opencv·计算机视觉·1024程序员节
兜兜风d'4 小时前
RabbitMQ 持久性详解
spring boot·分布式·rabbitmq·1024程序员节
MeowKnight9584 小时前
【Linux】常见的系统调用 函数和功能简单总结
linux·1024程序员节
游戏开发爱好者84 小时前
iOS 26 App 开发阶段性能优化 从多工具协作到数据驱动的实战体系
android·ios·小程序·uni-app·iphone·webview·1024程序员节
爱隐身的官人4 小时前
Ubuntu安装开源堡垒机JumpServer
linux·ubuntu·堡垒机·1024程序员节