第十八章 红外遥控实验(NEC协议)
1. 导入
红外遥控是家电最常见的人机交互方式。常用接收头(如 VS1838B、HS0038、TSOP1738)将38kHz调制的载波解调为电平脉冲,单片机只需"测时间、看长短"即可解码。本章以最普及的 NEC 协议为例,采用外部中断 + 定时器测时,完成地址/命令码解码与按键响应。
目标:
- 认识红外接收头与NEC时序;
- 配置 INT0(P3.2)捕获下降沿;
- 使用 Timer0 计时并按阈值解码0/1位;
- 识别引导码、重复码、32位数据,并验证反码;
- 用LED/串口显示解码结果,或触发功能。
2. 硬件设计
- 器件:51单片机(STC89C52 等)、红外接收头(VS1838B/HS0038/TSOP1738)、LED若干(或串口)。
- 连接(典型接法):
- 接收头
OUT→P3.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,下次上电仍有效。
通过本章,你已经掌握了红外遥控的核心解码技巧:用外部中断采边沿、用定时器测时间、用阈值分辨位型。结合此前的显示与执行部件,你可以快速构建完整的遥控控制系统。