RMT
1. RMT简介
1.1 概述
红外是设备间通讯的一种方案,一般在空调、电视、机顶盒等设备中被使用。其原理是通过定义好设备接收端和发送端的编码协议,按照特定的脉冲,由发射管通过控制亮灭发送红外信息。然后由接收管进行接收。
在设备间通讯的方案中,红外具有使用简单的优点,但也存在使用距离短,中间不能由物体阻隔等缺点。
1.2 红外编码
1.2.1 编码组成
这里简单介绍一下NEC红外编码,一般NEC编码包括引导码和数据码两部分。下面的NEC编码引导码和数据码是基于一种海尔空调可用的编码协议写的。
引导码预示着后面是有效信息,引导码由一段比较长时间的高低电平构成,比如下图
而数据码通过特定的协议编码0和1,比如500us低电平,跟着1600us高电平编码数据1,而500us低电平跟着600us高电平意味着数据0
1.2.2 载波
同时,红外编码发送的时候,并不单单是通过高低电平发送的,是在38khz的载波下进行发送的。
就比如如下案例,引导码和数据码高电平区域,是由38khz的脉冲构成的。
一般不同设备(尤其是空调)的红外编码协议会又所不同,所以进行红外设备控制的时候,需要进行测试。
1.3 RMT组件概述
emp32的RMT组件一共由8个通道,每个通道能够独立完成红外发射或者红外接收的工作,但是这两种功能不能同时进行。8个通道共用同一个RAM空间,具有完成载波调制、输入捕获、滤波等功能。
2. RMT框图剖析
esp32的RMT模块功能框图如下,包括四个部分
- (1) 时钟源
- (2) RAM存储空间
- (3) 发射模块
- (4) 接收模块
2.1 时钟
RMT模块的时钟源经过分频后可以给红外接收和发送的计数器提供时钟。
RMT模块的时钟源可以是APB_CLK(默认是80MHZ),也可以是REF_TICK时钟。
经过分频以后的每个时钟,将会在后面被称为1个tick。
比如默认时钟是80MHZ,如果进行80分频,用于计数的时钟就是1MHZ,也就是每次数一个数字都是1us,1个tick就是1us。
2.2 RAM
RMT组件8个通道共用同一段RAM,RAM分为0-7 一共8个block,每个block大小为64x32 bit。默认情况下每个通道分配一个block。主要用于红外接收和发送的时候存储数据
RMT的空间结构如下图
RAM的数据结构由32位组成,分为高低两个16位。其中level表示电平的高低,period表示分频时钟持续的周期数。当period为0的时候,发送停止。如果接收超时,也会往period中写入0,表示接收结束。这1个32位,用于表示电平高低和时钟持续周期数的数据结构定义,将在后面被称为item
RAM可以通过数据总结,直接使用地址进行访问。其起始地址为 0x3FF56800。如果一个通道接收或者发送红外数据的时候一个block大小不够(因为block大小为64x32bit,意味着最多能存储128个数据),可以在配置的时间增加block数量。通道n使用的RMA起始内存和终止内存地址计算公式为:
2.3 发送器
从框图中可以看出,发送模块包括两个部分,包括发射信号输出和调制信号输出两部分。
发射信号输出部分就是把给定的item数据结构中对电平高低和周期数的描述,转化为实际的电平输出。
如我们定义如下数据
static const rmt_item32_t morse_esp[] = {
{{{ 3276, 1, 3276, 0 }}},
{{{ 3276, 0, 3276, 0 }}},
{{{ 3276, 1, 3276, 0 }}},
{{{ 3276, 1, 3276, 0 }}},
{{{ 3276, 1, 3276, 0 }}},
{{{ 3276, 0, 3276, 0 }}},
{{{ 3276, 1, 3276, 0 }}},
{{{ 3276, 1, 3276, 1 }}},
{{{ 3276, 1, 3276, 0 }}},
{{{ 3276, 1, 3276, 1 }}},
{{{ 3276, 1, 3276, 0 }}},
{{{ 3276, 1, 3276, 0 }}},
{{{ 0, 1, 0, 0 }}}
};
就会产生这样的电信号输出
另外一部分是信号的调制,用于对输出的信号增加载波,变成红外支持的编码格式。比如我们在配置中,给上文中的发送信号增加载波,就会变成下图
放大后,发现原来的每个高电平,都是由38khz的脉冲构成的
2.4 接收器
接收器做的事情与发送器相反,是把引脚出的电平转换为item数据结构进行存储。并且每次在电平变化的时候,记录上一个电平的周期数和电平高低。当定时器超过给定的最大超时时间,接收器会在item中写入0,表示接收结束。
3. RMT结构体配置说明
RMT配置是通过配置rmt_config_t结构体实现的,rmt_config_t可以分为公共配置部分和特有配置部分,该结构体定义为:
typedef struct {
rmt_mode_t rmt_mode; // 配置RMT模块是发射或接收
rmt_channel_t channel; // 使用第几个通道
uint8_t clk_div; // 对时钟源进行多少分频,可以配置0-255,其中0表示256分频
gpio_num_t gpio_num; //使用第几个gpio完成该工作
uint8_t mem_block_num; // 使用多少个block的RAM进行收发数据
union{
rmt_tx_config_t tx_config; // 如果模式是接收,就配置这部分
rmt_rx_config_t rx_config; // 如果模式是发送,就配置这部分
};
} rmt_config_t;
4. RMT发送实验
4.1 功能描述
该实验讲解使用RMT进行发送的具体方法
4.2 硬件设计
完成该实验可以不使用任何外部电路。该实验使用GPIO_4作为输出示范,使用示波器检测电平变化即可
4.3 软件设计
4.3.1 配置结构体的公共部分
首先配置RMT模块结构体的公共部分,包括配置好使用的通道,对APB时钟进行多少分频,使用哪个引脚,block通道是多少,以及配置该通道的工作模式
示例代码
//01 rmt driver共有部分
rmt_config_t rmt;
rmt.channel = RMT_CHANNEL_0; //RMT有0-7一共8个通道
rmt.clk_div = 80; //默认的时钟是80MHZ,分频器是8位的,这个时钟与发送信号时候电平长度以及接收信号时候计数有关系。 n个时钟周期就是电平长度.如果80分频,数一个数就是1us
rmt.gpio_num = GPIO_NUM_4;
rmt.mem_block_num = 1; //默认每个通道使用1个block。一共block是64x32bit 这么大。 也就是能储存128个数据
rmt.rmt_mode = RMT_MODE_TX; //配置发送模式
- 在该案例中,我使用通道0进行试验,并且把通道0的工作模式设置为发送模式。
- 对时钟进行80分频,这样的话,对item的解析中,每个period都代表1us。
- 使用GPIO_NUM_4作为输出。
- 配置该通道占用1个block,1个block大小为64x32 bit,因为每个item可以存储2个电平,因此1个block,64个item,最多能够发送128个电平数据
4.3.2 配置结构体的发射部分
结构体的该部分最重要的是配置载波。
红外遥控器的话,一般是使用38khz载波
//02 配置tx独有的部分
rmt.tx_config.carrier_en = true; //打开载波
rmt.tx_config.carrier_freq_hz = 38000; //38khz载波
rmt.tx_config.carrier_duty_percent = 50; //占空比50%
rmt.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH; //载波默认为高电平
rmt.tx_config.idle_output_en = true; //空闲输出打开
rmt.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; //空闲时候为低电平
rmt.tx_config.loop_en = false; //关闭持续发送
- 在该发射案例中,我打开了载波,并设置载波频率为38
- 然后设置载波的占空比为50%
- 载波在高电平部分有效
- 打开空闲状态下的电平输出,并设置为低电平
- 关闭持续发送,意味着,每次发送都只发送一次数据。如果开启发送,会重复的发送RAM中的数据
4.3.3 载入结构体配置
与结构体配置载入有关的两个函数是
rmt_config()
rmt_driver_install()
- rmt_config()函数的功能是载入rmt_config_t结构体的数据
- rmt_driver_install()函数的功能是,为特定通道配置环形缓冲区。该缓冲区只在配置红外接收的时候有用。因为发射的时候是直接把自己定义的内存地址中的数据写入发射block即可。而接收的时候是把RAM的数据搬运到环形缓冲区ringbuff,然后把该缓冲区中的数据解析为item的数组。
- 配置rmt_driver_install()函数后,系统会对RMT自动生成一个中断服务函数,用于处理ringbuff缓冲区中的数据,因此,一旦定义了这个函数以后,请不要使用RMT模块中的接收数据完成中断、发送数据完成中断,以及RAM访问冲突等中断。
//03 进行配置
rmt_config(&rmt);
//04 加载配置
rmt_driver_install(RMT_CHANNEL_0, 0, 0); //发送不需要缓冲区,中断级别默认
4.3.4 定义要发送的数据
static const rmt_item32_t morse_esp[] = {
// E : dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 0, 3270, 0 }}}, // SPACE
// S : dot, dot, dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 0, 3270, 0 }}}, // SPACE
// P : dot, dash, dash, dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 1, 3270, 1 }}},
{{{ 3270, 1, 3270, 0 }}}, // dash
{{{ 3270, 1, 3270, 1 }}},
{{{ 3270, 1, 3270, 0 }}}, // dash
{{{ 3270, 1, 3270, 0 }}}, // dot
// RMT end marker
{{{ 0, 1, 0, 0 }}}
};
4.3.5 发送数据
发送数据主要使用函数为rmt_write_items,配置的参数包括使用哪个通道发送数据,发送数据地址,以及发送数据的大小。是否进行阻塞状态,即发送后进行等待,一直等待数据发送完成后,从阻塞状态退出。
如果阻塞状态为false,可以使用 rmt_wait_tx_done函数进行阻塞,等待发送完成。
rmt_write_items(RMT_CHANNEL_0, morse_esp, sizeof(morse_esp) / sizeof(morse_esp[0]), true);
4.3.6 完整发送数据的程序
/*
Name: Sketch1.ino
Created: 2021/4/12 18:46:05
Author: hp
*/
// the setup function runs once when you press reset or power the board
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "led.h"
#include "uart.h"
#include "driver/rmt.h"
//红外发送模拟
static const rmt_item32_t morse_esp[] = {
// E : dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 0, 3270, 0 }}}, // SPACE
// S : dot, dot, dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 0, 3270, 0 }}}, // SPACE
// P : dot, dash, dash, dot
{{{ 3270, 1, 3270, 0 }}}, // dot
{{{ 3270, 1, 3270, 1 }}},
{{{ 3270, 1, 3270, 0 }}}, // dash
{{{ 3270, 1, 3270, 1 }}},
{{{ 3270, 1, 3270, 0 }}}, // dash
{{{ 3270, 1, 3270, 0 }}}, // dot
// RMT end marker
{{{ 0, 1, 0, 0 }}}
};
static void rmt_tx_init(void)
{
//01 rmt driver共有部分
rmt_config_t rmt;
rmt.channel = RMT_CHANNEL_0; //RMT有0-7一共8个通道
rmt.clk_div = 80; //默认的时钟是80MHZ,分频器是8位的,这个时钟与发送信号时候电平长度以及接收信号时候计数有关系。 n个时钟周期就是电平长度.如果80分频,数一个数就是1us
rmt.gpio_num = GPIO_NUM_4;
rmt.mem_block_num = 1; //默认每个通道使用1个block。一共block是64x32bit 这么大。 也就是能储存128个数据
rmt.rmt_mode = RMT_MODE_TX; //配置发送模式
//02 配置tx独有的部分
rmt.tx_config.carrier_en = true; //打开载波
rmt.tx_config.carrier_freq_hz = 38000; //38khz载波
rmt.tx_config.carrier_duty_percent = 50; //占空比50%
rmt.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH; //载波默认为高电平
rmt.tx_config.idle_output_en = true; //空闲输出打开
rmt.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; //空闲时候为低电平
rmt.tx_config.loop_en = false; //关闭持续发送
//03 进行配置
rmt_config(&rmt);
//04 加载配置
rmt_driver_install(RMT_CHANNEL_0, 0, 0); //发送不需要缓冲区,中断级别默认
}
void setup() {
rmt_tx_init();
while (1)
{
rmt_write_items(RMT_CHANNEL_0, morse_esp, sizeof(morse_esp) / sizeof(morse_esp[0]), true);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void loop()
{
}
5. RMT接收实验
5.1 功能描述
该使用RMT接收功能,接收外遥控器发送的红外码,然后通过串口发送到电脑。
5.2 硬件设计
通过红外接收管接收红外信号,然后把输出引脚接到GPIO_15
5.3 软件设计
5.3.1 配置结构体的公共部分
首先配置RMT模块结构体的公共部分,包括配置好使用的通道,对APB时钟进行多少分频,使用哪个引脚,block通道是多少,以及配置该通道的工作模式
示例代码
//01 配置rmt
//共有部分
rmt_config_t rmt;
rmt.channel = RMT_CHANNEL_0; //一共与0-7 8个通道
rmt.clk_div = 80; //80分频,记一次数字1us
rmt.gpio_num = GPIO_NUM_15; //引脚15作为输入捕获引脚
rmt.mem_block_num = 3; //占用的block个数,一个block是64x32bit
- 在该案例中,我使用通道0进行试验,并且把通道0的工作模式设置为发送模式。
- 对时钟进行80分频,这样的话,对item的解析中,每个period都代表1us。
- 使用GPIO_NUM_15作为输入。
- 配置该通道占用3个block,因为红外输入码长度可能比较长,这个可以自行调整
5.3.2 配置结构体的接收部分
接收结构体最主要的是配置滤波,这里配置的是150us以下的脉冲都进行忽略。
同时设置了超时时间为50ms,如果50ms仍然没有电平变化,就判定超时,中断接收。
//rx私有部分
rmt.rmt_mode = RMT_MODE_RX; //接收模式
rmt.rx_config.filter_en = true; //开启滤波
rmt.rx_config.filter_ticks_thresh = 150; //150us以下的信号不接受
rmt.rx_config.idle_threshold = 50000; //超出时间设置为50ms,超过50ms认为接收信号完成
5.3.3 载入结构体配置
//02 写入配置
rmt_config(&rmt);
//03 rmt驱动
rmt_driver_install(RMT_CHANNEL_0, 2000, 0); //第二个数字如果不是0,就是定义了ringbuff的大小,可以通过乒乓操作的方法,定义接收缓冲区大小
- rmt_config()函数的功能是载入rmt_config_t结构体的数据
- rmt_driver_install()函数的功能是,为特定通道配置环形缓冲区。该缓冲区只在配置红外接收的时候有用。接收的时候是把RAM的数据搬运到环形缓冲区ringbuff,然后把该缓冲区中的数据解析为item的数组。这个ringbuff大小单位是byte,至少要大于红外码占用空间的两倍。
- 我们举个例子,如红外码的item占用空间为400byte,则占用50个item空间,因此至少需要占用2个block,同时,ringbuff空间至少为占用RAM大小的两倍,也就是不能小于800
- 配置rmt_driver_install()函数后,系统会对RMT自动生成一个中断服务函数,用于处理ringbuff缓冲区中的数据,因此,一旦定义了这个函数以后,请不要使用RMT模块中的接收数据完成中断、发送数据完成中断,以及RAM访问冲突等中断。
5.3.4 接收数据
与接收数据有关的函数主要为
- rmt_get_ringbuf_handle():之前通过rmt_driver_install()函数设置好了,让RAM中的数据完成接收后,存放到ringbuff中,这个函数就是用来获取这段ringbuff内存地址的
- rmt_rx_start():打开接收,这个函数使用之后,就好不断的进行接收红外数据
- xRingbufferReceive():这个函数用于获”取ringbuff中的数据,并转换为item格式进行返回。。这个函数最后一个参数是超时时间,也就是系统运行到这个函数的时候会进入阻塞状态,一直等待数据。实际测试下来,这个超时时间的单位似乎是ms
- vRingbufferReturnItem():释放自己使用的ringbuff和item数据
- rmt_rx_stop():停止接收数据
/ 定义接收变量
RingbufHandle_t rb = NULL; //循环缓冲区指针
rmt_item32_t* items = NULL; //items数据域指针
size_t length = 0; //items占用字节数,一个items占4字节
// 把ringbuf数据区域绑定到rb
rmt_get_ringbuf_handle(RMT_CHANNEL_0, &rb);
//04 打开接收
rmt_rx_start(RMT_CHANNEL_0, true);
while (1) {
// 获取items数据
items = (rmt_item32_t*)xRingbufferReceive(rb, &length, portMAX_DELAY);
// 如果获得到了数据
if (items)
{
//07 打印数据
printf("\n");
printf("\n");
printf("length=%d\n", length/4);
for (size_t t = 0; t < length / 4; t++)
{
printf("%d %d ", items[t].duration0, items[t].duration1);
if (t % 5 == 0)
{
printf("\n");
}
}
// 完成一次数据接收要清空循环缓冲区和items数据域
vRingbufferReturnItem(rb, (void*)items);
}
}
rmt_rx_stop(RMT_CHANNEL_0); //关闭接收
5.3.5 完整数据接收程序
/*
Name: Sketch1.ino
Created: 2021/4/12 18:46:05
Author: hp
*/
// the setup function runs once when you press reset or power the board
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/rmt.h"
#include "led.h"
#include "uart.h"
//配置红外接收
static void rmt_rx_init(void)
{
//01 配置rmt
//共有部分
rmt_config_t rmt;
rmt.channel = RMT_CHANNEL_0; //一共与0-7 8个通道
rmt.clk_div = 80; //80分频,记一次数字1us
rmt.gpio_num = GPIO_NUM_15; //引脚15作为输入捕获引脚
rmt.mem_block_num = 3; //占用的block个数,一个block是64x32bit
//rx私有部分
rmt.rmt_mode = RMT_MODE_RX; //接收模式
rmt.rx_config.filter_en = true; //开启滤波
rmt.rx_config.filter_ticks_thresh = 150; //150us以下的信号不接受
rmt.rx_config.idle_threshold = 50000; //超出时间设置为50ms,超过50ms认为接收信号完成
//02 写入配置
rmt_config(&rmt);
//03 rmt驱动
rmt_driver_install(RMT_CHANNEL_0, 2000, 0); //第二个数字如果不是0,就是定义了ringbuff的大小,可以通过乒乓操作的方法,定义接收缓冲区大小
}
void setup() {
//01 初始化红外接收器
rmt_rx_init();
//02 定义接收变量
RingbufHandle_t rb = NULL; //循环缓冲区指针
rmt_item32_t* items = NULL; //items数据域指针
size_t length = 0; //items占用字节数,一个items占4字节
//03 把ringbuf数据区域绑定到rb
rmt_get_ringbuf_handle(RMT_CHANNEL_0, &rb);
//04 打开接收
rmt_rx_start(RMT_CHANNEL_0, true);
while (1) {
//05 获取items数据
items = (rmt_item32_t*)xRingbufferReceive(rb, &length, portMAX_DELAY);
//06 如果获得到了数据
if (items)
{
//07 打印数据
printf("\n");
printf("\n");
printf("length=%d\n", length/4);
for (size_t t = 0; t < length / 4; t++)
{
printf("%d %d ", items[t].duration0, items[t].duration1);
if (t % 5 == 0)
{
printf("\n");
}
}
//08 完成一次数据接收要清空循环缓冲区和items数据域
vRingbufferReturnItem(rb, (void*)items);
}
}
rmt_rx_stop(RMT_CHANNEL_0); //关闭接收
}
void loop()
{
}
/*
Name: Sketch1.ino
Created: 2021/4/12 18:46:05
Author: hp
*/
// the setup function runs once when you press or power the board
6. 参考资料
- esp32技术参数指南 15 章红外遥控RMT
- ESP32-IDF变成指南 RMT
- ESP32实现红外遥控 发射与接收