Rotary Encoder for RP2040 board in Arduino-Pico Core

Hello!
I’ve been trying to get an EC11 rotary encoder to work with a PCB that I designed which runs an RP2040. I was using micropython initially since I wasn’t that experienced with arm programming and it just seemed like the easiest route to get up and running asap, but after a few months I noticed a serious performance bottleneck and so set out into the world of the PicoSDK (I landed on the Earle Philhower core, but I miss micropy :frowning: ).

I’ve been trying to add a feature where a user can set the duration for a 24v motor to run through a basic GUI on an OLED and a rotary encoder. Once said duration is reached, a GPIO pin from the RP2040 pulls the control pin of a boost converter IC low to disable power output. My issue is that I can’t for the life of me get the board to read the state of a rotary encoder. I’ve tried a handful of libraries and examples from here but nothing seems to return any life at all outside of the encoder button being pressed.

Was wondering if anyone had any pointers or ideas?
Sorry this isn’t about a specific product, I wanted to ask since I’ve seen a lot of Pi and encoder guides from here :stuck_out_tongue:

1 Like

Hi Finn
There is quite an array of solutions for decoding.
You will have to consider switch bounce. I did some experiments along these lines using a simple Arduino sketch which you would have to adapt to RPi (Python ???)
Results here

Encoder Module with button (CE09436)

Also here

Rotary Encoder de-bounce with Nand gates

This might help.
Sorry not familiar with RPi or the several variations of Python.
Cheers Bob

1 Like

Hi There,

As it happens, just at the moment, I’m working on a project that required decoding of a pair of quadrature motor encoders.

It’s written in micropython for an RP2040 Pico like board. It counts transitions using a PIO based state machine, thus using no CPU cycles, and then uses a timer based interrupt to read accumulated counts, aka velocity for me. The Maxon quadrature optical encoder I’m using delivers 100 counts per turn and with my 17.83:1 gear reduction I get, 1783 counts per revolution. The encoders output 5V so I use a basic 4k7 / 10k divider. It took me a while to fine tune but I have found this code extremely reliable, as long as you’re not generating too many other interrupts (for me no more than an average of 20 additional per 100ms). Also a regular gc.collect() is always in my main loop.

I sample at 50ms which is more that sufficient to code up a decent PID (proportional–integral–derivative) motor controller. The global array, dataV, contains the values of the last taken velocity sample, which is never more than 50ms old.

Hope this is of value.
mark

################################################################################
# Roverling Mk II - Quadrature Decoder
# Count transitions using RP2040 PIO based state machine, no CPU cycles and then
# use timer based interrupt to read accumulated counts, aka velocity.
# Motor quadrature optical encoder: 100 CPT, 17.83:1, 1783 counts per revolution

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

REncApin = Pin(12, Pin.IN, Pin.PULL_UP)
REncBpin = Pin(13, Pin.IN, Pin.PULL_UP)  
LEncApin = Pin(10, Pin.IN, Pin.PULL_UP)
LEncBpin = Pin(11, Pin.IN, Pin.PULL_UP)  

# Initis
PrevREnc = 0
PrevLEnc = 0

dataV = array('f', [0]*2)

# Parameters
EncoderSamplePeriod = 50      
Counts2Vel = 200

@asm_pio()
def encoder():       
    # adapted from https://github.com/adamgreen/QuadratureDecoder
    # In C/C++ can define base, which needs to be 0 for jump table to work
    # in python not working, but padding out to exactly 32 instructions fixes it
    # 16 element jump table based on 4-bit encoder last state and current state.

    jmp('delta0')     # 00-00
    jmp('delta0')     # 00-01
    jmp('delta0')     # 00-10
    jmp('delta0')     # 00-11

    jmp('plus1')      # 01-00
    jmp('delta0')     # 01-01
    jmp('delta0')     # 01-10
    jmp('minus1')     # 01-11

    jmp('minus1')     # 10-00
    jmp('delta0')     # 10-01
    jmp('delta0')     # 10-10
    jmp('plus1')      # 10-11

    jmp('delta0')     # 11-00
    jmp('delta0')     # 11-01
    jmp('delta0')     # 11-10
    jmp('delta0')     # 11-11
                        
    label('delta0')     # Program actually starts here.
    mov(isr, null)      # Make sure that the input shift register is cleared when table jumps to delta0.
    in_(y, 2)           # Upper 2-bits of address are formed from previous encoder pin readings Y -> ISR[3,2]
    mov(y, pins)        # Lower 2-bits of address are formed from current encoder pin readings. PINS -> Y
    in_(y, 2)           # Y -> ISR[1,0]
    mov(pc, isr)        # Jump into jump table which will then jump to delta0, minus1, or plus1 labels.

    label('minus1')
    jmp(x_dec,'output') # Decrement x
    jmp('output')

    label('plus1')
    mov(x, invert(x))   # Increment x by calculating x=~(~x - 1)
    jmp(x_dec,'next2')
    label('next2')
    mov(x, invert(x))

    label('output')
    mov(isr, x)         #Push out updated counter.
    push(noblock)
    jmp('delta0')

    nop()                #need to pad out to exactly 32 instructions
    nop()
    nop()

RightMotorEncoder = StateMachine(0, encoder, freq=1000000, in_base=REncApin)
LeftMotorEncoder = StateMachine(1, encoder, freq=1000000, in_base=LEncApin)

def twos_comp(val, bits):
    if (val & (1 << (bits - 1))) != 0:          # if sign bit is set e.g., 8bit: 128-255
        val = val - (1 << bits)                 # compute negative value
    return val                                  # return positive value as is

def QueryEncoders(): 
    global PrevLEnc, PrevREnc
    global LeftVel, RightVel

    k = 0
    while (LeftMotorEncoder.rx_fifo() > 0) and (k < 4):    # empty FIFO - last value used
        k += 1
        LEE = LeftMotorEncoder.get()
    if k > 0:
        LEE = twos_comp(LEE, 32)
        LE = LEE - PrevLEnc
        PrevLEnc = LEE
    else:
        LE = 0

    k = 0
    while (RightMotorEncoder.rx_fifo() > 0) and (k < 4):  
        k += 1
        REE = RightMotorEncoder.get()
    if k > 0:
        REE = twos_comp(REE, 32)
        RE = REE - PrevREnc
        PrevREnc = REE
    else:
        RE = 0
    
    dataV[0], dataV[1] = LE/ Counts2Vel, RE/Counts2Vel


def cbEncSampleTimer(t):
    QueryEncoders()
    
RightMotorEncoder.active(1)
LeftMotorEncoder.active(1)

EncSampleTimer = Timer(period=EncoderSamplePeriod , mode=Timer.PERIODIC, callback=cbEncSampleTimer)

1 Like

Oh awesome, thank you both for the replies and info!! I’ll mess around with adapting these and post any results I find :smiley:
Thank you both again!

1 Like

Hi Mark
Not familiar with MicroPython but that looks interesting. Your encoder being optical has not got the switch bounce problem I was addressing in the 2 links above. you also probably also have a Schmidt trigger output which would ensure a fast clean unambiguous pulse stream

The EC11 series however is mechanical switch which does have the debounce problem which I attempted to address above both without and with RC filters on the switch outputs. Using filters was OK but I personally would like to clean the output up by using a Schmidt trigger. You might find a read of these posts interesting along with the CRO screenshots.

A lot of this filtering has been done by using delays here and there in the processing system Personally I prefer the hardware approach which enables a system to be broken up into bits like:
Encoder output completely debounced-----driving------Processing-----applied to------end use.
I think then everything can be considered a different entity which can take a lot of the load off problem solving during development.

Diversing from rotary encoders with their single throw switches, if it is only a single button push or single throw switch operation to be debounced a nicer way would be to upgrade the switches to double throw types and use the 2 NAND gate latch system which is pretty fool proof.
Cheers Bob

1 Like

Hi Robert,

Good point re mechanical switches. Some time ago I developed a remote control unit using 8 cheap and nasty mechanical rotary encoders. I don’t have access to a CRO, so I couldn’t determine the typical chatter time of my particular encoders. I used the following code and adjusted the call period to mask the chatter whilst preserving the update speed I need. This was implemented on a ESP32C. Between each channel and ground is a 100n monolithic capacitor - that’s about the extent of the filter in combination with the pullup. The function takes less that 1ms to execute.

# encoder A dn B switches - pull up, switch to gnd
enc_pin = [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]]
enc_pin[0][0] = Pin(16, Pin.IN, Pin.PULL_UP)
enc_pin[0][1] = Pin(17, Pin.IN, Pin.PULL_UP)
enc_pin[1][0] = Pin(22, Pin.IN, Pin.PULL_UP)
enc_pin[1][1] = Pin(23, Pin.IN, Pin.PULL_UP)
enc_pin[2][0] = Pin(21, Pin.IN, Pin.PULL_UP)
enc_pin[2][1] = Pin(19, Pin.IN, Pin.PULL_UP)
enc_pin[3][0] = Pin(5,  Pin.IN, Pin.PULL_UP)
enc_pin[3][1] = Pin(18, Pin.IN, Pin.PULL_UP)
enc_pin[4][0] = Pin(25, Pin.IN, Pin.PULL_UP)
enc_pin[4][1] = Pin(26, Pin.IN, Pin.PULL_UP)
enc_pin[5][0] = Pin(32, Pin.IN, Pin.PULL_UP)
enc_pin[5][1] = Pin(33, Pin.IN, Pin.PULL_UP)
enc_pin[6][0] = Pin(34, Pin.IN)                 # these 4 req. ext. pullups
enc_pin[6][1] = Pin(35, Pin.IN)
enc_pin[7][0] = Pin(36, Pin.IN)
enc_pin[7][1] = Pin(39, Pin.IN)

last_enc_val = [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]]
enc = [0] * 8

....

def CheckEncoders():
    # block execution time: around 500us
    #xx = ticks_us()
    global last_enc_val
    global enc
    global Changed
    for i in range(8):
        a = enc_pin[i][0].value()
        b = enc_pin[i][1].value()
        la = last_enc_val[i][0]
        lb = last_enc_val[i][1]

        if a != la or b != lb:
            if (la,lb) == (1,0):
                if (a,b) == (1,1):
                    enc[i] += StepVal
                    Changed = True
                elif (a,b) == (0,0):
                    enc[i] -= StepVal
                    Changed = True
            if (la,lb) == (0,1):
                if (a,b) == (1,1):
                    enc[i] -= StepVal
                    Changed = True
                elif (a,b) == (0,0):
                    enc[i] += StepVal
                    Changed = True

        last_enc_val[i][0] = a
        last_enc_val[i][1] = b
    #print(ticks_us()-xx)

1 Like

Hi Finn,

Welcome to the forum!!

I’d also checkout the code for the encoder that Bob used in the post: Encoder Module with button | Core Electronics Australia

It uses interupts so the only code you’ll have to change is the pin numbers
Liam