Skip to content
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

Implement HardwarePWM class for Rp2040 and update Basic PWM sample #2908

Merged
merged 12 commits into from
Nov 5, 2024
18 changes: 4 additions & 14 deletions Sming/Arch/Esp32/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ uint32_t maxDuty(ledc_timer_bit_t bits)

} //namespace

HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins)
HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins)
{
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);
Expand Down Expand Up @@ -206,17 +206,7 @@ HardwarePWM::~HardwarePWM()
}
}

uint8_t HardwarePWM::getChannel(uint8_t pin)
{
for(uint8_t i = 0; i < channel_count; i++) {
if(channels[i] == pin) {
return i;
}
}
return -1;
}

uint32_t HardwarePWM::getDutyChan(uint8_t chan)
uint32_t HardwarePWM::getDutyChan(uint8_t chan) const
{
// esp32 defines the frequency / period per timer
return (chan == PWM_BAD_CHANNEL) ? 0 : ledc_get_duty(pinToGroup(chan), pinToChannel(chan));
Expand Down Expand Up @@ -245,7 +235,7 @@ bool HardwarePWM::setDutyChan(uint8_t chan, uint32_t duty, bool update)
return false;
}

uint32_t HardwarePWM::getPeriod()
uint32_t HardwarePWM::getPeriod() const
{
// Sming does not know how to handle different frequencies for channels: this will require an extended interface.
// For now, just report the period for group 0 channel 0.
Expand All @@ -268,7 +258,7 @@ void HardwarePWM::update()
// ledc_update_duty();
}

uint32_t HardwarePWM::getFrequency(uint8_t pin)
uint32_t HardwarePWM::getFrequency(uint8_t pin) const
{
return ledc_get_freq(pinToGroup(pin), pinToTimer(pin));
}
20 changes: 4 additions & 16 deletions Sming/Arch/Esp8266/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ static const uint8_t gpioPinFunc[]{
FUNC_GPIO15, //
};

HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t noOfPins) : channel_count(noOfPins)
HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t noOfPins) : channel_count(noOfPins)
{
if(noOfPins == 0) {
return;
Expand Down Expand Up @@ -85,19 +85,7 @@ HardwarePWM::~HardwarePWM()
// There is no function in the SDK to stop PWM output, yet.
}

uint8_t HardwarePWM::getChannel(uint8_t pin)
{
for(uint8_t i = 0; i < channel_count; i++) {
if(channels[i] == pin) {
return i;
}
}

debug_d("getChannel: can't find pin %d", pin);
return PWM_BAD_CHANNEL;
}

uint32_t HardwarePWM::getDutyChan(uint8_t chan)
uint32_t HardwarePWM::getDutyChan(uint8_t chan) const
{
return (chan == PWM_BAD_CHANNEL) ? 0 : pwm_get_duty(chan);
}
Expand All @@ -120,7 +108,7 @@ bool HardwarePWM::setDutyChan(uint8_t chan, uint32_t duty, bool update)
return false;
}

uint32_t HardwarePWM::getPeriod()
uint32_t HardwarePWM::getPeriod() const
{
return pwm_get_period();
}
Expand All @@ -137,7 +125,7 @@ void HardwarePWM::update()
pwm_start();
}

uint32_t HardwarePWM::getFrequency(uint8_t pin)
uint32_t HardwarePWM::getFrequency(uint8_t pin) const
{
(void)pin;
auto period = pwm_get_period();
Expand Down
20 changes: 10 additions & 10 deletions Sming/Arch/Host/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,36 @@

#include <HardwarePWM.h>

HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins)
HardwarePWM::HardwarePWM(const uint8_t*, uint8_t no_of_pins) : channel_count(no_of_pins)
{
}

HardwarePWM::~HardwarePWM() = default;

uint8_t HardwarePWM::getChannel(uint8_t pin)
{
return PWM_BAD_CHANNEL;
}

uint32_t HardwarePWM::getDutyChan(uint8_t chan)
uint32_t HardwarePWM::getDutyChan(uint8_t) const
{
return 0;
}

bool HardwarePWM::setDutyChan(uint8_t chan, uint32_t duty, bool update)
bool HardwarePWM::setDutyChan(uint8_t, uint32_t, bool)
{
return false;
}

uint32_t HardwarePWM::getPeriod()
uint32_t HardwarePWM::getPeriod() const
{
return 0;
}

void HardwarePWM::setPeriod(uint32_t period)
void HardwarePWM::setPeriod(uint32_t)
{
}

void HardwarePWM::update()
{
}

uint32_t HardwarePWM::getFrequency(uint8_t) const
{
return 0;
}
86 changes: 3 additions & 83 deletions Sming/Arch/Rp2040/Components/driver/include/driver/pwm.h
Original file line number Diff line number Diff line change
@@ -1,88 +1,8 @@
#pragma once

#if defined(__cplusplus)
extern "C" {
#endif

// #include <pwm.h>

#define PWM_CHANNEL_NUM_MAX 16

/**
* @defgroup pwm_driver PWM driver
* @ingroup drivers
* @{
*/

/**
* @fn void pwm_init(uint32 period, uint32 *duty,uint32 pwm_channel_num,uint32 (*pin_info_list)[3])
* @brief Initialize PWM function, including GPIO selection, period and duty cycle
* @param period PWM period
* @param duty duty cycle of each output
* @param pwm_channel_num PWM channel number
* @param pin_info_list Array containing an entry for each channel giving
* @note This API can be called only once.
*
* Example:
*
* uint32 io_info[][3] = {
* {PWM_0_OUT_IO_MUX, PWM_0_OUT_IO_FUNC, PWM_0_OUT_IO_NUM},
* {PWM_1_OUT_IO_MUX, PWM_1_OUT_IO_FUNC, PWM_1_OUT_IO_NUM},
* {PWM_2_OUT_IO_MUX, PWM_2_OUT_IO_FUNC, PWM_2_OUT_IO_NUM}
* };
*
* pwm_init(light_param.pwm_period, light_param.pwm_duty, 3, io_info);
*
*/

/**
* @fn void pwm_start(void)
* @brief Starts PWM
* @brief Maximum number of active PWM channels.
*
* This function needs to be called after PWM configuration is changed.
* The Pico has 8 PWM 'slices', each of which can drive two outputs.
*/

/**
* @fn void pwm_set_duty(uint32 duty, uint8 channel)
* @brief Sets duty cycle of a PWM output
* @param duty The time that high-level single will last, duty cycle will be (duty*45)/(period*1000)
* @param channel PWM channel, which depends on how many PWM channels are used
*
* Set the time that high-level signal will last.
* The range of duty depends on PWM period. Its maximum value of which can be Period * 1000 / 45.
*
* For example, for 1-KHz PWM, the duty range is 0 ~ 22222.
*/

/**
* @fn uint32 pwm_get_duty(uint8 channel)
* @brief Get duty cycle of PWM output
* @param channel PWM channel, which depends on how many PWM channels are used
* @retval uint32 Duty cycle of PWM output
*
* Duty cycle will be (duty*45) / (period*1000).
*/

/**
* @fn void pwm_set_period(uint32 period)
* @brief Set PWM period
* @param period PWM period in us. For example, 1-KHz PWM period = 1000us.
*/

/**
* @fn uint32 pwm_get_period(void)
* @brief Get PWM period
* @retval uint32 Return PWM period in us.
*/

/**
* @fn uint32 get_pwm_version(void)
* @brief Get version information of PWM
* @retval uint32 PWM version
*/

/** @} */

#if defined(__cplusplus)
}
#endif
#define PWM_CHANNEL_NUM_MAX 16
135 changes: 135 additions & 0 deletions Sming/Arch/Rp2040/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/****
* 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.
*
* Rp2040/Core/HardwarePWM.cpp
*
*/

#include <HardwarePWM.h>
#include <hardware/pwm.h>
#include <hardware/gpio.h>
#include <hardware/clocks.h>
#include <muldiv.h>
#include <algorithm>
#include <debug_progmem.h>

/*
* Note on use of divisor
* ----------------------
*
* Divisor is 8:4 fractional value, so 1 <= int <= 255, 0 <= frac <= 15
* Simplest way to use full range is to factor calculations by 16.
*
* Using default CSR_PH_CORRECT=0:
*
* F_PWM = 16 * F_SYS / (TOP + 1) / DIV
*
*/

#define PWM_FREQ_DEFAULT 1000

HardwarePWM::HardwarePWM(const uint8_t* pins, uint8_t noOfPins) : channel_count(noOfPins)
{
assert(noOfPins > 0 && noOfPins <= PWM_CHANNEL_NUM_MAX);
noOfPins = std::min(uint8_t(PWM_CHANNEL_NUM_MAX), noOfPins);
std::copy_n(pins, noOfPins, channels);
setPeriod(1e6 / PWM_FREQ_DEFAULT);

for(unsigned i = 0; i < noOfPins; ++i) {
auto pin = channels[i];
gpio_set_function(pin, GPIO_FUNC_PWM);
gpio_set_dir(pin, GPIO_OUT);
}
}

HardwarePWM::~HardwarePWM()
{
for(unsigned i = 0; i < channel_count; ++i) {
auto slice_num = pwm_gpio_to_slice_num(channels[i]);
pwm_set_enabled(slice_num, false);
}
}

uint32_t HardwarePWM::getDutyChan(uint8_t chan) const
{
if(chan >= channel_count) {
return 0;
}
auto pin = channels[chan];
auto slice_num = pwm_gpio_to_slice_num(pin);
auto value = pwm_hw->slice[slice_num].cc;
value >>= pwm_gpio_to_channel(pin) ? PWM_CH0_CC_B_LSB : PWM_CH0_CC_A_LSB;
return value & 0xffff;
}

bool HardwarePWM::setDutyChan(uint8_t chan, uint32_t duty, bool)
{
if(chan >= channel_count) {
return false;
}
auto pin = channels[chan];
duty = std::min(duty, maxduty);
pwm_set_gpio_level(pin, duty);
return true;
}

uint32_t HardwarePWM::getPeriod() const
{
// All channels configured with same clock
auto slice_num = pwm_gpio_to_slice_num(channels[0]);
uint32_t top = pwm_hw->slice[slice_num].top;
uint32_t div = pwm_hw->slice[slice_num].div;
return muldiv(62500ULL, (top + 1) * div, clock_get_hz(clk_sys));
}

void HardwarePWM::setPeriod(uint32_t period)
{
const uint32_t topMax{0xffff};
const uint32_t divMin{0x10}; // 1.0
const uint32_t divMax{0xfff}; // INT + FRAC
auto sysFreq = clock_get_hz(clk_sys);
// Calculate divisor assuming maximum value for TOP: ensure value is rounded UP
uint32_t div = ((uint64_t(period) * sysFreq / 62500) + topMax) / (topMax + 1);
uint32_t top;
if(div > divMax) {
// Period too big, set to maximum
top = topMax;
div = divMax;
} else {
if(div < divMin) {
// Period is very small, set div to minimum
div = divMin;
}
top = (uint64_t(period) * sysFreq / 62500 / div) - 1;
}

debug_d("[PWM] %s(%u): div %u, top %u", __FUNCTION__, period, div, top);

pwm_config cfg = pwm_get_default_config();
cfg.div = div;
cfg.top = top;

for(unsigned i = 0; i < channel_count; ++i) {
auto pin = channels[i];
auto slice_num = pwm_gpio_to_slice_num(pin);
pwm_init(slice_num, &cfg, true);
}

maxduty = top;
}

void HardwarePWM::update()
{
// Not implemented
}

uint32_t HardwarePWM::getFrequency(uint8_t pin) const
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note we can get the frequency for an individual pin, added for esp32 support I believe, but no corresponding setFrequency or setPeriod for a pin. We should either do that or get rid of the pin parameter I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the way it's currently implemented for the esp32 is that the frequency is global, not per pin.
While the esp32 platform can set the frequency per timer (of which it has up to eight, depending on the actual Soc), timers are mapped to Pins and there can be more pins than timers.
Getting the frequency for a sigle pin, with the current implementation, makes indeed no sense.
I think this is something that should be part of an extended implementation that exposes the better hardware of the esp32 and rp2040 platforms.

On the BasicHWPwm, I have something that works a bit better for me. I'll clean it up and push it in the next days (I'm travelling right now)

{
auto slice_num = pwm_gpio_to_slice_num(pin);
auto top = pwm_hw->slice[slice_num].top;
auto div = pwm_hw->slice[slice_num].div;
return 16UL * clock_get_hz(clk_sys) / (div * (top + 1));
}
Loading
Loading