本章将介绍 ESP32-S3 对红外遥控器的信号解码。 ESP32-S3 板子上标配的红外接收头和一个小巧的红外遥控器。将利用管脚输入功能,解码开发板标配的红外遥控器的编码信号,并将编码后的键值在 LCD 屏中显示出来。
        本章分为如下几个小节:
26.1 红外遥控简介
26.2 硬件设计
26.3 程序设计
26.4 下载验证

26.1 红外遥控简介
26.1.1 红外遥控技术介绍

        红外遥控技术是一种利用​​近红外光​​进行​​无线、非接触控制​​的技术。下面我将从基本原理、系统构成、编码协议、应用场景及优缺点等方面为你介绍这项广泛应用的技术。
        基本原理与工作方式:
        红外遥控技术的核心是利用波长为 ​​0.76μm~1.5μm​​ 的近红外光来传送控制指令。其工作过程可以概括为“​​发射-传输-接收​​”:

  • 发射端​​:当按下遥控器按键时,内部的​​编码芯片​​会生成一串特定的​​二进制脉冲编码信号​​(通常是一串代表0和1的时序)。该编码信号会对一个​​高频载波​​(通常为38kHz)进行​​脉冲幅度调制(PAM)​​,然后通过​​红外发光二极管​​(IRED)以红外光的形式发射出去;
  • 传输通道​​:调制后的红外光在空气中传播至接收设备。红外光无法穿透墙壁,因此不同房间的设备不会相互干扰;
  • 接收端​​:设备上的​​一体化红外接收头​​(如HS0038)会接收红外信号,并进行​​光电转换​​、​​放大​​、​​解调​​(去除38kHz载波)和​​整形​​,最终还原出原始的编码脉冲序列,送给微控制器(MCU)进行解码和执行相应操作。

        应用场景:

        ​​红外遥控技术因其​​成本低、抗干扰能力强、不干扰其他设备​​等特点,被广泛应用于:

  • 家用电器​​:电视、空调、风扇、音响、机顶盒、DVD播放器等是其最传统的应用领域;
  • ​​智能家居​​:部分智能灯具、插座等仍采用红外遥控;
  • ​​工业设备​​:用于某些工业设备的远程操作与控制;
  • ​​其他​​:一些投影仪、数码相机等也采用红外遥控。

        优势与局限:

​优势​

​局限​

​成本低廉​​:硬件结构简单,易于大规模生产

​方向性强​​:需要对准接收器方向,不能有大的遮挡

​抗干扰能力强​​:不易受其他电磁波干扰

​传输距离短​​:通常有效距离在10米以内

​保密性较好​​:信号不易穿透墙壁,不同房间设备互不干扰

​易受强光干扰​​:强烈的环境光(如阳光)可能影响接收

​技术成熟,应用广泛​

​通常仅支持单向通信​​(发射→接收)

表26.1.1 .1 红外遥控优势与局限

26.1.2 红外器件特性

        一个完整的红外遥控系统主要包括发射和接收两大部分:

部分​

​主要组件​

​功能描述​

​​发射端(遥控器)​​

键盘矩阵、编码调制电路、驱动电路、红外发射管(IRED)

将按键操作转换为特定的红外编码信号并发射。

​​接收端(设备端)​​

红外接收管(光电二极管/三极管)、放大器、解调电路、解码电路、微控制器(MCU)

接收红外信号,将其转换为电信号,并解码还原出控制命令。

表26.1.2.1 红外遥控系统组成

        现代设备中,接收部分常集成为一个​​三引脚的一体化红外接收头​​(如LF0038),大大简化了设计和应用。
        在本实验中,红外遥控器作为红外发射端,红外接收端就是板载的红外接收器,实物图可以查看 26.2.3 小节原理图部分。要使两者通信成功,收/发红外波长与载波频率需一致,在这里波长就是 940nm,载波频率就是 38kHz。
        红外发射管也是属于二极管类,红外发射电路通常使用三极管控制红外发射器的导通或者截止, 在导通的时候,红外发射管会发射出红外光,反之,就不会发射出红外光。虽然我们用肉眼看不到红外光,但是我们借助手机摄像头就能看到红外光。但是红外接收管的特性是当接收到红外载波信号时, OUT 引脚输出低电平;假如没有接收到红外载波信号时, OUT 引脚输出高电平。
        红外载波信号其实就是由一个个红外载波周期组成。在频率为 38KHz 下,红外载波周期约等于 26.3us(1s / 38KHz ≈ 26.3us)。在一个红外载波发射周期里,发射红外光时间 8.77us 和不发射红外光 17.53us,发射红外光的占空比一般为 1/3。相对的,整个周期内不发射红外光,就是载波不发射周期。在红外遥控器内已经把载波和不载波信号处理好,我们需要做的就是识别遥控器按键发射出的信号,信号也是遵循某种协议的。

26.1.3 红外编解码协议介绍

        红外遥控的编码协议定义了信号的组织格式,不同厂商或设备可能采用不同的协议。常见的协议有:

协议名称​

​主要特点​

​常见应用​

​​NEC协议​​

使用38kHz载波;引导码(9ms高+4.5ms低);32位数据帧(含16位地址码和8位命令码及反码)

电视机、机顶盒、DVD播放器等家电中非常普遍

​​Sony SIRC​​

使用40kHz载波;通过脉冲长度表示数据

索尼设备

​​Philips RC5/RC6​​

RC5使用14位数据帧,曼彻斯特编码;RC6是其增强版

飞利浦等品牌的设备

​​Samsung​​

三星设备采用的协议

三星设备

表26.1.3.1 红外遥控的编码协议

        这些协议在​​载波频率​​、​​引导码​​、​​数据帧结构​​和​​逻辑0/1的表示方法​​上存在差异。例如,NEC协议中,逻辑“0”用0.56ms高电平和0.56ms低电平表示,逻辑“1”用0.56ms高电平和1.68ms低电平表示。
        目前广泛使用的是: PWM( 脉冲宽度调制) 的 NEC 协议和 PhilipsPPM(脉冲位置调制) 的 RC-5 协议的。开发板配套的遥控器使用的是 NEC 协议,其特征如下:
1)8 位地址和 8 位指令长度;
2)地址和命令 2 次传输(确保可靠性);
3) PWM 脉冲位置调制,以发射红外载波的占空比代表“0”和“1”;
4)载波频率为 38Khz;
5)位时间为 1.125ms 或 2.25ms;
        在 NEC 协议中,如何为协议中的数据‘0’或者‘ 1’?这里分开红外接收器和红外发射器。
        红外发射器:发送协议数据‘ 0’ = 发射载波信号 560us + 不发射载波信号 560us;
                              发送协议数据‘1’ = 发射载波信号 560us + 不发射载波信号 1680us。
        红外发射器的位定义如下图所示。

图 26.1.3.1 红外发射器位定义图

        红外接收器:接收到协议数据‘0’ = 560us 低电平 + 560us 高电平;
                              接收到协议数据‘ 1’ = 560us 低电平 + 1680us 高电平。
        红外接收器的位定义如下图所示。

图 26.1.3.2 红外接收器位定义图

        NEC遥控指令的数据格式为:同步码头、地址码、地址反码、控制码、控制反码。同步码由一个 9ms 的低电平和一个 4.5ms 的高电平组成,地址码、地址反码、控制码、控制反码均是 8位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可用于校验)。
        遥控器的按键“ALIENTEK” 按下时,从红外接收头端收到的波形如下图所示。

图 26.1.3.3 按键“ALIENTEK” 所对应的红外波形

        从上图中可以看到,其地址码为 0,控制码为 0x47(正确解码后 01000111)。可以看到在100ms 之后,还收到了几个脉冲,这是 NEC 码规定的连发码(由 9ms 低电平+2.25ms 高电平+0.56ms 低电平+97.94ms 高电平组成),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码,即连发码可以通过统计连发码的次数来标记按键按下的长短/次数。

        NEC 红外协议帧结构:

  • 一个完整的 NEC 协议数据帧包含以下部分:
组成部分 符号数量 说明
引导码 2 个符号 9ms 低电平(符号1) + 4.5ms 高电平(符号2)
32位数据 32 个符号 地址码(16位) + 命令码(16位),
  • 每位数据用 1个符号 表示(高电平的持续时间);

  • 逻辑"0":560μs 高电平;

  • 逻辑"1":1.69ms 高电平。

每位数据前的低电平(560μs)是固定的,可能被合并或忽略计数

结束位 1 个符号 560μs 低电平
总计 35 个符号 实际上,结束位通常不单独计数,因为它就是最后一个数据位的低电平部分。因此实际总共34个符号。

图 26.1.3.3 NEC 红外协议帧结构

  • 当按键持续按下时,NEC 协议会发送简化的重复帧:

        符号1:9ms 低电平
        符号2:2.25ms 高电平
        总计:2 个符号

  • 为什么这样设计?

        效率:通过符号数量快速区分帧类型;
        可靠性:34 个符号是 NEC 标准帧的"指纹";
        错误检测:如果不是 34 或 2,说明接收出错或不是 NEC 协议;
        这种设计使得红外解码既高效又可靠,能够快速识别有效的遥控信号。

26.1.4 ESP32-S3 红外遥控(RMT)介绍

        RMT是一个红外发送和接收控制器,可通过软件加解密多种红外协议。 RMT模块可以实现将模块内置 RAM 中的脉冲编码转换为信号输出,或将模块的输入信号转换为脉冲编码存入RAM中。此外, RMT 模块可以选择是否对输出信号进行载波调制,也可以选择是否对输入信号进行滤波和去噪处理。
RMT 共有八个通道,编码为 0~7,各通道可独立用于发送或接收信号:
       (1) 0~3 通道专门用于发送信号;
       (2) 4~7 通道专门用于接收信号。
        每个发送通道和接收通道分别有一组功能相同的寄存器。另外,发送通道 3 和接收通道 7对应的 RAM 支持 DMA 访问,因此还有 DMA 相关的控制和状态寄存器。
        ESP32内置了​​RMT(Remote Control)外设​​,可通过软件加解密多种红外协议,非常适合处理红外遥控信号。

26.2 硬件设计
26.2.1 例程功能

        在 LCD 上显示一些实验信息之后,即进入等待红外触发,如果接收到正确的红外信号,则解码,并在 LCD 上显示键值和所代表的意义。 LED 闪烁用于提示程序正在运行。

26.2.2 硬件资源

        1. LED 灯
                LED-IO1
        2. USART0
                U0TXD-IO43
                U0RXD-IO44
        3. XL9555
                IIC_SDA-IO41
                IIC_SCL-IO42
        4. SPILCD
                CS-IO21
                SCK-IO12
                SDA-IO11
                DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
                PWR- IO1_3(XL9555)
                RST- IO1_2(XL9555)
        5. 红外接收头
                REMOTE_IN-IO2
        6. 点原子红外遥控器

26.2.3 原理图

        红外接收头相关原理图,如下图所示。

图 26.2.3.1 红外接收头原理图

        需要注意: REMOTE_IN 和 SD 卡片选共用了 IO2,所以它们不可以同时使用。
        开发板配套的红外遥控器外观如图 26.2.2 所示:

图 26.2.3.2 红外遥控器

        开发板上接收红外遥控器信号的红外管外观如图 26.2.3 所示。使用时需要遥控器有红外管的一端对准开发板上的红外管才能正确收到信号。

图 26.2.3.3 开发板上的红外接收管位置

26.3 程序设计
26.3.1 程序流程图

        本实验的程序流程图:

图 26.3.1.1 INFRARED_RECEPTION 实验程序流程图

26.3.2 RMT 函数解析

        ESP-IDF 提供了一套 API 来配置 RMT。要使用此功能,需要导入必要的头文件:

#include "driver/rmt_rx.h"

        接下来,介绍一些常用的 ESP32-S3 中的 RMT 函数,这些函数的描述及其作用如下:
(1)安装 RMT 接收通道
        该函数用于安装 RMT 接收通道,其函数原型如下所示:

esp_err_t rmt_new_rx_channel(const rmt_rx_channel_config_t *config,
                             rmt_channel_handle_t *ret_chan);

该函数的形参描述,如下表所示:

形参 描述
config 指向配置 RMT 接收通道的指针
ret_chan 返回的通用 RMT 通道句柄

表 26.3.2.1 函数 rmt_new_rx_channel()形参描述

rmt_rx_channel_config_t 结构体成员说明:

成员变量 类型 必需/可选 默认值 描述
gpio_num gpio_num_t 必需 RMT 接收信号的 GPIO 引脚编号
示例:GPIO_NUM_18
clk_src rmt_clock_source_t 可选 RMT_CLK_SRC_DEFAULT RMT 通道时钟源
RMT_CLK_SRC_APB - APB时钟 (80MHz)
RMT_CLK_SRC_REF_TICK - 参考时钟 (1MHz)
RMT_CLK_SRC_XTAL - 外部晶体时钟
resolution_hz uint32_t 必需 计数器时钟频率(分辨率)
决定能区分的最小脉冲宽度
示例:10000000 = 10MHz (100ns)
mem_block_symbols size_t 可选 64 分配的内存块大小(符号数)
每个符号存储一个电平的持续时间
ESP32: 每块64符号
ESP32-S2/S3: 每块48符号
queue_depth size_t 可选 4 接收事件队列深度
缓存接收完成的事件数量
防止事件丢失
flags.invert_input uint32_t 可选 0 输入信号反转
0 - 不反转(默认)
1 - 反转信号(高变低,低变高)
flags.with_dma uint32_t 可选 0 启用DMA后台传输
0 - 不使用DMA(默认)
1 - 使用DMA处理长数据流
flags.io_loop_back uint32_t 可选 0 GPIO内部环回模式
0 - 禁用(默认)
1 - 启用(用于自我测试)
flags.io_od_mode uint32_t 可选 0 GPIO开漏模式
0 - 标准输入模式(默认)
1 - 开漏输出模式
intr_priority int 可选 0 中断优先级
通常使用默认值0即可

表 26.3.2.2 rmt_rx_channel_config_t 结构体成员说明

该函数的返回值描述,如下表所示:

返回值 描述
ESP_OK 返回: 0,配置成功
ESP_ERR_INVALID_ARG 无效参数, 创建 RMT 接收通道失败
ESP_ERR_NOT_FOUND 创建 RMT接收通道失败,因为所有 RMT通道都已用
完,没有更多空闲通道
ESP_ERR_NO_MEM 内存不足, 创建 RMT 接收通道失败
ESP_ERR_NOT_SUPPORTED 创建 RMT 接收通道失败,因为硬件不支持某些功
能,例如硬件不支持 DMA 功能
ESP_FAIL 由于其他错误,创建 RMT 接收通道失败

表 26.3.2.3 函数 rmt_new_rx_channel()返回值描述

(2)配置 RMT 接收通道的回调函数
        该函数用于配置 RMT 接收通道的回调函数,其函数原型如下所示:

esp_err_t rmt_rx_register_event_callbacks(rmt_channel_handle_t rx_channel,
                                          const rmt_rx_event_callbacks_t *cbs,
                                          void *user_data);

该函数的形参描述,如下表所示:

形参 描述
rx_channel 创建的 RMT 通用通道
cbs 返回的通用 RMT 通道句柄
user_data 用户数据,将直接传递给回调函数

表 26.3.2.4 函数 rmt_rx_register_event_callbacks()形参描述

该函数的返回值描述,如下表所示:

返回值 描述
ESP_OK 返回: 0,配置成功
ESP_ERR_INVALID_ARG 无效参数,设置事件回调失败
ESP_FAIL 由于其他错误, 设置事件回调失败

表 26.3.2.5 函数 rmt_rx_register_event_callbacks()返回值描述

(3)创建一个基于 NEC 协议的 RMT 编码器
        该函数用于创建一个基于 NEC 协议的 RMT 编码器(发送使用,本次实验为数据接收,暂时不用),其函数原型如下所示:

esp_err_t rmt_new_ir_nec_encoder(const ir_nec_encoder_config_t *config,
                                 rmt_encoder_handle_t *ret_encoder);

该函数的形参描述,如下表所示:

形参 描述
config 指向 RMT 编码器配置的指针
ret_encoder RMT 编码器类型句柄

表 26.3.2.6 函数 rmt_new_ir_nec_encoder()形参描述

该函数的返回值描述,如下表所示:

返回值 描述
ESP_OK 返回: 0,配置成功

表 26.3.2.7 函数 rmt_new_ir_nec_encoder()返回值描述

(4)使能 RMT 接收通道
        该函数用于使能 RMT 接收通道,其函数原型如下所示:

esp_err_t rmt_enable(rmt_channel_handle_t channel);

该函数的形参描述,如下表所示:

形参 描述
channel 创建的 RMT 通用通道

表 26.3.2.8 函数 rmt_enable()形参描述

该函数的返回值描述,如下表所示:

返回值 描述
ESP_OK 返回: 0,配置成功
ESP_ERR_INVALID_ARG 无效参数, 创建 RMT 接收通道失败
ESP_ERR_INVALID_STATE 使能 RMT 接收通道失败,因为它已经启用
ESP_FAIL 由于其他错误,使能 RMT 接收通道失败

表 26.3.2.9 函数 rmt_enable()返回值描述

(5)启动 RMT 接收通道的接收任务
        该函数用于启动 RMT 接收通道的接收任务,其函数原型如下所示:

esp_err_t rmt_receive(rmt_channel_handle_t rx_channel,
                      void *buffer,
                      size_t buffer_size,
                      const rmt_receive_config_t *config);

该函数的形参描述,如下表所示:

形参 描述
rx_channel 创建的 RMT 通道
buffer 用于存储接收到的 RMT 符号的缓冲区
buffer_size 缓冲区大小
config 接收特定配置

表 26.3.2.10 函数 rmt_enable()形参描述

该函数的返回值描述,如下表所示:

返回值 描述
ESP_OK 返回: 0,配置成功
ESP_ERR_INVALID_ARG 无效参数, 使能接收任务失败
ESP_ERR_INVALID_STATE 由于通道未启用,使能接收任务失败
ESP_FAIL 由于其他错误,使能接收任务失败

表 26.3.2.11 函数 rmt_enable()返回值描述

26.3.3 RMT 驱动解析

        在 IDF 版的 16_infrared_reception 例程中,16_infrared_reception \components\BSP 路径下新增了一个 REMOTE 文件夹,分别用于存放 remote.c、 remote.h 和 ir_nec_encoder.c 以及ir_nec_encoder.h 这四个文件。其中, remote.h 文件负责声明 RMT 相关的函数和变量,ir_nec_encoder.h 存放用于 IR NEC 帧编码为 RMT 符号的 RMT 编码器的相关结构体成员,而remote.c 和 ir_nec_encoder.c 文件则实现了 RMT 的驱动代码。下面,我们将详细解析这四个文件的实现内容。

(1)remote.h 文件

/* 引脚定义 */
#define RMT_IN_GPIO_PIN                 GPIO_NUM_2  /* 连接REMOTE_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

/* 函数声明 */
void remote_init(void);                                                                                             /* 初始化RMT */
void remote_scan(rmt_symbol_word_t *rmt_nec_symbols, size_t symbol_num);                                            /* 根据NEC编码解析红外协议并打印指令结果 */
bool RMT_Rx_Done_Callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data);    /* RMT数据接收完成回调函数 */

(2)remote.c 文件

/**
 * @brief       初始化RMT
 * @param       无
 * @retval      无
 */
void remote_init(void)
{
    /* 配置接收通道 */
    rmt_rx_channel_config_t rx_channel_cfg = {
        .clk_src = RMT_CLK_SRC_DEFAULT,                 /* 设置RMT时钟源 */
        .resolution_hz = RMT_RESOLUTION_HZ,             /* 设置协议频率 */
        .mem_block_symbols = 64,                        /* 通道一次可以存储的RMT符号数量 */
        .gpio_num = RMT_IN_GPIO_PIN,                    /* 设置通道管脚 */
    };
    /* 创建接收通道 */
    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_Rx_Done_Callback,                                   /* RMT信号接收回调函数 */
    };
    /* 注册红外接收回调函数 */
    ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_channel, &cbs, receive_queue));  /* 配置RMT接收通道回调函数 */

    /* 以下时序要求基于NEC协议 */
    rmt_receive_config_t receive_config = {
    .signal_range_min_ns = 1250,                /* NEC信号的最短持续时间为560us,1250ns<560us,有效信号不会被视为噪声 */
    .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));            /* 准备接收 */

    while (1)
    {
        /* 以下时间要求基于NEC协议 */
        if (xQueueReceive(receive_queue, &rx_data, pdMS_TO_TICKS(1000)) == pdPASS)
        {
            remote_scan(rx_data.received_symbols, rx_data.num_symbols);                                     /* 解析接收符号并打印结果 */

            ESP_ERROR_CHECK(rmt_receive(rx_channel, raw_symbols, sizeof(raw_symbols), &receive_config));    /* 重新开始接收 */
        }
    }
}

        RMT 的各项配置以及介绍在 26.3.1 小节中介绍,接下来while 循环里的函数实现过程,首先,使用队列的方式对 RMT 数据进行处理,从中接收项目的队列句柄(定义为receive_queue),定义了指向缓冲区的指针 rx_data,接收到的项将被复制到这个缓冲区之中,之后通过 if 语句判断该项的值与 pdPASS 的值是否相等,如果队列成功创建则添加到就绪队列中。其次,根据NEC 编码解析红外协议并打印指令结果,最后,再次开启接收任务函数。
        接下来介绍红外按键扫描函数 remote_scan,代码如下:

/**
 * @brief       根据NEC编码解析红外协议并打印指令结果
 * @param       无
 * @retval      无
 */
void remote_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 = "DLETE";
                        break;
                    }
                        
                }
                lcd_fill(86, 110, 176, 150, WHITE);
                sprintf((char *)tbuf, "%d", rmt_data);
                printf("KEYVAL = %d, Command=%04X\n", rmt_data, s_nec_code_command);
                lcd_show_string(86, 110, 200, 16, 16, (char *)tbuf, BLUE);
                sprintf((char *)tbuf, "%s", str);
                lcd_show_string(86, 130, 200, 16, 16, (char *)tbuf, BLUE);
            }
            break;
        }
        
        case 2:         /* 重复NEC数据帧 */
        {
            if (rmt_nec_parse_frame_repeat(rmt_nec_symbols))
            {
				rmt_data = (s_nec_code_command >> 8);  //重复NEC数据帧,显示重复数据,否则会一直显示0
                printf("KEYVAL = %d, Command = %04X, repeat\n", rmt_data, s_nec_code_command);
            }
            break;
        }

        default:        /* 未知NEC数据帧 */
        {
            printf("Unknown NEC frame\r\n\r\n");
            break;
        }
    }
}

        该函数调用 rmt_nec_parse_frame()函数将 RMT结果解码出 NEC地址和命令,这里只需要处理解码出的命令即可,因为地址不会变。处理解码出来的命令转换成我们便于理解的十进制数值也就是代码中的的“KEYVAL”,为了便于对比,仍然保留解码出来的十六进制数据。当按下红外遥控器按键时,经过解码后会在开发板上显示出对应的键值与按键所表示的图标。同样的,也对重复的 NEC 数据帧以及未知的 NEC 数据帧进行识别与处理,重复的 NEC 数据帧会通过串口打印键值以及十六进制的数据,并添加上“repeat”的标识以作区分。而对于未知数据帧会通过串口打印“Unknown NEC frame”的字样。

/**
 * @brief       判断数据时序长度是否在NEC时序时长容差范围内 正负 REMOTE_NEC_DECODE_MARGIN 的值以内
 * @param       无
 * @retval      无
 */
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       无
 * @retval      无
 */
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       无
 * @retval      无
 */
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       无
 * @retval      无
 */
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       无
 * @retval      无
 */
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);
}

        rmt_nec_parse_frame()函数是按照前面分析NEC协议帧结构与解析逻辑实现,其中

  1. 引导码(Leading Code)​​:一个9ms的低电平脉冲和一个4.5ms的高电平脉冲,标志着帧的开始。cur刚开始指向第1个符号;
  2. 地址码(Address)​​:16位数据(通常包含8位地址和8位地址反码),循环16次,每次处理1位后cur++,共 ​​16次​​;
  3. ​​命令码(Command)​​:16位数据(通常包含8位命令和8位命令反码),循环16次,每次处理1位后cur++,共 ​​16次​​。

        函数将接收到的电平数组解码成红外编码,按照也就是 NEC 地址以及命令。首先,通过布尔型函数对比数据时序长度是否为逻辑 1或者逻辑 0,从而获取地址、地址反码以及命令、命令反码,并检查获取的数据是否正确,最后分别保存数据地址和命令到s_nec_code_address 以及s_nec_code_command,用于判断重复按键。

26.3.4 CMakeLists.txt 文件

        打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:

set(src_dirs
            IIC
            LCD
            LED
            REMOTE
            SPI
            XL9555)

set(include_dirs
            IIC
            LCD
            LED
            REMOTE
            SPI
            XL9555)

set(requires
            driver)

idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})

component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
26.3.5 实验应用代码

        打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。

i2c_obj_t i2c0_master;

/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    esp_err_t ret;
    
    ret = nvs_flash_init();             /* 初始化NVS */

    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

    led_init();                         /* 初始化LED */
    i2c0_master = iic_init(I2C_NUM_0);  /* 初始化IIC0 */
    spi2_init();                        /* 初始化SPI2 */
    xl9555_init(i2c0_master);           /* 初始化XL9555 */
    lcd_init();                         /* 初始化LCD */
   
    lcd_show_string(30,  50, 200, 16, 16, "ESP32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "REMOTE TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEYVAL:", RED);
    lcd_show_string(30, 130, 200, 16, 16, "SYMBOL:", RED);

    remote_init();                      /* 初始化REMOTE */
}

        main 函数代码比较简单,主要是通过 remote_scan 函数获得红外遥控输入的数据(控制码),然后显示在 LCD 上面。

26.4 下载验证

        下载代码后,按下按键3,可以看到 LCD 显示如下图所示。按下其他按键显示正常。

图 26.4.1 红外接收实验测试图

串口打印如下,发现一个问题是按下按键后不放,LCD和串口显示数据不对:

图 26.4.2 连续按下某一个按键红外接收异常显示测试图

修复代码在连续按下后,显示最后一次数据,如下所示:

图 26.4.3 连续按下某一个按键红外接收异常修复代码

图 26.4.4 连续按下某一个按键红外接收正常显示测试图

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐