Using Multiple interrupts in esp32

sajeel
Posts: 1
Joined: Thu Jul 29, 2021 1:11 pm

Using Multiple interrupts in esp32

Postby sajeel » Thu Jul 29, 2021 1:30 pm

Hi,

Good Afternoon!

We are using two external interrupts on the esp32, one interrupt is attached to core 1 (this is a high level interrupt on GPIO_NUM_35) and the other one is a low level interrupt which is tied to core 0 on GPIO_NUM_27.

On high level interrupt (GPIO_NUM_35) we are having an ADC(Analog to digital converter) which gives us an interrupt whenever the data is ready to take out. Since, we are required high level interrupt here because we need to service the ADC more often without any delays.

On low-level interrupt (GPIO_NUM_27) we are using an ethernet module ENC28J60 in order to transfer data. Ethernet module gives us an interrupt every time we receive a package or so on, and this also needs to be serviced.

The problem is: ESP32 is unable to service both interrupts at a time (simultaneously) because if we configure the ethernet then interrupt from ADC is not serviced. There might be a conflict in the interrupt service routines. We searched alot of this in forums and documentations but we did not have success.

We appreciate every little effort you guys can put on this. Thank you for your time. With best regards

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

Re: Using Multiple interrupts in esp32

Postby ESP_Sprite » Fri Jul 30, 2021 12:46 am

That sounds odd... do you have a minimal example that shows this, or at least more information (how long is the ADC code delayed, for instance)?

opcode_x64
Posts: 47
Joined: Sun Jan 13, 2019 5:39 pm

Re: Using Multiple interrupts in esp32

Postby opcode_x64 » Fri Jul 30, 2021 7:37 am

Hello ESP_Sprite,

Sajeel and me working together on that issue. Please don’t wonder why I am posting here the minimal code.
Before starting, I want to make clear, that we use an Level-5 interrupt to “catch” the “DATAREADY” interrupt of an external ADC as fast as possible. The Level-5 interrupt handler then just fires up a pre-configurated SPI transaction running on HSPI to read out the ADC data. This approach was introduced by the user “bienvenue” in this post: https://www.esp32.com/viewtopic.php?f=13&t=16093#p62375. For “better performance” we pinned the Level-5 interrupt to Core 1 by executing the interrupt allocation codes within a task which is pinned to Core 1. When the HSPI transaction finishes, thus all ADC data is read out, then the SPI (HSPI) handler is raised where we read out the SPI RX buffer to put the data into a buffer array. When the buffer array reaches the buffer limit, e.g., 256 packets, then the buffer array is send to an “RingBuffer”, so that Core0 can grab that data from the Ringbuffer. Also, the SPI (HSPI) handler is running on Core 1 since the codes for interrupt allocation are also executed within the same task pinned to Core 1 where the Level-5 interrupt is allocated. Now, our aim is to run the ENC28J60 codes (tasks, interrupt handler…) on Core0 to send the data received from the Ringbuffer via ethernet.
However, in the following you will find code fragments which, hopefully, makes the problem clearer. For the sake of overview, I will introduce every code fragment separately.

Level-5 interrupt handler (xt_hint5):

Code: Select all

#include …….

.data
_l5_intr_stack:
 .space      12     //L5_INTR_STACK_SIZE

.section .iram1,"ax"
 .global     xt_highint5
 .type       xt_highint5,@function
 .align      4
 .literal .GPIO_STATUS1_W1TC_REG,   0x3FF44058
 .literal .ADC_DRDY_PIN,        (1<<3)
 .literal .SPI_CMD_REG,             0x3FF64000
 .literal .SPI_USR,                 (1<<18)

xt_highint5:
    /* save contents of registers A2-A4 */
    movi    a0, _l5_intr_stack
    s32i    a2, a0, 0
    s32i    a3, a0, 4
    s32i    a4, a0, 8
    /* clearing the interrupt status of GPIO_NUM_35  */
    l32r a2, .GPIO_STATUS1_W1TC_REG
    l32r a3, .ADC_DRDY_PIN
    s32i a3, a2, 0

    /* SPI2.cmd.usr = 1; */
    /* The SPI peripheral takes a few hundred nanoseconds 
       to start, no need for extra delay */
    l32r a2, .SPI_CMD_REG
    l32r a3, .SPI_USR
    s32i a3, a2, 0
 
    /* restore contents of registers A2-A4 */
    movi    a0, _l5_intr_stack
    l32i    a2, a0, 0
    l32i    a3, a0, 4
    l32i    a4, a0, 8
    rsync                                   /* ensure register restored */
    /* hand back from interrupt */
    //rsr     a0,  EXCSAVE_5
    rsr.excsave5 a0
    rfi     5
    .global ld_include_highint_hdl
ld_include_highint_hdl:
adc_init:
This function is executed by the task “vTask_adc_init” pinned on Core 1 to make sure that all ADC related “stuff” is running on Core 1.

Code: Select all

Void adc_init(){

    // ….. SPI settngs here ……
    ESP_ERROR_CHECK(spi_bus_initialize(HSPI_HOST, &adc_spi_buscfg, 1));
    ets_delay_us(10);
    ESP_ERROR_CHECK(spi_bus_add_device(HSPI_HOST, &adc_spi_devcfg, &adc_spi_dev_handle));
    ets_delay_us(10);
    ESP_ERROR_CHECK(spi_device_acquire_bus(adc_spi_dev_handle, portMAX_DELAY));
    ets_delay_us(10);
    ESP_ERROR_CHECK(spi_device_queue_trans(adc_spi_dev_handle, &adc_spi_transaction, portMAX_DELAY));
    ESP_ERROR_CHECK(spi_device_get_trans_result(adc_spi_dev_handle, &adc_spi_transaction, portMAX_DELAY));
    ………..
    
    //…. SPI (HSPI) handler configuration here……
    intr_handle_t spi_int = spi_bus_get_intr(HSPI_HOST);
    intr_handle_t adc_spi_intr_handle;

    esp_intr_disable(spi_int);
    esp_intr_free(spi_int);
    esp_intr_alloc(ETS_SPI2_INTR_SOURCE, ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_IRAM , adc_spi_isr, NULL,   
     &adc_spi_intr_handle);
    esp_intr_enable(adc_spi_intr_handle)
     ………..

    // Configuring the “DATAREADY” interrupt pin for Level-5
    gpio_pad_select_gpio(adc_DRDY_INT_PIN);     
    gpio_config_t CONFIG_adc_DRDY_INT_PIN={
        .intr_type=GPIO_INTR_NEGEDGE,
        .pin_bit_mask=(1ULL<<adc_DRDY_INT_PIN),
        .mode=GPIO_MODE_INPUT,
        .pull_up_en=GPIO_PULLUP_DISABLE,
        .pull_down_en=GPIO_PULLDOWN_DISABLE,
    };
    gpio_config(&CONFIG_adc_DRDY_INT_PIN);  

    // Here the interrupt number 31 (Priority 5 – Level Triggered) is set to Core 1 with source “GPIO Interrupt”
    ESP_INTR_DISABLE(31);
    intr_matrix_set(1, ETS_GPIO_INTR_SOURCE, 31);   
    ESP_INTR_ENABLE(31);
    ………..
}
adc_spi_isr(…):
This is the SPI (HSPI) handler which raises when transaction is finished...

Code: Select all

static void IRAM_ATTR adc_spi_isr(void *arg){
    SPI2.slave.trans_done = 0; // reset the register
    //ets_printf("IRAM_ATTR adc_spi_isr @ CoreID: %d\n",xPortGetCoreID());
    if(samples_counter<SAMPLES_PER_CHANNEL){
        samples_package[samples_counter*4+0] = (SPI_SWAP_DATA_RX(SPI2.data_buf[0],32) >> 8);                                                                 //(byte1 >> 8)
        samples_package[samples_counter*4+1] = ((SPI_SWAP_DATA_RX(SPI2.data_buf[0],32)&0x000000FF)<<16) | (SPI_SWAP_DATA_RX(SPI2.data_buf[1],32) >> 16);     //  ((byte1&0x000000FF)<<16) | (byte2 >> 16)
        samples_package[samples_counter*4+2] = (((SPI_SWAP_DATA_RX(SPI2.data_buf[1],32) & 0X0000FFFF) << 8) | SPI_SWAP_DATA_RX(SPI2.data_buf[2],32) >> 24); // (((byte2 & 0X0000FFFF) << 8) | byte3 >> 24)
        samples_package[samples_counter*4+3] = (SPI_SWAP_DATA_RX(SPI2.data_buf[2],32) & 0x00FFFFFF); // byte3 & 0x00FFFFFF)
        samples_counter++;
    }
    else{
        return_ringbuf_send =  xRingbufferSendFromISR(ringbuf_samples_handle, samples_package, sizeof(samples_package), 0);
        if (return_ringbuf_send != pdTRUE) {
            ets_printf("Failed to send samples package to samples ringbuffer\n");
        }
        samples_counter=0;
    }
}
emac_enc28j60_init(…)

As Sajeel mentioned in the initial post, we are using an external ENC28J60 for ethernet connection. We are using the example code provided here: https://github.com/espressif/esp-idf/bl ... /README.md
After “analyzing” the code, one can find that the interrupt pin needed for the enc28j60 is configurated in the function “emac_enc28j60_init(…)”.
To ensure that all ethernet related code is running on core 0, we pinned the “ethernet main init” functions to the task “vTask_ethernet_init(..)” pinned to core 0. The “ethernet main init” will be introduced below.

Code: Select all

static esp_err_t emac_enc28j60_init(esp_eth_mac_t *mac)
{
    printf("emac_enc28j60_init @ CoreID: %d\n",xPortGetCoreID())
    
    //registering here the GPIO ISR
    ESP_ERROR_CHECK(gpio_install_isr_service(0));
    esp_err_t ret = ESP_OK;
    emac_enc28j60_t *emac = __containerof(mac, emac_enc28j60_t, parent);
    esp_eth_mediator_t *eth = emac->eth;
    /* init gpio used for reporting enc28j60 interrupt */
    gpio_reset_pin(emac->int_gpio_num);
    gpio_pad_select_gpio(emac->int_gpio_num);
    gpio_set_direction(emac->int_gpio_num, GPIO_MODE_INPUT);
    gpio_set_pull_mode(emac->int_gpio_num, GPIO_PULLUP_ONLY);
    gpio_set_intr_type(emac->int_gpio_num, GPIO_INTR_NEGEDGE);
    
    //enabling the GPIO interrupt here and assigning the handler...
    gpio_intr_enable(emac->int_gpio_num);
    gpio_isr_handler_add(emac->int_gpio_num, enc28j60_isr_handler, emac);
    ......
}
ethernet_init(…):
(same code as in the ENC28j60 example’s app_main)
The ethernet_init(...) function is execuded by the task "vTask_ethernet_init(...)" pinned to Core 0.

Code: Select all

void ethernet_init(){
  printf("ethernet_init @ CoreID: %d\n",xPortGetCoreID());    
    // Initialize TCP/IP network interface (should be called only once in application)
    ESP_ERROR_CHECK(esp_netif_init());
    // Create default event loop that running in background
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH();
    esp_netif_t *eth_netif = esp_netif_new(&netif_cfg);
    // Set default handlers to process TCP/IP stuffs
    ESP_ERROR_CHECK(esp_eth_set_default_handlers(eth_netif));
    // Register user defined event handers
    ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &eth_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL));
    
    spi_bus_config_t buscfg = {
        .miso_io_num = GPIO_NUM_19,
        .mosi_io_num = GPIO_NUM_23,
        .sclk_io_num = GPIO_NUM_18,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
    };
    ESP_ERROR_CHECK(spi_bus_initialize(VSPI_HOST, &buscfg, 2));
    // ENC28J60 ethernet driver is based on spi driver /
    spi_device_interface_config_t devcfg = {
        .command_bits = 3,
        .address_bits = 5,
        .mode = 0,
        .clock_speed_hz = 6 * 1000 * 1000,
        .spics_io_num = GPIO_NUM_5,
        .queue_size = 20,
        .post_cb = NULL,
        .pre_cb = NULL
    };
    spi_device_handle_t spi_handle = NULL;
    ESP_ERROR_CHECK(spi_bus_add_device(VSPI_HOST, &devcfg, &spi_handle));
    eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(spi_handle);
    enc28j60_config.int_gpio_num = GPIO_NUM_27;
    eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
    mac_config.smi_mdc_gpio_num = -1;  // ENC28J60 doesn't have SMI interface
    mac_config.smi_mdio_gpio_num = -1;
    esp_eth_mac_t *mac = esp_eth_mac_new_enc28j60(&enc28j60_config, &mac_config);
    eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
    phy_config.autonego_timeout_ms = 0; // ENC28J60 doesn't support auto-negotiation
    phy_config.reset_gpio_num = -1; // ENC28J60 doesn't have a pin to reset internal PHY
    esp_eth_phy_t *phy = esp_eth_phy_new_enc28j60(&phy_config);
    esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy);
    esp_eth_handle_t eth_handle = NULL;
    ESP_ERROR_CHECK(esp_eth_driver_install(&eth_config, &eth_handle));  //after this call, the ADC Interrupts are not working anymore
    // ENC28J60 doesn't burn any factory MAC address, we need to set it manually.
    //   02:00:00 is a Locally Administered OUI range so should not be used except when testing on a LAN under your control.
    mac->set_addr(mac, (uint8_t[]) {
        0x02, 0x00, 0x00, 0x12, 0x34, 0x56
    });
    // attach Ethernet driver to TCP/IP stack 
    ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle)));
    // start Ethernet driver state machine
    ESP_ERROR_CHECK(esp_eth_start(eth_handle));

    vTaskDelay(5000/portTICK_RATE_MS)
  }
Now the code fragments of app_main with two tasks pinned to Core 0 and Core 1:

Code: Select all

void vTask_ethernet_init(void * pvParameters){
  ethernet_init();
    while(1){
        vTaskDelay(1);
    }
}
void vTask_adc_spi_init(void * pvParameters){
  printf("vTask_adc_spi_init @ CoreID:%d\n",xPortGetCoreID());
  adc_init();
  while(1){
        vTaskDelay(1);
    }
}

app_main(..){
……
   xTaskCreatePinnedToCore(vTask_ethernet_init,"",4096,NULL,1,NULL,0);
    vTaskDelay(2500/portTICK_RATE_MS);
    xTaskCreatePinnedToCore(vTask_adc_spi_init,"",DAQ_TASK_STACKSIZE,NULL,1,NULL,1);
     while(1){
vTaskDelay(1);
     }
…
}

Thank you very much for your effort and time !

Best regards,
opcode_x64

bienvenu
Posts: 15
Joined: Fri Nov 27, 2015 11:06 am

Re: Using Multiple interrupts in esp32

Postby bienvenu » Fri Jul 30, 2021 9:14 am

FWIW, I was using a normal RMII PHY (LAN7820A) and didn't hit these interrupt issues (that I know of?).

How/why did you guys select the ENC28J60?

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

Re: Using Multiple interrupts in esp32

Postby ESP_Sprite » Fri Jul 30, 2021 9:18 am

Hm, I'm not seeing anything obviously wrong with that code... Can you tell me what problems specifically you see, that is, how do you notice specifically that the ADC interrupt isn't serviced (and how did you conclude it's the ADC interrupt and not something else in the chain that malfunctions) and how long does the malfunction persist?

opcode_x64
Posts: 47
Joined: Sun Jan 13, 2019 5:39 pm

Re: Using Multiple interrupts in esp32

Postby opcode_x64 » Fri Jul 30, 2021 9:32 am

Hello all,

@bienvenu:
Hello ! Thank for your answer. I thought you just left this community... I pm'ed you several months ago, but you never returned ;)
Can you tell me please what module you exactly used as RMII PHY (LAN7820A) ?? Maybe provide a link ?

@ESP_Sprite:
To test if the ADC interrupts are raising, I am using ets_printf() in the adc_spi_isr handler. If I deactivate "ethernet_init()" the console is flooded by the print outs (so the ADC interrupts are catched) and if I activate "ethernet_init()" there are no any print outs...

Just as information: When I just commenting out the GPIO interrupt register codes lines in "emac_enj28j60_init(...)" code, then also the ADC interrupts are working. For somehow, after the GPIO interrupt register "kills" the Level-5 interrupt.

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

Re: Using Multiple interrupts in esp32

Postby ESP_Sprite » Fri Jul 30, 2021 10:56 am

Aaaaah, okay, it disables it entirely; the wording in the original post ('ESP32 is unable to service both interrupts at a time (simultaneously)') made me think there was something wrong at runtime only when both interrupts were triggered.

In that case, I think the issue is that gpio_install_isr_service(0); is your problem - from the docs: 'This function is incompatible with gpio_isr_register() - if that function is used, a single global ISR is registered for all GPIO interrupts. If this function is used, the ISR service provides a global GPIO ISR and individual pin handlers are registered via the gpio_isr_handler_add() function.' - that likely also goes if you use lower-level functions like intr_matrix_set to register a GPIO interrupt.

There actually is a provision in hardware to have a separate interrupt source and pins for core 0 and core 1, but I'm not sure how much of that is implemented in the driver - as a simple test, you may try to see what happens if you initialize your ADC GPIO interrupt thing after initializing Ethernet, it may just be enough to make it work.

opcode_x64
Posts: 47
Joined: Sun Jan 13, 2019 5:39 pm

Re: Using Multiple interrupts in esp32

Postby opcode_x64 » Fri Jul 30, 2021 11:17 am

Hello ESP_Sprite,

sorry for the unclear wording in the initial post ;)

Unfortunately, executing ADC interrupt configuration after Ethernet Initialization did not help. Further, the Core IDs got interchanged, which means that ethernet ISR was running on Core 1 instead of Core 0. I think one calls this "Core Migration" as written in the ESP32 docs. Just by the way: executing ADC interrupt config after Ethernet Init means to run the ethernet init task first and then ADC init task?

According to gpio_install_isr_service(0):

Your answer is not so clear to me. How should I now allocate the GPIO interrupts ? Should I also allocate the GPIO interrupt for the ethernet with "low-level" functions as ADC interrupt (intr_matrix_set...) ? As I know, I cannot use the "high-level" codes to allocate the Level-5 interrupt...

Thank you very much!

Best regards,
opcode_x64

opcode_x64
Posts: 47
Joined: Sun Jan 13, 2019 5:39 pm

Re: Using Multiple interrupts in esp32

Postby opcode_x64 » Fri Jul 30, 2021 8:27 pm

Hello ESP_Sprite,

I have just changed the GPIO interrupt configuration in

Code: Select all

    
gpio_reset_pin(emac->int_gpio_num);
gpio_pad_select_gpio(emac->int_gpio_num);
gpio_set_direction(emac->int_gpio_num, GPIO_MODE_INPUT);
gpio_set_pull_mode(emac->int_gpio_num, GPIO_PULLUP_ONLY);
gpio_set_intr_type(emac->int_gpio_num, GPIO_INTR_NEGEDGE);
    
gpio_isr_register(enc28j60_isr_handler, emac ,ESP_INTR_FLAG_EDGE, NULL);
but it is still not working. This time both interrupts (ADC and ETH) are not raising....

I am getting frustated, awwww ....

opcode_x64

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

Re: Using Multiple interrupts in esp32

Postby ESP_Sprite » Sat Jul 31, 2021 1:57 am

It's complicated. Let me try to explain. You already seem to have done at least something right for this solution (run both interrupts on different cores) so I'm not exactly sure how much you know about this already.

So the thing is that according to the GPIO driver, there is only one interrupt you can trigger for all GPIOs, and then you can enable one or more specific GPIOs to trigger that interrupt. The interrupts are not per GPIO; you can't connect interrupt A to GPIO 1 and interrupt B to GPIO 2. The GPIO driver gracefully offers you to work around this by calling gpio_install_isr_service(). When you do that, it takes that single interrupt and installs its own interrupt handler in it (note: replacing anything that was there, hence your ADC interrupt getting kicked out.) As that GPIO driver interrupt handler is written in C, there's no way to use a high-level interrupt with it. End of story.

However.... the GPIO driver plasters over some details in the actual implementation, and that may save you here. For whatever reason (foresight of our digital team?) there actually are *two* interrupt sources associated with the GPIO subsystem. Both have the same interrupt source number, but one is routed to CPU0, the other is routed to CPU1. You can set a bit for each GPIO to select which interrupt it triggers.

As far as I can read, the GPIO driver uses the core for the interrupt gpio_intr_enable or gpio_isr_register is called on. If that indeed happens on core 0 as you say, it should not touch the core 1 interrupt you had installed earlier. However, this is a somewhat untested situation and it may accidentally have reset something it doesn't have, hence me telling you to see if flipping the two initializations around works.

Given that it does not... hmm, not entirely sure what's going on...

Who is online

Users browsing this forum: No registered users and 79 guests