2

I'm working on a project that uses an Arduino UNO and a motorized fader to send and receive MIDI data from my computer/musical keyboard. I'm trying to set up my motor with PID to control the position and speed of the motor - Been having some difficulty, but after watching a few tutorials I think I'm starting to grasp the idea.

Here's the code that I came up with so far - I'm not sure if this is correct for PID control or not, but it makes my motor respond very strangely to any positioning data that is coming in.

#include <SoftwareSerial.h>
#define rxPin 2
#define txPin 1 

SoftwareSerial midiSerial (rxPin, txPin);

const byte wiper = 0; //Position of fader relative to GND (Analog 0)

const byte motorUp   = 4;
const byte motorDown = 5;
const byte motorPWM  = 6;

double faderMax = 0;
double faderMin = 0;

byte motorSpeed = 150;  // Raise if the fader is too slow (0-255)
byte tolerance  = 10;  // Raise if the fader is too shaky (0-1023)

int incomingCommand;
int incomingNote;
int incomingVelocity;
    
int currentPosition;
unsigned targetPosition;
unsigned long distanceToTarget;
unsigned PIDspeed;

void setup() {
  midiSerial.begin(31250); 
  Serial.begin(250000);
  analogWrite(motorPWM, motorSpeed);
  calibrateFader();
}

void calibrateFader() {
  digitalWrite(motorUp, HIGH);
  analogWrite(motorPWM, motorSpeed);
  delay(300);
  digitalWrite(motorUp, LOW);
  faderMax = analogRead(wiper) - tolerance;
  digitalWrite(motorDown, HIGH);
  analogWrite(motorPWM, motorSpeed);
  delay(300);
  digitalWrite(motorDown, LOW);
  faderMin = analogRead(wiper) + tolerance;
}

void loop() {

  while ( midiSerial.available() > 0) {
    incomingCommand = midiSerial.read();
    incomingNote = midiSerial.read();
    incomingVelocity = midiSerial.read();
    midiSerial.write( incomingCommand);
    midiSerial.write( incomingNote);
    midiSerial.write( incomingVelocity);
    targetPosition = incomingVelocity*8.05511811024; 
    currentPosition = analogRead(A0);
    distanceToTarget = targetPosition-currentPosition;
  }
  PIDspeed = abs(distanceToTarget)/6;
     
  if (distanceToTarget >=0) {
    analogWrite(motorPWM, PIDspeed); 
    digitalWrite(motorUp, HIGH);
    digitalWrite(motorDown, LOW);
  }
  
  if (distanceToTarget <=0) {
    analogWrite(motorPWM, PIDspeed); 
    digitalWrite(motorDown, HIGH);
    digitalWrite(motorUp, LOW);
  }
}

Updated code:

#include <SoftwareSerial.h>
#define rxPin 2
#define txPin 1

SoftwareSerial midiSerial(rxPin, txPin);

const byte wiper = 0; //Position of fader relative to GND (Analog 0)

const byte motorUp = 4;
const byte motorDown = 5;
const byte motorPWM = 6;

double faderMax = 1023;
double faderMin = 0;

byte motorSpeed = 150;  // Raise if the fader is too slow (0-255)
byte tolerance = 10;  // Raise if the fader is too shaky (0-1023)
byte Proportional = 12;

unsigned int incomingCommand;
unsigned int incomingNote;
unsigned int incomingVelocity;

unsigned int PIDspeed;
int currentPosition;
int targetPosition;
int distanceToTarget;

void setup()
{
    midiSerial.begin(31250);
    Serial.begin(250000);
    calibrateFader();
}

void calibrateFader()
{
    digitalWrite(motorUp, HIGH);
    analogWrite(motorPWM, motorSpeed);
    delay(300);
    digitalWrite(motorUp, LOW);
    faderMax = analogRead(wiper) - tolerance;
    digitalWrite(motorDown, HIGH);
    analogWrite(motorPWM, motorSpeed);
    delay(300);
    digitalWrite(motorDown, LOW);
    faderMin = analogRead(wiper) + tolerance;
}

void loop()
{
    while (midiSerial.available() >= 3)
    {
        incomingCommand = midiSerial.read();
        incomingNote = midiSerial.read();
        incomingVelocity = midiSerial.read();
        if (targetPosition <= 1023)
        {
            midiSerial.write(incomingCommand);
            midiSerial.write(incomingNote);
            midiSerial.write(incomingVelocity);
        }
        targetPosition = incomingVelocity * 8.05511811024;
    }
    currentPosition = analogRead(A0);
    distanceToTarget = (targetPosition - currentPosition);
    PIDspeed = (abs(distanceToTarget) / Proportional) + 90;
    analogWrite(motorPWM, PIDspeed);
    if (targetPosition <= 1023)
    {
        if (distanceToTarget > 60)
        {
            digitalWrite(motorUp, HIGH);
        }
        if (distanceToTarget < 60)
        {
            digitalWrite(motorDown, HIGH);
        }
        if ((distanceToTarget >= -60) && (distanceToTarget <= 60))
        {
            digitalWrite(motorDown, LOW);
            digitalWrite(motorUp, LOW);
        }
    }
}

In this version, the motor is actually less responsive than before. I think it must have to do with the data that's coming in from my DAW, which isn't consistently a piece of command, note or velocity data. For example, if I select a different track in my DAW, it sends data into my Arduino. When I read anything greater than 0 bytes, the data still comes in, but the note, command and velocity readings are more sporadically located. It was suggested by another that I try using a MIDI parser, but that's a different topic I suppose.

7
  • what were you expecting to happen? ... what happened instead?
    – jsotola
    Jun 22, 2021 at 22:52
  • I was hoping to add a proportional speed control that would allow the motor to accelerate and decelerate to a specific 'target position', which is being fed into my Arduino via a MIDI cable. What happened is that my motor began behaving very sluggishly when receiving any data. In order to get the motor to move at all, I'd have to feed data into it constantly Jun 23, 2021 at 2:37
  • i would start at the beginning ... write the minimum code required to turn the motor in one direction at slow speed ... then edit to minimum code to turn slow for 5 seconds and change speed to medium and run for 5 seconds, repeat .... from that you will learn what is required to keep the motor happy
    – jsotola
    Jun 23, 2021 at 2:53
  • You are not doing any PID calculations. Is that really, what you want? Or do you want to accelerate and deccelerate your motor symmetrically?
    – chrisl
    Jun 23, 2021 at 11:59
  • Everything that I've heard from previous users of these motorized faders swear that to have fluid movement, you must use PID rather than PWM. I was thinking that the 'Proportional' value would accelerate my motor proportionally slower as it gets to the target position? Is that not correct? Jun 23, 2021 at 14:23

2 Answers 2

1

I see 2 main problems with your code, that would make it behave strangely:

  • You are calculating the new motor speed and decide about the direction based on the variable distanceToTarget. But this variable is only updated, when there is data coming from your MIDI interface. I guess thats only happening, when you want to change the target position. But the motor control runs on every loop iteration, which will be way more often, than you can get the MIDI data. To solve this please move the lines

      currentPosition = analogRead(A0);
      distanceToTarget = targetPosition-currentPosition;
    

    out of the while(midiSerial.available() > 0) loop. Just put then right after it.

  • At the start of your midi code you check for more than zero bytes being available from midiSerial, but then you are reading 3 bytes in total, without knowing, if there are actually 3 bytes to read. SoftwareSerial.read() will not wait for a byte to come in, it will just return -1. So it can very well be that, just one byte is already received, when the midi code is executed. The rest will be received later. So the rest of the variables is just garbage/invalid. Also this means that sender and receiver are now misaligned. The other 2 bytes (which are received later) will then be handled by the midi code next time as if they were byte 1 and 2 (not 2 and 3). So the next transitions will also be garbage/invalid, until it aligns itself again by chance.

    To solve this you should check for at least 3 bytes being available (since you want to read 3 bytes):

      while ( midiSerial.available() >= 3 ) {
    

Note: As of your comments you seem to have a misunderstanding, what PID and PWM actually is.

  • PWM is just a way to drive the motor with variable speed by turning the pin on and off very fast, with different ratios between on and off time. With a high ratio towards on time the motor will turn faster than with a low ratio towards on time. This ratio translates to the range from 0 to 255 on an Arduino. So you need PWM to drive a motor with variable speed on the Arduino. You cannot cut that out.

  • PID is a closed feedback loop control. It denotes a way to calculate a new output value (PWM motor value) based on the measured input value (measured current position). PID stands for Proportional Integral Differential, so for the different terms in the calculations. With PID first the deviation between measured input and target/setpoint is calculated then the PID function is evaluated (with its proportional, integral and differential part of the deviation). With that you get the output value, that you can feed to your output device (the motor here).

    PID has nothing to do with PWM. They are totally different things. Only when driving an electromechanical device you might need PWM in the same system as PID. PWM is the hardware way of driving the motor, PID is the way to calculate the output value in a closed feedback loop to reach the target position smoothly and fast.

Actually with your current code (when corrected as described above) already does the proportional part of PID (since you calculate the new speed from the distance to target, so the deviation from the target position). To get full PID you would also need to implement the rest. I would suggest using the Arduino PID library, which already handles the calculation for you. You just need to call it correctly (refer to the examples of the library).

2
  • Thanks so much for the details! I'll try this out asap Jun 23, 2021 at 16:32
  • I'm definitely still trying to understand the concept of PID. I'm glad I've at least got the Proportional section correct (thanks to Majenko in a previous post). I'll try out that library with this and see if I can get it to work out. Jun 23, 2021 at 17:47
0

There is a fundamental problem with the way you are reading the serial data. Read this excellent article by @Majenko: https://majenko.co.uk/blog/reading-serial-arduino

The following code will read the last 3 bytes. If, for example, there are 9 bytes available, it will ignore the first 6 bytes:

while (midiSerial.available() >= 3)
{
    incomingCommand = midiSerial.read();
    incomingNote = midiSerial.read();
    incomingVelocity = midiSerial.read();
. . .
}

This will read the next 3 bytes:

if (midiSerial.available() >= 3)
{
    incomingCommand = midiSerial.read();
    incomingNote = midiSerial.read();
    incomingVelocity = midiSerial.read();
. . .
}

Here is the tweaked loop() which:

  • uses if (midiSerial.available() >= 3) to read the next 3 bytes.
  • clips the targetPosition if it's out of bounds (0 to 1023).
  • clips the PIDspeed if it's out of range (0 to 255).
  • switches the other motor control signal off before enabling the current one just in case the targetPosition crossed zones between loop() iterations.
const int QUIESCENT_BOUNDARY = 60;
const float INCOMING_VELOCITY_SCALE = 1023.0 / 255.0;

byte incomingCommand;
byte incomingNote;
byte incomingVelocity;

void loop()
{
    if (midiSerial.available() >= 3)                // Use `if` to get the next 3 bytes available.
    {
        incomingCommand = midiSerial.read();
        incomingNote = midiSerial.read();
        incomingVelocity = midiSerial.read();
        midiSerial.write(incomingCommand);
        midiSerial.write(incomingNote);
        midiSerial.write(incomingVelocity);
        targetPosition = incomingVelocity * INCOMING_VELOCITY_SCALE;
        if (targetPosition > 1023)                  // Clip if out of bounds.
        {
            targetPosition = 1023;
        }
    }
    currentPosition = analogRead(A0);
    distanceToTarget = targetPosition - currentPosition;
    PIDspeed = abs(distanceToTarget) / 6;           // TODO: Tune the PID equation.
    if (PIDspeed > 255)                             // Clip if out of range.
    {
        PIDspeed = 255;
    }
    analogWrite(motorPWM, PIDspeed);
    if (distanceToTarget > QUIESCENT_BOUNDARY)      // Above the positive bound of the quiescent zone.
    {
        digitalWrite(motorDown, LOW);               // Switch the down control off before switching the up control on.
        digitalWrite(motorUp, HIGH);
    }
    else if (distanceToTarget < -QUIESCENT_BOUNDARY)// Below the negative bound of the quiescent zone.
    {
        digitalWrite(motorUp, LOW);                 // Switch the up control off before switching the down control on.
        digitalWrite(motorDown, HIGH);
    }
    else                                            // In the quiescent zone.
    {
        digitalWrite(motorDown, LOW);
        digitalWrite(motorUp, LOW);
    }
}

That should help you to tune the PID equation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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