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

Initial functioning version of ESP32 HardwarePWM. #2599

Merged
merged 29 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c7e7e33
initial functioning version of ESP32 HardwarePWM. Tested to run on th…
pljakobs Jan 1, 2023
806b9de
some documentation updates
pljakobs Jan 1, 2023
34ca146
more documentation
pljakobs Jan 1, 2023
246744e
more documentation
pljakobs Jan 1, 2023
30b5f1a
implemented requested changes from initial PR
pljakobs Feb 12, 2023
f67fb1a
implemented requested changes from initial PR (now *with* changes. Sigh)
pljakobs Feb 12, 2023
21ecf9a
Revert "more documentation"
pljakobs Feb 12, 2023
be7d04a
implemented requested changes from initial PR (now *with* changes. af…
pljakobs Feb 12, 2023
9e9928c
fixed some things I didn't understand at first.
pljakobs Feb 12, 2023
b48fcd9
HardwarePWM.h was not part of the last commit
pljakobs Feb 12, 2023
9fe74a1
.cpp was also missing. why?
pljakobs Feb 12, 2023
f510bba
Apply coding style
Feb 21, 2023
84e69f1
Rebase, tidy
Feb 21, 2023
eaf77f9
Tidy Esp8266 implementation, add new 'getFrequency' method
Feb 21, 2023
333db82
Add missing include
Feb 21, 2023
b90ea67
Tidy driver/pwm.h
Feb 21, 2023
3c9785f
Use static initializers
Feb 21, 2023
fc2fca2
minor updates as suggested by mikee47
pljakobs Mar 18, 2023
d96bc2f
minor updates as suggested by mikee47
pljakobs Mar 18, 2023
8b82fe0
removed unused variables
pljakobs Mar 30, 2023
d03688b
missed one occurrence and also messed up in a merge - cleaned out now
pljakobs Mar 30, 2023
0e06be0
removed a piece of code that was doubled
pljakobs Apr 1, 2023
b3489a1
fixed typos and coding style
pljakobs Apr 5, 2023
e5c1138
removed ToDos from the comments that are to be implemented in the nex…
pljakobs Apr 8, 2023
5457b4a
fixed return values
pljakobs Apr 19, 2023
dcf2834
changed some coding style issues in esp8266 code
pljakobs Apr 23, 2023
bbacc5a
removed unnecessary and improved useful debug output
pljakobs Apr 23, 2023
e1413ff
corrected the PWM_CHANNEL_NUM_MAX define in pwm.h to reflect the corr…
pljakobs Apr 23, 2023
f03ce14
Small coding style fixes.
slav-at-attachix Apr 28, 2023
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
62 changes: 0 additions & 62 deletions Sming/Arch/Esp32/Components/driver/include/driver/pwm.h
Original file line number Diff line number Diff line change
@@ -1,65 +1,3 @@
/*
* ESPRESSIF MIT License
*
* Copyright (c) 2016 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
*
* Permission is hereby granted for use on ESPRESSIF SYSTEMS ESP8266 only, in which case,
* it is free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/

#ifndef __PWM_H__
#define __PWM_H__

#pragma once
slaff marked this conversation as resolved.
Show resolved Hide resolved

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

/*pwm.h: function and macro definition of PWM API , driver level */
/*user_light.h: user interface for light API, user level*/
/*user_light_adj: API for color changing and lighting effects, user level*/

/*NOTE!! : DO NOT CHANGE THIS FILE*/

/*SUPPORT UP TO 8 PWM CHANNEL*/
#define PWM_CHANNEL_NUM_MAX 8

//struct pwm_param {
// uint32_t period;
// uint32_t freq;
// uint32_t duty[PWM_CHANNEL_NUM_MAX]; //PWM_CHANNEL<=8
//};
//
///* pwm_init should be called only once, for now */
//void pwm_init(uint32_t period, uint32_t* duty, uint32_t pwm_channel_num, uint32_t (*pin_info_list)[3]);
//void pwm_start(void);
//
//void pwm_set_duty(uint32_t duty, uint8_t channel);
//uint32_t pwm_get_duty(uint8_t channel);
//void pwm_set_period(uint32_t period);
//uint32_t pwm_get_period(void);
//
//uint32_t get_pwm_version(void);
//void set_pwm_debug_en(uint8_t print_en);

#if defined(__cplusplus)
}
#endif

#endif
315 changes: 315 additions & 0 deletions Sming/Arch/Esp32/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
/****
* 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.cpp
*
* Original Author: https://github.com/hrsavla
* Esp32 version: https://github.com/pljakobs
*
* This HardwarePWM library enables Sming framework uses to use the ESP32 ledc PWM api
*
* the ESP32 PWM Hardware is much more powerful than the ESP8266, allowing wider PWM timers (up to 20 bit)
* as well as much higher PWM frequencies (up to 40MHz for a 1 Bit wide PWM)
*
* Overview:
* +------------------------------------------------------------------------------------------------+
* | LED_PWM |
* | +-------------------------------------------+ +-------------------------------------------+ |
* | | High_Speed_Channels¹ | | Low_Speed_Channels | |
* | | +-----+ +--------+ | | +-----+ +--------+ | |
* | | | | --> | h_ch 0 | | | | | --> | l_ch 0 | | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | h_timer 0 | --> | | | | | l_timer 0 | --> | | | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | | --> | h_ch 1 | | | | | --> | l_ch 1 | | |
* | | | | +--------+ | | | | +--------+ | |
* | | | | | | | | | |
* | | | | +--------+ | | | | +--------+ | |
* | | | | --> | h_ch 2 | | | | | --> | l_ch 2 | | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | h_timer 1 | --> | | | | | l_timer 1 | --> | | | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | | --> | h_ch 3 | | | | | --> | l_ch 3 | | |
* | | | | +--------+ | | | | +--------+ | |
* | | | MUX | | | | MUX | | |
* | | | | +--------+ | | | | +--------+ | |
* | | | | --> | h_ch 4 | | | | | --> | l_ch 4 | | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | h_timer 2 | --> | | | | | l_timer 2 | --> | | | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | | --> | h_ch 5 | | | | | --> | l_ch 5 | | |
* | | | | +--------+ | | | | +--------+ | |
* | | | | | | | | | |
* | | | | +--------+ | | | | +--------+ | |
* | | | | --> | h_ch 6 | | | | | --> | l_ch 6²| | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | h_timer 3 | --> | | | | | l_timer 3 | --> | | | |
* | | +-----------+ | | +--------+ | | +-----------+ | | +--------+ | |
* | | | | --> | h_ch 7 | | | | | --> | l_ch 7²| | |
* | | | | +--------+ | | | | +--------+ | |
* | | +-----+ | | +-----+ | |
* | +-------------------------------------------+ +-------------------------------------------+ |
* +------------------------------------------------------------------------------------------------+
* ¹ High speed channels are only available when SOC_LEDC_SUPPORT_HS_MODE is defined as 1
* ² The ESP32C3 does only support six channels, so 6 and 7 are not available on that SoC
*
* The nomenclature of timers in the high speed / low speed blocks is a bit misleading as the idf api
* speaks of "speed mode", which, to me, implies that this would be a mode configurable in a specific timer
* while in reality, it does select a block of timers. I am considering re-naming that to "speed mode block"
* in my interface impmenentation.
*
* As an example, I would use
* setTimerFrequency(speedModeBlock, timer, frequency);
*
* ToDo: see, how this can be implemented to provide maximum overlap with the RP2040 pwm hardware so code does
* not become overly SoC specific.
*
* Maximum Timer width for PWM:
* ============================
* esp32 SOC_LEDC_TIMER_BIT_WIDE_NUM (20)
* esp32c3 SOC_LEDC_TIMER_BIT_WIDE_NUM (14)
* esp32s2 SOC_LEDC_TIMER_BIT_WIDE_NUM (14)
* esp32s3 SOC_LEDC_TIMER_BIT_WIDE_NUM (14)
*
* Number of Channels:
* ===================
* esp32 SOC_LEDC_CHANNEL_NUM (8)
* esp32c3 SOC_LEDC_CHANNEL_NUM (6)
* esp32s2 SOC_LEDC_CHANNEL_NUM (8)
* esp32s3 SOC_LEDC_CHANNEL_NUM 8
*
* Some SoSs support a mode called HIGHSPEED_MODE which is essentially another full block of PWM hardware
* that adds SOC_LEDC_CHANNEL_NUM channels.
* Those Architectures have SOC_LEDC_SUPPORT_HS_MODE defined as 1.
* In esp-idf-4.3 that's currently only the esp32 SOC
*
* Supports highspeed mode:
* ========================
* esp32 SOC_LEDC_SUPPORT_HS_MODE (1)
*
* ToDo: implement awareness of hs mode availablility
* ==================================================
* currently, the code just uses a % 8 operation on the pin index to calculate whether to assign a pin to either
* high speed or low speed pwm blocks. This doesn't make a whole lot of sense since it makes it impossible
* for Sming devs to actually use the functionality behind it.
* Also, it currently does not reflect the fact that different SOCs have a different number of channels per block
* (specifically, the esp32c3 only has six channels and no highspeed mode).
* I will continue in two ways:
* - implement the "vanilla" Sming HardwarePWM interface that will hide the underlying architecture but allow up to 16
* channels on an ESP32
* - implement overloads for the relevant functions that allow selecting hs mode where applicable.
*
* ToDo: implement PWM bit width control
* =====================================
* the current HardwarePWM implementation does not care about the PWM timer bit width. To leverage the functionality
* of the ESP32 hardware, it's necessary to make this configurable. As the width is per timer and all the Sming defined
* functions are basically per pin, this needs to be an extension of the overal model, exposing at least timers.
* This, too, will require a compatible "basic" interface and an advanced interface that allows assiging pins (channels)
* to timers and the configuration of the timers themselves.
* The esp_idf does not provide a way to read the bit width configured for a channel, but I think it'll be useful to be able
* to read back this value, not least to find the value for getMaxDuty() for a channel. It will either have to be stored in the
* module or maybe read from the hardware directly (LEDC_[HL]STIMERx_CONF_REG & 0x1f)
*
* ToDo: implement an abstraction layer
* ====================================
* as it stands now, this impelmentation does not provide a function to synchronize all the PWM channels (HardwarePWM::update())
* It might be a good idea to provide an intermediary abstraction that handles all the low level PWM functions (such as flexible
* timer to channel assignments, hs/ls mode awareness, pwm bit width etc) and implements the update() function on that level.
*
* hardware technical reference:
* =============================
* https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf#ledpwm
*
* Overview of the whole ledc-system here:
* https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html
*
* the ledc interface also exposes some advanced functions such as fades that are then done in hardware.
* ToDo: implement a Sming interface for fades
*
*/

#include <HardwarePWM.h>
#include <debug_progmem.h>
#include <driver/periph_ctrl.h>
#include <driver/ledc.h>
#include <esp_err.h>
#include <hal/ledc_types.h>

#define DEFAULT_RESOLUTION ledc_timer_bit_t(10)
#define DEFAULT_PERIOD 200
namespace
{
ledc_channel_t pinToChannel(uint8_t pin)
{
return ledc_channel_t(pin % 8);
}

ledc_mode_t pinToGroup(uint8_t pin)
{
return ledc_mode_t(pin / 8);
}

ledc_timer_t pinToTimer(uint8_t pin)
{
return ledc_timer_t((pin / 2) % 4);
}

uint32_t periodToFrequency(uint32_t period)
{
return (period == 0) ? -1 : (1000000 / period);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, change the return value to be a valid uint32_t. -1 is not a valid unsigned int.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}

uint32_t frequencyToPeriod(uint32_t freq)
{
return (freq == 0) ? -1 : (1000000 / freq);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, change the return value to be a valid uint32_t. -1 is not a valid unsigned int.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}
pljakobs marked this conversation as resolved.
Show resolved Hide resolved

uint32_t maxDuty(ledc_timer_bit_t bits)
{
return (1U << bits) - 1;
slaff marked this conversation as resolved.
Show resolved Hide resolved
}

} //namespace

HardwarePWM::HardwarePWM(uint8_t* pins, uint8_t no_of_pins) : channel_count(no_of_pins)
{
debug_d("starting HardwarePWM init");
periph_module_enable(PERIPH_LEDC_MODULE);
if((no_of_pins == 0) || (no_of_pins > SOC_LEDC_CHANNEL_NUM)) {
return;
}

uint32_t io_info[SOC_LEDC_CHANNEL_NUM][3]; // pin information
uint32_t pwm_duty_init[SOC_LEDC_CHANNEL_NUM]; // pwm duty
for(uint8_t i = 0; i < no_of_pins; i++) {
pwm_duty_init[i] = 0; // Start with zero output
Copy link
Contributor

Choose a reason for hiding this comment

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

io_info and pwm_duty_init aren't used, please remove

channels[i] = pins[i];

/*
* Prepare and then apply the LEDC PWM timer configuration.
* This may cofigure the same timer more than once (in fact up to 8 times)
* which should not be an issue, though, since the values should be the same for all timers
*/
// The two groups (if available) are operating in different speed modes, hence speed mode is an alias for group or vice versa
ledc_timer_config_t ledc_timer{
.speed_mode = pinToGroup(i),
.duty_resolution = LEDC_TIMER_10_BIT, // todo: make configurable later
.timer_num = pinToTimer(i),
.freq_hz = periodToFrequency(DEFAULT_PERIOD), // todo: make configurable later
.clk_cfg = LEDC_AUTO_CLK,
};

debug_d("ledc_timer.\r\n"
"\tspeed_mode: %i\r\n"
"\ttimer_num: %i\r\n"
"\tduty_resolution: %i\r\n"
"\tfreq: %i\n\tclk_cfg: %i\r\n\n",
ledc_timer.speed_mode, ledc_timer.timer_num, ledc_timer.duty_resolution, ledc_timer.freq_hz,
ledc_timer.clk_cfg);
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

/*
* Prepare and then apply the LEDC PWM channel configuration
*/
ledc_channel_config_t ledc_channel{
.gpio_num = pins[i],
.speed_mode = pinToGroup(i),
.channel = pinToChannel(i),
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = pinToTimer(i),
.duty = 0,
.hpoint = 0,
};
debug_d("ledc_channel\n"
"\tspeed_mode: %i\r\n"
"\tchannel: %i\r\n"
"\ttimer_sel %i\r\n"
"\tintr_type: %i\r\n"
"\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);
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
ledc_bind_channel_timer(pinToGroup(i), pinToChannel(i), pinToTimer(i));
}
maxduty = maxDuty(DEFAULT_RESOLUTION);
}

HardwarePWM::~HardwarePWM()
{
// Stop pwm for all pins and set idle level to 0
for(uint8_t i = 0; i < channel_count; i++) {
ledc_stop(pinToGroup(i), pinToChannel(i), 0);
}
}

uint8_t HardwarePWM::getChannel(uint8_t pin)
{
for(uint8_t i = 0; i < channel_count; i++) {
if(channels[i] == pin) {
// debug_d("getChannel %d is %d", pin, i);
Copy link
Contributor

Choose a reason for hiding this comment

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

Either remove the commented line or uncomment it. I would prefer to have it removed if it is not improving the debug output.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

return i;
}
}
return -1;
}

uint32_t HardwarePWM::getDutyChan(uint8_t chan)
{
// esp32 defines the frequency / period per timer
return (chan == PWM_BAD_CHANNEL) ? 0 : ledc_get_duty(pinToGroup(chan), pinToChannel(chan));
}

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

if(duty <= maxduty) {
ESP_ERROR_CHECK(ledc_set_duty(pinToGroup(chan), pinToChannel(chan), duty));
/*
* ignoring the update flag in this release, ToDo: implement a synchronized update mechanism
* if(update) {
* ESP_ERROR_CHECK(ledc_update_duty(pinToGroup(chan), pinToChannel(chan)));
* //update();
* }
*/
ESP_ERROR_CHECK(ledc_update_duty(pinToGroup(chan), pinToChannel(chan)));
return true;
}

debug_d("Duty cycle value too high for current period.");
return false;
}

uint32_t HardwarePWM::getPeriod()
{
// 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.
return frequencyToPeriod(ledc_get_freq(ledc_mode_t(0), ledc_timer_t(0)));
}

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)));
}
// ledc_update_duty();
update();
}

void HardwarePWM::update()
{
// ledc_update_duty();
pljakobs marked this conversation as resolved.
Show resolved Hide resolved
}

uint32_t HardwarePWM::getFrequency(uint8_t pin)
{
return ledc_get_freq(pinToGroup(pin), pinToTimer(pin));
}
Loading