Motorized carriage occasionally slams into limit of travel

Hey all,

Namaste. This community has been a great help over the past few weeks. I've gotten a printer carriage to move from position to position with pretty good performance.

At least, the performance is good most of the time. Sometimes the carriage slams into the limit of travel. Specifically, I think it ignores one or more I2C writes from master which should tell it to go to the desired position, and instead continues at a previously communicated speed and direction until it hits the wall. Testing this hypothesis, I figured that at higher speeds, I would have more encoder interrupts, and thus a higher chance of causing the carriage to run into the wall. It appears to be the case that higher speeds and longer travel distances (i.e. more interrupts) make this happen more often. It can happen at slow speeds too if you wait long enough. The position seems to be accurately counted throughout testing probably to within a line or two at 600 lpi as I am always able to go back to the initial position from power on with no visible position error. Paul's encoder library is pretty great!

I'm not too sure what the next step is from here. I would really prefer that there is zero chance of the carriage crashing in the middle of a performance or slurring the melody. I could use a second Atmega328P for handling motor and actuator control and then just leave the other one to solely count and communicate encoder lines. I plan on having 6 of these things sharing an I2C connections, so that would be 12 Atmega328Ps. That's not terrible cost-wise (I don't think), but I'm open for other suggestions. My schematic and code used in testing are below or attached:

Atmega328P Slave Code:

#include <PWMFreak.h>
#include <Wire.h>
#include <Encoder.h>
//#include <TimerOne.h>

#define RESOLUTION 100000

union Buffer
{
  long longNumber;
  byte longBytes[4];
};

Buffer position; // create an instance of Buffer

byte addr = 8;
Encoder myEnc(2, 3);
byte pluckPin = 4;
byte dampPin = 5;
byte PWMPin = 6;
byte DirPin = 7;
byte limPin = 8;

byte speed;
byte dir;

void setup() {
  
//  Serial.begin(250000);
//  Serial.println("Boot up");

  Wire.begin(addr);                // join i2c bus with address #8
  Wire.onRequest(requestEvent); // register event
  Wire.onReceive(receiveEvent); // register event
  
  pinMode(pluckPin, OUTPUT);
  pinMode(dampPin, OUTPUT);
  pinMode(DirPin, OUTPUT);
  pinMode(PWMPin, OUTPUT);
  pinMode(limPin, INPUT);

  setPwmFrequency(6, 1);
}

void loop()
{
  position.longNumber = myEnc.read();
}

void requestEvent()
{
  Wire.write(position.longBytes, 4); // send requested bytes
}

void receiveEvent(int howMany) {
  char act = 0;
  while (Wire.available()) { // loop through all but the last
//    Serial.print("Bytes available: "); Serial.println(Wire.available());
    if (Wire.available() > 1) {
      analogWrite(PWMPin, 0); // if the encoder interrupts this event handler but it gets this far, at least it stays put
      act = Wire.read();
      speed = Wire.read();
      dir = Wire.read();

//      Serial.print("act: "); Serial.print(act); Serial.print(", speed: "); Serial.print(speed); Serial.print(", dir: "); Serial.println(dir);
    }
    else {
      act = Wire.read(); // receive byte as a character
//      Serial.print("act: "); Serial.println(act);
    }
  }
  switch (act) {
    case 'p':
      toggle(pluckPin);
//      Serial.print("pluckPin state: "); Serial.println(digitalRead(pluckPin));
      break;
    case 'd':
      toggle(dampPin);
//      Serial.print("dampPin state: "); Serial.println(digitalRead(dampPin));
      break;
    case 's':
//      Serial.print("Setting speed to : ");Serial.print(speed);Serial.print(" with direction: ");Serial.println(dir);
      digitalWrite(DirPin, dir);
      analogWrite(PWMPin, speed);
      break;
    case 'h':
    //      home();
    default:
//      Serial.print("It defaulted! act was: "); Serial.println(act);
      break;
  }
//  Serial.println();
}

void toggle(byte pin) {
  bool state = digitalRead(pin);
  digitalWrite(pin, !state);
}

//void home() {
//  Wire.end();
//  digitalWrite(DirPin, LOW); analogWrite(PWMPin, 40);
//  while(digitalRead(limPin) == false){delay(20);}
//  analogWrite(PWMPin, 0);  digitalWrite(DirPin, HIGH);
//  myEnc.write(0);
//  Wire.begin(addr);
//}

Teensy Master code: attached as Maestro.ino
DiddleyBow library to simplify Teensy Master Code: Attached as DiddleyBow.h and DiddleyBow.cpp

Schematic: attached, and hopefully shown below

Maestro.ino (6.71 KB)

DiddleyBow.h (1.72 KB)

DiddleyBow.cpp (2.75 KB)

not much of a description of what this is or how it works, is controlled. the youtube link doesn't work

looks like the code expects to receive one of 4 ascii character commands via an I2C interface, one of which sets a direction and speed

the schematic shows a digital pot on an I2C bus that could provide feedback, but it's not clear if it's used

it looks like there's an encoder interface that updates a value, "longNumber". but i don't see where that value is used besides writing it over I2C. i don't see an encoder in the schematic

the components suggest that the encoder provides a desired position that is compared to an actual position measured with the "pot" and the code should drive the motor in one direction or the other to achieve that position within some tolerance.

instead it looks like a command is received to set the speed and direction of a motor.

have no idea what the "pluck" and "damp" pins are for

some additional explanation would be helpful

I can see 1 problem that may or may not be contributing to your problem. I see you substituted 12v for the 24v feeding the 12v & 5v regulators. Most linear regulators require some headroom on the input to maintain good regulation of the output, so the 12v regulator would need ~15v input (3v above output). Of course if you aren't using it then don't worry about it.

No, I didn't hook 12V to the input of a 12V regulator, lol. I'm not actually using a 12V regulator, but for the 12V going to the DC motor controller, I am getting it from a wall wart rated for 2A. My test circuit incorporates only what's inside the red outline, but I can see how you might be wondering where the 12V for the motor driver was gotten.

I actually tried to edit the post to fix the youtube link after posting, but It still didn't work. Maybe it will work this time. Let me shape your understanding of what is going on here. The digital pot is not used. The slave does receive 3 byte speed communications from the master that tell it 1) This command is to set the speed 2) the speed is #, 3) the direction is # (byte 1 or byte 0).

The slave also sends the position as read from a quadrature optical encoder. In the diagram, I had to make do with what looks like a DPDT switch to represent the encoder. It is read by the master (code in the attachments from the original post, went over 9000 characters, so I attached it, although that might have been the image?). The master takes the position from the slave and the desired position from MIDI data, calculates a speed and direction, and then tells the slave to go at different speeds based on how far away it is from the destination.

This is a musical instrument. Eventually it will pluck or dampen a string. Right now, I'm just focused on getting speed control down. I gave the full code, not all of it is relevant.

Thanks for your time.

if either the encoder data from the arduino or the command to the arduino is corrupted there's a problem. however, it's possible errors are occurring but hidden because the next command takes effect.

it might be better if a command is sent to the arduino to position the motor rather than control its speed. the arduino can implement a PID algorithm to control the speed of the motor toward the target.

if the commanded position is corrupted, the arduino can either ignore it or limit its position -- never let the motor hit its limits

presumably there are many encoder and commands transferred for each position of the device, each of which may be corrupted and cause a problem. "closing the control loop" inside the arduino reduces the amount of transmission

That would be great. I think I was just skeptical about controlling the speed from the Arduino. I thought that the encoder interrupts might interfere with that, but since I could put that in the main loop I guess there's nothing to worry about there.

With the code I tested with, it shouldn't be spamming messages to set the speed at the 4000us period of the conduct function in the Master code. It should only communicate when it crosses thresholds to change speed, so one communication each to tell the arduino to go fast, medium, slow, and stopped per movement (plus overshoot).

I did hope that I could change speed settings to accommodate legatos. I can still communicate PID settings or position thresholds and corresponding speeds via I2C. I'll give this a try.

I tested the I2C communications and found it was fraught with miscommunications. Even if I went to communicating positions, I'm not sure that I want to be susceptible to I2C communication errors, especially as I add more devices to the bus and more length to the bus wires themselves. It seems only to be when the Teensy 3.5 master is trying to communicate with the arduino slave that the problems occur.

I think I'll try SPI, although now I'll need 6 CS lines in addition to the SCK, COPI, and CIPO buses.

i doubt the problem is specifically with the I2C interface, the SPI hardware built into a chip won't be better. more likely the wiring.

how did you verify that it was an I2C communication problem? how did you verify the messsage?

why use I2C or SPI for command communication instead of straight serial, perhaps over rs-485

i've had problems with noisy encoder lines. some encoder transitions are invalid, indicating problems

I verified the I2C communication problem by going into the arduino (peripheral) code and adding a few lines to the receiveEvent code to see what I was getting from the Teensy (controller) for writes that were intended to change the speed. Speed writes should come in as 3 bytes - act, speed, and dir. All other writes should be 1 byte sent.

void receiveEvent(int howMany) 
  char act = 0;
  while (Wire.available()) { // loop through all but the last
//    Serial.print("Bytes available: "); Serial.println(Wire.available());
    if (Wire.available() > 1) {
       if (Wire.available != 3){
          Serial.print("bytes available: "); Serial.println(Wire.available);
          for(byte i = 0; i < Wire.available(); i++){
             Serial.print("byte");Serial.print(i+1);Serial.print(": ");Serial.println(Wire.read());
          }
        }
       else{
      analogWrite(PWMPin, 0); // if the encoder interrupts this event handler but it gets this far, at least it stays put
      act = Wire.read();
      speed = Wire.read();
      dir = Wire.read();
     }

Many times, there were 2 bytes available. Many other times there were 4 bytes available. It was fairly common that the bytes were corrupted. Most of the time the values came across fine.

I did a similar test to see if the position as communicated by the arduino peripherals are coming across fine, and that seems perfect. I can’t get it to slip. I may just error check by requesting the speed along with the position of the peripherals, and if it doesn’t match what was intended, then it will send another speed message.

I thought to use SPI because it has a chip select. I want to communicate with 6 slaves (at least). I suppose I could just communicate an address over async serial and then I don’t need to worry about having 6 different chip select lines?

My encoder on this carriage is pretty good. These are printer carriages from HP printers. I can go from one end of travel to the other several times and reliably read 0 at one end and 6033 on the other, so I think the encoder part of this is pretty rock solid (at least when I don’t put hardware serial code debugging lines in my peripheral code).

Also thought that SPI would be more reliable because it uses push/pull outputs rather than the open drain configuration of I2C. I figured that would make it a communication method that is harder to mess up.