Chapter 162: Motor Control Unit (MCPWM) Fundamentals
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the purpose and importance of the Motor Control PWM (MCPWM) peripheral.
- Describe the basic architecture of the MCPWM unit, including its timers, operators, comparators, and generators.
- Configure MCPWM timers to achieve desired frequencies and counting modes.
- Configure MCPWM operators and generators to produce various PWM signal patterns.
- Generate simple PWM signals for applications like brushed DC motor speed control.
- Implement complementary PWM signals with dead-time insertion for H-bridge control.
- Understand and utilize MCPWM fault handling mechanisms for safer operation.
- Use the MCPWM capture submodule to measure parameters of external input signals.
- Identify differences in MCPWM capabilities and configurations across various ESP32 variants.
- Apply best practices for MCPWM programming using ESP-IDF v5.x.
Introduction
Welcome to the world of precise motion and power control with the ESP32! While Chapter 144 introduced Pulse Width Modulation (PWM) and Chapter 145 detailed the LEDC (LED Control) peripheral for general-purpose PWM, this chapter focuses on a more specialized and powerful peripheral: the Motor Control PWM (MCPWM) unit.
The MCPWM peripheral is specifically designed for applications requiring sophisticated PWM waveforms, such as controlling various types of electric motors (brushed DC, brushless DC, servo), managing switched-mode power supplies (SMPS), and other power electronics applications. It offers features like multiple synchronized PWM outputs, complementary signal generation, programmable dead-time insertion, fault detection, and high-resolution timers, which are often essential for efficient and safe motor operation.
In this chapter, we will explore the fundamental concepts of the MCPWM peripheral, its architecture, and how to configure and use it effectively with the ESP-IDF v5.x framework. Understanding MCPWM will unlock a wide range of advanced control capabilities for your ESP32 projects.
Theory
What is PWM? (A Quick Recap for Motor Control)
Pulse Width Modulation (PWM) is a technique used to control the average power delivered to an electrical load by varying the duty cycle of a pulsed signal. The signal rapidly switches between an on (high) and off (low) state. The duty cycle is the ratio of the ‘on’ time to the total period of the signal, usually expressed as a percentage.
For motor control, PWM is primarily used to:
- Control Speed: For DC motors, varying the duty cycle effectively changes the average voltage applied to the motor, thus controlling its speed.
- Control Position: For servo motors, specific pulse widths (a form of PWM) determine the target angle of the servo shaft.
- Drive Brushless DC (BLDC) Motors: Complex sequences of PWM signals are used to commutate BLDC motors, energizing different phases in a specific order to create rotation.
Why MCPWM? Distinguishing from LEDC
While the LEDC peripheral (Chapter 145) is excellent for simpler PWM tasks like dimming LEDs or generating basic audio tones, the MCPWM peripheral is tailored for more demanding control applications, especially motor control. Key advantages of MCPWM over LEDC include:
Feature | MCPWM (Motor Control PWM) | LEDC (LED Control) |
---|---|---|
Primary Use Case | Advanced motor control, power electronics (SMPS), complex waveform generation. | LED dimming, simple audio tones, general-purpose PWM tasks. |
Complementary Outputs | ✔ Yes, with programmable dead-time insertion. Crucial for H-bridges. | ✖ No. Can’t generate synchronized, inverted signals with dead-time. |
Fault Handling | ✔ Yes. Can monitor external fault pins and automatically force outputs to a safe state. | ✖ No built-in hardware fault detection mechanism. |
Capture Inputs | ✔ Yes. Can measure parameters (pulse width, period) of external signals like Hall sensors or encoders. | ✖ No. Cannot measure external signal timings. |
Synchronization | ✔ High degree of synchronization between internal timers and operators. | ✖ Limited. Timers are independent. |
Waveform Shape | Symmetric (Up-Down count) and Asymmetric (Up or Down count) modes. | Asymmetric (Up count) mode primarily. Can simulate up-down with software. |
Complexity & Flexibility | High. Rich feature set with many configuration options for precise control. | Low. Simple, straightforward API for basic PWM generation. |
- Complementary Outputs with Dead-Time: MCPWM can generate pairs of PWM signals where one is the inverse of the other, with a programmable “dead-time” inserted between their transitions. This is crucial for driving H-bridges or half-bridges, preventing “shoot-through” (a short circuit condition where both high-side and low-side switches are momentarily on).
- Fault Detection: MCPWM can monitor external fault signals (e.g., over-current, over-temperature) and automatically force PWM outputs to a safe, predefined state, protecting the motor and drive circuitry.
- Capture Inputs: MCPWM can capture the timing of external events (e.g., pulses from Hall sensors in BLDC motors, or encoder signals) to measure speed, position, or pulse widths.
- Synchronization: Multiple timers and operators within an MCPWM unit can be synchronized, allowing for complex, coordinated PWM generation (e.g., for multi-phase motor control).
- Higher Resolution and Flexibility: MCPWM often offers more precise timing control and a richer set of configuration options for waveform generation.
MCPWM Architecture (ESP-IDF v5.x Perspective)
The MCPWM peripheral in ESP32 series chips is a complex module. With ESP-IDF v5.x, the API has been redesigned to be more object-oriented, using handles to manage different MCPWM resources. Let’s break down its main components:
MCPWM Units (Groups):
Most ESP32 variants have one or two independent MCPWM units (often referred to as MCPWM0
, MCPWM1
). Each unit acts as a self-contained PWM generation system. In ESP-IDF, you specify the group_id
when creating resources like timers or operators to select which MCPWM unit to use. For chips with a single MCPWM unit, group_id
will typically be 0.
Timers (mcpwm_timer_handle_t
):
Each MCPWM unit contains several timers (e.g., 2 or 3 timers per unit, depending on the ESP32 variant). Timers are the heart of PWM generation, providing the time base. They count from zero up to a period value, then reset or change direction.
Configuration:
clk_src
: Clock source for the timer (e.g., PLL clock, XTAL clock). Default is usually PLL.resolution_hz
: The desired resolution of the timer. The driver calculates the necessary prescaler based on this and the clock source frequency. This effectively sets the “tick rate” of the timer.count_mode
:MCPWM_TIMER_COUNT_MODE_UP
: Timer counts from 0 up to the period value, then resets to 0.MCPWM_TIMER_COUNT_MODE_DOWN
: Timer counts from period value down to 0, then resets to the period value.MCPWM_TIMER_COUNT_MODE_UP_DOWN
: Timer counts from 0 up to the period value, then counts down to 0. This mode generates symmetric PWM, which is often preferred for motor control as it centers the PWM pulse within the period, reducing harmonics.
period_ticks
: The value at which the timer overflows or changes direction. The PWM frequency is determined byresolution_hz / period_ticks
(for UP or DOWN mode) orresolution_hz / (2 * period_ticks)
(for UP-DOWN mode).
Parameter (in mcpwm_timer_config_t ) |
Description | Example Value / Notes |
---|---|---|
group_id |
Specifies the MCPWM unit (0 or 1 on dual-unit chips). | 0 for MCPWM0. Must match operators. |
clk_src |
The clock source for the timer. Affects max resolution. | MCPWM_TIMER_CLK_SRC_DEFAULT (usually PLL clock). |
resolution_hz |
The desired “tick” rate of the timer in Hz. The driver calculates the prescaler to achieve this. | 1000000 for 1MHz resolution (1µs per tick). |
count_mode |
Sets the counting behavior of the timer. | MCPWM_TIMER_COUNT_MODE_UP (Asymmetric)MCPWM_TIMER_COUNT_MODE_UP_DOWN (Symmetric) |
period_ticks |
The value at which the timer overflows or changes direction. Determines the PWM frequency. | For 1MHz res & 10kHz freq (Up mode): 1000000 / 10000 = 100 |
Creation: mcpwm_new_timer()
Operators (mcpwm_operator_handle_t
):
Each timer can be associated with one or more operators. Typically, an MCPWM timer has two operators (Operator A and Operator B). An operator links a timer to comparators and generators. It doesn’t generate PWM itself but acts as a control block for them.
Configuration:
group_id
: The MCPWM unit.intr_priority
: CPU interrupt priority if operator-level interrupts are used (less common than comparator/generator or fault/capture interrupts).
Creation: mcpwm_new_operator()
(associates with a specific timer handle).
Comparators (mcpwm_cmpr_handle_t
):
Each operator can have one or more comparators (usually one per operator, but the concept allows for more flexibility in some hardware). A comparator continuously compares its internal “compare value” (which determines the duty cycle) against the count value of the associated timer. When they match, a “compare event” is triggered.
Configuration:
update_cmp_on_tez
: Update compare value on Timer Equal Zero event.update_cmp_on_tep
: Update compare value on Timer Equal Period event.
Creation: mcpwm_new_comparator()
(associates with an operator handle).
Setting Duty Cycle: The duty cycle is set by calling mcpwm_comparator_set_compare_value()
. The compare_value
is in terms of timer ticks. For example, if period_ticks
is 1000, a compare_value
of 500 results in a 50% duty cycle (for simple cases).
Generators (mcpwm_gen_handle_t
):
Each operator typically has two generators (e.g., Generator A and Generator B, corresponding to PWMxA and PWMxB output signals). Generators determine the state (high or low) of the PWM output pins based on timer events (e.g., timer reaches zero, timer reaches period) and comparator events (timer value matches compare value).
Configuration:
gen_gpio_num
: The GPIO pin number to output the PWM signal.invert_pwm
: Whether to invert the PWM signal on this generator.
Creation: mcpwm_new_generator()
(associates with an operator handle).
Actions: The core of PWM waveform shaping. You define what action the generator should take on specific events:
mcpwm_generator_set_actions_on_timer_event()
: Defines actions (e.g.,MCPWM_GEN_ACTION_HIGH
,MCPWM_GEN_ACTION_LOW
,MCPWM_GEN_ACTION_TOGGLE
,MCPWM_GEN_ACTION_KEEP
) when the timer reaches zero (MCPWM_TIMER_EVENT_EMPTY
) or period (MCPWM_TIMER_EVENT_FULL
).mcpwm_generator_set_actions_on_compare_event()
: Defines actions when the timer matches the comparator’s value, for both increasing count (MCPWM_COMPARE_EVENT_UP
) and decreasing count (MCPWM_COMPARE_EVENT_DOWN
).
Example for typical up-counting PWM:
- On timer zero: Set PWM high.
- On compare match (up-counting): Set PWM low.
Dead-Time Module (mcpwm_dead_time_config_t
):
In H-bridge configurations (commonly used for controlling motor direction and braking), there are high-side and low-side switches (e.g., MOSFETs) for each phase. If the high-side switch turns on while the low-side switch is still turning off (or vice-versa), a short circuit (“shoot-through”) can occur, potentially damaging the switches or power supply. Dead-time insertion ensures that both switches are off for a brief period during transitions.
Configuration: Applied at the operator level when configuring generators.
posedge_delay_ticks
: Delay applied before a rising edge on the primary output (e.g., PWMxA) is propagated, and before the corresponding falling edge on the complementary output (e.g., PWMxB) if inverted.negedge_delay_ticks
: Delay applied before a falling edge on the primary output is propagated.flags.invert_output
: Specifies which output (A or B) is inverted to create the complementary signal. E.g.,MCPWM_GEN_PWMxB_DIRECT_PWMxA_INVERTED
means PWMxB is the inverse of PWMxA with dead-time.
How it works: The dead-time module delays the turning ON of one switch relative to the turning OFF of its complementary switch.
Fault Handler Module (mcpwm_fault_handle_t
, mcpwm_gpio_fault_config_t
):
Provides a mechanism to react to external fault conditions (e.g., over-current, over-temperature signals from driver ICs).
Configuration:
group_id
: The MCPWM unit.gpio_num
: The GPIO pin used as the fault input.active_level
:MCPWM_FAULT_ACTIVE_HIGH
orMCPWM_FAULT_ACTIVE_LOW
.flags.io_loop_back
: For testing, loop back GPIO output to fault input.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% graph TD A[Start: PWM Running Normally] --> B{Fault Pin Asserted?}; B -- "No" --> A; B -- "Yes" --> C[Fault Signal Detected by MCPWM Hardware]; C --> D{Trip Mode?}; D -- "Cycle-by-Cycle" --> E[Force PWM Outputs to Safe State<br>e.g., LOW]; D -- "Latched (One-Shot)" --> F[Force PWM Outputs to Safe State<br>e.g., LOW]; E --> G{Fault Pin De-asserted?}; G -- "No" --> E; G -- "Yes" --> H[Automatically Resume PWM<br>on next timer cycle]; H --> A; F --> I{Software Action?}; I -- "Yes" --> J["Clear Fault Condition<br>via 'mcpwm_fault_recover()'"]; I -- "No" --> F; J --> A; C -- "Optional" --> K(Trigger ISR / Callback); K --> L[Notify Application:<br>Log Error, Disable System, etc.]; %% Styling classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; class A,H,J primary; class B,D,G,I decision; class C,E,F,K,L process; class C check;
Creation: mcpwm_new_gpio_fault()
.
Actions on Fault: Once a fault is configured and detected, you define how the PWM generators should behave. This is done using mcpwm_generator_set_actions_on_fault_event()
.
direction
:MCPWM_FAULT_EVENT_ON_TIMER_CYCLE
(Cycle-by-Cycle trip: PWM resumes if fault clears) orMCPWM_FAULT_EVENT_ON_TIMER_ONESHOT
(Latched trip: PWM remains off until explicitly reset).- Action for generator A/B: e.g.,
MCPWM_GEN_ACTION_LOW
to force outputs low.
Event Callbacks: mcpwm_fault_register_event_callbacks()
can be used to trigger software actions (like logging or system shutdown) when a fault occurs.
Capture Module (mcpwm_cap_channel_handle_t
, mcpwm_capture_channel_config_t
):
Purpose of capture module is to measure the timing characteristics of external input signals, such as pulse width, period, or duty cycle. This is useful for reading feedback from Hall sensors, encoders, or other sensors. Each MCPWM unit has several capture channels.
Configuration:
group_id
: The MCPWM unit.gpio_num
: The GPIO pin used as the capture input.prescale
: A prescaler for the capture timer to adjust its resolution.flags.pos_edge
,flags.neg_edge
: Enable capture on positive/negative edges.flags.pull_up
,flags.pull_down
: Configure internal pull-up/down resistors for the capture GPIO.
Creation: mcpwm_new_capture_channel()
.
Associated Timer: Capture channels use a dedicated capture timer (distinct from the PWM generation timers) or can sometimes be configured to use one ofthe main MCPWM timers. ESP-IDF v5.x abstracts this; you typically associate a capture channel with an mcpwm_cap_timer_handle_t
(created via mcpwm_new_capture_timer()
).
Event Callbacks: mcpwm_capture_register_event_callbacks()
is used to register a function that will be called when a capture event occurs (e.g., on_cap
callback in mcpwm_capture_event_callbacks_t
). Inside the callback, you can read the captured timer value using mcpwm_capture_channel_get_capture_value()
.
Key Concepts for MCPWM Usage
- Frequency and Duty Cycle Calculation:
- If
timer_resolution_hz
is the timer’s clock frequency (after prescaler) andperiod_ticks
is the value set for the timer’s period:- For
UP
orDOWN
count mode: PWM Frequency =timer_resolution_hz / period_ticks
. - For
UP_DOWN
count mode: PWM Frequency =timer_resolution_hz / (2 * period_ticks)
.
- For
- Duty Cycle (%) = (
compare_value / period_ticks
) * 100.- Note: For
UP_DOWN
mode, the interpretation ofcompare_value
relative to actions needs careful consideration to achieve the desired pulse shape.
- Note: For
- If
- Symmetric vs. Asymmetric PWM:
- Asymmetric PWM: Generated using
UP
orDOWN
count modes. The PWM pulse is typically aligned to one end of the PWM period. - Symmetric PWM: Generated using
UP_DOWN
count mode. The PWM pulse is centered within the period. This is often preferred in motor control as it can reduce electromagnetic interference (EMI) and acoustic noise.
- Asymmetric PWM: Generated using
- Resource Management:
- With ESP-IDF v5.x, it’s crucial to manage the handles (
mcpwm_timer_handle_t
,mcpwm_operator_handle_t
, etc.) correctly. - Always enable resources after configuration (e.g.,
mcpwm_timer_enable()
). - Start timers using
mcpwm_timer_start_stop(timer_handle, MCPWM_TIMER_START_NO_STOP)
. - Deallocate resources when no longer needed using
mcpwm_del_timer()
,mcpwm_del_operator()
, etc., to free memory and hardware.
- With ESP-IDF v5.x, it’s crucial to manage the handles (
General Workflow for MCPWM Configuration (ESP-IDF v5.x)
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%% graph TD Start[Start] --> T1(1- Configure Timer<br><b>mcpwm_timer_config_t</b>); T1 --> T2("Call <b>mcpwm_new_timer()</b><br><i>Get Timer Handle</i>"); T2 --> O1(2- Configure Operator<br><b>mcpwm_operator_config_t</b>); O1 --> O2("Call <b>mcpwm_new_operator()</b><br><i>Get Operator Handle</i>"); O2 --> O3("Connect Timer & Operator<br><b>mcpwm_operator_connect_timer()</b>"); O3 --> C1(3- Configure Comparator<br><b>mcpwm_comparator_config_t</b>); C1 --> C2("Call <b>mcpwm_new_comparator()</b><br><i>Get Comparator Handle</i>"); C2 --> C3("Set Duty Cycle<br><b>mcpwm_comparator_set_compare_value()</b>"); C3 --> G1("4- Configure Generator(s)<br><b>mcpwm_generator_config_t</b>"); G1 --> G2("Call <b>mcpwm_new_generator()</b><br><i>Get Generator Handle(s)</i>"); G2 --> G3(Set Generator Actions<br>on Timer & Compare Events); G3 --> Opt1{Need Dead-Time?}; Opt1 -- Yes --> DT("5a- Configure & Apply Dead-Time<br><b>mcpwm_operator_apply_deadtime()</b>") --> E1; Opt1 -- No --> E1; E1 --> Opt2{Need Fault Handling?}; Opt2 -- Yes --> FH(5b- Configure Fault Input<br>& Generator Actions on Fault) --> E2; Opt2 -- No --> E2; E2 --> Opt3{Need Capture Input?}; Opt3 -- Yes --> CAP(5c- Configure Capture Timer<br>& Capture Channel with Callbacks) --> E3; Opt3 -- No --> E3; E3 --> F1("6- Enable Timer<br><b>mcpwm_timer_enable()</b>"); F1 --> F2("7- Start Timer<br><b>mcpwm_timer_start_stop()</b>"); F2 --> End([PWM is Active]); %% Styling classDef startEnd fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; class Start,End startEnd; class T1,T2,O1,O2,O3,C1,C2,C3,G1,G2,G3,DT,FH,CAP,E1,E2,E3,F1,F2 process; class Opt1,Opt2,Opt3 decision;
- Timer Configuration & Creation:
- Define
mcpwm_timer_config_t
. - Call
mcpwm_new_timer()
to getmcpwm_timer_handle_t
.
- Define
- Operator Configuration & Creation:
- Define
mcpwm_operator_config_t
(linking to the timer group). - Call
mcpwm_new_operator()
with the timer handle to getmcpwm_operator_handle_t
.
- Define
- Comparator Configuration & Creation:
- Define
mcpwm_comparator_config_t
. - Call
mcpwm_new_comparator()
with the operator handle to getmcpwm_cmpr_handle_t
. - Set initial compare value using
mcpwm_comparator_set_compare_value()
.
- Define
- Generator Configuration & Creation:
- Define
mcpwm_generator_config_t
(specifying GPIO). - Call
mcpwm_new_generator()
with the operator handle to getmcpwm_gen_handle_t
. - Define actions using
mcpwm_generator_set_actions_on_timer_event()
andmcpwm_generator_set_actions_on_compare_event()
. - If using complementary outputs and dead-time:
- Create two generators (A and B) for the same operator.
- Configure dead-time settings using
mcpwm_operator_apply_deadtime()
.
- Define
- Fault Configuration (Optional):
- Define
mcpwm_gpio_fault_config_t
. - Call
mcpwm_new_gpio_fault()
to getmcpwm_fault_handle_t
. - Set generator actions on fault using
mcpwm_generator_set_actions_on_fault_event()
. - Register fault event callbacks if needed.
- Define
- Capture Configuration (Optional):
- Define
mcpwm_capture_timer_config_t
and callmcpwm_new_capture_timer()
. - Define
mcpwm_capture_channel_config_t
. - Call
mcpwm_new_capture_channel()
with the capture timer handle to getmcpwm_cap_channel_handle_t
. - Register capture event callbacks using
mcpwm_capture_register_event_callbacks()
. - Enable the capture channel using
mcpwm_capture_channel_enable()
.
- Define
- Enable & Start:
- Enable the MCPWM timer:
mcpwm_timer_enable()
. - Start the MCPWM timer:
mcpwm_timer_start_stop(timer_handle, MCPWM_TIMER_START_NO_STOP)
.
- Enable the MCPWM timer:
Practical Examples
Before diving into the examples, ensure you have the ESP-IDF environment set up with VS Code as detailed in Chapter 2. All examples will use the driver/mcpwm_prelude.h
header.
Common Setup for Examples:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/mcpwm_prelude.h"
static const char *TAG = "mcpwm_example";
Example 1: Basic PWM Generation (e.g., for Brushed DC Motor Speed)
This example demonstrates generating a single PWM signal to control the brightness of an LED or the speed of a small brushed DC motor via a driver IC (like an L298N enable pin or a single MOSFET).
Goal: Generate a 10 kHz PWM signal on GPIO 12, with duty cycle controllable from 0% to 100%.
// Define the GPIO for PWM output
#define PWM_GPIO_NUM 12
// Define PWM frequency and timer resolution
#define PWM_TIMER_RESOLUTION_HZ 1000000 // 1MHz, 1us period
#define PWM_FREQUENCY_HZ 10000 // 10kHz
#define PWM_PERIOD_TICKS (PWM_TIMER_RESOLUTION_HZ / PWM_FREQUENCY_HZ) // 100 ticks for 10kHz
// MCPWM handles
mcpwm_timer_handle_t timer = NULL;
mcpwm_oper_handle_t oper = NULL;
mcpwm_cmpr_handle_t comparator = NULL;
mcpwm_gen_handle_t generator = NULL;
void init_basic_pwm(void) {
ESP_LOGI(TAG, "Create timer");
mcpwm_timer_config_t timer_config = {
.group_id = 0, // Select MCPWM group 0
.clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT,
.resolution_hz = PWM_TIMER_RESOLUTION_HZ,
.count_mode = MCPWM_TIMER_COUNT_MODE_UP,
.period_ticks = PWM_PERIOD_TICKS,
};
ESP_ERROR_CHECK(mcpwm_new_timer(&timer_config, &timer));
ESP_LOGI(TAG, "Create operator");
mcpwm_operator_config_t oper_config = {
.group_id = 0, // Must be in the same group to connect to the timer
};
ESP_ERROR_CHECK(mcpwm_new_operator(&oper_config, &oper));
ESP_LOGI(TAG, "Connect timer and operator");
ESP_ERROR_CHECK(mcpwm_operator_connect_timer(oper, timer));
ESP_LOGI(TAG, "Create comparator");
mcpwm_comparator_config_t comparator_config = {
.flags.update_cmp_on_tez = true, // Update compare value on timer zero event
};
ESP_ERROR_CHECK(mcpwm_new_comparator(oper, &comparator_config, &comparator));
ESP_LOGI(TAG, "Create generator");
mcpwm_generator_config_t generator_config = {
.gen_gpio_num = PWM_GPIO_NUM,
};
ESP_ERROR_CHECK(mcpwm_new_generator(oper, &generator_config, &generator));
// Set initial duty cycle to 0%
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, 0));
ESP_LOGI(TAG, "Set generator actions");
// Go high on timer zero (start of period)
ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(generator,
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH, 0),
MCPWM_GEN_TIMER_EVENT_ACTION_END()));
// Go low on compare match
ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(generator,
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, comparator, MCPWM_GEN_ACTION_LOW),
MCPWM_GEN_COMPARE_EVENT_ACTION_END()));
ESP_LOGI(TAG, "Enable and start timer");
ESP_ERROR_CHECK(mcpwm_timer_enable(timer));
ESP_ERROR_CHECK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP));
ESP_LOGI(TAG, "Basic PWM Initialized on GPIO %d", PWM_GPIO_NUM);
}
void set_pwm_duty_cycle(uint32_t duty_percentage) {
if (duty_percentage > 100) {
duty_percentage = 100;
}
uint32_t compare_value = (PWM_PERIOD_TICKS * duty_percentage) / 100;
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, compare_value));
ESP_LOGI(TAG, "Set duty cycle to %"PRIu32"%%, compare value: %"PRIu32, duty_percentage, compare_value);
}
void app_main(void) {
init_basic_pwm();
uint32_t duty = 0;
bool increasing = true;
while (1) {
set_pwm_duty_cycle(duty);
vTaskDelay(pdMS_TO_TICKS(500)); // Update duty cycle every 500ms
if (increasing) {
duty += 10;
if (duty >= 100) {
duty = 100;
increasing = false;
}
} else {
duty -= 10;
if (duty <= 0) { // duty is uint32_t, so check <=0
duty = 0;
increasing = true;
}
}
}
}
Build Instructions:
- Save the code as
main.c
in themain
directory of your ESP-IDF project. - Ensure your
CMakeLists.txt
in themain
directory has:idf_component_register(SRCS "main.c" INCLUDE_DIRS ".")
- From the VS Code terminal, run
idf.py build
.
Run/Flash/Observe:
- Flash the built binary:
idf.py -p (YourPort) flash monitor
. - Connect an LED (with a current-limiting resistor, e.g., 220-330 Ohm) between GPIO 12 and GND.
- Observe the LED brightness changing, corresponding to the duty cycle printed in the monitor. You can also connect an oscilloscope to GPIO 12 to see the PWM waveform.
Example 2: Complementary PWM with Dead-Time for Half-Bridge
This example generates two complementary PWM signals (PWM0A and PWM0B) with dead-time insertion, suitable for driving one leg of an H-bridge.
Goal: Generate 20kHz symmetric PWM on GPIOs 13 (PWM0A) and 14 (PWM0B), with 2µs rising edge dead-time and 2.5µs falling edge dead-time.
// Define GPIOs for complementary PWM output
#define PWM0A_GPIO_NUM 13
#define PWM0B_GPIO_NUM 14
// PWM Configuration
#define DT_TIMER_RESOLUTION_HZ 1000000 // 1MHz, 1us tick
#define DT_PWM_FREQUENCY_HZ 20000 // 20kHz
// For UP_DOWN mode, period_ticks = resolution / (2 * frequency)
#define DT_PWM_PERIOD_TICKS (DT_TIMER_RESOLUTION_HZ / (2 * DT_PWM_FREQUENCY_HZ)) // 25 ticks
// Dead time in timer ticks (1us per tick)
#define DEAD_TIME_RISE_EDGE_TICKS 2 // 2us
#define DEAD_TIME_FALL_EDGE_TICKS 3 // 3us, example, typically rise and fall are similar or fall is slightly longer
// MCPWM handles
mcpwm_timer_handle_t dt_timer = NULL;
mcpwm_oper_handle_t dt_oper = NULL;
mcpwm_cmpr_handle_t dt_comparator = NULL;
mcpwm_gen_handle_t dt_generator_a = NULL;
mcpwm_gen_handle_t dt_generator_b = NULL;
void init_complementary_pwm_with_dead_time(void) {
ESP_LOGI(TAG, "Create timer for dead-time example");
mcpwm_timer_config_t timer_config = {
.group_id = 0,
.clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT,
.resolution_hz = DT_TIMER_RESOLUTION_HZ,
.period_ticks = DT_PWM_PERIOD_TICKS,
.count_mode = MCPWM_TIMER_COUNT_MODE_UP_DOWN, // Symmetric PWM
};
ESP_ERROR_CHECK(mcpwm_new_timer(&timer_config, &dt_timer));
ESP_LOGI(TAG, "Create operator for dead-time example");
mcpwm_operator_config_t oper_config = {
.group_id = 0,
};
ESP_ERROR_CHECK(mcpwm_new_operator(&oper_config, &dt_oper));
ESP_ERROR_CHECK(mcpwm_operator_connect_timer(dt_oper, dt_timer));
ESP_LOGI(TAG, "Create comparator for dead-time example");
mcpwm_comparator_config_t comp_config = {
.flags.update_cmp_on_tez = true,
};
ESP_ERROR_CHECK(mcpwm_new_comparator(dt_oper, &comp_config, &dt_comparator));
// Set initial duty cycle (e.g., 50%)
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(dt_comparator, DT_PWM_PERIOD_TICKS / 2));
ESP_LOGI(TAG, "Create generator A");
mcpwm_generator_config_t gen_a_config = {
.gen_gpio_num = PWM0A_GPIO_NUM,
};
ESP_ERROR_CHECK(mcpwm_new_generator(dt_oper, &gen_a_config, &dt_generator_a));
ESP_LOGI(TAG, "Create generator B");
mcpwm_generator_config_t gen_b_config = {
.gen_gpio_num = PWM0B_GPIO_NUM,
};
ESP_ERROR_CHECK(mcpwm_new_generator(dt_oper, &gen_b_config, &dt_generator_b));
// Configure actions for symmetric PWM
// Generator A: Set high on TEP (Timer Equal Period, i.e., when counting down matches period/bottom)
// Set low on compare match while counting up
// Generator B: (Will be inverted by dead-time module or set oppositely)
// For symmetric PWM (UP_DOWN count):
// Action on timer zero: gen_a high
// Action on compare match up: gen_a low
// Action on timer period (bottom): gen_a low (or can be set high if compare value is for the "on" duration from center)
// Action on compare match down: gen_a high
// A common way for active high symmetric PWM:
// - Go high when timer counting up matches compare value (center of pulse starts)
// - Go low when timer counting down matches compare value (center of pulse ends)
// Or, simpler for many drivers:
// - High on TEP (bottom of UDC)
// - Low on CMP_UP
// - High on CMP_DOWN
// - Low on TEZ (top of UDC)
// Let's use a simpler standard setup for active high PWMxA:
// PWMxA will be high when timer < compare_value during up-count, and timer > compare_value during down-count (if centered)
// Or more simply: High on TEZ, Low on CMP_UP for PWMxA. Deadtime module inverts for PWMxB.
ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(dt_generator_a,
MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH, 0), // Rising edge at TEZ
MCPWM_GEN_TIMER_EVENT_ACTION_END()));
ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(dt_generator_a,
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, dt_comparator, MCPWM_GEN_ACTION_LOW), // Falling edge on CMP_UP
MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, dt_comparator, MCPWM_GEN_ACTION_HIGH), // Rising edge on CMP_DOWN
MCPWM_GEN_COMPARE_EVENT_ACTION_END()));
ESP_LOGI(TAG, "Enable dead time");
mcpwm_dead_time_config_t dt_config = {
.posedge_delay_ticks = DEAD_TIME_RISE_EDGE_TICKS,
.negedge_delay_ticks = DEAD_TIME_FALL_EDGE_TICKS, // For active low complementary, this is on PWMxB
.flags.invert_output_a = false, // PWMxA is active high
.flags.invert_output_b = true, // PWMxB is active low complementary to PWMxA
};
ESP_ERROR_CHECK(mcpwm_operator_apply_deadtime(dt_oper, &dt_config));
ESP_LOGI(TAG, "Enable and start timer for dead-time example");
ESP_ERROR_CHECK(mcpwm_timer_enable(dt_timer));
ESP_ERROR_CHECK(mcpwm_timer_start_stop(dt_timer, MCPWM_TIMER_START_NO_STOP));
ESP_LOGI(TAG, "Complementary PWM with Dead-Time Initialized.");
}
// In app_main:
// void app_main(void) {
// init_complementary_pwm_with_dead_time();
// // Example: set duty cycle for dt_comparator
// // mcpwm_comparator_set_compare_value(dt_comparator, new_compare_val);
// while(1) {
// vTaskDelay(pdMS_TO_TICKS(1000));
// }
// }
Build and Run: Similar to Example 1.
Observe:
- Use a dual-channel oscilloscope. Connect Channel 1 to GPIO 13 (PWM0A) and Channel 2 to GPIO 14 (PWM0B).
- You should see two complementary PWM signals.
- Zoom in on the rising edge of PWM0A and the corresponding falling edge of PWM0B. You should observe the dead-time where both signals are low. Similarly for the falling edge of PWM0A and rising edge of PWM0B.
Example 3: Using Fault Detection
This example demonstrates configuring a GPIO as a fault input. When the fault signal is asserted, PWM outputs are forced to a low state.
Goal: Use GPIO 4 as a fault input (active high). If GPIO 4 goes high, PWM output on GPIO 12 (from Example 1) should be forced low.
// (Requires init_basic_pwm() from Example 1 to be set up first)
#define FAULT_GPIO_NUM 4
mcpwm_fault_handle_t fault_handler = NULL;
// Callback for fault enter (optional, for logging or other actions)
static bool IRAM_ATTR on_fault_enter_callback(mcpwm_fault_handle_t fault, const mcpwm_fault_event_data_t *edata, void *user_data)
{
// Using ets_printf for ISR-safe logging if needed, or set a flag for main task
ets_printf("Fault detected on GPIO %d!\n", FAULT_GPIO_NUM);
// Return true if we want to yield from ISR
return false;
}
void init_fault_detection(void) {
ESP_LOGI(TAG, "Configure GPIO fault input");
mcpwm_gpio_fault_config_t gpio_fault_config = {
.group_id = 0, // Same group as the PWM timer/operator
.gpio_num = FAULT_GPIO_NUM,
.active_level = MCPWM_FAULT_ACTIVE_HIGH, // Fault on high level
.flags.pull_up = true, // Enable pull-up if fault signal might float
// Or pull_down if active_high and default is low
};
ESP_ERROR_CHECK(mcpwm_new_gpio_fault(&gpio_fault_config, &fault_handler));
ESP_LOGI(TAG, "Set generator action on fault (Cycle-by-Cycle trip)");
// Assuming 'generator' is the handle from Example 1
// Force PWM output low on fault, for generator associated with PWM_GPIO_NUM
ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_fault_event(generator,
MCPWM_GEN_FAULT_EVENT_ACTION(MCPWM_FAULT_EVENT_ON_TIMER_CYCLE, MCPWM_TIMER_DIRECTION_UP, fault_handler, MCPWM_GEN_ACTION_LOW),
MCPWM_GEN_FAULT_EVENT_ACTION(MCPWM_FAULT_EVENT_ON_TIMER_CYCLE, MCPWM_TIMER_DIRECTION_DOWN, fault_handler, MCPWM_GEN_ACTION_LOW),
MCPWM_GEN_FAULT_EVENT_ACTION_END()));
// Optional: Register callback for software notification
// mcpwm_fault_event_callbacks_t cbs = {
// .on_fault_enter = on_fault_enter_callback,
// };
// ESP_ERROR_CHECK(mcpwm_fault_register_event_callbacks(fault_handler, &cbs, NULL));
ESP_LOGI(TAG, "Fault detection initialized for GPIO %d.", FAULT_GPIO_NUM);
}
// In app_main, after init_basic_pwm():
// void app_main(void) {
// init_basic_pwm();
// init_fault_detection(); // Initialize fault detection
//
// uint32_t duty = 50; // Set a visible duty cycle
// set_pwm_duty_cycle(duty);
//
// ESP_LOGI(TAG, "PWM running. Trigger fault on GPIO %d (set high) to stop PWM.", FAULT_GPIO_NUM);
// // Loop to keep running, or add logic to clear fault if it's latched
// while(1) {
// vTaskDelay(pdMS_TO_TICKS(1000));
// }
// }
Build and Run: Similar to Example 1.
Observe:
- The PWM signal should be active on GPIO 12 (e.g., LED is on/dimmed).
- Connect GPIO 4 to VCC (3.3V) to simulate an active-high fault.
- The PWM output on GPIO 12 should immediately go low (LED off).
- If using cycle-by-cycle trip (
MCPWM_FAULT_EVENT_ON_TIMER_CYCLE
), disconnecting GPIO 4 from VCC (allowing it to go low, assuming pull-down or external low) should resume the PWM. For latched trip (MCPWM_FAULT_EVENT_ON_TIMER_ONESHOT
), a software reset of the fault condition would be needed (mcpwm_fault_recover()
).
Example 4: Using Capture Input
This example demonstrates configuring an MCPWM capture channel to measure the pulse width of an external signal.
Goal: Measure the positive pulse width of a signal on GPIO 5.
#define CAP_GPIO_NUM 5
#define CAP_TIMER_RESOLUTION_HZ 1000000 // 1MHz, 1us resolution for capture
mcpwm_cap_timer_handle_t cap_timer = NULL;
mcpwm_cap_channel_handle_t cap_channel = NULL;
// User context for callback
typedef struct {
uint32_t last_captured_value;
uint32_t current_pulse_width_us;
} capture_context_t;
capture_context_t cap_ctx;
// Capture event callback
static bool IRAM_ATTR on_capture_event_callback(mcpwm_cap_channel_handle_t cap_chan, const mcpwm_capture_event_data_t *edata, void *user_data)
{
capture_context_t *ctx = (capture_context_t *)user_data;
BaseType_t high_task_woken = pdFALSE;
if (edata->cap_edge == MCPWM_CAP_EDGE_POS) { // Captured positive edge
ctx->last_captured_value = edata->cap_value;
// For pulse width, we need two edges. This example is simplified.
// A more complete pulse width measurement would capture on POS then NEG edge.
// Or, if measuring period of positive pulses: capture POS edge, then next POS edge.
} else if (edata->cap_edge == MCPWM_CAP_EDGE_NEG) { // Captured negative edge
if (ctx->last_captured_value != 0) { // Ensure we had a previous positive edge
ctx->current_pulse_width_us = edata->cap_value - ctx->last_captured_value;
// Assuming capture timer doesn't overflow between edges.
// For longer pulses or lower resolution, overflow handling is needed.
ctx->last_captured_value = 0; // Reset for next positive edge
// Notify main task or print (use ISR-safe methods if printing directly)
// For simplicity, we'll just update the context.
}
}
return high_task_woken == pdTRUE;
}
void init_capture_input(void) {
ESP_LOGI(TAG, "Create capture timer");
mcpwm_capture_timer_config_t cap_timer_config = {
.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT,
.group_id = 0, // Assuming capture belongs to group 0
.resolution_hz = CAP_TIMER_RESOLUTION_HZ,
};
ESP_ERROR_CHECK(mcpwm_new_capture_timer(&cap_timer_config, &cap_timer));
ESP_LOGI(TAG, "Create capture channel on GPIO %d", CAP_GPIO_NUM);
mcpwm_capture_channel_config_t cap_chan_config = {
.gpio_num = CAP_GPIO_NUM,
.prescale = 1, // No prescaler for channel, timer resolution is key
.flags.pos_edge = true,
.flags.neg_edge = true, // Capture on both edges for pulse width
.flags.pull_up = true, // Enable pull-up if input might float
};
ESP_ERROR_CHECK(mcpwm_new_capture_channel(cap_timer, &cap_chan_config, &cap_channel)); // Connect to capture timer
ESP_LOGI(TAG, "Register capture callback");
mcpwm_capture_event_callbacks_t cbs = {
.on_cap = on_capture_event_callback,
};
ESP_ERROR_CHECK(mcpwm_capture_register_event_callbacks(cap_channel, &cbs, &cap_ctx));
ESP_LOGI(TAG, "Enable capture channel and timer");
ESP_ERROR_CHECK(mcpwm_capture_channel_enable(cap_channel));
ESP_ERROR_CHECK(mcpwm_capture_timer_enable(cap_timer));
ESP_ERROR_CHECK(mcpwm_capture_timer_start(cap_timer));
ESP_LOGI(TAG, "Capture input initialized on GPIO %d.", CAP_GPIO_NUM);
}
// In app_main:
// void app_main(void) {
// init_capture_input();
// // Generate a test signal on another GPIO using LEDC or another MCPWM
// // or use an external function generator connected to CAP_GPIO_NUM.
// while(1) {
// vTaskDelay(pdMS_TO_TICKS(1000));
// ESP_LOGI(TAG, "Last measured positive pulse width: %"PRIu32" us", cap_ctx.current_pulse_width_us);
// }
// }
Build and Run: Similar to Example 1.
Observe:
- Feed a PWM signal (e.g., from a function generator, another ESP32’s LEDC/MCPWM output, or even a simple 555 timer circuit) to GPIO 5.
- The serial monitor should periodically print the measured pulse width in microseconds.
- Verify the readings against the known pulse width of your input signal.
Tip: For accurate pulse width or frequency measurement, especially for noisy signals or signals with varying duty cycles, you might need more sophisticated logic in the callback, potentially averaging multiple readings or implementing more robust edge detection and timer overflow handling.
Variant Notes
The MCPWM peripheral is available on many ESP32 variants, but there can be differences in the number of units, timers per unit, operators per timer, and capture channels. The ESP-IDF mcpwm_prelude.h
API aims to provide a unified interface, abstracting many hardware differences.
ESP32 Variant | MCPWM Units (Groups) | Timers per Unit | Total Operators | Capture Channels per Unit |
---|---|---|---|---|
ESP32 (Original) | 2 (MCPWM0, MCPWM1) | 3 | 6 per unit (12 total) | 3 |
ESP32-S2 | 1 (MCPWM0) | 2 | 4 total | 3 |
ESP32-S3 | 2 (MCPWM0, MCPWM1) | 2 | 4 per unit (8 total) | 3 |
ESP32-C3 / C6 | 1 (MCPWM0) | 2 | 4 total | 3 |
ESP32-H2 | 1 (MCPWM0) | 2 | 4 total | 3 |
Key Considerations for Variants:
group_id
: When configuring MCPWM resources (mcpwm_timer_config_t
,mcpwm_operator_config_t
, etc.), thegroup_id
field specifies which MCPWM unit to use. For single-unit variants, this will always be0
. For dual-unit variants, it can be0
or1
.- Resource Limits: The number of available timers, operators, and capture channels will limit how many independent complex PWM schemes or motor controls you can implement simultaneously.
- GPIO Availability: Always consult the datasheet for your specific ESP32 variant to confirm which GPIOs can be used for MCPWM outputs, fault inputs, and capture inputs. Some pins may have dedicated MCPWM functionality or restrictions.
- Clock Sources: While
MCPWM_TIMER_CLK_SRC_DEFAULT
(usually PLL) andMCPWM_CAPTURE_CLK_SRC_DEFAULT
are common, specific variants might offer different clocking options or have different default source frequencies. Theresolution_hz
setting in configuration helps abstract this by allowing you to define your desired tick rate.
Tip: The ESP-IDF driver will generally return an error (e.g., ESP_ERR_NO_MEM
if out of hardware resources, or ESP_ERR_INVALID_ARG
for incorrect group IDs) if you try to configure more resources than available on the specific chip. Always check the return codes of mcpwm_new_*
functions.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
No PWM Output | LED doesn’t light up, motor doesn’t spin, oscilloscope shows a flat line (high or low). |
1. Check Timer State: Ensure you’ve called mcpwm_timer_enable() and then mcpwm_timer_start_stop() .2. Verify GPIO: Double-check that gen_gpio_num is a valid MCPWM pin for your ESP32 variant.3. Check Duty Cycle: A compare value of 0 might result in a constant low signal depending on your actions. Set a test duty cycle like 50%.
|
Incorrect Frequency | Motor runs at the wrong speed, PWM frequency on oscilloscope doesn’t match calculations. |
Review the frequency formula based on your count mode: – UP/DOWN: Freq = resolution_hz / period_ticks – UP-DOWN: Freq = resolution_hz / (2 * period_ticks) Ensure resolution_hz and period_ticks are set correctly.
|
Inverted Duty Cycle | Increasing duty cycle value makes the LED dimmer or motor slower. |
Your generator actions are likely reversed. For a typical active-high PWM: – Action on timer zero/empty should be MCPWM_GEN_ACTION_HIGH .– Action on compare match should be MCPWM_GEN_ACTION_LOW .
|
Dead-Time Shoot-Through | H-bridge driver gets hot, power supply current spikes, or components are damaged. |
1. Verify with Scope: Immediately check gate signals with an oscilloscope. There must be a gap where both are low. 2. Increase Dead-Time: Your posedge_delay_ticks and negedge_delay_ticks are too short for your MOSFETs’ switching time. Increase them.3. Check Inversion: Ensure one generator is inverted in the dead-time config, e.g., flags.invert_output_b = true .
|
Capture Callback Not Firing | The function registered with mcpwm_capture_register_event_callbacks() never runs. |
1. Enable Channel/Timer: Ensure you’ve called mcpwm_capture_channel_enable() and mcpwm_capture_timer_start() .2. Check Edges: Did you enable the correct edge detection in mcpwm_capture_channel_config_t ? E.g., flags.pos_edge = true .3. Check Input Signal: Verify with a scope that a valid signal is reaching the capture GPIO pin. |
Exercises
- Brushed DC Motor Speed Control with Potentiometer:
- Modify Example 1. Connect a potentiometer to an ADC pin (refer to Chapter 126).
- Read the ADC value and map it to a duty cycle percentage (0-100%).
- Use
set_pwm_duty_cycle()
to control the speed of a small brushed DC motor connected via an L298N driver or a suitable MOSFET.
- Servo Motor Control:
- Standard servo motors typically expect a PWM signal at 50Hz. The pulse width determines the angle (usually 1ms to 2ms pulse width for a 0-180 degree range, e.g., 1.5ms for 90 degrees).
- Configure an MCPWM timer for 50Hz (period = 20ms). Timer resolution could be 1MHz (1us ticks).
- Calculate the
compare_value
needed for 1ms, 1.5ms, and 2ms pulses. - Create a function
set_servo_angle(int angle)
that converts an angle (0-180) to the appropriatecompare_value
and updates the MCPWM comparator. - Test by commanding the servo to different angles via UART input or hardcoded values.
- H-Bridge Control for DC Motor (Forward/Reverse/Stop):
- Using an L298N or similar H-bridge driver:
- One MCPWM timer and operator (e.g.,
op0
) for PWMxA output (e.g., GPIO A) to control speed (L298N Enable A pin). - Use two regular GPIOs for direction control (e.g., IN1 and IN2 pins of L298N).
- Implement functions:
motor_forward(uint8_t speed)
,motor_reverse(uint8_t speed)
,motor_stop()
. motor_stop()
can set IN1=IN2=LOW (coast) or IN1=IN2=HIGH (brake, if supported), and PWM duty to 0.motor_forward()
sets IN1=HIGH, IN2=LOW, and applies speed via PWM.motor_reverse()
sets IN1=LOW, IN2=HIGH, and applies speed via PWM.
- One MCPWM timer and operator (e.g.,
- Advanced: Try to use two MCPWM outputs from different operators (or even different timers if synchronization is not critical for simple direction) to control IN1 and IN2 with PWM, potentially allowing more nuanced control if the H-bridge logic supports it (though usually, IN1/IN2 are just on/off for direction).
- Using an L298N or similar H-bridge driver:
- Basic Three-Phase PWM Generation (Foundation for BLDC):
- Goal: Generate three PWM signals (Phase U, V, W) that are 120 degrees phase-shifted from each other. This is a conceptual exercise for understanding timing.
- Use one MCPWM timer in
UP_DOWN
count mode. - Use three operators, each connected to this single timer.
- Configure three comparators, one for each operator.
- To achieve a 120-degree phase shift:
- If the timer period is
P
ticks, then 120 degrees corresponds toP/3
ticks. - Set the compare value for Operator U’s comparator normally.
- For Operator V, you might need to adjust its generator actions or its compare value relative to U to appear shifted. A common technique involves setting comparator values suchthat their effective “center” or active period is shifted.
- Alternatively, use three separate timers and start them with appropriate delays (e.g., using
mcpwm_timer_sync_phase_t
if supported for this exact scenario, or manually manage start times). This is more complex to synchronize perfectly.
- If the timer period is
- Hint: For a simpler approach with one timer, you can make all generators trigger based on the same compare value but have their GPIO actions defined differently or use a common compare value and then use
mcpwm_generator_set_force_level
to manually sequence them if not for continuous PWM (more like stepper control). For true phase-shifted PWM from one timer, the actions on compare/period for each generator become critical. A more robust way for BLDC is often multiple synchronized timers or very careful manipulation of compare values and actions on a single fast timer. - Observe the outputs on an oscilloscope.
- Frequency Measurement with Capture:
- Use Example 4 (
init_capture_input
) as a base. - Modify the
on_capture_event_callback
to calculate the frequency. On each positive edge capture:current_capture_time = edata->cap_value;
period_ticks = current_capture_time - previous_capture_time;
(handle timer overflow if necessary for low frequencies / high resolution).frequency_hz = CAP_TIMER_RESOLUTION_HZ / period_ticks;
previous_capture_time = current_capture_time;
- Test by feeding a known frequency from a function generator or another ESP32’s LEDC/MCPWM output to the capture GPIO. Display the measured frequency on the serial monitor.
- Use Example 4 (
Summary
- The MCPWM peripheral is a powerful tool for generating complex PWM signals, crucial for motor control, power supply regulation, and other precision timing applications.
- It comprises MCPWM units (groups), each containing timers, operators (which host comparators and generators), dead-time modules, fault handlers, and capture channels.
- ESP-IDF v5.x uses an object-oriented API with handles (
mcpwm_timer_handle_t
, etc.) for configuring and managing MCPWM resources. - Timers define the fundamental frequency and counting mode (UP, DOWN, UP-DOWN for symmetric PWM).
- Operators, Comparators, and Generators work together: the timer counts, the comparator matches a value (duty cycle), and the generator defines actions on these events to shape the PWM output on a GPIO.
- Dead-Time Insertion is vital for H-bridge applications, preventing shoot-through by ensuring a small off-time between complementary PWM signal transitions.
- Fault Detection allows the MCPWM to react to external error signals by forcing PWM outputs to a safe state, enhancing system reliability.
- Capture Inputs enable the measurement of external signal characteristics like pulse width, period, or frequency, useful for sensor feedback (e.g., Hall sensors, encoders).
- Different ESP32 variants have varying numbers of MCPWM units and sub-modules, but the
mcpwm_prelude.h
API provides a consistent programming interface. - Proper resource management (creation, enabling, starting, and deletion of handles) is essential.
Further Reading
- ESP-IDF Programming Guide: MCPWM Driver:
- For the latest stable version: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/mcpwm.html (Adjust
esp32
in the URL to your specific target, e.g.,esp32s3
,esp32c3
).
- For the latest stable version: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/mcpwm.html (Adjust
- ESP32 Series Technical Reference Manuals (TRM):
- These documents contain detailed hardware descriptions of the MCPWM peripheral for each specific chip variant. Search for the TRM of your target chip (e.g., “ESP32-S3 TRM”) on the Espressif website: https://www.espressif.com/en/support/documents/technical-documents
- Application Notes and Examples from Espressif:
- Check the
examples
directory within your ESP-IDF installation, particularly underperipherals/mcpwm
. - Espressif’s GitHub repositories often contain practical examples and projects.
- Check the