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

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)发出调制光信号,接收端(设备)使用光电二极管解调并解码信号。核心流程如下:

  1. 调制

    为避免环境光干扰,二进制数据需加载到载波频率上(常用 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)

  2. 编码格式

    主流协议采用脉冲位置调制(PPM)或脉冲宽度调制(PWM),定义如下:

    • 引导码(Start Code):标志数据帧开始,通常为长高电平和长低电平组合。
    • 数据码(Data Code):包含设备地址与操作指令。
    • 结束码(Stop Bit):标志帧结束。

三、红外NEC通信协议详解

1、协议概述

红外NEC协议是一种广泛应用于遥控器的串行通信协议。其特点包括:

  1. 载波频率:38 kHz(多数设备通用)
  2. 数据表示:使用脉冲位置调制(PPM)
  3. 数据帧结构
    • 引导码(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、应用场景

  1. 电视/空调遥控器
  2. 智能家居控制
  3. 嵌入式设备红外通信
相关推荐
勇敢牛牛_4 小时前
ESP32 + Rust 开发的简易语音助手
rust·嵌入式·esp32·语音助手
liwulin05062 天前
【ESP32-S3】ESP32-S3 + YDLIDAR X2 + ROS2 远程导航完整落地方案
esp32·ros2·ydlidar
风痕天际2 天前
ESP32-S3开发教程五-按键中断2(使用FreeRTOS)
单片机·嵌入式硬件·esp32·vs code·esp32s3·esp-idf
戏舟的嵌入式开源笔记3 天前
基于ESP32(PIO+Arduino)简单上手LVGL9
esp32·嵌入式软件
小灰灰搞电子3 天前
ESP32 使用ESP-IDF 建立WiFi热点(AP模式)并使用TCP客户端通信源码分享
tcp/ip·esp32·esp-idf
whik11944 天前
ESP32-C3-DevKitM-1开发板深度上手评测
wifi·嵌入式·esp32·arduino·蓝牙·开发板·乐鑫
星野云联AIoT技术洞察4 天前
ESP32 Edge AI 架构设计:固件、OTA 与端侧推理的完整实践
深度学习·esp32·模型部署·aiot·esp-idf·ota升级·固件开发
戏舟的嵌入式开源笔记5 天前
ESP32(PIO+Arduino框架)联网OTA升级思路
esp32·嵌入式软件·ota
小灰灰搞电子5 天前
ESP32 使用ESP-IDF 连接WiFi并使用TCP客户端通信源码分享
网络协议·tcp/ip·esp32