|
第二十八章 INFRARED_RECEPTION实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
28.1 红外遥控简介
28.1.1 红外遥控技术介绍
红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计算机系统中。 由于红外线遥控不具有像无线电遥控那样穿过障碍物去控制被控对象的能力,所以,在设计红外线遥控器时,不必要像无线电遥控器那样,每套(发射器和接收器)要有不同的遥控频率或编码(否则,就会隔墙控制或干扰邻居的家用电器),所以同类产品的红外线遥控器,可以有相同的遥控频率或编码,而不会出现遥控信号“串门”的情况。这对于大批量生产以及在家用电器上普及红外线遥控提供了极大的方便。由于红外线为不可见光,因此对环境影响很小,再由红外光波动波长远小于无线电波的波长,所以红外线遥控不会影响其他家用电器,也不会影响临近的无线电设备。
28.1.2 红外器件特性
红外遥控的情景中,必定会有一个红外发射端和红外接收端。在本实验中,正点原子的红外遥控器作为红外发射端,红外接收端就是板载的红外接收器,实物图可以查看28.2.3小节原理图部分。要使两者通信成功,收/发红外波长与载波频率需一致,在这里波长就是940nm,载波频率就是38kHz。
红外发射管也是属于二极管类,红外发射电路通常使用三极管控制红外发射器的导通或者截至,在导通的时候,红外发射管会发射出红外光,反之,就不会发射出红外光。虽然我们用肉眼看不到红外光,但是我们借助手机摄像头就能看到红外光。但是红外接收管的特性是当接收到红外载波信号时,OUT引脚输出低电平;假如没有接收到红外载波信号时,OUT引脚输出高电平。
红外载波信号其实就是由一个个红外载波周期组成。在频率为38KHz下,红外载波周期约等于26.3us(1s / 38KHz ≈ 26.3us)。在一个红外载波发射周期里,发射红外光时间8.77us和不发射红外光17.53us,发射红外光的占空比一般为1/3。相对的,整个周期内不发射红外光,就是载波不发射周期。在红外遥控器内已经把载波和不载波信号处理好,我们需要做的就是识别遥控器按键发射出的信号,信号也是遵循某种协议的。
28.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
红外发射器的位定义如下图所示。
图28.1.3.1 红外发射器位定义图
红外接收器:接收到协议数据‘0’ = 560us低电平 + 560us高电平
接收到协议数据‘1’ = 560us低电平 + 1680us高电平
红外接收器的位定义如下图所示。
图28.1.3.2 红外接收器位定义图
NEC遥控指令的数据格式为:同步码头、地址码、地址反码、控制码、控制反码。同步码由一个9ms的低电平和一个4.5ms的高电平组成,地址码、地址反码、控制码、控制反码均是8位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可用于校验)。
我们遥控器的按键“ALIENTEK”按下时,从红外接收头端收到的波形如下图所示。
图28.1.3.3 按键“ALIENTEK”所对应的红外波形
从上图中可以看到,其地址码为0,控制码为21(正确解码后00010101)。可以看到在100ms之后,我们还收到了几个脉冲,这是NEC码规定的连发码(由9ms低电平+2.25ms高电平+0.56ms低电平+97.94ms高电平组成),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码,即连发码可以通过统计连发码的次数来标记按键按下的长短/次数。
28.1.4 ESP32-P4红外遥控(RMT)外设介绍
红外遥控(RMT)是一个红外发送和接收控制器。各种红外控制协议都可通过RMT外设进行编码/解码。就网络分层而言,RMT硬件包含物理层和数据链路层。物理层定义通信介质和比特信号的表示方式,数据链路层定义RMT帧的格式。RMT帧的最小数据单元称为RMT符号。
ESP32-P4的RMT外设有8个通道,编号从0到7。每个通道都可独立运行,通道0~3(TX通道)专用于传输信号,通道4~7(RX通道)专用于接收信号。
ESP32-P4发送通道支持:正常发送模式、乒乓发送模式、连续发送模式、发送脉冲调制、可编程多通道同时传输数据、发送通道3支持GDMA。
ESP32-P4接收通道支持:正常接收模式、乒乓接收模式、接收过滤器、接收脉冲解调、接收通道7支持GDMA。
接下来,看一下RMT外设的结构框图,如下图所示。
图28.1.4.1 RMT外设结构框图
每个发送通道(SEND_CHn)内部各有:一个时钟分频计数器(Div Counter)、一个状态机(FSM)和一个发射器(Transmitter)。每个接收通道(RECV_CHm)内部也各有:一个时钟分频计数器(Div Counter)、一个状态机(FSM)和一个接收器(Receiver)。八个通道共享用一块384x32位的RAM。
简单来说,RMT外设可以实现将外设内置RAM中的脉冲编码转换成信号通过发送通道输出,或将外设的输入信号通过接收通道转换为脉冲编码存入RAM中。
接下来,通过一个简图更加清晰了解RMT发送通道的数据路径和控制路径,如下图所示。
图28.1.4.2 RMT发送通道的数据路径和控制路径
驱动程序将用户数据编码为RMT数据格式,随后由RMT发射器根据编码生成波形。在将波形发生GPIO管脚前,还可以调制高频载波信号。
同样的,也来了解一下RMT接收通道的数据路径和控制路径,如下图所示。
图28.1.4.3 RMT接收通道的数据路径和控制路径
RMT接收器可以对输入信号采样,将其转换为RMT数据格式,并将数据存储在内存中。还可以向接收器提供输入信号的基础特征,使其识别信号停止条件,并过滤信号干扰和噪声。RMT外设还支持从基准信号中解调出高频载波信号。
RAM中脉冲编码结构如下图所示。
图28.1.4.4 RAM中脉冲编码结构
每个脉冲编码为16位,由level和period两部分组成。其中level表示输入或输出信号的逻辑电平值(0或1),period表示该电平信号持续的时钟周期数。period的最小值为0,period为0是一次传输的结束标志。
28.2 硬件设计
28.2.1 例程功能
在LCD上显示一些实验信息后,进入等待红外触发,如果接收到正确的红外信号,则解码,并在LCD上显示键值和所代表的意义。LED0闪烁用于提示程序正在运行。
28.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)RGBLCD / MIPILCD(引脚太多,不罗列出来)
3)红外接收头
REMOTE_IN - IO2
4)正点原子红外遥控器
28.2.3 原理图
红外接收头相关原理图如下图所示。
图28.2.3.1 红外接收头原理图
开发板配套的红外遥控器外观如图28.2.3.2所示:
图28.2.3.2 红外遥控器
ESP32-P4开发板底板上接收红外遥控器信号的红外管外观如图28.2.3.3所示。使用时需要遥控器有红外管的一端对准开发板上的红外管才能正确收到信号。
图28.2.3.3 底板上的红外接收管位置
28.3 程序设计
28.3.1 RMT的IDF驱动
RMT外设驱动位于ESP-IDF下的components/esp_driver_rmt目录下。使用RMT功能,必须先导入以下头文件:
- #include "driver/rmt_rx.h"
复制代码 本例程主要实现的是RMT的接收功能。接下来,作者将介绍一些常用的函数,这些函数的描述及其作用如下:
1,创建RMT接收通道函数rmt_new_rx_channel
该函数用于创建RMT接收通道,其函数原型如下:
- <font size="3">esp_err_t rmt_new_rx_channel(const rmt_rx_channel_config_t *config, </font>
- <font size="3"> rmt_channel_handle_t *ret_chan);</font>
复制代码 函数形参:
表28.3.1.1 rmt_new_rx_channel函数形参描述
函数返回值:
ESP_OK表示创建RMT接收通道成功。
ESP_ERR_INVALID_ARG表示错误参数。
ESP_ERR_NO_MEM表示内存不足。
ESP_ERR_NOT_FOUND表示所有接收通道已经用完。
ESP_ERR_NOT_SUPPORTED表示某些功能不被硬件支持,比如DMA。
ESP_FAIL表示其他错误。
config为指向RMT接收通道配置结构体指针。接下来,笔者将介绍rmt_rx_channel_config_t结构体中各个成员,如下代码所示:
- typedef struct {
- gpio_num_t gpio_num; /* 接收器使用的GPIO编号 */
- rmt_clock_source_t clk_src; /* RMT通道的时钟源 */
- uint32_t resolution_hz; /* 内部滴答计数器的分辨率 */
- size_t mem_block_symbols; /* 专用内存块大小(启动DMA时,内部DMA缓冲区大小) */
- int intr_priority; /* 设置中断优先级 */
- struct {
- uint32_t invert_in: 1; /* 在输入信号传递到RMT接收器前对其进行反转 */
- uint32_t with_dma: 1; /* 为通道启动DMA后端 */
- uint32_t io_loop_back: 1; /* 用于调试/测试 */
- } flags; /* 接收通道配置标志 */
- } rmt_rx_channel_config_t;
复制代码 rmt_rx_channel_config_t结构体用于配置RMT接收通道参数,以下对各个成员做简单介绍。
1)gpio_num:
设置接收器使用的GPIO编号。
2)clk_src:
选择RMT通道时钟源,可选RMT_CLK_SRC_APB或RMT_CLK_SRC_REF_TICK,也可以选择RMT_CLK_SRC_DEFAULT,默认指的RMT_CLK_SRC_APB。注意:其他通道需要使用同一所选的时钟源。
3)resolution_hz:
设置内部滴答计数器的分辨率。基于此滴答,可计算出RMT信号的定时参数。
4)mem_block_symbols:
在启动DMA后端和未启用DMA后端,含义稍有不同。若with_dma启用DMA,这里的含义是最大化控制内部DMA缓冲区大小。若未启用DMA,这里的含义是控制通道专用内存块大小,至少为48。
5)intr_priority:
设置中断优先级。如果设置为0,系统将会使用一个中低优先级的中断。
6)invert_in:
在输入信号传递到RMT接收器前对其进行反转。该反转由GPIO交换矩阵完成,而非RMT外设。
7)with_dma:
为通道启用DMA后端。启动DMA后端可以释放CPU上的大部分通道工作负载,显著减轻CPU负担。
8)io_loop_back:
从GPIO输出的信号也将会被反馈到输入路径。
ret_chan为指向RMT通道句柄结构体指针,rmt_channel_handle_t结构体成员比较多,可以不需要了解。
2,配置RMT接收通道回调函数rmt_rx_register_event_callbacks
该函数用于配置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);
复制代码 函数形参:
表28.3.1.2 rmt_rx_register_event_callbacks函数形参描述
函数返回值:
ESP_OK表示配置成功。
ESP_ERR_INVALID_ARG表示错误参数。
ESP_FAIL表示
rx_channel为RMT通道句柄,在调用rmt_new_rx_channel函数时将返回RMT通道句柄。
cbs为指向RMT接收通道的回调函数指针,在RMT通道接收完成时调用。RMT通道接收回调函数的格式如下:
- typedef bool (*rmt_rx_done_callback_t)(rmt_channel_handle_t rx_chan, const rmt_rx_done_event_data_t *edata, void *user_ctx);
复制代码 在编写回调函数时需要注意格式一致。这里会涉及到rmt_rx_done_event_data_t结构体类型,定义如下:
- typedef struct {
- rmt_symbol_word_t *received_symbols; /* 指向接收到的RMT符号 */
- size_t num_symbols; /* 接收到的RMT符号的数量 */
- struct {
- uint32_t is_last: 1; /* 当前接收到的数据是否是事务的最后一部分 */
- } flags; /* 额外标志 */
- } rmt_rx_done_event_data_t;
复制代码 需要注意两个成员便可,一个是received_symbols,另一个是num_symbols。接收到的RMT数据将会存放在rmt_rx_done_event_data_t结构体类型的received_symbols成员中,而接收到的数据长度便在num_symbols成员中体现。received_symbols成员又是一个结构体类型,定义如下:
- typedef union {
- struct {
- uint16_t duration0 : 15; /* 第一部分的持续时间 */
- uint16_t level0 : 1; /* 第一部分的电平状态 */
- uint16_t duration1 : 15; /* 第二部分的持续时间 */
- uint16_t level1 : 1; /* 第二部分的电平状态 */
- };
- uint32_t val; /* 逻辑值 */
- } rmt_symbol_word_t;
复制代码 user_data为RMT接收通道的回调函数传递的参数。
3,使能RMT接收通道函数rmt_enable
该函数用于使能RMT接收通道,其函数原型如下:
- esp_err_t rmt_enable(rmt_channel_handle_t channel);
复制代码 函数形参:
表28.3.1.3 rmt_enable函数形参描述
函数返回值:
ESP_OK表示读取数据成功。
ESP_ERR_INVALID_ARG表示参数有误。
ESP_ERR_INVALID_STATE表示通道已经使能。
ESP_FAIL表示其他错误。
4,启动RMT接收通道接收任务函数rmt_receive
该函数用于启动RMT接收通道的接收任务,其函数原型如下:
- esp_err_t rmt_receive(rmt_channel_handle_t rx_channel,
- void *buffer,
- size_t buffer_size,
- const rmt_receive_config_t *config);
复制代码 函数形参:
表28.3.1.4 rmt_receive函数形参描述
函数返回值:
ESP_OK表示读取数据成功。
ESP_ERR_INVALID_ARG表示参数有误。
ESP_ERR_INVALID_STATE表示通道未启用。
ESP_FAIL表示其他错误。
rx_channel为RMT通道句柄,在调用rmt_new_rx_channel函数时将返回RMT通道句柄。
buffer为用于存储接收到的RMT符号的缓冲区,需要注意其数据类型,例程中设置其数据类型设置为rmt_symbol_word_t,且长度为64,可存储一个标准NEC帧的数据。
buffer_size为缓冲区大小。如果该值不足以容纳所有接收到的RMT符号,程序只会保存缓冲区能够容纳的最大数量的符号并丢弃或忽略多以的符号。
config为指向RMT接收特定配置结构体指针,rmt_receive_config_t结构体成员如下所示。
- typedef struct {
- uint32_t signal_range_min_ns; /* 高电平或低电平有效脉冲的最小持续时间 */
- uint32_t signal_range_max_ns; /* 高电平或低电平有效脉冲的最大持续时间 */
-
- struct extra_flags {
- uint32_t en_partial_rx: 1; /* 开启部分接收功能,缓冲区快满时多次进入回调函数 */
- } flags;
- } rmt_receive_config_t;
复制代码 仅调用rmt_enable函数,RX通道是无法接收RMT符号的,还需要通过rmt_receive_config_t去指明传入信号的基本特征。
28.3.2 程序流程图
图28.3.2.1 红外接收实验程序流程图
28.3.3 程序解析
在19_infrared_reception例程中,作者在19_infrared_reception\components\BSP路径下新建了1个文件夹RMT_RX,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1. RMT_RX驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。RMT_RX驱动源码包括两个文件:rmt_nec_rx.c和rmt_nec_rx.h。
其中,rmt_nec_rx.h文件负责声明RMT接收相关的函数以及定义一些宏,rmt_nec_rx.c存放用于实现RMT接收的驱动代码。
下面先解析rmt_nec_rx.h的程序,了解一下在该文件中定义了哪些宏。
- <font size="3">#define RMT_IN_GPIO_PIN GPIO_NUM_23 /* 连接RMT_RX_IN的端口 */</font>
- <font size="3">#define RMT_RESOLUTION_HZ 1000000 /* 1MHz 频率, 1 tick = 1us */</font>
- <font size="3">/* 判断NEC时序时长的容差值,小于(值+此值),大于(值-此值)为正确 */</font>
- <font size="3">#define RMT_NEC_DECODE_MARGIN 200 </font>
- <font size="3">/* NEC协议时序时间 */</font>
- <font size="3">#define NEC_LEADING_CODE_DURATION_0 9000</font>
- <font size="3">#define NEC_LEADING_CODE_DURATION_1 4500</font>
- <font size="3">#define NEC_PAYLOAD_ZERO_DURATION_0 560</font>
- <font size="3">#define NEC_PAYLOAD_ZERO_DURATION_1 560</font>
- <font size="3">#define NEC_PAYLOAD_ONE_DURATION_0 560</font>
- <font size="3">#define NEC_PAYLOAD_ONE_DURATION_1 1690</font>
- <font size="3">#define NEC_REPEAT_CODE_DURATION_0 9000</font>
- <font size="3">#define NEC_REPEAT_CODE_DURATION_1 2250</font>
复制代码 开发板上使用的是IO23作为RMT通道,内部滴答计数器的分辨率设置为1MHz以及NEC时序时长误差范围。还有NEC协议内容,其协议中的时序单元时间长短,比如协议头(同步码)为9ms低电平 + 4.5ms高电平,逻辑0为560us低电平 + 560us高电平,逻辑1为560us低电平 + 1690us高电平,重复码为9ms电平+2.25ms高电平。后续在rmt_nec_rx.c中的解码函数中便能发现使用这些宏做判断,解码出NEC协议内容。
接下来,解析rmt_nec_rx.c程序。首先看RMT接收初始化函数rmt_nec_rx_init,代码如下。
- <font size="3">/**</font>
- <font size="3"> * @brief RMT红外接收初始化</font>
- <font size="3"> * [url=home.php?mod=space&uid=271674]@param[/url] 无</font>
- <font size="3"> * @retval ESP_OK:初始化成功</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t rmt_nec_rx_init(void)</font>
- <font size="3">{ /* 配置接收通道 */</font>
- <font size="3"> rmt_rx_channel_config_t rx_channel_cfg = {</font>
- <font size="3"> .gpio_num = RMT_IN_GPIO_PIN, /* 设置红外接收通道管脚 */</font>
- <font size="3"> .clk_src = RMT_CLK_SRC_DEFAULT, /* 设置RMT时钟源 */</font>
- <font size="3"> .resolution_hz = RMT_RESOLUTION_HZ, /* 设置时钟分辨率 */</font>
- <font size="3"> .mem_block_symbols = 64, /* 通道一次可以存储的符号数量 */</font>
- <font size="3">};</font>
- <font size="3">/* 创建接收通道 */</font>
- <font size="3"> ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_channel_cfg, &rx_channel)); </font>
- <font size="3">/* 创建消息队列,用于接收红外编码 */</font>
- <font size="3">receive_queue = xQueueCreate(1, sizeof(rmt_rx_done_event_data_t)); </font>
- <font size="3">assert(receive_queue);</font>
- <font size="3">/* 配置红外接收完成回调 */</font>
- <font size="3"> rmt_rx_event_callbacks_t cbs = {</font>
- <font size="3"> .on_recv_done = rmt_nec_rx_done_callback, /* RMT信号接收完成回调函数 */</font>
- <font size="3"> };</font>
- <font size="3">ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_channel, &cbs, </font>
- <font size="3">receive_queue)); /* 配置RMT接收通道回调函数 */</font>
- <font size="3">/* NEC协议的时序要求 */</font>
- <font size="3">/* NEC信号的最短持续时间为560us,1250ns<560us,有效信号不会被视为噪声 */</font>
- <font size="3">receive_config.signal_range_min_ns = 1250; </font>
- <font size="3">/* NEC信号的最长持续时间为9000us,12000000ns>9000us,接收不会提前停止 */</font>
- <font size="3"> receive_config.signal_range_max_ns = 12000000; </font>
- <font size="3"> /* 开启RMT通道 */</font>
- <font size="3"> ESP_ERROR_CHECK(rmt_enable(rx_channel)); /* 使能RMT接收通道 */</font>
- <font size="3">ESP_ERROR_CHECK(rmt_receive(rx_channel, raw_symbols, sizeof(raw_symbols), </font>
- <font size="3">&receive_config)); /* 准备接收 */</font>
- <font size="3"> return ESP_OK;</font>
- <font size="3">}</font>
复制代码 在RMT接收初始化函数中,首先对rx_channel_cfg变量的成员进行赋值,设置好红外接收通道的引脚为IO23,RMT时钟源为默认,分辨率为1Mz以及通道容量大小为64,然后调用rmt_new_rx_channel函数创建RMT接收通道。注册红外接收回调函数rmt_nec_rx_done_callback用于获取接收到的RMT数据,通过消息队列方式去传递RMT数据。后面对receive_config结构体成员进行赋值,设置NEC协议的时序要求。最后再调用rmt_enable函数使能RMT接收通道以及调用rmt_receive函数进行数据接收。当RMT接收通道接收数据完成后,便进入红外接收回调函数中。
这里注意:rmt_nec_rx.c定义了比较多的全局变量,主要目的是方便其他文件调用。
紧接着看一下,红外接收回调函数rmt_nec_rx_done_callback,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief RMT数据接收完成回调函数</font>
- <font size="3"> * @param channel : 通道</font>
- <font size="3"> * @param edata : 接收的数据</font>
- <font size="3"> * @param user_data : 传入的参数</font>
- <font size="3"> * @retval 返回是否唤醒了任何任务</font>
- <font size="3"> */</font>
- <font size="3">bool rmt_nec_rx_done_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data)</font>
- <font size="3">{</font>
- <font size="3"> BaseType_t high_task_wakeup = pdFALSE;</font>
- <font size="3">QueueHandle_t receive_queue = (QueueHandle_t)user_data;</font>
- <font size="3">/* 将收到的RMT数据通过消息队列发送到解析任务 */</font>
- <font size="3"> xQueueSendFromISR(receive_queue, edata, &high_task_wakeup);</font>
- <font size="3"> return high_task_wakeup == pdTRUE;</font>
- <font size="3">}</font>
复制代码 该函数主要实现的功能是将RMT接收通道接收到的数据edata通过在rmt_nec_rx_init函数中创建的消息队列发送出去,而在app_main函数中进行获取解析。
最后介绍NEC协议解析函数rmt_nec_parse_frame,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief 将RMT接收结果解码出NEC地址和命令</font>
- <font size="3"> * @param rmt_nec_symbols:RMT数据帧</font>
- <font size="3"> * @retval true成功;false失败</font>
- <font size="3"> */</font>
- <font size="3">bool rmt_nec_parse_frame(rmt_symbol_word_t *rmt_nec_symbols)</font>
- <font size="3">{</font>
- <font size="3"> rmt_symbol_word_t *cur = rmt_nec_symbols;</font>
- <font size="3"> uint16_t address = 0;</font>
- <font size="3"> uint16_t command = 0;</font>
- <font size="3">bool valid_leading_code = rmt_nec_check_range(cur->duration0, </font>
- <font size="3">NEC_LEADING_CODE_DURATION_0) &&</font>
- <font size="3"> rmt_nec_check_range(cur->duration1, </font>
- <font size="3">NEC_LEADING_CODE_DURATION_1);</font>
- <font size="3"> if (!valid_leading_code) </font>
- <font size="3"> {</font>
- <font size="3"> return false;</font>
- <font size="3"> }</font>
- <font size="3"> cur++;</font>
- <font size="3"> for (int i = 0; i < 16; i++)</font>
- <font size="3"> {</font>
- <font size="3"> if (rmt_nec_logic1(cur)) </font>
- <font size="3"> {</font>
- <font size="3"> address |= 1 << i;</font>
- <font size="3"> } </font>
- <font size="3"> else if (rmt_nec_logic0(cur))</font>
- <font size="3"> {</font>
- <font size="3"> address &= ~(1 << i);</font>
- <font size="3"> } </font>
- <font size="3"> else </font>
- <font size="3"> {</font>
- <font size="3"> return false;</font>
- <font size="3"> }</font>
- <font size="3"> cur++;</font>
- <font size="3"> }</font>
- <font size="3"> for (int i = 0; i < 16; i++)</font>
- <font size="3"> {</font>
- <font size="3"> if (rmt_nec_logic1(cur))</font>
- <font size="3"> {</font>
- <font size="3"> command |= 1 << i;</font>
- <font size="3"> }</font>
- <font size="3"> else if (rmt_nec_logic0(cur))</font>
- <font size="3"> {</font>
- <font size="3"> command &= ~(1 << i);</font>
- <font size="3"> }</font>
- <font size="3"> else</font>
- <font size="3"> {</font>
- <font size="3"> return false;</font>
- <font size="3"> }</font>
- <font size="3"> cur++;</font>
- <font size="3"> }</font>
- <font size="3"> /* 保存数据地址和命令,用于判断重复按键 */</font>
- <font size="3"> s_nec_code_address = address;</font>
- <font size="3"> s_nec_code_command = command;</font>
- <font size="3"> return true;</font>
- <font size="3">}</font>
复制代码 该函数主要就是根据NEC遥控指令数据格式对数据进行解析,首先是判断同步码头,然后是地址码(地址码 + 地址反码),接着是控制码(控制码 + 控制反码)。上述部分的判断会涉及到以下函数,如下所示。
- <font size="3">/* 判断数据时序长度是否在NEC时序时长误差范围内 */</font>
- <font size="3">bool rmt_nec_check_range(uint32_t signal_duration, uint32_t spec_duration);</font>
- <font size="3">/* 判断数据时序长度判断是否为逻辑0 */</font>
- <font size="3">bool rmt_nec_logic0(rmt_symbol_word_t *rmt_nec_symbols);</font>
- <font size="3">/* 判断数据时序长度判断是否为逻辑1 */</font>
- <font size="3">bool rmt_nec_logic1(rmt_symbol_word_t *rmt_nec_symbols);</font>
复制代码 同步码头便是9ms低电平 + 4.5ms高电平,比较特殊,直接调用两次rmt_nec_check_range函数即可。而地址码和控制码都是数据来的,直接通过调用rmt_nec_logic0和rmt_nec_logic1去判断出逻辑0和1即可。注意:NEC协议发码的顺序为先发送低位,再发送高位。若红外信号一直被持续发送,这时会有重复码的出现,可用文件中的rmt_nec_parse_frame_repeat函数去解析判断即可。
红外接收实验代码讲解到这里。
2. CMakeLists.txt文件
本例程的功能实现主要依靠RMT_RX驱动。要在main函数中,成功调用RMT_RX文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
- <font size="3">set(src_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> LCD</font>
- <font size="3"> RMT_RX)</font>
- <font size="3">set(include_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> LCD</font>
- <font size="3"> RMT_RX)</font>
- <font size="3">set(requires</font>
- <font size="3"> driver</font>
- <font size="3"> esp_lcd</font>
- <font size="3"> esp_common)</font>
- <font size="3">idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})</font>
- <font size="3">component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)</font>
复制代码 3. main.c驱动代码
在main.c里面编写如下代码。
- <font size="3">void rmt_rx_scan(rmt_symbol_word_t *rmt_nec_symbols, size_t symbol_num)</font>
- <font size="3">{</font>
- <font size="3"> uint8_t rmt_data = 0;</font>
- <font size="3"> uint8_t tbuf[40];</font>
- <font size="3"> char *str = 0;</font>
- <font size="3"> switch (symbol_num) /* 解码RMT接收数据 */</font>
- <font size="3"> {</font>
- <font size="3"> case 34: /* 正常NEC数据帧 */</font>
- <font size="3"> {</font>
- <font size="3"> if (rmt_nec_parse_frame(rmt_nec_symbols) )</font>
- <font size="3"> {</font>
- <font size="3"> rmt_data = (s_nec_code_command >> 8);</font>
- <font size="3"> switch (rmt_data)</font>
- <font size="3"> {</font>
- <font size="3"> case 0xBA: str = "POWER"; break;</font>
- <font size="3"> case 0xB9: str = "UP"; break;</font>
- <font size="3"> case 0xB8: str = "ALIENTEK"; break;</font>
- <font size="3"> case 0xBB: str = "BACK"; break;</font>
- <font size="3"> case 0xBF: str = "PLAY/PAUSE"; break;</font>
- <font size="3"> case 0xBC: str = "FORWARD"; break;</font>
- <font size="3"> case 0xF8: str = "vol-"; break;</font>
- <font size="3"> case 0xEA: str = "DOWN"; break;</font>
- <font size="3"> case 0xF6: str = "VOL+"; break;</font>
- <font size="3"> case 0xE9: str = "1"; break;</font>
- <font size="3"> case 0xE6: str = "2"; break;</font>
- <font size="3"> case 0xF2: str = "3"; break;</font>
- <font size="3"> case 0xF3: str = "4"; break;</font>
- <font size="3"> case 0xE7: str = "5"; break;</font>
- <font size="3"> case 0xA1: str = "6"; break;</font>
- <font size="3"> case 0xF7: str = "7"; break;</font>
- <font size="3"> case 0xE3: str = "8"; break;</font>
- <font size="3"> case 0xA5: str = "9"; break;</font>
- <font size="3"> case 0xBD: str = "0"; break;</font>
- <font size="3"> case 0xB5: str = "DELETE"; break;</font>
- <font size="3"> }</font>
- <font size="3"> lcd_fill(86, 110, 286, 150, WHITE);</font>
- <font size="3"> sprintf((char *)tbuf, "%d", rmt_data);</font>
- <font size="3"> lcd_show_string(86, 110, 200, 16, 16, (char *)tbuf, BLUE);</font>
- <font size="3"> sprintf((char *)tbuf, "%s", str);</font>
- <font size="3"> lcd_show_string(86, 130, 200, 16, 16, (char *)tbuf, BLUE);</font>
- <font size="3"> ESP_LOGI(TAG, "KEYVAL = %d, Command=%04X", rmt_data, </font>
- <font size="3">s_nec_code_command);</font>
- <font size="3"> }</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 2: /* 重复NEC数据帧 */</font>
- <font size="3"> {</font>
- <font size="3"> if (rmt_nec_parse_frame_repeat(rmt_nec_symbols))</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGI(TAG,"KEYVAL = %d, Command = %04X, repeat", rmt_data, </font>
- <font size="3">s_nec_code_command);</font>
- <font size="3"> }</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> default: /* 未知NEC数据帧 */</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGI(TAG, "Unknown NEC frame");</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> }</font>
- <font size="3">}</font>
- <font size="3">void app_main(void)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret;</font>
- <font size="3"> rmt_rx_done_event_data_t rx_data;</font>
- <font size="3"> </font>
- <font size="3">ret = nvs_flash_init(); /* 初始化NVS */</font>
- <font size="3"> if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_erase());</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_init());</font>
- <font size="3"> }</font>
- <font size="3"> led_init(); /* LED初始化 */</font>
- <font size="3"> lcd_init(); /* LCD屏初始化 */</font>
- <font size="3">rmt_nec_rx_init(); /* 红外接收初始化 */ </font>
- <font size="3"> </font>
- <font size="3"> lcd_show_string(30, 50, 200, 16, 16, "ESP32-P4", RED);</font>
- <font size="3"> lcd_show_string(30, 70, 200, 16, 16, "REMOTE RX TEST", RED);</font>
- <font size="3"> lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);</font>
- <font size="3"> lcd_show_string(30, 110, 200, 16, 16, "KEYVAL:", RED);</font>
- <font size="3"> lcd_show_string(30, 130, 200, 16, 16, "SYMBOL:", RED);</font>
- <font size="3"> while (1)</font>
- <font size="3"> {</font>
- <font size="3"> if (xQueueReceive(receive_queue,&rx_data,pdMS_TO_TICKS(1000)) == pdPASS) </font>
- <font size="3"> { /* 解析接收符号并打印结果 */</font>
- <font size="3"> rmt_rx_scan(rx_data.received_symbols, rx_data.num_symbols); </font>
- <font size="3"> ESP_ERROR_CHECK(rmt_receive(rx_channel, raw_symbols,</font>
- <font size="3"> sizeof(raw_symbols), &receive_config)); /* 重新开始接收 */</font>
- <font size="3"> }</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(10));</font>
- <font size="3"> }</font>
- <font size="3">}</font>
复制代码 rmt_rx_scan函数用来扫描解码结果的,相当于我们的按键扫描函数,从中可以得到红外遥控器按下的按键值(控制码)。
在app_main函数,调用rmt_nec_rx_init函数初始化RMT外设后,在while中就得到消息队列有数据,便可调用rmt_tx_scan对数据进行解析,得到在程序中得知哪个按键被按下。解析完数据后,还需要继续调用rmt_receive函数重新开始接收数据。
28.4 下载验证
在代码编译成功后,下载代码到开发板上,可以看到LCD显示如下图所示。
图28.4.1 红外接收实验测试图
此时,我们通过遥控器按下不同的按键,则可以看到LCD上显示了不同按键的键值和对应遥控器上的符号,如下图所示。
图28.4.2 红外接收实验测试图 |