Skip to content

Latest commit

 

History

History
614 lines (435 loc) · 41.6 KB

File metadata and controls

614 lines (435 loc) · 41.6 KB

红外遥控 (RMT)

:link_to_translation:`en:[English]`

简介

红外遥控 (RMT) 外设是一个红外发射和接收控制器。其数据格式灵活,可进一步扩展为多功能的通用收发器,发送或接收多种类型的信号。就网络分层而言,RMT 硬件包含物理层和数据链路层。物理层定义通信介质和比特信号的表示方式,数据链路层定义 RMT 帧的格式。RMT 帧的最小数据单元称为 RMT 符号,在驱动程序中以 :cpp:type:`rmt_symbol_word_t` 表示。

{IDF_TARGET_NAME} 的 RMT 外设存在多个通道 [1],每个通道都可以独立配置为发射器或接收器。

RMT 外设通常支持以下场景:

  • 发送或接收红外信号,支持所有红外线协议,如 NEC 协议
  • 生成通用序列
  • 有限或无限次地在硬件控制的循环中发送信号
  • 多通道同时发送
  • 将载波调制到输出信号或从输入信号解调载波

RMT 符号的内存布局

RMT 硬件定义了自己的数据模式,称为 RMT 符号。下图展示了一个 RMT 符号的位字段:每个符号由两对两个值组成,每对中的第一个值是一个 15 位的值,表示信号持续时间,以 RMT 滴答计。每对中的第二个值是一个 1 位的值,表示信号的逻辑电平,即高电平或低电平。

.. packetdiag:: /../_static/diagrams/rmt/rmt_symbols.diag
    :caption: RMT 符号结构(L - 信号电平)
    :align: center

RMT 发射器概述

RMT 发送通道 (TX Channel) 的数据路径和控制路径如下图所示:

.. blockdiag:: /../_static/diagrams/rmt/rmt_tx.diag
    :caption: RMT 发射器概述
    :align: center

驱动程序将用户数据编码为 RMT 数据格式,随后由 RMT 发射器根据编码生成波形。在将波形发送到 GPIO 管脚前,还可以调制高频载波信号。

RMT 接收器概述

RMT 接收通道 (RX Channel) 的数据路径和控制路径如下图所示:

.. blockdiag:: /../_static/diagrams/rmt/rmt_rx.diag
    :caption: RMT 接收器概述
    :align: center

RMT 接收器可以对输入信号采样,将其转换为 RMT 数据格式,并将数据存储在内存中。还可以向接收器提供输入信号的基本特征,使其识别信号停止条件,并过滤掉信号干扰和噪声。RMT 外设还支持从基准信号中解调出高频载波信号。

功能概述

下文将分节概述 RMT 的功能:

资源分配

驱动程序中,:cpp:type:`rmt_channel_handle_t` 用于表示 RMT 的 TX 和 RX 通道。驱动程序在内部管理可用的通道,并在收到请求时提供空闲通道。

安装 RMT TX 通道

要安装 RMT TX 通道,应预先提供配置结构体 :cpp:type:`rmt_tx_channel_config_t`。以下列表介绍了配置结构体中的各个部分。

将必要参数填充到结构体 :cpp:type:`rmt_tx_channel_config_t` 后,可以调用 :cpp:func:`rmt_new_tx_channel` 来分配和初始化 TX 通道。如果函数运行正确,会返回 RMT 通道句柄;如果 RMT 资源池内缺少空闲通道,会返回 :c:macro:`ESP_ERR_NOT_FOUND` 错误;如果硬件不支持 DMA 后端等部分功能,则返回 :c:macro:`ESP_ERR_NOT_SUPPORTED` 错误。

rmt_channel_handle_t tx_chan = NULL;
rmt_tx_channel_config_t tx_chan_config = {
    .clk_src = RMT_CLK_SRC_DEFAULT,   // 选择时钟源
    .gpio_num = 0,                    // GPIO 编号
    .mem_block_symbols = 64,          // 内存块大小,即 64 * 4 = 256 字节
    .resolution_hz = 1 * 1000 * 1000, // 1 MHz 滴答分辨率,即 1 滴答 = 1 µs
    .trans_queue_depth = 4,           // 设置后台等待处理的事务数量
    .flags.invert_out = false,        // 不反转输出信号
    .flags.with_dma = false,          // 不需要 DMA 后端
};
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &tx_chan));

安装 RMT RX 通道

要安装 RMT RX 通道,应预先提供配置结构体 :cpp:type:`rmt_rx_channel_config_t`。以下列表介绍了配置结构体中的各个部分。

将必要参数填充到结构体 :cpp:type:`rmt_rx_channel_config_t` 后,可以调用 :cpp:func:`rmt_new_rx_channel` 来分配和初始化 RX 通道。如果函数运行正确,会返回 RMT 通道句柄;如果 RMT 资源池内缺少空闲通道,会返回 :c:macro:`ESP_ERR_NOT_FOUND` 错误;如果硬件不支持 DMA 后端等部分功能,则返回 :c:macro:`ESP_ERR_NOT_SUPPORTED` 错误。

rmt_channel_handle_t rx_chan = NULL;
rmt_rx_channel_config_t rx_chan_config = {
    .clk_src = RMT_CLK_SRC_DEFAULT,   // 选择时钟源
    .resolution_hz = 1 * 1000 * 1000, // 1 MHz 滴答分辨率,即 1 滴答 = 1 µs
    .mem_block_symbols = 64,          // 内存块大小,即 64 * 4 = 256 字节
    .gpio_num = 2,                    // GPIO 编号
    .flags.invert_in = false,         // 不反转输入信号
    .flags.with_dma = false,          // 不需要 DMA 后端
};
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_chan_config, &rx_chan));

Note

由于 GPIO 驱动程序中的软件限制,当 TX 和 RX 通道都绑定到同一 GPIO 时,请确保在 TX 通道之前初始化 RX 通道。如果先设置 TX 通道,那么在 RX 通道设置期间,GPIO 控制信号将覆盖先前的 RMT TX 通道信号。

卸载 RMT 通道

如果不再需要之前安装的 RMT 通道,建议调用 :cpp:func:`rmt_del_channel` 回收资源,使底层软件与硬件重新用于其他功能。

载波调制与解调

RMT 发射器可以生成载波信号,并将其调制到消息信号上。载波信号的频率远高于消息信号。此外,仅支持配置载波信号的频率和占空比。RMT 接收器可以从输入信号中解调出载波信号。注意,并非所有 ESP 芯片都支持载波调制和解调功能,在配置载波前,请参阅 [TRM]。若所选芯片不支持载波调制和解调功能,可能会报告 :c:macro:`ESP_ERR_NOT_SUPPORTED` 错误。

载波相关配置位于 :cpp:type:`rmt_carrier_config_t` 中,该配置中的各部分详情如下:

Note

RX 通道的载波频率不应设置为理论值,建议为载波频率留出一定的容差。例如,以下代码片段的载波频率设置为 25 KHz,而非 TX 侧配置的 38 KHz。因为信号在空气中传播时会发生反射和折射,导致接收端接收的频率失真。

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_carrier_config_t rx_carrier_cfg = {
    .duty_cycle = 0.33,                 // 载波占空比为 33%
    .frequency_hz = 25000,              // 载波频率为 25 KHz,应小于发射器的载波频率
    .flags.polarity_active_low = false, // 载波调制到高电平
};
// 从 RX 通道解调载波
ESP_ERROR_CHECK(rmt_apply_carrier(rx_chan, &rx_carrier_cfg));

注册事件回调

当 RMT 通道生成发送或接收完成等事件时,会通过中断告知 CPU。如果需要在发生特定事件时调用函数,可以为 TX 和 RX 通道分别调用 :cpp:func:`rmt_tx_register_event_callbacks`:cpp:func:`rmt_rx_register_event_callbacks`,向 RMT 驱动程序的中断服务程序 (ISR) 注册事件回调。由于上述回调函数是在 ISR 中调用的,因此,这些函数不应涉及 block 操作。可以检查调用 API 的后缀,确保在函数中只调用了后缀为 ISR 的 FreeRTOS API。回调函数具有布尔返回值,指示回调是否解除了更高优先级任务的阻塞状态。

有关 TX 通道支持的事件回调,请参阅 :cpp:type:`rmt_tx_event_callbacks_t`

有关 RX 通道支持的事件回调,请参阅 :cpp:type:`rmt_rx_event_callbacks_t`

也可使用参数 user_data,在 :cpp:func:`rmt_tx_register_event_callbacks`:cpp:func:`rmt_rx_register_event_callbacks` 中保存自定义上下文。用户数据将直接传递给每个回调函数。

在回调函数中可以获取驱动程序在 edata 中填充的特定事件数据。注意,edata 指针仅在回调的持续时间内有效。

有关 TX 完成事件数据的定义,请参阅 :cpp:type:`rmt_tx_done_event_data_t`

有关 RX 完成事件数据的定义,请参阅 :cpp:type:`rmt_rx_done_event_data_t`

启用及禁用通道

在发送或接收 RMT 符号前,应预先调用 :cpp:func:`rmt_enable`。启用 TX 通道会启用特定中断,并使硬件准备发送事务。启用 RX 通道也会启用中断,但由于传入信号的特性尚不明确,接收器不会在此时启动,而是在 :cpp:func:`rmt_receive` 中启动。

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

ESP_ERROR_CHECK(rmt_enable(tx_chan));
ESP_ERROR_CHECK(rmt_enable(rx_chan));

发起 TX 事务

RMT 是一种特殊的通信外设,无法像 SPI 和 I2C 那样发送原始字节流,只能以 :cpp:type:`rmt_symbol_word_t` 格式发送数据。然而,硬件无法将用户数据转换为 RMT 符号,该转换只能通过 RMT 编码器在软件中完成。编码器将用户数据编码为 RMT 符号,随后写入 RMT 内存块或 DMA 缓冲区。有关创建 RMT 编码器的详细信息,请参阅 :ref:`rmt-rmt-encoder`

获取编码器后,调用 :cpp:func:`rmt_transmit` 启动 TX 事务,该函数会接收少数位置参数,如通道句柄、编码器句柄和有效负载缓冲区。此外,还需要在 :cpp:type:`rmt_transmit_config_t` 中提供专用于发送的配置,具体如下:

Note

如果将 :cpp:member:`rmt_transmit_config_t::loop_count` 设置为非零值,即启用循环功能,则传输的大小将受到限制。编码的 RMT 符号不应超过 RMT 硬件内存块容量,否则会出现类似 encoding artifacts can't exceed hw memory block for loop transmission 的报错信息。如需通过循环启动大型事务,请尝试以下任一方法:

:cpp:func:`rmt_transmit` 会在其内部构建一个事务描述符,并将其发送到作业队列中,该队列将在 ISR 中调度。因此,在 :cpp:func:`rmt_transmit` 返回时,事务可能尚未启动。为确保完成所有挂起的事务,请调用 :cpp:func:`rmt_tx_wait_all_done`

多通道同时发送

在一些实时控制应用程序中,启动多个 TX 通道(例如使两个器械臂同时移动)时,应避免出现任何时间漂移。为此,RMT 驱动程序可以创建 同步管理器 帮助管理该过程。在驱动程序中,同步管理器为 :cpp:type:`rmt_sync_manager_handle_t`。RMT 同步发送过程如下图所示:

RMT TX Sync

RMT TX 同步发送

安装 RMT 同步管理器

要创建同步管理器,应预先在 :cpp:type:`rmt_sync_manager_config_t` 中指定要管理的通道:

成功调用 :cpp:func:`rmt_new_sync_manager` 函数将返回管理器句柄,该函数也可能因为无效参数等错误而无法调用。在已经安装了同步管理器,且缺少硬件资源来创建另一个管理器时,该函数将报告 :c:macro:`ESP_ERR_NOT_FOUND` 错误。此外,如果硬件不支持同步管理器,将报告 :c:macro:`ESP_ERR_NOT_SUPPORTED` 错误。在使用同步管理器功能之前,请参阅 [TRM]。

发起同时发送

在调用 :cpp:member:`rmt_sync_manager_config_t::tx_channel_array` 中所有通道上的 :cpp:func:`rmt_transmit` 前,任何受管理的 TX 通道都不会启动发送机制,而是处于待命状态。由于各通道事务不同,TX 通道通常会在不同的时间完成相应事务,这可能导致无法同步。因此,在重新启动同时发送程序之前,应调用 :cpp:func:`rmt_sync_reset` 函数重新同步所有通道。

调用 :cpp:func:`rmt_del_sync_manager` 函数可以回收同步管理器,并使通道可以在将来独立启动发送程序。

rmt_channel_handle_t tx_channels[2] = {NULL}; // 声明两个通道
int tx_gpio_number[2] = {0, 2};
// 依次安装通道
for (int i = 0; i < 2; i++) {
    rmt_tx_channel_config_t tx_chan_config = {
        .clk_src = RMT_CLK_SRC_DEFAULT,       // 选择时钟源
        .gpio_num = tx_gpio_number[i],    // GPIO 编号
        .mem_block_symbols = 64,          // 内存块大小,即 64 * 4 = 256 字节
        .resolution_hz = 1 * 1000 * 1000, // 1 MHz 分辨率
        .trans_queue_depth = 1,           // 设置可以在后台挂起的事务数量
    };
    ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &tx_channels[i]));
}
// 安装同步管理器
rmt_sync_manager_handle_t synchro = NULL;
rmt_sync_manager_config_t synchro_config = {
    .tx_channel_array = tx_channels,
    .array_size = sizeof(tx_channels) / sizeof(tx_channels[0]),
};
ESP_ERROR_CHECK(rmt_new_sync_manager(&synchro_config, &synchro));

ESP_ERROR_CHECK(rmt_transmit(tx_channels[0], led_strip_encoders[0], led_data, led_num * 3, &transmit_config));
// 只有在调用 tx_channels[1] 的 rmt_transmit() 函数返回后,tx_channels[0] 才会开始发送数据。
ESP_ERROR_CHECK(rmt_transmit(tx_channels[1], led_strip_encoders[1], led_data, led_num * 3, &transmit_config));

发起 RX 事务

:ref:`rmt-enable-and-disable-channel` 一节所述,仅调用 :cpp:func:`rmt_enable` 时,RX 通道无法接收 RMT 符号。为此,应在 :cpp:type:`rmt_receive_config_t` 中指明传入信号的基本特征:

根据以上配置调用 :cpp:func:`rmt_receive` 后,RMT 接收器会启动 RX 机制。注意,以上配置均针对特定事务存在,也就是说,要开启新一轮的接收时,需要再次设置 :cpp:type:`rmt_receive_config_t` 选项。接收器会将传入信号以 :cpp:type:`rmt_symbol_word_t` 的格式保存在内部内存块或 DMA 缓冲区中。

.. only:: SOC_RMT_SUPPORT_RX_PINGPONG

    由于内存块大小有限,RMT 接收器会交替提醒驱动程序将累积的符号复制到外部处理。

.. only:: not SOC_RMT_SUPPORT_RX_PINGPONG

    由于内存块大小有限,RMT 接收器只能保存长度不超过内存块容量的短帧。硬件会将长帧截断,并由驱动程序报错:``hw buffer too small, received symbols truncated``。

应在 :cpp:func:`rmt_receive` 函数的 buffer 参数中提供复制目标。如果由于缓冲区大小不足而导致缓冲区溢出,接收器仍可继续工作,但会丢弃溢出的符号,并报告此错误信息:user buffer too small, received symbols truncated。请注意 buffer 参数的生命周期,确保在接收器完成或停止工作前不会回收缓冲区。

当接收器完成工作,即接收到持续时间大于 :cpp:member:`rmt_receive_config_t::signal_range_max_ns` 的信号时,驱动程序将停止接收器。如有需要,应再次调用 :cpp:func:`rmt_receive` 重新启动接收器。在 :cpp:member:`rmt_rx_event_callbacks_t::on_recv_done` 的回调中可以获取接收到的数据。要获取更多有关详情,请参阅 :ref:`rmt-register-event-callbacks`

static bool example_rmt_rx_done_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data)
{
    BaseType_t high_task_wakeup = pdFALSE;
    QueueHandle_t receive_queue = (QueueHandle_t)user_data;
    // 将接收到的 RMT 符号发送到解析任务的消息队列中
    xQueueSendFromISR(receive_queue, edata, &high_task_wakeup);
    // 返回是否唤醒了任何任务
    return high_task_wakeup == pdTRUE;
}

QueueHandle_t receive_queue = xQueueCreate(1, sizeof(rmt_rx_done_event_data_t));
rmt_rx_event_callbacks_t cbs = {
    .on_recv_done = example_rmt_rx_done_callback,
};
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_channel, &cbs, receive_queue));

// 以下时间要求均基于 NEC 协议
rmt_receive_config_t receive_config = {
    .signal_range_min_ns = 1250,     // NEC 信号的最短持续时间为 560 µs,由于 1250 ns < 560 µs,有效信号不会视为噪声
    .signal_range_max_ns = 12000000, // NEC 信号的最长持续时间为 9000 µs,由于 12000000 ns > 9000 µs,接收不会提前停止
};

rmt_symbol_word_t raw_symbols[64]; // 64 个符号应足够存储一个标准 NEC 帧的数据
// 准备开始接收
ESP_ERROR_CHECK(rmt_receive(rx_channel, raw_symbols, sizeof(raw_symbols), &receive_config));
// 等待 RX 完成信号
rmt_rx_done_event_data_t rx_data;
xQueueReceive(receive_queue, &rx_data, portMAX_DELAY);
// 解析接收到的符号数据
example_parse_nec_frame(rx_data.received_symbols, rx_data.num_symbols);

RMT 编码器

RMT 编码器是 RMT TX 事务的一部分,用于在特定时间生成正确的 RMT 符号,并将其写入硬件内存或 DMA 缓冲区。对于编码函数,存在以下特殊限制条件:

  • 由于目标 RMT 内存块无法一次性容纳所有数据,在单个事务中,须多次调用编码函数。为突破这一限制,可以采用 交替 方式,将编码会话分成多个部分。为此,编码器需要 记录其状态,以便从上一部分编码结束之处继续编码。
  • 编码函数在 ISR 上下文中运行。为加快编码会话,建议将编码函数放入 IRAM,这也有助于避免在编码过程中出现 cache 失效的情况。

为帮助用户更快速地上手 RMT 驱动程序,该程序默认提供了一些常用编码器,可以单独使用,也可以链式组合成新的编码器,有关原理请参阅 组合模式。驱动程序在 :cpp:type:`rmt_encoder_t` 中定义了编码器接口,包含以下函数:

拷贝编码器

调用 :cpp:func:`rmt_new_copy_encoder` 可以创建拷贝编码器,将 RMT 符号从用户空间复制到驱动程序层。拷贝编码器通常用于编码 const 数据,即初始化后在运行时不会发生更改的数据,如红外协议中的前导码。

调用 :cpp:func:`rmt_new_copy_encoder` 前,应预先提供配置结构体 :cpp:type:`rmt_copy_encoder_config_t`。目前,该配置保留用作未来的扩展功能,暂无具体用途或设置项。

字节编码器

调用 :cpp:func:`rmt_new_bytes_encoder` 可以创建字节编码器,将用户空间的字节流动态转化成 RMT 符号。字节编码区通常用于编码动态数据,如红外协议中的地址和命令字段。

调用 :cpp:func:`rmt_new_bytes_encoder` 前,应预先提供配置结构体 :cpp:type:`rmt_bytes_encoder_config_t`,具体配置如下:

除驱动程序提供的原始编码器外,也可以将现有编码器链式组合成自定义编码器。常见编码器链如下图所示:

.. blockdiag:: /../_static/diagrams/rmt/rmt_encoder_chain.diag
    :caption: RMT 编码器链
    :align: center

自定义 NEC 协议的 RMT 编码器

本节将演示编写 NEC 编码器的流程。NEC 红外协议使用脉冲距离编码来发送消息位,每个脉冲突发的持续时间为 562.5 µs,逻辑位发送详见下文。注意,各字节的最低有效位会优先发送。

  • 逻辑 0562.5 µs 的脉冲突发后有 562.5 µs 的空闲时间,总发送时间为 1.125 ms
  • 逻辑 1562.5 µs 的脉冲突发后有 1.6875 ms 的空闲时间,总发送时间为 2.25 ms

在遥控器上按下某个按键时,将按以下顺序发送有关信号:

红外 NEC 帧

红外 NEC 帧

  • 9 ms 的引导脉冲发射,也称为 AGC 脉冲
  • 4.5 ms 的空闲时间
  • 接收设备的 8 位地址
  • 地址的 8 位逻辑反码
  • 8 位命令
  • 命令的 8 位逻辑反码
  • 最后的 562.5 µs 脉冲突发,表示消息发送结束

随后可以按相同顺序构建 NEC :cpp:member:`rmt_encoder_t::encode` 函数,例如

// 红外 NEC 扫码表示法
typedef struct {
    uint16_t address;
    uint16_t command;
} ir_nec_scan_code_t;

// 通过组合原始编码器构建编码器
typedef struct {
    rmt_encoder_t base;           // 基础类 "class" 声明了标准编码器接口
    rmt_encoder_t *copy_encoder;  // 使用拷贝编码器来编码前导码和结束码
    rmt_encoder_t *bytes_encoder; // 使用字节编码器来编码地址和命令数据
    rmt_symbol_word_t nec_leading_symbol; // 使用 RMT 表示的 NEC 前导码
    rmt_symbol_word_t nec_ending_symbol;  // 使用 RMT 表示的 NEC 结束码
    int state; // 记录当前编码状态,即所处编码阶段
} rmt_ir_nec_encoder_t;

static size_t rmt_encode_ir_nec(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
{
    rmt_ir_nec_encoder_t *nec_encoder = __containerof(encoder, rmt_ir_nec_encoder_t, base);
    rmt_encode_state_t session_state = RMT_ENCODING_RESET;
    rmt_encode_state_t state = RMT_ENCODING_RESET;
    size_t encoded_symbols = 0;
    ir_nec_scan_code_t *scan_code = (ir_nec_scan_code_t *)primary_data;
    rmt_encoder_handle_t copy_encoder = nec_encoder->copy_encoder;
    rmt_encoder_handle_t bytes_encoder = nec_encoder->bytes_encoder;
    switch (nec_encoder->state) {
    case 0: // 发送前导码
        encoded_symbols += copy_encoder->encode(copy_encoder, channel, &nec_encoder->nec_leading_symbol,
                                                sizeof(rmt_symbol_word_t), &session_state);
        if (session_state & RMT_ENCODING_COMPLETE) {
            nec_encoder->state = 1; // 只有在当前编码器完成工作时才能切换到下一个状态
        }
        if (session_state & RMT_ENCODING_MEM_FULL) {
            state |= RMT_ENCODING_MEM_FULL;
            goto out; // 如果没有足够的空间来存放其他编码相关的数据,程序会暂停当前操作,并跳转到指定位置继续执行。
        }
    // 继续执行
    case 1: // 发送地址
        encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, &scan_code->address, sizeof(uint16_t), &session_state);
        if (session_state & RMT_ENCODING_COMPLETE) {
            nec_encoder->state = 2; // 只有在当前编码器完成工作时才能切换到下一个状态
        }
        if (session_state & RMT_ENCODING_MEM_FULL) {
            state |= RMT_ENCODING_MEM_FULL;
            goto out; // 如果没有足够的空间来存放其他编码相关的数据,程序会暂停当前操作,并跳转到指定位置继续执行。
        }
    // 继续执行
    case 2: // 发送命令
        encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, &scan_code->command, sizeof(uint16_t), &session_state);
        if (session_state & RMT_ENCODING_COMPLETE) {
            nec_encoder->state = 3; // 只有在当前编码器完成工作时才能切换到下一个状态
        }
        if (session_state & RMT_ENCODING_MEM_FULL) {
            state |= RMT_ENCODING_MEM_FULL;
            goto out; // 如果没有足够的空间来存放其他编码相关的数据,程序会暂停当前操作,并跳转到指定位置继续执行。
        }
    // 继续执行
    case 3: // 发送结束码
        encoded_symbols += copy_encoder->encode(copy_encoder, channel, &nec_encoder->nec_ending_symbol,
                                                sizeof(rmt_symbol_word_t), &session_state);
        if (session_state & RMT_ENCODING_COMPLETE) {
            nec_encoder->state = RMT_ENCODING_RESET; // 返回初始编码会话
            state |= RMT_ENCODING_COMPLETE; // 告知调用者 NEC 编码已完成
        }
        if (session_state & RMT_ENCODING_MEM_FULL) {
            state |= RMT_ENCODING_MEM_FULL;
            goto out; // 如果没有足够的空间来存放其他编码相关的数据,程序会暂停当前操作,并跳转到指定位置继续执行。
        }
    }
out:
    *ret_state = state;
    return encoded_symbols;
}

完整示例代码存放在 :example:`peripherals/rmt/ir_nec_transceiver` 目录下。以上代码片段使用了 switch-case 和一些 goto 语句实现了一个 有限状态机,借助此模式可构建更复杂的红外协议。

电源管理

通过 :ref:`CONFIG_PM_ENABLE` 选项启用电源管理时,系统会在进入 Light-sleep 模式前调整 APB 频率。该操作可能改变 RMT 内部计数器的分辨率。

然而,驱动程序可以通过获取 :cpp:enumerator:`ESP_PM_APB_FREQ_MAX` 类型的电源管理锁,防止系统改变 APB 频率。每当驱动创建以 :cpp:enumerator:`RMT_CLK_SRC_APB` 作为时钟源的 RMT 通道时,都会在通过 :cpp:func:`rmt_enable` 启用通道后获取电源管理锁。反之,调用 :cpp:func:`rmt_disable` 时,驱动程序释放锁。这也意味着 :cpp:func:`rmt_enable`:cpp:func:`rmt_disable` 应成对出现。

如果将通道时钟源设置为其他选项,如 :cpp:enumerator:`RMT_CLK_SRC_XTAL`,则驱动程序不会为其安装电源管理锁。对于低功耗应用程序来说,只要时钟源仍然可以提供足够的分辨率,不安装电源管理锁更为合适。

IRAM 安全

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

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

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

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

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

线程安全

RMT 驱动程序会确保工厂函数 :cpp:func:`rmt_new_tx_channel`:cpp:func:`rmt_new_rx_channel`:cpp:func:`rmt_new_sync_manager` 的线程安全。使用时,可以直接从不同的 RTOS 任务中调用此类函数,无需额外锁保护。 其他以 :cpp:type:`rmt_channel_handle_t`:cpp:type:`rmt_sync_manager_handle_t` 作为第一个位置参数的函数均非线程安全,在没有设置互斥锁保护的任务中,应避免从多个任务中调用这类函数。

以下函数允许在 ISR 上下文中使用:

Kconfig 选项

应用示例

FAQ

  • RMT 编码器为什么会产生比预期更多的数据?

RMT 编码在 ISR 上下文中发生。如果 RMT 编码会话耗时较长(例如,记录调试信息),或者由于中断延迟导致编码会话延迟执行,则传输速率可能会超过编码速率。此时,编码器无法及时准备下一组数据,致使传输器再次发送先前的数据。由于传输器无法停止并等待,可以通过以下方法来缓解此问题:

API 参考

.. include-build-file:: inc/rmt_tx.inc
.. include-build-file:: inc/rmt_rx.inc
.. include-build-file:: inc/rmt_common.inc
.. include-build-file:: inc/rmt_encoder.inc
.. include-build-file:: inc/components/driver/rmt/include/driver/rmt_types.inc
.. include-build-file:: inc/components/hal/include/hal/rmt_types.inc


[1]不同 ESP 芯片系列可能具有不同数量的 RMT 通道,详情请参阅 [TRM]。驱动程序对通道申请数量不做限制,但当硬件资源用尽时,驱动程序将返回错误。因此,每次进行 :ref:`rmt-resource-allocation` 时,请注意检查返回值。
[2]注意,回调函数(如 :cpp:member:`rmt_tx_event_callbacks_t::on_trans_done`)及回调函数所调用的函数也应位于 IRAM 中。