Using I2C inside a interrupt handler

MyBigJoker
Posts: 3
Joined: Thu Apr 13, 2023 3:26 pm

Using I2C inside a interrupt handler

Postby MyBigJoker » Thu Apr 13, 2023 4:30 pm

Hello forum members,

this is my first post here. I hope I chose the right place to post this.

I am currently working on my masterthesis in Embedded Systems Engineering and came upon a problem I wasn't able to solve by myself.

Board: ESP32 DevKit C V4
IDE: Arduino IDE
OS: Windows
on ESP: FreeRTOS

my Question:
Is there a way to use the I2C-interface inside a interrupt handler, without triggering the interrupt wdt when the slave-device takes too long to answer? Or at least be able to catch such errors without the esp crashing?

Background:
I'm developing a control unit, that consists of two PCBs (One Master and up to 16 Slaves).
Those PCBs comunicate via CANBUS.
The Master gets sensor values from all the slaves and regulates 8 seperate outputs accordingly.
As I didn't have enough pins free to control the outputs directly I chose to use a additional DAC (DAC5578SPW) which is controlled via I2C from my ESP32. This was an oversight because I did not think thoroughly enough about all the desired output signals. The hardware layout can not be changed in this iteration, as its already ordered and will arrive soon.

I need the capability to create a PWM Signal on at least 2 outputs. But as all the outputs are controlled via the I2C I cannot use the integrated PWM peripheral(bound to GPIO pins) and have to create the PWM somehow in software. I tried one approach using a task to create it. But here my dutycycle resolution is bound to my tick rate in FreeRTOS(I read that its not wise to reduce the tick rate below 1ms). If I want to have a resolution of 1% my PWM-frequency cannot be higher than:
100% * 1ms = 100ms --> 1/0.1 = 10Hz
This is way too far away from my goal, which is around 1kHz.

My Problem:
So i tried to use the hardware timers to create the PWM. This works greate until I want to change the output states using the I2C interface. As I don't have the hardware yet, the DAC does not exist of right now. So the wire.begin waits so long for a response that the interrupt wdt gets triggered. But even if the DAC exists I think I should not accept the possibility of any delay in communication to crash my ESP entirely. I tried to change the Interrupt wdt time by changing CONFIG_ESP_INT_WDT_TIMEOUT_MS without any effect on the outcome(the wdt gets triggered almost instantly {propably 300ms default} even if I change the timeout to 10000ms). Even if I try to disable the wdt (I know, not advised.) with CONFIG_ESP_INT_WDT nothing changes. Does someone know why nothing is changing? Something to do with the Arduino IDE?

The interrupt handler:

Code: Select all

//ISR handler of ESP-Timer for channel 0
void IRAM_ATTR channel0_timer_callback(void* args)
{
  BaseType_t _woken_higher_prio_task = pdFALSE;
  uint64_t _temp_interval = 0;
  //Cast args to info-struct and safe it in internal struct
  _struct_timer_info *info = (_struct_timer_info*) args;
  //Enter cricital Section (disable othe rinterrupts)
  // timer_spinlock_take(info->group);      //Deprecated!

  //Get interrupt status
  uint32_t _intr_status = TIMERG0.int_st_timers.val;
  //Trigger a counter value update to read out correct value
  TIMERG0.hw_timer[info->timer].update = 1;
  //Get counter value <-- not needed righ now
  // uint64_t _counter_value = ((uint64_t) TIMERG0.hw_timer[info->timer].cnt_high) << 32 | TIMERG0.hw_timer[info->timer].cnt_low;
 
  //Get semaphore for pwm-array
  xSemaphoreTakeFromISR(_sema_output, &_woken_higher_prio_task);
  //Check whether output is PWM
  if(_output[0].state == PWM)
  {
    //Check the last state, skip when dc is 100%
    if(_output[0].last_state_high && _output[0].dutycycle < 255)
    {
      //Switch output
      mySerial0.print("pulling output low: ");
      update_DAC_Channel(0, 0x00, true);
      mySerial0.println(esp_timer_get_time());
      //Update flag for status
      _output[0].last_state_high = false;
      //Calculate new interval (must be in 0.1us --> *10)
      _temp_interval = (_output[0].t_period - _output[0].dc_time) * 10;
      //Set new interval/alarm value
      timer_group_set_alarm_value_in_isr(info->group, info->timer, _temp_interval);
    }
    else if (_output[0].dutycycle > 0)
    {
      //Switch output
      mySerial0.print("pulling output high: ");
      update_DAC_Channel(0, (uint8_t) _output[0].output_value, true);
      mySerial0.println(esp_timer_get_time());
      //Update flag for status
      _output[0].last_state_high = true;
      //Calculate new interval (must be in 0.1us --> *10)
      _temp_interval = (_output[0].dc_time) * 10;
      //Set new interval/alarm value
      timer_group_set_alarm_value_in_isr(info->group, info->timer, _temp_interval);
    }
  }
  //Channel has constant output
  else if (_output[0].state == CONSTANT)
  {
    // update_DAC_Channel(0, (uint8_t) _output[0].output_value, true);
    timer_pause(info->group, info->timer);
    mySerial0.print("Channel0 is constant\n");
  }
  //Channel is off
  else if (_output[0].state == OFF)
  {
    // update_DAC_Channel(0, 0x00, true);
    timer_pause(info->group, info->timer);
    mySerial0.print("Channel0 is off\n");
  }
  xSemaphoreGiveFromISR(_sema_output, &_woken_higher_prio_task);
  //Reset interrupt flag
  if(_intr_status & BIT(info->timer))
  {
    TIMERG0.int_clr_timers.t0 = 1;
    //Setting new alarm value (only when autoload is disabled)
    // timer_counter_value += (uint64_t) (TIMER_INTERVAL0_SEC * TIMER_SCALE);
    // TIMERG0.hw_timer[timer_idx].alarm_high = (uint32_t) (timer_counter_value >> 32);
    // TIMERG0.hw_timer[timer_idx].alarm_low = (uint32_t) timer_counter_value;
  }
  else
  {
    //Interrupt not triggered because of value reached...
  }
  //Reenable alarm
  TIMERG0.hw_timer[info->timer].config.alarm_en = TIMER_ALARM_EN;
  // timer_spinlock_give(info->group);    //Deprecated!
}
The function that changes the outputs via I2C:

Code: Select all

//Sends register update to DAC on specific channel, direct update of register optional.
//Value 0...255 (8bit), channels 0...7.
uint8_t update_DAC_Channel(uint8_t _channel, uint8_t _value, bool _update)
{
  Wire.beginTransmission(DAC_ADR);
  //Send command and channel for changing Channel output
  //Update register directly
  if (_update)
  {Wire.write(_DAC_writeupdate_mask | _channel);}
  //Don't update register directly
  else
  {Wire.write(_DAC_write_mask | _channel);}
  //Send value for channel register
  Wire.write(_value);
  //Return errorcode
  return Wire.endTransmission();
}
The error code:

Code: Select all

pulling output high: Guru Meditation Error: Core  1 panic'ed (Interrupt wdt timeout on CPU1). 

Core  1 register dump:
PC      : 0x4008cca6  PS      : 0x00060235  A0      : 0x8008b5de  A1      : 0x3ffbf13c  
A2      : 0x3ffb29b8  A3      : 0x3ffb81a4  A4      : 0x00000004  A5      : 0x00060223  
A6      : 0x00060223  A7      : 0x00000001  A8      : 0x3ffb81a4  A9      : 0x00000018  
A10     : 0x3ffb81a4  A11     : 0x00000018  A12     : 0x3ffc2f4c  A13     : 0x00060223  
A14     : 0x007bf3a8  A15     : 0x003fffff  SAR     : 0x0000000c  EXCCAUSE: 0x00000006  
EXCVADDR: 0x00000000  LBEG    : 0x40086a78  LEND    : 0x40086a8e  LCOUNT  : 0x00000000  
Core  1 was running in ISR context:
EPC1    : 0x400e04fb  EPC2    : 0x00000000  EPC3    : 0x00000000  EPC4    : 0x00000000

Backtrace: 0x4008cca3:0x3ffbf13c |<-CORRUPTED


Core  0 register dump:
PC      : 0x4008ce3b  PS      : 0x00060035  A0      : 0x8008b207  A1      : 0x3ffbeacc  
A2      : 0x3ffbf3a8  A3      : 0xb33fffff  A4      : 0x0000abab  A5      : 0x00060023  
A6      : 0x00060021  A7      : 0x0000cdcd  A8      : 0x0000abab  A9      : 0xffffffff  
A10     : 0x3ffc2d68  A11     : 0x00000000  A12     : 0x3ffc2d64  A13     : 0x00000007  
A14     : 0x007bf3a8  A15     : 0x003fffff  SAR     : 0x0000001d  EXCCAUSE: 0x00000006  
EXCVADDR: 0x00000000  LBEG    : 0x00000000  LEND    : 0x00000000  LCOUNT  : 0x00000000  


Backtrace: 0x4008ce38:0x3ffbeacc |<-CORRUPTED

Is there any way to communicate via I2C inside a interrupt handler?
Or is there another more elegant way to produce the desired PWM signal on my DAC?
How would you advice me to solve this problem?
I'm pretty lost right now(even google can't help me right now) and would appreciate any advice from more experienced engineeres...

Thanks for your time in advance!

PS: If I accidently violated some etiquette in creating forum posts please let me know! I'm a newbie...

ESP_Sprite
Posts: 9724
Joined: Thu Nov 26, 2015 4:08 am

Re: Using I2C inside a interrupt handler

Postby ESP_Sprite » Fri Apr 14, 2023 2:01 am

You can do very little in an interrupt handler, and specifically doing anything that block the calling task will crash the ESP32 (as an interrupt handler is not a task). The proper way would be to use FreeRTOS tasks and semaphores (or other inter-task things that FreeRTOS provides): the task blocks on a semaphore, the interrupt handler raises ('gives') the semaphore causing the task to un-block, the task then does the I2C thing.

Note that as FreeRTOS is pre-emptive, the task blocking/unblocking is not dependent on the FreeRTOS tick rate here.

MyBigJoker
Posts: 3
Joined: Thu Apr 13, 2023 3:26 pm

Re: Using I2C inside a interrupt handler

Postby MyBigJoker » Tue Apr 25, 2023 12:55 pm

Thank you ESP_Sprite for the quick response.
It took me a while to work it out(other stuff to do as well), but with the use of tasknotifications and a lot of semaphores I made it work.
Thanks for the encouragement!
It works great up to around 5kHz. Although I still have to test it with the I²C interface. That probably will reduce the frequency I can operate at.

One last question that I couldn't figure out:
Is there an Interrupt for every timer or one per group?
Although I can add one for every timer, I think they get triggered by one main interrupt per group. Is that correct?
Curious enough the program only worked when I used the timer_isr_register function instead of the timer_isr_add. So if I understand correctly I bypassed the "normal" main interrupt.

For anyone that might find it useful here are some code snippets.
I will only include the essential parts as my program got pretty big by now.

Here the timer setup:

Code: Select all

void init_hardware_timer(timer_group_t _group, timer_idx_t _timer, double _frequency)
{
  uint8_t _temp_channel = (uint8_t)_group << 1 | (uint8_t)_timer;
  uint64_t _temp_alarm_val = TIMER_SCALER_US * 1000000/_frequency;

  mySerial0.print("Hardware Timer ");mySerial0.print(_temp_channel);mySerial0.print(":\n");

  //Create config-struct for initializing timer
  timer_config_t config;

  config.alarm_en     = TIMER_ALARM_EN;          //Alarm for timer is enabled <-- always enabled
  config.counter_en   = TIMER_PAUSE;             //Timer won't start directly after initialization
  config.intr_type    = TIMER_INTR_LEVEL;        //Set interrupt mode
  config.counter_dir  = TIMER_COUNT_UP;          //Direction for timer to count to
  config.auto_reload  = TIMER_AUTORELOAD_EN;     //After Alarm timer value is reloaded <--always enabled
  config.divider      = TIMER_DEFAULT_DIVIDER;   //Sets the divider for the timer, default is 8 --> timer counts with 10MHz -->0.1us period
                          
  //Initialize timer with config
  if(timer_init(_group, _timer, &config) == ESP_OK)
  {mySerial0.print("\tInit\t\t\t-->done\n");}
  else
  {mySerial0.print("\tInit\t\t\t-->FAILED\n");}

  //Set reload-value to zero
  if(timer_set_counter_value(_group, _timer, (uint64_t) 0) == ESP_OK)
  {mySerial0.print("\tSet Reload Val\t\t-->done\n");}
  else
  {mySerial0.print("\tSet Reload Val\t\t-->FAILED\n");}

  //Set the alarm value
  if(timer_set_alarm_value(_group, _timer, _temp_alarm_val) == ESP_OK)
  {mySerial0.print("\tSet Alarm\t\t-->done\n");}
  else
  {mySerial0.print("\tSet Alarm\t\t-->FAILED\n");}

  //Enable interrupts for this timer
  if(timer_enable_intr(_group, _timer) == ESP_OK)
  {mySerial0.print("\tIntr_enable\t\t-->done\n");}
  else
  {mySerial0.print("\tIntr_enable\t\t-->FAILED\n");}
  //Cast timer number to int
  int timer_info = (int) _timer;
  //Register interrupts according to channel number. Group0: channel0 & channel1, Gorup1: channel2 & channel3
  //There is one ISR handler per group
  if(_temp_channel == 0 | _temp_channel == 1)
  {
    if(timer_isr_register(TIMER_GROUP_0, _timer, timer_group0_callback, (void*) timer_info, ESP_INTR_FLAG_IRAM, NULL) == ESP_OK)
    {mySerial0.print("\tIntr_register\t\t-->done\n");}
    else
    {mySerial0.print("\tIntr_register\t\t-->FAILED\n");}
  }
  else if(_temp_channel == 2 | _temp_channel == 3)
  {
    if(timer_isr_register(TIMER_GROUP_1, _timer, timer_group1_callback, (void*) timer_info, ESP_INTR_FLAG_IRAM, NULL) == ESP_OK)
    {mySerial0.print("\tIntr_register\t\t-->done\n");}
    else
    {mySerial0.print("\tIntr_register\t\t-->FAILED\n");}
  }
}
Here the ISR handler. I have one for each timer group, but essentially they work the same:

Code: Select all

void IRAM_ATTR timer_group0_callback(void *args)
{
  BaseType_t _woken_higher_prio_task = pdFALSE;
  _struct_ctrl _temp_output;
  
  //Cast args to info-struct and save it in internal struct
  int _timer = (int) args;

  //Take semaphore for pwm-array
  if(xSemaphoreTakeFromISR(_sema_output[_timer], &_woken_higher_prio_task) == pdTRUE)
  {
    _temp_output.state            = _output[_timer].state;
    _temp_output.first_period     = _output[_timer].first_period;
    _temp_output.second_period    = _output[_timer].second_period;
    _temp_output.last_state_high  = _output[_timer].last_state_high;
    _temp_output.dutycycle        = _output[_timer].dutycycle;
    xSemaphoreGiveFromISR(_sema_output[_timer], &_woken_higher_prio_task);
    //Check whether output is PWM
    if(_temp_output.state == PWM)
    {
      //Check whether interrupt was triggered because of value reached
      if(TIMERG0.int_st_timers.val & BIT0 && _timer == 0)
      {
        //Check last state, skip when dc is 100%
        if(_temp_output.last_state_high && _temp_output.dutycycle < 255)
        {
          //Notify PWM-task for changing outputs
          xTaskNotifyFromISR(control_pwm_handle, 0b00000000, eSetValueWithoutOverwrite, &_woken_higher_prio_task);
          //Set new interval/alarm value
          timer_group_set_alarm_value_in_isr(TIMER_GROUP_0, (timer_idx_t)_timer, _temp_output.second_period * TIMER_SCALER_US);   //Calculate new interval (must be in 0.1us --> *10)
        }
        //Check last state, skip when dc is 0%
        else if (!_temp_output.last_state_high && _temp_output.dutycycle > 0)
        {
          xTaskNotifyFromISR(control_pwm_handle, 0b00000100, eSetValueWithoutOverwrite, &_woken_higher_prio_task);
          timer_group_set_alarm_value_in_isr(TIMER_GROUP_0, (timer_idx_t)_timer, _temp_output.first_period * TIMER_SCALER_US);//Calculate new interval (must be in 0.1us --> *10)
        }
        //Reset interrupt flag
        TIMERG0.int_clr_timers.t0 = 1;
      }
      else if (TIMERG0.int_st_timers.val & BIT1 && _timer == 1) 
      {
        if(_temp_output.last_state_high && _temp_output.dutycycle < 255)
        {
          xTaskNotifyFromISR(control_pwm_handle, 0b00000001, eSetValueWithoutOverwrite, &_woken_higher_prio_task);
          timer_group_set_alarm_value_in_isr(TIMER_GROUP_0, (timer_idx_t)_timer, _temp_output.second_period * TIMER_SCALER_US);   //Calculate new interval (must be in 0.1us --> *10)
        }
        else if (!_temp_output.last_state_high && _temp_output.dutycycle > 0)
        {
          xTaskNotifyFromISR(control_pwm_handle, 0b00000101, eSetValueWithoutOverwrite, &_woken_higher_prio_task);
          timer_group_set_alarm_value_in_isr(TIMER_GROUP_0, (timer_idx_t)_timer, _temp_output.first_period * TIMER_SCALER_US);//Calculate new interval (must be in 0.1us --> *10)
        }
        TIMERG0.int_clr_timers.t1 = 1;
      }
      else
      {ESP_LOGE(TAG, "ERROR: HW-Timer triggered but value not reached...");}
    }
    //Channel is not in PWM-mode --> pause timer
    else
    {timer_pause(TIMER_GROUP_0, (timer_idx_t)_timer);}
  }
  else
  {ESP_LOGE(TAG, "ERROR: Semaphore blocked!");}

  //Reenable alarm
  TIMERG0.hw_timer[(timer_idx_t)_timer].config.alarm_en = TIMER_ALARM_EN;
  portYIELD_FROM_ISR(_woken_higher_prio_task);
}

Who is online

Users browsing this forum: No registered users and 99 guests