-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Add RPI PIO example for quadrature encoder. #6894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Example that is using StateMachine to offload quadrature encoder pooling.
May you test? |
After fixing two coding errors it works so far. At 125Mhz up to ~3.5KHz signal frequency, at 250MHz up to ~7 kHz. |
reported by robert-hh
@robert-hh fixed, thank you. The stats are interesting. Not exactly expert in this, i am curious if what i did is actually helpful and or could be done more efficiently. |
The example is very helpful, showing what can be done with PIO. The code is really tricky. The speed is limited by the irq latency. It can be marginally (~15%) improved by adding the |
I spent a few moments to pack your encoder into the portable Encoder class of @peterhinch, hoping that class member accesses are faster than global symbol accesses. They are not. It's the opposite. But the code looks more like the other Encoder projects now.
|
I think +2/-2 in state_look_up_table are wrong. See Quadrature decoder state table |
@robert-hh I am not sure if this applies to micropython. But, objects store their attributes in dictionaries, and there is some related cost in creating, maintaining (RAM) and accessing them. My python code is kind of ugly, but I wanted to avoid any overhead. However, in normal python, one can uses
However I cannot test this now on the RPI since I don't have one with me (I am not at home). |
@IhorNehrutsa the Wikipedia example take into consideration only one step changes while this implementation can infer 2 steps given the previous direction was known. Please follow the link from where i was inspired and the related links from within, there are some quite interesting observations in those. |
|
I think that taking the previous direction is a weak assumption. |
@robert-hh I've a new take on this. In my opinion your OO approach is the right way because it allows more than one instance to exist, so constructors take a SM no. and a I have produced two versions, one uses Viper for the ISR, the other does not. You may want to compare the performance with the other solutions. As you probably know, getting a Viper function to retain state is difficult. Consequently this script is not as easy to follow: which to use depends on what these demos are intended to achieve. Non-Viper: from machine import Pin
import rp2
class Encoder:
def __init__(self, sm_no, base_pin, scale=1):
self.scale = scale
self._pos = 0
self._x0 = 0
self.sm = rp2.StateMachine(sm_no, self.pio_quadrature, in_base=base_pin)
self.sm.irq(self.isr)
self.sm.exec("set(y, 99)") # Ensure initial y differs from the input
self.sm.active(1)
def isr(self, sm):
while sm.rx_fifo():
v = sm.get() & 3
x = v & 1
y = v >> 1
s = 1 if (x ^ y) else -1
self._pos += s if (x ^ self._x0) else -s
self._x0 = x
@rp2.asm_pio()
def pio_quadrature(in_init=rp2.PIO.IN_LOW):
wrap_target()
label("again")
in_(pins, 2)
mov(x, isr)
jmp(x_not_y, "push_data")
mov(isr, null)
jmp("again")
label("push_data")
push()
irq(block, rel(0))
mov(y, x)
wrap()
def position(self, value=None):
if value is not None:
self._pos = round(value / self.scale)
return self._pos * self.scale
def value(self, value=None):
if value is not None:
self._pos = value
return self._pos Viper from machine import Pin
from array import array
import rp2
def make_isr(pos): # Closure enables Viper to retain state
old_x = array('i', (0,)) # but nonlocal doesn't work so using arrays
@micropython.viper
def isr(sm):
i = ptr32(pos)
p = ptr32(old_x)
while sm.rx_fifo():
v : int = int(sm.get()) & 3
x : int = v & 1
y : int = v >> 1
s : int = 1 if (x ^ y) else -1
i[0] = i[0] + (s if (x ^ p[0]) else (0 - s))
p[0] = x
return isr
class Encoder:
def __init__(self, sm_no, base_pin, scale=1):
self.scale = scale
self._pos = array("i", (0,)) # [pos]
self.sm = rp2.StateMachine(sm_no, self.pio_quadrature, in_base=base_pin)
self.sm.irq(make_isr(self._pos)) # Instantiate the closure
self.sm.exec("set(y, 99)") # Initialise y: guarantee different to the input
self.sm.active(1)
@rp2.asm_pio()
def pio_quadrature(in_init=rp2.PIO.IN_LOW):
wrap_target()
label("again")
in_(pins, 2)
mov(x, isr)
jmp(x_not_y, "push_data")
mov(isr, null)
jmp("again")
label("push_data")
push()
irq(block, rel(0))
mov(y, x)
wrap()
def position(self, value=None):
if value is not None:
self._pos[0] = round(value / self.scale)
return self._pos[0] * self.scale
def value(self, value=None):
if value is not None:
self._pos[0] = value
return self._pos[0] |
At 125MHz clock and not other load, like in the other tests, the non-viper version works up tp ~6kHz, the viper version up to ~10kHz. P.S.: I like the implementation of your ISR functions. Very clear and concise. |
Just for interest I checked the latency of the RPI, using Pin.IRQ. The latency there is ~22µs until a callback creates a pulse at a Pin. That matches the ~10Khz figure, which is 25µs for a quadrature signal phase. |
… running. Thank you peterhinch
@peterhinch agree OO is cleaner. Many interesting things learned here. Didn't know about Viper. EDIT: I think the pio_quadrature can be expanded to look for 4 encoder state changes at once. |
I assume you're talking about handling two encoders. I think this would complicate - and slow down - the ISR. My solution should support up to 8 encoders (the number of SM's). Of course each encoder uses a SM which could be a problem if you need some for other purposes. That said, your PIO code is very neat. I can't help thinking there must be other uses for firing an interrupt on a state change of any one of N inputs.
It's fast but tricky to use. Support for closures is problematic. |
@robert-hh I have attempted to optimise the Viper closure as follows. The rest of the code is unchanged. You might like to run your timing test on it. def make_isr(pos):
vals = array("i", (1, -1, 0)) # vals[2] is previous x
@micropython.viper
def isr(sm):
i = ptr32(pos) # Position
g = ptr32(vals)
while sm.rx_fifo():
v : int = int(sm.get()) & 3
i[0] += g[int((v >> 1) ^ g[2])]
g[2] = v & 1
return isr It's a bit cryptic, but I can't see a way to optimise it further. |
Hello, perhaps we can let this topic open. Meanwhile I devised a simulation program which generates quadrature codes including some bouncing before the onset of stable impulses, also using PIO.
The generator has many parameters which are best understood by the appended illustration. b_y and b_y may be zero, then there is no bouncing.
This will allow for a systematic testing of many encoders. |
I apologise for this comment, I overlooked a simple error on my part. The below code works just fine. I formatted my strings incorrectly.
This works correctly. Apologies for unnecessary comment. |
Fix with PWM for brightness slowing devices down
Anyone else thought about a count reset pin? |
|
Example that is using StateMachine to offload quadrature encoder pulling.