图27.3.1.1
INFRARED_RECEP
tiON
实验程序流程图 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(const rmt_tx_channel_config_t *config,
rmt_channel_handle_t *ret_chan);
该函数的形参描述,如下表所示:
表27.3.2.1 函数rmt_new_tx_channel()形参描述
该函数的返回值描述,如下表所示:
| |
| |
| |
| 创建RMT发送通道失败,因为所有RMT通道都已用完,没有更多空闲通道 |
| |
| 创建RMT发送通道失败,因为硬件不支持某些功能,例如硬件不支持DMA功能 |
| |
表27.3.2.2 函数rmt_new_tx_channel()返回值描述
2,使能RMT发送通道
该函数用于使能RMT发送通道,其函数原型如下所示:
esp_err_t rmt_enable(rmt_channel_handle_t channel);
该函数的形参描述,如下表所示:
表27.3.2.3 函数rmt_enable()形参描述
该函数的返回值描述,如下表所示:
表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,
const rmt_transmit_config_t *config)
该函数的形参描述,如下表所示:
| |
| |
| 用户自己创建的编码器或者通过其它API构建的编码器 |
| |
| |
| |
表27.3.2.5 函数rmt_transmit()形参描述
该函数的返回值描述,如下表所示:
| |
| |
| |
| |
| 传输数据失败,因为硬件不支持某些功能,例如不支持的循环计数 |
| |
表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];
/**
* @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_t scan_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 无
*/
void example_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("RX KEYCNT = %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("RX KEYCNT = %d, repeat\n", s_nec_code_command);
}
break;
}
default: /* 未知NEC数据帧 */
{
printf("Unknown NEC frame\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;
bool valid_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, "REMOTE TEST", 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显示如下图所示。
图27.4.1 红外发射实验测试图
我们可以看到我们开发板发送的红外信号全部红外接收头接收到,说明我们已经实现了开发板自发自收红外信号的功能。温馨提示:由于开发板的红外接收头和红外发射头没有正对,有可能会造成接收不到数据情况,这样我们需要一个提供一个反射面,最简单的做法就是将手放在传感器正前方大约10cm左右的位置。