第二十六章 INFRARED_RECEPtiON实验
本章,我们将介绍ESP32-S3对红外遥控器的信号解码。ESP32-S3板子上标配的红外接收头和一个小巧的红外遥控器。我们将利用管脚输入功能,解码开发板标配的红外遥控器的编码信号,并将编码后的键值在LCD屏中显示出来。 本章分为如下几个小节:
26.1红外遥控简介
26.2 硬件设计
26.3 程序设计
26.4 下载验证
26.1 红外遥控简介
26.1.1红外遥控技术介绍
红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计算机系统中。 由于红外线遥控不具有像无线电遥控那样穿过障碍物去控制被控对象的能力,所以,在设计红外线遥控器时,不必要像无线电遥控器那样,每套(发射器和接收器)要有不同的遥控频率或编码(否则,就会隔墙控制或干扰邻居的家用电器),所以同类产品的红外线遥控器,可以有相同的遥控频率或编码,而不会出现遥控信号“串门”的情况。这对于大批量生产以及在家用电器上普及红外线遥控提供了极大的方便。由于红外线为不可见光,因此对环境影响很小,再由红外光波动波长远小于无线电波的波长,所以红外线遥控不会影响其他家用电器,也不会影响临近的无线电设备。 26.1.2红外器件特性
红外遥控的情景中,必定会有一个红外发射端和红外接收端。在本实验中,正点原子的红外遥控器作为红外发射端,红外接收端就是板载的红外接收器,实物图可以查看26.2.3小节原理图部分。要使两者通信成功,收/发红外波长与载波频率需一致,在这里波长就是940nm,载波频率就是38kHz。 红外发射管也是属于二极管类,红外发射电路通常使用三极管控制红外发射器的导通或者截至,在导通的时候,红外发射管会发射出红外光,反之,就不会发射出红外光。虽然我们用肉眼看不到红外光,但是我们借助手机摄像头就能看到红外光。但是红外接收管的特性是当接收到红外载波信号时,OUT引脚输出低电平;假如没有接收到红外载波信号时,OUT引脚输出高电平。 红外载波信号其实就是由一个个红外载波周期组成。在频率为38KHz下,红外载波周期约等于26.3us(1s / 38KHz ≈ 26.3us)。在一个红外载波发射周期里,发射红外光时间8.77us和不发射红外光17.53us,发射红外光的占空比一般为1/3。相对的,整个周期内不发射红外光,就是载波不发射周期。在红外遥控器内已经把载波和不载波信号处理好,我们需要做的就是识别遥控器按键发射出的信号,信号也是遵循某种协议的。
26.1.3红外编解码协议介绍
红外遥控的编码方式目前广泛使用的是:PWM(脉冲宽度调制)的NEC协议和Philips PPM(脉冲位置调制)的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,控制码为21(正确解码后00010101)。可以看到在100ms之后,我们还收到了几个脉冲,这是NEC码规定的连发码(由9ms低电平+2.25ms高电平+0.56ms低电平+97.94ms高电平组成),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码,即连发码可以通过统计连发码的次数来标记按键按下的长短/次数。
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相关的控制和状态寄存器。
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);
该函数的形参描述,如下表所示:
表26.3.2.1 函数rmt_new_rx_channel()形参描述
该函数的返回值描述,如下表所示:
| |
| |
| |
| 创建RMT接收通道失败,因为所有RMT通道都已用完,没有更多空闲通道 |
| |
| 创建RMT接收通道失败,因为硬件不支持某些功能,例如硬件不支持DMA功能 |
| |
表26.3.2.2 函数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);
该函数的形参描述,如下表所示:
表26.3.2.3 函数rmt_rx_register_event_callbacks()形参描述
该函数的返回值描述,如下表所示:
表26.3.2.4 函数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);
该函数的形参描述,如下表所示:
表26.3.2.5 函数rmt_new_ir_nec_encoder()形参描述
该函数的返回值描述,如下表所示:
表26.3.2.6 函数rmt_new_ir_nec_encoder()返回值描述
4,使能RMT接收通道
该函数用于使能RMT接收通道,其函数原型如下所示:
esp_err_t rmt_enable(rmt_channel_handle_t channel);
该函数的形参描述,如下表所示:
表26.3.2.7 函数rmt_enable()形参描述
该函数的返回值描述,如下表所示:
表26.3.2.8 函数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);
该函数的形参描述,如下表所示:
表26.3.2.9 函数rmt_enable()形参描述
该函数的返回值描述,如下表所示:
表26.3.2.10 函数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 REMOTE_IN_GPIO_PIN GPIO_NUM_2
#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文件
const static char *TAG = "REMOTE_RECEIVE TEST";
QueueHandle_t receive_queue;
rmt_channel_handle_t rx_channel;
rmt_symbol_word_t raw_symbols[64]; /*对于标准NEC框架应该足够*/
rmt_receive_config_t receive_config;
rmt_rx_done_event_data_t rx_data;
/**
*@retval 无
*/
void remote_init(void)
{
ESP_LOGI(TAG, "Create RMT RX channel");
rmt_rx_channel_config_t rx_channel_cfg;
/*选择APB时钟源作为默认选项*/
rx_channel_cfg.clk_src = RMT_CLK_SRC_DEFAULT;
/*通道时钟分辨率*/
rx_channel_cfg.resolution_hz = REMOTE_RESOLUTION_HZ;
/*内存块大小*/
rx_channel_cfg.mem_block_symbols = 64;
/*配置RMT接收引脚*/
rx_channel_cfg.gpio_num = REMOTE_IN_GPIO_PIN;
rx_channel = NULL;
/*创建一个RMT接收通道*/
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_channel_cfg, &rx_channel));
ESP_LOGI(TAG,"register RX done callback");
/*创建能包含一个RMTRX完成事件数据类型的队列*/
receive_queue=xQueueCreate(1, sizeof(rmt_rx_done_event_data_t));
/*使用断言*/
assert(receive_queue);
/*事件回调,当一个RMT通道接收事务完成时调用*/
rmt_rx_event_callbacks_t cbs = {
/*RMT数据接收完成回调函数*/
.on_recv_done = RMT_Rx_Done_Callback,
};
/*配置RMT接收通道回调*/
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,
};
/*配置循环发送RMT配置*/
rmt_transmit_config_t transmit_config = {
/*不循环*/
.loop_count = 0,
};
ESP_LOGI(TAG,"installIRNECencoder");
ir_nec_encoder_config_t nec_encoder_cfg = {
/*编码器分辨率*/
.resolution = REMOTE_RESOLUTION_HZ,
};
/*创建一个NEC协议的RMT编码器*/
rmt_encoder_handle_tnec_encoder = NULL;
/*创建一个RMT编码器*/
ESP_ERROR_CHECK(rmt_new_ir_nec_encoder(&nec_encoder_cfg, &nec_encoder));
ESP_LOGI(TAG, "开启RMT发送和接收通道");
/*使能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)
{
/*解析接收符号并打印结果*/
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));
}
}
}
RMT的各项配置以及介绍在26.3.1小节中进行了讲解,我们来看一下while循环里的函数实现过程。首先,我们使用队列的方式对RMT数据进行处理,从中接收项目的队列句柄(我们定义为receive_queue),由于该项目是通过复制接收的,必须提供足够大小的缓冲区,故而我们定义了指向缓冲区的指针rx_data,接收到的项将被复制到这个缓冲区之中,之后通过if语句判断该项的值与pdPASS的值是否相等,如果队列成功创建则添加到就绪队列中。其次,根据NEC编码解析红外协议并打印指令结果,最后,再次开启接收任务函数。
接下来,介绍一下红外按键扫描函数remote_scan,代码如下:
/**
* @brief 根据NEC编码解析红外协议并打印指令结果
* @param 无
* @retval 无
*/
void example_parse_nec_frame(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 (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 (nec_parse_frame_repeat(rmt_nec_symbols))
{
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;
}
}
}
该函数调用nec_parse_frame()函数将RMT结果解码出NEC地址和命令,我们这里只需要处理解码出的命令即可,因为地址是不会变的。处理解码出来的命令我们通过转置运算,转换成我们便于理解的十进制数值也就是我们代码中的的“KEYVAL”,为了便于对比与学习,我们仍然保留解码出来的十六进制数据。当我们使用正点原子配发的红外遥控器,并按下遥控器上面的按键时,经过解码后会在开发板上显示出对应的键值与按键所表示的图标。同样的,我们也对重复的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,用于判断重复按键。
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)
上述的红色REMOTE驱动需要由开发者自行添加,以确保RMT驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了RMT驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
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编解码过程中的一个辅助文件,由于代码过长,不便在此处张贴,请读者到相关的例程中结合资料进行学习。
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 下载验证
下载代码后,可以看到LCD显示如下图所示。
图26.4.1 红外接收实验测试图
此时,我们通过遥控器按下不同的按键,则可以看到LCD上显示了不同按键的键值和对应遥控器上的符号,如下图所示。
图26.4.2 红外接收实验测试图