Improve wheel encoder accuracy without replacing hardware?

Continuing the discussion from GoPi5Go-Dave has "Flopsy Wheel Syndrome":

Thinking about Dave’s odometery issues, I’m wondering:

  • Can we improve the resolution of the wheel encoders by reading the encoders directly?

That then begs the question:

  • Can we read the encoder output directly without circuit modifications?

Assuming the hall sensors are switching hall sensors instead of linear hall sensors, each sensor should return a square wave - a rising edge when approaching the magnetic pole and a falling edge when leaving the magnetic pole - the frequency of which is proportional to the velocity of the robot and the phase relationship of the two square waves is dependent on the direction the motor, (wheel) is turning, either forward or backward.

Since we can assume the controller can tell the difference between “forward” and “backward”, we can ignore the phase relationship and concentrate on the square waves themselves.

Note that the (approximate) angular difference is about 165°[1], which translates to a lead/lag of one sensor to the other of about 15°.  (sin 165° = 0.26.  arcsin 0.26 = 15°.)

Another assumption:
The 165°(±) offset, (which is identical in relative phase to a 15° offset), was chosen so that the two hall sensors aren’t too close to each other.

What this means is that within about a 30 to 45 degree angle there are four edges - two leading edges and two trailing edges - and because of the actual, physical, distance between the sensors, they don’t interact and might not even overlap.  (This is an oscilloscope experiment waiting to happen.)

Question:
Can we use the “raw” encoder signals, (assuming we can get them), to improve accuracy?

==================== Footnotes ====================

  1. Note that this measurement was an “eyeball” measurement - it’s close to 180°, but not exactly 180° - it could be as close as 175° or so - which would make the second assumption about spacing between the hall sensors even more likely.
1 Like

Yes or not much. For a 16-tick GoPiGo3 the raw encoder counts are 5.33 times per degree of wheel rotation, and for a 6-tick GoPiGo3 the raw encoder counts are 2 times per degree of wheel.

The raw encoder counts are obtainable using the GoPiGo3 class method spi_read_32:

l = gpg.spi_read_32(gpg.SPI_MESSAGE_TYPE.GET_MOTOR_ENCODER_LEFT)
r = gpg.spi_read_32(gpg.SPI_MESSAGE_TYPE.GET_MOTOR_ENCODER_RIGHT)

(and adjusting for values in the negative range.)

def get_raw_LR_encoders(gpg):

    """
    Read left and right raw encoder values (in ticks)
    For 16-tick GoPiGo3: 1920 ticks per 360 degree wheel revolution
    (For 6-tick GoPiGo3:  720 ticks per 360 degree wheel rev)
    """

    l = gpg.spi_read_32(gpg.SPI_MESSAGE_TYPE.GET_MOTOR_ENCODER_LEFT)
    if DEBUG:
        print("left value read: {}".format(l))
    if l & 0x80000000:
        l = int(l - 0x100000000)

    r = gpg.spi_read_32(gpg.SPI_MESSAGE_TYPE.GET_MOTOR_ENCODER_RIGHT)
    if DEBUG:
        print("right value read: {}".format(r))

    if r & 0x80000000:
        r = int(r - 0x100000000)

    return l,r

The GoPiGo3 API “cleans up” the round-off issue of half tick uncertainty for the 6-tick GoPiGo3s by requiring two ticks before reporting a “one degree encoder tick”. Indeed the 16-tick GoPiGo3s can do better BUT I think we end up with less variability around a poor estimate, rather than a more accurate estimate.

Here is a version of my raw_encoders.py file for reporting the difference between raw encoder readings and the GoPiGo3 API for:

  • Drive forward 1m
  • Turn clockwise 180 degrees
  • Turn counter-clockwise 180 degrees
  • Drive backward 1m

with 60 seconds between the actions to allow measuring and resetting to “straight”:

raw_encoders.py

#!/bin/env python3

from easygopigo3 import EasyGoPiGo3

import time
import math

DEBUG = True
rad_to_deg = 180.0/math.pi

def get_raw_LR_encoders(gpg):

    """
    Read left and right raw encoder values (in ticks)
    For 16-tick GoPiGo3: 1920 ticks per 360 degree wheel revolution
    (For 6-tick GoPiGo3:  720 ticks per 360 degree wheel rev)
    """

    l = gpg.spi_read_32(gpg.SPI_MESSAGE_TYPE.GET_MOTOR_ENCODER_LEFT)
    if DEBUG:
        print("left value read: {}".format(l))
    if l & 0x80000000:
        l = int(l - 0x100000000)

    r = gpg.spi_read_32(gpg.SPI_MESSAGE_TYPE.GET_MOTOR_ENCODER_RIGHT)
    if DEBUG:
        print("right value read: {}".format(r))

    if r & 0x80000000:
        r = int(r - 0x100000000)

    return l,r

def get_LR_encoders_in_degrees(gpg):
    """
    Read left and right gopigo3 API encoder values (in degrees)
    """
    l = gpg.get_motor_encoder(gpg.MOTOR_LEFT)
    r = gpg.get_motor_encoder(gpg.MOTOR_RIGHT)
    return l,r

def dHeading_API_in_degrees(end_l, end_r, start_l, start_r, gpg):

    dl = (end_l - start_l) / 360 * gpg.WHEEL_CIRCUMFERENCE
    dr = (end_r - start_r) / 360 * gpg.WHEEL_CIRCUMFERENCE
    dHeading_rad = (dr-dl) / gpg.WHEEL_BASE_WIDTH
    dHeading_deg = dHeading_rad * 180.0 / math.pi 
    return dHeading_deg

def dHeading_raw_in_radians(end_l, end_r, start_l, start_r, gpg):

    dl = (end_l - start_l) / gpg.ENCODER_TICKS_PER_ROTATION * gpg.MOTOR_GEAR_RATIO * gpg.WHEEL_CIRCUMFERENCE
    dr = (end_r - start_r) / gpg.ENCODER_TICKS_PER_ROTATION * gpg.MOTOR_GEAR_RATIO * gpg.WHEEL_CIRCUMFERENCE
    dHeading_rad = (dr-dl) / gpg.WHEEL_BASE_WIDTH
    return dHeading_rad

def  main():
    egpg = EasyGoPiGo3(use_mutex=True)
    egpg.set_speed(150)

    test_distance_cm = 100  # 1m
    test_turn_deg = 180

    print("Reset Encoders")
    egpg.reset_encoders()
    raw_heading = 0
    api_heading = 0

    start_raw_lenc, start_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(start_raw_lenc, start_raw_renc))
    start_deg_lenc, start_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(start_deg_lenc, start_deg_renc))
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))


    print("\nDrive {}cm".format(test_distance_cm))
    egpg.drive_cm(test_distance_cm)

    end_raw_lenc, end_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(end_raw_lenc, end_raw_renc))
    end_deg_lenc, end_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(end_deg_lenc, end_deg_renc))
    raw_heading = dHeading_raw_in_radians(end_raw_lenc, end_raw_renc, start_raw_lenc, start_raw_renc, egpg)
    api_heading = dHeading_API_in_degrees(end_deg_lenc, end_deg_renc, start_deg_lenc, start_deg_renc, egpg)
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))

    print("\nSleeping 60s ...")
    time.sleep(60)


    print("\nReset Encoders")
    egpg.reset_encoders()
    raw_heading = 0
    api_heading = 0

    start_raw_lenc, start_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(start_raw_lenc, start_raw_renc))
    start_deg_lenc, start_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(start_deg_lenc, start_deg_renc))
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))


    print("\nTurn {} deg".format(test_turn_deg))
    egpg.turn_degrees(test_turn_deg)

    end_raw_lenc, end_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(end_raw_lenc, end_raw_renc))
    end_deg_lenc, end_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(end_deg_lenc, end_deg_renc))
    raw_heading = dHeading_raw_in_radians(end_raw_lenc, end_raw_renc, start_raw_lenc, start_raw_renc, egpg)
    api_heading = dHeading_API_in_degrees(end_deg_lenc, end_deg_renc, start_deg_lenc, start_deg_renc, egpg)
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))


    print("\nSleeping 60s ...")
    time.sleep(60)


    print("\nReset Encoders")
    egpg.reset_encoders()
    raw_heading = 0
    api_heading = 0

    start_raw_lenc, start_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(start_raw_lenc, start_raw_renc))
    start_deg_lenc, start_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(start_deg_lenc, start_deg_renc))
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))


    print("\nTurn back {} deg".format(-test_turn_deg))
    egpg.turn_degrees(-test_turn_deg)

    end_raw_lenc, end_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(end_raw_lenc, end_raw_renc))
    end_deg_lenc, end_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(end_deg_lenc, end_deg_renc))
    raw_heading = dHeading_raw_in_radians(end_raw_lenc, end_raw_renc, start_raw_lenc, start_raw_renc, egpg)
    api_heading = dHeading_API_in_degrees(end_deg_lenc, end_deg_renc, start_deg_lenc, start_deg_renc, egpg)
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))


    print("\nSleeping 60s ...")
    time.sleep(60)


    print("\nReset Encoders")
    egpg.reset_encoders()
    raw_heading = 0
    api_heading = 0

    start_raw_lenc, start_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(start_raw_lenc, start_raw_renc))
    start_deg_lenc, start_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(start_deg_lenc, start_deg_renc))
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))


    print("\nDrive backward {}cm".format(-test_distance_cm))
    egpg.drive_cm(-test_distance_cm)

    end_raw_lenc, end_raw_renc = get_raw_LR_encoders(egpg)
    print("Raw Encoders (l,r) ({},{})".format(end_raw_lenc, end_raw_renc))
    end_deg_lenc, end_deg_renc = get_LR_encoders_in_degrees(egpg)
    print("API Encoders (l,r) ({},{} degrees)".format(end_deg_lenc, end_deg_renc))
    raw_heading = dHeading_raw_in_radians(end_raw_lenc, end_raw_renc, start_raw_lenc, start_raw_renc, egpg)
    api_heading = dHeading_API_in_degrees(end_deg_lenc, end_deg_renc, start_deg_lenc, start_deg_renc, egpg)
    print("raw_heading {:.2f} rad {:.1f} deg  api_heading {:.1f} deg".format(raw_heading, raw_heading*rad_to_deg, api_heading))




if __name__ =='__main__':
    main()

It would be interesting to hear your bots’ results.

Carl drove “straight 1m” to within 2mm off to the right , and turned just short of 180, back exactly to his original heading, then returned to within 3mm off of straight back to his original position and heading - That is like super impressive, no?

pi@Carl:~/Carl/systests/encoders $ ./raw_encoders.py 
Reset Encoders
left value read: 0
right value read: 0
Raw Encoders (l,r) (0,0)
API Encoders (l,r) (0,0 degrees)
raw_heading 0.00 rad 0.0 deg  api_heading 0.0 deg

Drive 100cm
left value read: 3561
right value read: 3557
Raw Encoders (l,r) (3561,3557)
API Encoders (l,r) (1781,1778 degrees)
raw_heading -0.00 rad -0.2 deg  api_heading -0.8 deg

Sleeping 60s ...

Reset Encoders
left value read: 0
right value read: 0
Raw Encoders (l,r) (0,0)
API Encoders (l,r) (0,0 degrees)
raw_heading 0.00 rad 0.0 deg  api_heading 0.0 deg

Turn 180 deg
left value read: 645
right value read: 4294966655
Raw Encoders (l,r) (645,-641)
API Encoders (l,r) (322,-320 degrees)
raw_heading -1.17 rad -67.2 deg  api_heading -178.8 deg

Sleeping 60s ...

Reset Encoders
left value read: 1
right value read: 4294967295
Raw Encoders (l,r) (1,-1)
API Encoders (l,r) (0,0 degrees)
raw_heading 0.00 rad 0.0 deg  api_heading 0.0 deg

Turn back -180 deg
left value read: 4294966651
right value read: 640
Raw Encoders (l,r) (-645,640)
API Encoders (l,r) (-322,320 degrees)
raw_heading 1.17 rad 67.2 deg  api_heading 178.8 deg

Sleeping 60s ...

Reset Encoders
left value read: 4294967295
right value read: 0
Raw Encoders (l,r) (-1,0)
API Encoders (l,r) (0,0 degrees)
raw_heading 0.00 rad 0.0 deg  api_heading 0.0 deg

Drive backward -100cm
left value read: 4294963735
right value read: 4294963739
Raw Encoders (l,r) (-3561,-3557)
API Encoders (l,r) (-1780,-1778 degrees)
raw_heading 0.00 rad 0.2 deg  api_heading 0.6 deg
pi@Carl:~/Carl/systests/encoders $ 

(There’s was math error on the raw encoder heading values - I hardcoded 1920 ticks per rev instead of reading from the GoPiGo3 config, so wrong for Carl. I fixed it in the code above.)

Dave drove “1m straight” and ended up 2cm to the right and headed 2-3 degrees off clockwise, while his encoders claimed a heading of 0.0 deg using the raw readings, and -0.3 deg using the GoPiGo3 API.

Drive 100cm
left value read: 9254
right value read: 9253
Raw Encoders (l,r) (9254,9253)
API Encoders (l,r) (1735,1734 degrees)
raw_heading 0.00 rad 0.0 deg  api_heading -0.3 deg

2 Likes

Absolutely!

If you can get good wheels for Dave, he should enjoy similar accuracy.

1 Like

New wheels arrived today - Dave drives straight again - forward that is.

Backward is a still a problem, but he only goes backward 17cm at a time.

Found Dave shutdown at 6am this morning - docking failure backing onto his dock. Troubles, Troubles - will Dave ever have a peaceful life?

2 Likes

Bummer. Hope you get it rectified. Happy holidays. /K

2 Likes

Thank you for the holiday wish.

As to getting Dave to drive straight, I am really asking too much of the cantilevered plastic axle with no bearings, especially with all the extra weight I have built above the stock GoPiGo3 platform.

Hopefully new motors will be enough.

Wishing you and your family peace and joy as we head to a new year,

Alan

2 Likes

Yep, I forgot that Dave needs to go on a diet.  Almost as much as I do! :man_facepalming:  Short of migrating to an entirely different chassis and motors, I don’t know what to do about it.
 

Likewise to both yourself, @KeithW, @cleoqc and everyone else who has contributed to our obsession - insanity - hobby.

2 Likes

If:

  • someone smart with electronics could find
    • a commonly available, GoPiGo3 redboard compatible, metal shaft motor with encoder, and
    • narrow, grippy, wheels with metal hub/metal set screw

and

  • someone smart with 3D printing or laser cutting could make two 10" round plates with
    • mounting holes for
      • said compatible motor/encoder
      • a Raspberry Pi5 w/cooler/GoPiGo3 redboard stack,
      • YDLidar X4 mounting holes
      • Pololu 1" Plastic Ball Castor mounting holes
    • and print a 180 degree wrap around bumper for the front

then I could

  • build some “extended length motor cables”, and
  • publish a “Jazzy GoPiGo5” sdcard image
    • with wheel_base, wheel_diameter, motor_gear_ratio, and encoder_ticks values
      to put in the gpg3_config.json file

We would be able to create the new “GoPiGo5 Robot Extender Kit”

  • with the latest supported Raspberry Pi 4 and 5 compatible OS
  • compatible with Raspberry Pi 4 or 5
  • Optional ROS 2 Jazzy GoPiGo5 node,
    • with introduction to ROS 2 tutorials
    • with LIDAR wallfollowing and wander examples
    • with preconfigured mapping and navigation examples

And I would be able to create:

“GoPiGo5-WaLI” - GoPiGo5 Wallfollower Looking For Intelligence!

Just a little dream of mine

1 Like