PIO: where am I going wrong? [SOLVED-ish]

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
User avatar
pythoncoder
Posts: 5140
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

PIO: where am I going wrong? [SOLVED-ish]

Post by pythoncoder » Tue Feb 23, 2021 9:26 am

As a first attempt with the PIO I wrote a script to measure the period of a square wave on a pin. This worked after a fashion, producing correct results initially but they gradually "went off" becoming less and less accurate. I've produced this minimal version which only measures the mark time. The square wave is produced by a timer, and viewed on a scope looks perfect with no measurable jitter. I suspect I'm misunderstanding something about the FIFO.

This minimal script runs for a while producing a correct maximum but rubbish minimums before eventually locking up completely.

You'll probably spot my mistake without needing to run it, but to run, link 16 and 17 and paste.

Code: Select all

from machine import Pin, Timer
from rp2 import PIO, StateMachine, asm_pio
import time

@rp2.asm_pio(set_init=rp2.PIO.IN_LOW, autopush=True, push_thresh=32)
def mark():
    wrap_target()
    set(x, 0)
    wait(0, pin, 0)  # Wait for pin to go low
    wait(1, pin, 0)  # Low to high transition
    label('low_high')
    jmp(x_dec, 'next')  # unconditional
    label('next')
    jmp(pin, 'low_high')  # while pin is high

    in_(x, 32)  # Auto push: SM stalls if FIFO full
    wrap()

pin16 = Pin(16, Pin.IN, Pin.PULL_UP)
sm0 = rp2.StateMachine(0, mark, in_base=pin16, jmp_pin=pin16)
_ = sm0.active(1)

pin17 = Pin(17, Pin.OUT)
tim = Timer(freq=1000, mode=Timer.PERIODIC, callback=lambda _: pin17(not pin17()))

maxval = 0
minval = 1000_000
while True:
    # _ = sm0.active(1)
    a = sm0.get()
    # _ = sm0.active(0)
    a = 1 + (a ^ 0xffffffff)  # 2's complement
    maxval = max(maxval, a)
    minval = min(minval, a)
    print(a, maxval, minval)
    time.sleep_ms(200)
Peter Hinch

cebersp
Posts: 30
Joined: Mon Feb 08, 2021 12:07 pm

Re: PIO: where am I going wrong?

Post by cebersp » Tue Feb 23, 2021 10:28 am

Hi Peter,
unfortunately I cannot spot the problem.
Perhaps it is a good idea to use pwm to produce the square wave signal. I would not trust the interrupt routine. :oops:
A second idea is to try this without auto-push first and use push(noblock) like here: viewtopic.php?f=21&t=9839
Christof

danjperron
Posts: 20
Joined: Thu Dec 27, 2018 11:38 pm
Location: Québec, Canada

Re: PIO: where am I going wrong?

Post by danjperron » Tue Feb 23, 2021 4:29 pm

Maybe because your loop is not perfectly always at the same cycle count depending if you are using the jmp or not!

B.T.W. I made more or less the same thing than you!

I don't wrap. It gives the result and stall.
It's on a class . I'm checking FiFo empty flag to check for timeout.

Code: Select all

'''
 *
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2021 Daniel Perron
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
'''

''' this is a program to read the duration of the high pulse in micro-second '''


import utime
import rp2
from rp2 import PIO, asm_pio
from machine import Pin, mem32

@asm_pio()
def PULSE_IN_PIO():
    # clock set at 500Khz  Cycle is 2us
    # drive output low for at least 20ms
    set(pindirs,0)
    set(x,0)
    mov(osr,x)
    wait(0,pin,0)
    wait(1,pin,0)
    label('loop1')
    jmp(x_dec,'loop2')
    label('loop2')
    jmp( pin, 'loop1')
    mov(isr,x)
    push()
    label('done')
    jmp('done')

class pulseIN:

    def __init__(self,dataPin, stateMachine=0, timeout_us = 10000):
        self.dataPin = dataPin
        self.stateMachine = stateMachine
        self.sm= rp2.StateMachine(stateMachine)
        self.SIO_BASE = 0xd0000000
        self.cpuID = mem32[self.SIO_BASE]
        self.PIO_BASE = [ 0x50200000, 0x50300000]
        self.FSTAT = 0x4
        self.timeout_us = timeout_us

    def isFifoTxEmpty(self):
        fifoStat = mem32[ self.PIO_BASE[self.cpuID] | self.FSTAT]
        if self.stateMachine == 0 :
            return (fifoStat >> 24) & 1
        if self.stateMachine == 1 :
            return (fifoStat >> 25) & 1
        if self.stateMachine == 2 :
            return (fifoStat >> 26) & 1
        else:
            return (fifoStat >> 27) & 1

    def isFifoRxEmpty(self):
        fifoStat = mem32[ self.PIO_BASE[self.cpuID] | self.FSTAT]
        if self.stateMachine == 0 :
            return (fifoStat >> 8) & 1
        if self.stateMachine == 1 :
            return (fifoStat >> 9) & 1
        if self.stateMachine == 2 :
            return (fifoStat >> 10) & 1
        else:
            return (fifoStat >> 11) & 1

    def get(self):
        self.sm.init(PULSE_IN_PIO,freq=10000000,
                     in_base=self.dataPin,
                     jmp_pin=self.dataPin)
        self.sm.put(0x100)
        self.sm.active(1)
        start = utime.ticks_us()
        while True:
            now = utime.ticks_us()
            if (now - start) > self.timeout_us:
                return 0
            level = self.isFifoRxEmpty()
            if level==0:
                return (3 + (0xffffffff - self.sm.get())) // 5


if __name__ == "__main__":
    from machine import Pin
    from pulseIN import pulseIN
    import utime

    i = Pin(17,Pin.IN)
    pulsein = pulseIN(i, timeout_us=100000)

    while True:
        print(pulsein.get())
        utime.sleep_ms(250)
My asm code timing is with step of 0.2us but I use 10Mhz clock. it's only counting the HIGH pulse.
The final value are in 1 us step. The first clock is wrong this is why I added 3.
1 to fix the first clock and 2 to switch in half of the 1us since is divide by 5 to get 1us.

User avatar
pythoncoder
Posts: 5140
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: PIO: where am I going wrong?

Post by pythoncoder » Tue Feb 23, 2021 5:26 pm

I'm confident of the timer code having checked the waveform carefully with an oscilloscope.

I tried the push(noblock) change, but that made things worse. Using push(block) produced similar results to my original.

I feel I've failed to grasp something fundamental. In this simple script I can't see why block or noblock makes any difference: you should end up with a valid reading at the end of each loop regardless. For some reason this is evidently not the case.
Peter Hinch

danjperron
Posts: 20
Joined: Thu Dec 27, 2018 11:38 pm
Location: Québec, Canada

Re: PIO: where am I going wrong?

Post by danjperron » Tue Feb 23, 2021 6:45 pm

Ok I tried your version

Thonny print out is way too late against the statemachine fifo.

Even if you stall with the autopush IN command the data store are late by 200ms per fifo data(your delay in the get).

So the next get will output data from the fifo which is a passed value.

I.M.O. You will have a lot of overrun using the print. But if it is only one information at a time just push once.

I use my cycle pulse statemachine to check your code and it is very accurate. I need to figure out your pulse but it is always the same +/- 1

pulse.set(1) => pulse of 1us give 62 63 63 after 2 minutes.
pulse.set(1000) => pulse of 1ms gives 62500 62500 62499 after 2 minutes.

Obviously your counter is working fine.

Maybe it's the timer. I will need to verify it.

This is my code to generate pulse [1us LOW][ n X 1us HIGH]
The PIN 13 is the output and by default is statemachine 1. This way you could connect Pin 13 to pin 16

Code: Select all

'''
 *
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2021 Daniel Perron
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
'''

import utime
import rp2
from rp2 import PIO, asm_pio
from machine import Pin

#
#     A  B
#       ___
#     _/   \
#
#     A = 1 cycle pulse always 1us
#     B = pulse length in us


@asm_pio(set_init=(PIO.OUT_HIGH),autopush=True, push_thresh=8)
def PULSE_PIO():
    pull()                      # 0 get pulse length
    mov(y,osr)                  # 1 store pulse length into y
    jmp(y_dec,'decY')
    label('decY')
    mov(x,y)
    set(pindirs,1) [1]
    label('pulseLoop')
    set(pins,1) [18]
    jmp(x_dec,'pulseLoop')
    set(pins,0) [17]
    mov(x,y)
    jmp('pulseLoop')




class cyclePulse:
    def __init__(self,outputPin,smID=1):
        self.outputPin = outputPin
        self.smID = smID
        self.outputPin.init(Pin.OUT)
        self.outputPin.value(0)
        self.sm= rp2.StateMachine(smID)


    def set(self, pulseLength):
        #start state machine
        self.sm.init(PULSE_PIO,freq=20000000,
                     set_base=self.outputPin)

        self.sm.put(pulseLength)
        self.sm.active(1)

    def stop(self):
        self.sm.active(0)
        self.outputPin.value(0)

if __name__ == "__main__":
    from machine import Pin
    from cyclePulse import cyclePulse
    pulse = cyclePulse(Pin(13,Pin.OUT))

    while True:
        for i in range(1,1000):
            pulse.set(i)
            utime.sleep_ms(500)

Code: Select all

from machine import Pin
from cyclePulse import cyclePulse
pulse=cyclePulse(Pin(13,Pin.OUT))
pulse.set(1000)

danjperron
Posts: 20
Joined: Thu Dec 27, 2018 11:38 pm
Location: Québec, Canada

Re: PIO: where am I going wrong?

Post by danjperron » Tue Feb 23, 2021 7:03 pm

A small note on sm.get()

if you have no pulse, sm.get() never returns! This is why on my class I check if the fifo is empty. If yes then I check against a timeout.

danjperron
Posts: 20
Joined: Thu Dec 27, 2018 11:38 pm
Location: Québec, Canada

Re: PIO: where am I going wrong?

Post by danjperron » Tue Feb 23, 2021 7:09 pm

And yes Timer is not very precise

this is the result with your code
[
62472 62761 61967
62548 62761 61967
62371 62761 61967
62498 62761 61967
62610 62761 61967
62547 62761 61967]

Better code the timer clock with the PIO if you need precision.

User avatar
marfis
Posts: 206
Joined: Fri Oct 31, 2014 10:29 am
Location: Zurich / Switzerland

Re: PIO: where am I going wrong?

Post by marfis » Tue Feb 23, 2021 8:47 pm

I could reproduce this and it seems indeed the square wave that gets distorted at the time when the PIO starts the measurement.

For debugging I added a sidestep that toggles a pin while the measurement was active:

Code: Select all

    label('low_high')
    jmp(x_dec, 'next')  .side(0) 
    label('next')
    jmp(pin, 'low_high')  .side(1) # while pin is high
..
sm0 = rp2.StateMachine(0, mark, freq=1000000, in_base=pin16, jmp_pin=pin16, sideset_base=Pin(2))
    

On the saleae it showed the following: (see attachment, top is the pin toggling of the sidestep). You can see that during the meassurement time, the pulse is distorted, resulting in the wrong value. But the actual measurement is perfect (toggling of pulses start at the rising edge of the input and end at the falling edge).

You can solve your issue by using these lines for the waveform generator (that uses the HW for PWM):

Code: Select all

pwm = PWM(Pin(17))
pwm.freq(1000)
pwm.duty_u16(0xffff // 2)
which resulted on my pico in a perfect timing measurment:

Code: Select all

31250 31250 31250
31250 31250 31250
31250 31250 31250
31250 31250 31250
31250 31250 31250
31250 31250 31250
31250 31250 31250
31250 31250 31250
So the issue is likely that the Timer's Irq callback is delayed when the foreground is active.
Attachments
2021-02-23_21-37-55.png
2021-02-23_21-37-55.png (7.39 KiB) Viewed 732 times

User avatar
pythoncoder
Posts: 5140
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Firmware bug

Post by pythoncoder » Wed Feb 24, 2021 6:14 am

Thank you :!: All my scripts now work with superb accuracy.

Apologies to @cebersp who first suggested using PWM - I was relying on an oscilloscope view of the square wave which missed the rare corruption. As a PIO n00b I assumed I must be doing something stupid...
marfis wrote:
Tue Feb 23, 2021 8:47 pm
I could reproduce this and it seems indeed the square wave that gets distorted at the time when the PIO starts the measurement...
So there is a firmware bug which should be reported. Do you want to do this as you have gathered the definitive evidence?

[EDIT]
I tried this script which produced some odd results:

Code: Select all

from machine import Pin, Timer
from time import sleep_ms
import math

pin17 = Pin(17, Pin.OUT)
tim = Timer(freq=1000, mode=Timer.PERIODIC, callback=lambda _: pin17(not pin17()))
pin18 = Pin(18, Pin.OUT)

while True:
    pin18(1)  # Trigger
    pin18(0)
    y = 0
    for x in range(100):
        y += math.sin(x * 2 * math.pi/100)  # 6.2ms of busywork
    sleep_ms(100)
Two issues: firstly it doesn't run for more than a few minutes before the waveforms stop. It's not a complete crash: ctrl-c gets the REPL back. I found my original scripts also stopped after a while.
Secondly the timer pulse durations vary, occasionally quite radically. But it can take quite a few single shot readings to spot this. Maybe it's only certain types of busy work which affect the timer?
Peter Hinch

User avatar
pythoncoder
Posts: 5140
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

PIO blocking

Post by pythoncoder » Wed Feb 24, 2021 7:14 am

danjperron wrote:
Tue Feb 23, 2021 7:03 pm
A small note on sm.get()

if you have no pulse, sm.get() never returns! This is why on my class I check if the fifo is empty. If yes then I check against a timeout.
Good point.

This strikes me as a flaw in the Python API. There should be methods for checking get fifo empty or put fifo full. This would enable nonblocking code to be written without register hacking. Do you want to raise an issue or shall I?
[EDIT]
I think the IRQ feature could be used to achieve this. For example if the SM sends an IRQ each time it takes something out of the TX FIFO, the application could determine how many words were in the FIFO. A similar strategy on the receiver would avoid a blocking read on an empty FIFO.

One thing I'm unclear about is DMA. When you write to the TX, are you writing to a 4-word FIFO or a DMA buffer that feeds it?
Peter Hinch

Post Reply