ESP-IDF学习笔记-RMT的使用 作为物联网芯片,ESP32C3包含一个RMT外设,可以用来产生一些单线协议信号。可以实现NEC协议(红外遥控器使用),或者驱动WS2812灯珠。这边以WS2812的协议为例,学习使用该外设。
官方文档
相关声明在driver/rmt_tx.h
或者driver/rmt_rx.h
下。
外设简介 有一些通信协议使用了单线协议,利用高低电平的不同时间表示0和1,这样可以节省信道数量,但是这对于软件来说很不好实现。比如常用的红外协议NEC:
需要控制高电平时间和低电平时间来分别传输0和1,这对软件来说比较难控制,因此ESP32包含了一个专用外设RMT用来实现这类时序。
ESP32C3有一个RMT外设,包含多个通道,每个通道可单独设置为发送和接收。RMT使用RMT symbol
配置高低电平的时间,在IDF中数据类型为rmt_symbol_word_t
。
用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, .gpio_num = 0 , .mem_block_symbols = 64 , .resolution_hz = 10000000 , .trans_queue_depth = 4 , .flags.invert_out = false , .flags.with_dma = false , }; ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &led_chan));
如果需要卸载,使用rmt_del_channel()
函数释放资源。
载波调制 RMT发生器可以将信号调制到高频:
相关配置结构体为rmt_carrier_config_t
1 2 3 4 5 6 7 8 typedef struct { uint32_t frequency_hz; float duty_cycle; 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 , .frequency_hz = 38000 , .flags.polarity_active_low = false , }; ESP_ERROR_CHECK(rmt_apply_carrier(tx_chan, &tx_carrier_cfg));
RMT编码器 对于RMT外设来说,它只会将内存块中的RMT Symbol按顺序发出,并没有解析byte数据的能力,因此IDF在驱动层面添加了一个编码器事务机制,用于在传输中将数据编码为RMT Symbol。由于RMT 内存块无法一次性容纳所有数据,在单个事务中,会多次调用编码函数。同时编码器函数运行在中断 中,因此建议将其放入IRAM空间运行。
编码器的具体编写方式可以参考官方文档 ,给出了NEC协议的编码器。
这里给出官方例程中的LED编码器实现:
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 #pragma once #include <stdint.h> #include "driver/rmt_encoder.h" #ifdef __cplusplus extern "C" {#endif typedef struct { uint32_t resolution; } led_strip_encoder_config_t ; esp_err_t rmt_new_led_strip_encoder (const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder) ;#ifdef __cplusplus } #endif
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 #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 : encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state); if (session_state & RMT_ENCODING_COMPLETE) { led_encoder->state = 1 ; } if (session_state & RMT_ENCODING_MEM_FULL) { state |= RMT_ENCODING_MEM_FULL; goto out; } case 1 : 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; state |= RMT_ENCODING_COMPLETE; } if (session_state & RMT_ENCODING_MEM_FULL) { state |= RMT_ENCODING_MEM_FULL; goto out; } } 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; rmt_bytes_encoder_config_t bytes_encoder_config = { .bit0 = { .level0 = 1 , .duration0 = 0.3 * config->resolution / 1000000 , .level1 = 0 , .duration1 = 0.9 * config->resolution / 1000000 , }, .bit1 = { .level0 = 1 , .duration0 = 0.9 * config->resolution / 1000000 , .level1 = 0 , .duration1 = 0.3 * config->resolution / 1000000 , }, .flags.msb_first = 1 }; 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(©_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed" ); uint32_t reset_ticks = config->resolution / 1000000 * 50 / 2 ; 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; 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 , }; 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 ,该选项:
支持在禁用 cache 时启用所需中断
支持将 ISR 使用的所有函数存放在 IRAM 中
支持将驱动程序实例存放在 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_t
和 rmt_sync_manager_handle_t
作为第一个位置参数的函数均非线程安全,调用时应注意互斥锁保护。
rmt_receive()
可以在中断中使用。