ESP32 是一款功能强大的 Wi-Fi 和蓝牙双模芯片,其丰富的硬件外设使其在处理各种脉冲信号方面表现出色。无论是生成精确的脉冲,还是精确测量外部脉冲,ESP32 都提供了多种灵活高效的解决方案。本文将详细介绍 ESP32 处理脉冲信号的几种主要方式及其适用场景。

核心思想: ESP32 通过集成专用硬件模块(如 PWM、RMT、PCNT)来高效、精确地生成和测量脉冲信号,从而解放 CPU,提高实时性和系统整体性能。


一、脉冲信号基础

脉冲信号是指在电平或强度上发生短暂变化的信号。在数字电子中,脉冲通常表现为高电平 (High) 和低电平 (Low) 之间的快速切换。

脉冲的几个关键参数:

  • 周期 (Period):一个完整脉冲波形所需的时间。
  • 频率 (Frequency):每秒钟脉冲重复的次数,频率 = 1 / 周期。
  • 脉宽 (Pulse Width):脉冲处于高电平或低电平的持续时间。
  • 占空比 (Duty Cycle):高电平脉宽与周期之比,通常以百分比表示。
    $占空比 = (高电平脉宽 / 周期) \times 100%$

二、ESP32 脉冲生成方式

2.1 PWM (Pulse Width Modulation) - 脉冲宽度调制

PWM 是一种通过调制方波的占空比来模拟模拟信号的技术。ESP32 提供了专门的 LEDC (LED Control) 外设来生成 PWM 信号。

2.1.1 LEDC 模块特性

  • 独立通道:ESP32 有 16 个独立的 PWM 通道。
  • 高分辨率:支持最高 16 位的 PWM 占空比分辨率(意味着一个周期可以分成 $2^{16}$ 份)。
  • 宽频率范围:从几 Hz 到几十 MHz。
  • 硬件控制:一旦配置完成,PWM 信号完全由硬件生成,不占用 CPU 资源。
  • 渐变功能:支持平滑的占空比渐变 (fade) 功能。

2.1.2 适用场景

  • LED 亮度控制:通过改变占空比控制 LED 亮度。
  • 电机速度控制:驱动直流电机,通过改变占空比控制转速。
  • 舵机控制:通过发送特定脉宽的脉冲来控制舵机角度。
  • DAC 模拟:通过低通滤波器将高频 PWM 信号转换为模拟电压。
  • 音频输出:用于简单的音频播放(通常需要高频 PWM 和滤波)。

2.1.3 使用示例 (Go 语言)

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
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/ledc"
)

const (
PWM_GPIO = gpio.NUM_2 // 示例:连接LED的GPIO引脚
PWM_CHANNEL = ledc.CHANNEL_0
PWM_TIMER = ledc.TIMER_0
PWM_FREQ_HZ = 5000 // 5kHz
PWM_RESOLUTION = ledc.RESOLUTION_10_BIT // 10位分辨率
)

func main() {
idf.Init()

// 1. 配置LED PWM Timer
timerConfig := ledc.TimerConfig{
MODE: ledc.MODE_LOW_SPEED, // 或 ledc.MODE_HIGH_SPEED
FREQ_HZ: PWM_FREQ_HZ,
DUTY_RESOLUTION: PWM_RESOLUTION,
TIMER_NUM: PWM_TIMER,
CLK_CFG: ledc.REF_TICK, // 使用参考时钟 (1MHz)
}
if err := ledc.TimerConfig(&timerConfig); err != nil {
fmt.Printf("Failed to config LEDC timer: %v\n", err)
return
}

// 2. 配置LED PWM Channel
channelConfig := ledc.ChannelConfig{
GPIO_NUM: PWM_GPIO,
SPEED_MODE: ledc.MODE_LOW_SPEED,
CHANNEL: PWM_CHANNEL,
TIMER_SEL: PWM_TIMER,
INTR_TYPE: ledc.INTR_DISABLE, // 不启用中断
DUTY: 0, // 初始占空比为0
HPOINT: 0,
}
if err := ledc.ChannelConfig(&channelConfig); err != nil {
fmt.Printf("Failed to config LEDC channel: %v\n", err)
return
}

fmt.Printf("PWM on GPIO %d configured. Freq: %dHz, Resolution: %d-bit\n", PWM_GPIO, PWM_FREQ_HZ, PWM_RESOLUTION)

// 3. 改变占空比来控制LED亮度
maxDuty := uint32(1<<PWM_RESOLUTION) - 1 // 10位分辨率下最大值1023

for {
for duty := uint32(0); duty <= maxDuty; duty += 10 {
if err := ledc.SetDuty(ledc.MODE_LOW_SPEED, PWM_CHANNEL, duty); err != nil {
fmt.Printf("Failed to set duty: %v\n", err)
}
ledc.UpdateDuty(ledc.MODE_LOW_SPEED, PWM_CHANNEL) // 更新占空比
time.Sleep(5 * time.Millisecond)
}
for duty := maxDuty; duty >= 0; duty -= 10 {
if err := ledc.SetDuty(ledc.MODE_LOW_SPEED, PWM_CHANNEL, duty); err != nil {
fmt.Printf("Failed to set duty: %v\n", err)
}
ledc.UpdateDuty(ledc.MODE_LOW_SPEED, PWM_CHANNEL)
time.Sleep(5 * time.Millisecond)
}
}
}

2.2 RMT (Remote Control) - 红外遥控模块

RMT 模块是一个非常灵活的硬件外设,设计用于处理时间敏感的脉冲序列,如红外遥控信号。它不仅能接收,也能高精度地发送脉冲。

2.2.1 RMT 模块特性

  • 高精度:能够生成和测量微秒甚至纳秒级别的脉冲(取决于时钟分频)。
  • DMA 支持:通过 DMA 将脉冲数据从内存发送或接收到内存,无需 CPU 实时干预。
  • 多通道:通常有 8 个独立的 RMT 通道,每个通道可配置为发送或接收。
  • 载波调制/解调:特别适合红外通信,可自动处理载波。
  • 数据项 (Item) 结构:通过一系列 rmt_item32_t 结构体描述脉冲,每个结构体可包含两个脉冲段。

2.2.2 适用场景

  • 红外遥控:发送和接收 NEC、RC5、RC6 等各种红外协议。
  • 自定义串行通信:实现位宽精确的自定义串行协议(如单线通信协议)。
  • 精确脉冲序列生成:生成特定的、时间精度要求高的脉冲串。
  • 步进电机控制:如果需要非常精确的脉冲序列来控制步进电机。

2.2.3 使用示例 (Go 语言)

详见前文 “ESP32 RMT红外控制详解” 的发送部分。核心是构建 []rmt.Item32 数组并使用 rmt.WriteItems 发送。

2.3 GPIO 输出 (软件模拟)

对于频率要求不高、精度要求不苛刻的脉冲,可以直接通过 GPIO 软件控制高低电平来生成。

2.3.1 特性

  • 简单易用:直接调用 gpio.SetLevel()time.Sleep()
  • 灵活性高:可以生成任意复杂的脉冲波形。
  • CPU 占用高:CPU 需要实时控制 GPIO,如果脉冲频率高或需要精确计时,会大量占用 CPU。
  • 精度差time.Sleep() 的精度受操作系统调度和中断影响,无法保证微秒级甚至纳秒级精度。

2.3.2 适用场景

  • 低频信号:如控制继电器、蜂鸣器等。
  • 简单协议:如软件模拟 I2C、SPI 等(但通常有硬件 I2C/SPI 模块)。
  • 调试和测试:快速生成一个简单的脉冲。

2.3.3 使用示例 (Go 语言)

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
package main

import (
"fmt"
"time"

"github.com/espressif/esp-idf-go/idf"
"github.com/espressif/esp-idf-go/idf/hal/gpio"
)

const (
SOFTWARE_PWM_GPIO = gpio.NUM_4 // 示例:GPIO4
SOFTWARE_PWM_FREQ = 100 // 100Hz
SOFTWARE_PWM_DUTY = 50 // 50% 占空比
)

func main() {
idf.Init()

gpio.SetMode(SOFTWARE_PWM_GPIO, gpio.MODE_OUTPUT)
gpio.SetDriveStrength(SOFTWARE_PWM_GPIO, gpio.DRIVE_STRENGTH_STRONGER) // 设置驱动能力

periodMs := time.Duration(1000/SOFTWARE_PWM_FREQ) * time.Millisecond
highTime := time.Duration(float64(periodMs) * float64(SOFTWARE_PWM_DUTY) / 100.0)
lowTime := periodMs - highTime

fmt.Printf("Software PWM on GPIO %d. Freq: %dHz, Duty: %d%%\n", SOFTWARE_PWM_GPIO, SOFTWARE_PWM_FREQ, SOFTWARE_PWM_DUTY)

for {
gpio.SetLevel(SOFTWARE_PWM_GPIO, 1) // 高电平
time.Sleep(highTime)
gpio.SetLevel(SOFTWARE_PWM_GPIO, 0) // 低电平
time.Sleep(lowTime)
}
}

三、ESP32 脉冲测量方式

3.1 PCNT (Pulse Counter) - 脉冲计数器

PCNT 模块是一个专用的硬件脉冲计数器,能够高速、精确地计数输入 GPIO 上的脉冲。

3.1.1 PCNT 模块特性

  • 8 个独立通道:ESP32 有 8 个脉冲计数器单元。
  • 正向/反向计数:可配置为只计数上升沿、只计数下降沿,或同时计数上升沿/下降沿(用于编码器)。
  • 可编程阈值:当计数达到预设阈值时触发中断。
  • 软件清零:计数器可随时通过软件清零。
  • 过滤器:可配置输入信号滤波器,过滤毛刺和噪声。
  • 自由运行:计数过程独立于 CPU,不占用 CPU 资源。

3.1.2 适用场景

  • 旋转编码器:测量旋转角度或速度。
  • 流量计:测量流体通过的脉冲数量。
  • 事件计数:计数外部触发事件的次数。
  • 频率测量:在一定时间内计数脉冲,从而计算频率。
  • 测量脉冲宽度:结合定时器,测量单个脉冲的持续时间(较复杂)。

3.1.3 使用示例 (Go 语言)

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
package main

import (
"fmt"
"time"
"runtime"
"unsafe"

"github.com/espressif/esp-idf-go/idf"
"github.com/espressif/esp-idf-go/idf/hal/gpio"
"github.com/espressif/esp-idf-go/idf/hal/pcnt"
)

const (
PCNT_GPIO_NUM = gpio.NUM_34 // 示例:连接脉冲源的GPIO
PCNT_UNIT = pcnt.UNIT_0
)

func main() {
idf.Init()

// 1. 配置 PCNT Unit
pcntConfig := pcnt.Config{
UNIT: PCNT_UNIT,
CHANNEL: pcnt.CHANNEL_0,
GPIO_NUM: PCNT_GPIO_NUM,
PIN_MODE: pcnt.COUNT_DIS, // 初始禁用计数
LCTRL_MODE: pcnt.COUNT_DIS,
HCTRL_MODE: pcnt.COUNT_INC, // 高电平边沿计数
POS_MODE: pcnt.COUNT_INC, // 上升沿计数
NEG_MODE: pcnt.COUNT_DIS, // 下降沿不计数
COUNTER_L_LIM: -32768, // 计数器范围下限
COUNTER_H_LIM: 32767, // 计数器范围上限
}
if err := pcnt.UnitConfig(&pcntConfig); err != nil {
fmt.Printf("Failed to config PCNT unit: %v\n", err)
return
}

// 2. 配置 PCNT 过滤器 (可选)
// 过滤短于 100 个 APB_CLK 周期 (80MHz APB_CLK, 1.25ns/tick) 的脉冲,用于去抖
if err := pcnt.FilterEnable(PCNT_UNIT); err != nil {
fmt.Printf("Failed to enable PCNT filter: %v\n", err)
}
if err := pcnt.SetFilterValue(PCNT_UNIT, 100); err != nil { // 过滤值通常根据实际需求设定
fmt.Printf("Failed to set PCNT filter value: %v\n", err)
}

// 3. 清零计数器并启动
if err := pcnt.CounterClear(PCNT_UNIT); err != nil {
fmt.Printf("Failed to clear PCNT counter: %v\n", err)
}
if err := pcnt.CounterResume(PCNT_UNIT); err != nil { // 启用计数
fmt.Printf("Failed to resume PCNT counter: %v\n", err)
}

fmt.Printf("PCNT Unit %d configured on GPIO %d. Counting rising edges...\n", PCNT_UNIT, PCNT_GPIO_NUM)

ticker := time.NewTicker(1 * time.Second) // 每秒读取一次
defer ticker.Stop()

for range ticker.C {
count, err := pcnt.GetCounterValue(PCNT_UNIT)
if err != nil {
fmt.Printf("Failed to get PCNT counter value: %v\n", err)
continue
}
fmt.Printf("Pulse count: %d\n", count)
// 如果需要清零并重新计数,可以在这里调用 pcnt.CounterClear(PCNT_UNIT)
}
}

3.2 RMT 接收 (RX)

RMT 模块在接收模式下可以精确测量外部脉冲的持续时间。

3.2.1 RMT RX 特性

  • 硬件解调:可自动解调带有载波的信号,还原原始脉冲。
  • DMA 接收:将测量到的脉冲高/低电平持续时间数据存储到内存。
  • 空闲阈值:用于判断一个脉冲序列(数据包)的结束。
  • 高精度测量:与发送模式相同,提供微秒级甚至纳秒级精度。

3.2.2 适用场景

  • 红外遥控数据解析:测量红外接收头输出的脉冲序列,并解析为数据。
  • 脉冲编码器:测量特殊编码的脉冲信号。
  • 测量自定义脉冲协议:解析其他设备发送的自定义脉冲协议。

3.2.3 使用示例 (Go 语言)

详见前文 “ESP32 RMT红外控制详解” 的接收部分。核心是配置 RMT RX 通道,启动接收,然后从事件队列和 DMA 缓冲区中读取 []rmt.Item32 数组并解析。

3.3 GPIO 输入 (中断或轮询)

对于频率较低、无需精确时间测量的脉冲,可以通过 GPIO 中断或轮询来检测。

3.3.1 特性

  • 中断:当 GPIO 电平变化时触发中断,CPU 在中断服务程序 (ISR) 中处理。
    • 优点:实时性较好,CPU 占用低(只在事件发生时处理)。
    • 缺点:ISR 尽可能短,不能执行耗时操作。
  • 轮询:CPU 周期性读取 GPIO 状态。
    • 优点:简单。
    • 缺点:实时性差,CPU 占用高。不适合高频脉冲。
  • 精度差:与软件生成脉冲类似,时间测量依赖软件 time.Now() 等,精度受限。

3.3.2 适用场景

  • 按键检测:检测按键按下或释放。
  • 传感器状态变化:如光电开关、门磁开关等。
  • 低频事件计数:使用中断服务程序来计数。

3.3.3 使用示例 (Go 语言 - 中断)

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
package main

import (
"fmt"
"sync/atomic"
"time"

"github.com/espressif/esp-idf-go/idf"
"github.com/espressif/esp-idf-go/idf/hal/gpio"
)

const (
GPIO_INT_NUM = gpio.NUM_35 // 示例:连接外部脉冲的GPIO
)

var (
pulseCount uint32
)

// ISR函数,在Go中通过Cgo包装
// 注意:ISR中不能做耗时操作,不能使用fmt.Println等
func gpioISR(arg unsafe.Pointer) {
atomic.AddUint32(&pulseCount, 1)
}

func main() {
idf.Init()

gpio.SetMode(GPIO_INT_NUM, gpio.MODE_INPUT)
gpio.SetPullMode(GPIO_INT_NUM, gpio.PULL_UP) // 上拉电阻

// 配置中断:上升沿触发
gpio.SetIntType(GPIO_INT_NUM, gpio.INTR_POS_EDGE)

// 安装中断服务程序 (ISR)
if err := gpio.InstallISRService(0); err != nil { // 0为默认ISR标志
fmt.Printf("Failed to install GPIO ISR service: %v\n", err)
return
}
if err := gpio.ISRHandlerAdd(GPIO_INT_NUM, gpioISR, nil); err != nil { // 注册ISR
fmt.Printf("Failed to add GPIO ISR handler: %v\n", err)
return
}
if err := gpio.IntEnable(GPIO_INT_NUM); err != nil { // 启用GPIO中断
fmt.Printf("Failed to enable GPIO interrupt: %v\n", err)
return
}

fmt.Printf("GPIO %d configured for rising edge interrupt. Counting pulses...\n", GPIO_INT_NUM)

for {
time.Sleep(1 * time.Second)
count := atomic.LoadUint32(&pulseCount)
fmt.Printf("Total pulses detected: %d\n", count)
}
}

四、总结与选择指南

ESP32 提供了多种处理脉冲信号的方法,选择哪种方式取决于具体的应用需求:

特性 / 模块 PWM (LEDC) RMT (Remote Control) PCNT (Pulse Counter) GPIO (软件模拟)
功能 精确生成可变占空比脉冲 精确生成/测量任意复杂脉冲序列 高速计数外部脉冲 软件控制高低电平
精度 硬件级,高精度 (纳秒/微秒) 硬件级,极高精度 (纳秒) 硬件级,高精度计数 软件级,精度差 (毫秒级,受OS调度影响)
CPU 占用 极低 (硬件自动运行) 极低 (DMA传输,硬件自动运行) 极低 (硬件自动运行) 高 (实时CPU控制或中断处理)
频率范围 几 Hz 到几十 MHz 数百 Hz 到几十 MHz (取决于CLK_DIV) 最高达几十 MHz 几 Hz 到几 kHz (取决于CPU负载和代码效率)
应用场景 LED亮度、电机调速、舵机、模拟信号输出 红外遥控、自定义串行协议、精确时序脉冲 旋转编码器、流量计、事件计数、频率粗测 按键检测、低频信号、简单开关量控制
优势 高分辨率,多通道,支持渐变 极高精度,DMA,载波调制/解调 高速计数,正反向,可编程阈值,过滤器 简单易用,灵活性高
劣势 只能生成方波,不能直接测量 数据结构复杂,解析协议需软件 主要用于计数,测量脉宽需结合其他模块 占用CPU,精度低,不适合高频和时间敏感应用

在设计 ESP32 应用程序时,应优先考虑使用专门的硬件外设(LEDC、RMT、PCNT),因为它们能提供更好的性能、更高的精度和更低的 CPU 负载,从而使系统更加稳定和高效。只有在硬件模块无法满足需求或功能过于简单时,才考虑使用 GPIO 软件模拟的方式。