ESP32 Timer-based interrupts not reloading properly

Hi All,

Using an ESP32-S3 FN8 on a custom PCB, I'm trying to use a timer-based interrupt to frequently adjust settings on an analog-out pin (i.e., update both frequency of pwm and duration) for a piezo speaker.

Each time the timer alarm fires, the interrupt routine should adjust the pwm frequency and also re-arm the timer alarm for the new updated time period.

Here's the simplest version of the code that exemplifies the issue:

(Note: I've been developing on ESP32 Board Manager package v2.0.14)

#define SPEAKER_PIN       41
#define PWM_CHANNEL       0   // ESP32 has many channels; we'll use the first

#define SPKR_CLK_DIV  40000   // max 65535
#define FX_NOTE_LENGTH  500*2 // timer ticks twice every ms, so multiply desired ms by 2 for proper number of ticks 

hw_timer_t *speaker_timer = NULL;
void onSpeakerTimerSimple(void);
volatile uint16_t tonal_frequency = 440;

void setup() {
  delay(1000);
  Serial.begin(115200);
  delay(1000);

  //configure pinouts and PWM channel
  pinMode(SPEAKER_PIN, OUTPUT);
  ledcSetup(PWM_CHANNEL, 1000, 8);
  ledcAttachPin(SPEAKER_PIN, PWM_CHANNEL);

  //setup speaker timer interrupt to track each "beat" of sound
	speaker_timer = timerBegin(1, SPKR_CLK_DIV, true);                    // timer#, clock divider, count up -- this just configures, but doesn't start.   NOTE: timerEnd() de-configures.
  timerAttachInterrupt(speaker_timer, &onSpeakerTimerSimple, true);     // timer, ISR call, rising edge                                                  NOTE: timerDetachInterrupt() does the opposite
}

void loop() {  
  onSpeakerTimerSimple();
  while(1);
}

void IRAM_ATTR onSpeakerTimerSimple() {
  Serial.print("ENTER ISR: "); Serial.print(tonal_frequency); Serial.print(" @ "); Serial.println(millis());
  ledcWriteTone(PWM_CHANNEL, tonal_frequency);        
  tonal_frequency += 100;

  timerAlarmWrite(speaker_timer, FX_NOTE_LENGTH, false);  // set timer for play period (don't reload; we'll trigger back into this ISR when time is up)
  timerWrite(speaker_timer, 0);                           // start at 0
  timerAlarmEnable(speaker_timer);                        // and.. go!       
}

I expect to see a continuous series of updates every 500ms.

What I actually see is two updates, then nothing. The Serial.print messages are:

ENTER ISR: 100 @ 2103
ENTER ISR: 200 @ 2603

...and that's it. The ISR routine seems to run successfully once from the initial main loop call, and seems to successfully "re-arm" itself, and then gets called once again... but then ceases to fire after that. We only make it through that code path twice.

Any ideas why it doesn't work again after that?

On the off chance the 3.0 update fixed things, I gave that a shot here:

(running on ESP32 v3.0.1)

#define SPEAKER_PIN       41

#define SPKR_CLK_DIV  40000   // max 65535
#define FX_NOTE_LENGTH  1000  // timer ticks per note play

hw_timer_t *speaker_timer = NULL;
void onSpeakerTimerSimple(void);
volatile uint16_t tonal_frequency = 440;

void setup() {
  delay(1000);
  Serial.begin(115200);
  delay(1000);

  //configure pinouts and PWM channel
  pinMode(SPEAKER_PIN, OUTPUT);
  ledcAttach(SPEAKER_PIN, 1000, 10);

  
  //setup speaker timer interrupt to track each "beat" of sound
	speaker_timer = timerBegin(1000);                    
  timerAttachInterrupt(speaker_timer, &onSpeakerTimerSimple);     // timer, ISR call

}

void loop() {  
  onSpeakerTimerSimple();
  while(1);
}

void IRAM_ATTR onSpeakerTimerSimple() {
  Serial.print("ENTER ISR: "); Serial.print(tonal_frequency); Serial.print(" @ "); Serial.println(millis());
  ledcWriteTone(SPEAKER_PIN, tonal_frequency);        
  tonal_frequency += 100;
  if (tonal_frequency >= 2200) tonal_frequency = 100;

  timerAlarm(speaker_timer, FX_NOTE_LENGTH, false, 0);    // set timer for play period (don't reload; we'll trigger back into this ISR when time is up)
  timerWrite(speaker_timer, 0);                           // start at 0
}

Curiously, this time it runs continuously, but twice per "update cycle". Serial.print output is:

ENTER ISR: 440 @ 2097
ENTER ISR: 540 @ 3097
ENTER ISR: 640 @ 3097
ENTER ISR: 740 @ 4097
ENTER ISR: 840 @ 4097
ENTER ISR: 940 @ 5097
ENTER ISR: 1040 @ 5097
... etc etc

Note the millis() timestamp repeating twice. So now the ISR is executing twice in a row each time.

Any help solving these interrupt issues would be much appreciated!

PS: a few notes:

  1. I've learned that putting Serial.print debugging statements inside an ISR is bad form. In this case they're just for clarity during debugging, and I've tried the code with those removed and get the same results.
  2. I'm aware that using interrupts is probably not required. I'm making a variometer for paragliding/sailplanes, where the key feature is adjusting a continuous beeping tone based on rate of climb (altitude change). Beeps get more rapid and higher pitch as climb rate increases. The shortest a "beep" might be is around 100ms. And my main loop code will be handling tasks that take approximately 8~14ms. I'd prefer to not have 8~14ms of variation on a 100ms beep.
    Regardless, one of the main goals of this project is just to learn Arduino and ESP32 architecture, so I'd love to get smarter on interrupt handling!

Thanks!

How about this instead. Just set a flag in the interrupt and do everything else in loop():

#define SPEAKER_PIN 41
#define PWM_CHANNEL 0  // ESP32 has many channels; we'll use the first

#define SPKR_CLK_DIV 40000      // max 65535
#define FX_NOTE_LENGTH 500 * 2  // timer ticks twice every ms, so multiply desired ms by 2 for proper number of ticks

hw_timer_t *speaker_timer = NULL;
void onSpeakerTimerSimple(void);
volatile uint16_t tonal_frequency = 440;
volatile bool triggered = false;

void setup() {
   delay(1000);
   Serial.begin(115200);
   delay(1000);

   //configure pinouts and PWM channel
   pinMode(SPEAKER_PIN, OUTPUT);
   ledcSetup(PWM_CHANNEL, 1000, 8);
   ledcAttachPin(SPEAKER_PIN, PWM_CHANNEL);

   //setup speaker timer interrupt to track each "beat" of sound
   speaker_timer = timerBegin(1, SPKR_CLK_DIV, true);                 // timer#, clock divider, count up -- this just configures, but doesn't start.   NOTE: timerEnd() de-configures.
   timerAlarmWrite(speaker_timer, FX_NOTE_LENGTH, true);              // set timer for play period
   timerWrite(speaker_timer, 0);                                      // start at 0
   timerAlarmEnable(speaker_timer);                                   // and.. go!
   timerAttachInterrupt(speaker_timer, &onSpeakerTimerSimple, true);  // timer, ISR call, rising edge                                                  NOTE: timerDetachInterrupt() does the opposite
}

void loop() {
   if( triggered ) {
      Serial.print("ENTER ISR: ");
      Serial.print(tonal_frequency);
      Serial.print(" @ ");
      Serial.println(millis());
      ledcWriteTone(PWM_CHANNEL, tonal_frequency);
      tonal_frequency += 100;
      triggered = false;
   }
}

void IRAM_ATTR onSpeakerTimerSimple() {
   triggered = true;
}

Output:

Monitor port settings:
baudrate=115200
Connecting to /dev/ttyUSB0. Press CTRL-C to exit.
ENTER ISR: 440 @ 2517
ENTER ISR: 540 @ 3017
ENTER ISR: 640 @ 3517
ENTER ISR: 740 @ 4017
ENTER ISR: 840 @ 4517
ENTER ISR: 940 @ 5017
ENTER ISR: 1040 @ 5517
ENTER ISR: 1140 @ 6017
ENTER ISR: 1240 @ 6517
ENTER ISR: 1340 @ 7017
ENTER ISR: 1440 @ 7517
ENTER ISR: 1540 @ 8017
ENTER ISR: 1640 @ 8517
ENTER ISR: 1740 @ 9017
ENTER ISR: 1840 @ 9517
ENTER ISR: 1940 @ 10017
ENTER ISR: 2040 @ 10517
ENTER ISR: 2140 @ 11017
ENTER ISR: 2240 @ 11517
ENTER ISR: 2340 @ 12017

Thanks for the response van_der_decken!

I certainly appreciate the simplicity of that approach. Though if I'm going to handle the event in the main loop, I don't think I even need the interrupt, I could just do a millis() compare to handle the timing.

The benefit I see of using an interrupt is that I can get the timing more precise. The rest of the main loop will take up to ~14ms depending on what task(s) are being performed, so if interrupt-driven, the short 100ms audio pulses won't be shifting in length by ~14% depending what the rest of the main loop is doing.

And while the code snippet I posted uses a fixed cycle time (FX_NOTE_LENGTH) for simplicity, in reality this duration is being constantly adjusted to a different value (as short as 100ms or maybe even shorter; and as long as 1500ms). And there are also times the audio stops, or some UI noises are played instead of the altitude beeping sounds, which should execute immediately after a button push, for example. So I'd prefer not to wait for the next cycle of an auto-reloading timer as used in your example. So that's the intention of being able to start, stop, and reload the timer at-will, including from the interrupt itself. I realize that's more detail than I included in my first post; and I still appreciate your suggestions though!

Above all, I'd love to learn and understand the ESP32 capabilities more. So would still like to figure this out :slight_smile:

Why is it that you can bang your head against a wall for 2 days, and it's only after you post for help that you get some mental clarity? :sweat_smile:

In reviewing the newer 3.0 APIs and Function changes, I see that writing the new alarm value also starts the timer (writing & starting were two separate steps in v2.x if I understood properly)

So this 3.0 code:

  timerAlarm(speaker_timer, FX_NOTE_LENGTH, false, 0);    // set timer for play period (don't reload; we'll trigger back into this ISR when time is up)
  timerWrite(speaker_timer, 0);                           // start at 0

Should switch order to this:

  timerWrite(speaker_timer, 0);                           // start at 0
  timerAlarm(speaker_timer, FX_NOTE_LENGTH, false, 0);    // set timer for play period (don't reload; we'll trigger back into this ISR when time is up)

Now that works just as expected and removes the repeat calling of the ISR at the same instant. The timer resets and executes the ISR cleanly on the new alarm value.

I still don't know why the V2.0 code was behaving as it was. I'd love to understand that but maybe it will remain a mystery.

Happy to hear any other input folks have about this approach or ESP32 timers and interrupts more generally. Trying to read and learn all I can on the topic so I don't make mistakes or take bad approaches going forward.

Thanks!

Hi @oxothnk423 ,

Welcome to the forum..
Have you seen High Resolution Timer (ESP Timer)..

You can use a one shot high res timer and restart it..
Example blinking a Led..

Just another way..

good luck.. ~q

Here's how you could do it using the IDF APIs and a high-priority FreeRTOS task. I left out the ledc and speaker stuff. But, it would go where the Serial.printf() statements are:

This is the v2.x version:

#include "Arduino.h"
#include "driver/timer.h"

void timerTask(void *params);
bool timerISR(void *param);

void setup() {
	Serial.begin(115200);
	delay(2000);
	Serial.println("Starting");

	xTaskCreatePinnedToCore(timerTask, "Timer", 1000, NULL, 5, NULL, ARDUINO_RUNNING_CORE);
}

void loop() {
	vTaskDelete(NULL);
}

void timerTask(void *params) {
	TaskHandle_t myTaskHandle {xTaskGetCurrentTaskHandle()};
	timer_config_t config = {
			.alarm_en = TIMER_ALARM_EN,
			.counter_en = TIMER_PAUSE,
			.counter_dir = TIMER_COUNT_UP,
			.auto_reload = TIMER_AUTORELOAD_EN,
			.divider = 80 // Run timer at 1 Mhz
			};

	assert((timer_init(TIMER_GROUP_0, TIMER_0, &config)==ESP_OK) && "Failed to Init Timer");
	assert((timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0)==ESP_OK) && "Failed to Set Starting Timer Value");  // Set start and reload count
	assert((timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, 500000)==ESP_OK) && "Failed to Set Alarm Value");  // 500,000 ticks at 1 MHz  = 2 Hz
	assert((timer_enable_intr(TIMER_GROUP_0, TIMER_0)==ESP_OK) && "Failed to Enable Timer Interrupt");  // Enable Timer Interrupt
	assert((timer_isr_callback_add(TIMER_GROUP_0, TIMER_0, timerISR, static_cast<void*>(myTaskHandle), ESP_INTR_FLAG_IRAM)==ESP_OK) && "Failed to Register Interrupt Callback");
	assert((timer_start(TIMER_GROUP_0, TIMER_0)==ESP_OK) && "Failed to Start Timer");;  // Start the timer

	for (;;) {
		ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
		uint32_t tickCount {xTaskGetTickCount()};
		Serial.printf("Timer Interrupt Fired at Tick Count: %lu\n", tickCount);
	}
}

bool IRAM_ATTR timerISR(void *param) {
	TaskHandle_t handle {static_cast<TaskHandle_t>(param)};
	BaseType_t pxHigherPriorityTaskWoken {pdFALSE};
	vTaskNotifyGiveFromISR(handle, &pxHigherPriorityTaskWoken);
	return pxHigherPriorityTaskWoken == pdTRUE;
}

This is the v3.x version:

#include "Arduino.h"
#include "driver/gptimer.h"

void BlinkingTask(void *params);
bool timerCallback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *param);

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println("Starting");
  xTaskCreatePinnedToCore(BlinkingTask, "Blinking", 1000, NULL, 5, NULL, ARDUINO_RUNNING_CORE);
}

void loop() {
  vTaskDelete(NULL);
}

void BlinkingTask(void *params) {
  TaskHandle_t myTaskHandle { xTaskGetCurrentTaskHandle() };
  pinMode(4, OUTPUT);
  digitalWrite(4, LOW);

  gptimer_handle_t gptimer = NULL;
  const gptimer_config_t timer_config = {
      .clk_src = GPTIMER_CLK_SRC_DEFAULT,
      .direction = GPTIMER_COUNT_UP,
      .resolution_hz = 1000000, // 1MHz, 1 tick=1us
      .intr_priority = 0,
      .flags = 0
      };
  assert((gptimer_new_timer(&timer_config, &gptimer)==ESP_OK) && "Failed to Create Timer");

  gptimer_event_callbacks_t cbs = {
      .on_alarm = timerCallback,
  };
  assert((gptimer_register_event_callbacks(gptimer, &cbs, static_cast<void*>(myTaskHandle))==ESP_OK) && "Failed to Register Callback");
  assert((gptimer_enable(gptimer)==ESP_OK) && "Failed to Enable Timer");

  gptimer_alarm_config_t alarm_config ={500000, 0, true};
  assert((gptimer_set_alarm_action(gptimer, &alarm_config)==ESP_OK) && "Failed to Set Alarm Action");
  assert((gptimer_start(gptimer)==ESP_OK) && "Failed to Start Timer");

  for (;;) {
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
    uint32_t tickCount {xTaskGetTickCount()};
    Serial.printf("Timer Interrupt Fired at Tick Count: %lu\n", tickCount);
  }
}

bool IRAM_ATTR timerCallback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *param) {
  TaskHandle_t handle { static_cast<TaskHandle_t>(param) };
  BaseType_t pxHigherPriorityTaskWoken { pdFALSE };
  vTaskNotifyGiveFromISR(handle, &pxHigherPriorityTaskWoken);
  return pxHigherPriorityTaskWoken == pdTRUE;
}

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