RMT (Remote Control) 模块是 ESP32 特有的一个高度灵活的外设,主要用于红外 (Infrared) 遥控信号的生成和解析 。它提供了一种高效且精确的方式来处理时间敏感的信号,尤其适用于各种自定义的红外协议,而无需占用大量 CPU 资源。
核心思想: RMT 模块通过硬件方式,精确控制脉冲的持续时间(高电平或低电平的宽度),从而实现对各种红外编码协议(如 NEC、RC5、RC6 等)的编解码,大大减轻了 CPU 软件模拟脉冲的负担和精度问题。
一、RMT 模块概述 ESP32 的 RMT 模块提供以下主要特性:
高精度脉冲控制 :能够生成和解析微秒级别的脉冲。
DMA (Direct Memory Access) 支持 :RMT 可以直接从内存读取数据并发送,或将接收到的数据直接写入内存,无需 CPU 干预,提高了效率。
多通道 :通常有8个独立的RMT通道,每个通道都可以配置为发送或接收模式。
编码/解码灵活 :可以通过编程配置,适应多种红外协议。
空闲状态检测 :接收模式下可检测总线空闲,判断数据包结束。
载波调制/解调 :支持对发送信号进行载波调制 (通常是38kHz) 和对接收信号进行载波解调,这是红外通信的必要环节。
1.1 红外通信基础 红外遥控通常采用载波调制 技术。
载波频率 :最常见的载波频率是 38kHz。在发送数据时,实际的脉冲信号会被 38kHz 的方波调制(即“打断”成38kHz的脉冲串)。接收端通过解调器识别并滤除 38kHz 载波,还原出原始的数据脉冲。
优势 :使用载波调制可以有效区分数据信号和环境中的低频噪声,提高抗干扰能力和传输距离。
1.2 RMT 在红外控制中的作用
发送 (TX) :RMT 模块负责将应用程序提供的逻辑脉冲序列(高电平持续时间、低电平持续时间)转换为带有 38kHz 载波的红外光脉冲,并通过红外 LED 发送出去。
接收 (RX) :RMT 模块负责接收红外接收头(如 VS1838B)输出的电信号,对其进行 38kHz 解调,并测量每个高电平/低电平脉冲的持续时间,然后将这些时间数据存储到内存中供应用程序解析。
二、RMT 发送 (TX) 功能详解 RMT 发送流程是将内存中的RMT项(RMT Item)数据转换为电平脉冲,并通过DMA发送出去。
2.1 RMT 发送配置 发送通道的初始化通常包括以下步骤:
选择 RMT 通道 :rmt_channel_t channel (0-7)。
选择 GPIO 引脚 :gpio_num_t gpio_num (连接红外 LED 的引脚)。
设置时钟分频器 :clk_div (用于精确控制脉冲宽度,通常设置为1或8,使得一个 RMT 计数周期为12.5ns或100ns)。
设置载波参数 :carrier_en, carrier_duty_percent, carrier_freq_hz (例如 38kHz, 50% 占空比)。
设置空闲电平 :idle_output_en, idle_output_level (通常空闲时引脚为低电平)。
代码示例 (配置 RMT TX 通道):
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 package mainimport ( "fmt" "time" "github.com/espressif/esp-idf-go/idf" "github.com/espressif/esp-idf-go/idf/hal/gpio" "github.com/espressif/esp-idf-go/idf/hal/rmt" ) const ( RMT_TX_CHANNEL = rmt.CHANNEL_0 RMT_TX_GPIO_NUM = gpio.NUM_18 RMT_CARRIER_FREQ = 38000 ) func initRmtTx () error { rmtConfig := rmt.Config{ RMT_MODE: rmt.MODE_TX, CHANNEL: RMT_TX_CHANNEL, GPIO_NUM: RMT_TX_GPIO_NUM, CLK_DIV: 8 , TX_CONFIG: rmt.TXConfig{ LOOP_EN: false , IDLE_OUTPUT_EN: true , IDLE_OUTPUT_LEVEL: rmt.IDLE_LEVEL_LOW, CARRIER_EN: true , CARRIER_DUTY_PERCENT: 50 , CARRIER_FREQ_HZ: RMT_CARRIER_FREQ, }, } if err := rmt.ConfigRMT(&rmtConfig); err != nil { return fmt.Errorf("failed to config RMT: %w" , err) } if err := rmt.Install(RMT_TX_CHANNEL, 0 , 0 ); err != nil { return fmt.Errorf("failed to install RMT driver: %w" , err) } if err := rmt.TxStart(RMT_TX_CHANNEL, true ); err != nil { return fmt.Errorf("failed to start RMT TX: %w" , err) } fmt.Printf("RMT TX channel %d initialized on GPIO %d\n" , RMT_TX_CHANNEL, RMT_TX_GPIO_NUM) return nil }
2.2 RMT 项 (RMT Item) RMT 模块通过处理一系列 rmt_item32_t 结构体来生成脉冲。每个 rmt_item32_t 结构体占用 32 位,可以描述两个脉冲段。
1 2 3 4 5 6 7 8 9 10 typedef union { struct { uint32_t duration0 : 15 ; uint32_t level0 : 1 ; uint32_t duration1 : 15 ; uint32_t level1 : 1 ; }; uint32_t val; } rmt_item32_t ;
duration0, duration1:脉冲持续时间,单位是 RMT 时钟周期(tick)。最大值为 $2^{15}-1 = 32767$ tick。
level0, level1:脉冲电平,0表示低电平,1表示高电平。
红外协议通常由一系列高低电平脉冲组成。例如,NEC 协议的引导码可能是一个 9ms 高电平,然后 4.5ms 低电平。
转换为 RMT Item 的示例 (NEC 引导码):
假设 CLK_DIV = 8,一个 tick = 100ns。
9ms 高电平 = 9000us = 90000 tick
4.5ms 低电平 = 4500us = 45000 tick
一个 rmt_item32_t 就可以表示这 9ms 高电平和 4.5ms 低电平:
1 2 3 4 5 6 rmt_item32_t nec_leader_code;nec_leader_code.duration0 = 90000 ; nec_leader_code.level0 = 1 ; nec_leader_code.duration1 = 45000 ; nec_leader_code.level1 = 0 ;
由于 duration 最大只有 32767,所以 90000 和 45000 无法直接放入一个 rmt_item32_t 中。这意味着一个长的脉冲需要分成多个 rmt_item32_t 来表示。
正确处理长脉冲的示例:
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 func usToRmtTicks (us uint32 ) uint32 { return us * 10 } func createNecLeaderCode () []rmt.Item32 { items := []rmt.Item32{} totalHighTicks := usToRmtTicks(9000 ) for totalHighTicks > 0 { currentDuration := uint32 (32767 ) if totalHighTicks < 32767 { currentDuration = totalHighTicks } items = append (items, rmt.Item332{ Duration0: uint16 (currentDuration), Level0: 1 , Duration1: 0 , Level1: 0 , }) totalHighTicks -= currentDuration } totalLowTicks := usToRmtTicks(4500 ) for totalLowTicks > 0 { currentDuration := uint32 (32767 ) if totalLowTicks < 32767 { currentDuration = totalLowTicks } items = append (items, rmt.Item332{ Duration0: uint16 (currentDuration), Level0: 0 , Duration1: 0 , Level1: 0 , }) totalLowTicks -= currentDuration } return items } func sendNecCode (items []rmt.Item32) error { return rmt.WriteItems(RMT_TX_CHANNEL, items, len (items), true ) }
注意: 实际在 Go 语言或 C 语言中,rmt_item32_t (或 Go 中的 rmt.Item32) 的定义可能略有不同。idf.rmt.Item32 通常只包含一个 duration 和 level,需要将每个脉冲(高电平或低电平)作为一个单独的 Item32。这意味着你需要将连续的 duration0 和 level0 组成一个 item,再将 duration1 和 level1 组成下一个 item。
ESP-IDF 的 rmt_item32_t 结构实际上是:
1 2 3 4 5 6 7 typedef struct { uint32_t duration0 : 15 ; uint32_t level0 : 1 ; uint32_t duration1 : 15 ; uint32_t level1 : 1 ; } rmt_item32_t ;
所以,如果一个脉冲超过 32767 ticks,你确实需要将其分解。如果脉冲短于 32767 ticks,可以将两个连续的脉冲(一个高电平,一个低电平)打包到一个 rmt_item32_t 中。
更常见的 RMT Item 构建方式 (例如 9ms High + 4.5ms Low):
1 2 3 4 5 6 7 8 9 10 11 func createNecLeaderCodePacked () []rmt.Item32 { items := make ([]rmt.Item32, 1 ) items[0 ] = rmt.Item32{ Duration0: uint16 (usToRmtTicks(9000 )), Level0: 1 , Duration1: uint16 (usToRmtTicks(4500 )), Level1: 0 , } return items }
这种方式是更常见的做法,只要单个脉冲持续时间不超过 32767 * tick_period 即可。如果需要更长的脉冲,ESP-IDF 提供了一个函数 rmt_fill_tx_items 来帮助处理这种情况,或者手动将其拆分成多个 Item32。
2.3 发送数据 一旦 RMT 项序列准备好,就可以通过 rmt_write_items 函数将其发送出去。
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 func main () { err := initRmtTx() if err != nil { fmt.Printf("Error initializing RMT TX: %v\n" , err) return } necItems := []rmt.Item32{ {Duration0: uint16 (usToRmtTicks(9000 )), Level0: 1 , Duration1: uint16 (usToRmtTicks(4500 )), Level1: 0 }, {Duration0: uint16 (usToRmtTicks(560 )), Level0: 1 , Duration1: uint16 (usToRmtTicks(560 )), Level1: 0 }, {Duration0: uint16 (usToRmtTicks(560 )), Level0: 1 , Duration1: uint16 (usToRmtTicks(1690 )), Level1: 0 }, {Duration0: uint16 (usToRmtTicks(560 )), Level0: 1 , Duration1: 0 , Level1: 0 }, } fmt.Println("Sending NEC infrared code..." ) for i := 0 ; i < 3 ; i++ { if err := rmt.WriteItems(RMT_TX_CHANNEL, necItems, len (necItems), true ); err != nil { fmt.Printf("Error sending RMT items: %v\n" , err) return } time.Sleep(100 * time.Millisecond) } fmt.Println("NEC infrared code sent." ) }
三、RMT 接收 (RX) 功能详解 RMT 接收流程是将红外接收头输出的信号经过硬件解调和脉冲测量,将时间数据存储到内存中。
3.1 RMT 接收配置 接收通道的初始化包括以下步骤:
选择 RMT 通道 :rmt_channel_t channel。
选择 GPIO 引脚 :gpio_num_t gpio_num (连接红外接收头的输出引脚)。
设置时钟分频器 :clk_div (同 TX,用于精确测量脉冲宽度)。
设置载波解调 :filter_en, filter_tics_thresh (过滤短于特定时长的脉冲,避免噪声)。
设置内存缓冲区 :rx_buf_size (DMA 缓冲区大小,用于存储接收到的 rmt_item32_t 数据)。
设置空闲阈值 :idle_threshold (如果一段时间内没有接收到脉冲,则认为一个数据包接收结束)。
代码示例 (配置 RMT RX 通道):
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 const ( RMT_RX_CHANNEL = rmt.CHANNEL_1 RMT_RX_GPIO_NUM = gpio.NUM_19 RMT_RX_BUFFER_SIZE = 128 RMT_RX_IDLE_THRESHOLD = 9000 ) func initRmtRx () error { rmtConfig := rmt.Config{ RMT_MODE: rmt.MODE_RX, CHANNEL: RMT_RX_CHANNEL, GPIO_NUM: RMT_RX_GPIO_NUM, CLK_DIV: 8 , RX_CONFIG: rmt.RXConfig{ FILTER_EN: true , FILTER_TICS_THRESH: 100 , IDLE_THRESHOLD: uint16 (usToRmtTicks(RMT_RX_IDLE_THRESHOLD) / 10 ), }, } rmtConfig.RX_CONFIG.IDLE_THRESHOLD = uint16 (usToRmtTicks(RMT_RX_IDLE_THRESHOLD)) if err := rmt.ConfigRMT(&rmtConfig); err != nil { return fmt.Errorf("failed to config RMT: %w" , err) } if err := rmt.Install(RMT_RX_CHANNEL, RMT_RX_BUFFER_SIZE*idf.SizeofRmtItem32, 0 ); err != nil { return fmt.Errorf("failed to install RMT driver: %w" , err) } fmt.Printf("RMT RX channel %d initialized on GPIO %d\n" , RMT_RX_CHANNEL, RMT_RX_GPIO_NUM) return nil }
注意: rmt.Install 中 rx_buf_size 参数通常是以字节为单位,所以需要 RMT_RX_BUFFER_SIZE * idf.SizeofRmtItem32。
3.2 接收数据 接收数据通常涉及事件队列和DMA缓冲区。
启动接收 :rmt_rx_start。
获取事件队列 :rmt_get_event_queue (用于接收RMT事件,如接收完成)。
等待事件 :从事件队列中读取事件。
读取数据 :当接收到 RMT_RX_DONE 事件时,从 RMT DMA 缓冲区中读取 rmt_item32_t 数据。
停止接收 :rmt_rx_stop (在处理完一个数据包后)。
解析数据 :根据接收到的脉冲持续时间,反向解析出红外协议的逻辑位。
代码示例 (接收并解析红外数据):
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 func parseNecCode (items []rmt.Item32) (uint32 , error ) { if len (items) < 3 { return 0 , fmt.Errorf("too few RMT items to parse NEC code" ) } const ( NEC_LEADER_HIGH_US = 9000 NEC_LEADER_LOW_US = 4500 NEC_BIT_HIGH_US = 560 NEC_BIT_0_LOW_US = 560 NEC_BIT_1_LOW_US = 1690 NEC_STOP_HIGH_US = 560 NEC_TOLERANCE_US = 150 ) matchDuration := func (actualTicks uint16 , expectedUs uint32 ) bool { actualUs := uint32 (actualTicks) / 10 lowerBound := expectedUs - NEC_TOLERANCE_US upperBound := expectedUs + NEC_TOLERANCE_US return actualUs >= lowerBound && actualUs <= upperBound } if !matchDuration(items[0 ].Duration0, NEC_LEADER_HIGH_US) || !matchDuration(items[0 ].Duration1, NEC_LEADER_LOW_US) { return 0 , fmt.Errorf("NEC leader code mismatch: H=%d us, L=%d us" , items[0 ].Duration0/10 , items[0 ].Duration1/10 ) } data := uint32 (0 ) bitCount := 0 dataItems := items[1 :] for i := 0 ; i < len (dataItems)-1 ; i++ { if bitCount >= 32 { break } item := dataItems[i] if item.Level0 == 0 || !matchDuration(item.Duration0, NEC_BIT_HIGH_US) { return 0 , fmt.Errorf("data bit %d: expected high pulse" , bitCount) } if item.Duration1 == 0 { if matchDuration(item.Duration0, NEC_STOP_HIGH_US) { break } return 0 , fmt.Errorf("data bit %d: missing low pulse in data item" , bitCount) } if matchDuration(item.Duration1, NEC_BIT_0_LOW_US) { data <<= 1 } else if matchDuration(item.Duration1, NEC_BIT_1_LOW_US) { data = (data << 1 ) | 1 } else { return 0 , fmt.Errorf("data bit %d: invalid low pulse duration: %d us" , bitCount, item.Duration1/10 ) } bitCount++ } return data, nil } func startRmtRxLoop () { if err := initRmtRx(); err != nil { fmt.Printf("Error initializing RMT RX: %v\n" , err) return } eventQueue, err := rmt.GetEventQueue(RMT_RX_CHANNEL, 10 ) if err != nil { fmt.Printf("Error getting RMT event queue: %v\n" , err) return } if err := rmt.RxStart(RMT_RX_CHANNEL, true ); err != nil { fmt.Printf("Error starting RMT RX: %v\n" , err) return } fmt.Println("RMT RX started. Waiting for IR signals..." ) rxBuf := make ([]rmt.Item32, RMT_RX_BUFFER_SIZE) for { select { case event := <-eventQueue: if event.EventType == rmt.EVENT_RX_DONE { fmt.Printf("RMT RX Done event received. Size: %d\n" , event.Size) readLen, err := rmt.ReadItems(RMT_RX_CHANNEL, rxBuf, int (event.Size), 100 * time.Millisecond) if err != nil { fmt.Printf("Error reading RMT items: %v\n" , err) rmt.RxStart(RMT_RX_CHANNEL, true ) continue } if readLen > 0 { receivedItems := rxBuf[:readLen] necData, err := parseNecCode(receivedItems) if err != nil { fmt.Printf("Failed to parse NEC code: %v\n" , err) } else { fmt.Printf("Received NEC Code: 0x%X\n" , necData) } } if err := rmt.RxStart(RMT_RX_CHANNEL, true ); err != nil { fmt.Printf("Error restarting RMT RX: %v\n" , err) return } } else { fmt.Printf("Unhandled RMT event type: %d\n" , event.EventType) } default : time.Sleep(10 * time.Millisecond) } } } func main () { go startRmtRxLoop() select {} }
四、RMT 的局限性与注意事项
内存使用 :DMA 缓冲区需要占用较大的内存,特别是 RX 模式。需要合理设置缓冲区大小。
协议复杂性 :虽然 RMT 提供了硬件支持,但解析和编码复杂的红外协议(如 RC5/RC6 存在曼彻斯特编码、翻转位等)仍然需要应用程序层的软件逻辑。
时钟精度 :RMT 的脉冲精度受 ESP32 内部时钟和 CLK_DIV 的影响。选择合适的 CLK_DIV 可以在精度和最大脉冲长度之间取得平衡。
GPIO 复用 :每个 RMT 通道绑定一个 GPIO 引脚,确保 GPIO 不被其他外设复用。
空闲阈值 :RX 模式下的 idle_threshold 是区分不同红外数据包的关键,设置不当可能导致数据包合并或截断。
载波调制/解调 :确保发送和接收的载波频率匹配,且接收端滤波器设置合理。
五、总结 ESP32 的 RMT 模块是一个功能强大且高度灵活的外设,为红外遥控和其他时间敏感的脉冲信号处理提供了理想的硬件加速方案。通过精确配置 RMT 通道参数、合理构造 rmt_item32_t 序列进行发送,以及利用 DMA 缓冲区和事件队列高效接收和解析脉冲数据,开发者可以轻松实现各种自定义的红外通信协议,从而在智能家居、遥控设备等物联网应用中发挥重要作用。尽管需要一定的软件逻辑来处理协议细节,但 RMT 大大减轻了 CPU 的负担,使得 ESP32 在这类应用中表现出色。