ESP-IDF学习笔记-RMT的使用

作为物联网芯片,ESP32C3包含一个RMT外设,可以用来产生一些单线协议信号。可以实现NEC协议(红外遥控器使用),或者驱动WS2812灯珠。这边以WS2812的协议为例,学习使用该外设。

官方文档

相关声明在driver/rmt_tx.h或者driver/rmt_rx.h下。

外设简介

有一些通信协议使用了单线协议,利用高低电平的不同时间表示0和1,这样可以节省信道数量,但是这对于软件来说很不好实现。比如常用的红外协议NEC:

image-20240323111218986

需要控制高电平时间和低电平时间来分别传输0和1,这对软件来说比较难控制,因此ESP32包含了一个专用外设RMT用来实现这类时序。

ESP32C3有一个RMT外设,包含多个通道,每个通道可单独设置为发送和接收。RMT使用RMT symbol配置高低电平的时间,在IDF中数据类型为rmt_symbol_word_t

image-20240323112953162

用15bit设定持续多长的RMT ticks,一个bit(L)设定是高电平还是低电平。翻译过来就是一个这样的信息:{(11,high,7,low),(5,high,5l,ow)}。

发送通道

安装TX通道

首先我们需要安装通道。一个通道由rmt_channel_handle_t句柄进行管理,使用rmt_tx_channel_config_t进行设置,使用rmt_new_tx_channel()进行创建。

rmt_tx_channel_config_t定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
gpio_num_t gpio_num;
rmt_clock_source_t clk_src;
uint32_t resolution_hz;
size_t mem_block_symbols;
size_t trans_queue_depth;
struct {
uint32_t invert_out: 1;
uint32_t with_dma: 1;
uint32_t io_loop_back: 1;
uint32_t io_od_mode: 1;
} flags;
} rmt_tx_channel_config_t;
  • gpio_num:设置输出IO引脚。
  • clk_src:选择时钟源,一般选择RMT_CLK_SRC_DEFAULT即APB时钟。
  • resolution_hz:设置RMT tick的频率。
  • mem_block_symbols:必须为偶数。普通模式下设置该通道使用的RMT内存块数量,至少为 48(ESP32C3没有RMTDMA,因此不考虑DMA模式)-越大灯珠闪烁越小
  • trans_queue_depth:内部事务队列深度。队列越深,在待处理队列中可以准备的事务越多。
  • invert_out:设置输出是否反相(电平相反)
  • with_dma:是否开启dma,C3没有,因此不用管。
  • io_loop_back:启用通道所分配的 GPIO 上的输入和输出功能,将发送通道和接收通道绑定到同一个 GPIO 上,从而实现回环功能。配合io_od_mode可以实现一些单线协议。
  • io_od_mode:配合io_loop_back,将io口设置为开漏模式,这样可以监控输入。

使用例:

1
2
3
4
5
6
7
8
9
10
11
rmt_channel_handle_t led_chan = NULL;
rmt_tx_channel_config_t tx_chan_config = {
.clk_src = RMT_CLK_SRC_DEFAULT, // select source clock
.gpio_num = 0,
.mem_block_symbols = 64, // increase the block size can make the LED less flickering
.resolution_hz = 10000000,
.trans_queue_depth = 4, // set the number of transactions that can be pending in the background
.flags.invert_out = false,
.flags.with_dma = false,
};
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &led_chan));

如果需要卸载,使用rmt_del_channel() 函数释放资源。

载波调制

RMT发生器可以将信号调制到高频:

image-20240324215824803

相关配置结构体为rmt_carrier_config_t

1
2
3
4
5
6
7
8
typedef struct {
uint32_t frequency_hz; /*载波频率 */
float duty_cycle; /*载波占空比 0~1 */
struct {
uint32_t polarity_active_low: 1; /*极性选择,决定载波调制到高电平还是低电平,默认是高电平 */
uint32_t always_on: 1; /*设置空闲时是否发送载波 */
} flags;
} rmt_carrier_config_t;

使用rmt_apply_carrier()应用到具体通道。

1
2
3
4
5
6
7
rmt_carrier_config_t tx_carrier_cfg = {
.duty_cycle = 0.33, // 载波占空比为 33%
.frequency_hz = 38000, // 38 KHz
.flags.polarity_active_low = false, // 载波应调制到高电平
};
// 将载波调制到 TX 通道
ESP_ERROR_CHECK(rmt_apply_carrier(tx_chan, &tx_carrier_cfg));

RMT编码器

对于RMT外设来说,它只会将内存块中的RMT Symbol按顺序发出,并没有解析byte数据的能力,因此IDF在驱动层面添加了一个编码器事务机制,用于在传输中将数据编码为RMT Symbol。由于RMT 内存块无法一次性容纳所有数据,在单个事务中,会多次调用编码函数。同时编码器函数运行在中断中,因此建议将其放入IRAM空间运行。

编码器的具体编写方式可以参考官方文档,给出了NEC协议的编码器。

这里给出官方例程中的LED编码器实现:

  • led_strip_encoder.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once

#include <stdint.h>
#include "driver/rmt_encoder.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
* @brief Type of led strip encoder configuration
*/
typedef struct {
uint32_t resolution; /*!< Encoder resolution, in Hz */
} led_strip_encoder_config_t;

/**
* @brief Create RMT encoder for encoding LED strip pixels into RMT symbols
*
* @param[in] config Encoder configuration
* @param[out] ret_encoder Returned encoder handle
* @return
* - ESP_ERR_INVALID_ARG for any invalid arguments
* - ESP_ERR_NO_MEM out of memory when creating led strip encoder
* - ESP_OK if creating encoder successfully
*/
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);

#ifdef __cplusplus
}
#endif

  • led_strip_encoder.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/*
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/

#include "esp_check.h"
#include "led_strip_encoder.h"

static const char *TAG = "led_encoder";

typedef struct {
rmt_encoder_t base;
rmt_encoder_t *bytes_encoder;
rmt_encoder_t *copy_encoder;
int state;
rmt_symbol_word_t reset_code;
} rmt_led_strip_encoder_t;

static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder;
rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder;
rmt_encode_state_t session_state = RMT_ENCODING_RESET;
rmt_encode_state_t state = RMT_ENCODING_RESET;
size_t encoded_symbols = 0;
switch (led_encoder->state) {
case 0: // send RGB data
encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = 1; // switch to next state when current encoding session finished
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
// fall-through
case 1: // send reset code
encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code,
sizeof(led_encoder->reset_code), &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = RMT_ENCODING_RESET; // back to the initial encoding session
state |= RMT_ENCODING_COMPLETE;
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
}
out:
*ret_state = state;
return encoded_symbols;
}

static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_del_encoder(led_encoder->bytes_encoder);
rmt_del_encoder(led_encoder->copy_encoder);
free(led_encoder);
return ESP_OK;
}

static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_reset(led_encoder->bytes_encoder);
rmt_encoder_reset(led_encoder->copy_encoder);
led_encoder->state = RMT_ENCODING_RESET;
return ESP_OK;
}

esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
{
esp_err_t ret = ESP_OK;
rmt_led_strip_encoder_t *led_encoder = NULL;
ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
led_encoder = calloc(1, sizeof(rmt_led_strip_encoder_t));
ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder");
led_encoder->base.encode = rmt_encode_led_strip;
led_encoder->base.del = rmt_del_led_strip_encoder;
led_encoder->base.reset = rmt_led_strip_encoder_reset;
// different led strip might have its own timing requirements, following parameter is for WS2812
rmt_bytes_encoder_config_t bytes_encoder_config = {
.bit0 = {
.level0 = 1,
.duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
},
.bit1 = {
.level0 = 1,
.duration0 = 0.9 * config->resolution / 1000000, // T1H=0.9us
.level1 = 0,
.duration1 = 0.3 * config->resolution / 1000000, // T1L=0.3us
},
.flags.msb_first = 1 // WS2812 transfer bit order: G7...G0R7...R0B7...B0
};
ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed");
rmt_copy_encoder_config_t copy_encoder_config = {};
ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(&copy_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed");

uint32_t reset_ticks = config->resolution / 1000000 * 50 / 2; // reset code duration defaults to 50us
led_encoder->reset_code = (rmt_symbol_word_t) {
.level0 = 0,
.duration0 = reset_ticks,
.level1 = 0,
.duration1 = reset_ticks,
};
*ret_encoder = &led_encoder->base;
return ESP_OK;
err:
if (led_encoder) {
if (led_encoder->bytes_encoder) {
rmt_del_encoder(led_encoder->bytes_encoder);
}
if (led_encoder->copy_encoder) {
rmt_del_encoder(led_encoder->copy_encoder);
}
free(led_encoder);
}
return ret;
}

使用方法:

1
2
3
4
5
6
ESP_LOGI(TAG, "Install led strip encoder");
rmt_encoder_handle_t led_encoder = NULL;
led_strip_encoder_config_t encoder_config = {
.resolution = RMT_LED_STRIP_RESOLUTION_HZ,
};
ESP_ERROR_CHECK(rmt_new_led_strip_encoder(&encoder_config, &led_encoder));

启用通道

在正式发送数据前,使用rmt_enable()启动外设。启用 TX 通道会启用特定中断,并使硬件准备发送事务。

相反,rmt_disable() 会禁用中断并清除队列中的中断,同时禁用发射器和接收器。

发送数据

rmt_transmit()函数用于启动发送事务:

1
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);

channel:发送通道的句柄

encoder:之前创建好的编码器

payload:要发送的数据。

payload_bytes:要发送的数据字节数。

config:发送设置

1
2
3
4
5
6
typedef struct {
int loop_count; /*设置发送循环次数,设置-1为无限循环*/
struct {
uint32_t eot_level : 1; /*设置发送结束后电平 */
} flags;
} rmt_transmit_config_t;

使用该函数后会创建一个事务,并将其发送到作业队伍中,并在中断中调度。因此这个是非阻塞的发送,可以使用rmt_tx_wait_all_done()等待发送结束。

1
ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY));

使用例子:

1
2
3
4
5
rmt_transmit_config_t tx_config = {
.loop_count = 0, // no transfer loop
};
ESP_ERROR_CHECK(rmt_transmit(led_chan, led_encoder, led_strip_pixels, sizeof(led_strip_pixels), &tx_config));
ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY));

同步发送

(待续)

接收通道

(待续)

IRAM安全

默认情况下,禁用 cache 时,写入/擦除主 flash 等原因将导致 RMT 中断延迟,事件回调函数也将延迟执行。在实时应用程序中,应避免此类情况。此外,当 RMT 事务依赖 交替 中断连续编码或复制 RMT 符号时,上述中断延迟将导致不可预测的结果。

因此,可以启用 Kconfig 选项 CONFIG_RMT_ISR_IRAM_SAFE,该选项:

  1. 支持在禁用 cache 时启用所需中断
  2. 支持将 ISR 使用的所有函数存放在 IRAM 中
  3. 支持将驱动程序实例存放在 DRAM 中,以防其意外映射到 PSRAM 中

启用该选项可以保证 cache 禁用时的中断运行,但会相应增加 IRAM 占用。

另外一个 Kconfig 选项 CONFIG_RMT_RECV_FUNC_IN_IRAM 可以将 rmt_receive() 函数放进内部的 IRAM 中,从而当 flash cache 被关闭的时候,这个函数也能够被使用。

线程安全

rmt_new_tx_channel()rmt_new_rx_channel()rmt_new_sync_manager() 线程安全,其他以 rmt_channel_handle_trmt_sync_manager_handle_t 作为第一个位置参数的函数均非线程安全,调用时应注意互斥锁保护。

rmt_receive()可以在中断中使用。