当前位置: 首页 > article >正文

【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种可能,满足公式:

T=T_{PWM\_clk}\times 8\times (PWM\_CARRIERx\_PRESCALE+1)\times (PWM\_CARRIERx\_OSHTWTH+1)

        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。


http://www.kler.cn/a/376739.html

相关文章:

  • 流畅!HTMLCSS打造网格方块加载动画
  • AprilTag在相机标定中的应用简介
  • 小家电常用防水触摸IC
  • 2342423
  • 《Python游戏编程入门》注-第4章8
  • lua学习笔记---面向对象
  • 架构师之路-学渣到学霸历程-37
  • 修复因Ubuntu升级导致无法联网的问题
  • 线程池学习之执行流程、拒绝策略、线程池状态
  • 心觉:别再等完美工具了!用“小米加步枪”也能战斗,边干边升级才是最强策略!
  • 搜索引擎算法更新对网站优化的影响与应对策略
  • Tenda路由器 敏感信息泄露
  • FreeRTOS工程编译缺失头文件freertos_mpool.h或freertos_os2.h
  • Leetcode 热题100 之 二叉树3
  • 结合无监督表示学习与伪标签监督的自蒸馏方法,用于稀有疾病影像表型分类的分散感知失衡校正|文献速递-基于生成模型的数据增强与疾病监测应用
  • 从0开始学PHP面向对象内容之(类,对象,构造/析构函数)
  • npm入门教程9:npm配置
  • Python爬虫:揭开淘宝商品描述的神秘面纱
  • 逼着自己深度思考
  • 四款主流的3D创作和游戏开发软件的核心特点和关系
  • 前端 javascript 存储数据的方式有哪些
  • 归并排序速记
  • python 数据结构 2
  • 【云原生】云原生后端:数据管理
  • 设计卷积神经网络CNN为什么不是编程?
  • NFT Insider #153:The Sandbox 推出 Biggie 奇妙宇宙体验,ApeChain 推出顶级交易员游戏