Hardware timer issue with ESP32

I’m trying to follow this tutorial explaining the interaction between FreeRTOS and the ESP32 hardware timers. As such, I have the following code right now:

static const uint16_t timer_prescaler = 80; // Clock ticks at 80 MHz / 80 = 1 MHz
static const uint64_t timer_max_count = 1e7; // 1e7 max count means that the timer will reset after 1 second

static hw_timer_t *timer = NULL;

// IRAM_ATTR forces the ISR to live in internal RAM and not the flash for faster access
void IRAM_ATTR onTimer() {
  Serial.println("ISR");
  int pin_state = digitalRead(led_pin);
  Serial.println(pin_state);
  digitalWrite(led_pin, !pin_state);
}

void setup() {
  Serial.begin(115200);
  pinMode(led_pin, OUTPUT);

  vTaskDelay(1000 / portTICK_PERIOD_MS); // Wait a moment to start
  Serial.println();
  Serial.println("--- FreeRTOS Hardware Timer Demo ---");

  // Create and start the hardware timer and attach the ISR
  timer = timerBegin(0 /* timer number */, timer_prescaler, true /* countUp? */);
  timerAttachInterrupt(timer, &onTimer, true /* edge */);
  timerAlarmWrite(timer, timer_max_count, true /* autoreload? */);

  // Allow the ISR to trigger
  timerAlarmEnable(timer);
}

void loop() {
}

I would expect this to at the very least print “ISR” to the serial port every second, but it doesn’t for some reason:

ESP-ROM:esp32s2-rc4-20191025
Build:Oct 25 2019
rst:0x1 (POWERON),boot:0x8 (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:2
load:0x3ffe6100,len:0x424
load:0x4004c000,len:0x844
load:0x40050000,len:0x2460
entry 0x4004c180
�
--- FreeRTOS Hardware Timer Demo ---

I suspect that the issue is either in some issue with the timer configuration or my custom build target:

[env:ESP32-S2-Mini-1] ; See https://community.platformio.org/t/using-the-platformio-and-the-arduino-framework-with-the-esp32-s2-devkitm-1/21930
platform = espressif32
platform_packages =
	toolchain-xtensa32s2
	framework-arduinoespressif32@https://github.com/espressif/arduino-esp32.git#2.0.0-alpha1
framework = arduino

board = esp32dev
board_build.mcu = esp32s2
board_build.partitions = huge_app.csv

build_unflags =
  -DARDUINO_ESP32_DEV
  -DARDUINO_VARIANT="esp32"
build_flags =
  -DARDUINO_MAGTAG29_ESP32S2
  ; -DARDUINO_SERIAL_PORT=1
  -DARDUINO_VARIANT="adafruit_magtag29_esp32s2"
  ; -DBOARD_HAS_PSRAM
  ; -DCORE_DEBUG_LEVEL=5

; Alternative baud rate for PART3
; monitor_speed = 300
monitor_speed = 115200

Could anyone help me figure out what’s going on? I’m at a loss :slight_smile:

Doing a print in an interrupt function should be an instant crash. The way I see the code in the tutorial is that it only increments a variable, protected by a critical section. The main loop then continuously prints the variable. Does that work?

Oh interesting. I was trying the Serial.prints as a debugging step because the code from the tutorial wasn’t working. (i.e. the LED wasn’t flashing)

I’ll check and see if introduction-to-rtos/esp32-freertos-09-demo-timer-interrupt.ino at main · ShawnHymel/introduction-to-rtos · GitHub is working normally on my ESP32 (not a S2 version). This should narrow it down where the fault is.

1 Like

The ESP32-S2 TimerInterrupt is different from that of ESP32.

Try examples in my ESP32_S2_TimerInterrupt Library, then compare to those in standard ESP32TimerInterrupt to verify.

The API in those examples is quite foreign to me… I don’t think I’ve ever seen those functions/objects before. Could you briefly explain what the notable differences are for hardware timer interrupts?

EDIT: I realize my prior comments may seem agitated or angry. I’m very interested in what you have to say, but I honestly don’t really understand how the libraries you linked to relate to the ESP-IDF Arduino core’s implementation of hardware timer interrupts.

Hey Max! Were you able to try the program on your ESP32?

Ah, sorry for forgetting about this, but yes, I’ve just tested the linked sketch and it actually does not work. There is no blinky on my defined LED pin (2). Even looking with an oscilliscope at 12MHz (for a possible high-speed signal due to wrongly configured timer), IO2 is always low. Let me investigate on why that is.

1 Like

There is a possibility that this is a bug related to the Arduino-ESP32 code and optimization. Further digging by printing some debug info in the loop

void loop() {
  Serial.println("Timer seconds: " + String(timerReadSeconds(timer)));
  Serial.println("Timer alarm seconds: " + String(timerAlarmReadSeconds(timer)));
  Serial.println("Timer is alarm enabled: " + String(timerAlarmEnabled(timer)));
  Serial.println("Timer is started: " + String(timerStarted(timer)));
  delay(250);
}

shows for the first iteration

Timer seconds: 0.00
Timer alarm seconds: 1.00
Timer is alarm enabled: 0
Timer is started: 1

The timer alarm should be enabled since previously timerAlarmWrite() (for divider etc setup) and timerAlarmEnable() was called.

Magically, adding a further

  if(!timerAlarmEnabled(timer)) {
    timerAlarmEnable(timer);
  }

in the loop() function to check on the timer makes it work. It’s like the first call to timerAlarmEnable() in setup() was ignored.

Possibly the compiler has optimized or re-ordered some operations, let me check.

1 Like

Yes, it’s a bug with compiler optimizations.

The timer register definitions

are not marked as volatile and so the compiler does some breaking optimizations. Putting volatile in front of every uint32_t ... member as in

typedef struct {
    union {
        struct {
            volatile uint32_t reserved0:   10;
            volatile uint32_t alarm_en:     1;             /*When set  alarm is enabled*/
            volatile uint32_t level_int_en: 1;             /*When set  level type interrupt will be generated during alarm*/
            volatile uint32_t edge_int_en:  1;             /*When set  edge type interrupt will be generated during alarm*/
            volatile uint32_t divider:     16;             /*Timer clock (T0/1_clk) pre-scale value.*/
            volatile uint32_t autoreload:   1;             /*When set  timer 0/1 auto-reload at alarming is enabled*/
            volatile uint32_t increase:     1;             /*When set  timer 0/1 time-base counter increment. When cleared timer 0 time-base counter decrement.*/
            volatile uint32_t enable:       1;             /*When set  timer 0/1 time-base counter is enabled*/
        };
        volatile uint32_t val;
    } config;
    volatile uint32_t cnt_low;                             /*Register to store timer 0/1 time-base counter current value lower 32 bits.*/
    volatile uint32_t cnt_high;                            /*Register to store timer 0 time-base counter current value higher 32 bits.*/
    volatile uint32_t update;                              /*Write any value will trigger a timer 0 time-base counter value update (timer 0 current value will be stored in registers above)*/
    volatile uint32_t alarm_low;                           /*Timer 0 time-base counter value lower 32 bits that will trigger the alarm*/
    volatile uint32_t alarm_high;                          /*Timer 0 time-base counter value higher 32 bits that will trigger the alarm*/
    volatile uint32_t load_low;                            /*Lower 32 bits of the value that will load into timer 0 time-base counter*/
    volatile uint32_t load_high;                           /*higher 32 bits of the value that will load into timer 0 time-base counter*/
    volatile uint32_t reload;                              /*Write any value will trigger timer 0 time-base counter reload*/
} hw_timer_reg_t;

solved the problem for me, now without any additional call to timerAlarmEnable(), it works.

Let me check if this behavior is still the same with 2.0.0-alpha1 and the new compiler. Sadly the register definitions have stayed the same.

1 Like

The bug is the same in 2.0.0-Alpha1 and the code in master also looks the same. I’ll open an issue about this.

I really don’t know how the Arduino IDE did compile that succesfully…

In the meantime, add

delayMicroseconds(0);

between the timerAlarmWrite() and timerAlarmEnable() calls in setup() to work-around the problem.

EDIT: Above only works for the standard 1.0.6 version, not when using alpha1 or latest master version of the core.

1 Like

Issue is open per Timer functions do not work at all in PlatformIO · Issue #5337 · espressif/arduino-esp32 · GitHub.

1 Like

The issue has been corrected in Fix hardware timers · espressif/arduino-esp32@8f46bad · GitHub.

For the “normal” 1.0.6 version you get with latest platform = espressif32 and not no additional platform_packages, above workaround is valid (use delayMicroseconds()) as well as the real bugfix with volatile.

As for the versions after that, it needs volatile and calling timerAttachInterrupt with the last parameter set to false instead of true in the sketch code for ESP32 chips (edge interrupts do not work properly there – this does not apply to other ESP32-S2, etc. chip types)

So @ifconfig, can you test it first whether the LED blinky works with the simple delayMicroseconds workaround, or even better, the volatile fix?

1 Like

Ah @ifconfig, I forgot that you’re not using the stable 1.0.6 core since you have a ESP32-S2 board. So, also the bit about the timer function not supporting edge = true should be invalid for you.

As for the easieset fix, please replace

[env:ESP32-S2-Mini-1] ; See https://community.platformio.org/t/using-the-platformio-and-the-arduino-framework-with-the-esp32-s2-devkitm-1/21930
platform = espressif32
platform_packages =
	toolchain-xtensa32s2
	framework-arduinoespressif32@https://github.com/espressif/arduino-esp32.git#2.0.0-alpha1
framework = arduino

in your platformio.ini with

[env:ESP32-S2-Mini-1] ; See https://community.platformio.org/t/using-the-platformio-and-the-arduino-framework-with-the-esp32-s2-devkitm-1/21930
platform = espressif32
platform_packages =
	toolchain-xtensa32s2
	framework-arduinoespressif32@https://github.com/espressif/arduino-esp32.git#master
	platformio/tool-esptoolpy @ ~1.30100
framework = arduino

to get to the master branch with the fix (along with a necessary esptoolpy update).

Notice that if you have used some master version before, PlatformIO might not do a git update, so it’s best to remove all folders C:\Users\<user>\.platformio\packages\framework-arduinoespressif32* to get a clean build.

1 Like

WOW! Thanks for all of the detective work here, and for writing it all down so I understand your thought process! Seriously.

I’ll try this when I get home from work tonight. I suppose you’re right that changing to the master branch instead of using the 2.0.0-alpha1 release will incorporate the recent commit you were able to get made to add volatile to the hw_timer_reg_t struct, so I’ll try letting go of the edge trigger flag.

However, I’m a little confused about what the timer interrupt actually triggers on if not on the counter edge… could you explain a little?

Ah! Yes, this works now with timerAttachInterrupt(timer, &onTimer, false /* edge */);, but I don’t quite understand why.

Interestingly, the Serial.println I put in the ISR also works :slight_smile:

Well per the ESP32’s technical reference a timer can be set up to either “generate an edge type interrupt.” or “generate a level type interrupt”. And per chapter 3 of that document and here a CPU core can have 32 interrupt sources where you can stick the timer interrupt into, but each slot is hardcoded for level or edge-type interrupt. But not really sure what it means specifically for the counter / timer after reading that…

Lucky. In any ISR, you aren’t supposed to be able to wait on mutexes, since if they are already taken by someone else, you would have to wait until the user releases it (for a specified ‘block time’ in FreeRTOS), however if the ISR halts to wait then whole CPU is halted (this is different to when only a FreeRTOS thread gets halted).

A Serial.println() goes eventually to the HardwareSerial::write function

where

So I still stand by it that if you write to the UART in the exact moment in which another thread was writing a byte to the UART, the mutex locking will lock up the system. This may be hard to trigger though (but not impossible). And in any case, calling xSemaphoreTake from inside an ISR is a FreeRTOS spec violation.

2 Likes

Thanks, Max! This really helps, I appreciate it a lot.