RMT (Remote Control) 模块是 ESP32 特有的一个高度灵活的外设,主要用于红外 (Infrared) 遥控信号的生成和解析。它提供了一种高效且精确的方式来处理时间敏感的信号,尤其适用于各种自定义的红外协议,而无需占用大量 CPU 资源。

核心思想: RMT 模块通过硬件方式,精确控制脉冲的持续时间(高电平或低电平的宽度),从而实现对各种红外编码协议(如 NEC、RC5、RC6 等)的编解码,大大减轻了 CPU 软件模拟脉冲的负担和精度问题。


一、RMT 模块概述

ESP32 的 RMT 模块提供以下主要特性:

  1. 高精度脉冲控制:能够生成和解析微秒级别的脉冲。
  2. DMA (Direct Memory Access) 支持:RMT 可以直接从内存读取数据并发送,或将接收到的数据直接写入内存,无需 CPU 干预,提高了效率。
  3. 多通道:通常有8个独立的RMT通道,每个通道都可以配置为发送或接收模式。
  4. 编码/解码灵活:可以通过编程配置,适应多种红外协议。
  5. 空闲状态检测:接收模式下可检测总线空闲,判断数据包结束。
  6. 载波调制/解调:支持对发送信号进行载波调制 (通常是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 发送配置

发送通道的初始化通常包括以下步骤:

  1. 选择 RMT 通道rmt_channel_t channel (0-7)。
  2. 选择 GPIO 引脚gpio_num_t gpio_num (连接红外 LED 的引脚)。
  3. 设置时钟分频器clk_div (用于精确控制脉冲宽度,通常设置为1或8,使得一个 RMT 计数周期为12.5ns或100ns)。
  4. 设置载波参数carrier_en, carrier_duty_percent, carrier_freq_hz (例如 38kHz, 50% 占空比)。
  5. 设置空闲电平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
// 假定使用 esp-idf go binding 或 C 语言代码
package main

import (
"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 // 示例:连接红外LED的GPIO
RMT_CARRIER_FREQ = 38000 // 38kHz
)

func initRmtTx() error {
rmtConfig := rmt.Config{
RMT_MODE: rmt.MODE_TX,
CHANNEL: RMT_TX_CHANNEL,
GPIO_NUM: RMT_TX_GPIO_NUM,
CLK_DIV: 8, // 80MHz / 8 = 10MHz -> 100ns per tick
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, // 50% 占空比
CARRIER_FREQ_HZ: RMT_CARRIER_FREQ, // 38kHz 载波
},
}

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 { // no rx buffer for tx
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
// C 语言结构体定义
typedef union {
struct {
uint32_t duration0 : 15; // 第一个脉冲的持续时间 (以tick为单位)
uint32_t level0 : 1; // 第一个脉冲的电平 (0或1)
uint32_t duration1 : 15; // 第二个脉冲的持续时间
uint32_t level1 : 1; // 第二个脉冲的电平
};
uint32_t val; // 32位原始值
} 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
// C 语言示例
rmt_item32_t nec_leader_code;
nec_leader_code.duration0 = 90000; // 9ms high
nec_leader_code.level0 = 1;
nec_leader_code.duration1 = 45000; // 4.5ms low
nec_leader_code.level1 = 0;

由于 duration 最大只有 32767,所以 9000045000 无法直接放入一个 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
// duration_ticks = microseconds * (1000 / tick_ns)
// For 100ns tick: duration_ticks = microseconds * 10
func usToRmtTicks(us uint32) uint32 {
return us * 10 // Assuming 100ns per tick (CLK_DIV=8)
}

func createNecLeaderCode() []rmt.Item32 {
// NEC 引导码: 9ms 高电平, 4.5ms 低电平
// 由于 max duration 是 32767 tick (约3.27ms),所以需要拆分

items := []rmt.Item32{}

// 9ms 高电平
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
}

// 4.5ms 低电平
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
}

// 实际发送时,我们会将这些Item发送到RMT驱动
func sendNecCode(items []rmt.Item32) error {
// ... 初始化RMT TX ...
// 实际发送函数会调用 rmt.WriteItems
return rmt.WriteItems(RMT_TX_CHANNEL, items, len(items), true)
}

注意: 实际在 Go 语言或 C 语言中,rmt_item32_t (或 Go 中的 rmt.Item32) 的定义可能略有不同。idf.rmt.Item32 通常只包含一个 durationlevel,需要将每个脉冲(高电平或低电平)作为一个单独的 Item32。这意味着你需要将连续的 duration0level0 组成一个 item,再将 duration1level1 组成下一个 item。

ESP-IDF 的 rmt_item32_t 结构实际上是:

1
2
3
4
5
6
7
// C 语言结构体定义
typedef struct {
uint32_t duration0 : 15; // 第一个脉冲的持续时间 (以tick为单位)
uint32_t level0 : 1; // 第一个脉冲的电平 (0或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) // 只需要一个 Item 来表示 9ms H + 4.5ms L

items[0] = rmt.Item32{
Duration0: uint16(usToRmtTicks(9000)), // 9ms High
Level0: 1,
Duration1: uint16(usToRmtTicks(4500)), // 4.5ms Low
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() {
// ... (idf.Init()...)
err := initRmtTx()
if err != nil {
fmt.Printf("Error initializing RMT TX: %v\n", err)
return
}

// 假设 NEC 协议的帧数据 (引导码 + 地址 + 数据 + 停止位)
// 这是一个简化的NEC码,实际需要更复杂的编码逻辑
// 实际应用中,你需要一个完整的NEC编码器函数
necItems := []rmt.Item32{
// 引导码: 9ms H, 4.5ms L
{Duration0: uint16(usToRmtTicks(9000)), Level0: 1, Duration1: uint16(usToRmtTicks(4500)), Level1: 0},
// 示例数据位 (假设0x00FF00FF,需要拆分成多个 item)
// NEC '0' 码: 560us H, 560us L
// NEC '1' 码: 560us H, 1690us L
// 以发送一个 '0' 为例 (实际需要32位数据)
{Duration0: uint16(usToRmtTicks(560)), Level0: 1, Duration1: uint16(usToRmtTicks(560)), Level1: 0}, // '0'
{Duration0: uint16(usToRmtTicks(560)), Level0: 1, Duration1: uint16(usToRmtTicks(1690)), Level1: 0}, // '1'
// ... 30 more bits ...
// 停止位: 560us H
{Duration0: uint16(usToRmtTicks(560)), Level0: 1, Duration1: 0, Level1: 0},
}

fmt.Println("Sending NEC infrared code...")
for i := 0; i < 3; i++ { // 循环发送3次
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.DriverUninstall()...)
}

三、RMT 接收 (RX) 功能详解

RMT 接收流程是将红外接收头输出的信号经过硬件解调和脉冲测量,将时间数据存储到内存中。

3.1 RMT 接收配置

接收通道的初始化包括以下步骤:

  1. 选择 RMT 通道rmt_channel_t channel
  2. 选择 GPIO 引脚gpio_num_t gpio_num (连接红外接收头的输出引脚)。
  3. 设置时钟分频器clk_div (同 TX,用于精确测量脉冲宽度)。
  4. 设置载波解调filter_en, filter_tics_thresh (过滤短于特定时长的脉冲,避免噪声)。
  5. 设置内存缓冲区rx_buf_size (DMA 缓冲区大小,用于存储接收到的 rmt_item32_t 数据)。
  6. 设置空闲阈值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 // 示例:连接红外接收头的GPIO
RMT_RX_BUFFER_SIZE = 128 // DMA 缓冲区大小 (Item 数量)
RMT_RX_IDLE_THRESHOLD = 9000 // 9ms (9000us * 10 = 90000 ticks), 假设空闲时间超过9ms认为一个数据包结束
)

func initRmtRx() error {
rmtConfig := rmt.Config{
RMT_MODE: rmt.MODE_RX,
CHANNEL: RMT_RX_CHANNEL,
GPIO_NUM: RMT_RX_GPIO_NUM,
CLK_DIV: 8, // 100ns per tick
RX_CONFIG: rmt.RXConfig{
FILTER_EN: true, // 启用滤波器
FILTER_TICS_THRESH: 100, // 过滤掉短于100个tick的脉冲 (10us)
IDLE_THRESHOLD: uint16(usToRmtTicks(RMT_RX_IDLE_THRESHOLD) / 10), // Convert to us for RMT_RX_IDLE_THRESHOLD; RMT IDLE threshold is in ticks
// Note: The conversion for IDLE_THRESHOLD might need to be adjusted based on the actual ESP-IDF API,
// typically it's directly in ticks but some bindings might expect microseconds.
// Assuming usToRmtTicks() for consistency here but verify `rmt.RXConfig.IDLE_THRESHOLD`'s unit.
// Let's assume it wants ticks directly.
// IDLE_THRESHOLD: usToRmtTicks(RMT_RX_IDLE_THRESHOLD), // Should be in ticks
},
}
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 { // RX缓冲区大小
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.Installrx_buf_size 参数通常是以字节为单位,所以需要 RMT_RX_BUFFER_SIZE * idf.SizeofRmtItem32

3.2 接收数据

接收数据通常涉及事件队列和DMA缓冲区。

  1. 启动接收rmt_rx_start
  2. 获取事件队列rmt_get_event_queue (用于接收RMT事件,如接收完成)。
  3. 等待事件:从事件队列中读取事件。
  4. 读取数据:当接收到 RMT_RX_DONE 事件时,从 RMT DMA 缓冲区中读取 rmt_item32_t 数据。
  5. 停止接收rmt_rx_stop (在处理完一个数据包后)。
  6. 解析数据:根据接收到的脉冲持续时间,反向解析出红外协议的逻辑位。

代码示例 (接收并解析红外数据):

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
// 这是一个简化的示例,实际的NEC解析需要更复杂的状态机或逻辑
func parseNecCode(items []rmt.Item32) (uint32, error) {
if len(items) < 3 { // 至少需要引导码和停止位
return 0, fmt.Errorf("too few RMT items to parse NEC code")
}

// 假设 RMT_TX_CLK_DIV = 8,tick = 100ns
// NEC 协议脉冲时间定义
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 // +/- 150us 容忍度
)

matchDuration := func(actualTicks uint16, expectedUs uint32) bool {
actualUs := uint32(actualTicks) / 10 // Convert ticks to us
lowerBound := expectedUs - NEC_TOLERANCE_US
upperBound := expectedUs + NEC_TOLERANCE_US
return actualUs >= lowerBound && actualUs <= upperBound
}

// 检查引导码
// 第一个 item 通常包含引导码的高低电平
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
// 忽略引导码的第一个item,从第二个item开始解析数据位
// NEC 协议有32位数据,再加上停止位,总共66个脉冲(33个Item,如果每个Item只存一个脉冲)
// 或者 1个引导Item + 16个数据Item + 1个停止Item (如果每个数据Item存2个位)
// 实际通常是 1个引导Item + 32个数据脉冲 + 1个停止脉冲
// 所以需要处理 34个RMT Item (引导高,引导低,32个数据脉冲对,停止高)
// 或者 1个引导Item, 32个 (高电平, 0/1低电平) 脉冲对,1个停止Item
// 为了简化,我们假设每个RMT Item只代表一个高/低脉冲,且是解调后的

// 这里需要根据实际接收的 RMT Item 结构来调整解析逻辑
// 假设接收到的是一系列的 H-L-H-L... 脉冲对,每个脉冲对代表一个位
// 引导码占 2 个脉冲 (1个 Item 的 Duration0/1)
// 32 位数据占 32 对脉冲 (每个数据位都是 H-L) -> 32 * 2 = 64 个脉冲
// 停止位 1 个脉冲 (H) -> 1 个脉冲
// 总共 2 + 64 + 1 = 67 个脉冲。
// 如果 RMT Item 是 `Duration0, Level0, Duration1, Level1` 结构,则需要大约 34 个 Item

// 跳过引导码
dataItems := items[1:] // 假设引导码被完美封装在 items[0]

for i := 0; i < len(dataItems)-1; i++ { // 循环直到停止位
if bitCount >= 32 { // NEC 协议是 32 位数据
break
}

item := dataItems[i]

// 期望是高电平脉冲,然后是低电平脉冲
if item.Level0 == 0 || !matchDuration(item.Duration0, NEC_BIT_HIGH_US) {
//fmt.Printf("Data bit %d: Expected High pulse, got L=%d H=%d\n", bitCount, item.Level0, item.Duration0/10)
return 0, fmt.Errorf("data bit %d: expected high pulse", bitCount)
}

if item.Duration1 == 0 { // 这种情况通常发生在最后一位,或脉冲被截断
// 处理最后一个脉冲 (停止位)
if matchDuration(item.Duration0, NEC_STOP_HIGH_US) {
//fmt.Println("Found stop bit")
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 // 接收到0
} else if matchDuration(item.Duration1, NEC_BIT_1_LOW_US) {
data = (data << 1) | 1 // 接收到1
} else {
return 0, fmt.Errorf("data bit %d: invalid low pulse duration: %d us", bitCount, item.Duration1/10)
}
bitCount++
}

// 还需要检查停止位
// 实际中可能需要判断最后一个item是否只包含停止位的高电平
// 或者通过总的脉冲数量来判断

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) // buffer 10 events
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)

// 从 RMT 缓冲区读取数据
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]
// 打印原始脉冲数据 (调试用)
// for i, item := range receivedItems {
// fmt.Printf("Item %d: D0=%d L0=%d, D1=%d L1=%d\n", i, item.Duration0, item.Level0, item.Duration1, item.Level1)
// }

// 解析 NEC 协议数据
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)
}
}

// 处理完数据后,需要重新启动接收,否则 RMT 将停止接收
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() {
// 实际应用中,你需要调用 idf.Init()
// idf.Init()

// 在 Go 中,你可以同时启动 TX 和 RX,例如使用 goroutine
go startRmtRxLoop()
//go sendNecCodeLoop() // 如果你需要同时测试发送

// 防止 main 函数退出
select {}
}

四、RMT 的局限性与注意事项

  1. 内存使用:DMA 缓冲区需要占用较大的内存,特别是 RX 模式。需要合理设置缓冲区大小。
  2. 协议复杂性:虽然 RMT 提供了硬件支持,但解析和编码复杂的红外协议(如 RC5/RC6 存在曼彻斯特编码、翻转位等)仍然需要应用程序层的软件逻辑。
  3. 时钟精度:RMT 的脉冲精度受 ESP32 内部时钟和 CLK_DIV 的影响。选择合适的 CLK_DIV 可以在精度和最大脉冲长度之间取得平衡。
  4. GPIO 复用:每个 RMT 通道绑定一个 GPIO 引脚,确保 GPIO 不被其他外设复用。
  5. 空闲阈值:RX 模式下的 idle_threshold 是区分不同红外数据包的关键,设置不当可能导致数据包合并或截断。
  6. 载波调制/解调:确保发送和接收的载波频率匹配,且接收端滤波器设置合理。

五、总结

ESP32 的 RMT 模块是一个功能强大且高度灵活的外设,为红外遥控和其他时间敏感的脉冲信号处理提供了理想的硬件加速方案。通过精确配置 RMT 通道参数、合理构造 rmt_item32_t 序列进行发送,以及利用 DMA 缓冲区和事件队列高效接收和解析脉冲数据,开发者可以轻松实现各种自定义的红外通信协议,从而在智能家居、遥控设备等物联网应用中发挥重要作用。尽管需要一定的软件逻辑来处理协议细节,但 RMT 大大减轻了 CPU 的负担,使得 ESP32 在这类应用中表现出色。