🔣Week 11: Inputs

Project Heros

A graph in thonny printing out the motor shaft speed being calculated from an encoder input, plugged into my Pico motor controller break out board:

Group Assignment

First of all here is our group page. This week we probed some input devices and it was quite fun. I have never directly probed an input device like we did their and seeing the debounce on a scope instead of seeing it as a problem to deal with was really nice. I also hadn't had much experience with force sensitive resistors so it was great to go out and use one and to also see the fluctutations in voltage from the fluctuations in pressure being applied to it, all the tiny little jitters. I wonder what it would look like if a surgeon or someone with fine motor control would squeeze it, would it be a smoother curve?

Measuring Shaft Speed

This week we will be getting a part of my robot going - the encoders. Encoders are a rotational sensor that have 2 digital inputs working together. Mine came attatched to my Polulu motors and as the shaft spins, these encoders will produce digital high and low signals. My encoder produces 1920 clicks per rotation and we can use this to determine the RPM of the shaft. So we can check Encoder pin A and count how many clicks a second to determine rpm. That second encoder signal is used determine direction, when encoder A is set high, we can check encoder b to determine the direction as if its spinning one direction, it will be low, and the other it will be high.

I started by plugging my encoder into my milled Pico breakout board which I made in week 8. The breakout board has made connecting the motors extremely easy with less chances of mixing up wires. When we are going to be dealing with 4 encoders, this is going to help prevent making it easy to switch 2 signal wires and get a reverse direction reading. So I went ahead and plugged in ground, 3.3 volt and the 2 signal wires into my board.

It is getting harder and harder to show connections the more wires that are being run.

With it connected I started writing the script, first we import the required libraries, set up the encoder pins, and declare some helpful variables.

from machine import Pin, Timer
import time

# Rear Right Pins
RRencoderPinA = Pin(0, Pin.IN, Pin.PULL_UP)
RRencoderPinB = Pin(1, Pin.IN, Pin.PULL_UP)

# Encoder variables
RRencoder_position = 0

# Constants
counts_per_revolution = 1920.0
rpm_calculation_period = 0.04

Now we need to set up an interrupt. We cannot periodically check if the state of these 2 encoder pins as they may tick by far quicker than our period or be out of sync (well we technically could but it may be extremely inefficient). Instead we are going to use an external interrupt. This will call a function which will compare the encoder pins, calculate the the direction of movement, and update the encoder position counter.

from machine import Pin, Timer
import time

# Rear Right Pins
RRencoderPinA = Pin(0, Pin.IN, Pin.PULL_UP)
RRencoderPinB = Pin(1, Pin.IN, Pin.PULL_UP)

# Encoder variables
RRencoder_position = 0

# Constants
counts_per_revolution = 1920.0
rpm_calculation_period = 0.04

Then we will create a while true loop which will calculate the rpm of the wheel based on the encoder count and the fixed period it calculates this over. We will then print it so we can see whats going on, and reset the counter. Then sleep by the calculation period, super easy.

while True:
    RRinputRPM = (166.666 * RRencoder_position / counts_per_revolution) / rpm_calculation_period

    print("RR", RRinputRPM)

    # Reset encoder positions for the next calculation period
    RRencoder_position = 0

    time.sleep(rpm_calculation_period)

And if we run this code and turn the shaft, we can see that we are reading its RPM!

Issues I faced

Now this wasn't all smooth sailing and I encountered a lot of issues to get to this solution. My initial solution would of been okay for a single motor in isolation but we are at the point where we need to start looking ahead and integrating this into the larger project.

The first issue that I faced was that it would work... up to about 40 RPM. I would crank the wheels and it would seem perfectly fine and work well at speed of under 40 rpm, but it would saturate at 40.3 RPM no matter how fast I spun it. This was a bit of a difficult one to solve but I figure out that it was in how I was calculating the RPM. This error was because every time the encoder clicked, I would use the ticks_ms function (which retrieves the time since the board was powered on) to calculate how much time has passed since the last click. From this I would calculate the RPM. Although this method is more accurate, it is also incredibly resource intensive and runnin that calculation inside an interrupt half a million times a second AND WITH PYTHON was not going to work. So instead I changed the code to calculate the RPM over a period which worked well and gave good enough results for this robot. If I have time to come back to it, I would write some extremely efficient code to use the Rp2040's PIO to handle the encoders.

Okay problem solved, i tested the shaft upto the maximum 330 RPM just fine. Then I got all the other motors working and they wouldnt measure above 100 RPM. Below is a photo I took of the motor graphs.

It turns out I had another issue with resource limitations and Python. I was trying to handle about a million interrupts a minute - the fix was easy though. With this old version of the code, I was calculating speed based on the both of the encoders click rates. So I only checked one of them for speed and the other for direction like the current code does. This effectively halved my interrupts being handled and should of doubled the maximum RPM that could be read. But I had no issues reading all 4 encoders upto 330 RPM, it should of made 200 the new maximum, and I have no clue why it didn't but I'm very happy with that.

And thats how we got to this code! Above is the code for handling just one encoder input and below is the code I used to handle all 4.

Final Code

from machine import Pin, PWM, Timer, UART
import time


# Rear Left Pins
RLencoderPinA = Pin(2, Pin.IN, Pin.PULL_UP)
RLencoderPinB = Pin(3, Pin.IN, Pin.PULL_UP)

# Rear Right Pins
RRencoderPinA = Pin(0, Pin.IN, Pin.PULL_UP)
RRencoderPinB = Pin(1, Pin.IN, Pin.PULL_UP)

# Front Left Pins
FLencoderPinA = Pin(26, Pin.IN, Pin.PULL_UP)
FLencoderPinB = Pin(22, Pin.IN, Pin.PULL_UP)

# Front Right Pins
FRencoderPinA = Pin(27, Pin.IN, Pin.PULL_UP)
FRencoderPinB = Pin(28, Pin.IN, Pin.PULL_UP)

# Encoder variables
RLencoder_position = 0
RRencoder_position = 0
FLencoder_position = 0
FRencoder_position = 0

# Constants
counts_per_revolution = 1920.0
rpm_calculation_period = 0.04

# Interrupt handler for encoder
def RRencoder(pin):
    global RRencoder_position

    if RRencoderPinA.value() == RRencoderPinB.value():
        RRencoder_position += 1
    else:
        RRencoder_position -= 1

def RLencoder(pin):
    global RLencoder_position

    if RLencoderPinA.value() == RLencoderPinB.value():
        RLencoder_position -= 1
    else:
        RLencoder_position += 1

def FRencoder(pin):
    global FRencoder_position

    if FRencoderPinA.value() == FRencoderPinB.value():
        FRencoder_position += 1
    else:
        FRencoder_position -= 1

def FLencoder(pin):
    global FLencoder_position

    if FLencoderPinA.value() == FLencoderPinB.value():
        FLencoder_position -= 1
    else:
        FLencoder_position += 1

# Attach interrupt to encoder pins
RLencoderPinA.irq(trigger=Pin.IRQ_RISING, handler=RLencoder)
RRencoderPinA.irq(trigger=Pin.IRQ_RISING, handler=RRencoder)
FLencoderPinA.irq(trigger=Pin.IRQ_RISING, handler=FLencoder)
FRencoderPinA.irq(trigger=Pin.IRQ_RISING, handler=FRencoder)


while True:
    RRinputRPM = (166.666 * RRencoder_position / counts_per_revolution) / rpm_calculation_period
    RLinputRPM = (166.666 * RLencoder_position / counts_per_revolution) / rpm_calculation_period 
    FRinputRPM = (166.666 * FRencoder_position / counts_per_revolution) / rpm_calculation_period
    FLinputRPM = (166.666 * FLencoder_position / counts_per_revolution) / rpm_calculation_period

    print("RR", RRinputRPM, "RL", RLinputRPM, "FR", FRinputRPM, "FL", FLinputRPM)

    # Reset encoder positions for the next calculation period
    RRencoder_position = 0
    RLencoder_position = 0
    FRencoder_position = 0
    FLencoder_position = 0

    time.sleep(rpm_calculation_period)