《ESP32-S3使用指南—IDF版 V1.6》第二十七章 INFRARED_TRANSMISSION实验

0 阅读14分钟

第二十七章 INFRARED_TRANSMISSION实验

本章,我们将介绍ESP32-S3的红外发射器。ESP32-S3板子上标配的红外发射头。我们将利用管脚输入功能,发送我们自己定义的红外编码信号,并将编码后的键值在LCD屏中显示出来。 本章分为如下几个小节: 27.1红外发射器简介 27.2 硬件设计 27.3 程序设计 27.4 下载验证

27.1 红外发射器介绍

RMT外设是一个红外发射和接收控制器。它的数据格式灵活,可进一步扩展为多功能的通用收发器,发送或者接收多种类型的信号。上一章节我们介绍了红外接收控制器,那么本章就来介绍一下红外发射控制器。ESP32的RMT发送通道(TX Channel)的数据路径以及控制器路径如下图所示:

image001.png

图27.1.1 RMT发射器概述

驱动程序将用户数据编码为RMT数据格式,随后有RMT发射器根据编码生成波形。再将波形发送至GPIO引脚,发射器还可以提供载波,并调制高频载波信号。

27.2 硬件设计

27.2.1 例程功能

开机后在LCD上显示一些信息,然后等待红外接收触发解码,在死循环里面,我们利用红外发射头发送键值(每200ms加1),同时在LCD上显示当前发送键值与接收到的键值,来观察自发自收情况。其中LED用来指示程序运行状态。 27.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. 红外发送头 REMOTE_IN-IO8(需要在P3端口处用跳线帽将AIN与RMT两个引脚连接起来)

27.2.3 原理图

红外接收头的原理图请参照上一章节的内容,在此不再做出赘述。红外发送头相关原理图,如下图所示。

image003.png

图27.2.3.1 红外接收头原理图

需要注意:REMOTE_OUT并没有直接与ESP32-S3芯片的引脚相连,而是需要在P3端口处使用跳线帽将REMOTE_OUT和ADC_IN连接起来,P3端口的原理图以及实物连接方式如下所示:

image005.png

图27.2.3.2 P3端口原理图

image007.png

图27.2.3.3 P3实物连接方式

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

image009.png

图27.2.3.4 开发板上的红外发送管位置

27.3 程序设计

27.3.1 程序流程图

程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图:

image012.png

图27.3.1.1 INFRARED_RECEPTION实验程序流程图

27.3.2 RMT函数解析

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

#include "driver/rmt_tx.h"
#include "driver/rmt_rx.h"
#include "ir_nec_encoder.h"

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

esp_err_t rmt_new_tx_channel(constrmt_tx_channel_config_t *config,
                              rmt_channel_handle_t *ret_chan);

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

QQ截图20250626153411.png

表27.3.2.1 函数rmt_new_tx_channel()形参描述

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

QQ截图20250626153424.png

表27.3.2.2 函数rmt_new_tx_channel()返回值描述

2,使能RMT发送通道 该函数用于使能RMT发送通道,其函数原型如下所示:
esp_err_t rmt_enable(rmt_channel_handle_t channel);

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

QQ截图20250626153432.png

表27.3.2.3 函数rmt_enable()形参描述

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

QQ截图20250626153439.png

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

3,通过RMT发送通道传输数据 该函数用于启动RMT接收通道的接收任务,其函数原型如下所示:
esp_err_t rmt_transmit(rmt_channel_handle_t channel,
                         rmt_encoder_t *encoder,
                        const void *payload,
                        size_t payload_bytes,
                        constrmt_transmit_config_t *config)

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

QQ截图20250626153449.png

表27.3.2.5 函数rmt_transmit()形参描述

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

QQ截图20250626153505.png

表27.3.2.6 函数rmt_transmit()返回值描述

27.3.3 RMT驱动解析

在IDF版的17_infrared_transmission例程中,作者在17_infrared_transmission\components\BSP路径下新增了一个EMISSION文件夹,分别用于存放emission.c、emission.h和ir_nec_encoder.c以及ir_nec_encoder.h这四个文件。其中,emission.h文件负责声明RMT相关的函数和变量,ir_nec_encoder.h存放用于IR NEC帧编码为RMT符号的RMT编码器的相关结构体成员,而emission.c和ir_nec_encoder.c文件则实现了RMT的驱动代码。笔者仅介绍关于 1,remote.h文件

/* 引脚定义 */
#define REMOTE_IN_GPIO_PIN              GPIO_NUM_2
#define RMT_TX_PIN                     GPIO_NUM_8
#define REMOTE_RESOLUTION_HZ            1000000
#define REMOTE_NEC_DECODE_MARGIN        200
/* NEC协议时序时间 */
#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
/* 保存NEC解码的地址和命令字节 */
static uint16_t s_nec_code_address;
static uint16_t s_nec_code_command;

2,remote.c文件 我们选择使用IO8作为红外接收管的引脚。 接下来,看一下红外接收初始化函数emission_init,代码如下:

const static char *TAG = "RMT_Transmission TEST";
/* 保存NEC解码的地址和命令字节 */
uint16_t s_nec_code_address;
uint16_t s_nec_code_command;
QueueHandle_t receive_queue;
uint8_t tbuf[40];
/**
* @brief       初始化RMT
* @param       无
* @retval      无
*/
void emission_init(void)
{
    uint8_t t = 0;
    /* 配置接收通道 */
    ESP_LOGI(TAG, "create RMT RX channel");
   rmt_rx_channel_config_t rx_channel_cfg = {
        
        /* RMT接收通道时钟源 */
        .clk_src =RMT_CLK_SRC_DEFAULT,
        
        /* RMT接收通道时钟分辨率 */
        .resolution_hz = RMT_RESOLUTION_HZ,
        
        /* 通道一次可以存储的RMT符号数量 */
        .mem_block_symbols = 64,
        
        /* RMT接收通道引脚 */
        .gpio_num = RMT_RX_PIN,
    };
   rmt_channel_handle_t rx_channel = NULL;
   
    /* 创建一个RMT接收通道 */
    ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_channel_cfg, &rx_channel));
    /* 配置消息队列 */
    ESP_LOGI(TAG, "register RX done callback");
   
    /* 定义一个消息队列,用以处理RMT接收的回调函数 */
QueueHandle_t receive_queue=xQueueCrate(1,sizeof(rmt_rx_done_event_data_t));
    assert(receive_queue);
   
    /* 事件回调,当一个RMT通道接收事务完成时调用 */
   rmt_rx_event_callbacks_t cbs = {
        .on_recv_done =RMT_Rx_Done_Callback,
    };
   
    /* 为RMT RX信道设置回调 */
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_channel,
                                                     &cbs,
                                                     receive_queue));
    /* 以下时间要求基于NEC协议 */
   rmt_receive_config_t receive_config = {
        /* NEC信号的最短持续时间为560us,1250ns<560us,有效信号不会被视为噪声 */
        .signal_range_min_ns= 1250,
        /* NEC信号的最长持续时间为9000us,12000000ns>9000us,接收不会提前停止 */
        .signal_range_max_ns= 12000000,
    };
    /* 配置发送通道 */
    ESP_LOGI(TAG, "create RMT TX channel");
   rmt_tx_channel_config_t tx_channel_cfg = {
        
        /* RMT发送通道时钟源 */
        .clk_src =RMT_CLK_SRC_DEFAULT,
        
        /* RMT发送通道时钟分辨率 */
        .resolution_hz = RMT_RESOLUTION_HZ,
        
        /* 通道一次可以存储的RMT符号数量 */
        .mem_block_symbols = 64,
        
        /* 允许在后台挂起的事务数,本例不会对多个事务进行排队,因此队列深度>1就足够了 */
        .trans_queue_depth = 4,
        
        /* RMT发送通道引脚 */
        .gpio_num = RMT_TX_PIN,
    };
   rmt_channel_handle_t tx_channel = NULL;
   
    /* 创建一个RMT发送通道 */
    ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_channel_cfg, &tx_channel));
    /* 配置载波与占空比s */
    ESP_LOGI(TAG, "modulate carrier to TX channel");
   rmt_carrier_config_t carrier_cfg = {
        
        /* 载波频率,0表示禁用载波 */
        .frequency_hz = 38000,
        
        /* 载波占空比 */
        .duty_cycle = 0.33,
    };
   
    /* 对发送信道应用调制功能 */
    ESP_ERROR_CHECK(rmt_apply_carrier(tx_channel, &carrier_cfg));
    /* 不会在循环中发送NEC帧 */
   rmt_transmit_config_t transmit_config = {
        
        /* 0为不循环,-1为无限循环 */
        .loop_count = 0,
    };
    /* 配置编码器 */
    ESP_LOGI(TAG, "install IR NEC encoder");
   ir_nec_encoder_config_t nec_encoder_cfg = {
        
        /* 编码器分辨率 */
        .resolution = RMT_RESOLUTION_HZ,
    };
   rmt_encoder_handle_t nec_encoder = NULL;
   
    /* 配置编码器 */
    ESP_ERROR_CHECK(rmt_new_ir_nec_encoder(&nec_encoder_cfg, &nec_encoder));
    /* 使能发送、接收通道 */
    ESP_LOGI(TAG, "enable RMT TX and RX channels");
   
    /* 使能发送通道 */
    ESP_ERROR_CHECK(rmt_enable(tx_channel));
   
    /* 使能接收通道 */
    ESP_ERROR_CHECK(rmt_enable(rx_channel));
    /* 保存接收到的RMT符号,64个符号对于标准NEC框架应该足够 */
   rmt_symbol_word_t raw_symbols[64];
   rmt_rx_done_event_data_t rx_data;
    /* 准备接收 */
ESP_ERROR_CHECK(rmt_receive(rx_channel,
                                 raw_symbols,
                                 sizeof(raw_symbols),
                                &receive_config));
    while (1)
    {
        /* 等待RX完成信号 */
        if (xQueueReceive(receive_queue,
                          &rx_data,
                          pdMS_TO_TICKS(1000)) == pdPASS)
        {
            /* 解析接收符号并打印结果 */
           example_parse_nec_frame(rx_data.received_symbols,
                                    rx_data.num_symbols);
            
            /* 重新开始接收 */
           ESP_ERROR_CHECK(rmt_receive(rx_channel,
                                        raw_symbols,
                                        sizeof(raw_symbols),
                                        &receive_config));
        }
        else    /* 超时,传输预定义的IR NEC数据包 */
        {
            t++;
            if (t == 0)
            {
                t = 1;
            }
            const ir_nec_scan_code_tscan_code = {
                .command = t,
            };
           lcd_fill(116, 110, 176, 150, WHITE);
            sprintf((char *)tbuf, "%d", scan_code.command);
            printf("TX KEYVAL =%d\n", scan_code.command);
           lcd_show_string(116, 110, 200, 16, 16, (char *)tbuf, BLUE);
            
             /* 通过RMT发送信道传输数据 */
           ESP_ERROR_CHECK(rmt_transmit(tx_channel,
                           nec_encoder,
                           &scan_code, sizeof(scan_code),
                           &transmit_config));
        }
    }
}

RMT的接收器配置在26.3.1小节中进行了讲解,我们来看一下while循环里的函数实现过程。首先,我们使用队列的方式对RMT数据进行处理,从中接收项目的队列句柄(我们定义为receive_queue),由于该项目是通过复制接收的,必须提供足够大小的缓冲区,故而我们定义了指向缓冲区的指针rx_data,接收到的项将被复制到这个缓冲区之中,之后通过if语句判断该项的值与pdPASS的值是否相等,如果队列成功创建则添加到就绪队列中。其次,根据NEC编码解析红外协议并打印指令结果,最后,再次开启接收任务函数。 在else语句中我们定义了一个t,t的初始值为0,通过t的自加我们能做到发送t自加后的数值,达到发送不同红外编码的目的,但也仅能发送0~255的数值。 接下来,介绍一下红外按键扫描函数remote_scan,代码如下:

/**
* @brief       根据NEC编码解析红外协议并打印指令结果
* @param       无
* @retval      无
*/
voidexample_parse_nec_frame(rmt_symbol_word_t *rmt_nec_symbols, size_t symbol_num)
{
    switch (symbol_num) /* 解码RMT接收数据 */
    {
        case 34:        /* 正常NEC数据帧 */
        {
            if (nec_parse_frame(rmt_nec_symbols) )
            {
               lcd_fill(116, 130, 176, 150, WHITE);
               sprintf((char *)tbuf, "%d", s_nec_code_command);
               printf("RXKEYCNT = %d\n", s_nec_code_command);
               lcd_show_string(116, 130, 200, 16, 16, (char *)tbuf, BLUE);
            }
            break;
        }
        
        case 2:         /* 重复NEC数据帧 */
        {
            if (nec_parse_frame_repeat(rmt_nec_symbols))
            {
               printf("RXKEYCNT = %d, repeat\n", s_nec_code_command);
            }
            break;
        }
        default:        /* 未知NEC数据帧 */
        {
            printf("Unknown NECframe\r\n\r\n");
            break;
        }
    }
}

该函数调用nec_parse_frame()函数将RMT结果解码出NEC地址和命令,我们这里只需要处理解码出的命令即可,因为地址是不会变的。处理解码出来的命令我们可以直观的在LCD以及串口助手上看见。由于我们在程序中编写的功能是实现开发板的自发自收功能,所以当我们接上跳线帽的时候会看见发送端和接收端显示的数据是一样的。同样的,我们也对重复的NEC数据帧以及未知的NEC数据帧进行识别与处理,重复的NEC数据帧会通过串口打印键值以及十六进制的数据,并添加上“repeat”的标识以作区分。而对于未知数据帧会通过串口打印“Unknown NEC frame”的字样。

/**
* @brief       将RMT接收结果解码出NEC地址和命令
* @param       无
* @retval      无
*/
bool 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;
boolvalid_leading_code = nec_check_in_range(cur->duration0,
                                                  NEC_LEADING_CODE_DURATION_0) &&
                               nec_check_in_range(cur->duration1,
                                                  NEC_LEADING_CODE_DURATION_1);
    if (!valid_leading_code)
    {
        return false;
    }
    cur++;
    for (int i = 0; i < 16; i++)
    {
        if (nec_parse_logic1(cur))
        {
            address|= 1 << i;
        }
        else if (nec_parse_logic0(cur))
        {
            address&= ~(1 << i);
        }
        else
        {
            return false;
        }
        cur++;
    }
    for (int i = 0; i < 16; i++)
    {
        if (nec_parse_logic1(cur))
        {
            command|= 1 << i;
        }
        else if (nec_parse_logic0(cur))
        {
            command&= ~(1 << i);
        }
        else
        {
            return false;
        }
        cur++;
    }
    /* 保存数据地址和命令,用于判断重复按键 */
   s_nec_code_address = address;
   s_nec_code_command = command;
    return true;
}

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

/**
*@brief IR NEC scan code representation
*/
typedef struct {
    uint16_t address;
    uint16_t command;
}ir_nec_scan_code_t;
/**
*@brief Type of IR NEC encoder configuration
*/
typedef struct {
    uint32_t resolution; /*!< Encoder resolution, in Hz */
}ir_nec_encoder_config_t;

4,ir_nec_encoder.c文件 该文件是ESP32的一个红外编解码文件,区别于API函数,该文件是作为RMT编解码过程中的一个辅助文件,由于代码过长,不便在此处张贴,请读者到相关的例程中结合资料进行学习。

27.3.4 CMakeLists.txt文件

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

set(src_dirs
           EMISSION
           IIC
           LCD
           LED
           SPI
           XL9555)
set(include_dirs
           EMISSION
           IIC
           LCD
           LED
           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)

上述的红色EMISSION驱动需要由开发者自行添加,以确保RMT驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了RMT驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。

27.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, "REMOTETEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "TX KEYVAL:", RED);
    lcd_show_string(30, 130, 200, 16, 16, "RX KEYCNT:", RED);
    emission_init();                      /* 初始化REMOTE */
}

main函数比较简单,主要外设初始化之后LCD显示实验信息,再针对RMT外设进行初始化。

27.4 下载验证

下载代码后,可以看到LCD显示如下图所示。

image013.png

图27.4.1 红外发射实验测试图

我们可以看到我们开发板发送的红外信号全部红外接收头接收到,说明我们已经实现了开发板自发自收红外信号的功能。温馨提示:由于开发板的红外接收头和红外发射头没有正对,有可能会造成接收不到数据情况,这样我们需要一个提供一个反射面,最简单的做法就是将手放在传感器正前方大约10cm左右的位置。