【ESP32】ESP-IDF开发 | 红外遥控RMT + WS2812灯驱动例程

1. 简介

        字如其名,RMT是一个红外发送和接收控制器,可通过软件加解密多种红外协议。RMT模块可以实现将模块内置RAM中的脉冲编码转换为信号输出,或将模块的输入信号转换为脉冲编码存入RAM中。此外,RMT模块可以选择是否对输出信号进行载波调制,也可以选择是否对输入信号进行滤波和去噪处理

        RMT有8个通道,前4个为发送通道,后4个为接收通道。通道3和7支持DMA传输

1.1 编码结构

        RMT的每个脉冲编码为16位,由level与period两部分组成。其中level表示输入或输出信号的逻辑电平值(0 或 1),period表示该电平信号持续的时钟周期数

        period的最小值为0,period为0是一次传输的结束标志,对于非0的period需要满足以下公式:

3\times T_{apb\_clk}+5\times T_{rmt\_clk}<period\times T_{clk\_div}

1.2 时钟源

        RMT支持APB_CLK、RC_FAST_CLK或XTAL_CLK作为其输入时钟,输入时钟经过小数分频得到RMT的工作时钟,分频公式如下:

RMT\_SCLK\_DIV\_NUM+1+\frac{RMT\_SCLK\_DIV\_A}{RMT\_SCLK\_DIV\_B}

1.3 RAM访问

        RMT内部带有48×32bit的专用RAM,它们会分为8个块,默认情况下一个通道用一个块,但用户可以配置RMT_MEM_SIZE_CHx寄存器允许通道占用多个内存块

        但要注意一点,因为通道与内存块的映射是一一对应的,所以某个通道不能使用序号比自己小的内存块,例如通道3可以使用序号3以后的内存块,不能使用序号0-2的内存块。

        RMT的RAM有3种访问方式——FIFO模式、NONFIFO模式和DMA模式

1. FIFO模式

        APB通过固定地址(RMT_CHxDATA_REG)向RAM写数据或从RAM读数据。

2. NONFIFO模式

        APB向连续地址(RMT基地址+偏移)段写入数据或从连续地址段读出数据

3. DMA模式

        开启DMA模式后,FIFO和NONFIFO访问方式将会忽略。为保证传输数据的正确性,需要先启动DMA,在DMA通道已经接收到数据后,再启动RMT发送。

1.4 发送模式

1.4.1 普通发送

        使能RMT发送后,通道x的发射器开始从通道对应内存块的起始地址,按照地址从低到高依次读取脉冲编码进行发送。当遇到结束标志(period 等于 0)时,发射器将结束发送返回空闲状态,并产生发送结束中断。

1.4.2 乒乓发送

        当发送的脉冲编码较多时,可使能通道的乒乓模式。在乒乓操作模式下,发射器会循环从通道对应的RAM区域取出脉冲编码进行发送,直到遇到结束标识为止。

        这个模式一般搭配RMT_MEM_SIZE_CHx > 1的配置使用,如果发射器遍历到通道占用的RAM的末尾,仍然没有遇到结束标志,那么发射器又会从通道RAM地址的开头继续发送。

1.4.3 持续发送

        发射器会循环发送RAM中的脉冲编码。持续发送模式下,如果遇到结束标志,则重新从该通道RAM中的第一个数据开始发送;如果没有结束标志,会在发送到最后一个数据处回卷,重新开始发送第一个数据。

        发射器每遇到一次结束标志,循环发送的次数会加1。当该次数达到RMT_TX_LOOP_NUM_CHx设定的值时,会产生RMT_CHx_TX_LOOP_INT中断。如果置位 RMT_LOOP_STOP_EN_CHx,则发送会在产生RMT_CHx_TX_LOOP_INT中断后立即停止,否则会继续发送。

1.4.4 多通道发送

        配置RMT_TX_SIM_CHx用于选择同步发送的通道;置位RMT_TX_SIM_EN使能发射器多个通道同步发送的功能;最后将所选同步发送通道的RMT_TX_START_CHx置为1,即可使能该模式。

1.5 接收模式

1.5.1 普通接收

        接收器会从信号的第一个跳变沿开始计数,并检测信号电平及其持续的时钟周期数,将其按照脉冲编码的格式存入RAM中。当信号在一个电平下持续的时钟周期数超过RMT_IDLE_THRES_CHx时,接收器结束接收过程,返回空闲状态,并产生 RMT_CHx_RX_END_INT中断。

1.5.2 乒乓接收

        当接收的脉冲编码较多时,可使能通道的乒乓模式。当RAM采用DMA方式访问时,不需要额外操作就能支持多于一个RAM的脉冲编码接收。而采用APB方式访问时,需要软件进行乒乓操作;在RMT将RAM填满的时候要及时地取出数据,否则,RMT又会回到通道RAM的起始地址开始接收覆盖掉之前的数据。

1.6 载波

        发送模式下RMT可以对输出信号加载波。用户可以分别设置载波的高电平和低电平时间,同时还可以选择只在某些编码上加载载波,而不是全部。

        接收模式下RMT可以对输入信号去载波。用户可以分别设置高电平和低电平载波的滤除阈值。

2. 例程

        因为手头上没有带红外发射和接收的模块,因此这里写一个WS2812灯的简单驱动,其实这个应用得还更广泛一点。WS2812其实就是我们在高楼大厦上见到的彩色LED灯带中的灯珠,它接收GRB888的颜色格式,它只有1根信号输入线,逻辑0和1是通过不同高低电平的占空比来识别的,输入的数据是高位优先

#include <string.h>
#include <math.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/rmt_tx.h"


#define RMT_RESOLUTION_HZ 10000000  // 10MHz resolution, 0.1us

#define TAG "main"

static uint8_t rgb[3];
static rmt_channel_handle_t led_chan = NULL;
static rmt_encoder_handle_t led_encoder = NULL;

static const rmt_symbol_word_t ws2812_zero = {
    .level0 = 1,
    .duration0 = 0.4f * RMT_RESOLUTION_HZ / 1000000, // T0H=0.4us
    .level1 = 0,
    .duration1 = 0.85f * RMT_RESOLUTION_HZ / 1000000, // T0L=0.85us
};

static const rmt_symbol_word_t ws2812_one = {
    .level0 = 1,
    .duration0 = 0.8f * RMT_RESOLUTION_HZ / 1000000, // T1H=0.8us
    .level1 = 0,
    .duration1 = 0.45f * RMT_RESOLUTION_HZ / 1000000, // T1L=0.45us
};

static const rmt_symbol_word_t ws2812_reset = {
    .level0 = 1,
    .duration0 = RMT_RESOLUTION_HZ / 1000000 * 50 / 2,
    .level1 = 0,
    .duration1 = RMT_RESOLUTION_HZ / 1000000 * 50 / 2,
};

static const rmt_transmit_config_t tx_config = {
    .loop_count = 0,
};

static size_t encoder_callback(
    const void *data,
    size_t data_size,
    size_t symbols_written,
    size_t symbols_free,
    rmt_symbol_word_t *symbols,
    bool *done,
    void *arg)
{
    /* 写一个字节至少需要8个符号 */
    if (symbols_free < 8) {
        return 0;
    }

    size_t data_pos = symbols_written / 8;
    uint8_t *data_bytes = (uint8_t*) data;
    if (data_pos < data_size) {
        /* 写一个字节,高位在先 */
        size_t symbol_pos = 0;
        for (uint8_t bitmask = 0x80; bitmask; bitmask >>= 1) {
            if (data_bytes[data_pos] & bitmask) {
                symbols[symbol_pos++] = ws2812_one;
            } else {
                symbols[symbol_pos++] = ws2812_zero;
            }
        }
        return symbol_pos;
    } else {
        /* 传输完成,发送复位 */
        symbols[0] = ws2812_reset;
        *done = 1;
        return 1;
    }
}

void app_main(void)
{
    /* 初始化RMT通道 */
    const rmt_tx_channel_config_t tx_chan_config = {
        .clk_src = RMT_CLK_SRC_DEFAULT,  // 默认时钟源,APB时钟
        .gpio_num = 48,  // IO48
        .mem_block_symbols = 64,  // 内存块大小,必须双数
        .resolution_hz = RMT_RESOLUTION_HZ,  // 时钟频率
        .trans_queue_depth = 4,  // 传输队列深度
    };
    ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &led_chan));

    /* 初始化编码器 */
    const rmt_simple_encoder_config_t encoder_cfg = {
        .callback = encoder_callback
    };
    ESP_ERROR_CHECK(rmt_new_simple_encoder(&encoder_cfg, &led_encoder));

    ESP_ERROR_CHECK(rmt_enable(led_chan));

    float offset = 0;
    while (1) {
        /* 计算RGB值 */
        float angle = offset + (0.3);
        const float color_off = (M_PI * 2) / 3;
        rgb[0] = sin(angle + color_off * 0) * 127 + 128;
        rgb[1] = sin(angle + color_off * 1) * 127 + 128;
        rgb[2] = sin(angle + color_off * 2) * 117 + 128;

        /* 输出 */
        ESP_ERROR_CHECK(rmt_transmit(led_chan, led_encoder, rgb, sizeof(rgb), &tx_config));
        ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY));
        vTaskDelay(pdMS_TO_TICKS(20));

        /* 计算偏移 */
        offset += 0.02;
        if (offset > 2 * M_PI) {
            offset -= 2 * M_PI;
        }
    }
}

1. 初始化RMT通道

        初始化结构体的定义如下:

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;
    int intr_priority;
    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:输出管脚;
  • clk_src:时钟源;
  • resolution_hz:输出时钟频率;
  • mem_block_symbols:非DMA模式下它代表编码单元的数量,不能少于64且必须双数;DMA模式下它代表DMA缓冲区大小,最好大于1024;
  • trans_queue_depth:发送队列深度;
  • intr_priority:中断优先级,一般保持默认(0)即可;
  • flags.invert_out:输出信号反转;
  • flags.with_dma:使能DMA模式;
  • flags.io_loop_back:IO输出同时接到输入;
  • flags.io_od_mode:使能开漏模式。

        我这块板子的WS2812使用IO48管脚,时钟源使用APB时钟,频率设置为10MHz。

2. 初始化编码器

        这一步只是注册一个回调函数,在这个回调函数中我们需要处理数据的编码,将原始颜色数据编码成RMT识别的编码格式,填入到对应的RAM空间中;函数需要返回当前已经编码完成的数据数。

3. 主循环

        我这里实现的是一个彩虹灯的效果,所以主循环就是计算RGB的值,并发送数据到RMT外设中。RGB的值计算这里用的是余弦函数,当然用更简单的线性函数也是可以的。

        将代码编译烧录到板子上就能看到LED灯在不断变换颜色了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马浩同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值