How to stop PWM in single-pulse function?

Hello, I try to create a single pulse function in a "fire and forget" fashion. It's purpose is to control a bi-stable relay.

The code below intends to create a single pulse (duration 50ms on GPIO02 in this example). The pulse width is fine, however the output produces two pulses before it stops. How to resolve?

#define SingleShotOutput 	2 
#define PWMchannel 		1

void IRAM_ATTR pwmstop()
{
  ledcWrite(PWMchannel, 0);
}

void setup() 
{
  ledcAttachPin(SingleShotOutput, PWMchannel);
  pinMode(SingleShotOutput, OUTPUT);
  ledcSetup(PWMchannel, 10, 8); // 10 Hz PWM (100ms), 8-bit resolution
  attachInterrupt(digitalPinToInterrupt(SingleShotOutput), pwmstop, FALLING);
}

void loop() 
{
  ledcWrite(PWMchannel, 128); //50% duty cycle to achieve 50ms on-time
  delay(2000); // Wait a long time to restart the single pulse again
}

Which controller (and libraries) are you using?

It's a ESP32 Dev module, and i'm just using the code as listed above, no additional library's.

On an AVR I'd suspect buffered update of the compare register. For the ESP you can check the data sheet or ask your question in an ESP forum.

#include "sdkconfig.h" // used for log printing
#include "esp_system.h"
#include "freertos/FreeRTOS.h" //freeRTOS items to be used
#include "freertos/task.h"
////
#define evtDoSinglePulse     ( 1 << 0 ) // declare an event
EventGroupHandle_t eg; // variable for the event group handle
// 
esp_timer_handle_t oneshot_timer; //veriable to store the hardware timer handle
//
void IRAM_ATTR oneshot_timer_callback( void* arg )
{
  BaseType_t xHigherPriorityTaskWoken;
  xEventGroupSetBitsFromISR( eg, evtDoSinglePulse, &xHigherPriorityTaskWoken );
} //void IRAM_ATTR oneshot_timer_callback( void* arg )
////
void setup() 
{
  eg = xEventGroupCreate(); // get an event group handle
  //
  gpio_config_t io_cfg = {}; // initialize the gpio configuration structure
  io_cfg.mode = GPIO_MODE_OUTPUT; // set gpio mode
  io_cfg.pin_bit_mask = ( (1ULL << GPIO_NUM_2) ); //bit mask of the pins to set
  gpio_config(&io_cfg); // configure the gpio based upon the parameters as set in the configuration structure
  gpio_set_level( GPIO_NUM_2, LOW);
  // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html?highlight=hardware%20timer High Resoultion Timer API
  esp_timer_create_args_t oneshot_timer_args = {}; // initialize High Resoulition Timer (HRT) configuration structure
  oneshot_timer_args.callback = &oneshot_timer_callback; // configure for callback, name of callback function
  esp_timer_create( &oneshot_timer_args, &oneshot_timer ); // assign configuration to the HRT, receive timer handle
//
  xTaskCreatePinnedToCore( fDoTheSInglePulseThing, "fDoTheSInglePulseThing", 5000,  NULL, 5, NULL, 1 );
}
////
void fDoTheSInglePulseThing( void * parameter )
{
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xFrequency = 2000; //delay for mS
  for (;;)
  {
    gpio_set_level( GPIO_NUM_2, HIGH );
    esp_timer_start_once( oneshot_timer, 50000 ); // trigger one shot timer for a 50000us timeout, 50ms
    xEventGroupWaitBits (eg, evtDoSinglePulse, pdTRUE, pdTRUE, portMAX_DELAY ); // event will be triggered by the timer expiring, wait here for the 50000uS
    gpio_set_level( GPIO_NUM_2, LOW );
    xLastWakeTime = xTaskGetTickCount();
    vTaskDelayUntil( &xLastWakeTime, xFrequency );
  }
  vTaskDelete( NULL );
}// end void fDoTheSInglePulseThing

////
void loop() {}

Not tested.
Is one way to do the thing with a ESP32.

I use the HRT, High Resolution Timer, to cause a delay, non-blocking, of 50000us and a task that repeats once every 2000ms.

50000us is a short time to see, so I did not include any PWM code to ignite the LED, just use GPIO High/Low.

void
pulse (
    int  pin)
{
    digitalWrite (pin, HIGH);
    delay (50);
    digitalWrite (pin, LOW);
}

Idahowalker & Greg: thanks for your suggestions. However, my search is to find a solution without the use of a timer or delay function as I want to use it in a complex pattern of multiple relays to be operated (which may take 500ms to execute and I do not want the loop to wait for that).

My code is basically suitable, however the extra pulse seems to be a library bug. Considering the 50ms between the two pulses I cannot believe it is related to latency.

Whilst experimenting I discovered the following: I changed the line in the interrupt handler from ledcWrite(PWMchannel, 0); into ledcWrite(PWMchannel, 65) and noticed that the duty cycle also changes after two pulses. That brings me to the idea that the code behind ledcwrite is actually a hidden TASK that needs time to schedule and execute. Could this hypothesis be true?

if two pulses occur.

just as a quick test did you try

attachInterrupt(digitalPinToInterrupt(SingleShotOutput), pwmstop, RISING);

?

executing a few lines with a timer-interrupt is finished in less than **0,**1 milliseconds.

Another approach would be to setup a timerinterrupt that checks a flag-variable.
if a latency of up to 50 milliseconds is OK this would be simply
if flag is set to true => IOn-pin HIGH
set flag false
on next call flag is false => set IO-pin LOW

at a timer-interrupt-frequency of 20 Hz the pulse would be 50 ms long
on first call of timer-interrupt set IO-pin HIGH
on second call set IO-pin LOW
==> 50 ms pulse.
inside loop do

generatePulse = true;

and you are done

inside the timer-ISR

if (generatePulse) { // wenever flag is true
  generatePulse = false; // reset flag to false
  digitalWrite(myPulsePin,HIGH);
}
else { // whenever flag is false
  digitalWrite(myPulsePin,LOW); 
}

If a latency of 50 ms is too long
you could setup a much higher frequency and then store a timestampe when createPulse is true
and you would have to check for elapsed time >= 50 ms

checking for elapsed time is completely different from a delay()

delay() waits until time is over
an if-condition like

if (millis() - StartOfPulse >= 50)
is executed very fast and very often.
this checking is repeated inside the timer-ISR until condition becomes true

best regards Stefan

I am delighted to report that the issue is solved. A very recent update of the ESP32 library solved my issue. So, the code at the top of this topic may serve as an example of a very easy and straightforward one-shot function. Thanks for your support!

I have to update my previous post. The solution was not the library update (i tried that as well) but to remove the line "pinMode(SingleShotOutput, OUTPUT);"

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.