Page 1 of 2

High level interrupt (5) and NMI issues

Posted: Mon Aug 19, 2024 1:42 am
by OSCPUDEV
[SOLVED] PROBLEM #1 - interrupt firing twice

Hardware configuration: ESP32-S2 and ESP32-S3 Dev Modules - (both do the same thing)

I created a very basic high level interrupt test for a GPIO input, with the required assembly code (.S) handler. This works fine, but there is an issue that was also reported by another person in this thread: viewtopic.php?f=13&t=8826&start=10#p37384

The issue is that the interrupt occurs twice, and there was no resolution given.

I initially had just a momentary switch on GPIO6 (the GPIO I am using to trigger the interrupt). I thought I might be getting a lot of switch bounce, so I added a few different caps and I still got a double count every time the switch was pressed (grounding the GPIO pin). So, I setup a function generator with a 10 second period (5 seconds of low pulse, and 5 seconds of high pulse). The interrupt is setup so the GPIO interrupt triggers on the negative edge, and when the pulse goes from high to low the interrupt fires - apparently twice. When the pulse goes back to high, nothing happens (expected)... until 5 seconds later when the interrupt fires again (twice) on the next negative edge.

I am doing this under v2.0.17 of the Arduino core, but I did switch to v3.0.4 just to see if that made any difference, and it didn't. I currently have some libraries that are broken under 3.x so i am stuck using the older core until those issues are resolved. Here is the C++ code I am using for the test:

  1. #include "soc/periph_defs.h"
  2.  
  3. #define GPIO_INPUT_IO_0 6                               // GPIO 6 as the input
  4. #define GPIO_INPUT_PIN_SEL (1ULL<<GPIO_INPUT_IO_0)
  5.  
  6. //#define INTR_NUM 14                                     // NMI
  7. #define INTR_NUM 31                                     // LEVEL 5
  8.  
  9. volatile uint32_t intCounter = 0;
  10.  
  11. void setup() {
  12.   Serial.begin(115200);
  13.   gpio_config_t io_conf;
  14.   io_conf.intr_type = GPIO_INTR_NEGEDGE;                // interrupt of falling edge
  15.   io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;            // bit mask of the pin(s)
  16.   io_conf.mode = GPIO_MODE_INPUT;                       // set as input mode
  17.   io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;         // disable pull-down mode
  18.   io_conf.pull_up_en = GPIO_PULLUP_ENABLE;              // enable pull-up mode
  19.   gpio_config(&io_conf);
  20.  
  21.   ESP_INTR_DISABLE(INTR_NUM);                           // disable interrupt 31
  22.   esp_intr_alloc(ETS_GPIO_INTR_SOURCE, ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_IRAM, NULL, NULL, NULL);
  23.   ESP_INTR_ENABLE(INTR_NUM);                            // enable interrupt 31
  24. }
  25.  
  26. void loop() {
  27.   Serial.println(intCounter);
  28.   delay(100);
  29. }

You can see I used the ESP-IDF method of setting up GPIO06 as an input with pull-up, and using the negative edge for the trigger. Then the interrupt handler is installed. Once the setup() section is complete the main loop just outputs the value of a variable every 100ms. The interrupt handler updates that variable every time the interrupt is fired. This code should be easy to compile under the ESP-IDF when you add the necessary includes.

The assembly interrupt handler code:

  1. #include <xtensa/coreasm.h>
  2. #include <xtensa/corebits.h>
  3. #include <xtensa/config/system.h>
  4. #include "freertos/xtensa_context.h"
  5. #include "esp_private/panic_reason.h"
  6. #include "sdkconfig.h"
  7. #include "soc/soc.h"
  8. #include "soc/gpio_reg.h"
  9. #include "soc/dport_reg.h"
  10.  
  11. #define L5_INTR_STACK_SIZE 8
  12. #define LX_INTR_A15_OFFSET 0
  13. #define LX_INTR_A14_OFFSET 4
  14.  
  15. .data
  16. _l5_intr_stack:
  17. .space    L5_INTR_STACK_SIZE                            // define stack size
  18.  
  19. .section  .iram1,"ax"                                   // place ISR into IRAM
  20. .global   xt_highint5                                   // re-define highint5 pointer
  21. .type     xt_highint5,@function                         // set type to interrupt
  22. .align    4
  23.  
  24. /* start of ISR */
  25. xt_highint5:
  26.  
  27. /* Save registers - a0 saved in ISR entry */
  28.   movi  a0, _l5_intr_stack                              // get stack pointer
  29.   s32i  a15, a0, LX_INTR_A15_OFFSET                     // save a15
  30.   s32i  a14, a0, LX_INTR_A14_OFFSET                     // save a14
  31.  
  32. /* Increment intr_cnt */
  33.   movi  a14, intCounter                                 // get address of intCounter variable from main program
  34.   l32i  a15, a14, 0                                     // get value from variable
  35.   addi  a15, a15, 1                                     // add 1 to value
  36.   s32i  a15, a14, 0                                     // put value back into variable
  37.   memw                                                  // write through
  38.  
  39. /* Clear interrupt flag for input */
  40.   movi  a14, GPIO_STATUS_W1TC_REG                       // address of interrupt flags register
  41.   movi  a15, (1 << 6)                                   // mask for GPIO 6 (our input)
  42.   s32i  a15, a14, 0                                     // store mask into interrupt flags (clear interrupt)
  43.   memw                                                  // write through
  44.  
  45. /* Restore registers */
  46.   l32i  a14, a0, LX_INTR_A14_OFFSET                     // restore a14
  47.   l32i  a15, a0, LX_INTR_A15_OFFSET                     // restore a15
  48.   memw                                                  // write through
  49.  
  50.   rsr a0, EXCSAVE_5                                     // restore a0
  51.   rfi   5                                               // exist ISR
  52.  
  53. .global ld_include_xt_highint5                          // force compiler to override weak inclusion
  54. ld_include_xt_highint5:

Here you can see the registers are saved, the variable (intCounter) is incremented, the registers are restored, and the routine exits. The weak override is clearly working because I do get an incrementing value - just by 2 instead of 1 that I expect!

I am hoping someone can explain why the interrupt is firing twice.



PROBLEM #2 - NMI interrupt crashes and reports BRK instruction wasn't handled

I have a situation where I need two high level interrupts, neither of which I want interrupted by anything in the ESP32 OS. One interrupt has to take priority over the other so I thought the simple solution would be to have xt_highint5 and a xt_nmi interrupts.

I took the above code and modified it so that the interrupt number (INTR_NUM) for the interrupt installation is 14 (NMI) instead of 31 (INT5), and then modified the assembly code so that it overrides the NMI vector using the same method I used for the high level interrupt (5). This compiles correctly but when the interrupt is triggered I get a core panic with a message that a BRK instruction was not handled... almost like the wrong INTR_NUM is being used. Is there any known issues with taking over the NMI? Does something else need to be cleared or set because it's an NMI interrupt?

This is probably above most user's requirements, but if you know something please chime in! Thank you!

Re: High level interrupt (5) and NMI issues

Posted: Mon Aug 19, 2024 5:39 am
by ESP_Sprite
Could be that your interrupt acknowledge (clearing the interrupt flag) takes a somewhat long time to propagate back to the CPU, meaning that after the 5 or so instructions you take afterwards to return from the ISR, the interrupt is seen as triggered by the CPU causing the ISR to run a 2nd time. You could try to swap the code for that and the code for increasing the counter.

On why the NMI doesn't work, I have no idea. The only difference vs high-level interrupts I could spot is that you don't need to clear them using INTCLEAR.

Re: High level interrupt (5) and NMI issues

Posted: Mon Aug 19, 2024 10:34 am
by OSCPUDEV
You were spot on about why the interrupt could be firing twice in a row! I swapped the code around so that the interrupt flag is cleared immediately following saving the registers. That does in fact solve the problem with the interrupt firing twice! Why is this the case though? I would expect that since I am flushing the writes out using "memw" that this should not be possible. Regardless this solves the issue for me, at least with the ESP32-S2. I will try this with the ESP32-S3 to confirm that is also the case.

In case anyone is interested, it takes no less than 20 CPU instruction cycles for whatever magic the CPU needs to do (to register the interrupt flag is really cleared) before you can exit the interrupt and not have it immediately re-trigger the interrupt. I experimented with using NOP's and other instructions to determine the required amount of time. It's definitely the best choice to clear the interrupt flag immediately after saving registers and before doing any other code.


In regards to the NMI... I thought that you always needed to clear the GPIO interrupt flag... is this not the case with NMI interrupt? I *really* need to get the NMI to work as I need it and the high level interrupt functioning.

Thanks for your help!

Re: High level interrupt (5) and NMI issues

Posted: Mon Aug 19, 2024 7:48 pm
by OSCPUDEV
Well, after looking over my code to try to get the NMI version to work I see that I made a big mistake in the interrupt setup (C++ code). I had the INTR_NUM at 14 correct, but I had the flags wrong. The esp_intr_alloc() flags should be ESP_INTR_FLAG_NMI (not ESP_INTR_FLAG_LEVEL5).

Now, the ESP32 doesn't crash when the pulse occurs, however, my interrupt code is not actually being installed. I verified this by commenting out the 'rfi' instruction which should crash the ESP32 because it would not be exiting the interrupt properly.

So, here is the project using NMI instead of Level 5. @ESP_Sprite, can you try this code to see if it overrides the weak casting? I have seen one other person also report that they were not able to replace the NMI. Can you also explain what you meant about not requiring to clear the interrupt flag? Thanks!


C++ code:

  1. #include "soc/periph_defs.h"
  2.  
  3. #define GPIO_INPUT_IO_0 6                               // GPIO 6 as the input
  4. #define GPIO_INPUT_PIN_SEL (1ULL<<GPIO_INPUT_IO_0)
  5.  
  6. #define INTR_NUM 14                                     // NMI
  7. //#define INTR_NUM 31                                     // LEVEL 5
  8.  
  9. volatile uint32_t intCounter = 0;
  10.  
  11. void setup() {
  12.   Serial.begin(115200);
  13.   gpio_config_t io_conf;
  14.   io_conf.intr_type = GPIO_INTR_NEGEDGE;                // interrupt of falling edge
  15.   io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;            // bit mask of the pin(s)
  16.   io_conf.mode = GPIO_MODE_INPUT;                       // set as input mode
  17.   io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;         // disable pull-down mode
  18.   io_conf.pull_up_en = GPIO_PULLUP_ENABLE;              // enable pull-up mode
  19.   gpio_config(&io_conf);
  20.  
  21.   ESP_INTR_DISABLE(INTR_NUM);                           // disable interrupt 14
  22.   esp_intr_alloc(ETS_GPIO_INTR_SOURCE, ESP_INTR_FLAG_NMI|ESP_INTR_FLAG_IRAM, NULL, NULL, NULL);
  23.   ESP_INTR_ENABLE(INTR_NUM);                            // enable interrupt 14
  24. }
  25.  
  26. void loop() {
  27.   Serial.println(intCounter);
  28.   delay(100);
  29. }


Int handler assembly code:

  1. #include <xtensa/coreasm.h>
  2. #include <xtensa/corebits.h>
  3. #include <xtensa/config/system.h>
  4. #include "freertos/xtensa_context.h"
  5. #include "esp_private/panic_reason.h"
  6. #include "sdkconfig.h"
  7. #include "soc/soc.h"
  8. #include "soc/gpio_reg.h"
  9. #include "soc/dport_reg.h"
  10.  
  11. #define NMI_INTR_STACK_SIZE 8
  12. #define LX_INTR_A15_OFFSET 0
  13. #define LX_INTR_A14_OFFSET 4
  14.  
  15. .data
  16. _nmi_intr_stack:
  17. .space    NMI_INTR_STACK_SIZE                           // define stack size
  18.  
  19. .section  .iram1,"ax"                                   // place ISR into IRAM
  20. .global   xt_nmi                                        // re-define highint5 pointer
  21. .type     xt_nmi,@function                              // set type to interrupt
  22. .align    4
  23.  
  24. /* start of ISR */
  25. xt_nmi:
  26.  
  27. /* Save registers - a0 saved in ISR entry */
  28.   movi  a0, _nmi_intr_stack                             // get stack pointer
  29.   s32i  a15, a0, LX_INTR_A15_OFFSET                     // save a15
  30.   s32i  a14, a0, LX_INTR_A14_OFFSET                     // save a14
  31.  
  32. /* Clear interrupt flag for input */
  33.   movi  a14, GPIO_STATUS_W1TC_REG                       // address of interrupt flags register
  34.   movi  a15, (1 << 6)                                   // mask for GPIO 6 (our input)
  35.   s32i  a15, a14, 0                                     // store mask into interrupt flags
  36.   memw                                                  // write through
  37.  
  38. /* Increment intr_cnt */
  39.   movi  a14, intCounter                                 // get address of intCounter variable from main program
  40.   l32i  a15, a14, 0                                     // get value from variable
  41.   addi  a15, a15, 1                                     // add 1 to value
  42.   s32i  a15, a14, 0                                     // put value back into variable
  43.   memw                                                  // write through
  44.  
  45. /* Restore registers */
  46.   l32i  a14, a0, LX_INTR_A14_OFFSET                     // restore a14
  47.   l32i  a15, a0, LX_INTR_A15_OFFSET                     // restore a15
  48.   memw
  49.  
  50.   rsr a0, EXCSAVE_5                                     // restore a0
  51.   rfi   5                                               // exist ISR
  52.  
  53.  
  54. .global ld_include_xt_nmi                               // force compiler to override weak inclusion
  55. ld_include_xt_nmi:

Re: High level interrupt (5) and NMI issues

Posted: Mon Aug 19, 2024 8:52 pm
by OSCPUDEV
I also have a question about changing interrupt handlers. How can I have 5 different _xt_nmi routines and choose which one is used? I do NOT want to have to test inside of the interrupt and branch/jump to the appropriate code.. that would require >25ns, which is way too long. There must be a way to point the interrupt vector(s) at specific code. I would like to know how to do that. Thanks!

Re: High level interrupt (5) and NMI issues

Posted: Tue Aug 20, 2024 3:40 am
by ESP_Sprite
OSCPUDEV wrote:
Mon Aug 19, 2024 10:34 am
Why is this the case though? I would expect that since I am flushing the writes out using "memw" that this should not be possible.
memw is a *memory* fence; it makes sure that whatever else is in the system reads back the written value if it reads any location you previously wrote. However, the 'side effect' of an interrupt signal being cleared is not memory-related and as such it may take some time to get back to the CPU.

Do you actually refer to 'ld_include_xt_nmi' in your CMakelists.txt? (cflags needs a '-u ld_include_xt_nmi' to force the linker to evaluate your asm file)

Re: High level interrupt (5) and NMI issues

Posted: Tue Aug 20, 2024 12:03 pm
by OSCPUDEV
Do you actually refer to 'ld_include_xt_nmi' in your CMakelists.txt? (cflags needs a '-u ld_include_xt_nmi' to force the linker to evaluate your asm file)

I am using the Arduino IDE. I don't do anything with make files, just click the compile button. The exact same code, using .global ld_include_xt_highint5 works. I would expect that doing the exact same thing using .global ld_include_xt_nmi would work as well.

I do not have the option of using the ESP-IDF.

BTW, I have a great working routine using the high level int (5). I am able to handle 2 million interrupts per second. Now I just need to do the same using the NMI.

Re: High level interrupt (5) and NMI issues

Posted: Wed Aug 21, 2024 2:59 am
by ESP_Sprite
You need to refer to that symbol somehow, otherwise the linker will ignore your code. Perhaps something like

Code: Select all

external void ld_include_xt_nmi();
volatile uint32_t *p;
p=&ld_include_xt_nmi;
in any routine that is used will also do the trick.

Re: High level interrupt (5) and NMI issues

Posted: Wed Aug 21, 2024 6:16 am
by OSCPUDEV
I tried a variety of things to invoke the override. "External" is not valid (Extern is). Also, this is not valid under Arduino IDE (compiler errors):

  1. p=&ld_include_xt_nmi;

I tried even making a subroutine calls like: call ld_include_xt_nmi(), call _xt_nmi(), etc. and that didn't change anything.

Do you know if the Arduino core is maybe blocking the NMI from being overridden? It seems that if the _ld_include_xt_highint5 works then so should the NMI version.

Re: High level interrupt (5) and NMI issues

Posted: Wed Aug 21, 2024 6:52 am
by ESP_Sprite
Ah sorry, that should be 'extern' indeed. Sorry, I don't know enough on Arduino to know what happened there.