0
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ a platform that supports high speed hardware interrupts would be much better. \$\endgroup\$ – Scott Seidman Nov 25 '19 at 23:06
  • 2
    \$\begingroup\$ This is just plain not going to work. Use either a dedicated encoder counter chip, a suitable MCU, or some carefully chosen programmable logic to track the encoder count, and then read an atomically "frozen" sample of that over to the pi by the slower interface of your choice. There are pi-like systems with a very fast auxiliary MCU core in the SoC but those bring additional complications; that said the pi itself may not be a wise choice (in terms of reliability/longevity) in your environment either. \$\endgroup\$ – Chris Stratton Nov 25 '19 at 23:16
  • \$\begingroup\$ I wrote explicit assembly code in 2001 for the x86 that handled one A or B channel event every \$10\:\mu\text{s}\$, on average, at maximum RPM. And that's the average. Since the speed isn't perfect and neither is when I'm sampling (the x86 isn't exactly a predictably-timed instruction set), I had to be sure I could handle half that time. It was pure assembly code and it never had a problem. This was for calibrating medical infusion pump encoders, so it was critical medical software. Never a reported problem in nearly 20 years. Single motor, though. RPi without I/O expander should handle 8. \$\endgroup\$ – jonk Nov 26 '19 at 4:50
0
\$\begingroup\$

2 years ago we designed the system with 14 quadrature encoders with 4096ppr running at up to 10RPM. We used FPGA to implement 14 bidir 24bit counters which were available to CPU via SPI.

It appears to me that second good way to control 8 motors is to use smart encoders (SSI type or similar, it depends on raspberry PI i/o availability).

\$\endgroup\$
  • \$\begingroup\$ If this is all there was to the application, then I think an FPGA was over-kill. I'd have no problems using a cheap MCU and 14 A/B quadrature encoders [at under 10k pulses (for all 14) per second to deal with.] That is, if all it is has to do is update counters, anyway, and perhaps a little communication with another MCU about them. (But I don't know the application details and the application probably had a lot more than I know about from here.) \$\endgroup\$ – jonk Nov 26 '19 at 5:56
0
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the suggestions. I tried using the GPIO pins + the pigpio library, but it's still skipping. I'll try an MCU next (maybe an STM32?) \$\endgroup\$ – qtc0 Nov 26 '19 at 20:59
  • \$\begingroup\$ Just get Blue Pill for experiments. it is sufficient for this application. \$\endgroup\$ – Maple Nov 26 '19 at 21:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.