How to make PWM rising edge trigger an interrupt?

The official pyboard running MicroPython.
This is the reference design and main target board for MicroPython.
You can buy one at the store.
Target audience: Users with a pyboard.
TheSilverBullet
Posts: 50
Joined: Thu Jul 07, 2022 7:40 am

Re: How to make PWM rising edge trigger an interrupt?

Post by TheSilverBullet » Tue Jul 19, 2022 2:06 pm

Interesting.
I did a similar measurement, only with the rp2040 doing the measurement on itself.
The resolution is precisely 40ns (PIO at 125MHz)
To test it, I ran the PWM at 2000Hz with duty 50%. The PIO measurement showed exactly 250.000µs from High to low edge. Then I added a hard interrupt which manually forced the GPIO to low for a very short time. Just enough that the PIO was able to detect the falling edge.

Code: Select all

@micropython.viper
def _force_led_off(pin):
    p: ptr32 = ptr32(0x400140cc) # direct GPIO25 access
    saved: int = p[0]  # save state
    p[0] = (3<<12) | (2<<8)  # override OEOVER and OUTOVER
    loop_count: int = 100
    while loop_count > 0:  # wait a moment so that the PIO can pick up the new state
        loop_count -= 1
    p[0] = saved  # restore state

and later in the program:    
_ = generate.irq(trigger=Pin.IRQ_RISING, handler=_force_led_off, hard=True)
On most interrupts, the measured latency was 14.89µs with the occasional 12.2µs
The question is: Why are the first couple of interrupts are so ›all over the place‹?
The graph only shows 200 samples, a 50000 sample long run was very stable and basically a continuation of the first graph.
Attachments
temp.log.zip
(301 Bytes) Downloaded 1921 times
irq_latency.png
irq_latency.png (103.73 KiB) Viewed 29729 times

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: How to make PWM rising edge trigger an interrupt?

Post by Roberthh » Tue Jul 19, 2022 2:19 pm

Difference in measuerements: My code creates a GPIO pulse and the measurement is about these external pulses. That itself has a few µs latency for the call, until the Pin level changes.
First samples large: Could be a caching effect of the flash.

TheSilverBullet
Posts: 50
Joined: Thu Jul 07, 2022 7:40 am

Re: How to make PWM rising edge trigger an interrupt?

Post by TheSilverBullet » Tue Jul 19, 2022 2:31 pm

Roberthh wrote:
Tue Jul 19, 2022 2:19 pm
First samples large: Could be a caching effect of the flash.
Nice idea. That's worth investigating further.
Not that it would matter IRL but inquiring minds want to know.
For anything in the sub 100µs range, it'll be thumb assembler, hardware, DMA or I'll heat up the C-compiler. ;-)

But still, those numbers are pretty impressive for a Python program.

rkompass
Posts: 66
Joined: Fri Sep 17, 2021 8:25 pm

Re: How to make PWM rising edge trigger an interrupt?

Post by rkompass » Tue Jul 19, 2022 4:30 pm

You could try to pwm and trigger the same interrupt callback function on a different pin (which is then reset in the callback, using the pin argument provided by the dispatcher) immediately before doing this on the pin you measure with the PIO. If it's about caching the ISR then the latency should already be low from the beginning with the new pin.

rkompass
Posts: 66
Joined: Fri Sep 17, 2021 8:25 pm

Re: How to make PWM rising edge trigger an interrupt?

Post by rkompass » Tue Jul 19, 2022 5:29 pm

I made a fast asm_thumb ISR (the calling will take about 4 us anyway) and tested it the following setup:

Code: Select all

from machine import Pin
from time import sleep_ms

pinA = Pin(9, Pin.OUT)     # has a led in my setup :-)
pinB = Pin(8, Pin.OUT)     # has a led in my setup :-)

@micropython.asm_thumb
def _callb_pinclr(r0):     # r0 is the pin id:  id(pinX)
    ldr(r3, [r0, 4])       # r3 is pin_number now
    mov(r2, 1)
    lsl(r2, r3)            # r2 = 1 << pin_number
    mov(r6, 0xd0)          # construct SIO base address (0xd0000004 = 0xd0 << 24)
    lsl(r6, r6, 24)
    str(r2,[r6, 0x18])     # store r2 at SIO GPIO_OUT_CLR offset

print('Without pin clearing ISR')
sleep_ms(1600)
pinA(1); pinB(1)
sleep_ms(1200)
pinA(0); pinB(0)

pinA.irq(trigger=Pin.IRQ_RISING, handler=_callb_pinclr, hard=True)

print('With pin clearing ISR')
sleep_ms(1600)
pinA(1); pinB(1)
sleep_ms(1200)
pinA(0); pinB(0)
I observe only pinB flashing for 1.2 seconds. This demonstrates that the ISR only clears the pin it is attached to as ISR, as intended.
Of course this is for RPI2040, we are in the wrong group now.

#TheSilverBullet perhaps you would like to use that in your test setup. BTW, could you present the measurement PIO program, would save me some effort to reproduce your findings?
Thanks, Raul

TheSilverBullet
Posts: 50
Joined: Thu Jul 07, 2022 7:40 am

Re: How to make PWM rising edge trigger an interrupt?

Post by TheSilverBullet » Tue Jul 19, 2022 5:49 pm

rkompass wrote:
Tue Jul 19, 2022 5:29 pm
#TheSilverBullet perhaps you would like to use that in your test setup. BTW, could you present the measurement PIO program, would save me some effort to reproduce your findings?
Thanks, Raul
Sure, no problem. Here are the snippets:

Code: Select all

@asm_pio(fifo_join=PIO.JOIN_RX)
def pio_prog_v2():
    # runs @ 125MHz: cycletime == 1e9 / 125e6 == 8ns
    wrap_target()
    mov     (y, null)           #       0 into Y (counter)
    wait    (1, pin, 0)         #       wait for pin changing to high

    # Now pin is 1 (on)
    label   ('b1')
    jmp     (y_dec, 'f1')       # 1     Y = Y - 1
    label   ('f1')
    mov     (isr, null)         # 1     clear isr
    in_     (pins, 1)           # 1     shift pin into ISR
    mov     (x, isr)            # 1     ISR into X
    jmp     (x_dec, 'b1')       # 1     jump if pin is NOT 0 (NOT low)

    # Due to the 5cycle loop, there's a 0…40ns fuzzyness possible

    # Now pin is 0 (off)
    mov     (isr, invert(y))    # -Y into isr
    push    (noblock)           # value into FIFO
    wrap()                      # aaaand again…

Code: Select all

    CPU_SPEED = int(125 * 1e6)
    machine.freq(CPU_SPEED)

    LED = 25
    generate = Pin(LED, Pin.OUT) # LED on Pi pico board PWM-CH:4B

    sm = StateMachine(0)
    sm.init(pio_prog_v2, freq=CPU_SPEED, in_base=generate)
    sm.active(True)
    sm.restart()

    pwm = PWM(generate)
    pwm.freq(2000)
    pwm.duty_ns(250_000)  # a 250µs pulse each 500µs
Ups, almost forgotten this:

Code: Select all

    for _ in range(20):
        if sm.rx_fifo() == 0:
            sleep_ms(1)
        else:
            counter = sm.get()
            t_ns = counter * 5 * 8  # each count: 5 asm-instructions times 8ns
            print(f'{t_ns / 1000.0:.3f} µs')



rkompass
Posts: 66
Joined: Fri Sep 17, 2021 8:25 pm

Re: How to make PWM rising edge trigger an interrupt?

Post by rkompass » Wed Jul 20, 2022 6:47 pm

Hello @TheSilverBullet, thank you for sharing code.
Glad to find other inquiring minds I was trying to reproduce and improve :-)
I constructed a PulseTimer class that has 2 period (=16 ns at 125 MHz) resolution. The idea is to use jmp(pin, LABEL) to get out of the loop with one instruction.

The same logic is used in https://github.com/dhylands/upy-example ... measure.py btw., with an obscure delay [1], as I noticed now.

Code: Select all

class PulseTimer:

    def __init__(self, pin, sm_id=4):   # Pin should be a machine.Pin instance
        self.sm = StateMachine(sm_id, self.sm_d_pulse, freq=125_000_000, in_base=pin, jmp_pin=pin)
        self.sm.restart()
        self.sm.active(1)   # start the StateMachine

    @asm_pio()
    def sm_d_pulse():
        wait(0, pin, 0)     # in case pin is still high, at start, wait for low
        label('start')
        mov(x, null)        # initialize X to zero
        mov(x, invert(x))
        wait(1, pin, 0)     # now being low, wait for high, which starts the measurement
        label('loop')
        jmp(x_dec, 'here')  # decrement x, jump nowhere; note that invert, decrement, invert = increment
        label('here')
        jmp(pin, 'loop')    # in case pin is high, repeat counting
        mov(isr, invert(x)) # we send the (again inverted) counter value to FIFO
        push(noblock)       # (in case of block: and halt, till it's read)
        jmp('start')

    def has_data(self):
        return self.sm.rx_fifo() > 0
    
    def read(self):
        return self.sm.get() if self.sm.rx_fifo() else None

    def duration_ns(self):
        return self.sm.get()*16+8 if self.sm.rx_fifo() else None  # additional 8: we tested
Idea: One could go extreme and employ 2 PIO state machines in parallel with changed order of jmp(x_dec, 'here') and jmp(pin, 'loop') to get 1 period resolution.

I observed that my assembler function did not turn off the pin in PWM mode. I measured small latencies also in the 9-17 us range for switching off programmatically.
So I had to use your viper code @TheSilverBullet. I learnt that the SIO has no way to enable output against special function PWM.

New assembler function is here:

Code: Select all

@micropython.asm_thumb
def _callb_pinclr_asm(r0):     # r0 is the pin id:  id(pinX)
        ldr(r3, [r0, 4])       # r3 is pin_number now
        data(2, 0x4d00)        # ldr  r5, [pc, #0]  load r5 with data following the branch
        b(HERE0)
        data(4, 0x10005001)    # r5 = GPIO0_CTRL: 0x40014004 but we put 0x40014004>>2 here
        label(HERE0)
        lsl(r5, r5, 2)         # correct r5
        lsl(r3, r3, 3)
        add(r5, r5, r3)        # r5 = GPIOx_CTRL of pin x given by r0
        ldr(r2, [r5, 0])       # save CTRL bits in r2
        mov(r1, 0x32)
        lsl(r1, r1, 8)         # r1 = (3<<12) | (2<<8)
        str(r1, [r5, 0])       # override OEOVER=0x3: enable output and OUTOVER=0x2: drive output low in GPIO0_CTRL
        nop()
        nop()
        nop()                  # 3 nops are enough for pin to settle !?!
#         mov(r4, 3)
#         label(LOOP)
#         sub(r4, 1)
#         cmp(r4, 0)
#         bgt(LOOP)
        str(r2, [r5, 0])       # restore CTRL bits in GPIOx_CTRL register
With these functions I did measurements at different PWM frequencies.
I also observed that the first 2-3 interrupts had excess latencies.
I found no such regularity as you in the measured latencies.
Statistical evaluation of latencies 11..2000 (excluding the 10 first) of PWM runs at different frequencies yielded:

Code: Select all

PWM_Frequ:   500  Min: 11224 Median: 15240.0 Max: 28328 Mean: 16567.5
PWM_Frequ:  1000  Min: 10008 Median: 15240.0 Max: 25912 Mean: 14701.7
PWM_Frequ:  2000  Min:  7576 Median: 15240.0 Max: 16056 Mean: 13546.3
PWM_Frequ:  5000  Min:  6760 Median:  7960.0 Max: 16056 Mean: 10152.8
PWM_Frequ: 10000  Min:  6760 Median:  7576.0 Max: 12168 Mean:  7569.5
PWM_Frequ: 20000  Min:  6760 Median:  6776.0 Max: 11080 Mean:  7101.5
PWM_Frequ: 30000  Min:  6760 Median:  6760.0 Max:  8104 Mean:  6844.3
PWM_Frequ: 40000  Min:  6248 Median:  6248.0 Max:  6248 Mean:  6248.0
You can see the latencies going down a lot with increasing PWM frequencies, which seems to confirm Roberts assumption that caching might be the reason for the long latencies at the beginning of runs.
Also I think that a max of 6.2 us in the "heavy duty" situation (and practically no variation) seems phenomenal. Unbelievable almost, could I have something missed?

rkompass
Posts: 66
Joined: Fri Sep 17, 2021 8:25 pm

Re: How to make PWM rising edge trigger an interrupt?

Post by rkompass » Mon Jul 25, 2022 11:29 am

I have to correct my previous findings. My numbers and those by @TheSilverBullet are to a great extent artefacts.

The functions used for stopping the PWM signal suppressed the PWM pulse by forcing it low for a brief time (~2-3 us) but then gave way to the original PWM pulse which was much longer (250 us). That way another interrupt was generated by the then rising flank and so on for several times. When the PWM pulse ended, a much briefer positive signal was measured, which was not related to interrupt latency but was just the rest of PWM pulse duration (when all previous latencies subtracted, so to say). I assume that the regularity in @TheSilverBullet graphs might reflect this.
In my last setup, when I increased the PWM frequency to 40000Hz I used PWM pulses that were just as short as the reported 6.248 us, therefore no variation in the measurement.
At higher PWM frequencies I noticed a considerable breakdown of micropython speed, which was explained by the permanent interrupts occurring in the above pattern. I later measured that instead of 2000 interrupts in a run, > 52000 interrupts were generated. The slow uPy interpreter code that attempted to get the measured delays from the PIO FIFO could not cope with that and in the end a incontrolled selection of the measurement data took place.

I did not know about PIO DMA usage in @Roberthh's repository yet so another approach to solve the speed problem in collecting the FIFO data came to my mind:
It should be possible to write a PIO program that determines the maximum of a series of pulses. As far as I knew this had not been done yet.
I wrote such a program then and noticed I could also write a PIO program for minimum and mean. So now I had a class that did minimal pulse statistics. Nexts days passed by testing it thoroughly, as I did not want to contribute more questionable results here.
In the new setup the ISR generated a short termination pulse on another pin, so the PWM pulse duration problem was avoided.
The new data are:

Code: Select all

Freq:   500   Latency min: 6648 max: 36936 mean: 6663.1 ns;  n: 2000
Freq:   500   Latency min: 6648 max: 26856 mean: 6658.5 ns;  n: 2000
Freq:   500   Latency min: 6648 max: 19128 mean: 6654.2 ns;  n: 2000
Freq:   500   Latency min: 6648 max: 25224 mean: 6657.2 ns;  n: 2000
Freq:   500   Latency min: 6648 max: 43344 mean: 6666.3 ns;  n: 2000

Freq:  1000   Latency min: 6648 max: 23232 mean: 6656.4 ns;  n: 2000
Freq:  1000   Latency min: 6648 max: 32088 mean: 6661.1 ns;  n: 2000
Freq:  1000   Latency min: 6648 max: 30864 mean: 6660.1 ns;  n: 2000
Freq:  1000   Latency min: 6648 max: 24816 mean: 6657.0 ns;  n: 2000
Freq:  1000   Latency min: 6648 max: 25224 mean: 6657.2 ns;  n: 2000

Freq:  2000   Latency min: 6648 max: 22824 mean: 6656.0 ns;  n: 2000
Freq:  2000   Latency min: 6648 max: 20400 mean: 6654.8 ns;  n: 2000
Freq:  2000   Latency min: 6648 max: 25608 mean: 6657.6 ns;  n: 2000
Freq:  2000   Latency min: 6648 max: 16776 mean: 6653.0 ns;  n: 2000
Freq:  2000   Latency min: 6648 max: 27240 mean: 6658.2 ns;  n: 2000

Freq:  5000   Latency min: 6648 max:  9480 mean: 6649.4 ns;  n: 2000
Freq:  5000   Latency min: 6648 max: 16872 mean: 6654.3 ns;  n: 2000
Freq:  5000   Latency min: 6648 max: 16344 mean: 6653.2 ns;  n: 2000
Freq:  5000   Latency min: 6648 max:  8664 mean: 6649.0 ns;  n: 2000
Freq:  5000   Latency min: 6648 max: 47328 mean: 6673.4 ns;  n: 2000

Freq: 10000   Latency min: 6648 max: 34680 mean: 6675.0 ns;  n: 2000
Freq: 10000   Latency min: 6648 max: 28080 mean: 6669.3 ns;  n: 2000
Freq: 10000   Latency min: 6648 max: 13104 mean: 6653.6 ns;  n: 2000
Freq: 10000   Latency min: 6648 max: 28776 mean: 6669.8 ns;  n: 2000
Freq: 10000   Latency min: 6648 max: 13896 mean: 6655.0 ns;  n: 2000

Freq: 20000   Latency min: 6648 max: 35328 mean: 6675.1 ns;  n: 2000
Freq: 20000   Latency min: 6648 max: 57528 mean: 6683.9 ns;  n: 2000
Freq: 20000   Latency min: 6648 max: 33456 mean: 6673.8 ns;  n: 2000
Freq: 20000   Latency min: 6648 max: 36480 mean: 6676.5 ns;  n: 2000
Freq: 20000   Latency min: 6648 max: 37368 mean: 6677.1 ns;  n: 2000
In all runs a period of 200 us passes in which interrupts are elicited by PWM but where the measurement is not started yet. So the first 2-3 longer ISR latencies, which are still there, are avoided.
The same minimal latency throughout all test runs is observed. This is with assembler code using SIO. In viper the same minimal latency is 700 ns larger (but still very low). Then rather large ISR latency maxima are observed which arbitrarily get as large as 60 us, independent of the PWM speed. From a comparison with the means reported one can conclude that these are rather rare outliers. Means are quite close to the minimal values and a single outlier latency like ~60000 ns pushes them just 60000/2000=30 ns higher.

Anyway these outliers occur in almost all test runs, so the nice finding that ISR latencies are limited to about 17 us, if the interrupts occur regularly in short succession, is not confirmed by this closer inspection.

I will share the pulse stat class on the RPI2040 section of this forum.

Post Reply