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.

tim
  • 699
  • 6
  • 15
zRockafellow
  • 131
  • 7

2 Answers2

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).

chrisl
  • 16,622
  • 2
  • 18
  • 27
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.

tim
  • 699
  • 6
  • 15