interrupt latency on ESP32 - Page 1
Forum section: electronics / general

author: apraxophobia
author_rank: Contributor
timestamp: July 04, 2024, 03:56:04 pm
content: I need to output SPI messages from my ESP32 with well-defined regular intervals (100 kHz). Effectively, I need to make a DAC using an external DAC SPI IC. But I only need to make square waves and constant DC.In my experience the only way to get a solid, jitter-free 70 kHz trigger is with ledcWrite. Timers, delays etc. always introduce jitter.So the current approach is to make a 70 kHz square wave on one pin, where a change on that pin triggers sending an SPI message with an interrupt routine.Via some simplifying (e.g. just toggling a pin instead of using SPI), there's clearly an issue with the timing jitter of the execution of the interrupt routine. The 70 kHz wave is solid, and via debugging I know that the actual execution of the interrupt routine (toggling a pin) always takes the same duration of time. But there's jitter in the delay between when the interrupt is triggered (the solid 70 kHz square wave) and when the function within the interrupt is executed. This isn't terrible, but results in a jittery cycle about one out of every thousand or so cycles.I've tried using higher priorities, disabling wifi, etc., and the interrupt function itself is dead simple, but I always have this issue. Am I trying to do something impossible? I have had near-zero luck with finding anything online about this (seemingly straightforward) issue.TLDR:why is it so hard to generate a 100 kHz square wave with esp32 using interrupts and an external triggering 100 kHz square wave?

author: selcuk
author_rank: Frequent Contributor
timestamp: July 04, 2024, 04:06:46 pm
content: I used ledc in the past with a 20 MHz output and I didn't remember issues. There is a glitch filter. You may want to disable that. And I recommend you to use ESP-IDF for such a project. You may have better control over hardware access.

author: apraxophobia
author_rank: Contributor
timestamp: July 04, 2024, 04:14:43 pm
content: The 100 kHz wave from ledc is solid, it's the ISR triggered off a change on the ledc pin that is giving issues.The jitter appears somewhat regular, which makes me think it's a delay caused by some background routine, but I don't know.I'll try ESP-IDF, though, thanks.

author: globoy
author_rank: Frequent Contributor
timestamp: July 04, 2024, 04:22:23 pm
content: One out of 1000 cycles seems pretty good (but I understand, not good enough). The ESP32 memory system is slightly complex and you could be seeing the result of a contention or a cache miss somewhere (maybe contention more likely). Probably there are synchronization domains as well which can introduce jitter. Is this something you could do with the ULP?

author: nctnico
author_rank: Super Contributor
timestamp: July 04, 2024, 04:40:52 pm
content: My approach would be to trigger the SPI transfer from a timer to update the DAC and use a timer-driven pin to update the DAC output. That way the jitter on the SPI transfer has zero effect on the DAC update.The routines and variables to update the DAC need to be in internal RAM to avoid flash read/write latencies.

author: westfw
author_rank: Super Contributor
timestamp: July 05, 2024, 12:28:02 am
content: Do you have your ISR in tightly coupled memory (I guess internal RAM), rather than QSPI flash/ram?On most of these "fast" CPUs, caching interferes with determinancy, and having to reset QSPI addresses to new locations can make that significantly worse.

author: mtwieg
author_rank: Frequent Contributor
timestamp: July 05, 2024, 12:38:37 pm
content: In the past I once drove the CS pin of the slave (an ADC in this case) with the output of a PWM peripheral instead of letting the SPI peripheral control it. Just have to make sure the serial transfer doesn't jitter so much that it overlaps with the CS edges. Of course this approach probably won't work if there are other slaves on the same bus. In that case you should get a slave with a dedicated pin for controlling when the output updates.

author: NorthGuy
author_rank: Super Contributor
timestamp: July 05, 2024, 12:43:27 pm
content: Does your DAC get triggered by a signal edge?If so, it's very easy - trigger it with the external pulse that you already have. Also wire this same external pulse to your MCU and produce an interrupt on the same edge. The interrupt will happen right after the DAC has been triggered, so you have full 10 us to transfer a new value to DAC through SPI. 10 us is plenty, and the exact timing doesn't matter.

author: apraxophobia
author_rank: Contributor
timestamp: July 05, 2024, 01:07:47 pm
content: Quote from: NorthGuy on July 05, 2024, 12:43:27 pmTriggering the DAC isn't the issue. The simplest version of the problem is: given an external 100 kHz square wave, can I make an identical (but maybe slightly phase-shifted) square wave with the esp32 using an interrupt function, just following/copying the external one. I am using IRAM and a very simple interrupt function, which always executes in the same period of time, but there's jitter in the delay after which it is executed so the copied wave generated with the interrupt is jittery.

author: NorthGuy
author_rank: Super Contributor
timestamp: July 05, 2024, 01:30:12 pm
content: Quote from: apraxophobia on July 05, 2024, 01:07:47 pmMy point was that you don't need to create a copy of your square wave and can use the square wave you already have. All you need to do is to synchronize the loading of the DAC with the existing square wave, which does not require any strict timing. Does this make sense?

author: apraxophobia
author_rank: Contributor
timestamp: July 05, 2024, 01:42:36 pm
content: Quote from: NorthGuy on July 05, 2024, 01:30:12 pmI don't quite see what you mean. Even if I can trigger the DAC with a solid square wave, if there's jitter on the SPI messages themselves, the DAC output won't be a solid square wave, right?

author: NorthGuy
author_rank: Super Contributor
timestamp: July 05, 2024, 01:56:29 pm
content: Quote from: apraxophobia on July 05, 2024, 01:42:36 pmThe SPI message doesn't happen all at once, it includes several clock edges which are spread over time. SPI message is not a single event you can time or synchronize.There must be some event which causes the DAC to change output. What you need to do is to precisely time this event. Timing of everything else doesn't matter. What is this exact event which causes your DAC to change output?

author: apraxophobia
author_rank: Contributor
timestamp: July 05, 2024, 02:31:26 pm
content: Quote from: NorthGuy on July 05, 2024, 01:56:29 pmRight- the event triggering the DAC change is the SPI SYNC pin, but this is simultaneous with SPI clock and MOSI. So the problem is the same- I can't get that sync pin (or the other SPI controls) to synchronize with the fast square wave.

author: nctnico
author_rank: Super Contributor
timestamp: July 05, 2024, 03:18:14 pm
content: Typically a DAC has a seperate pin to update the outputs. If your DAC doesn't have such a pin, change to a DAC which has it. It will make your life much easier. Alternatively, you could try to make the sync pin low for the majority of the time (using a PWM output) and send the SPI message during this low time. I have not seen SPI devices which have a sync-active to clock active maximum time limit.

author: NorthGuy
author_rank: Super Contributor
timestamp: July 05, 2024, 03:32:30 pm
content: Quote from: apraxophobia on July 05, 2024, 02:31:26 pmOk. So, your "SPI SYNC" signal goes down, then you transfer data through SPI, then "SPI SYNC" goes up which triggers DAC reload. Is that correct?Then, instead of "SPI SYNC" from the MCU, connect your 100 kHz input signal (assuming it's 50% duty). Also wire the same signal to MCU. Create an interrupt on the falling edge of the 100 kHz signal. When processing this interrupt, transmit SPI. You have 5 us for this, which is still plenty. And the SPI timing doesn't matter.The whole thing will work as follows:On the falling edge of 100 kHz signal the DAC sees the SYNC going down. At the same time, the interrupt happens in your MCU, you start transmitting SPI and quit the interrupt. Some time later, the SPI transmission completes, the DAC receives the data, but the DAC does not update the output and waits until it gets a rising edge of SYNC.On the rising edge of 100 kHz signal, DAC sees is as SYNC going up and updates the output. The change of the DAC output is thereby synchronized to your 100 kHz signal directly. Hence you don't need to create a copy of it.

author: mianos
author_rank: Regular Contributor
timestamp: July 06, 2024, 11:52:07 am
content: With the esp idf RTOS running you can't do something jitter free at 100khz in an interrupt or software. Kinda end of story. I tried. I thought the new "Event Task Matrix" in the C6 would help but it only allows GPIOs to be task targets.The hardware timers and counters do work perfectly, never missing an edge to 10s of Mhz but interrupts have heaps of unavoidable jitter due to higher priority system interrupts.You could try disabling the wifi. That helps a lot.There are quite a few github tickets asking about this.What might be worth a try is using a RP2040 PIO peripheral to do this in real time, outside the CPU core. I got a few boards but have not had time to try.

author: xvr
author_rank: Frequent Contributor
timestamp: July 06, 2024, 12:10:33 pm
content: You could try to use high priority interrupt to reduce jitter. But you have to write it in assembler - C handlers for them not supported.

author: apraxophobia
author_rank: Contributor
timestamp: July 08, 2024, 05:15:52 pm
content: For anyone still following this:-Couldn't quite figure out how to get high-level interrupts to work in assembly with the esp32.-Implemented the same functionality with a teensy 4.0 very, very easily. Don't need wifi/BT for my application, so sticking with this one.Thanks for all your suggestions!

author: xvr
author_rank: Frequent Contributor
timestamp: July 08, 2024, 07:19:16 pm
content: QuoteQuote from file esp-idf\components\xtensa\xtensa_vectors.SCode:/*******************************************************************************HIGH PRIORITY (LEVEL > XCHAL_EXCM_LEVEL) INTERRUPT VECTORS AND HANDLERSHigh priority interrupts are by definition those with priorities greaterthan XCHAL_EXCM_LEVEL. This includes non-maskable (NMI). High priorityinterrupts cannot interact with the RTOS, that is they must save all regsthey use and not call any RTOS function.A further restriction imposed by the Xtensa windowed architecture is thathigh priority interrupts must not modify the stack area even logically"above" the top of the interrupted stack (they need to provide theirown stack or static save area).Cadence Design Systems recommends high priority interrupt handlers be coded in assemblyand used for purposes requiring very short service times.Here are templates for high priority (level 2+) interrupt vectors.They assume only one interrupt per level to avoid the burden of identifyingwhich interrupts at this level are pending and enabled. This allows forminimum latency and avoids having to save/restore a2 in addition to a0.If more than one interrupt per high priority level is configured, this burdenis on the handler which in any case must provide a way to save and restoreregisters it uses without touching the interrupted stack.Each vector goes at a predetermined location according to the Xtensahardware configuration, which is ensured by its placement in a specialsection known to the Xtensa linker support package (LSP). It performsthe minimum necessary before jumping to the handler in the .text section.*******************************************************************************//*Currently only shells for high priority interrupt handlers are providedhere. However a template and example can be found in the Cadence Design Systems toolsdocumentation: "Microprocessor Programmer's Guide".*/#if XCHAL_NUM_INTLEVELS >=2 && XCHAL_EXCM_LEVEL <2 && XCHAL_DEBUGLEVEL !=2.begin literal_prefix .Level2InterruptVector.section .Level2InterruptVector.text, "ax".global _Level2Vector.type _Level2Vector,[member=46715]function.global xt_highint2.align 4_Level2Vector:wsr a0, EXCSAVE_2 /* preserve a0 */call0 xt_highint2 /* load interrupt handler */.end literal_prefix.global xt_highint2.weak xt_highint2.set xt_highint2, _xt_highint2.section .iram1, "ax".type _xt_highint2,[member=46715]function.align 4_xt_highint2:#ifdef XT_INTEXC_HOOKS/* Call interrupt hook if present to (pre)handle interrupts. */movi a0, _xt_intexc_hooksl32i a0, a0, 2<<2beqz a0, 1f.Ln_xt_highint2_call_hook:callx0 a0 /* must NOT disturb stack! */1:#endif/* USER_EDIT:ADD HIGH PRIORITY LEVEL 2 INTERRUPT HANDLER CODE HERE.*/.align 4.L_xt_highint2_exit:rsr a0, EXCSAVE_2 /* restore a0 */rfi 2And the same for levels up to 6