PIO program for probing quadrature encoders

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
Post Reply
rkompass
Posts: 66
Joined: Fri Sep 17, 2021 8:25 pm

PIO program for probing quadrature encoders

Post by rkompass » Sun Jun 19, 2022 9:38 pm

Hello,

I present a quadrature code generator that is intended for probing different quadrature encoder approaches.
Therefore it generates the quadrature code with options to simulate contract bouncing.
It is written in RPi2040 Pio code for maximum autonomy, self-starting after a specified delay.
Hopefully this will allow us to investigate how different (python, native, viper or assembler) - possibly interrupt driven- encoders
cope with quadrature signals including contact bounce.

Code: Select all

#
#    Quadrature code generator using PIO of RPi2040
#
#    Created 2022 by Raul Kompass.
#
#    License: The MIT License (MIT)
#
#
# --------------------------------------------------------
#
#  Example of usage:
#  -----------------
#
# pinA = Pin(8, Pin.OUT)                            #  Pins for generation of the quadrature signal, have to be adjacent pins.
# pinB = Pin(9, Pin.OUT)
# 
# qgen = QGen_Pio((pinA, pinB), 250, freq=60_000)   #  Pair of pins as argument, number of output-(00-01-11-10-)-cycles, frequeny of internal generation
# 
# # configure a Quadrature Encoder now
# # pinX = Pin(6, Pin.IN, Pin.PULL_UP)
# # pinY = Pin(7, Pin.IN, Pin.PULL_UP)
# # qenc = QEnc_Int2_4b((pinX, pinY))      # Interrupt driven Quadrature Encoder, 4 counts per cycle
# 
# qgen.start()                                      # Now generation of quadrature sequence starts
# 
# for i in range(20):
#     print('x:', qenc.read(), qgen.running())      # The generator runs autonomously, but we may query if it still runs
#     sleep_ms(100)
#
#
# In this default configuration an output-(00-01-11-10-)-cycle takes 8*(10+10+10+10) + 14 = 334 cycles of PIO freqency.
# In the above example an output-cycle will take 334 / 60000 s = 5.6 ms. The 250 cycles thus take 1.4 seconds.
# 
# -------------------------------
#
# There are options to simulate contact bouncing:
#   - The 00-01 output pin transition may be preceded by a number bXon of (01-00-) cycles of (1-7-) PIO clock periods.
#   - The 01-11 output pin transition may be preceded by a number bYon of (11-01-) cycles of (1-7-) PIO clock periods.
#   - The 11-10 output pin transition may be preceded by a number bXoff of (10-11-) cycles of (6-2-) PIO clock periods.
#   - The 10-00 output pin transition may be preceded by a number bYoff of (00-10-) cycles of (6-2-) PIO clock periods.
#
# You may configure this in the QGen_Pio initializer or with the .config() function by setting inner-cycle-counts
#     stable=(sX, sXY, sY, s0),  bounce=(bXon, bYon, bXoff, bYoff).
# Stable inner-cycle-counts have to be in the range 1..16, bounce- inner-cycle-counts in the range 0..15.
#
# E.g.:   qgen.config(250, stable=(5,5,5,5), bounce=(5,5,5,5))
#
# Each stable (01, 11, 10 or 00) output lasts 3+8*inner-cycle-count PIO clock periods. 
# Each bouncing (01-00-, 11-01-, 10-11-, or 00-10-) subsequence has a duration of 8*inner-cycle-count PIO clock periods. 
# 
# In the above config example the overall quadrature cycle duration is the same, because instead of
#    8*(10+10+10+10) + 14 we have 8*(5+5+5+5+5+5+5+5) + 14 PIO clock cycles. ( 8*(bXon+sX+bYon+sXY+bXoff+sY+bYoff+s0)+14 ).
#
# A start-delay before the generation of the quadrature signal of st_del*8 PIO clock cycles may be configured. Default: st_del=200.
# Of course the PIO state machine may be configured in the initializer. Default: sm_id=4, thus taking the second block of PIO state machines.

from rp2 import PIO, StateMachine, asm_pio

# --------------  Quadrature code generator class, using RPI2040 PIO  ----------------------
class QGen_Pio:
    def __init__(self, pins, cycles, stable=(10,10,10,10), bounce=(0,0,0,0), st_del=200, sm_id=4, freq=10_000_000):
        if not isinstance(pins, (tuple, list)) or len(pins) != 2:
            raise ValueError('Pair of 2 successive pins required')
        pinA = int(str(pins[0]).split(')')[0].split('(')[1].split(',')[0]) # Extract pin number from pin string representation.
        pinB = int(str(pins[1]).split(')')[0].split('(')[1].split(',')[0]) #        -   "   -
        if abs(pinA-pinB) != 1:
            raise ValueError('Pair of 2 successive pins required')
        pin_base = pins[0] if pinA < pinB else pins[1]
        self.qgen = StateMachine(sm_id, self.sm_qgen, freq=freq, sideset_base=pin_base)
        self.cycles = cycles     # again set in self.config()
        self.stable = stable     # checked in self.config()
        self.bounce = bounce     # checked in self.config()
        self.st_del = st_del     # again set in self.config()
        self.config(cycles, stable, bounce, st_del)

    # By specifying a pair of values for sideset_init we reserve 2 of the 5 delay/side-set bits for sideset.
    # We have to take care that every instruction has a .side() instruction added, otherwise another bit is reserved for enabling the side-set
    #    thus reducing the maximum delay to 3 instead of 7.
    @asm_pio(sideset_init=(PIO.OUT_LOW, PIO.OUT_LOW), in_shiftdir=PIO.SHIFT_LEFT, out_shiftdir=PIO.SHIFT_RIGHT)
    def sm_qgen():
        nop()                  .side(0b00)        #    so that we can start by setting PC to 0.
        nop()                  .side(0b00)        #
        nop()                  .side(0b00)        # Reason: The instruction memory is filled top-down when the state machine is loaded.
        nop()                  .side(0b00)        #
        label("st_d")
        jmp(y_dec, "st_d")     .side(0b00)  [7]
        # ---------------------
        label("loop")
        mov(osr, isr)          .side(0b00)        # The count variable for the inner loops in ISR is moved to OSR to be consumed by out()'s.
        out(y, 4)              .side(0b00)        # 4 bit count bXon
        label("bXon")
        jmp(y_dec, "bXon_st")  .side(0b01)  [0]
        jmp("bXon_fi")         .side(0b01)
        label("bXon_st")
        jmp("bXon")            .side(0b00)  [6]
        label("bXon_fi")
        out(y, 4)              .side(0b01)        # 4 bit count sX
        label("sX")
        jmp(y_dec, "sX")       .side(0b01)  [7]
        out(y, 4)              .side(0b01)        # 4 bit count bYon
        label("bYon")
        jmp(y_dec, "bYon_st")  .side(0b11)  [0]
        jmp("bYon_fi")         .side(0b11)
        label("bYon_st")
        jmp("bYon")            .side(0b01)  [6]
        label("bYon_fi")
        out(y, 4)              .side(0b11)        # 4 bit count sXY
        label("sXY")
        jmp(y_dec, "sXY")      .side(0b11)  [7]
        out(y, 4)              .side(0b11)        # 4 bit count bXoff
        label("bXoff")
        jmp(y_dec, "bXoff_st") .side(0b10)  [5]
        jmp("bXoff_fi")        .side(0b10)
        label("bXoff_st")
        jmp("bXoff")           .side(0b11)  [1]
        label("bXoff_fi")
        out(y, 4)              .side(0b10)        # 4 bit count sY
        label("sY")
        jmp(y_dec, "sY")       .side(0b10)  [7]
        out(y, 4)              .side(0b10)        # 4 bit count bYoff
        label("bYoff")
        jmp(y_dec, "bYoff_st") .side(0b00)  [5]
        jmp("bYoff_fi")        .side(0b00)
        label("bYoff_st")
        jmp("bYoff")           .side(0b10)  [1]
        label("bYoff_fi")
        out(y, 4)              .side(0b00)        # 4 bit count s0
        label("s0")
        jmp(y_dec, "s0")       .side(0b00)  [7]
        jmp(x_dec, "loop")     .side(0b00)
        push(noblock)          .side(0b00)

    # This function prepares QGen_Pio.start() by checking and setting arguments for start()
    # Inner cycle counts are in:  stable=(sX, sXY, sY, s0),  bounce=(bXon, bYon, bXoff, bYoff).                                
    def config(self, cycles, stable=None, bounce=None, st_del=None):
        if cycles < 1:
            raise ValueError('cycles >= 1 required')
        self.cycles = cycles
        if stable is None:
            stable = self.stable
        if not isinstance(stable, (tuple, list)) or len(stable) != 4:
            raise ValueError('stable: 4 element - list/tuple required')
        if any(i<1 or i>16 for i in stable):
            raise ValueError('all n in stable require 0 < n <= 16')
        if bounce is None:
            bounce = self.bounce
        if not isinstance(bounce, (tuple, list)) or len(bounce) != 4:
            raise ValueError('bounce: 4 element - list/tuple required')
        if any(i<0 or i>15 for i in bounce):
            raise ValueError('all n in bounce require 0 <= n < 16')
        if st_del is not None:
            self.st_del = st_del
        self._cnts = 0
        _stb = list(stable)
        for n in range(4): # Decrement stable counts by 1, because 1 cycle is always passed in loop.
            _stb[n] -= 1
        for n in reversed(sum(zip(bounce, _stb),())):
            self._cnts = self._cnts << 4 | n & 0b1111 # bounce[0] is last shifted into count to left, first read out to right

    def start(self):
        # We initialize the PIO registers here, so there are more instructions available for the loop
        self.qgen.active(0)             # stop anywhere
        self.qgen.restart()             # resets, but does not start yet :-)
        for _ in range(self.qgen.rx_fifo()):  # we have to clear the output FIFO ourselves
            self.qgen.get()
        self.qgen.put(self.st_del)      # write startdelay into FIFO
        self.qgen.exec("pull(block)")
        self.qgen.exec("mov(y, osr)")   # Y = startdelay
        self.qgen.put(self.cycles-1)    # write cycles-1 into FIFO
        self.qgen.exec("pull(block)")
        self.qgen.exec("mov(x, osr)")   # X = cycles-1
        self.qgen.put(self._cnts)       # write counts (s0, by_off, sy, bx_off, sxy, by_on, sx, bx_on) into FIFO
        self.qgen.exec("pull(block)")
        self.qgen.exec("mov(isr, osr)") # ISR = inner cycle counts
        self.qgen.active(1)             # start PIO

    def stop(self):
        self.qgen.active(0)             # stop PIO

    def running(self):                  # query if generator is still running
        return not self.qgen.rx_fifo() 
Attached is an illustration of the generated signal with timing calculations in PIO-clock-cycles and program parameters.

Greetings,

Raul
Attachments
QGenIllu2.zip
(74.62 KiB) Downloaded 106 times

Post Reply