mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-01 18:50:34 +03:00
docs: restructure MCPWM programming guide
This commit is contained in:
@@ -70,7 +70,7 @@ Other Peripheral Events
|
||||
:SOC_SYSTIMER_SUPPORT_ETM: - Refer to :doc:`/api-reference/system/esp_timer` for how to get the ETM event handle from esp_timer.
|
||||
:SOC_TIMER_SUPPORT_ETM: - Refer to :ref:`gptimer-etm-event-and-task` for how to get the ETM event handle from GPTimer.
|
||||
:SOC_GDMA_SUPPORT_ETM: - Refer to :doc:`/api-reference/peripherals/async_memcpy` for how to get the ETM event handle from async memcpy.
|
||||
:SOC_MCPWM_SUPPORT_ETM: - Refer to :doc:`/api-reference/peripherals/mcpwm` for how to get the ETM event handle from MCPWM.
|
||||
:SOC_MCPWM_SUPPORT_ETM: - Refer to :doc:`/api-reference/peripherals/mcpwm/mcpwm_etm` for how to get the ETM event handle from MCPWM.
|
||||
:SOC_ANA_CMPR_SUPPORT_ETM: - Refer to :doc:`/api-reference/peripherals/ana_cmpr` for how to get the ETM event handle from analog comparator.
|
||||
:SOC_TEMPERATURE_SENSOR_SUPPORT_ETM: - Refer to :doc:`/api-reference/peripherals/temp_sensor` for how to get the ETM event handle from temperature sensor.
|
||||
:SOC_I2S_SUPPORTS_ETM: - Refer to :doc:`/api-reference/peripherals/i2s` for how to get the ETM event handle from I2S.
|
||||
|
||||
@@ -32,7 +32,7 @@ Peripherals API
|
||||
lcd/index
|
||||
:SOC_GP_LDO_SUPPORTED: ldo_regulator
|
||||
:SOC_LEDC_SUPPORTED: ledc
|
||||
:SOC_MCPWM_SUPPORTED: mcpwm
|
||||
:SOC_MCPWM_SUPPORTED: mcpwm/index
|
||||
:SOC_PARLIO_SUPPORTED: parlio/index
|
||||
:SOC_PCNT_SUPPORTED: pcnt
|
||||
:SOC_PPA_SUPPORTED: ppa
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
============================================
|
||||
Motor Control Pulse Width Modulator (MCPWM)
|
||||
============================================
|
||||
|
||||
:link_to_translation:`zh_CN:[中文]`
|
||||
|
||||
Start Here
|
||||
==========
|
||||
|
||||
MCPWM turns a counter into accurately timed output edges. It is a good fit when an LEDC-style PWM is no longer enough: motor bridges need complementary outputs and dead time, inverters need synchronized phases, and sensors need pulse-width measurement.
|
||||
|
||||
The smallest useful MCPWM design has four objects: a :doc:`timer <mcpwm_timer>` supplies time, an :doc:`operator <mcpwm_operator>` owns the waveform resources, a :doc:`comparator <mcpwm_cmpr>` chooses an edge position, and a :doc:`generator <mcpwm_gen>` drives a GPIO. The other modules extend that design without changing its foundation.
|
||||
|
||||
Build a PWM output
|
||||
==================
|
||||
|
||||
For a first PWM output, create objects from left to right in the following flow. Each stage of the main path is color-coded by role: time base (blue), operator core (purple), waveform setup (cyan), start and output (green). Amber nodes are optional additions; the red node is the safety brake. Start the timer only after all output actions are configured.
|
||||
|
||||
.. mermaid::
|
||||
|
||||
flowchart LR
|
||||
T1["1. Create timer<br/>mcpwm_new_timer"]:::time
|
||||
O1["2. Create operator<br/>mcpwm_new_operator"]:::core
|
||||
LINK["3. Connect time base<br/>mcpwm_operator_connect_timer"]:::core
|
||||
C1["4. Create comparator<br/>mcpwm_new_comparator"]:::wave
|
||||
G1["5. Create generator<br/>mcpwm_new_generator"]:::wave
|
||||
A1["6. Describe edges<br/>mcpwm_generator_set_action_on_*_event"]:::wave
|
||||
RUN["7. Enable and start<br/>mcpwm_timer_enable<br/>mcpwm_timer_start_stop"]:::run
|
||||
PIN["PWM on GPIO"]:::output
|
||||
|
||||
T1 --> O1 --> LINK --> C1 --> G1 --> A1 --> RUN --> PIN
|
||||
|
||||
DT["Dead time<br/>mcpwm_generator_set_dead_time"]:::optional
|
||||
BR["Fault and brake<br/>mcpwm_new_*_fault<br/>mcpwm_operator_set_brake_on_fault"]:::safety
|
||||
SY["Phase synchronization<br/>mcpwm_new_*_sync_src<br/>mcpwm_timer_set_phase_on_sync"]:::optional
|
||||
CA["Carrier modulation<br/>mcpwm_operator_apply_carrier"]:::optional
|
||||
|
||||
A1 -. extend .-> DT
|
||||
O1 -. protect .-> BR
|
||||
T1 -. align .-> SY
|
||||
O1 -. modulate .-> CA
|
||||
|
||||
classDef time fill:#dbeafe,stroke:#2563eb,color:#172554
|
||||
classDef core fill:#ede9fe,stroke:#7c3aed,color:#2e1065
|
||||
classDef wave fill:#cffafe,stroke:#0891b2,color:#164e63
|
||||
classDef run fill:#dcfce7,stroke:#16a34a,color:#14532d
|
||||
classDef output fill:#bbf7d0,stroke:#15803d,color:#14532d
|
||||
classDef optional fill:#fef3c7,stroke:#d97706,color:#78350f
|
||||
classDef safety fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
|
||||
|
||||
The following code creates one 20 kHz PWM output with a 30% duty cycle. It is meant to be read before the individual pages so a first-time user can see the whole object chain in one place. It also shows the most common runtime adjustment: changing the comparator rather than rebuilding the waveform.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_timer_handle_t timer = NULL;
|
||||
mcpwm_oper_handle_t oper = NULL;
|
||||
mcpwm_cmpr_handle_t comparator = NULL;
|
||||
mcpwm_gen_handle_t generator = NULL;
|
||||
|
||||
// 1 MHz → 1 tick = 1 µs
|
||||
// 50 ticks → 50 µs period → 20 kHz
|
||||
ESP_ERROR_CHECK(mcpwm_new_timer(
|
||||
&(mcpwm_timer_config_t) {
|
||||
.group_id = 0,
|
||||
.clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 1000000,
|
||||
.period_ticks = 50,
|
||||
.count_mode = MCPWM_TIMER_COUNT_MODE_UP,
|
||||
},
|
||||
&timer));
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_new_operator(
|
||||
&(mcpwm_operator_config_t) {
|
||||
.group_id = 0,
|
||||
},
|
||||
&oper));
|
||||
ESP_ERROR_CHECK(mcpwm_operator_connect_timer(oper, timer));
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_new_comparator(
|
||||
oper,
|
||||
&(mcpwm_comparator_config_t) {
|
||||
.flags.update_cmp_on_tez = true,
|
||||
},
|
||||
&comparator));
|
||||
// 15 / 50 = 30% duty cycle
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, 15));
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_new_generator(
|
||||
oper,
|
||||
&(mcpwm_generator_config_t) {
|
||||
.gen_gpio_num = 18,
|
||||
},
|
||||
&generator));
|
||||
|
||||
// timer empty → output HIGH; compare match → output LOW
|
||||
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));
|
||||
|
||||
// Change duty at run time by moving the edge.
|
||||
// 25 / 50 = 50% duty
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, 25));
|
||||
|
||||
The timer's ``resolution_hz`` and ``period_ticks`` set the timing scale. The comparator's ``compare_value`` chooses an edge in that scale, and the generator action APIs decide the output level at the timer boundary or comparator crossing. This division is useful when tuning: change the timer for frequency, the comparator for duty or edge position, and generator actions for polarity or waveform shape.
|
||||
|
||||
After the waveform is configured, call :cpp:func:`mcpwm_timer_enable()` and :cpp:func:`mcpwm_timer_start_stop()`. At run time, update the comparator with :cpp:func:`mcpwm_comparator_set_compare_value()` rather than rebuilding the generator actions. Use the relevant optional branch only when the application needs it: dead time for a half bridge, fault and brake for a safety path, sync for phase alignment, and carrier for isolated drive.
|
||||
|
||||
Feature map
|
||||
===========
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 18 38 34 24
|
||||
|
||||
* - Goal
|
||||
- Read first
|
||||
- Key APIs
|
||||
- Typical use
|
||||
* - One PWM output
|
||||
- :doc:`timer <mcpwm_timer>` -> :doc:`operator <mcpwm_operator>` -> :doc:`comparator <mcpwm_cmpr>`
|
||||
|
||||
:doc:`generator <mcpwm_gen>`
|
||||
- :cpp:func:`mcpwm_new_timer`
|
||||
|
||||
:cpp:func:`mcpwm_new_comparator`
|
||||
|
||||
``mcpwm_generator_set_action_on_*_event``
|
||||
- Servo, dimming, basic control
|
||||
* - Complementary half-bridge PWM
|
||||
- dead-time section in :doc:`generator <mcpwm_gen>` + :doc:`fault <mcpwm_fault>`
|
||||
- :cpp:func:`mcpwm_generator_set_dead_time`
|
||||
|
||||
:cpp:func:`mcpwm_operator_set_brake_on_fault`
|
||||
- Half bridge, inverter leg
|
||||
* - Aligned or phase-shifted outputs
|
||||
- :doc:`sync <mcpwm_sync>`
|
||||
- :cpp:func:`mcpwm_timer_set_phase_on_sync`
|
||||
|
||||
:cpp:func:`mcpwm_new_timer_sync_src`
|
||||
- Multi-phase motor, paralleled converters
|
||||
* - Measure pulse width or period
|
||||
- :doc:`capture <mcpwm_cap>`
|
||||
- :cpp:func:`mcpwm_new_capture_timer`
|
||||
|
||||
:cpp:func:`mcpwm_capture_channel_register_event_callbacks`
|
||||
- HC-SR04, tachometer, RC input
|
||||
* - Hardware peripheral linking
|
||||
- :doc:`ETM <mcpwm_etm>`
|
||||
- :cpp:func:`mcpwm_timer_new_etm_event`
|
||||
|
||||
:cpp:func:`mcpwm_new_event_comparator`
|
||||
- ADC trigger, timing chains
|
||||
|
||||
Each page in this guide covers one MCPWM module:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
mcpwm_timer
|
||||
mcpwm_operator
|
||||
mcpwm_cmpr
|
||||
mcpwm_gen
|
||||
mcpwm_fault
|
||||
mcpwm_sync
|
||||
mcpwm_cap
|
||||
mcpwm_etm
|
||||
mcpwm_advanced
|
||||
|
||||
Resource and lifetime rules
|
||||
===========================
|
||||
|
||||
All objects belong to an MCPWM group. A timer and the operator connected to it must be in the same group; GPIO fault and GPIO sync sources can likewise be consumed only inside their group. Hardware resources are limited, so creation can return :c:macro:`ESP_ERR_NOT_FOUND`.
|
||||
|
||||
Every object is created by a ``mcpwm_new_*()`` factory that returns an opaque handle, and released with the matching ``mcpwm_del_*()`` function — for example :cpp:func:`mcpwm_new_timer()` and :cpp:func:`mcpwm_del_timer()`. Create parent objects before their children and destroy them in reverse order: generators/comparators first, then their operator, then the timer. A timer must be disabled before it can be deleted. Capture channels must be deleted before their capture timer.
|
||||
|
||||
The group clock divider is shared by timers and, on some targets, capture timers. Allocate objects in monotonic requested-resolution order (high-to-low or low-to-high) to avoid a divider conflict. See :doc:`advanced topics <mcpwm_advanced>` for the exact resolution rules.
|
||||
|
||||
Glossary
|
||||
========
|
||||
|
||||
.. list::
|
||||
|
||||
- **TEZ:** Timer equals zero, when the timer count reaches zero.
|
||||
- **TEP:** Timer equals peak, when the timer count reaches its peak.
|
||||
- **Timer:** The time base that defines PWM frequency and tick granularity.
|
||||
- **Operator:** The container between the timer and the outputs; it manages comparators, generators, brake, dead time, and carrier.
|
||||
- **Comparator:** Emits an event when the count reaches a threshold; often used to place an edge or define duty.
|
||||
- **Generator:** Drives the GPIO level in response to timer, comparator, fault, or sync events.
|
||||
- **Dead time:** A non-overlap interval between half-bridge transitions to avoid shoot-through.
|
||||
- **Fault:** An abnormal condition source, from GPIO or software.
|
||||
- **Brake:** The output safety policy applied after a fault.
|
||||
- **CBC:** Cycle-by-cycle braking that recovers automatically at a cycle boundary after the fault clears.
|
||||
- **OST:** One-shot braking that stays latched until software recovers it.
|
||||
- **Sync:** Loading a timer to a chosen count and direction on a sync edge.
|
||||
- **Capture:** Timestamping external input edges to measure pulse width, period, or speed.
|
||||
|
||||
Application examples
|
||||
====================
|
||||
|
||||
.. list::
|
||||
|
||||
- :example:`peripherals/mcpwm/mcpwm_servo_control` — one PWM output for an RC servo.
|
||||
- :example:`peripherals/mcpwm/mcpwm_bdc_speed_control` — brushed DC motor and speed feedback.
|
||||
- :example:`peripherals/mcpwm/mcpwm_bldc_hall_control` — BLDC commutation using Hall-sensor feedback.
|
||||
- :example:`peripherals/mcpwm/mcpwm_capture_hc_sr04` — pulse width measurement with an HC-SR04.
|
||||
- :example:`peripherals/mcpwm/mcpwm_sync` — GPIO, timer, and software synchronization.
|
||||
- :example:`peripherals/mcpwm/mcpwm_foc_svpwm_open_loop` — three complementary PWM pairs for open-loop FOC.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
Common types
|
||||
------------
|
||||
|
||||
.. include-build-file:: inc/components/esp_driver_mcpwm/include/driver/mcpwm_types.inc
|
||||
.. include-build-file:: inc/components/esp_hal_mcpwm/include/hal/mcpwm_types.inc
|
||||
@@ -0,0 +1,62 @@
|
||||
======================
|
||||
MCPWM Advanced Topics
|
||||
======================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
Resolution and shared clocks
|
||||
============================
|
||||
|
||||
``resolution_hz`` is the timer tick rate; one tick lasts ``1 / resolution_hz`` seconds. MCPWM selects dividers from the selected source clock. If the requested rate is exactly divisible, it is preferred. Otherwise the driver keeps the group clock as high as possible and selects the closest achievable submodule rate.
|
||||
|
||||
The group divider is shared. On targets with capture clock from the group, PWM and capture timers share it too. When multiple objects need different resolutions, allocate them in one monotonic order (all high-to-low or all low-to-high), rather than allocating an arbitrary mix.
|
||||
|
||||
The clock tree below shows how a single source clock fans out. The group divider is fixed once for the whole group, and each timer only gets its own prescaler on top of it:
|
||||
|
||||
.. mermaid::
|
||||
|
||||
flowchart LR
|
||||
src["Source clock<br/>(e.g. 80 MHz)"]:::src -->|"÷ group prescale<br/>shared by the group"| grp["Group clock<br/>(e.g. 40 MHz)"]:::grp
|
||||
grp -->|"÷ timer prescale"| pwm["PWM timer<br/>resolution_hz = 10 MHz"]:::mod
|
||||
grp -->|"÷ capture prescale"| cap["Capture timer<br/>resolution_hz = 20 MHz"]:::mod
|
||||
classDef src fill:#fef3c7,stroke:#d97706,color:#78350f
|
||||
classDef grp fill:#dbeafe,stroke:#2563eb,color:#172554
|
||||
classDef mod fill:#dcfce7,stroke:#16a34a,color:#14532d
|
||||
|
||||
Because the group prescale is shared, it is chosen once for all timers. A later request for a different resolution cannot move that fixed divider, so the driver keeps the group clock as high as possible and picks the closest achievable prescale for the new submodule instead.
|
||||
|
||||
Power management and sleep
|
||||
==========================
|
||||
|
||||
With power management enabled, :cpp:func:`mcpwm_timer_enable()` and :cpp:func:`mcpwm_capture_timer_enable()` hold an :cpp:enumerator:`esp_pm_lock_type_t::ESP_PM_NO_LIGHT_SLEEP` lock so the timer keeps a stable clock frequency. Call :cpp:func:`mcpwm_timer_disable()` or :cpp:func:`mcpwm_capture_timer_disable()` to release the lock.
|
||||
|
||||
Both the timer config and the capture timer config carry an :cpp:member:`allow_pd <mcpwm_timer_config_t::flags::allow_pd>` field (the latter in :cpp:type:`mcpwm_capture_timer_config_t`). Set it if the application permits MCPWM's power domain to power down during sleep. The driver then saves and restores registers, at the cost of extra RAM. This capability is target dependent.
|
||||
|
||||
ISR and task safety
|
||||
===================
|
||||
|
||||
Callbacks for timer, comparator, fault, operator brake, and capture run in ISR context. Keep them non-blocking and use ISR-safe RTOS calls only. The first callback registered in a group fixes its shared interrupt priority; make all later event users in that group use the same priority.
|
||||
|
||||
Factory functions such as :cpp:func:`mcpwm_new_timer()` are thread-safe. :cpp:func:`mcpwm_timer_set_period()` and :cpp:func:`mcpwm_comparator_set_compare_value()` may run from ISR context. Other control APIs are not generally thread-safe; serialize them if more than one task can access the same object.
|
||||
|
||||
Cache-safe real-time operation
|
||||
==============================
|
||||
|
||||
Normally, MCPWM interrupts are deferred while cache is disabled (for example during flash operations). Enable :menuitem:`CONFIG_MCPWM_ISR_CACHE_SAFE` when callbacks must continue to run; this places ISR-required code in IRAM and objects in DRAM, increasing internal RAM use.
|
||||
|
||||
.. note::
|
||||
|
||||
With this option enabled, MCPWM interrupts still fire immediately and are not deferred even while cache is disabled. However, every function on the interrupt path — including your registered callback and all functions it calls — must live in IRAM: with cache off, the CPU cannot fetch instructions from flash, so calling any function still resident in flash crashes the CPU. The option only moves the driver's own ISR code to IRAM; you must place your callbacks and the functions they call in IRAM explicitly (for example with ``IRAM_ATTR``).
|
||||
|
||||
:menuitem:`CONFIG_MCPWM_CTRL_FUNC_IN_IRAM` additionally places :cpp:func:`mcpwm_timer_set_period()` and :cpp:func:`mcpwm_comparator_set_compare_value()` in IRAM, so those calls keep working even while cache is off — for example you can retune the PWM period or duty from a cache-disabled context, such as inside a flash-writing routine, without waiting for the cache to be re-enabled.
|
||||
|
||||
Kconfig options
|
||||
===============
|
||||
|
||||
.. list::
|
||||
|
||||
- :menuitem:`CONFIG_MCPWM_ISR_CACHE_SAFE` enables cache-safe interrupts.
|
||||
- :menuitem:`CONFIG_MCPWM_CTRL_FUNC_IN_IRAM` moves selected control functions to IRAM.
|
||||
- :menuitem:`CONFIG_MCPWM_ENABLE_DEBUG_LOG` forces the MCPWM driver to compile in and print its own debug logs, ignoring the global log settings, and raises the runtime log level to verbose for the driver only — other modules are unaffected. The price is increased firmware size.
|
||||
@@ -0,0 +1,148 @@
|
||||
======================================
|
||||
MCPWM Capture: Measure an Input Pulse
|
||||
======================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
Capture is an independent MCPWM path: a capture timer timestamps edges on capture-channel GPIOs. It does not require a PWM timer, operator, comparator, or generator. This makes it ideal for echo pulses, tachometers, Hall sensors, and RC receiver signals.
|
||||
|
||||
It is the MCPWM path for bringing external timing into the chip. Use it when the problem is pulse width, period, phase, or speed rather than PWM generation.
|
||||
|
||||
Measure a pulse width
|
||||
=====================
|
||||
|
||||
Configure both edges, save the rising timestamp, and subtract it from the falling timestamp. With a 1 MHz capture resolution, the difference is directly in microseconds.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_cap_timer_handle_t cap_timer = NULL;
|
||||
mcpwm_cap_channel_handle_t cap_channel = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_new_capture_timer(
|
||||
&(mcpwm_capture_timer_config_t) {
|
||||
.group_id = 0,
|
||||
.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 1000000,
|
||||
}, &cap_timer));
|
||||
ESP_ERROR_CHECK(mcpwm_new_capture_channel(cap_timer,
|
||||
&(mcpwm_capture_channel_config_t) {
|
||||
.gpio_num = 6,
|
||||
.prescale = 1,
|
||||
.flags.pos_edge = true,
|
||||
.flags.neg_edge = true,
|
||||
}, &cap_channel));
|
||||
|
||||
Allocation alone does not start measurement. Arm the channel and run the capture timer:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
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));
|
||||
|
||||
:cpp:func:`mcpwm_capture_channel_enable()` and :cpp:func:`mcpwm_capture_timer_enable()` set up the system services the capture needs; neither starts the measurement yet. :cpp:func:`mcpwm_capture_timer_start()` finally makes the counter run, so edges start being timestamped.
|
||||
|
||||
The captured edge values reach the application through a callback, described in the next section.
|
||||
|
||||
.. figure:: /../_static/mcpwm/capture_measurement.svg
|
||||
:align: center
|
||||
:alt: Capture timestamps the rising and falling edges; subtracting them yields the high-pulse width.
|
||||
|
||||
Capture the rising and falling edge timestamps, then subtract to get the high-pulse width.
|
||||
|
||||
The two configuration structs are worth reading separately:
|
||||
|
||||
Capture timer configuration
|
||||
---------------------------
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`group_id <mcpwm_capture_timer_config_t::group_id>` — the MCPWM group the capture timer is allocated from.
|
||||
- :cpp:member:`clk_src <mcpwm_capture_timer_config_t::clk_src>` — the clock feeding the capture timer. :c:macro:`MCPWM_CAPTURE_CLK_SRC_DEFAULT` is right for most applications. Pick a specific source when the default one may be gated — for example, in low-power scenarios where a clock that can be switched off would stop the capture timer and corrupt your timestamps.
|
||||
- :cpp:member:`resolution_hz <mcpwm_capture_timer_config_t::resolution_hz>` — the tick rate of the capture timer. One tick lasts ``1 / resolution_hz`` seconds, so 1 MHz gives microsecond resolution. It directly sets the precision of every captured timestamp.
|
||||
- :cpp:member:`allow_pd <mcpwm_capture_timer_config_t::flags::allow_pd>` — lets the MCPWM power domain switch off during sleep, backing up and restoring the capture registers around the sleep transition at the cost of extra RAM.
|
||||
|
||||
Capture channel configuration
|
||||
-----------------------------
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`gpio_num <mcpwm_capture_channel_config_t::gpio_num>` — the GPIO carrying the input signal.
|
||||
- :cpp:member:`prescale <mcpwm_capture_channel_config_t::prescale>` — divides the input signal before capture; the effective input frequency is the capture clock divided by ``prescale``. Raise it to extend the measurable period range, at the cost of time resolution.
|
||||
- :cpp:member:`pos_edge <mcpwm_capture_channel_config_t::flags::pos_edge>` and :cpp:member:`neg_edge <mcpwm_capture_channel_config_t::flags::neg_edge>` — which edges are captured. The example captures both, which is what a pulse-width measurement needs.
|
||||
- :cpp:member:`invert_cap_signal <mcpwm_capture_channel_config_t::flags::invert_cap_signal>` — inverts the input signal before capture, so a logical ``1`` on the pin is seen as ``0`` by the capture peripheral and vice versa.
|
||||
- :cpp:member:`intr_priority <mcpwm_capture_channel_config_t::intr_priority>` — the interrupt priority used by the capture callbacks. Not setting it (``0``) lets the driver choose a low priority.
|
||||
|
||||
.. note::
|
||||
|
||||
The capture driver configures the GPIO as an input but does not set any pull-up or pull-down resistor. If the input signal is not actively driven to both levels, call :cpp:func:`gpio_set_pull_mode()` to select the pull direction that keeps the pin at the level you expect when the line is idle.
|
||||
|
||||
Capture event callbacks
|
||||
=======================
|
||||
|
||||
The event data tells you the edge and latched count. The calculation below leaves heavy work to a task in a real application.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
static uint32_t rise_tick;
|
||||
static bool IRAM_ATTR on_capture(mcpwm_cap_channel_handle_t channel,
|
||||
const mcpwm_capture_event_data_t *edata,
|
||||
void *user_data)
|
||||
{
|
||||
if (edata->cap_edge == MCPWM_CAP_EDGE_POS) {
|
||||
rise_tick = edata->cap_value;
|
||||
} else {
|
||||
uint32_t width_ticks = edata->cap_value - rise_tick;
|
||||
// Notify a task with width_ticks; do not printf here.
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_capture_channel_register_event_callbacks(cap_channel,
|
||||
&(mcpwm_capture_event_callbacks_t) { .on_cap = on_capture }, NULL));
|
||||
|
||||
Obtain the actual resolution with :cpp:func:`mcpwm_capture_timer_get_resolution()` before converting ticks to time. On targets where capture shares the MCPWM group clock, create capture and PWM timers in a consistent requested-resolution order.
|
||||
|
||||
To measure speed or period, record two timestamps of the same edge type, subtract them to get period ticks, then convert that value to frequency or RPM with the actual capture resolution.
|
||||
|
||||
Useful controls
|
||||
===============
|
||||
|
||||
:cpp:func:`mcpwm_capture_channel_trigger_soft_catch()` generates a software capture event, commonly used for testing but also handy to land the timing of important software events on the same capture timebase as hardware edges; it invokes the callback as well. :cpp:func:`mcpwm_capture_get_latched_value()` reads the latest timestamp without registering any callback.
|
||||
|
||||
:cpp:func:`mcpwm_capture_timer_stop()` halts the counter, :cpp:func:`mcpwm_capture_channel_disable()` gates an individual input, and stopping the timer gates the whole measurement engine. Call :cpp:func:`mcpwm_capture_timer_disable()` to undo the setup done by :cpp:func:`mcpwm_capture_timer_enable()` before deleting the objects.
|
||||
|
||||
Capture timer synchronization
|
||||
=============================
|
||||
|
||||
The capture timer free-runs by default, so the zero point of its count is arbitrary and timestamps can only be compared with each other. Synchronization makes the running capture timer load a given count value when a sync edge arrives, anchoring the timestamps to a meaningful reference.
|
||||
|
||||
The most common use is aligning the capture timer with a PWM timer: use the sync emitted by the PWM timer at each period zero (TEZ) as the source and set the count value to 0, so the capture timer restarts from zero every period. A captured timestamp then directly represents the phase within the PWM period. This matters in motor control and power conversion, where feedback edges from a Hall sensor, encoder, or current sense are only meaningful at a specific phase of the PWM cycle.
|
||||
|
||||
Sync sources are shared with the PWM timers (GPIO, software, or timer — all in the same MCPWM group). Configure the receiving side with :cpp:func:`mcpwm_capture_timer_set_phase_on_sync()`:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_capture_timer_set_phase_on_sync(cap_timer,
|
||||
&(mcpwm_capture_timer_sync_phase_config_t) {
|
||||
.sync_src = timer_a_sync, // created with mcpwm_new_timer_sync_src()
|
||||
.count_value = 0,
|
||||
.direction = MCPWM_TIMER_DIRECTION_UP,
|
||||
}));
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`sync_src <mcpwm_capture_timer_sync_phase_config_t::sync_src>` — the sync source; pass ``NULL`` to detach synchronization.
|
||||
- :cpp:member:`count_value <mcpwm_capture_timer_sync_phase_config_t::count_value>` — the count loaded when the sync edge arrives.
|
||||
- :cpp:member:`direction <mcpwm_capture_timer_sync_phase_config_t::direction>` — the counting direction after loading; the capture timer only counts up, so it is always :cpp:enumerator:`MCPWM_TIMER_DIRECTION_UP`.
|
||||
|
||||
Software and GPIO sync sources can also give the capture timer a known origin or align it to an external reference. See :doc:`synchronization <mcpwm_sync>` for how to create the sync sources and other details.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM Capture Driver Functions
|
||||
------------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_cap.inc
|
||||
@@ -0,0 +1,116 @@
|
||||
============================================
|
||||
MCPWM Comparator: Turn a Ratio into an Edge
|
||||
============================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
A comparator emits an event when the timer count reaches ``cmp_ticks``. A generator converts that event into a GPIO transition. In the usual up-counting PWM arrangement, the comparator value is the high-time in ticks.
|
||||
|
||||
In practice, a comparator is how you turn "I want the edge earlier, later, narrower, or wider" into hardware timing. Runtime duty control is usually nothing more than changing the comparator threshold.
|
||||
|
||||
Set a 30% duty cycle
|
||||
=====================
|
||||
|
||||
Create a comparator from an existing operator, then set its threshold. With the 50-tick timer from the :doc:`timer page <mcpwm_timer>`, a value of 15 represents 30% duty. The waveform below shows the compare event at tick 15 — the generator can use this to end the high pulse.
|
||||
|
||||
.. figure:: /../_static/mcpwm/compare_event.svg
|
||||
:align: center
|
||||
:alt: Timer counts up; the comparator fires at tick 15. The generator turns this into a falling edge.
|
||||
|
||||
Timer counts up; the comparator fires at tick 15. The generator turns this into a falling edge.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_cmpr_handle_t comparator = NULL;
|
||||
mcpwm_comparator_config_t comparator_config = {
|
||||
.flags.update_cmp_on_tez = true, // Change duty only at cycle boundary
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_new_comparator(oper, &comparator_config, &comparator));
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, 15));
|
||||
|
||||
The comparator configuration has only one field besides the flags:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`intr_priority <mcpwm_comparator_config_t::intr_priority>` — the interrupt priority used by the :cpp:member:`on_reach <mcpwm_comparator_event_callbacks_t::on_reach>` callback. Not setting it (``0``) lets the driver choose a low priority; raise it when the callback must preempt other ISRs.
|
||||
- :cpp:member:`flags <mcpwm_comparator_config_t::flags>` — the update points explained below. The example enables :cpp:member:`update_cmp_on_tez <mcpwm_comparator_config_t::flags::update_cmp_on_tez>`, which is the usual choice for changing duty at the cycle boundary.
|
||||
|
||||
For a runtime duty request in percent, calculate ``period_ticks * percent / 100``. Keep the result within the timer period.
|
||||
|
||||
This is why duty updates normally change the comparator rather than the generator actions: actions describe the waveform rule, while the comparator is the runtime edge position.
|
||||
|
||||
Why buffer the update?
|
||||
======================
|
||||
|
||||
Updating a comparator immediately can move an edge in the middle of the active cycle. :cpp:member:`update_cmp_on_tez <mcpwm_comparator_config_t::flags::update_cmp_on_tez>` buffers it until the counter reaches zero, :cpp:member:`update_cmp_on_tep <mcpwm_comparator_config_t::flags::update_cmp_on_tep>` until it reaches the peak, and :cpp:member:`update_cmp_on_sync <mcpwm_comparator_config_t::flags::update_cmp_on_sync>` until a sync event. In up-counting or down-counting mode the peak coincides with the cycle boundary, so ``tez`` and ``tep`` select almost the same update point; only in up-down mode does the peak sit at the midpoint of the cycle, making ``tez`` and ``tep`` two distinct update points. The buffered choice is usually the right one for motors and power conversion.
|
||||
|
||||
Two comparators for pulse placement
|
||||
====================================
|
||||
|
||||
A single comparator gives one edge per cycle. With two comparators in the same operator, you can place a pulse anywhere inside the period — one comparator opens the pulse and the other closes it. This is useful for sampling windows, trigger signals, or asymmetric dead-time compensation.
|
||||
|
||||
That pattern appears often in motor control, for example when an ADC sample window should sit away from switching noise, or when an external device needs a timing pulse that is not tied to the PWM boundary.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_cmpr_handle_t cmp_a, cmp_b;
|
||||
mcpwm_new_comparator(oper, &comparator_config, &cmp_a);
|
||||
mcpwm_new_comparator(oper, &comparator_config, &cmp_b);
|
||||
mcpwm_comparator_set_compare_value(cmp_a, 10);
|
||||
mcpwm_comparator_set_compare_value(cmp_b, 30);
|
||||
|
||||
Use compare events as a timing marker
|
||||
=====================================
|
||||
|
||||
The :cpp:member:`on_reach <mcpwm_comparator_event_callbacks_t::on_reach>` callback fires when the compare value is reached. This is useful when software must observe a precise point in the PWM cycle. Register it before starting time-critical work. The callback runs in ISR context, so keep it short.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
static bool IRAM_ATTR on_compare(mcpwm_cmpr_handle_t cmpr,
|
||||
const mcpwm_compare_event_data_t *edata,
|
||||
void *user_ctx)
|
||||
{
|
||||
// Signal a task or trigger only ISR-safe work.
|
||||
return false;
|
||||
}
|
||||
|
||||
mcpwm_comparator_event_callbacks_t callbacks = { .on_reach = on_compare };
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_register_event_callbacks(comparator,
|
||||
&callbacks, NULL));
|
||||
|
||||
Comparator kinds
|
||||
================
|
||||
|
||||
The **operator comparator**, created with :cpp:func:`mcpwm_new_comparator()`, drives the generators so it can shape the PWM output.
|
||||
|
||||
.. only:: SOC_MCPWM_SUPPORT_EVENT_COMPARATOR
|
||||
|
||||
The **event comparator**, created with :cpp:func:`mcpwm_new_event_comparator()`. Its compare event only reaches other peripherals through :doc:`ETM </api-reference/peripherals/etm>`; it never drives a generator and does not affect the PWM output.
|
||||
|
||||
.. note::
|
||||
|
||||
The name is the trap: the operator comparator can also produce ETM events, so the event comparator is not the only way to link MCPWM to ETM. The event comparator exists to *supplement* the operator comparator, not to replace it. An event comparator consumes none of the operator-comparator slots and never moves a PWM edge, which makes it the flexible choice when you need an extra compare point purely as a timing marker — for example, to find a sampling window for an ADC trigger that must not disturb the PWM waveform.
|
||||
|
||||
Both types accept the same compare value and ETM event setup:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_event_comparator_config_t evt_cmp_cfg = {};
|
||||
mcpwm_cmpr_handle_t evt_cmpr = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_new_event_comparator(oper, &evt_cmp_cfg, &evt_cmpr));
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(evt_cmpr, 25));
|
||||
|
||||
esp_etm_event_handle_t evt = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_new_etm_event(evt_cmpr,
|
||||
&(mcpwm_cmpr_etm_event_config_t){ .event_type = MCPWM_CMPR_ETM_EVENT_EQUAL },
|
||||
&evt));
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM Comparator Driver Functions
|
||||
----------------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_cmpr.inc
|
||||
@@ -0,0 +1,66 @@
|
||||
===============================================
|
||||
MCPWM ETM: Hardware Linking Between Peripherals
|
||||
===============================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
.. only:: SOC_MCPWM_SUPPORT_ETM
|
||||
|
||||
The Event Task Matrix (ETM) can route an MCPWM timer or comparator event directly to an ETM task, avoiding ISR latency. Use it when another peripheral must respond at an exact PWM phase.
|
||||
|
||||
Create an ETM event from the timer or comparator, create a compatible task from the destination peripheral, then connect both with an ETM channel. The destination driver's documentation defines its task and the complete channel setup. For the general ETM workflow — allocating a channel and connecting an event to a task — see the :doc:`ETM </api-reference/peripherals/etm>` documentation.
|
||||
|
||||
.. mermaid::
|
||||
|
||||
flowchart LR
|
||||
T["MCPWM Timer<br/>TEZ/TEP event"]:::source --> E["ETM Channel"]:::route
|
||||
C["MCPWM Comparator<br/>compare event"]:::source --> E
|
||||
E --> D["Destination<br/>peripheral task"]:::dest
|
||||
classDef source fill:#dbeafe,stroke:#2563eb,color:#172554
|
||||
classDef route fill:#ede9fe,stroke:#7c3aed,color:#2e1065
|
||||
classDef dest fill:#dcfce7,stroke:#16a34a,color:#14532d
|
||||
|
||||
A timer emits a `TEZ` (timer reaches zero) or `TEP` (timer reaches peak) event. To get one:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_etm_event_handle_t timer_event = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_timer_new_etm_event(timer,
|
||||
&(mcpwm_timer_etm_event_config_t) {
|
||||
.event_type = MCPWM_TIMER_ETM_EVENT_TEZ,
|
||||
}, &timer_event));
|
||||
// Create a destination ETM task, allocate a channel, then connect:
|
||||
// esp_etm_channel_connect(channel, timer_event, destination_task);
|
||||
|
||||
A comparator provides an `EQUAL` event, firing each time the timer count equals the comparator value. This pins the event to an arbitrary phase of the PWM period rather than just the crest or trough. To get one:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
esp_etm_event_handle_t cmp_event = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_new_etm_event(cmp,
|
||||
&(mcpwm_cmpr_etm_event_config_t) {
|
||||
.event_type = MCPWM_CMPR_ETM_EVENT_EQUAL,
|
||||
}, &cmp_event));
|
||||
// esp_etm_channel_connect(channel, cmp_event, destination_task);
|
||||
|
||||
Release the event with :cpp:func:`esp_etm_del_event()` when no longer needed.
|
||||
|
||||
A very common use of a comparator event is to trigger an ADC sampling: place the comparator at the phase you want to sample, then have the comparator event start an ADC so the converter samples a clean, settled waveform exactly in phase with the PWM cycle. Because the whole link is done in hardware, the ADC sample point tracks the PWM with no CPU and no ISR latency.
|
||||
|
||||
.. only:: SOC_MCPWM_SUPPORT_EVENT_COMPARATOR
|
||||
|
||||
Which comparator feeds the event matters. The **operator comparator** (:cpp:func:`mcpwm_new_comparator()`) also drives the generators, so its compare value defines an actual PWM output edge, and its ETM event is limited to that edge — you cannot ask it for a phase that is not one of the edges it produces. The **event comparator** (:cpp:func:`mcpwm_new_event_comparator()`) is a dedicated ETM timing marker: it drives no generator, consumes none of the operator-comparator slots, and therefore can fire at *any* phase inside the PWM period, with no effect on the PWM waveform. That freedom is exactly what an ADC trigger needs, so the event comparator is the recommended source — pick a sample point where the voltage has settled, not just where an edge happens to be.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM ETM Driver Functions
|
||||
--------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_etm.inc
|
||||
|
||||
.. only:: not SOC_MCPWM_SUPPORT_ETM
|
||||
|
||||
{IDF_TARGET_NAME} does not support MCPWM ETM events.
|
||||
@@ -0,0 +1,84 @@
|
||||
==================================================
|
||||
MCPWM Fault: Bring a Protection Signal into MCPWM
|
||||
==================================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
A fault object represents an abnormal condition. A GPIO fault is for a hardware signal such as an over-current comparator; a software fault lets application logic trigger the same protection route. Use the object with the operator :ref:`brake <mcpwm-brake>` to define the output response.
|
||||
|
||||
The purpose of the fault path is to make protection depend as little as possible on software polling or task scheduling. In motor drives and power converters, over-current, interlock, or emergency-stop conditions usually need hardware to force a safe output state first, then let software decide how to log and recover.
|
||||
|
||||
Create an active-low GPIO fault
|
||||
===============================
|
||||
|
||||
Create the fault in the same group as the operator that will consume it. The pin's electrical pull configuration is separate GPIO setup, so make the inactive level unambiguous before starting the power stage. The MCPWM driver does not enable internal pull resistors for a GPIO fault pin; if the fault signal does not drive the pin in the inactive state, configure the pull direction yourself with :cpp:func:`gpio_set_pull_mode()`.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_fault_handle_t fault = NULL;
|
||||
mcpwm_gpio_fault_config_t fault_config = {
|
||||
.group_id = 0,
|
||||
.gpio_num = 4,
|
||||
.flags.active_level = 0,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_new_gpio_fault(&fault_config, &fault));
|
||||
|
||||
The GPIO fault configuration has a few fields to consider:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`group_id <mcpwm_gpio_fault_config_t::group_id>` — the MCPWM group the fault belongs to. It must match the group of the operator consuming the fault.
|
||||
- :cpp:member:`gpio_num <mcpwm_gpio_fault_config_t::gpio_num>` — the GPIO carrying the fault signal.
|
||||
- :cpp:member:`active_level <mcpwm_gpio_fault_config_t::flags::active_level>` — the level treated as active. The example uses ``0``, so the fault is active low; the pin's pull direction must keep it inactive (high) when nothing asserts the fault. The driver leaves the pad's pull configuration untouched, so call :cpp:func:`gpio_set_pull_mode()` to select the pull-up/pull-down as appropriate.
|
||||
- :cpp:member:`intr_priority <mcpwm_gpio_fault_config_t::intr_priority>` — the interrupt priority used by the fault event callbacks. Not setting it (``0``) lets the driver choose a low priority.
|
||||
|
||||
Create a software fault
|
||||
=======================
|
||||
|
||||
For an application-detected condition, create :cpp:func:`mcpwm_new_soft_fault()` and invoke :cpp:func:`mcpwm_soft_fault_activate()` when the condition occurs, instead of wiring a GPIO fault pin. The activation is a one-time fault event; its output policy is still configured by the operator :ref:`brake <mcpwm-brake>` mechanism.
|
||||
|
||||
.. note::
|
||||
|
||||
Bind the soft fault to an operator with :cpp:func:`mcpwm_operator_set_brake_on_fault()` before activating it. The driver does not attach the soft fault to an operator at :cpp:func:`mcpwm_new_soft_fault()` time; the operator association and its brake mode are set by the bind call, and a soft fault can be bound to only one operator. Calling :cpp:func:`mcpwm_soft_fault_activate()` before binding is undefined behavior.
|
||||
|
||||
Fault as a trigger for generator actions
|
||||
=========================================
|
||||
|
||||
A GPIO fault can also directly trigger a generator action via :cpp:func:`mcpwm_generator_set_action_on_fault_event()`. This is a local edge-level response — it changes the output at the fault edge but does not latch a safe state. For persistent braking with recovery, use the operator :ref:`brake mechanism <mcpwm-brake>`.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 22 34 28
|
||||
|
||||
* - Mechanism
|
||||
- Behavior
|
||||
- Best fit
|
||||
* - Generator fault action
|
||||
- Immediately changes one output at the fault edge
|
||||
- Local fast reaction on a single output
|
||||
* - Operator brake
|
||||
- Defines safe state, latching, and recovery policy
|
||||
- Primary protection path for a power stage
|
||||
|
||||
Fault event callbacks
|
||||
=====================
|
||||
|
||||
The :cpp:member:`on_fault_enter <mcpwm_fault_event_callbacks_t::on_fault_enter>` and :cpp:member:`on_fault_exit <mcpwm_fault_event_callbacks_t::on_fault_exit>` callbacks report GPIO fault transitions and are only available for GPIO faults — the driver rejects registering them on a soft fault. Soft faults trigger the brake immediately in hardware, with no callback. The callbacks run in ISR context. Timestamp the event or notify a task with an ISR-safe primitive, then make logging and recovery decisions in the task.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_fault_event_callbacks_t cbs = {
|
||||
.on_fault_enter = my_fault_enter_cb,
|
||||
.on_fault_exit = my_fault_exit_cb,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_fault_register_event_callbacks(fault, &cbs, NULL));
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM Fault Driver Functions
|
||||
----------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_fault.inc
|
||||
@@ -0,0 +1,327 @@
|
||||
=========================================
|
||||
MCPWM Generator: Create the PWM Waveform
|
||||
=========================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
The generator is the final digital output. It does not have a fixed duty-cycle setting; instead, you teach it what level to drive at timer and comparator events. This makes simple PWM easy and leaves room for asymmetric, complementary, and phase-sensitive waveforms.
|
||||
|
||||
This is one of the biggest differences from a simpler PWM peripheral: MCPWM does not just ask for frequency and duty, it lets you describe what should happen at each important event. That adds concepts up front, but gives much tighter control over waveform structure.
|
||||
|
||||
Your first PWM output
|
||||
=====================
|
||||
|
||||
This is the completion of the :doc:`timer <mcpwm_timer>`/:doc:`operator <mcpwm_operator>`/:doc:`comparator <mcpwm_cmpr>` setup in the preceding pages. At timer zero, drive the GPIO high. When the comparator reaches 15, drive it low. With a 50-tick period, the output is high for 15 ticks (30%).
|
||||
|
||||
Application: basic single-output PWM
|
||||
------------------------------------
|
||||
|
||||
Use this for the simplest single-output PWM cases, such as an RC servo control signal, LED dimming, or a basic duty-controlled output where polarity and protection are already handled elsewhere.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_gen_handle_t generator = NULL;
|
||||
mcpwm_generator_config_t gen_config = { .gen_gpio_num = 18 };
|
||||
ESP_ERROR_CHECK(mcpwm_new_generator(oper, &gen_config, &generator));
|
||||
|
||||
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)));
|
||||
|
||||
.. figure:: /../_static/mcpwm/single_edge_asym_active_high.svg
|
||||
:align: center
|
||||
:alt: Up-counting, active-high PWM: set high at zero and low at compare.
|
||||
|
||||
Up-counting, active-high PWM: set high at zero and low at compare.
|
||||
|
||||
The generator configuration is small:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`gen_gpio_num <mcpwm_generator_config_t::gen_gpio_num>` — the GPIO that carries the PWM output. A second generator in the same operator, configured with the same actions, drives a second pin from the same time base.
|
||||
- :cpp:member:`invert_pwm <mcpwm_generator_config_t::flags::invert_pwm>` — inverts the PWM signal through the GPIO matrix. This is a hardware inversion of the final output, distinct from changing the actions; choose one of the two, not both.
|
||||
|
||||
Action configuration
|
||||
====================
|
||||
|
||||
The configuration names say exactly what happens: :cpp:enumerator:`MCPWM_GEN_ACTION_HIGH <mcpwm_generator_action_t::MCPWM_GEN_ACTION_HIGH>`, :cpp:enumerator:`MCPWM_GEN_ACTION_LOW <mcpwm_generator_action_t::MCPWM_GEN_ACTION_LOW>`, or :cpp:enumerator:`MCPWM_GEN_ACTION_TOGGLE <mcpwm_generator_action_t::MCPWM_GEN_ACTION_TOGGLE>` at a particular event. The helper macros make the three important choices visible at the call site — direction, event source, and output level.
|
||||
|
||||
For an up-counting timer, :cpp:enumerator:`MCPWM_TIMER_EVENT_EMPTY <mcpwm_timer_event_t::MCPWM_TIMER_EVENT_EMPTY>` is the zero boundary and :cpp:enumerator:`MCPWM_TIMER_EVENT_FULL <mcpwm_timer_event_t::MCPWM_TIMER_EVENT_FULL>` fires at the timer peak. In up-counting mode the peak equals the period, so ``FULL`` lands on the period boundary; in up-down mode the peak is ``period_ticks / 2``, so ``FULL`` lands in the middle of the cycle. A compare action uses the comparator's threshold. The first example therefore means "start the cycle high; end the active part when the count reaches 15." A compare value outside the timer range never produces its event.
|
||||
|
||||
Every action must specify a timer direction, even though the choice only makes a visible difference in up-down mode. In up-counting mode the counter only runs upward, so the action configured for :cpp:enumerator:`MCPWM_TIMER_DIRECTION_UP <mcpwm_timer_direction_t::MCPWM_TIMER_DIRECTION_UP>` is the one that fires — you still have to write it explicitly. In up-down mode, configure actions for both :cpp:enumerator:`MCPWM_TIMER_DIRECTION_UP <mcpwm_timer_direction_t::MCPWM_TIMER_DIRECTION_UP>` and :cpp:enumerator:`MCPWM_TIMER_DIRECTION_DOWN <mcpwm_timer_direction_t::MCPWM_TIMER_DIRECTION_DOWN>` when both edges matter. This is what turns one comparator into a center-aligned PWM.
|
||||
|
||||
Classic Waveform Examples
|
||||
=========================
|
||||
|
||||
The examples below build on the first PWM output from the previous section, reusing the same timer, operator, and comparator objects to create other common waveforms.
|
||||
|
||||
Invert the active polarity
|
||||
--------------------------
|
||||
|
||||
Some gate drivers and LEDs are active low, such as a low-active gate-driver enable, an inverted LED path, or a board-level interface that is already inverted. Instead of adding GPIO inversion, set low at the period boundary and high at the comparator:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_timer_event(
|
||||
generator, MCPWM_GEN_TIMER_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_LOW)));
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
generator, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, comparator, MCPWM_GEN_ACTION_HIGH)));
|
||||
|
||||
.. figure:: /../_static/mcpwm/single_edge_asym_active_low.svg
|
||||
:align: center
|
||||
:alt: Up-counting, active-low PWM. Change the actions, rather than the wiring, when the output polarity is part of the design.
|
||||
|
||||
Up-counting, active-low PWM. Change the actions, rather than the wiring, when the output polarity is part of the design.
|
||||
|
||||
Place a pulse inside the period
|
||||
-------------------------------
|
||||
|
||||
When a short pulse must sit at a controlled position inside the cycle — an ADC sample window, a peripheral trigger pulse, or a latch strobe — two compare values choose its opening and closing edges:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
generator, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, comparator_a, 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_b, MCPWM_GEN_ACTION_LOW)));
|
||||
|
||||
.. figure:: /../_static/mcpwm/pulse_placement_asym.svg
|
||||
:align: center
|
||||
:alt: Pulse placement: the distance between the two compare values is the pulse width.
|
||||
|
||||
Pulse placement: the distance between the two compare values is the pulse width.
|
||||
|
||||
Set ``comparator_a`` below ``comparator_b``. Moving both by the same tick offset changes the position without changing width; moving only one changes width. Hardware places both edges, so this is more precise than a timer callback.
|
||||
|
||||
Two-edge asymmetric PWM
|
||||
-----------------------
|
||||
|
||||
When several edges must be placed independently within one cycle and the active interval does not need to stay symmetric around the center — for example in certain asymmetric inverter modulation or custom gate-drive timing — use two generators and two comparators. Each generator has its own edge per cycle, so the high time splits across the period boundary:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
gen_a, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, cmp_a, MCPWM_GEN_ACTION_HIGH)));
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
gen_a, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, cmp_b, MCPWM_GEN_ACTION_LOW)));
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
gen_b, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, cmp_a, MCPWM_GEN_ACTION_LOW)));
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
gen_b, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, cmp_b, MCPWM_GEN_ACTION_HIGH)));
|
||||
|
||||
.. figure:: /../_static/mcpwm/dual_edge_asym_active_low.svg
|
||||
:align: center
|
||||
:alt: Dual-edge asymmetric (edge-aligned) PWM: two generators produce complementary outputs with two edges per cycle.
|
||||
|
||||
Dual-edge asymmetric (edge-aligned) PWM: two generators produce complementary outputs with two edges per cycle.
|
||||
|
||||
Center-aligned PWM
|
||||
------------------
|
||||
|
||||
Motor drives, inverters, and power stages that care about harmonic behavior often prefer center-aligned PWM because it gives more symmetric switching and lower harmonic distortion. Select ``MCPWM_TIMER_COUNT_MODE_UP_DOWN`` when creating the timer, then use the same threshold in both directions:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
generator, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, comparator, MCPWM_GEN_ACTION_HIGH)));
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_compare_event(
|
||||
generator, MCPWM_GEN_COMPARE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_DOWN, comparator, MCPWM_GEN_ACTION_LOW)));
|
||||
|
||||
.. figure:: /../_static/mcpwm/dual_edge_sym_active_low.svg
|
||||
:align: center
|
||||
:alt: Center-aligned PWM: the up-count and down-count actions create matching edges around the period center.
|
||||
|
||||
Center-aligned PWM: the up-count and down-count actions create matching edges around the period center.
|
||||
|
||||
The timer reaches its peak and returns to zero in each complete up-down cycle, so account for both legs when calculating frequency. Adding a second generator with opposite actions creates complementary *logical* outputs:
|
||||
|
||||
.. figure:: /../_static/mcpwm/dual_edge_sym_complementary.svg
|
||||
:align: center
|
||||
:alt: Complementary generator actions have no dead time by themselves; do not connect them directly to a power stage.
|
||||
|
||||
Complementary generator actions have no dead time by themselves; do not connect them directly to a power stage.
|
||||
|
||||
.. warning::
|
||||
|
||||
Logical complementary outputs are not yet safe half-bridge outputs. If the high-side and low-side devices have finite turn-off delay, add dead time and verify non-overlap at the actual gate pins.
|
||||
|
||||
Duty updates
|
||||
============
|
||||
|
||||
Change duty by setting the comparator threshold, not the generator actions. The threshold is expressed in timer ticks: for an up-counting active-high waveform, ``compare_value / period_ticks`` is the duty ratio. Choose a timer resolution high enough that one tick gives the adjustment granularity the application needs.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_comparator_set_compare_value(comparator, 25); // 50 %
|
||||
|
||||
Forced levels
|
||||
=============
|
||||
|
||||
Adjusting the comparator threshold changes the normal duty. When you instead need to temporarily take over the output, ignore all event actions, and hold a fixed level, use the force-level API. :func:`mcpwm_generator_set_force_level` takes a ``level`` and a ``hold_on`` flag:
|
||||
|
||||
- ``level`` (second parameter) is the raw generator level to force: ``0`` or ``1`` overrides all event actions, while ``-1`` releases the force and returns control to event actions.
|
||||
- ``hold_on`` (third parameter) decides how long the forced level lasts: ``true`` holds it until another call releases it, whereas ``false`` lets the next event action override it.
|
||||
|
||||
For example, ``mcpwm_generator_set_force_level(generator, 0, true)`` overrides all event actions and holds the raw generator low. Force acts before dead time and GPIO inversion, so confirm the physical pin level with a scope when using those features.
|
||||
|
||||
Force level is useful for power-up checks, a temporary post-fault safe output, or a mode-transition state. It is not a long-term replacement for a proper PWM configuration.
|
||||
|
||||
For a half bridge, add a second generator and configure the :ref:`dead-time module <mcpwm-dead-time>` to produce non-overlapping complementary outputs.
|
||||
|
||||
.. _mcpwm-dead-time:
|
||||
|
||||
Dead time and half-bridge drive
|
||||
===============================
|
||||
|
||||
Dead time delays an output edge, leaving a short interval in which both switches in a half bridge are off. It compensates for transistor turn-off delay and helps prevent shoot-through. Configure and verify it before connecting a power stage.
|
||||
|
||||
A half bridge drives a load from a DC bus through a high-side and a low-side switch. Both switches are usually N-channel MOSFETs: the low-side source sits at ground and is easy to drive, while the high-side source swings with the output, so its gate needs a drive voltage above the bus voltage. The MCPWM outputs are 3.3 V logic and cannot drive the gates directly. For example, an IRS2101 uses a separate low-voltage driver supply (VCC, typically 10-20 V), with COM connected to power ground. Its bootstrap diode should be connected from VCC to VB, and the external bootstrap capacitor between VB and VS; VS must be connected to the OUT switch node. The high-side output is HO and the low-side output is LO, and both drive their MOSFET gates through gate resistors. VCC is not the high-voltage DC bus: the bootstrap diode charges the bootstrap capacitor from the regulated driver supply while the low-side switch is on. If the two switches were turned on and off simultaneously, the switch that is still turning off would overlap the one already turning on, shorting the bus to ground through both switches. Dead time inserts a both-off gap so the next switch turns on only after the previous one has fully turned off:
|
||||
|
||||
.. figure:: /../_static/mcpwm/half_bridge_dead_time.svg
|
||||
:align: center
|
||||
:alt: Half-bridge gate-driver circuit with bootstrap supply and dead-time comparison.
|
||||
|
||||
Half-bridge gate-driver circuit with bootstrap supply and dead-time comparison.
|
||||
|
||||
Create complementary outputs
|
||||
----------------------------
|
||||
|
||||
Create two generators in one operator. Feed generator A into its own output with a rising-edge delay, then feed it into generator B with a falling-edge delay and inversion.
|
||||
|
||||
.. note::
|
||||
|
||||
Here, generator A is the first generator allocated from the operator handle, and generator B is the second.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_dead_time_config_t dead_time = { .posedge_delay_ticks = 2 };
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gen_a, gen_a, &dead_time));
|
||||
dead_time = (mcpwm_dead_time_config_t) {
|
||||
.negedge_delay_ticks = 2,
|
||||
.flags.invert_output = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gen_a, gen_b, &dead_time));
|
||||
|
||||
.. figure:: /../_static/mcpwm/deadtime_active_high_complementary.svg
|
||||
:align: center
|
||||
:alt: Complementary PWM with a dead-time interval between the switch transitions.
|
||||
|
||||
Complementary PWM with a dead-time interval between the switch transitions.
|
||||
|
||||
Understanding the routing and parameters
|
||||
----------------------------------------
|
||||
|
||||
:func:`mcpwm_generator_set_dead_time(in_generator, out_generator, config) <mcpwm_generator_set_dead_time>` treats dead time as a small signal-processing stage. Passing the same generator for both handles changes that output in place. Passing ``gen_a`` as input and ``gen_b`` as output derives B from A, which is how the complementary example shares one PWM source.
|
||||
|
||||
:cpp:member:`posedge_delay_ticks <mcpwm_dead_time_config_t::posedge_delay_ticks>` delays a rising edge and :cpp:member:`negedge_delay_ticks <mcpwm_dead_time_config_t::negedge_delay_ticks>` delays a falling edge. Ticks use the connected timer's resolution, so a 2-tick setting at 10 MHz is 200 ns. The diagram below shows the basic effect: the rising edge of ``pwm_A`` is delayed (RED) and the falling edge of ``pwm_B`` is delayed (FED) relative to the original signal. Start with the maximum turn-off delay from the switch and gate-driver data sheets plus margin; then measure at the transistor gates and reduce it only after confirming that process, temperature, and layout still leave enough margin. Set both delays to zero to bypass the dead-time stage. :cpp:member:`invert_output <mcpwm_dead_time_config_t::flags::invert_output>` changes polarity after that stage.
|
||||
|
||||
.. figure:: /../_static/mcpwm/deadtime_active_high.svg
|
||||
:align: center
|
||||
:alt: Basic dead-time effect: rising edge delayed (RED) and falling edge delayed (FED) relative to the original.
|
||||
|
||||
Basic dead-time effect: rising edge delayed (RED) and falling edge delayed (FED) relative to the original.
|
||||
|
||||
Resource limits per operator
|
||||
----------------------------
|
||||
|
||||
The hardware has one rising-edge and one falling-edge delay resource per operator, so do not assign the same delay type independently to both generators. The following requests the one rising-edge resource twice and is invalid:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_dead_time_config_t rise_delay = { .posedge_delay_ticks = 10 };
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gen_a, gen_a, &rise_delay));
|
||||
// This second independent rising-edge delay cannot be allocated.
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gen_b, gen_b, &rise_delay));
|
||||
|
||||
You may assign the rising delay to A and the falling delay to B. You may also use both delay resources for B while A bypasses the module. If the first generator uses both delay resources, the other generator cannot use dead time.
|
||||
|
||||
More output patterns
|
||||
--------------------
|
||||
|
||||
The complementary configuration above is the usual half-bridge starting point. Swap the output inversions to make both outputs active low while retaining the non-overlap:
|
||||
|
||||
.. figure:: /../_static/mcpwm/deadtime_active_low_complementary.svg
|
||||
:align: center
|
||||
:alt: Active-low complementary outputs. The timing resources are the same; only the post-dead-time polarity changes.
|
||||
|
||||
Active-low complementary outputs. The timing resources are the same; only the post-dead-time polarity changes.
|
||||
|
||||
Dead time is also useful when only one channel needs an edge delay. Keep one output bypassed by passing a zero-delay configuration, and apply the available delay to the other:
|
||||
|
||||
.. figure:: /../_static/mcpwm/deadtime_reda_bypassb.svg
|
||||
:align: center
|
||||
:alt: Delay A's rising edge while B bypasses dead time. This is not a complementary half bridge; it is an independent edge-placement tool.
|
||||
|
||||
Delay A's rising edge while B bypasses dead time. This is not a complementary half bridge; it is an independent edge-placement tool.
|
||||
|
||||
.. figure:: /../_static/mcpwm/deadtime_redb_fedb_bypassa.svg
|
||||
:align: center
|
||||
:alt: Bypass A and delay both edges of B, consuming both delay resources.
|
||||
|
||||
Bypass A and delay both edges of B, consuming both delay resources.
|
||||
|
||||
A single-edge delay can also be applied individually. The next diagram shows the falling edge delayed on B while A is bypassed, using only the FED resource:
|
||||
|
||||
.. figure:: /../_static/mcpwm/deadtime_fedb_bypassa.svg
|
||||
:align: center
|
||||
:alt: Apply only the falling-edge delay to B, leaving A unchanged. This uses one delay resource.
|
||||
|
||||
Apply only the falling-edge delay to B, leaving A unchanged. This uses one delay resource.
|
||||
|
||||
When the output is inverted, the dead-time behavior shifts accordingly. The following shows the active-low version of the basic delay, where the invert flag flips the polarity of both outputs:
|
||||
|
||||
.. figure:: /../_static/mcpwm/deadtime_active_low.svg
|
||||
:align: center
|
||||
:alt: Active-low dead time: same delay resources, but the output polarity is inverted after the delay stage.
|
||||
|
||||
Active-low dead time: same delay resources, but the output polarity is inverted after the delay stage.
|
||||
|
||||
.. note::
|
||||
|
||||
For a waveform where each edge must have an independently movable position, use two comparators and generator actions instead. The dead-time module is the better choice when the requirement is specifically a delayed edge plus polarity control.
|
||||
|
||||
Update at a safe boundary
|
||||
-------------------------
|
||||
|
||||
Set the operator's :cpp:member:`update_dead_time_on_tez <mcpwm_operator_config_t::flags::update_dead_time_on_tez>`, :cpp:member:`update_dead_time_on_tep <mcpwm_operator_config_t::flags::update_dead_time_on_tep>`, or :cpp:member:`update_dead_time_on_sync <mcpwm_operator_config_t::flags::update_dead_time_on_sync>` flag when a changed dead-time value must take effect only at a known boundary.
|
||||
|
||||
.. note::
|
||||
|
||||
Probe both physical gate pins: GPIO inversion, carrier modulation, and gate-driver polarity can all alter what appears at the transistor. When several stages invert the signal, two inversions can cancel out and look correct in software while the hardware does something unexpected, so always verify against the real waveform.
|
||||
|
||||
Other event sources
|
||||
===================
|
||||
|
||||
Generator actions can also react directly to GPIO fault events or a sync event:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:func:`mcpwm_generator_set_action_on_fault_event()` — immediate hardware reaction to a GPIO fault. Uses limited operator trigger slots.
|
||||
- :cpp:func:`mcpwm_generator_set_action_on_sync_event()` — transition at a synchronization edge. Each generator has one sync-action slot.
|
||||
- :cpp:func:`mcpwm_generator_set_action_on_brake_event()` — per-generator output state during an operator :ref:`brake <mcpwm-brake>`. It is set per brake mode and timer direction; see :ref:`Fault connection <mcpwm-brake-fault-connection>` for a full example.
|
||||
|
||||
For safety policy and persistent braking, prefer the operator :ref:`brake mechanism <mcpwm-brake>`. A generator fault action is best for a local edge-level response; a brake defines the safe state and recovery behavior for the whole output stage.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM Generator Driver Functions
|
||||
---------------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_gen.inc
|
||||
@@ -0,0 +1,187 @@
|
||||
=========================================
|
||||
MCPWM Operator: Assemble an Output Stage
|
||||
=========================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
An operator is the container between a timer and its generators. It owns the comparators, generator actions, brake handling, dead-time routing, and carrier modulation. One timer can drive multiple operators in the same group, while an operator connects to exactly one timer.
|
||||
|
||||
If the timer is the clock source, the operator is the output-stage container. It lets several outputs share one time base while keeping protection, dead time, and carrier features grouped with the power stage they belong to.
|
||||
|
||||
Connect the building blocks
|
||||
===========================
|
||||
|
||||
Create the operator in the same group as the timer, then connect them with :cpp:func:`mcpwm_operator_connect_timer()`. The connection must exist before the generator can use timer events.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_oper_handle_t oper = NULL;
|
||||
mcpwm_operator_config_t oper_config = {
|
||||
.group_id = 0,
|
||||
.flags.update_gen_action_on_tez = true,
|
||||
.flags.update_dead_time_on_tez = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_new_operator(&oper_config, &oper));
|
||||
ESP_ERROR_CHECK(mcpwm_operator_connect_timer(oper, timer));
|
||||
|
||||
The operator configuration is small, but a few fields do not appear in the example:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`group_id <mcpwm_operator_config_t::group_id>` — the MCPWM group the operator is allocated from. It must match the timer's group, because an operator can only connect to a timer inside the same group.
|
||||
- :cpp:member:`intr_priority <mcpwm_operator_config_t::intr_priority>` — the interrupt priority used by the brake event callbacks. Not setting it (``0``) lets the driver choose a low priority; raise it when brake notifications must preempt other ISRs.
|
||||
|
||||
The ``flags`` choose when new generator actions and dead-time settings take effect. They are all off by default, so changes apply immediately — possibly in the middle of a PWM cycle:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`update_gen_action_on_tez <mcpwm_operator_config_t::flags::update_gen_action_on_tez>`, :cpp:member:`update_gen_action_on_tep <mcpwm_operator_config_t::flags::update_gen_action_on_tep>`, and :cpp:member:`update_gen_action_on_sync <mcpwm_operator_config_t::flags::update_gen_action_on_sync>` — buffer generator action changes until the counter reaches zero, the peak, or a sync event.
|
||||
- :cpp:member:`update_dead_time_on_tez <mcpwm_operator_config_t::flags::update_dead_time_on_tez>`, :cpp:member:`update_dead_time_on_tep <mcpwm_operator_config_t::flags::update_dead_time_on_tep>`, and :cpp:member:`update_dead_time_on_sync <mcpwm_operator_config_t::flags::update_dead_time_on_sync>` — buffer dead-time changes the same way; see :ref:`dead time <mcpwm-dead-time>` for the update-point rules.
|
||||
|
||||
For a running power stage, use the zero (``tez``), peak (``tep``), or sync update point to avoid partial cycles.
|
||||
|
||||
One timer, multiple operators
|
||||
=============================
|
||||
|
||||
The same timer can drive several operators, each producing a different waveform. This is useful for multi-phase inverters or multiple motors running at the same frequency but with independent duty cycles.
|
||||
|
||||
The reverse is also important: one operator connects to exactly one timer, so all comparators and generators inside that operator inherently share the same time base. That is why in-phase, complementary, and paired outputs are easy to build there.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_oper_handle_t oper_b = NULL;
|
||||
mcpwm_operator_config_t oper_config_b = {
|
||||
.group_id = 0,
|
||||
.flags.update_gen_action_on_tez = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_new_operator(&oper_config_b, &oper_b));
|
||||
ESP_ERROR_CHECK(mcpwm_operator_connect_timer(oper_b, timer));
|
||||
// Create separate comparators and generators under oper_b.
|
||||
|
||||
.. _mcpwm-brake:
|
||||
|
||||
Brake and safe output state
|
||||
===========================
|
||||
|
||||
The operator turns a :doc:`fault <mcpwm_fault>` into a brake action. Arrange brake actions before starting PWM: this makes the reaction entirely hardware driven and avoids software latency in the fault path.
|
||||
|
||||
Recovery policy
|
||||
---------------
|
||||
|
||||
.. list::
|
||||
|
||||
- **CBC (cycle by cycle):** brakes while the fault is active and recovers at the configured timer zero or peak. This suits a transient current limit.
|
||||
- **OST (one shot):** stays braked after the fault disappears. Software must explicitly recover it. Use it for an interlock or serious over-current condition.
|
||||
|
||||
For CBC, set :cpp:member:`cbc_recover_on_tez <mcpwm_brake_config_t::flags::cbc_recover_on_tez>` or :cpp:member:`cbc_recover_on_tep <mcpwm_brake_config_t::flags::cbc_recover_on_tep>` to choose the boundary at which a cleared fault releases the outputs. A boundary avoids restoring a switch in the middle of a PWM cycle.
|
||||
|
||||
.. warning::
|
||||
|
||||
Do not enable both ``cbc_recover_on_tez`` and ``cbc_recover_on_tep`` at the same time; choose the boundary that matches the waveform and gate-driver timing.
|
||||
|
||||
.. _mcpwm-brake-fault-connection:
|
||||
|
||||
Fault connection
|
||||
----------------
|
||||
|
||||
Connect the fault to the operator, then specify the state of every generator during that brake mode. This example drives the raw generator low in both timer directions for OST braking:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_operator_set_brake_on_fault(oper,
|
||||
&(mcpwm_brake_config_t) {
|
||||
.fault = fault,
|
||||
.brake_mode = MCPWM_OPER_BRAKE_MODE_OST,
|
||||
}));
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_brake_event(
|
||||
generator, MCPWM_GEN_BRAKE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_UP, MCPWM_OPER_BRAKE_MODE_OST,
|
||||
MCPWM_GEN_ACTION_LOW)));
|
||||
ESP_ERROR_CHECK(mcpwm_generator_set_action_on_brake_event(
|
||||
generator, MCPWM_GEN_BRAKE_EVENT_ACTION(
|
||||
MCPWM_TIMER_DIRECTION_DOWN, MCPWM_OPER_BRAKE_MODE_OST,
|
||||
MCPWM_GEN_ACTION_LOW)));
|
||||
|
||||
For a bridge, configure both generators with the same brake action. Confirm the electrical safe state at the gate driver; a logical low can be inverted by dead time, GPIO matrix, or external circuitry.
|
||||
|
||||
The distinction from a generator fault action is important: generator fault actions are best for a local immediate edge response, while operator brake defines the safe state, latch behavior, and recovery policy for the entire output stage. The primary protection path should usually use operator brake.
|
||||
|
||||
OST fault recovery
|
||||
------------------
|
||||
|
||||
CBC recovers on its configured boundary after the fault goes inactive. For OST, remove and validate the root cause first, then call:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_operator_recover_from_fault(oper, fault));
|
||||
|
||||
The call fails while the source is still active.
|
||||
|
||||
.. figure:: /../_static/mcpwm/brake_cbc_ost.svg
|
||||
:align: center
|
||||
:alt: A fault asserts while PWM runs. CBC holds the output at the brake level while the fault is active and resumes at the next cycle boundary; OST stays latched until software recovery.
|
||||
|
||||
CBC brakes only while the fault is active and recovers at the next cycle boundary; OST stays latched until software recovery.
|
||||
|
||||
Brake event callbacks
|
||||
---------------------
|
||||
|
||||
The operator can report brake events through the :cpp:member:`on_brake_cbc <mcpwm_operator_event_callbacks_t::on_brake_cbc>` and :cpp:member:`on_brake_ost <mcpwm_operator_event_callbacks_t::on_brake_ost>` callbacks. They run in ISR context; use them for notification, not blocking recovery.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_operator_event_callbacks_t cbs = {
|
||||
.on_brake_cbc = my_brake_cbc_cb,
|
||||
.on_brake_ost = my_brake_ost_cb,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_operator_register_event_callbacks(oper, &cbs, NULL));
|
||||
|
||||
.. _mcpwm-carrier:
|
||||
|
||||
Carrier modulation
|
||||
==================
|
||||
|
||||
Carrier modulation superimposes a high-frequency carrier on an operator's PWM output. It is commonly used with transformer-isolated gate-drive schemes: even a base PWM held at 100% duty then contains transitions that can cross the isolation barrier. Configure the base PWM first; carrier settings affect the operator's all generators.
|
||||
|
||||
Carrier configuration
|
||||
---------------------
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_carrier_config_t carrier = {
|
||||
.clk_src = MCPWM_CARRIER_CLK_SRC_DEFAULT,
|
||||
.frequency_hz = 100000,
|
||||
.duty_cycle = 0.5f,
|
||||
.first_pulse_duration_us = 20,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_operator_apply_carrier(oper, &carrier));
|
||||
|
||||
.. figure:: /../_static/mcpwm/carrier_modulation.svg
|
||||
:align: center
|
||||
:alt: Carrier modulation of a 50% duty base PWM
|
||||
|
||||
A 100 kHz carrier gates a 50% duty base PWM. The first pulse is stretched to 20 us (two carrier periods), and no chopping occurs while the base PWM is low.
|
||||
|
||||
Carrier parameters
|
||||
------------------
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`clk_src <mcpwm_carrier_config_t::clk_src>` selects the carrier clock source. It defaults to an internal PLL clock (e.g. PLL_F160M); some chips also expose RC_FAST or XTAL as alternatives. Different sources offer different resolution and power consumption. The default is fine for most applications; pick a different source only to avoid noise from a particular clock, when PLL precision is insufficient, or when power consumption matters.
|
||||
- :cpp:member:`frequency_hz <mcpwm_carrier_config_t::frequency_hz>` is the carrier frequency; select a value compatible with the isolation transformer, gate driver, switching loss budget, and target clock resolution.
|
||||
- :cpp:member:`duty_cycle <mcpwm_carrier_config_t::duty_cycle>` accepts the hardware steps 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, or 0.875, rather than an arbitrary ratio.
|
||||
- :cpp:member:`first_pulse_duration_us <mcpwm_carrier_config_t::first_pulse_duration_us>` controls the first pulse after modulation begins. It must be nonzero and at least one carrier period. A longer first pulse can help establish current in an inductive isolation path, but must stay within the gate-drive system's limits.
|
||||
- Use :cpp:member:`invert_before_modulate <mcpwm_carrier_config_t::flags::invert_before_modulate>` when the raw PWM needs a polarity change and :cpp:member:`invert_after_modulate <mcpwm_carrier_config_t::flags::invert_after_modulate>` when the modulated output needs one.
|
||||
|
||||
Pass ``NULL`` as the configuration to :cpp:func:`mcpwm_operator_apply_carrier` when carrier modulation is not needed.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM Operator Driver Functions
|
||||
-------------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_oper.inc
|
||||
@@ -0,0 +1,157 @@
|
||||
========================================
|
||||
MCPWM Synchronization: Align PWM Phases
|
||||
========================================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
Why sync is needed
|
||||
==================
|
||||
|
||||
Each MCPWM timer is an independent hardware counter. When you call :cpp:func:`mcpwm_timer_start()` on two timers, the two writes are issued sequentially by the CPU — the second timer starts a few dozen CPU cycles after the first. Even if both are configured with the same period, their counters will be at different positions relative to the cycle, and the phase relationship between their PWM outputs is unpredictable.
|
||||
|
||||
Synchronization solves this by loading a chosen count and direction into a **running** timer when a sync edge arrives. The timers must already be running; sync does not start or stop them. It is a runtime phase correction mechanism.
|
||||
|
||||
If the sync edge arrives every period (for example, from a timer sync source at TEZ), the correction repeats each cycle, keeping the phase locked indefinitely. This is the typical use case: one timer acts as the reference, and other timers re-align to it on every period.
|
||||
|
||||
MCPWM provides three types of sync sources. All sources produce a handle of type :cpp:type:`mcpwm_sync_handle_t`, and any source can feed any timer in the same group.
|
||||
|
||||
GPIO sync source
|
||||
================
|
||||
|
||||
A GPIO sync source reacts to an edge on an external pin — useful when an external controller, sensor, or encoder provides a periodic reference.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_sync_handle_t sync = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_new_gpio_sync_src(
|
||||
&(mcpwm_gpio_sync_src_config_t) {
|
||||
.group_id = 0,
|
||||
.gpio_num = 5,
|
||||
.flags.active_neg = false,
|
||||
}, &sync));
|
||||
|
||||
The GPIO sync source configuration is small:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`group_id <mcpwm_gpio_sync_src_config_t::group_id>` — the MCPWM group the source belongs to. It must match the group of every timer that receives this sync.
|
||||
- :cpp:member:`gpio_num <mcpwm_gpio_sync_src_config_t::gpio_num>` — the GPIO carrying the sync signal.
|
||||
- :cpp:member:`active_neg <mcpwm_gpio_sync_src_config_t::flags::active_neg>` — by default the rising edge is the active edge; set it to treat the falling edge as active instead.
|
||||
|
||||
Software sync source
|
||||
====================
|
||||
|
||||
A software sync source produces a sync edge on demand from application code. It has no configuration fields; create it and activate it when needed.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_sync_handle_t soft_sync = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_new_soft_sync_src(NULL, &soft_sync));
|
||||
|
||||
// later, when the application decides to synchronize:
|
||||
ESP_ERROR_CHECK(mcpwm_soft_sync_activate(soft_sync));
|
||||
|
||||
.. note::
|
||||
|
||||
Activate the soft sync only after binding it to a timer via :cpp:func:`mcpwm_timer_set_phase_on_sync()` or :cpp:func:`mcpwm_capture_timer_set_phase_on_sync()`. The driver does not assign a timer at creation time; calling :cpp:func:`mcpwm_soft_sync_activate()` before binding is undefined behavior.
|
||||
|
||||
This is useful when timers are already running and the application needs to trigger a one-time phase correction — for example, after recovering from a fault, or before starting a new control cycle. Because the sync is one-shot, the phase relationship will drift over time if no further sync edges arrive. For sustained phase lock, use a periodic source (GPIO or timer sync).
|
||||
|
||||
Timer sync source
|
||||
=================
|
||||
|
||||
A timer sync source emits a sync edge when the timer reaches a chosen event — for example, every time the timer hits zero (TEZ). This lets one timer act as a periodic reference for other timers, keeping their phase locked every cycle.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_sync_handle_t timer_sync = NULL;
|
||||
ESP_ERROR_CHECK(mcpwm_new_timer_sync_src(
|
||||
timer_a,
|
||||
&(mcpwm_timer_sync_src_config_t) {
|
||||
.timer_event = MCPWM_TIMER_EVENT_EMPTY,
|
||||
},
|
||||
&timer_sync));
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`timer_event <mcpwm_timer_sync_src_config_t::timer_event>` — the timer event that triggers the sync output. Common choices are :cpp:enumerator:`MCPWM_TIMER_EVENT_EMPTY` (zero) for the start of each period or :cpp:enumerator:`MCPWM_TIMER_EVENT_PEAK` for the peak value. In up-counting mode the peak is the period boundary; in up-down mode the peak is the midpoint of the period.
|
||||
- :cpp:member:`propagate_input_sync <mcpwm_timer_sync_src_config_t::flags::propagate_input_sync>` — when set, the timer forwards its own received input sync to its sync output, enabling a chain of timers without extra GPIO wiring. In this mode the hardware selects the input sync as the output source, so the :cpp:member:`timer_event` field is ignored.
|
||||
|
||||
A timer can create at most one sync source. Multiple timers can receive the same sync source.
|
||||
|
||||
Because the timer sync source fires every period, the receiving timer gets corrected on every cycle. This is the most common way to maintain a stable phase relationship between multiple PWM channels.
|
||||
|
||||
Set the receiving phase
|
||||
=======================
|
||||
|
||||
No matter which source type you chose, the receiving timer uses the same API. Call :cpp:func:`mcpwm_timer_set_phase_on_sync()` to configure what happens when the sync edge arrives. The timer must already be running for the sync to take effect.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_timer_set_phase_on_sync(timer,
|
||||
&(mcpwm_timer_sync_phase_config_t) {
|
||||
.sync_src = sync,
|
||||
.count_value = 25,
|
||||
.direction = MCPWM_TIMER_DIRECTION_UP,
|
||||
}));
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`sync_src <mcpwm_timer_sync_phase_config_t::sync_src>` — the source object. Set it to ``NULL`` to detach synchronization.
|
||||
- :cpp:member:`count_value <mcpwm_timer_sync_phase_config_t::count_value>` — the count loaded when the sync event arrives. Keep it within the timer period.
|
||||
- :cpp:member:`direction <mcpwm_timer_sync_phase_config_t::direction>` — the counting direction after loading.
|
||||
|
||||
Two outputs with a 90-degree phase shift
|
||||
========================================
|
||||
|
||||
Now that you know all three source types and how to set the receiving phase, here is a complete example. It uses a timer sync source: ``timer_a`` emits a sync every time it reaches zero, and ``timer_b`` receives that sync and loads ``count_value = 25``, producing a 90-degree phase lag. Because the sync repeats each period, the phase relationship between the two channels is maintained indefinitely.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_timer_handle_t timer_a = NULL;
|
||||
mcpwm_timer_handle_t timer_b = NULL;
|
||||
mcpwm_sync_handle_t timer_a_sync = NULL;
|
||||
|
||||
// timer_a and timer_b already exist, both with period_ticks = 100
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_new_timer_sync_src(
|
||||
timer_a,
|
||||
&(mcpwm_timer_sync_src_config_t) {
|
||||
.timer_event = MCPWM_TIMER_EVENT_EMPTY,
|
||||
},
|
||||
&timer_a_sync));
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_timer_set_phase_on_sync(timer_b,
|
||||
&(mcpwm_timer_sync_phase_config_t) {
|
||||
.sync_src = timer_a_sync,
|
||||
.count_value = 25,
|
||||
.direction = MCPWM_TIMER_DIRECTION_UP,
|
||||
}));
|
||||
|
||||
// timer_a emits sync at TEZ; timer_b receives it and continues from tick 25.
|
||||
|
||||
Understand lead and lag
|
||||
-----------------------
|
||||
|
||||
In the sketch below, ``PWM_A`` starts its cycle first and ``PWM_B`` appears one quarter cycle later. That means ``PWM_B`` lags ``PWM_A`` by 90 degrees; equivalently, ``PWM_A`` leads ``PWM_B`` by 90 degrees.
|
||||
|
||||
.. figure:: /../_static/mcpwm/phase_shift.svg
|
||||
:align: center
|
||||
:alt: PWM phase shift 90 degree lag
|
||||
|
||||
PWM_A and PWM_B with a 90-degree phase shift: PWM_B starts 25 ticks after PWM_A.
|
||||
|
||||
Other considerations
|
||||
====================
|
||||
|
||||
The capture timer can use the same source through :cpp:func:`mcpwm_capture_timer_set_phase_on_sync()`; capture always counts up. The receiver and source must remain in the same group. Delete a source only after detaching or deleting every object that uses it.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM Synchronization Driver Functions
|
||||
--------------------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_sync.inc
|
||||
@@ -0,0 +1,146 @@
|
||||
===============================
|
||||
MCPWM Timer: Set the Frequency
|
||||
===============================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
The timer is the time base for every PWM output attached to its operator. It counts ticks at :cpp:member:`resolution_hz <mcpwm_timer_config_t::resolution_hz>` and wraps around at :cpp:member:`period_ticks <mcpwm_timer_config_t::period_ticks>`. Choose the resolution first — it determines the finest step of edge placement — then choose the period for the target frequency.
|
||||
|
||||
For a servo, speed loop, or inverter, the timer answers the two most basic questions: how fine is one tick, and how long is one PWM cycle. Comparators and generators only place edges on top of that time base.
|
||||
|
||||
Build a 20 kHz time base
|
||||
=========================
|
||||
|
||||
For an up-counting timer, ``period_ticks = resolution_hz / frequency_hz``. The following timer has a 1 MHz tick (one microsecond per tick) and a 50-tick period, giving 20 kHz. The diagram below shows the counter climbing from 0 to 50, then resetting — the ``TEZ`` (timer event zero) and ``TEP`` (timer event peak) markers are the two boundaries that generators use.
|
||||
|
||||
.. figure:: /../_static/mcpwm/timer_up_count.svg
|
||||
:align: center
|
||||
:alt: Up-counting timer: the counter forms a sawtooth, rising from 0 to 50, firing TEZ at zero and TEP at peak.
|
||||
|
||||
Up-counting timer: the counter forms a sawtooth, rising from 0 to 50, firing TEZ at zero and TEP at peak.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
mcpwm_timer_handle_t timer = NULL;
|
||||
mcpwm_timer_config_t timer_config = {
|
||||
.group_id = 0,
|
||||
.clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 1000000,
|
||||
.period_ticks = 50,
|
||||
.count_mode = MCPWM_TIMER_COUNT_MODE_UP,
|
||||
};
|
||||
ESP_ERROR_CHECK(mcpwm_new_timer(&timer_config, &timer));
|
||||
|
||||
The timer configuration is worth reading field by field, because a few important knobs are not shown in the example:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`group_id <mcpwm_timer_config_t::group_id>` — the MCPWM group the timer is allocated from. Chips may expose more than one groups; each group bundles timers, operators, comparators, and generators that share clock dividers. ``0`` selects the first group, which is enough for most designs.
|
||||
- :cpp:member:`clk_src <mcpwm_timer_config_t::clk_src>` — the clock that feeds the timer. :c:macro:`MCPWM_TIMER_CLK_SRC_DEFAULT` selects a PLL clock and is right for almost every application. On targets with extra sources, you can pick one explicitly — for example to keep the timer counting when the PLL is switched off, such as during light sleep.
|
||||
- :cpp:member:`resolution_hz <mcpwm_timer_config_t::resolution_hz>` — the tick rate of the counter. One tick lasts ``1 / resolution_hz`` seconds, so 1 MHz means one microsecond per tick. This sets the finest edge step available to the comparator.
|
||||
- :cpp:member:`period_ticks <mcpwm_timer_config_t::period_ticks>` — the length of one full PWM cycle in ticks. The frequency is ``resolution_hz / period_ticks``.
|
||||
- :cpp:member:`count_mode <mcpwm_timer_config_t::count_mode>` — whether the counter counts up only (edge-aligned PWM) or up and down (center-aligned PWM). See :ref:`Counting modes and waveforms <mcpwm-timer-counting-modes>` for the two shapes; the hardware also supports counting down.
|
||||
- :cpp:member:`intr_priority <mcpwm_timer_config_t::intr_priority>` — the interrupt priority used by the timer callbacks. Not setting it (``0``) lets the driver choose a low priority; raise it when a callback must preempt other ISRs, for example in tightly timed motor control.
|
||||
|
||||
The example does not touch :cpp:member:`flags <mcpwm_timer_config_t::flags>`, so all of them are off — which is the safe default. Two of them are worth knowing:
|
||||
|
||||
.. list::
|
||||
|
||||
- :cpp:member:`update_period_on_empty <mcpwm_timer_config_t::flags::update_period_on_empty>` and :cpp:member:`update_period_on_sync <mcpwm_timer_config_t::flags::update_period_on_sync>` — off by default, so :cpp:func:`mcpwm_timer_set_period()` takes effect immediately. Turn them on to defer frequency changes to a safe boundary; see :ref:`Safe frequency updates <mcpwm-timer-safe-update>`.
|
||||
- :cpp:member:`allow_pd <mcpwm_timer_config_t::flags::allow_pd>` — lets the MCPWM power domain switch off during sleep. The driver then backs up and restores the timer registers around the sleep transition, saving power at the cost of extra RAM.
|
||||
|
||||
Do not start the timer yet. First create and connect the operator, comparator, and generator (see the following pages), then enable and start:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
ESP_ERROR_CHECK(mcpwm_timer_enable(timer));
|
||||
ESP_ERROR_CHECK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP));
|
||||
|
||||
:cpp:func:`mcpwm_timer_enable()` activates the services the timer needs to run: it enables the timer interrupt and, with power management on, holds the group's power-management lock so clock scaling cannot disturb PWM timing. :cpp:func:`mcpwm_timer_start_stop()` then starts and later stops the counter. Call :cpp:func:`mcpwm_timer_disable()` to reverse the enable before freeing the timer with :cpp:func:`mcpwm_del_timer()`.
|
||||
|
||||
The third argument of :cpp:func:`mcpwm_timer_start_stop()` selects the stop behavior:
|
||||
|
||||
.. list::
|
||||
|
||||
- :c:macro:`MCPWM_TIMER_START_NO_STOP` — runs continuously until you explicitly stop it.
|
||||
- :c:macro:`MCPWM_TIMER_START_STOP_EMPTY` — stops automatically when the next count reaches zero (TEZ). Use this for a single-shot or synchronized start where the cycle should complete before stopping.
|
||||
- :c:macro:`MCPWM_TIMER_START_STOP_FULL` — stops automatically when the next count reaches the peak (TEP). Use this for a single cycle that ends at the period boundary.
|
||||
|
||||
.. _mcpwm-timer-counting-modes:
|
||||
|
||||
Counting modes and waveforms
|
||||
============================
|
||||
|
||||
In **up mode**, the counter counts from 0 to :cpp:member:`period_ticks <mcpwm_timer_config_t::period_ticks>` and resets. The waveform is a sawtooth and the PWM edges align to one side of the period — this is called *edge-aligned* PWM.
|
||||
|
||||
In **up-down mode**, the counter counts up to ``period_ticks / 2`` and then down to 0. The waveform is a triangle and the PWM edges are centered around the middle of the period — *center-aligned* PWM. Center-aligned PWM is preferred for motor control because it produces less harmonic distortion.
|
||||
|
||||
.. figure:: /../_static/mcpwm/timer_up_down_count.svg
|
||||
:align: center
|
||||
:alt: Up-down counting: the counter forms a triangle, rising to 25 (half of 50), then falling back to 0.
|
||||
|
||||
Up-down counting: the counter forms a triangle, rising to 25 (half of 50), then falling back to 0.
|
||||
|
||||
The frequency is still ``resolution_hz / period_ticks`` in both modes. Choose a resolution high enough for the duty precision you need, then choose a period for the desired frequency.
|
||||
|
||||
.. important::
|
||||
|
||||
:cpp:member:`period_ticks <mcpwm_timer_config_t::period_ticks>` is the total number of ticks in one full PWM cycle. It is not always the same thing as the timer peak value.
|
||||
|
||||
.. list::
|
||||
|
||||
- In ``MCPWM_TIMER_COUNT_MODE_UP``, the counter runs ``0 -> period_ticks``.
|
||||
- In ``MCPWM_TIMER_COUNT_MODE_UP_DOWN``, the hardware peak is ``period_ticks / 2``, and the full cycle is ``0 -> peak -> 0``.
|
||||
|
||||
For example, with :cpp:member:`resolution_hz <mcpwm_timer_config_t::resolution_hz>` = 1 MHz and :cpp:member:`period_ticks <mcpwm_timer_config_t::period_ticks>` = 50: up mode counts ``0 -> 50``, while up-down mode counts ``0 -> 25 -> 0``. Both still take 50 microseconds for a full cycle, so both are 20 kHz. What changes is the edge placement, not the period length.
|
||||
|
||||
.. _mcpwm-timer-safe-update:
|
||||
|
||||
Safe frequency updates
|
||||
======================
|
||||
|
||||
:cpp:func:`mcpwm_timer_set_period()` takes effect immediately by default. That can truncate the current cycle and produce a runt pulse. Set :cpp:member:`update_period_on_empty <mcpwm_timer_config_t::flags::update_period_on_empty>` to defer the new period until the counter reaches zero, or :cpp:member:`update_period_on_sync <mcpwm_timer_config_t::flags::update_period_on_sync>` to defer it until a sync event. When changing the period, also scale the comparator threshold if the duty ratio must remain unchanged:
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
// Keep 40 % duty while changing a 50-tick period to 100 ticks.
|
||||
ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, 40));
|
||||
ESP_ERROR_CHECK(mcpwm_timer_set_period(timer, 100));
|
||||
|
||||
In most runtime tuning paths, change the comparator to change duty and touch the timer only when the PWM frequency itself must change. Motor-control and power-conversion applications should usually combine this with :cpp:member:`update_period_on_empty <mcpwm_timer_config_t::flags::update_period_on_empty>` or a sync-triggered update to avoid mid-cycle changes.
|
||||
|
||||
Timer event callbacks
|
||||
=====================
|
||||
|
||||
The timer can notify your application at peak (:cpp:member:`on_full <mcpwm_timer_event_callbacks_t::on_full>`), zero (:cpp:member:`on_empty <mcpwm_timer_event_callbacks_t::on_empty>`), or when it stops (:cpp:member:`on_stop <mcpwm_timer_event_callbacks_t::on_stop>`). Register callbacks before enabling the timer. They run in ISR context: do not block, allocate memory, or call normal FreeRTOS APIs; use ``...FromISR`` variants when needed.
|
||||
|
||||
.. note::
|
||||
|
||||
The timer and capture timer may share a divider with other objects in the same group. When one group needs several resolutions, create objects in monotonic requested-resolution order to avoid divider conflicts. See :doc:`advanced topics <mcpwm_advanced>` for the full rule.
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
static bool IRAM_ATTR on_timer_empty(mcpwm_timer_handle_t timer,
|
||||
const mcpwm_timer_event_data_t *edata,
|
||||
void *user_ctx)
|
||||
{
|
||||
BaseType_t high_task_woken = pdFALSE;
|
||||
vTaskNotifyGiveFromISR((TaskHandle_t)user_ctx, &high_task_woken);
|
||||
return high_task_woken == pdTRUE;
|
||||
}
|
||||
|
||||
mcpwm_timer_event_callbacks_t cbs = { .on_empty = on_timer_empty };
|
||||
ESP_ERROR_CHECK(mcpwm_timer_register_event_callbacks(timer, &cbs,
|
||||
xTaskGetCurrentTaskHandle()));
|
||||
|
||||
The :doc:`synchronization <mcpwm_sync>` page shows how a timer can reset to a chosen phase on a sync edge.
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
MCPWM Timer Driver Functions
|
||||
----------------------------
|
||||
|
||||
.. include-build-file:: inc/mcpwm_timer.inc
|
||||
Reference in New Issue
Block a user