【ESP32】ESP-IDF开发 | PWM脉宽调制器+PWM波形输出和捕获例程
1. 简介
脉宽调制器 (PWM) 广泛用于电机和电源的控制。在ESP32的PWM模块如下图所示。
PWM外设中包含一个时钟分频器(预分频器),三个 PWM 定时器,三个 PWM 操作器和一个捕获模块。 定时器和操作器之间是可以相互绑定的,不仅能一对一,也可以一对多,多对多地绑定,这使得ESP32可以适应各种各样的应用需求。
1.1 定时器模块
定时器模块包括预分频器和定时器。定时器模块只能使用160MHz的时钟输入,因此要通过预分配器分频到较低的频率。PWM的定时器有3个,定时器的输出既能通过自身的计数,也能通过外部的信号脉冲实现。
定时器可以配置成递增、递减和递增递减循环计数模式,前面两种用于生成非对称PWM波,后一种用于生成对称PWM波。它们的工作原理如下:
- 递增计数模式:定时器从0开始递增,计数器的值到达周期寄存器值时清零并重新开始计数;
- 递减计数模式:定时器从周期寄存器的值开始递减,当计数器的值达到0时,计数器值恢复为周期寄存器的值,重新计数;
- 递增递减循环计数模式:结合了前两种模式,定时器从0开始递增,直到达到周期值,再次递减为0。PWM周期为周期寄存器的周期值 × 2 + 1。
定时器运行时会产生以下的事件:
- UTEP:当定时器等于周期寄存器值且定时器递增计数时生成的定时事件;
- UTEZ:当定时器等于0且定时器递增计数时生成的定时事件;
- DTEP:当定时器等于周期寄存器值且定时器递减时生成的定时事件;
- DTEZ:当定时器等于0且定时器递减时生成的定时事件。
1.2 操作器模块
PWM外设拥有3个操作器。它们的功能非常丰富,除了常规的生成PWM波形外;还可以生成死区用于电机控制;也可以生成PWM载波,但应用比较少。同时操作器可以接收错误事件,根据配置执行对应操作,如PWM拉高、拉低等等。
1.2.1 PWM波形生成
操作器模块最基本的功能就是生成PWM波,不同占空比的波形由寄存器A和B来决定,它们分别决定PWMB和PWMA的波形(是反的,比较反人类)。
下面是一个递增计数模式的波形示例,周期为6,寄存器A为3,寄存器B为5,高电平有效。
PWMA的占空比由寄存器B决定,定时器开始计数时,PWMA为高电平;当计数器值到达寄存器B的值时,PWMA变为低电平;当定时器到达周期寄存器值,产生溢出时,电平由恢复为高电平。PWMB的占空比由寄存器A决定,流程与上面类似。
下面再展示一个递增递减模式的例子,其他参数和上面一样。
定时器开始计数时,PWMA输出同样为高电平,当计数器值到达寄存器B值时,输出为低电平;定时器值到达周期寄存器值时,计数器产生上溢并开始向下计数;当计数器的值再次达到寄存器B的值时,输出电平又变回高电平。PWMB的流程类似。
1.2.2 死区生成器
在电力电子学中,常常会用到整流器和逆变器,这就涉及到了整流桥和逆变桥的应用。每个桥臂配有两个功率电子器件,例如MOSFET、IGBT等。同一桥臂上的两个MOSFET不能同时导通,否则会造成短路。所以在实际应用中,在PWM波形显示MOSFET开关已关闭后,仍需要一段时间窗口才能完全关闭MOSFET,这就是死区。
死区生成器可以通过在波形的上升沿或下降沿增加延迟来生成死区。对信号对而言,可以生成:高电平有效互补(AHC)、低电平有效互补(ALC)、高电平有效(AH)和低电平有效(AL),4种模式,它们的形状分别如下。
1.2.3 PWM载波模块
将PWM输出耦合到电机驱动器可能需要使用变压器隔离。变压器只提供交流信号,而PWM信号的占空比可能在0%到100%之间变化。PWM载波模块可以通过使用高频载波对其进行调制,将该信号传递给变压器。
从上图可以看到,当PWM输出处于有效时,载波模块会进行调制输出,并且用户可以设置第一个脉冲的宽度。载波的占空比有8种选择,相当于分辨率为12.5%。
第一个脉冲的宽度有16种可能,满足公式:
TPMW_clk为PWM时钟周期 (PWM_clk) ;(PWM_CARRIERx_OSHTWTH + 1) 为一次性脉冲宽度值(取值范围:1-16);(PWM_CARRIERx_PRESCALE + 1) PWM载波时钟 (PC_clk) 预分频值。
1.3 故障检测模块
故障检测模块比较简单,就是检测管脚的状态,从而发送一个故障事件,操作器再根据事件执行对应的操作。当故障事件发生时,可以配置以下的PWM输出信号状态:高、低、取反和无操作。故障操作可分为逐周期操作(CBC)和一次性操作(OST)。
逐周期操作(CBC):
故障事件来临时,根据寄存器的设置改变PWM的输出波形;当没有故障事件时,故障操作会在下一次定时器事件(如D/UTEP或D/UTEZ事件)时自动清除。
一次性操作(OST):
故障事件来临时,根据寄存器的设置改变PWM的输出波形;当没有故障事件时,故障操作会在不会清除,必须人为对寄存器取反进行清除。
1.4 捕获模块
捕获模块也较简单,配置捕获信号的极性和预分频对管脚信号进行捕获。 ESP32的PWM捕获模块有3个通道,可以配置任意的GPIO管脚进行连接;每个通道配有一个32位时间戳和一个捕获预分频器,预分频范围1-256;任何捕获通道的边沿极性(上升/下降沿)可独立选择。
2. 例程
例程中演示PWM的输出和捕获,PWM输出25%、50%和75%占空比的波形,每2秒改变一次,输出管脚连接捕获管脚,对输出的波形进行捕获并计算占空比。
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "driver/mcpwm_timer.h"
#include "driver/mcpwm_oper.h"
#include "driver/mcpwm_gen.h"
#include "driver/mcpwm_cap.h"
#include "driver/mcpwm_cmpr.h"
#define TAG "app"
#define MCPWM_FREQ 1000000
#define MCPWM_PERIOD 20000
#define DUTY_CHANGE_PERIOD_MS 2000
#define MCPWM_CAP_FREQ 1000000
typedef struct {
uint32_t period;
uint32_t duty;
} cap_result;
static QueueHandle_t noti;
static bool mcpwm_capture_cb(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_data_t *edata, void *user_ctx)
{
static uint32_t last_period = 0;
static uint32_t last_duty = 0;
static uint32_t last_pos = 0;
static uint32_t cur_pos = 0;
static uint32_t neg = 0;
BaseType_t higher_task_woken = pdFALSE;
if (edata->cap_edge == MCPWM_CAP_EDGE_POS) {
last_pos = cur_pos;
cur_pos = edata->cap_value;
uint32_t period = cur_pos - last_pos;
uint32_t duty = neg - last_pos;
if (last_period != period || last_duty != duty) { // 只有在改变时才通知
cap_result result = {period, duty};
xQueueSendFromISR(noti, &result, &higher_task_woken);
last_period = period;
last_duty = duty;
}
} else {
neg = edata->cap_value;
}
return higher_task_woken == pdTRUE;
}
static void capture_log_task(void *args)
{
mcpwm_cap_timer_handle_t *timer = (mcpwm_cap_timer_handle_t *) args;
while (1) {
cap_result result = {0};
if (pdTRUE == xQueueReceive(noti, &result, portMAX_DELAY)) {
uint32_t cap_freq = 0;
mcpwm_capture_timer_get_resolution(*timer, &cap_freq);
float period = result.period * 1000.0f / cap_freq;
float duty = result.duty * 100.0f / result.period;
ESP_LOGI(TAG, "capture period: %.2f ms, duty: %.2f%%", period , duty);
}
}
}
void app_main()
{
/* 初始化定时器 */
static mcpwm_timer_handle_t timer = NULL;
mcpwm_timer_config_t timer_config = {
.group_id = 0, // 定时器组0
.clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, // 默认时钟源,默认为160MHz
.resolution_hz = MCPWM_FREQ, // 频率1MHz
.period_ticks = MCPWM_PERIOD, // 周期20ms
.count_mode = MCPWM_TIMER_COUNT_MODE_UP, // 向上计数
};
ESP_ERROR_CHECK(mcpwm_new_timer(&timer_config, &timer));
/* 初始化操作器 */
static mcpwm_oper_handle_t oper = NULL;
mcpwm_operator_config_t operator_config = {
.group_id = 0, // 操作器组0
};
ESP_ERROR_CHECK(mcpwm_new_operator(&operator_config, &oper));
/* 绑定定时器与操作器 */
ESP_ERROR_CHECK(mcpwm_operator_connect_timer(oper, timer));
/* 初始化生成器 */
static mcpwm_gen_handle_t generator = NULL;
mcpwm_generator_config_t generator_config = {
.gen_gpio_num = 17, // 输出管脚
};
ESP_ERROR_CHECK(mcpwm_new_generator(oper, &generator_config, &generator));
/* 初始化比较器 */
static mcpwm_cmpr_handle_t comparator = NULL;
mcpwm_comparator_config_t comparator_config = {
.flags.update_cmp_on_tez = true, // 计数器为0时更新比较器值
};
ESP_ERROR_CHECK(mcpwm_new_comparator(oper, &comparator_config, &comparator));
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, 5000)); // 初始占空比25%
/* 初始化输出波形 */
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_timer_event(generator,
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH)));
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(generator,
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, comparator, MCPWM_GEN_ACTION_LOW)));
/* 使能波形输出 */
ESP_ERROR_CHECK(mcpwm_timer_enable(timer));
ESP_ERROR_CHECK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP));
/* 初始化捕获定时器 */
static mcpwm_cap_timer_handle_t cap_timer = NULL;
mcpwm_capture_timer_config_t cap_conf = {
.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT, // 默认时钟源,默认为80MHz
.group_id = 1, // 定时器组1
};
ESP_ERROR_CHECK(mcpwm_new_capture_timer(&cap_conf, &cap_timer));
/* 初始化捕获通道 */
static mcpwm_cap_channel_handle_t cap_chan = NULL;
mcpwm_capture_channel_config_t cap_ch_conf = {
.gpio_num = 18, // 捕获管脚
.prescale = 1, // 预分频系数
.flags.pos_edge = true, // 上升沿捕获
.flags.neg_edge = true, // 下降沿捕获
.flags.pull_up = true, // 内部上拉
};
ESP_ERROR_CHECK(mcpwm_new_capture_channel(cap_timer, &cap_ch_conf, &cap_chan));
/* 注册捕获回调 */
mcpwm_capture_event_callbacks_t cap_cbs = {
.on_cap = mcpwm_capture_cb,
};
ESP_ERROR_CHECK(mcpwm_capture_channel_register_event_callbacks(cap_chan, &cap_cbs, NULL));
/* 创建捕获任务 */
noti = xQueueCreate(1, sizeof(cap_result));
xTaskCreate(capture_log_task, "cap_task", 2048, &cap_timer, 5, NULL);
/* 使能捕获 */
ESP_ERROR_CHECK(mcpwm_capture_channel_enable(cap_chan));
ESP_ERROR_CHECK(mcpwm_capture_timer_enable(cap_timer));
ESP_ERROR_CHECK(mcpwm_capture_timer_start(cap_timer));
while (1) {
static int stage = 0;
uint32_t tick = (stage == 0 ? 5000 : (stage == 1 ? 10000 : (stage == 2 ? 15000 : 5000)));
mcpwm_comparator_set_compare_value(comparator, tick);
ESP_LOGI(TAG, "duty change to %.1f%%", tick * 100.0f / MCPWM_PERIOD);
stage = (stage + 1) % 3;
vTaskDelay(DUTY_CHANGE_PERIOD_MS / portTICK_PERIOD_MS);
}
}
这个例程的代码量是比较大的,主要分为PWM波输出和PWM波捕获两部分。
1. PWM波生成
第一步,先初始化定时器,初始化结构体如下。
typedef struct {
int group_id;
mcpwm_timer_clock_source_t clk_src;
uint32_t resolution_hz;
mcpwm_timer_count_mode_t count_mode;
uint32_t period_ticks;
int intr_priority;
struct {
uint32_t update_period_on_empty: 1;
uint32_t update_period_on_sync: 1;
} flags;
} mcpwm_timer_config_t;
- group_id:定时器组号(0-2);
- clk_src:定时器时钟源;
- resolution_hz:定时器分辨率,即频率;
- count_mode:计数模式;
- period_ticks:周期寄存器值;
- intr_priority:中断优先级;
- update_period_on_empty:计数器值为0时更新周期寄存器值;
- update_period_on_sync:同步事件更新周期寄存器值。
第二步,初始化操作器,初始化结构体如下。
typedef struct {
int group_id;
int intr_priority;
struct {
uint32_t update_gen_action_on_tez: 1;
uint32_t update_gen_action_on_tep: 1;
uint32_t update_gen_action_on_sync: 1;
uint32_t update_dead_time_on_tez: 1;
uint32_t update_dead_time_on_tep: 1;
uint32_t update_dead_time_on_sync: 1;
} flags;
} mcpwm_operator_config_t;
- group_id:操作器组号(0-2);
- intr_priority:中断优先级;
- update_gen_action_on_tez:当计数器值为0时更新生成器操作;
- update_gen_action_on_tep:当计数器溢出时更新生成器操作;
- update_gen_action_on_sync:同步事件时更新生成器操作;
- update_dead_time_on_tez:当计数器值为0时更新死区时间;
- update_dead_time_on_tep:当计数器溢出时更新死区时间;
- update_dead_time_on_sync:同步事件时更新死区时间。
第三步,绑定定时器与操作器。调mcpwm_operator_connect_timer函数即可。
第四步,初始化生成器,初始化结构体如下。
typedef struct {
int gen_gpio_num;
struct {
uint32_t invert_pwm: 1;
uint32_t io_loop_back: 1;
uint32_t io_od_mode: 1;
uint32_t pull_up: 1;
uint32_t pull_down: 1;
} flags;
} mcpwm_generator_config_t;
- gen_gpio_num:输出管脚;
- invert_pwm:输出极性反转;
- io_loop_back:输出同时连接到输入通路;
- io_od_mode:输出开漏;
- pull_up:内部上拉;
- pull_down:内部下拉。
第五步,初始化比较器,初始化结构体如下。
typedef struct {
int intr_priority;
struct {
uint32_t update_cmp_on_tez: 1;
uint32_t update_cmp_on_tep: 1;
uint32_t update_cmp_on_sync: 1;
} flags;
} mcpwm_comparator_config_t;
- intr_priority:中断优先级;
- update_cmp_on_tez:当计数器值为0时更新比较器;
- update_cmp_on_tep:当计数器溢出时更新比较器;
- update_cmp_on_sync:同步事件时更新比较器。
第六步,初始化生成器规则。我这里设置两个;一个是当计数器值为0时PWM输出拉高;另一个是当比较器值到达设置值时PWM输出拉低。
第七步,使能定时器和启动定时器。
2. PWM波捕获
第一步,初始化捕获定时器,初始化结构体如下。
typedef struct {
int group_id;
mcpwm_capture_clock_source_t clk_src;
uint32_t resolution_hz;
} mcpwm_capture_timer_config_t;
- group_id:定时器组号(0-2);
- resolution_hz:定时器分辨率,即频率(默认是不能设置的,默认80MHz频率)。
第二步,初始化捕获通道,初始化结构体如下。
typedef struct {
int gpio_num;
int intr_priority;
uint32_t prescale;
struct extra_flags {
uint32_t pos_edge: 1;
uint32_t neg_edge: 1;
uint32_t pull_up: 1;
uint32_t pull_down: 1;
uint32_t invert_cap_signal: 1;
uint32_t io_loop_back: 1;
uint32_t keep_io_conf_at_exit: 1;
} flags;
} mcpwm_capture_channel_config_t;
- gpio_num:捕获管脚号;
- prescale:捕获分频;
- pos_edge:上升沿捕获;
- neg_edge:下降沿捕获;
- pull_up:内部上拉;
- pull_down:内部下拉;
- invert_cap_signal:捕获信号极性反转;
- io_loop_back:捕获信号同时连接到管脚输出;
- keep_io_conf_at_exit:当捕获通道删除时保持GPIO配置。
第三步,注册捕获回调,主要用于获取捕获数据并发送给上层。捕获回调可以获取到捕获的信号沿和当前捕获计数器的值。当捕获到下降沿时保存值,当捕获到上升沿时发送数据;数据有两个,一个是下降沿与上一个上升沿的差值,另一个是当前上升沿与上一个上升沿的差值,通过这两个值就可以计算PWM波的周期和占空比。
第四步,创建捕获任务(可选)。任务负责接收回调函数发来的数据并计算出周期和占空比。
第五步,使能捕获通道、使能捕获定时器和启动捕获定时器。
主循环主要就是定时改变PWM输出的占空比,下面就是程序运行的log。