ESP32 使用ESP-IDF 驱动红外遥控器源码分享

一、源码分享
1、效果展示

2、开发环境搭建
参考我这篇博文:VS Code 在线安装ESP-IDF,ESP32开发环境搭建详细教程
3、源码分享
infrared.h
c
#ifndef __RMT_NEC_RX_H
#define __RMT_NEC_RX_H
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "freertos/queue.h"
#include "driver/rmt_rx.h"
#include "esp_err.h"
/* 引脚定义 */
#define RMT_IN_GPIO_PIN GPIO_NUM_21 /* 连接RMT_RX_IN的GPIO端口 */
#define RMT_RESOLUTION_HZ 1000000 /* 1MHz 频率, 1 tick = 1us */
#define RMT_NEC_DECODE_MARGIN 200 /* 判断NEC时序时长的容差值,小于(值+此值),大于(值-此值)为正确 */
/* NEC 协议时序时间,协议头9.5ms 4.5ms 逻辑0两个电平时长,逻辑1两个电平时长,重复码两个电平时长 */
#define NEC_LEADING_CODE_DURATION_0 9000
#define NEC_LEADING_CODE_DURATION_1 4500
#define NEC_PAYLOAD_ZERO_DURATION_0 560
#define NEC_PAYLOAD_ZERO_DURATION_1 560
#define NEC_PAYLOAD_ONE_DURATION_0 560
#define NEC_PAYLOAD_ONE_DURATION_1 1690
#define NEC_REPEAT_CODE_DURATION_0 9000
#define NEC_REPEAT_CODE_DURATION_1 2250
/* 外部调用 */
extern QueueHandle_t receive_queue;
extern rmt_channel_handle_t rx_channel;
extern rmt_symbol_word_t raw_symbols[64];
extern rmt_receive_config_t receive_config;
extern uint16_t s_nec_code_address;
extern uint16_t s_nec_code_command;
/* 函数声明 */
esp_err_t rmt_nec_rx_init(void); /* RMT红外接收初始化 */
bool rmt_nec_parse_frame(rmt_symbol_word_t *rmt_nec_symbols); /* 将RMT接收结果解码出NEC地址和命令 */
bool rmt_nec_parse_frame_repeat(rmt_symbol_word_t *rmt_nec_symbols); /* 检查数据帧是否为重复按键 */
#endif
infrared.c
c
#include "infrared.h"
/* 保存NEC解码的地址和命令字节 */
uint16_t s_nec_code_address = 0x0000;
uint16_t s_nec_code_command = 0x0000;
QueueHandle_t receive_queue = NULL;
rmt_channel_handle_t rx_channel = NULL;
rmt_symbol_word_t raw_symbols[64]; /* 对于标准NEC框架应该足够 */
rmt_receive_config_t receive_config;
/**
* @brief RMT数据接收完成回调函数
* @param channel : 通道
* @param edata : 接收的数据
* @param user_data : 传入的参数
* @retval 返回是否唤醒了任何任务
*/
bool rmt_nec_rx_done_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data)
{
BaseType_t high_task_wakeup = pdFALSE;
QueueHandle_t receive_queue = (QueueHandle_t)user_data;
xQueueSendFromISR(receive_queue, edata, &high_task_wakeup); /* 将收到的RMT数据通过消息队列发送到解析任务 */
return high_task_wakeup == pdTRUE;
}
/**
* @brief RMT红外接收初始化
* @param 无
* @retval ESP_OK:初始化成功
*/
esp_err_t rmt_nec_rx_init(void)
{
ESP_ERROR_CHECK(gpio_reset_pin(RMT_IN_GPIO_PIN));
/* 配置接收通道 */
rmt_rx_channel_config_t rx_channel_cfg = {
.gpio_num = RMT_IN_GPIO_PIN, /* 设置红外接收通道管脚 */
.clk_src = RMT_CLK_SRC_DEFAULT, /* 设置RMT时钟源 */
.resolution_hz = RMT_RESOLUTION_HZ, /* 设置时钟分辨率 */
.mem_block_symbols = 64, /* 通道一次可以存储的RMT符号数量 */
};
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_channel_cfg, &rx_channel)); /* 创建接收通道 */
/* 配置红外接收完成回调 */
receive_queue = xQueueCreate(1, sizeof(rmt_rx_done_event_data_t)); /* 创建消息队列,用于接收红外编码 */
assert(receive_queue);
rmt_rx_event_callbacks_t cbs = {
.on_recv_done = rmt_nec_rx_done_callback, /* RMT信号接收完成回调函数 */
};
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_channel, &cbs, receive_queue)); /* 配置RMT接收通道回调函数 */
/* NEC协议的时序要求 */
receive_config.signal_range_min_ns = 1250; /* NEC信号的最短持续时间为560us,1250ns<560us,有效信号不会被视为噪声 */
receive_config.signal_range_max_ns = 12000000; /* NEC信号的最长持续时间为9000us,12000000ns>9000us,接收不会提前停止 */
/* 开启RMT通道 */
ESP_ERROR_CHECK(rmt_enable(rx_channel)); /* 使能RMT接收通道 */
ESP_ERROR_CHECK(rmt_receive(rx_channel, raw_symbols, sizeof(raw_symbols), &receive_config)); /* 准备接收 */
return ESP_OK;
}
/**
* @brief 判断数据时序长度是否在NEC时序时长容差范围内 正负REMOTE_NEC_DECODE_MARGIN的值以内
* @param signal_duration:信号持续时间
* @param spec_duration:信号的标准持续时间
* @retval true:符合条件;false:不符合条件
*/
inline bool rmt_nec_check_range(uint32_t signal_duration, uint32_t spec_duration)
{
return (signal_duration < (spec_duration + RMT_NEC_DECODE_MARGIN)) &&
(signal_duration > (spec_duration - RMT_NEC_DECODE_MARGIN));
}
/**
* @brief 对比数据时序长度判断是否为逻辑0
* @param rmt_nec_symbols:RMT数据帧
* @retval true:符合条件;false:不符合条件
*/
bool rmt_nec_logic0(rmt_symbol_word_t *rmt_nec_symbols)
{
return rmt_nec_check_range(rmt_nec_symbols->duration0, NEC_PAYLOAD_ZERO_DURATION_0) &&
rmt_nec_check_range(rmt_nec_symbols->duration1, NEC_PAYLOAD_ZERO_DURATION_1);
}
/**
* @brief 对比数据时序长度判断是否为逻辑1
* @param rmt_nec_symbols:RMT数据帧
* @retval true:符合条件;false:不符合条件
*/
bool rmt_nec_logic1(rmt_symbol_word_t *rmt_nec_symbols)
{
return rmt_nec_check_range(rmt_nec_symbols->duration0, NEC_PAYLOAD_ONE_DURATION_0) &&
rmt_nec_check_range(rmt_nec_symbols->duration1, NEC_PAYLOAD_ONE_DURATION_1);
}
/**
* @brief 将RMT接收结果解码出NEC地址和命令
* @param rmt_nec_symbols:RMT数据帧
* @retval true成功;false失败
*/
bool rmt_nec_parse_frame(rmt_symbol_word_t *rmt_nec_symbols)
{
rmt_symbol_word_t *cur = rmt_nec_symbols;
uint16_t address = 0;
uint16_t command = 0;
bool valid_leading_code = rmt_nec_check_range(cur->duration0, NEC_LEADING_CODE_DURATION_0) &&
rmt_nec_check_range(cur->duration1, NEC_LEADING_CODE_DURATION_1);
if (!valid_leading_code)
{
return false;
}
cur++;
for (int i = 0; i < 16; i++)
{
if (rmt_nec_logic1(cur))
{
address |= 1 << i;
}
else if (rmt_nec_logic0(cur))
{
address &= ~(1 << i);
}
else
{
return false;
}
cur++;
}
for (int i = 0; i < 16; i++)
{
if (rmt_nec_logic1(cur))
{
command |= 1 << i;
}
else if (rmt_nec_logic0(cur))
{
command &= ~(1 << i);
}
else
{
return false;
}
cur++;
}
/* 保存数据地址和命令,用于判断重复按键 */
s_nec_code_address = address;
s_nec_code_command = command;
return true;
}
/**
* @brief 检查数据帧是否为重复按键:一直按住同一个键
* @param rmt_nec_symbols:RMT数据帧
* @retval true:符合条件;false:不符合条件
*/
bool rmt_nec_parse_frame_repeat(rmt_symbol_word_t *rmt_nec_symbols)
{
return rmt_nec_check_range(rmt_nec_symbols->duration0, NEC_REPEAT_CODE_DURATION_0) &&
rmt_nec_check_range(rmt_nec_symbols->duration1, NEC_REPEAT_CODE_DURATION_1);
}
main.c
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/projdefs.h"
#include "led.h"
#include "ds18b20.h"
#include "infrared.h"
const char *TAG = "rmt_rx";
/**
* @brief 根据NEC编码解析红外协议并打印指令结果
* @param rmt_nec_symbols : 数据帧
* @param symbol_num : 数据帧大小
* @retval 无
*/
void rmt_rx_scan(rmt_symbol_word_t *rmt_nec_symbols, size_t symbol_num)
{
uint8_t rmt_data = 0;
uint8_t tbuf[40];
char *str = 0;
switch (symbol_num) /* 解码RMT接收数据 */
{
case 34: /* 正常NEC数据帧 */
{
if (rmt_nec_parse_frame(rmt_nec_symbols) )
{
rmt_data = (s_nec_code_command >> 8);
switch (rmt_data)
{
case 0xBA:
{
str = "POWER";
break;
}
case 0xB9:
{
str = "UP";
break;
}
case 0xB8:
{
str = "ALIENTEK";
break;
}
case 0xBB:
{
str = "BACK";
break;
}
case 0xBF:
{
str = "PLAY/PAUSE";
break;
}
case 0xBC:
{
str = "FORWARD";
break;
}
case 0xF8:
{
str = "vol-";
break;
}
case 0xEA:
{
str = "DOWN";
break;
}
case 0xF6:
{
str = "VOL+";
break;
}
case 0xE9:
{
str = "1";
break;
}
case 0xE6:
{
str = "2";
break;
}
case 0xF2:
{
str = "3";
break;
}
case 0xF3:
{
str = "4";
break;
}
case 0xE7:
{
str = "5";
break;
}
case 0xA1:
{
str = "6";
break;
}
case 0xF7:
{
str = "7";
break;
}
case 0xE3:
{
str = "8";
break;
}
case 0xA5:
{
str = "9";
break;
}
case 0xBD:
{
str = "0";
break;
}
case 0xB5:
{
str = "DELETE";
break;
}
}
ESP_LOGI(TAG, "KEYVAL = %d KEY = %s", rmt_data,str);
ESP_LOGI(TAG, "KEYVAL = %d, Command=%04X", rmt_data, s_nec_code_command);
}
break;
}
case 2: /* 重复NEC数据帧 */
{
if (rmt_nec_parse_frame_repeat(rmt_nec_symbols))
{
ESP_LOGI(TAG,"KEYVAL = %d, Command = %04X, repeat", rmt_data, s_nec_code_command);
}
break;
}
default: /* 未知NEC数据帧 */
{
ESP_LOGI(TAG, "Unknown NEC frame");
break;
}
}
}
void app_main(void)
{
short temp = 0;
led_init();
rmt_nec_rx_init(); /* 红外接收初始化 */
while (1)
{
if (xQueueReceive(receive_queue, &rx_data, pdMS_TO_TICKS(1000)) == pdPASS)
{
rmt_rx_scan(rx_data.received_symbols, rx_data.num_symbols); /* 解析接收符号并打印结果 */
ESP_ERROR_CHECK(rmt_receive(rx_channel, raw_symbols, sizeof(raw_symbols), &receive_config)); /* 重新开始接收 */
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
二、红外遥控器介绍
红外遥控技术广泛应用于家电、物联网设备等领域,其核心在于通过红外光脉冲编码传输控制指令。
红外遥控利用近红外光谱(波长约 850 − 940 nm 850-940\,\text{nm} 850−940nm)传输数据。发射端(遥控器)通过红外发光二极管(IR LED)发出调制光信号,接收端(设备)使用光电二极管解调并解码信号。核心流程如下:
-
调制
为避免环境光干扰,二进制数据需加载到载波频率上(常用 38 kHz 38\,\text{kHz} 38kHz)。逻辑"1"和"0"通过不同脉冲宽度或周期组合表示。
示例载波调制 :
信号 = 数据码 × sin ( 2 π ⋅ 38 kHz ⋅ t ) \text{信号} = \text{数据码} \times \sin(2\pi \cdot 38\,\text{kHz} \cdot t) 信号=数据码×sin(2π⋅38kHz⋅t) -
编码格式
主流协议采用脉冲位置调制(PPM)或脉冲宽度调制(PWM),定义如下:
- 引导码(Start Code):标志数据帧开始,通常为长高电平和长低电平组合。
- 数据码(Data Code):包含设备地址与操作指令。
- 结束码(Stop Bit):标志帧结束。
三、红外NEC通信协议详解
1、协议概述
红外NEC协议是一种广泛应用于遥控器的串行通信协议。其特点包括:
- 载波频率:38 kHz(多数设备通用)
- 数据表示:使用脉冲位置调制(PPM)
- 数据帧结构 :
- 引导码(Start Code)
- 地址码(Address)
- 命令码(Command)
- 重复码(Repeat Code)
2、数据帧详解
2.1、 引导码
- 由 9 ms 高电平 + 4.5 ms 低电平 组成
- 作用:标识数据帧开始
2.2、 数据位结构
每个数据位由 560 μs 载波脉冲 和后续间隔组成:
- 逻辑 "0":560 μs 脉冲 + 560 μs 低电平
- 逻辑 "1":560 μs 脉冲 + 1.68 ms 低电平
逻辑0时长 = 560 μ s + 560 μ s = 1.12 m s 逻辑1时长 = 560 μ s + 1680 μ s = 2.24 m s \text{逻辑0时长} = 560\mu s + 560\mu s = 1.12ms \\ \text{逻辑1时长} = 560\mu s + 1680\mu s = 2.24ms 逻辑0时长=560μs+560μs=1.12ms逻辑1时长=560μs+1680μs=2.24ms

2.3、数据内容
-
地址码(8位):设备识别码
-
命令码(8位):具体操作指令
-
反码校验:地址和命令各附带8位反码(提高可靠性)
示例帧结构:
[引导码] + [地址码] + [地址反码] + [命令码] + [命令反码]
2.4、重复码
当按键持续按下时发送:
- 9 ms 高电平 + 2.25 ms 低电平 + 560 μs 脉冲
- 周期约 108 ms
3、时序特性
| 信号类型 | 高电平时长 | 低电平时长 |
|---|---|---|
| 引导码 | 9000 μs | 4500 μs |
| 逻辑0 | 560 μs | 560 μs |
| 逻辑1 | 560 μs | 1680 μs |
| 重复码 | 9000 μs | 2250 μs |
4、应用场景
- 电视/空调遥控器
- 智能家居控制
- 嵌入式设备红外通信
