PlatformIO Community

ESP32 Change PWM Duty Cycle in Interrupt possible?

Hello, I’m using platfomio on vscode with Arduino Framework and try to change the duty cycle of a pwm channel in an Interrupt. Is there any possibility to do that? The ledcWrite function that is used typically to change a duty Cycle does not work because there is no IRAM_ATTR in the ledcWrite function and the functions below that.

The Project Compiles fine but the PWM doesn’t change in Interrupt.

Basic Example would look something like this:
main.cpp

#include <Arduino.h>
#define PWM_PIN 16
hw_timer_t * timer0 = NULL;
portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED;
volatile uint32_t duty = 0;

void IRAM_ATTR TimerHandler0(void)
{
   ledcWrite(1, duty++);
}

void setup(){
   Serial.begin(115200);
   timer0 = timerBegin(0, 80, true); // 12,5 ns * 80 = 1000ns = 1us
   timerAttachInterrupt(timer0, &TimerHandler0, false); 
   timerAlarmWrite(timer0, 5000, true); // 1us * 5000 = 5ms
   timerAlarmEnable(timer0);
   pinMode(PWM_PIN, OUTPUT);
   ledcSetup(1, 4000, 8);
   ledcAttachPin(PWM_PIN, 1);
   ledcWrite(1, 0);
}
void loop()
{
}

The only thing I can think of is that you copy the implementation of ledcWrite (https://github.com/espressif/arduino-esp32/blob/46d5afb17fb91965632dc5fef237117e1fe947fc/cores/esp32/esp32-hal-ledc.c#L197-L223 for the 1.0.6 core), remove the mutex locks and mark it as IRAM. The function does only direct peripheral writes to LEDC as far as I can see, so it should work fine.

The alternative would be to as discussed in the previous topic to turn off the IRAM allocation flags in the timer implementation to allow the function to be run from flash (or mixed IRAM and flash for speed). But that disables the interrupt during flash operations and thus your application might not respond to critical input.

It works as you implied here is the new function myledcWrite that repaces ledcWrite:

void IRAM_ATTR myledcWrite(uint8_t chan, uint32_t duty)
{
    if (chan > 15)
    {
        return;
    }
    uint8_t group = (chan / 8), channel = (chan % 8);
    LEDC_CHAN(group, channel).duty.duty = duty << 4; //25 bit (21.4)
    if (duty)
    {
        LEDC_CHAN(group, channel).conf0.sig_out_en = 1; //This is the output enable control bit for channel
        LEDC_CHAN(group, channel).conf1.duty_start = 1; //When duty_num duty_cycle and duty_scale has been configured. these register won't take effect until set duty_start. this bit is automatically cleared by hardware.
        if (group)
        {
            LEDC_CHAN(group, channel).conf0.low_speed_update = 1;
        }
        else
        {
            LEDC_CHAN(group, channel).conf0.clk_en = 1;
        }
    }
    else
    {
        LEDC_CHAN(group, channel).conf0.sig_out_en = 0; //This is the output enable control bit for channel
        LEDC_CHAN(group, channel).conf1.duty_start = 0; //When duty_num duty_cycle and duty_scale has been configured. these register won't take effect until set duty_start. this bit is automatically cleared by hardware.
        if (group)
        {
            LEDC_CHAN(group, channel).conf0.low_speed_update = 1;
        }
        else
        {
            LEDC_CHAN(group, channel).conf0.clk_en = 0;
        }
    }
}

With the v2.x.x ESP32 Arduino core things have changed (quite a lot). You can call ledcWrite() from within an interrupt when compiling under the Arduino IDE. However, just like what was seen above any calls to ledcWrite() from within an interrupt with code generated by PlatformIO randomly crashes. I thought it was a fluke (and I suppose it still could be) that this worked under v2.0.2 of the Arduino core when compiling under the Arduino IDE, but it definitely works and I saw a mention of this somewhere when I was trying to track down a solution. My problem is that I have switched to PlatformIO and now I am back to having this not work from within an interrupt again.

So, I do have everything in PlatformIO updated. Using v3.5.0 of the Espressif plaform. I am not sure how that equates to the Arduino core though. I am new to PlatformIO so I am not familiar with it yet.

I am using this setup for the .ini

platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
lib_extra_dirs = ~/Documents/Arduino/libraries
monitor_speed = 115200

I have tried following the info above and taking the code from latest Arduino core (2.0.2), but I can’t get it to compile.

void IRAM_ATTR myledcWrite(uint8_t chan, uint32_t duty)
{
    if(chan >= LEDC_CHANNELS){
        return;
    }

    uint8_t group=(chan/8), channel=(chan%8);

    //Fixing if all bits in resolution is set = LEDC FULL ON

    uint32_t max_duty = (1 << channels_resolution[chan]) - 1;
    if(duty == max_duty){
        duty = max_duty + 1;
    }
    ledc_set_duty(group, channel, duty);
    ledc_update_duty(group, channel);
}

I tried every include file I could think of as well, and the errors are things are not defined, ie:

LEDC_CHANNELS' was not declared in this scope
     if(chan >= LEDC_CHANNELS)

I also get this when I look at “problems”:

identifier "LEDC_CHANNELS" is undefined
identifier "channels_resolution" is undefined
argument of type "uint8_t" is incompatible with parameter of type "ledc_mode_t"
argument of type "uint8_t" is incompatible with parameter of type "ledc_channel_t"
argument of type "uint8_t" is incompatible with parameter of type "ledc_mode_t"
argument of type "uint8_t" is incompatible with parameter of type "ledc_channel_t"

Any ideas on how to make this work with the latest Arudino framework?

Thanks!

PlatformIO is still using Arduino-ESP32 core 1.0.6. So if you copy code from the 2.0.2 core into it, it will likely not work.

There’s a technical preview for 2.0.2 available as instructed in https://github.com/platformio/platform-espressif32/issues/619#issuecomment-1002306803. Does that work when you replace your current platform = .. accordingly?

Ah… ok… thanks. So, if I try to use the 1.0.6 code above I get a single error message:

identifier “LEDC_CHAN” is undefined

Any idea WHERE that is defined?

I see the link to the 2.0.2.zip, but I am not sure where this goes. Like I said, I am brand new to PlatformIO. I was stunned that my compile time went from over 5 minutes under the Arduino IDE to 12 seconds.with PlatformIO, so I don’t want to go back! :slight_smile:

Thanks!

Did you include what the reference file includes?

#include "soc/dport_reg.h"
#include "soc/ledc_reg.h"
#include "soc/ledc_struct.h"

etc

I tried a variety of includes… no dport_reg.h for sure. I will look at this again.

Actually, hold up. There is not referenced in 1.0.6 code. It just does

Are you 1000% sure you copied the 1.0.6 code?

With 2.0.2, there is that define.

I copied the code that was marked as the solution (above).
https://community.platformio.org/t/esp32-change-pwm-duty-cycle-in-interrupt-possible/24319/3

I didn’t take code from 1.0.6 since I figured the above code would work as it was suppose to be the solution. I thought that PlatformIO was using the 2.0.2 core already which is why I was going down that road.

And that code does not contain LEDC_CHANNELS.

identifier “LEDC_CHAN” is undefined

That is the message I get when I try to compile the above code.

C:/Users/Jim/Documents/PlatformIO/Projects/CameraSync/src/CameraSync.ino: In function 'void myledcWrite(uint8_t, uint32_t)':
C:/Users/Jim/Documents/PlatformIO/Projects/CameraSync/src/CameraSync.ino:788:29: error: 'LEDC_CHAN' was not declared in this scope
     LEDC_CHAN(group, channel).duty.duty = duty << 4; //25 bit (21.4)

Oh right, they define that macro in the file.

Woot! \o/

I got it to compile by adding these:

#include "soc/dport_reg.h"
#include "soc/ledc_reg.h"
#include "soc/ledc_struct.h"
#include "esp32-hal-ledc.h"
#include "esp32-hal-ledc.C"

I am not sure which one(s) made the difference, but it now works. I do definitely want to look into what is necessary to try the 2.0.2. core though. There were a ton of changes (beside the interrupt fix for ledc).

oh oh no, don’t include a .c file in a .cpp file, this will lead to a reimplementation of these functions with a C++ symbol name. use

#include <Arduino.h>
#include "soc/ledc_reg.h"
#include "soc/ledc_struct.h"
 
#define LEDC_CHAN(g,c) LEDC.channel_group[(g)].channel[(c)]
#define LEDC_TIMER(g,t) LEDC.timer_group[(g)].timer[(t)] 

void IRAM_ATTR myledcWrite(uint8_t chan, uint32_t duty)
{
    if (chan > 15)
    {
        return;
    }
    uint8_t group = (chan / 8), channel = (chan % 8);
    LEDC_CHAN(group, channel).duty.duty = duty << 4; //25 bit (21.4)
    if (duty)
    {
        LEDC_CHAN(group, channel).conf0.sig_out_en = 1; //This is the output enable control bit for channel
        LEDC_CHAN(group, channel).conf1.duty_start = 1; //When duty_num duty_cycle and duty_scale has been configured. these register won't take effect until set duty_start. this bit is automatically cleared by  hardware.
        if (group)
        {
            LEDC_CHAN(group, channel).conf0.low_speed_update = 1;
        }
        else
        {
            LEDC_CHAN(group, channel).conf0.clk_en = 1;
        }
    }
    else
    {
        LEDC_CHAN(group, channel).conf0.sig_out_en = 0; //This is the output enable control bit for channel
        LEDC_CHAN(group, channel).conf1.duty_start = 0; //When duty_num duty_cycle and duty_scale has been configured. these register won't take effect until set duty_start. this bit is automatically cleared by hardware.
        if (group)
        {
            LEDC_CHAN(group, channel).conf0.low_speed_update = 1;
        }
        else
        {
            LEDC_CHAN(group, channel).conf0.clk_en = 0;
        }
    }
}

void setup() {
   myledcWrite(0, 1024);
}

void loop(){}

OK… thanks for this info as well! :slight_smile:

Is there a way to make the ledc in the “high speed” group? Changes to the duty cycle cause the PWM to freak out during the change… so, a LED controlled by the ledc function will flicker anytime the duty cycle is changed, and based on the description from Espressif’s ledc technical data info, only the high speed group queues the change until the next cycle whereas the low speed group changes it immediately and will glitch.

Better ask Espressif – if this occurs in the regular Arduino IDE too, they surely wanna fix that.