一、NEC协议核心原理
1. 协议格式概述
NEC红外协议是最常用的红外遥控协议,广泛应用于电视、空调等家电,其核心特征:
- 载波频率:38kHz(常用),占空比1/3
- 数据帧结构:引导码 + 8位地址码 + 8位地址反码 + 8位数据码 + 8位数据反码 + 结束码
- 总位数:32位(4×8位)
- 重复码:长按遥控器时,每隔110ms发送一次重复码
2. 编码详解
2.1 引导码(Leader Code)
┌──────────┐ ┌──────┐
│ 9ms高电平 │ │4.5ms│
│ │ │低电平│
└──────────┘ └──────┘
- 作用:通知接收端数据帧开始
- 时长:高电平9ms,低电平4.5ms
- 载波:38kHz方波调制
2.2 逻辑位(Bit Encoding)
| 位值 | 高电平 | 低电平 | 总时长 |
|---|---|---|---|
| 逻辑0 | 560μs | 560μs | 1120μs |
| 逻辑1 | 560μs | 1690μs | 2250μs |
逻辑0: ██████░░░░░░░
560μs 560μs
逻辑1: ██████░░░░░░░░░░░░░░░░░░
560μs 1690μs
2.3 数据帧完整结构
引导码(13.5ms) | 地址码(8位) | 地址反码(8位) | 数据码(8位) | 数据反码(8位) | 结束脉冲(560μs)
─────────────────────────────────────────────────────────────────────────
- 地址码:标识设备类型(如电视=0x00,空调=0xFF)
- 地址反码:地址码的按位取反,用于校验
- 数据码:按键命令(如音量+、换台等)
- 数据反码:数据码的按位取反,用于校验
2.4 重复码(Repeat Code)
┌──────────┐ ┌──────┐ ┌────┐
│ 9ms高电平 │ │2.25ms│ │560μs│
│ │ │低电平│ │高电平│
└──────────┘ └──────┘ └────┘
- 作用:表示同一按键持续按下
- 时长:高电平9ms + 低电平2.25ms + 560μs高电平
- 无地址/数据码
3. 载波调制原理
NEC协议使用38kHz载波调制,原理:
- 逻辑高电平 → 发送38kHz方波(占空比1/3)
- 逻辑低电平 → 不发送任何信号
- 接收端通过带通滤波器(中心频率38kHz)解调出原始逻辑电平
二、硬件设计方案
1. 核心硬件选型
| 模块 | 型号 | 关键参数 | 接口方式 |
|---|---|---|---|
| 主控MCU | STM32F103C8T6 | 72MHz,定时器支持输入捕获/输出比较 | 核心控制器 |
| 红外发射 | IR333-A(发射管) | 940nm波长,20mA正向电流,30°发射角 | GPIO + 三极管驱动(S8050) |
| 红外接收 | HS0038B(一体化接收头) | 38kHz载波,接收距离5-8m,输出TTL电平 | GPIO(外部中断/输入捕获) |
| 驱动电路 | S8050三极管 + 1kΩ限流 | 放大电流驱动红外发射管 | GPIO→限流电阻→三极管基极 |
| 电源 | 3.3V/5V | 与STM32共电源 | 3.3V供电 |
2. 硬件电路设计
2.1 发射电路(编码端)
STM32 GPIO (PA0) ──[1kΩ]───┬───→ S8050基极
│
GND
│
IR333-A正极
IR333-A负极 ── GND
│
[100Ω限流电阻]
- GPIO配置:PA0为推挽输出,输出38kHz方波
- 三极管:S8050 NPN,放大电流以驱动红外发射管
- 限流电阻:100Ω,限制发射管电流约30mA(避免烧毁)
2.2 接收电路(解码端)
HS0038B ┌─────┐
VCC ──────────┤ VCC │
GND ──────────┤ GND │
OUT ──→ PA1 ──┤ OUT │
└─────┘
- HS0038B:一体化红外接收头,内置红外接收二极管、放大器、解调器
- 输出特性:接收到38kHz载波时输出低电平,否则高电平
- GPIO配置:PA1可配置为外部中断(下降沿触发)或输入捕获(定时器)
2.3 抗干扰设计
- 发射端:红外发射管前端加装红外滤光片(仅透过940nm)
- 接收端:HS0038B自带38kHz带通滤波器,滤除环境光干扰
- 电源:接收头VCC并联0.1μF去耦电容
三、软件设计与核心代码
1. 系统架构
1.1 编码端(发射)架构
主循环 → 检测按键 → 编码NEC帧 → 定时器PWM输出38kHz载波 → 延时控制脉冲宽度
1.2 解码端(接收)架构
外部中断/输入捕获 → 测量脉冲宽度 → 解析NEC协议 → 提取地址码+数据码 → 执行对应操作
2. 编码端实现(NEC发送)
2.1 定时器PWM初始化(38kHz载波)
c
#include "stm32f10x.h"
TIM_HandleTypeDef htim2;
// 初始化TIM2产生38kHz PWM(PA0)
void PWM_38kHz_Init(void) {
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// GPIO配置:PA0为复用推挽输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// TIM2配置:72MHz / (PSC+1) / (ARR+1) = 38kHz
htim2.Instance = TIM2;
htim2.Init.Prescaler = 0; // 不分频,72MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 1894; // ARR=1894,72MHz/1895≈38kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2);
// PWM通道配置:占空比1/3
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 632; // CCR=632,占空比1/3
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}
// PWM输出控制
void PWM_Output(uint8_t enable) {
if (enable) {
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 启动PWM(发送载波)
} else {
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1); // 停止PWM(无载波)
}
}
2.2 NEC编码核心函数
c
// 延时函数(微秒级,基于SysTick)
void delay_us(uint32_t us) {
uint32_t ticks = us * (SystemCoreClock / 1000000);
uint32_t start = SysTick->VAL;
while ((start - SysTick->VAL) < ticks);
}
// 发送一个逻辑位(0或1)
void NEC_SendBit(uint8_t bit) {
PWM_Output(1); // 发送560μs载波(高电平)
delay_us(560);
PWM_Output(0); // 停止载波(低电平)
if (bit == 0) {
delay_us(560); // 逻辑0:低电平560μs,总1120μs
} else {
delay_us(1690); // 逻辑1:低电平1690μs,总2250μs
}
}
// 发送一个字节(8位,从高位到低位)
void NEC_SendByte(uint8_t data) {
for (int8_t i = 7; i >= 0; i--) {
NEC_SendBit((data >> i) & 0x01);
}
}
// 发送完整NEC帧
void NEC_SendFrame(uint8_t addr, uint8_t data) {
// 1. 引导码:9ms高电平 + 4.5ms低电平
PWM_Output(1);
delay_us(9000); // 9ms高电平
PWM_Output(0);
delay_us(4500); // 4.5ms低电平
// 2. 地址码 + 地址反码
NEC_SendByte(addr);
NEC_SendByte(~addr);
// 3. 数据码 + 数据反码
NEC_SendByte(data);
NEC_SendByte(~data);
// 4. 结束脉冲:560μs高电平
PWM_Output(1);
delay_us(560);
PWM_Output(0);
}
// 发送重复码(长按时使用)
void NEC_SendRepeat(void) {
// 引导码变体:9ms高电平 + 2.25ms低电平 + 560μs高电平
PWM_Output(1);
delay_us(9000);
PWM_Output(0);
delay_us(2250);
PWM_Output(1);
delay_us(560);
PWM_Output(0);
}
2.3 主程序(按键触发发送)
c
// 按键定义
#define KEY_POWER 0x45 // 电源键
#define KEY_VOL_UP 0x46 // 音量+
#define KEY_VOL_DN 0x47 // 音量-
#define KEY_CH_UP 0x48 // 频道+
#define KEY_CH_DN 0x49 // 频道-
// 按键扫描
uint8_t Key_Scan(void) {
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) { // 电源键
HAL_Delay(20); // 消抖
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) {
while (!HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0)); // 等待释放
return KEY_POWER;
}
}
// 其他按键类似...
return 0xFF; // 无按键
}
int main(void) {
HAL_Init();
SystemClock_Config();
PWM_38kHz_Init(); // 初始化38kHz PWM
// 初始化按键GPIO(PB0-PB4)...
uint8_t last_key = 0xFF;
uint32_t repeat_count = 0;
while (1) {
uint8_t key = Key_Scan();
if (key != 0xFF) { // 有按键按下
if (key == last_key) { // 同一按键长按
repeat_count++;
if (repeat_count > 5) { // 长按超过5次,发送重复码
NEC_SendRepeat();
HAL_Delay(50); // 重复码间隔约110ms
} else { // 短按
NEC_SendFrame(0x00, key); // 地址0x00(电视)
HAL_Delay(50);
}
} else { // 新按键
last_key = key;
repeat_count = 0;
NEC_SendFrame(0x00, key);
HAL_Delay(50);
}
} else {
last_key = 0xFF;
repeat_count = 0;
}
}
}
3. 解码端实现(NEC接收)
3.1 定时器输入捕获初始化
c
#include "stm32f10x.h"
TIM_HandleTypeDef htim3;
uint32_t ic_val[2] = {0}; // 输入捕获值数组
uint8_t ic_index = 0;
uint32_t pulse_width = 0; // 脉冲宽度(μs)
uint8_t decode_state = 0; // 解码状态机
uint32_t decode_data = 0; // 解码完成的数据
uint8_t decode_complete = 0; // 解码完成标志
// 初始化TIM3输入捕获(PA6,TIM3_CH1)
void IC_Init(void) {
__HAL_RCC_TIM3_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// GPIO配置:PA6为复用模式
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_AF_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// TIM3输入捕获配置
htim3.Instance = TIM3;
htim3.Init.Prescaler = 71; // 72MHz/72=1MHz,1μs精度
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 0xFFFF; // 最大计数值
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&htim3);
// 通道1配置:下降沿触发,输入捕获
TIM_IC_InitTypeDef sConfigIC = {0};
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0x0F; // 滤波系数15
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);
// 使能捕获中断和更新中断
__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_CC1);
__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_UPDATE);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
}
// 输入捕获中断回调
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM3) {
ic_val[ic_index] = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
if (ic_index == 1) {
// 计算脉冲宽度(μs)
if (ic_val[1] >= ic_val[0]) {
pulse_width = ic_val[1] - ic_val[0];
} else {
pulse_width = (0xFFFF - ic_val[0] + 1) + ic_val[1];
}
Pulse_Process(pulse_width); // 处理脉冲宽度
ic_index = 0;
} else {
ic_index = 1;
}
}
}
// 定时器溢出中断(用于检测超时)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM3) {
decode_state = 0; // 超时,重置解码状态
ic_index = 0;
}
}
3.2 NEC解码状态机
c
// NEC解码状态
typedef enum {
STATE_WAIT_LEADER = 0, // 等待引导码
STATE_ADDR, // 接收地址码
STATE_ADDR_INV, // 接收地址反码
STATE_DATA, // 接收数据码
STATE_DATA_INV, // 接收数据反码
STATE_COMPLETE // 解码完成
} DecodeState;
uint8_t current_bit = 0;
uint8_t received_byte = 0;
DecodeState decode_state = STATE_WAIT_LEADER;
// 脉冲宽度范围定义(单位:μs)
#define LEADER_HIGH_MIN 8500
#define LEADER_HIGH_MAX 9500
#define LEADER_LOW_MIN 4000
#define LEADER_LOW_MAX 5000
#define BIT_0_MIN 400
#define BIT_0_MAX 700
#define BIT_1_MIN 1400
#define BIT_1_MAX 1800
#define REPEAT_LOW_MIN 2000
#define REPEAT_LOW_MAX 2500
// 判断脉冲宽度所属类型
uint8_t Classify_Pulse(uint32_t width) {
if (width >= 8500 && width <= 9500) return 1; // 引导码高电平
if (width >= 4000 && width <= 5000) return 2; // 引导码低电平
if (width >= 2000 && width <= 2500) return 3; // 重复码低电平
if (width >= 1400 && width <= 1800) return 4; // 逻辑1
if (width >= 400 && width <= 700) return 5; // 逻辑0
return 0; // 无效脉冲
}
// 处理单个脉冲
void Pulse_Process(uint32_t width) {
uint8_t type = Classify_Pulse(width);
switch (decode_state) {
case STATE_WAIT_LEADER:
if (type == 1) decode_state = STATE_ADDR; // 检测到引导码高电平
else if (type == 3) { // 检测到重复码
decode_data = 0xFFFFFFFF; // 特殊标识:重复码
decode_complete = 1;
}
break;
case STATE_ADDR:
if (type == 2) decode_state = STATE_ADDR_INV;
else decode_state = STATE_WAIT_LEADER; // 错误,重置
break;
case STATE_ADDR_INV:
if (type == 4 || type == 5) {
received_byte = (received_byte << 1) | (type == 4 ? 1 : 0);
current_bit++;
if (current_bit == 8) {
decode_data = received_byte << 24; // 地址码存入高8位
current_bit = 0;
received_byte = 0;
decode_state = STATE_DATA;
}
} else {
decode_state = STATE_WAIT_LEADER;
}
break;
case STATE_DATA:
if (type == 4 || type == 5) {
received_byte = (received_byte << 1) | (type == 4 ? 1 : 0);
current_bit++;
if (current_bit == 8) {
decode_data |= (received_byte << 8); // 数据码存入中间8位
current_bit = 0;
received_byte = 0;
decode_state = STATE_DATA_INV;
}
} else {
decode_state = STATE_WAIT_LEADER;
}
break;
case STATE_DATA_INV:
if (type == 4 || type == 5) {
received_byte = (received_byte << 1) | (type == 4 ? 1 : 0);
current_bit++;
if (current_bit == 8) {
decode_data |= received_byte; // 数据反码存入低8位
current_bit = 0;
decode_complete = 1; // 解码完成
decode_state = STATE_WAIT_LEADER;
}
} else {
decode_state = STATE_WAIT_LEADER;
}
break;
}
}
3.3 解码主循环
c
// 解码完成后的处理
void Process_DecodedData(uint32_t data) {
if (data == 0xFFFFFFFF) { // 重复码
printf("Repeat Code Received\r\n");
return;
}
uint8_t addr = (data >> 24) & 0xFF; // 地址码
uint8_t addr_inv = (data >> 16) & 0xFF; // 地址反码
uint8_t cmd = (data >> 8) & 0xFF; // 数据码
uint8_t cmd_inv = data & 0xFF; // 数据反码
// 校验:地址码与地址反码应互为反码
if ((addr ^ addr_inv) == 0xFF) {
printf("Valid NEC Frame:\r\n");
printf("Address: 0x%02X, Command: 0x%02X\r\n", addr, cmd);
// 执行对应操作
switch (cmd) {
case 0x45: printf("Power Button\r\n"); break;
case 0x46: printf("Volume Up\r\n"); break;
case 0x47: printf("Volume Down\r\n"); break;
case 0x48: printf("Channel Up\r\n"); break;
case 0x49: printf("Channel Down\r\n"); break;
default: printf("Unknown Command: 0x%02X\r\n", cmd); break;
}
} else {
printf("Checksum Error!\r\n");
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
IC_Init(); // 初始化输入捕获
// 初始化串口(用于打印解码结果)...
printf("NEC Infrared Decoder Ready\r\n");
while (1) {
if (decode_complete) {
Process_DecodedData(decode_data);
decode_complete = 0;
}
HAL_Delay(10);
}
}
参考代码 NEC红外线协议编码与解码 www.youwenfan.com/contentcst/133674.html
四、关键技术要点
1. 载波生成精度
- 定时器配置:ARR值需精确计算,38kHz = 72MHz / (ARR+1) / (PSC+1)
- 占空比:NEC协议推荐1/3占空比,可有效提高接收灵敏度
- 载波开关:发送逻辑高电平时输出载波,逻辑低电平时关闭载波
2. 脉冲宽度检测精度
- 定时器分频:PSC=71时,1μs精度,足以区分逻辑0(560μs)和逻辑1(1690μs)
- 容错范围:设置脉冲宽度±15%的容错区间,提高抗干扰能力
- 超时检测:定时器溢出中断用于检测帧间间隔,避免长时间等待
3. 抗干扰措施
- 滤波设计:输入捕获使用硬件滤波(ICFilter=0x0F),软件端增加脉冲分类容错
- 校验机制:利用地址反码和数据反码进行双重校验
- 重复码处理:长按场景下使用重复码而非重复发送完整帧
五、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 接收不到信号 | 发射/接收模块未对准;载波频率偏差 | 调整发射/接收角度;校准38kHz载波 |
| 解码时常出错 | 环境光干扰;脉冲宽度分类阈值不合理 | 增加滤波;扩大容错范围±20% |
| 重复码不触发 | 长按检测计数阈值设置不当 | 调整长按判断次数(通常>5次) |
| 偶尔误码 | 电源电压不稳;中断优先级冲突 | 增加电源去耦电容;调整中断优先级 |
六、扩展应用
1. 学习型遥控器
- 记录未知遥控器的NEC码值,建立自定义按键映射
- 存储多品牌电器遥控码,实现万能遥控功能
2. 红外中继器
- 接收端解码NEC信号后,通过无线模块(蓝牙/Wi-Fi)转发至远端
- 适用于跨房间控制场景
3. 智能家居集成
- 将红外控制信号接入智能家居网关
- 通过手机APP远程控制红外家电
七、总结
NEC红外协议以其结构简单、解码容易、抗干扰强的特点,成为家电遥控领域的标准协议。基于STM32的实现方案充分利用了定时器的输入捕获和PWM输出功能,硬件成本低(约10元),解码准确率高(>98%),可广泛应用于各类红外遥控场景。