Electrical – Reading a high-speed rotary encoder with a Raspberry Pi

controlcontrol systemdc motorencoderraspberry pi

I'm trying to setup a Raspberry Pi to measure the position and speed of 8 DC motors that all have incremental quadrature encoders. At full speed (unloaded), each encoder ticks at 3.3 kHz. I assume that this means that I have to sample the encoders at >13.2 kHz to measure the position without missing any ticks. (Note: I only drive one or two motors at a time.)

The current board that I have has a MCP23008 I/O expander on it to sample the encoders, but I think that the I2C communication is too slow. With a simple Python script, I can sample a single encoder at ~1kHz:

from adafruit_mcp230xx.mcp23008 import MCP23008

encoder_address=0x20

# Initialize I2C
i2c = board.I2C()

# Setup encoder reader (I/O expander)
mcp = MCP23008(i2c, address=encoder_address)
encoder1a = mcp.get_pin(6)
encoder1b = mcp.get_pin(7)
encoder1a.direction = digitalio.Direction.INPUT
encoder1b.direction = digitalio.Direction.INPUT
encoder1a.pull = digitalio.Pull.UP
encoder1b.pull = digitalio.Pull.UP

# Read encoders
A = opt.encoder1a.value
B = opt.encoder1b.value

I also tried calling i2c-tools from my Python script, but this was insanely slow (~75Hz):

import subprocess as sb

output = sb.Popen("i2cget -y 1 0x20 0x09", shell=True, stdout=sb.PIPE).stdout.read()

I then tried a simple C++ program, but this only got me up to 1.5 kHz (reading the entire GPIO register):

/* encoder.h */

class Encoder {
    private:
        unsigned int device;  // device address
        int file;             // I2C file
    public:
        // Constructor
        Encoder(int device_address);

        // Read motor encoders
        unsigned char read_encoders();
};
/* encoder.cpp */

#include "encoder.h"
#include<iostream>
#include<sstream>
#include<fcntl.h>
#include<iomanip>
#include<stdio.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include<linux/i2c.h>
#include<linux/i2c-dev.h>

Encoder::Encoder(int device_address) {
    std::string name = "/dev/i2c-1";
    this->device = device_address;
    this->file = open(name.c_str(), O_RDWR);
    if (this->file < 0) {throw;}
    if (ioctl(file, I2C_SLAVE, device) < 0) {throw;}
}

unsigned char Encoder::read_encoders() {

    // Write to GPIO address
    unsigned char buffer[1] = {0x09};
    if (::write(file, buffer, 1) < 0) {throw;};

    // Read GPIO register
    unsigned char output[1];
    if (::read(file, output, 1)<0) {throw;};

    return output[0];
}

My questions are:

  1. Is there any way to use my current board to sample up to 13.5 kHz? I.e., is there any way to make my Python/C++ programs run faster?

  2. Would it be better to connect the encoders directly to the GPIO pins? (I didn't want to do this initially because I have so many motors + other peripherals.)

  3. Or, is it necessary to use a dedicated microcontroller? I.e., a microcontroller that keeps track of the relative position, which I can then periodically send to the raspberry pi.

I'm hoping to have a simple PID loop to control the speed of the motors + detect if they hit their limits and stop moving.

Best Answer

For this many motors sampling becomes problematic. You need to switch to interrupts. There are several options for you to try.

  1. Connect INT output of MCP23008 to RPi and read IO expander only when you get pin-change notification.

  2. If communication is still slow, replace your expander to MCP23S08 and use SPI interface instead of I2C. This chip can support clock speed up to 10 Mhz.

  3. If that is still not fast enough, connect encoders to RPi GPIO (with some simple RC filters). You'd need something like RPi.GPIO for this.

  4. Finally, if everything else fails, use some cheap MCU to do decoding for you. In fact, you do not even need to send decoded pins to RPi in this case. Simply implement PID on this MCU and connect motor controllers to it as well, which will turn it into 8-channel servo. Then communication becomes much simpler: target positions are sent to MCU and current positions are sent back. And you will free almost all Rpi pins for other needs.