Skip to content

Commit

Permalink
Initial functioning version of ESP32 HardwarePWM. (#2599)
Browse files Browse the repository at this point in the history
* initial functioning version of ESP32 HardwarePWM. Tested to run on the ESP32C3 with up to 5kHz pwm frequency.

* some documentation updates

* more documentation

* more documentation

* implemented requested changes from initial PR

* implemented requested changes from initial PR (now *with* changes. Sigh)

* Revert "more documentation"

This reverts commit 7279c66.

* implemented requested changes from initial PR (now *with* changes. after goofing up with git Sigh)

* fixed some things I didn't understand at first.

* HardwarePWM.h was not part of the last commit

* .cpp was also missing. why?

* Apply coding style

* Rebase, tidy

* Tidy Esp8266 implementation, add new 'getFrequency' method

* Add missing include

* Tidy driver/pwm.h

* Use static initializers

* minor updates as suggested by mikee47

* minor updates as suggested by mikee47

* removed unused variables

* missed one occurrence and also messed up in a merge - cleaned out now

* removed a piece of code that was doubled

* fixed typos and coding style

* removed ToDos from the comments that are to be implemented in the next iteraton as they are already underway

* fixed return values

* changed some coding style issues in esp8266 code

* removed unnecessary and improved useful debug output

* corrected the PWM_CHANNEL_NUM_MAX define in pwm.h to reflect the correct SoC config

* Small coding style fixes.

---------

Co-authored-by: mikee47 <[email protected]>
Co-authored-by: Slavey Karadzhov <[email protected]>
  • Loading branch information
3 people authored Apr 28, 2023
1 parent 4195b90 commit 5eb7426
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 119 deletions.
71 changes: 12 additions & 59 deletions Sming/Arch/Esp32/Components/driver/include/driver/pwm.h
Original file line number Diff line number Diff line change
@@ -1,65 +1,18 @@
/*
* ESPRESSIF MIT License
/****
* 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.
*
* Copyright (c) 2016 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
* pwm.h
*
* 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

#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

#ifdef SOC_LEDC_CHANNEL_NUM
#define PWM_CHANNEL_NUM_MAX SOC_LEDC_CHANNEL_NUM
#else
// this should not happen if the correct esp32 includes are used, just to be absolutely sure
#define PWM_CHANNEL_NUM_MAX 8
#endif
275 changes: 275 additions & 0 deletions Sming/Arch/Esp32/Core/HardwarePWM.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/****
* 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.
*
* 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)
*
* 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) ? 0 : (1000000 / period);
}

uint32_t frequencyToPeriod(uint32_t freq)
{
return (freq == 0) ? 0 : (1000000 / freq);
}

uint32_t maxDuty(ledc_timer_bit_t bits)
{
return (1U << bits) - 1;
}

} //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;
}

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

/*
* Prepare and then apply the LEDC PWM timer configuration.
* This may configure 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) {
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, max duty cycle is %d", maxduty);
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();
}

uint32_t HardwarePWM::getFrequency(uint8_t pin)
{
return ledc_get_freq(pinToGroup(pin), pinToTimer(pin));
}
4 changes: 2 additions & 2 deletions Sming/Arch/Esp8266/Components/driver/include/driver/pwm.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ extern "C" {
*
* Example:
*
* uint32 io_info[][3] = {
* uint32 ioInfo[][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);
* pwm_init(light_param.pwm_period, light_param.pwm_duty, 3, ioInfo);
*
*/

Expand Down
6 changes: 3 additions & 3 deletions Sming/Arch/Esp8266/Components/driver/new-pwm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Example usage:
const uint32_t period = 5000; // * 200ns ^= 1 kHz

// PWM setup
uint32 io_info[PWM_CHANNELS][3] = {
uint32 ioInfo[PWM_CHANNELS][3] = {
// MUX, FUNC, PIN
{PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12, 12},
{PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15, 15},
Expand All @@ -47,9 +47,9 @@ Example usage:
};

// initial duty: all off
uint32 pwm_duty_init[PWM_CHANNELS] = {0, 0, 0, 0, 0};
uint32 pwmDutyInit[PWM_CHANNELS] = {0, 0, 0, 0, 0};

pwm_init(period, pwm_duty_init, PWM_CHANNELS, io_info);
pwm_init(period, pwmDutyInit, PWM_CHANNELS, ioInfo);
pwm_start();

// do something like this whenever you want to change duty
Expand Down
Loading

0 comments on commit 5eb7426

Please sign in to comment.