Skip to content

[WIP] Refactor/i hardware pwm #2956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions Sming/Arch/Esp32/Components/driver/include/driver/pwm.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,106 @@
#include <soc/soc_caps.h>

#define PWM_CHANNEL_NUM_MAX SOC_LEDC_CHANNEL_NUM

/**
* @file pwm.h
* @brief PWM (Pulse Width Modulation) configuration structures and enumerations for ESP32.
*/

/**
* @enum PWM_phase_shift_mode
* @brief Defines the modes for phase shifting in PWM.
*
* @var PWM_PHASE_OFF
* No phase shifting.
*
* @var PWM_PHASE_CUSTOM
* Use custom per-channel phase delay values.
*
* @var PWM_PHASE_AUTO
* Evenly distribute phases across the PWM period.
*/
enum PWM_phase_shift_mode {
PWM_PHASE_OFF, // No phase shifting
PWM_PHASE_CUSTOM, // Use custom per-channel values
PWM_PHASE_AUTO // Evenly distribute phases across period
};

/**
* @enum PWM_spread_spectrum_mode
* @brief Defines the modes for spread spectrum in PWM.
*
* @var PWM_SPREAD_OFF
* No spread spectrum.
*
* @var PWM_SPREAD_CUSTOM
* Use custom per-channel frequency offset values.
*
* @var PWM_SPREAD_AUTO
* Automatically apply spread spectrum with a specified percentage.
*/
enum PWM_spread_spectrum_mode {
PWM_SPREAD_OFF,
PWM_SPREAD_CUSTOM,
PWM_SPREAD_AUTO
};

/**
* @struct PWM_phase_shift
* @brief Configuration structure for PWM phase shifting.
*
* @var PWM_phase_shift::mode
* Specifies the phase shift mode. See @ref PWM_phase_shift_mode.
*
* @var PWM_phase_shift::phaseDelayPercent
* Array of phase delay percentages for each channel (used when mode is PWM_PHASE_CUSTOM).
*
* @var PWM_phase_shift::phaseStartPercent
* Optional starting phase percentage for automatic phase distribution (used when mode is PWM_PHASE_AUTO).
*/
struct PWM_phase_shift {
PWM_phase_shift_mode mode = PWM_PHASE_OFF;
union {
uint8_t phaseDelayPercent[PWM_CHANNEL_NUM_MAX]; // Used when mode is PWM_PHASE_CUSTOM
uint8_t phaseStartPercent; // Optional starting phase for PWM_PHASE_AUTO
} ;
};

/**
* @struct PWM_spread_spectrum
* @brief Configuration structure for PWM spread spectrum.
*
* @var PWM_spread_spectrum::mode
* Specifies the spread spectrum mode. See @ref PWM_spread_spectrum_mode.
*
* @var PWM_spread_spectrum::frequencyOffsetHz
* Array of frequency offsets in Hz for each channel (used when mode is PWM_SPREAD_CUSTOM).
*
* @var PWM_spread_spectrum::spreadPercentage
* Spread percentage for automatic spread spectrum (used when mode is PWM_SPREAD_AUTO).
*/
struct PWM_spread_spectrum {
PWM_spread_spectrum_mode mode = PWM_SPREAD_OFF;
union {
int32_t frequencyOffsetHz[PWM_CHANNEL_NUM_MAX]; // Used when mode is PWM_SPREAD_CUSTOM
uint8_t spreadPercentage; // Percentage for PWM_SPREAD_AUTO (0.0-100.0)
} ;
};

/**
* @struct PWM_Options
* @brief Configuration options for PWM (Pulse Width Modulation).
*
* This structure defines the configuration parameters for PWM, including
* phase shift and spread spectrum options.
*
* @var PWM_Options::phaseShift
* Specifies the phase shift configuration for the PWM signal.
*
* @var PWM_Options::spreadSpectrum
* Specifies the spread spectrum configuration for the PWM signal.
*/
struct PWM_Options {
PWM_phase_shift phaseShift;
PWM_spread_spectrum spreadSpectrum;
};
47 changes: 44 additions & 3 deletions Sming/Arch/Esp32/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,30 @@ uint32_t maxDuty(ledc_timer_bit_t bits)

HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins)
{
PWM_Options options;
options.phaseShift.mode=PWM_PHASE_OFF;
options.spreadSpectrum.mode=PWM_SPREAD_OFF;
Init(pins, no_of_pins, options);
}

HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t no_of_pins, PWM_Options& options) : channel_count(no_of_pins), _options(options)
{
Init(pins, no_of_pins, options);
}

void HardwarePWM::Init(const uint8_t* pins, uint8_t no_of_pins, PWM_Options& options)
{
_options = options;

assert(no_of_pins > 0 && no_of_pins <= SOC_LEDC_CHANNEL_NUM);
no_of_pins = std::min(uint8_t(SOC_LEDC_CHANNEL_NUM), no_of_pins);

periph_module_enable(PERIPH_LEDC_MODULE);

if (_options.phaseShift.mode!=PWM_PHASE_OFF) {
debug_i("Using phase shift for %d channels\n", no_of_pins);
}

for(uint8_t i = 0; i < no_of_pins; i++) {
channels[i] = pins[i];

Expand Down Expand Up @@ -181,7 +200,7 @@ HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t no_of_pins) : channel_coun
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = pinToTimer(i),
.duty = 0,
.hpoint = 0,
.hpoint = hpointForPin(i,channel_count),
};
debug_d("ledc_channel\n"
"\tspeed_mode: %i\r\n"
Expand All @@ -191,7 +210,7 @@ HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t no_of_pins) : channel_coun
"\tgpio_num: %i\r\n"
"\tduty: %i\r\n"
"\thpoint: %i\r\n\n",
pinToGroup(i), pinToChannel(i), pinToTimer(i), ledc_channel.intr_type, pins[i], 0, 0);
pinToGroup(i), pinToChannel(i), pinToTimer(i), ledc_channel.intr_type, pins[i], 0, hpointForPin(i,channel_count));
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
ledc_bind_channel_timer(pinToGroup(i), pinToChannel(i), pinToTimer(i));
}
Expand Down Expand Up @@ -247,7 +266,11 @@ void HardwarePWM::setPeriod(uint32_t period)
// Set the frequency globally, will add per timer functions later.
// Also, this can be done smarter.
for(uint8_t i = 0; i < channel_count; i++) {
ESP_ERROR_CHECK(ledc_set_freq(pinToGroup(i), pinToTimer(i), periodToFrequency(period)));
if(_options.phaseShift.mode!= PWM_PHASE_OFF) {
ESP_ERROR_CHECK(ledc_set_duty_with_hpoint(pinToGroup(i), pinToChannel(i), period, hpointForPin(i,channel_count)));
}else{
ESP_ERROR_CHECK(ledc_set_freq(pinToGroup(i), pinToTimer(i), periodToFrequency(period)));
}
}
// ledc_update_duty();
update();
Expand All @@ -262,3 +285,21 @@ uint32_t HardwarePWM::getFrequency(uint8_t pin) const
{
return ledc_get_freq(pinToGroup(pin), pinToTimer(pin));
}

uint32_t HardwarePWM::getMaxDuty() const
{
return maxduty;
}

int HardwarePWM::hpointForPin(uint8_t channelIndex, uint8_t channel_count)
{
switch (_options.phaseShift.mode) {
case PWM_PHASE_AUTO:
return (maxDuty(DEFAULT_RESOLUTION) * channelIndex / channel_count) + (maxDuty(DEFAULT_RESOLUTION) / channel_count / 2);
case PWM_PHASE_CUSTOM:
return _options.phaseShift.phaseDelayPercent[channelIndex];
case PWM_PHASE_OFF:
default:
return 0;
}
}
110 changes: 110 additions & 0 deletions Sming/Arch/Esp32/Core/HardwarePWM.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/****
* Sming Framework Project - Open Source framework for high efficiency native ESP8266 development.
* Created 2015 by Skurydin Alexey
* http://github.com/SmingHub/Sming
* All files of the Sming Core are provided under the LGPL v3 license.
*
* HardwarePWM.h
*
****/

#pragma once

#include <IHardwarePWM.h>

/**
* @brief ESP32-specific implementation of pulse width modulation
*
* ESP8266 has no hardware PWM and uses an interrupt-based software implementation.
* Period of PWM is fixed to 1000us / Frequency = 1kHz by default
* For a period of 1000, max duty = 25000
* You can use function setPeriod() to change frequency/period.
*/




/* * @param usePhaseShift Use ledc's hpoint phase shift feature to stagger the rising edge of the PWM
* singals evenly across the period. This is useful for driving multiple LEDs

Check failure on line 28 in Sming/Arch/Esp32/Core/HardwarePWM.h

View workflow job for this annotation

GitHub Actions / Check spelling

singals ==> signals, singles
* connected to the same power rail to reduce the peak current draw and help with
* electromagnetic interference.
* @param useSpreadSpectrum implement minimal spread spectrum modulation to further reduce EMI. This
* feature uses a hardware timer to modulate the base frequency of the PWM signal.
* Depending on the base frequency, this may cause quite some system load, so
* choosing a good balance between CPU resources and acceptable EMI is important.
*
* */
class HardwarePWM : public IHardwarePWM
{
public:
/** @brief Instantiate hardware PWM object
* @param pins Pointer to array of pins to control
* @param no_of_pins Quantity of elements in array of pins
* @param usePhaseShift Parameter ignored on ESP8266 (present for API compatibility)
*/
HardwarePWM(const uint8_t* pins, uint8_t no_of_pins);
HardwarePWM(const uint8_t* pins, uint8_t no_of_pins, PWM_Options& options);

virtual ~HardwarePWM();

/** @brief initialize PWM hardware
* @param pins Pointer to array of pins to control
* @param no_of_pins Quantity of elements in array of pins
* @param options PWM options
*/
void Init(const uint8_t* pins, uint8_t no_of_pins, PWM_Options& options);

/** @brief Set PWM duty cycle for a channel
* @param chan Channel to set
* @param duty Value of duty cycle to set channel to
* @param update Update PWM output
* @retval bool True on success
*/
bool setDutyChan(uint8_t chan, uint32_t duty, bool update = true) override;

/** @brief Get PWM duty cycle
* @param chan Channel to get duty cycle for
* @retval uint32_t Value of PWM duty cycle
*/
uint32_t getDutyChan(uint8_t chan) const override;

/** @brief Set PWM period
* @param period PWM period in microseconds
*/
void setPeriod(uint32_t period) override;

/** @brief Get PWM period
* @retval uint32_t Value of PWM period in microseconds
*/
uint32_t getPeriod() const override;

/** @brief Get channel number for a pin
* @param pin GPIO to interrogate
* @retval uint8_t Channel of GPIO
*/
uint8_t getChannel(uint8_t pin) const override;

/** @brief Get the maximum duty cycle value
* @retval uint32_t Maximum permissible duty cycle
*/
uint32_t getMaxDuty() const override;

/** @brief Update PWM to use new settings
*/
void update() override;

/** @brief Get PWM Frequency
* @param pin GPIO to get frequency for
* @retval uint32_t Value of Frequency
*/
uint32_t getFrequency(uint8_t pin) const override;


private:
int hpointForPin(uint8_t channelIndex, uint8_t channel_count);

uint8_t channel_count;
uint8_t channels[PWM_CHANNEL_NUM_MAX];
uint32_t maxduty{0};
PWM_Options _options;
};
2 changes: 2 additions & 0 deletions Sming/Arch/Esp8266/Components/driver/include/driver/pwm.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ extern "C" {

#include <pwm.h>

struct PWM_Options{};

/**
* @defgroup pwm_driver PWM driver
* @ingroup drivers
Expand Down
12 changes: 8 additions & 4 deletions Sming/Arch/Esp8266/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,16 @@ static const uint8_t gpioPinFunc[]{
FUNC_GPIO14, //
FUNC_GPIO15, //
};

HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t noOfPins) : channel_count(noOfPins)
HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins)
{
if(noOfPins == 0) {
if(no_of_pins == 0) {
return;
}

uint32_t ioInfo[PWM_CHANNEL_NUM_MAX][3]; // pin information
uint32_t pwmDutyInit[PWM_CHANNEL_NUM_MAX]; // pwm duty
unsigned pinCount = 0;
for(uint8_t i = 0; i < noOfPins; i++) {
for(uint8_t i = 0; i < no_of_pins; i++) {
auto pin = pins[i];
assert(pin < 16);
if(pin >= 16) {
Expand Down Expand Up @@ -131,3 +130,8 @@ uint32_t HardwarePWM::getFrequency(uint8_t pin) const
auto period = pwm_get_period();
return (period == 0) ? 0 : 1000000U / period;
}

uint32_t HardwarePWM::getMaxDuty() const
{
return maxduty;
}
Loading
Loading