1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-01-18 12:52:44 +01:00

Merge pull request #18276 from gschorcht/cpu/esp32/periph_hal_esp32_pwm

cpu/esp32: use ESP-IDF ledc HAL for periph/pwm
This commit is contained in:
benpicco 2022-07-14 18:09:01 +02:00 committed by GitHub
commit 94e9116c04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 553 additions and 383 deletions

View File

@ -138,33 +138,92 @@ static const i2c_conf_t i2c_config[] = {
*/
/**
* @brief Static array of GPIOs that can be used as channels of PWM0
* @brief GPIOs used as channels for the according PWM device
*/
#ifdef PWM0_GPIOS
static const gpio_t pwm0_channels[] = PWM0_GPIOS;
static const gpio_t pwm0_gpios[] = PWM0_GPIOS;
#endif
/**
* @brief Static array of GPIOs that can be used as channels of PWM0
* @brief GPIOs used as channels for the according PWM device
*/
#ifdef PWM1_GPIOS
static const gpio_t pwm1_channels[] = PWM1_GPIOS;
static const gpio_t pwm1_gpios[] = PWM1_GPIOS;
#endif
/**
* @brief GPIOs used as channels for the according PWM device
*/
#ifdef PWM2_GPIOS
static const gpio_t pwm2_gpios[] = PWM2_GPIOS;
#endif
/**
* @brief GPIOs used as channels for the according PWM device
*/
#ifdef PWM3_GPIOS
static const gpio_t pwm3_gpios[] = PWM3_GPIOS;
#endif
/**
* @brief PWM device configuration based on defined PWM channel GPIOs
*/
static const pwm_config_t pwm_config[] =
{
#ifdef PWM0_GPIOS
{
.module = PERIPH_LEDC_MODULE,
.group = LEDC_LOW_SPEED_MODE,
.timer = LEDC_TIMER_0,
.ch_numof = ARRAY_SIZE(pwm0_gpios),
.gpios = pwm0_gpios,
},
#endif
#ifdef PWM1_GPIOS
{
.module = PERIPH_LEDC_MODULE,
#ifdef SOC_LEDC_SUPPORT_HS_MODE
.group = LEDC_HIGH_SPEED_MODE,
#else
.group = LEDC_LOW_SPEED_MODE,
#endif
.timer = LEDC_TIMER_1,
.ch_numof = ARRAY_SIZE(pwm1_gpios),
.gpios = pwm1_gpios,
},
#endif
#ifdef PWM2_GPIOS
{
.module = PERIPH_LEDC_MODULE,
.group = LEDC_LOW_SPEED_MODE,
.timer = LEDC_TIMER_2,
.ch_numof = ARRAY_SIZE(pwm2_gpios),
.gpios = pwm2_gpios,
},
#endif
#ifdef PWM3_GPIOS
{
.module = PERIPH_LEDC_MODULE,
#ifdef SOC_LEDC_SUPPORT_HS_MODE
.group = LEDC_HIGH_SPEED_MODE,
#else
.group = LEDC_LOW_SPEED_MODE,
#endif
.timer = LEDC_TIMER_3,
.ch_numof = ARRAY_SIZE(pwm3_gpios),
.gpios = pwm3_gpios,
},
#endif
};
/**
* @brief Number of PWM devices
*
* The number of PWM devices is determined from the PWM0_GPIOS and PWM1_GPIOS
* definitions.
* The number of PWM devices is determined from the PWM device configuration.
*
* @note PWM_NUMOF definition must not be changed.
*/
#if defined(PWM0_GPIOS) && defined(PWM1_GPIOS)
#define PWM_NUMOF (2)
#elif defined(PWM0_GPIOS) || defined(PWM1_GPIOS)
#define PWM_NUMOF (1)
#else
#define PWM_NUMOF (0)
#endif
#define PWM_NUMOF ARRAY_SIZE(pwm_config)
/** @} */

View File

@ -88,10 +88,6 @@
#endif
#endif /* PWM0_GPIOS */
/** PWM_DEV(1) is not used */
#ifndef PWM1_GPIOS
#define PWM1_GPIOS { }
#endif
/** @} */
/**

View File

@ -101,10 +101,6 @@
#define PWM0_GPIOS { GPIO2, GPIO0, GPIO4, GPIO15 }
#endif
/** PWM_DEV(1) is not used */
#ifndef PWM1_GPIOS
#define PWM1_GPIOS { }
#endif
/** @} */
/**

View File

@ -134,14 +134,9 @@ extern "C" {
#else
#error Configuration problem: Flash mode qio or qout is used, \
GPIO9 and GPIO10 cannot be used as PWM channels as configured
#define PWM0_GPIOS { }
#endif
#endif
/** PWM_DEV(1) is not used */
#ifndef PWM1_GPIOS
#define PWM1_GPIOS { }
#endif
/** @} */
/**

View File

@ -119,10 +119,6 @@
#define PWM0_GPIOS { GPIO0, GPIO2 }
#endif
/** PWM_DEV(1) is not used */
#ifndef PWM1_GPIOS
#define PWM1_GPIOS { }
#endif
/** @} */
/**

View File

@ -127,10 +127,6 @@
#endif
#endif
/** PWM_DEV(1) is not used */
#ifndef PWM1_GPIOS
#define PWM1_GPIOS { }
#endif
/** @} */
/**

View File

@ -99,6 +99,8 @@ Parameter | Short Description
[I2C1_SDA](#esp32_i2c_interfaces) | GPIO used as SCL for I2C_DEV(1) | o
[PWM0_GPIOS](#esp32_pwm_channels) | GPIOs that can be used at channels of PWM_DEV(0) | o
[PWM1_GPIOS](#esp32_pwm_channels) | GPIOs that can be used at channels of PWM_DEV(1) | o
[PWM3_GPIOS](#esp32_pwm_channels) | GPIOs that can be used at channels of PWM_DEV(2) | o
[PWM4_GPIOS](#esp32_pwm_channels) | GPIOs that can be used at channels of PWM_DEV(3) | o
[SPI0_CTRL](#esp32_spi_interfaces) | SPI Controller used for SPI_DEV(0), can be `VSPI` `HSPI` | o
[SPI0_SCK](#esp32_spi_interfaces) | GPIO used as SCK for SPI_DEV(0) | o
[SPI0_MOSI](#esp32_spi_interfaces) | GPIO used as MOSI for SPI_DEV(0) | o
@ -184,8 +186,8 @@ The key features of ESP32 are:
| Ethernet | MAC interface with dedicated DMA and IEEE 1588 support | yes |
| CAN | version 2.0 | yes |
| IR | up to 8 channels TX/RX | no |
| Motor PWM | 2 devices x 6 channels | yes |
| LED PWM | 16 channels | no |
| Motor PWM | 2 devices x 6 channels | no |
| LED PWM | 16 channels | yes |
| Crypto | Hardware acceleration of AES, SHA-2, RSA, ECC, RNG | no |
| Vcc | 2.5 - 3.6 V | |
| Documents | [Datasheet](https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf) <br> [Technical Reference](https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf) | |
@ -810,6 +812,62 @@ waiting.
ESP supports two types of PWM generators
The implementation of the PWM peripheral driver uses the LED PWM Controller
(LEDC) module of the ESP32x SoC. This LEDC module has one or two channel
groups with 6 or 8 channels each. The channels of each channel group can
use one of 4 timers as clock source. Thus, it is possible to define at
4 or 8 virtual PWM devices in RIOT with different frequencies and
resolutions. Regardless of whether the LEDC module of the ESP32x SoC has
one or two channel groups, the PWM driver implementation allows to organize
the available channels into up to 4 virtual PWM devices.
The assignment of the available channels to the virtual PWM devices is
done in the board-specific peripheral configuration by defining the
macros `PWM0_GPIOS`, `PWM1_GPIOS`, `PWM2_GPIOS` and `PWM3_GPIOS` These
macros specify the GPIOs that are used as channels for the available
virtual PWM devices PWM_DEV(0) ... PWM_DEV(3) in RIOT. The mapping of
these channels to the available channel groups and channel group timers
is done by the driver automatically as follows:
<center>
Macro | 1 Channel Group | 2 Channel Groups | Timer
-------------|-----------------------|------------------------|---------------
`PWM0_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_LOW_SPEED_MODE` | `LEDC_TIMER_0`
`PWM1_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_HIGH_SPEED_MODE` | `LEDC_TIMER_1`
`PWM2_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_LOW_SPEED_MODE` | `LEDC_TIMER_2`
`PWM3_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_HIGH_SPEED_MODE` | `LEDC_TIMER_3`
</center>
For example, if the LEDC module of the ESP32x SoC has two channel groups,
two virtual PWM devices with 2 x 6/8 channels could be used by defining
'PWM0_GPIOS' and 'PWM1_GPIOS' with up to 6 or 8 GPIOs each.
Example:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
#define PWM0_GPIOS { GPIO0, GPIO2, GPIO4, GPIO16, GPIO17 }
#define PWM1_GPIOS { GPIO27, GPIO32, GPIO33 }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This configuration can be changed by
[application specific configurations](#esp32_application_specific_configurations).
@note
- The total number of channels defined for a channel group must not exceed
#PWM_CH_NUMOF_MAX.
- The definition of `PWM0_GPIOS`, `PWM1_GPIOS`, `PWM2_GPIOS` and
`PWM3_GPIOS` can be omitted. In this case the existing macros should
be defined in ascending order, as the first defined macro is assigned
to PWM_DEV(0), the second defined macro is assigned to PWM_DEV(1)
and so on. So the minimal configuration would define all channels by
`PWM0_GPIOS` as PWM_DEV(0).
- #PWM_NUMOF is determined automatically.
- The order of the GPIOs in these macros determines the mapping between
RIOT's PWM channels and the GPIOs.
- As long as the GPIOs listed in `PWM0_GPIOS`, `PWM1_GPIOS`,
`PWM2_GPIOS` and `PWM3_GPIOS` are not initialized as PWM channels with
the #pwm_init function, they can be used for other purposes.
- one LED PWM controller (LEDPWM) with 16 channels, and
- two high-speed Motor Control PWM controllers (MCPWM) with 6 channels each.

View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2022 Gunar Schorcht
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup cpu_esp32_esp_idf_api
* @{
*
* @file
* @brief Interface for the ESP-IDF LEDC HAL API
*
* @author Gunar Schorcht <gunar@schorcht.net>
* @}
*/
#include <stdbool.h>
#include "driver/ledc.h"
int esp_ledc_channel_config(const ledc_channel_config_t* ledc_conf)
{
return ledc_channel_config(ledc_conf);
}
int esp_ledc_timer_config(const ledc_timer_config_t* timer_conf)
{
return ledc_timer_config(timer_conf);
}
int esp_ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel)
{
return ledc_update_duty(speed_mode, channel);
}
int esp_ledc_set_duty_with_hpoint(ledc_mode_t speed_mode,
ledc_channel_t channel,
uint32_t duty, uint32_t hpoint)
{
return ledc_set_duty_with_hpoint(speed_mode, channel, duty, hpoint);
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (C) 2022 Gunar Schorcht
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup cpu_esp32_esp_idf_api
* @{
*
* @file
* @brief Interface for the ESP-IDF LEDC HAL API
*
* @author Gunar Schorcht <gunar@schorcht.net>
* @}
*/
#ifndef ESP_IDF_API_LEDC_H
#define ESP_IDF_API_LEDC_H
#include "hal/ledc_types.h"
#ifndef DOXYGEN /* Hide implementation details from doxygen */
#ifdef __cplusplus
extern "C" {
#endif
/**
* @name ESP-IDF interface wrapper functions
* @{
*/
int esp_ledc_channel_config(const ledc_channel_config_t* ledc_conf);
int esp_ledc_timer_config(const ledc_timer_config_t* timer_conf);
int esp_ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel);
int esp_ledc_set_duty_with_hpoint(ledc_mode_t speed_mode,
ledc_channel_t channel,
uint32_t duty, uint32_t hpoint);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* DOXYGEN */
#endif /* ESP_IDF_API_LEDC_H */

View File

@ -21,6 +21,10 @@
#include <stdint.h>
#include "sdkconfig.h"
#include "hal/ledc_types.h"
#include "soc/ledc_struct.h"
#include "soc/periph_defs.h"
#include "soc/soc_caps.h"
#ifdef __cplusplus
extern "C" {
@ -363,39 +367,86 @@ typedef struct {
/**
* @name PWM configuration
*
* PWM implementation uses ESP32's high-speed MCPWM modules. ESP32 has 2 such
* modules, each with up to 6 channels (PWM_CHANNEL_NUM_DEV_MAX). Thus, the
* maximum number of PWM devices is 2 and the maximum total number of PWM
* channels is 12.
* The implementation of the PWM peripheral driver uses the LED PWM Controller
* (LEDC) module of the ESP32x SoC. This LEDC module has one or two channel
* groups with 6 or 8 channels each. The channels of each channel group can
* use one of 4 timers as clock source. Thus, it is possible to define at
* 4 or 8 virtual PWM devices in RIOT with different frequencies and
* resolutions. Regardless of whether the LEDC module of the ESP32x SoC has
* one or two channel groups, the PWM driver implementation allows to organize
* the available channels into up to 4 virtual PWM devices.
*
* PWM0_GPIOS and PWM1_GPIOS in the board-specific peripheral configuration
* each define a list of GPIOs that can be used with the respective PWM
* devices as PWM channels. The order of the listed GPIOs determines the
* association between the RIOT PWM channels and the GPIOs.
* The assignment of the available channels to the virtual PWM devices is
* done in the board-specific peripheral configuration by defining the
* macros `PWM0_GPIOS`, `PWM1_GPIOS`, `PWM2_GPIOS` and `PWM3_GPIOS` These
* macros specify the GPIOs that are used as channels for the available
* virtual PWM devices PWM_DEV(0) ... PWM_DEV(3) in RIOT. The mapping of
* these channels to the available channel groups and channel group timers
* is done by the driver automatically as follows.
*
* @note The definition of PWM0_GPIOS and PWM1_GPIOS can be omitted or
* empty. In the latter case, they must at least contain the curly braces.
* The corresponding PWM device can not be used in this case.
* <center>
* Macro | 1 Channel Group | 2 Channel Groups | Timer
* -------------|-----------------------|------------------------|---------------
* `PWM0_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_LOW_SPEED_MODE` | `LEDC_TIMER_0`
* `PWM1_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_HIGH_SPEED_MODE` | `LEDC_TIMER_1`
* `PWM2_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_LOW_SPEED_MODE` | `LEDC_TIMER_2`
* `PWM3_GPIOS` | `LEDC_LOW_SPEED_MODE` | `LEDC_HIGH_SPEED_MODE` | `LEDC_TIMER_3`
*
* PWM_NUMOF is determined automatically from the PWM0_GPIOS and PWM1_GPIOS
* definitions.
* </center>
* For example, if the LEDC module of the ESP32x SoC has two channel groups,
* two virtual PWM devices with 2 x 6/8 channels could be used by defining
* 'PWM0_GPIOS' and 'PWM1_GPIOS' with 6/8 GPIOs each.
*
* @note As long as the GPIOs listed in PWM0_GPIOS and PMW1_GPIOS are not
* initialized as PWM channels with the *pwm_init* function, they can be used
* other purposes.
* @note
* - The total number of channels defined for a channel group must not exceed
* #PWM_CH_NUMOF_MAX.
* - The definition of `PWM0_GPIOS`, `PWM1_GPIOS`, `PWM2_GPIOS` and
* `PWM3_GPIOS` can be omitted. In this case the existing macros should
* be defined in ascending order, as the first defined macro is assigned
* to PWM_DEV(0), the second defined macro is assigned to PWM_DEV(1)
* and so on. So the minimal configuration would define all channels by
* `PWM0_GPIOS` as PWM_DEV(0).
* - #PWM_NUMOF is determined automatically.
* - The order of the GPIOs in these macros determines the mapping between
* RIOT's PWM channels and the GPIOs.
* - As long as the GPIOs listed in `PWM0_GPIOS`, `PWM1_GPIOS`,
* `PWM2_GPIOS` and `PWM3_GPIOS` are not initialized as PWM channels with
* the #pwm_init function, they can be used for other purposes.
*
* @{
*/
/**
* @brief PWM configuration structure type
*
* The implementation of the PWM peripheral driver uses the LED PWM Controller
* (LEDC) module of the ESP32x SoC. The LEDC module has up to 2 channel groups
* with 6 or 8 channels each, which can use one of 4 timers.
*
* Based on these maximum 2 channel groups with 6 or 8 channels each and 4
* timers, up to 4 PWM devices can be configured in RIOT. The configuration
* structure defines static parameters for each virtual PWM device, i.e.
* the channel group used, the timer used, the number of channels used and
* the GPIOs assigned to the channels. The number of channels used by a PWM
* device corresponds to the number of GPIOs assigned to this PWM device.
*/
typedef struct {
uint8_t module; /**< LEDC module identifier */
ledc_mode_t group; /**< LEDC channel group used (low/high speed) */
ledc_timer_t timer; /**< LEDC timer used by this device */
uint8_t ch_numof; /**< Number of channels used by this device */
const gpio_t *gpios; /**< GPIOs used as channels of this device */
} pwm_config_t;
/**
* @brief Maximum number of PWM devices
*/
#define PWM_NUMOF_MAX (2)
#define PWM_NUMOF_MAX (4)
/**
* @brief Maximum number of channels per PWM device.
*/
#define PWM_CHANNEL_NUM_DEV_MAX (6)
#define PWM_CH_NUMOF_MAX (SOC_LEDC_CHANNEL_NUM)
/** @} */

View File

@ -18,410 +18,352 @@
* @}
*/
#define ENABLE_DEBUG 0
#include "debug.h"
#include "bitarithm.h"
#include "board.h"
#include "cpu.h"
#include "gpio_arch.h"
#include "kernel_defines.h"
#include "log.h"
#include "irq_arch.h"
#include "periph/pwm.h"
#include "periph/gpio.h"
#include "esp_common.h"
#include "gpio_arch.h"
#include "esp_rom_gpio.h"
#include "hal/ledc_hal.h"
#include "soc/ledc_struct.h"
#include "soc/rtc.h"
#include "driver/periph_ctrl.h"
#include "soc/gpio_struct.h"
#include "soc/gpio_sig_map.h"
#include "soc/mcpwm_reg.h"
#include "soc/mcpwm_struct.h"
#include "esp_idf_api/periph_ctrl.h"
#if defined(PWM0_GPIOS) || defined(PWM1_GPIOS)
#define ENABLE_DEBUG 0
#include "debug.h"
#define PWM_CLK (160000000UL) /* base clock of PWM devices */
#define PWM_CPS_MAX (10000000UL) /* maximum cycles per second supported */
#define PWM_CPS_MIN (2500UL) /* minimum cycles per second supported */
#if defined(PWM0_GPIOS) || defined(PWM1_GPIOS) || defined(PWM2_GPIOS) || defined(PWM3_GPIOS)
#define PWM_TIMER_MOD_FREEZE 0 /* timer is disabled */
#define PWM_TIMER_MOD_UP 1 /* timer counts up */
#define PWM_TIMER_MOD_DOWN 2 /* timer counts down */
#define PWM_TIMER_MOD_UP_DOWN 3 /* timer counts up and then down */
#define SOC_LEDC_CLK_DIV_BIT_NUM 18
#define SOC_LEDC_CLK_DIV_INT_BIT_NUM 10 /* integral part of CLK divider */
#define SOC_LEDC_CLK_DIV_FRAC_BIT_NUM 8 /* fractional part of CLK divider */
#define PWM_TIMER_STOPS_AT_TEZ 0 /* PWM starts, then stops at next TEZ */
#define PWM_TIMER_STOPS_AT_TEP 1 /* PWM starts, then stops at next TEP */
#define PWM_TIMER_RUNS_ON 2 /* PWM runs on */
#define PWM_TIMER_STARTS_STOPS_AT_TEZ 3 /* PWM starts and stops at next TEZ */
#define PWM_TIMER_STARTS_STOPS_AT_TEP 4 /* PWM starts and stops at next TEP */
#define PWM_HW_RES_MAX ((uint32_t)1 << SOC_LEDC_TIMER_BIT_WIDE_NUM)
#define PWM_HW_RES_MIN ((uint32_t)1 << 1)
#define PWM_TIMER_UPDATE_IMMIDIATE 0 /* update period immediately */
#define PWM_TIMER_UPDATE_AT_TEZ 1 /* update period at TEZ */
#define PWM_TIMER_UPDATE_AT_SYNC 2 /* update period at sync */
#define PWM_TIMER_UPDATE_AT_TEZ_SYNC 3 /* update period at TEZ and sync */
#define _DEV _pwm_dev[pwm] /* shortcut for PWM device descriptor */
#define _CFG pwm_config[pwm] /* shortcut for PWM device configuration */
#define PWM_OP_ACTION_NO_CHANGE 0 /* do not change output */
#define PWM_OP_ACTION_LOW 1 /* set the output to high */
#define PWM_OP_ACTION_HIGH 2 /* set the output to low */
#define PWM_OP_ACTION_TOGGLE 3 /* toggle the output */
#define PWM_OP_CHANNEL_A 0 /* operator channel A */
#define PWM_OP_CHANNEL_B 0 /* operator channel B */
/* forward declaration of internal functions */
static void _pwm_start(pwm_t pwm);
static void _pwm_stop(pwm_t pwm);
static bool _pwm_configuration(void);
/* data structure for static configuration of PWM devices */
struct _pwm_hw_t {
mcpwm_dev_t* regs; /* PWM's registers set address */
uint8_t mod; /* PWM's hardware module */
uint8_t int_src; /* PWM's peripheral interrupt source */
uint32_t signal_group; /* PWM's base peripheral signal index */
uint8_t gpio_num; /* number of GPIOs used as channels outputs */
const gpio_t* gpios; /* GPIOs used as channel outputs */
};
#ifdef PWM0_GPIOS
static const gpio_t _pwm_channel_gpios_0[] = PWM0_GPIOS;
#endif
#ifdef PWM1_GPIOS
static const gpio_t _pwm_channel_gpios_1[] = PWM1_GPIOS;
#endif
/* static configuration of PWM devices */
static const struct _pwm_hw_t _pwm_hw[] =
{
#ifdef PWM0_GPIOS
{
.regs = &MCPWM0,
.mod = PERIPH_PWM0_MODULE,
.int_src = ETS_PWM0_INTR_SOURCE,
.signal_group = PWM0_OUT0A_IDX,
.gpio_num = ARRAY_SIZE(_pwm_channel_gpios_0),
.gpios = _pwm_channel_gpios_0,
},
#endif
#ifdef PWM1_GPIOS
{
.regs = &MCPWM1,
.mod = PERIPH_PWM1_MODULE,
.int_src = ETS_PWM1_INTR_SOURCE,
.signal_group = PWM1_OUT0A_IDX,
.gpio_num = ARRAY_SIZE(_pwm_channel_gpios_1),
.gpios = _pwm_channel_gpios_1,
},
#endif
};
/* data structure dynamic channel configuration */
/* data structure for dynamic channel parameters */
typedef struct {
bool used;
uint32_t duty;
} _pwm_chn_t;
uint32_t duty; /* actual duty value */
uint32_t hpoint; /* actual hpoing value */
bool used; /* true if the channel is set by pwm_set */
uint8_t ch; /* actual channel index within used channel group */
} _pwm_ch_t;
/* data structure for dynamic configuration of PWM devices */
struct _pwm_dev_t {
uint16_t res;
uint32_t freq;
pwm_mode_t mode;
uint8_t chn_num;
_pwm_chn_t chn[PWM_CHANNEL_NUM_DEV_MAX];
};
/* data structure for device handling at runtime */
typedef struct {
uint32_t freq; /* specified frequency parameter */
uint32_t res; /* specified resolution parameter */
uint32_t hw_freq; /* used hardware frequency */
uint32_t hw_res; /* used hardware resolution */
uint32_t hw_clk_div; /* used hardware clock divider */
_pwm_ch_t ch[PWM_CH_NUMOF_MAX]; /* dynamic channel parameters at runtime */
ledc_hal_context_t hw; /* used hardware device context */
pwm_mode_t mode; /* specified mode */
ledc_timer_bit_t hw_res_bit; /* used hardware resolution in bit */
bool enabled; /* true if the device is used (powered on) */
} _pwm_dev_t;
/* dynamic configuration of PWM devices */
static struct _pwm_dev_t _pwm_dev[PWM_NUMOF_MAX] = {};
static _pwm_dev_t _pwm_dev[PWM_NUMOF] = { };
/* if pwm_init is called first time, it checks the overall pwm configuration */
static bool _pwm_init_first_time = true;
/* if pwm_init is called first time, it checks the pwm configuration */
static bool _pwm_initialized = false;
/* static configuration checks and initialization on first pwm_init */
static bool _pwm_initialize(void);
/* Initialize PWM device */
uint32_t pwm_init(pwm_t pwm, pwm_mode_t mode, uint32_t freq, uint16_t res)
{
DEBUG ("%s pwm=%u mode=%u freq=%u, res=%u\n", __func__, pwm, mode, freq, res);
_Static_assert(PWM_NUMOF <= PWM_NUMOF_MAX, "Too many PWM devices defined");
CHECK_PARAM_RET (pwm < PWM_NUMOF, 0);
CHECK_PARAM_RET (freq > 0, 0);
if (_pwm_init_first_time) {
if (!_pwm_configuration())
if (!_pwm_initialized) {
if (!_pwm_initialize()) {
return 0;
}
_pwm_initialized = true;
}
if (_pwm_hw[pwm].gpio_num == 0) {
LOG_TAG_ERROR("pwm", "PWM device %d has no assigned pins\n", pwm);
assert(pwm < PWM_NUMOF);
assert(freq > 0);
_DEV.enabled = true;
_DEV.res = res;
_DEV.freq = freq;
_DEV.mode = mode;
if ((res < PWM_HW_RES_MIN) || (_DEV.res > PWM_HW_RES_MAX)) {
LOG_TAG_ERROR("pwm", "Resolution of PWM device %u to be in "
"range [%"PRIu32", %"PRIu32"]\n",
pwm, PWM_HW_RES_MIN, PWM_HW_RES_MAX);
return 0;
}
/* reset by disabling and enable the PWM module */
periph_module_disable(_pwm_hw[pwm].mod);
periph_module_enable(_pwm_hw[pwm].mod);
/*
* The hardware resolution must be a power of two, so we determine the
* next power of two, which covers the desired resolution
*/
ledc_timer_bit_t hw_res_bit = bitarithm_msb(res - 1);
if (hw_res_bit < SOC_LEDC_TIMER_BIT_WIDE_NUM) {
hw_res_bit++;
}
_pwm_dev[pwm].res = res;
_pwm_dev[pwm].freq = freq;
_pwm_dev[pwm].mode = mode;
_pwm_dev[pwm].chn_num = _pwm_hw[pwm].gpio_num;
uint32_t hw_res = 1 << hw_res_bit;
for (int i = 0; i < _pwm_dev[pwm].chn_num; i++) {
uint32_t hw_ticks_max = rtc_clk_apb_freq_get();
uint32_t hw_ticks_min = hw_ticks_max / (1 << SOC_LEDC_CLK_DIV_INT_BIT_NUM);
uint32_t hw_freq_min = hw_ticks_min / (1 << SOC_LEDC_TIMER_BIT_WIDE_NUM) + 1;
if (freq < hw_freq_min) {
LOG_TAG_ERROR("pwm", "Frequency of %"PRIu32" Hz is too less, minimum "
"frequency is %"PRIu32" Hz\n", freq, hw_freq_min);
return 0;
}
/* number of hardware ticks required, at maximum it can be APB clock */
uint32_t hw_ticks = MIN(freq * hw_res, rtc_clk_apb_freq_get());
/*
* if the number of required ticks is less than minimum ticks supported by
* the hardware supports, we have to increase the resolution.
*/
while (hw_ticks < hw_ticks_min) {
hw_res_bit++;
hw_res = 1 << hw_res_bit;
hw_ticks = freq * hw_res;
}
/* LEDC_CLK_DIV is given in Q10.8 format */
uint32_t hw_clk_div =
((uint64_t)rtc_clk_apb_freq_get() << SOC_LEDC_CLK_DIV_FRAC_BIT_NUM) / hw_ticks;
_DEV.freq = freq;
_DEV.res = res;
_DEV.hw_freq = hw_ticks / hw_res;
_DEV.hw_res = hw_res;
_DEV.hw_res_bit = hw_res_bit;
_DEV.hw_clk_div = hw_clk_div;
DEBUG("%s hw_freq=%"PRIu32" hw_res=%"PRIu32" hw_ticks=%"PRIu32
" hw_clk_div=%"PRIu32"\n", __func__,
_DEV.hw_freq, _DEV.hw_res, hw_ticks, _DEV.hw_clk_div);
for (int i = 0; i < _CFG.ch_numof; i++) {
/* initialize channel data */
_pwm_dev[pwm].chn[i].used = false;
_pwm_dev[pwm].chn[i].duty = 0;
_DEV.ch[i].used = false;
_DEV.ch[i].duty = 0;
/* reset GPIO usage type if the pins were used already for PWM before
to make it possible to reinitialize PWM with new parameters */
if (gpio_get_pin_usage(_pwm_hw[pwm].gpios[i]) == _PWM) {
gpio_set_pin_usage(_pwm_hw[pwm].gpios[i], _GPIO);
if (gpio_get_pin_usage(_CFG.gpios[i]) == _PWM) {
gpio_set_pin_usage(_CFG.gpios[i], _GPIO);
}
if (gpio_get_pin_usage(_pwm_hw[pwm].gpios[i]) != _GPIO) {
LOG_TAG_ERROR("pwm", "GPIO%d is used for %s and cannot be used as PWM output\n", i,
gpio_get_pin_usage_str(_pwm_hw[pwm].gpios[i]));
if (gpio_get_pin_usage(_CFG.gpios[i]) != _GPIO) {
LOG_TAG_ERROR("pwm", "GPIO%d is used for %s and cannot be used as "
"PWM output\n",
i, gpio_get_pin_usage_str(_CFG.gpios[i]));
return 0;
}
if (gpio_init(_pwm_hw[pwm].gpios[i], GPIO_OUT) < 0) {
/* initialize the GPIOs and route the PWM signal output to the GPIO */
if (gpio_init(_CFG.gpios[i], GPIO_OUT) < 0) {
return 0;
}
/* initialize the GPIO and route the PWM signal output to the GPIO */
gpio_set_pin_usage(_pwm_hw[pwm].gpios[i], _GPIO);
gpio_clear (_pwm_hw[pwm].gpios[i]);
GPIO.func_out_sel_cfg[_pwm_hw[pwm].gpios[i]].func_sel = _pwm_hw[pwm].signal_group + i;
gpio_set_pin_usage(_CFG.gpios[i], _PWM);
gpio_clear(_CFG.gpios[i]);
esp_rom_gpio_connect_out_signal(
_CFG.gpios[i],
ledc_periph_signal[_CFG.group].sig_out0_idx + _DEV.ch[i].ch, 0, 0);
}
/* start the PWM device */
_pwm_start(pwm);
pwm_poweron(pwm);
return freq;
return _DEV.hw_freq;
}
uint8_t pwm_channels(pwm_t pwm)
{
CHECK_PARAM_RET (pwm < PWM_NUMOF, 0);
return _pwm_hw[pwm].gpio_num;
assert(pwm < PWM_NUMOF);
return _CFG.ch_numof;
}
void pwm_set(pwm_t pwm, uint8_t channel, uint16_t value)
{
DEBUG("%s pwm=%u channel=%u value=%u\n", __func__, pwm, channel, value);
CHECK_PARAM (pwm < PWM_NUMOF);
CHECK_PARAM (channel < _pwm_dev[pwm].chn_num);
CHECK_PARAM (value <= _pwm_dev[pwm].res);
assert(pwm < PWM_NUMOF);
assert(channel < _CFG.ch_numof);
uint32_t state = irq_disable();
value = MIN(value, _DEV.res);
_pwm_dev[pwm].chn[channel].duty = value;
_pwm_dev[pwm].chn[channel].used = true;
_DEV.ch[channel].used = true;
_DEV.ch[channel].duty = value * _DEV.hw_res / _DEV.res;
_DEV.ch[channel].hpoint = 0;
/* determine used operator and operator output */
uint8_t op_idx = channel >> 1;
uint8_t op_out = channel & 0x01;
/* compute and set shadow register (compare) )value of according channel */
uint16_t cmp = 0;
switch (_pwm_dev[pwm].mode) {
case PWM_LEFT: cmp = value;
break;
case PWM_RIGHT: cmp = value - 1;
break;
case PWM_CENTER: cmp = _pwm_hw[pwm].regs->timer[0].timer_cfg0.timer_period - value;
break;
}
_pwm_hw[pwm].regs->operators[op_idx].timestamp[op_out].gen = cmp;
/* set actions for timing events (reset all first) */
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].val = 0;
if (op_out == 0)
{
/* channel/output A is used -> set actions for channel A */
switch (_pwm_dev[pwm].mode)
{
case PWM_LEFT:
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_utez = PWM_OP_ACTION_HIGH;
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_utea = PWM_OP_ACTION_LOW;
break;
case PWM_RIGHT:
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_dtea = PWM_OP_ACTION_HIGH;
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_dtep = PWM_OP_ACTION_LOW;
break;
case PWM_CENTER:
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_utea = PWM_OP_ACTION_HIGH;
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_dtea = PWM_OP_ACTION_LOW;
break;
}
}
else {
/* channel/output B is used -> set actions for channel B */
switch (_pwm_dev[pwm].mode)
{
case PWM_LEFT:
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_utez = PWM_OP_ACTION_HIGH;
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_uteb = PWM_OP_ACTION_LOW;
break;
case PWM_RIGHT:
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_dteb = PWM_OP_ACTION_HIGH;
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_dtep = PWM_OP_ACTION_LOW;
break;
case PWM_CENTER:
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_uteb = PWM_OP_ACTION_HIGH;
_pwm_hw[pwm].regs->operators[op_idx].generator[op_out].gen_dteb = PWM_OP_ACTION_LOW;
break;
}
switch (_DEV.mode) {
case PWM_LEFT:
_DEV.ch[channel].hpoint = 0;
break;
case PWM_RIGHT:
_DEV.ch[channel].hpoint = _DEV.hw_res - 1 - _DEV.ch[channel].duty;
break;
case PWM_CENTER:
_DEV.ch[channel].hpoint = (_DEV.hw_res - _DEV.ch[channel].duty) >> 1;
break;
}
irq_restore(state);
DEBUG("%s pwm=%u duty=%"PRIu32" hpoint=%"PRIu32"\n",
__func__, pwm, _DEV.ch[channel].duty, _DEV.ch[channel].hpoint);
unsigned ch = _DEV.ch[channel].ch; /* internal channel mapping */
critical_enter();
ledc_hal_set_duty_int_part(&_DEV.hw, ch, _DEV.ch[channel].duty);
ledc_hal_set_hpoint(&_DEV.hw, ch, _DEV.ch[channel].hpoint);
ledc_hal_set_sig_out_en(&_DEV.hw, ch, true);
ledc_hal_ls_channel_update(&_DEV.hw, ch);
ledc_hal_set_duty_start(&_DEV.hw, ch, true);
critical_exit();
}
void pwm_poweron(pwm_t pwm)
{
CHECK_PARAM (pwm < PWM_NUMOF);
periph_module_enable(_pwm_hw[pwm].mod);
_pwm_start(pwm);
DEBUG("%s pwm=%u\n", __func__, pwm);
/* enable and init the module and select the right clock source */
esp_idf_periph_module_enable(_CFG.module);
ledc_hal_init(&_DEV.hw, _CFG.group);
ledc_hal_set_slow_clk_sel(&_DEV.hw, LEDC_SLOW_CLK_APB);
ledc_hal_set_clock_source(&_DEV.hw, _CFG.timer, LEDC_APB_CLK);
/* update the timer according to determined parameters */
ledc_hal_set_clock_divider(&_DEV.hw, _CFG.timer, _DEV.hw_clk_div);
ledc_hal_set_duty_resolution(&_DEV.hw, _CFG.timer, _DEV.hw_res_bit);
ledc_hal_ls_timer_update(&_DEV.hw, _CFG.timer);
ledc_hal_timer_rst(&_DEV.hw, _CFG.timer);
critical_enter();
for (unsigned i = 0; i < _CFG.ch_numof; i++) {
/* static configuration of the channel, no fading */
ledc_hal_set_duty_direction(&_DEV.hw, _DEV.ch[i].ch, 1);
ledc_hal_set_duty_num(&_DEV.hw, _DEV.ch[i].ch, 1);
ledc_hal_set_duty_cycle(&_DEV.hw, _DEV.ch[i].ch, 1);
ledc_hal_set_duty_scale(&_DEV.hw, _DEV.ch[i].ch, 0);
ledc_hal_set_fade_end_intr(&_DEV.hw, _DEV.ch[i].ch, 0);
/* bind the channel to the timer and disable the output for now */
ledc_hal_bind_channel_timer(&_DEV.hw, _DEV.ch[i].ch, _CFG.timer);
/* restore used parameters */
ledc_hal_set_duty_int_part(&_DEV.hw, _DEV.ch[i].ch, _DEV.ch[i].duty);
ledc_hal_set_hpoint(&_DEV.hw, _DEV.ch[i].ch, _DEV.ch[i].hpoint);
ledc_hal_set_sig_out_en(&_DEV.hw, _DEV.ch[i].ch, _DEV.ch[i].used);
ledc_hal_ls_channel_update(&_DEV.hw, _DEV.ch[i].ch);
ledc_hal_set_duty_start(&_DEV.hw, _DEV.ch[i].ch, _DEV.ch[i].used);
}
_DEV.enabled = true;
critical_exit();
}
void pwm_poweroff(pwm_t pwm)
{
CHECK_PARAM (pwm < PWM_NUMOF);
_pwm_stop (pwm);
periph_module_disable(_pwm_hw[pwm].mod);
}
DEBUG("%s pwm=%u\n", __func__, pwm);
static void _pwm_start(pwm_t pwm)
{
pwm_mode_t mode = _pwm_dev[pwm].mode;
uint16_t res = _pwm_dev[pwm].res;
uint32_t freq = _pwm_dev[pwm].freq;
uint32_t period = 0;
if (!_pwm_dev[pwm].enabled) {
return;
}
/* set timer mode */
switch (mode) {
case PWM_LEFT:
period = res;
_pwm_hw[pwm].regs->timer[0].timer_cfg1.timer_mod = PWM_TIMER_MOD_UP;
break;
case PWM_RIGHT:
period = res;
_pwm_hw[pwm].regs->timer[0].timer_cfg1.timer_mod = PWM_TIMER_MOD_DOWN;
break;
case PWM_CENTER:
period = res * 2;
_pwm_hw[pwm].regs->timer[0].timer_cfg1.timer_mod = PWM_TIMER_MOD_UP_DOWN;
unsigned i;
/* disable the signal of all channels */
critical_enter();
for (i = 0; i < _CFG.ch_numof; i++) {
ledc_hal_set_idle_level(&_DEV.hw, _DEV.ch[i].ch, 0);
ledc_hal_set_sig_out_en(&_DEV.hw, _DEV.ch[i].ch, false);
ledc_hal_set_duty_start(&_DEV.hw, _DEV.ch[i].ch, false);
ledc_hal_ls_channel_update(&_DEV.hw, _DEV.ch[i].ch);
}
_DEV.enabled = false;
critical_exit();
/* check whether all devices of the same hardware module are disabled */
for (i = 0; i < PWM_NUMOF; i++) {
if ((_CFG.module == pwm_config[i].module) && _pwm_dev[i].enabled) {
break;
}
}
uint32_t cps = period * freq;
/* maximum number of timer clock cycles per second (freq*period) must not
be greater than PWM_CPS_MAX, reduce the freq if necessary and keep
the resolution */
if (cps > PWM_CPS_MAX) {
freq = PWM_CPS_MAX / period;
_pwm_dev[pwm].freq = freq;
DEBUG("%s freq*res was to high, freq was reduced to %d Hz\n",
__func__, freq);
}
/* minimum number of timer clock cycles per second (freq*period) must not
be less than PWM_CPS_MIN, increase the freq if necessary and keep
the resolution */
else if (cps < PWM_CPS_MIN) {
freq = PWM_CPS_MIN / period;
_pwm_dev[pwm].freq = freq;
DEBUG("%s freq*res was to low, freq was increased to %d Hz\n",
__func__, freq);
}
/* determine a suitable pwm clock prescale */
uint32_t prescale;
if (cps > 1000000) {
/* pwm clock is not scaled,
8 bit timer prescaler can scale down timer clock to 625 kHz */
prescale = 1;
}
else if (cps > 100000) {
/* pwm clock is scaled down to 10 MHz,
8 bit timer prescaler can scale down timer clock to 39,0625 kHz */
prescale = 16;
}
else if (cps > 10000) {
/* pwm clock is scaled down to 1 MHz
8 bit timer prescaler can scale down timer clock to 3,90625 kHz */
prescale = 160;
}
else {
/* pwm clock is scaled down to 640 kHz
8 bit timer prescaler can scale down timer clock to 2,5 kHz */
prescale = 250;
}
_pwm_hw[pwm].regs->clk_cfg.clk_prescale = prescale - 1;
/* set timing parameters (only timer0 is used) */
_pwm_hw[pwm].regs->timer[0].timer_cfg0.timer_prescale = (PWM_CLK / prescale / cps) - 1;
_pwm_hw[pwm].regs->timer[0].timer_cfg0.timer_period = (mode == PWM_CENTER) ? res : res - 1;
_pwm_hw[pwm].regs->timer[0].timer_cfg0.timer_period_upmethod = PWM_TIMER_UPDATE_IMMIDIATE;
/* start the timer */
_pwm_hw[pwm].regs->timer[0].timer_cfg1.timer_start = PWM_TIMER_RUNS_ON;
/* set timer sync phase and enable timer sync input */
_pwm_hw[pwm].regs->timer[0].timer_sync.timer_phase = 0;
_pwm_hw[pwm].regs->timer[0].timer_sync.timer_synci_en = 1;
/* set the duty for all channels to start them */
for (int i = 0; i < _pwm_dev[pwm].chn_num; i++) {
if (_pwm_dev[pwm].chn[i].used)
pwm_set(pwm, i, _pwm_dev[pwm].chn[i].duty);
}
/* sync all timers */
for (unsigned i = 0; i < PWM_NUMOF; i++) {
_pwm_hw[i].regs->timer[0].timer_sync.timer_sync_sw =
~_pwm_hw[i].regs->timer[0].timer_sync.timer_sync_sw;
/* if all devices of the same hardware module are disable, it is powered off */
if (i == PWM_NUMOF) {
esp_idf_periph_module_disable(_CFG.module);
}
}
static void _pwm_stop(pwm_t pwm)
void pwm_print_config(void)
{
/* disable the timer */
_pwm_hw[pwm].regs->timer[0].timer_cfg1.timer_mod = PWM_TIMER_MOD_FREEZE;
for (unsigned pwm = 0; pwm < PWM_NUMOF; pwm++) {
printf("\tPWM_DEV(%d)\tchannels=[ ", pwm);
for (int i = 0; i < _CFG.ch_numof; i++) {
printf("%d ", _CFG.gpios[i]);
}
printf("]\n");
}
}
/* do some static initialization and configuration checks */
static bool _pwm_configuration(void)
/* do static configuration checks */
static bool _pwm_initialize(void)
{
if (PWM_NUMOF > PWM_NUMOF_MAX) {
LOG_TAG_ERROR("pwm", "%d PWM devices were defined, only %d PWM are "
"supported\n", PWM_NUMOF, PWM_NUMOF_MAX);
return false;
}
unsigned ch_numof[2] = {};
for (unsigned i = 0; i < PWM_NUMOF; i++) {
if (_pwm_hw[i].gpio_num > PWM_CHANNEL_NUM_DEV_MAX) {
for (unsigned pwm = 0; pwm < PWM_NUMOF; pwm++) {
/* compute the channel indices */
for (unsigned i = 0; i < _CFG.ch_numof; i++) {
_pwm_dev[pwm].ch[i].ch = ch_numof[_CFG.group] + i;
}
ch_numof[_CFG.group] += _CFG.ch_numof;
if (_CFG.ch_numof > PWM_CH_NUMOF_MAX) {
LOG_TAG_ERROR("pwm", "Number of PWM channels of device %d is %d, "
"at maximum only %d channels per PWM device are "
"supported\n",
i, _pwm_hw[i].gpio_num, PWM_CHANNEL_NUM_DEV_MAX);
"only %d channels per PWM device are supported\n",
pwm, _CFG.ch_numof, PWM_CH_NUMOF_MAX);
return false;
}
}
unsigned total_ch_numof = ch_numof[0] + ch_numof[1];
if (total_ch_numof > (SOC_LEDC_CHANNEL_NUM * ARRAY_SIZE(ledc_periph_signal))) {
LOG_TAG_ERROR("pwm", "Total number of PWM channels is %d, only "
"%d channels are supported at maximum\n", total_ch_numof,
PWM_CH_NUMOF_MAX * ARRAY_SIZE(ledc_periph_signal));
return false;
}
bool multiple_used = false;
for (unsigned i = 0; i < PWM_NUMOF; i++) {
for (unsigned j = 0; j < PWM_NUMOF; j++) {
if (i != j) {
for (unsigned k = 0; k < _pwm_hw[i].gpio_num >> 2; k++) {
for (unsigned l = 0; l < _pwm_hw[j].gpio_num >> 2; l++) {
if (_pwm_hw[i].gpios[k] == _pwm_hw[j].gpios[l]) {
LOG_TAG_ERROR("pwm", "GPIO%d is used multiple times in "
"PWM devices %d and %d\n",
_pwm_hw[i].gpios[k], i, j);
multiple_used = true;
}
for (unsigned pi = 0; pi < PWM_NUMOF; pi++) {
for (unsigned ci = 0; ci < pwm_config[pi].ch_numof; ci++) {
for (unsigned pj = 0; pj < PWM_NUMOF; pj++) {
if (pi == pj) {
continue;
}
for (unsigned cj = 0; cj < pwm_config[pj].ch_numof; cj++) {
if (pwm_config[pi].gpios[ci] == pwm_config[pj].gpios[cj]) {
LOG_TAG_ERROR("pwm", "GPIO%d is used multiple times in "
"PWM devices %d and %d\n",
pwm_config[pi].gpios[ci], pi, pj);
multiple_used = true;
}
}
}
@ -434,22 +376,11 @@ static bool _pwm_configuration(void)
return true;
}
void pwm_print_config(void)
{
for (unsigned pwm = 0; pwm < PWM_NUMOF; pwm++) {
printf("\tPWM_DEV(%d)\tchannels=[ ", pwm);
for (int i = 0; i < _pwm_hw[pwm].gpio_num; i++) {
printf("%d ", _pwm_hw[pwm].gpios[i]);
}
printf("]\n");
}
}
#else /* defined(PWM0_GPIOS) || defined(PWM1_GPIOS) */
#else
void pwm_print_config(void)
{
LOG_TAG_INFO("pwm", "no PWM devices\n");
}
#endif /* defined(PWM0_GPIOS) || defined(PWM1_GPIOS) */
#endif