Wednesday 26 April 2017

Motion control - H bridges and movement

In my last post I was talking about getting the feedback mechanism from a printhead carriage up and working. This enabled me to track how much relative movement of the carriage had occurred, but didn't enable me to make the thing move! That comes in this part. In order to get motion happening, I need something that can handle the voltages/currents the motor needs - a job for an H bridge!



But, before we get to that part, let's work through what I'd been doing. I'd identified the output of the incremental optical encoder on the printhead carriage, and I could track the relative movement. This means zero is wherever the carriage happens to be when I apply power. The motor that controls the carriage is just a standard brushed DC motor, and this hooks up to a toothed belt to make things happen. This seems a good point for a picture as a reminder of what I'm working with,


If I manually apply a voltage across the motor terminals, I can get it to move as long as I apply a sufficient voltage. Sufficient in this case is at least 8 or 9v, and I suspect that 12v to 24v might be more appropriate. This is obviously more voltage and current that an I/O pin on a microcontroller can supply or sink, so I need something that can interface the microcontroller to the motor. If the motor only needed to turn one way, I could use a simple transistor (and diode for back EMF protection). However, I want the motor to be able to move both ways, so I need to be able switch the polarity of the connections to the motor. Fortunately there are ready made parts for this - the H Bridge. If you want to know the ins and outs of how the H bridge works, and what the challenges with working with them are, I'd recommend reading Modular Circuits' H-Bridges – the Basics as well as the rest of articles in that series to help you not make the same mistakes I did!

The H bridge in practice


So, how does the L298N  actually work in practice? First, the chip contains two H bridges, but these are identical, so I'll consider just the one of the pair - I'm also only driving one motor at the moment. Each of the H bridges has three inputs - input 1, input 2 and enable, and two outputs -  out 1 and out 2. Let's draw up a quick truth table to illustrate what the various different states are:


InputsOutputs
In 1In 2EnableOut 1Out 2
LowLowHighGNDGND
LowHighHighGNDVS
HighLowHighVSGND
HighHighHighVSVS
Don't careDon't careLowFloatFloat

From this truth table, we can see that setting an input to low will cause the output to be connected to ground, and setting an input to high will connect the output to the power supply. We can also see that if we take the enable pin low, the H bridge doesn't connect the outputs to anything. When one side of the motor is connected to the supply voltage (VS), and the other side is connected to ground, the motor will spin in one direction. Switching the polarity will cause it to spin the other direction. However, we still have two other states, where both sides of the motor are connected to the supply voltage or to ground. In these states, the motor is effectively shorted through the H bridge. Now, this is where I started to overthink things, and lead to my becoming unstuck.

But first I set up a quick test, connecting the three inputs of the H bridge to three outputs on the Seeduino Lite. This allows me to confirm that setting In 1 high and In 2 low causes the motor to spin one way, and switching the states causes it to spin the other way. I did this testing with a variable power supply, as I had no idea of the specs of the motor. Initially it was done with a low voltage (3-4v), which resulted in no movement as it wasn't enough to overcome the stiction in the motor/carriage. Turning up the voltage a little to about 6v or so was enough for the motor to start moving, and for me to see which inputs caused what direction.

With the success of a simple test, I wanted to introduce PWM to allow me to control the speed of the motor. This was because, at the full 24V, the motor was quite capable of slamming the carriage into the ends of the mechanism faster than I could respond. Therefore, I figured it might make sense to make smaller movements with less power. My initial attempt was to connect one of the inputs of the H bridge to a PWM pin, and the other to a digital pin.

This lead to an interesting discovery that should have been apparent to begin with. The motor is driven when the two input pins are different. If your digital pin is low, and you set the PWM pin to a low duty cycle, you get a low average power. On the other hand, if your digital pin is high and you set the PWM pin to a low duty cycle, you get a high average power. This means that in one direction the carriage might crawl, but in the other direction it runs at nearly full speed!

The fix for this is fairly simple - test to see which direction the motor should be going, and invert the duty cycle if we're running in reverse. The code to do this looks like the following:

  if (Speed<0) {
    digitalWrite(PIN_OUTA,HIGH);
    analogWrite(PIN_OUTB, 255-abs(XaxisSpd));
  }

  if (Speed>0) {
    digitalWrite(DIR_OUTA,LOW);
    analogWrite(PIN_OUTB, abs(XaxisSpd));
  }

So, we have some code that allows us to change the direction of the motor, but we want more. Ideally, we'd like to be able to command the system to go to a position and have it deal with figuring out the direction. Effectively, we want it to take the output of the encoder, and use that to set the speed and direction of the motor to get to a commanded position.

We could simply write some code that would figure out the direction to move the head in, and then set a suitable PWM output until the encoder indicates the head has reached the set point we asked for. Unfortunately, the real world gets in our way and things like inertia means that the carriage takes time to accelerate and slow down. This means the carriage would continue to travel past the set point by some distance. Not only that, but the friction in the system isn't constant, so the torque the motor has to apply before anything happens isn't constant either! I'm beginning to discover that there's a whole bunch of things needed to make this work. First up, I need a servo compensator.

There appear to be a number of different possibilities for the compensator, but I've chosen to go for a straightforward PID implementation. Brett Beauregard has written a PID library, so this seems a good place to start. His library includes features such as bumpless transfer, anti-windup and removal of the derivative kick, and he's written a series of good blog posts on PID that are well worth a read.

Enter the PID

The PID controller has two main inputs: the setpoint (the position we want to reach), and the current position. From that, it calculates a suitable output signal. However, the PID controller needs some tuning in order to work best for the mechanism it is controlling. There are three main terms for tuning the PID, one for each letter - Kp, Ki and Kd.

In understanding these terms, it is helpful to know that the error is simply how far away the current position is from the set point.

The Proportional term only considers how large the error is at an instant in time. Its contribution is simply a percentage of the error. You could consider it to be a spring, trying to pull the carriage to the setpoint.

The Derivative Term considers the rate at which the error is changing - effectively determining the slope of the error over time. The idea is that this help to predict the movement, and thus help to dampen the movement of the integral spring, like a shock absorber on your car.

The Integral term considers both how far and for how long the current position has deviated the setpoint. At a basic level, it sums the error over time. So, even if an error is small, over a period of time, the integral contribution should get large enough to correct for this. This helps to overcome the effects of friction in the system

Now that we've got a definition of the terms, I think it would be a good time to have a picture that illustrates how these parts all fit together.



Another challenge I encounter here is the fact that each of the PID constants needs to be tuned for my application. Now, I've not attempted to tune anything yet, and these figures are taken from the work Michael Ball did on building a reprap from scrap. that  They are likely to be sub-optimal, but you'll have to wait for another update to get more info on tuning! The code I ended up with for testing is as follows (again, heavily influenced by Michael's work that can be found on github):

#define ENCODER_USE_INTERRUPTS
#define PIN_OUTPUT 5    // PWM for inhibit
#define DIR_OUTA 4      // H-bridge
#define DIR_OUTB 7      // H-bridge

#include "Encoder.h"
#include "PID_v1.h"

Encoder myEnc(2, 3); // INT1, INT2

int counter=0;

// Tuning parameters
float Kp=0.6;              //Proportional Gain 
float Ki=40;               //Integral Gain 
float Kd=0.2;              //Differential Gain 

// PID input/outputs
double XaxisSetpoint=0;    // Setpoint for carriage
double XaxisPos=0;         // Actual position of carriage
double XaxisSpd;           // Carriage speed from -255 to 255

// Hardcoded target positions
int targets[] = { 6000,3500 };
char ptr=0;

// Instantiate PID control
PID XaxisPID(&XaxisPos, &XaxisSpd, &XaxisSetpoint, Kp, Ki, Kd, DIRECT); 

const int sampleRate = 1; 

void setup() {
  analogWrite(PIN_OUTPUT,0);    // Should ensure motor won't turn at startup.
  digitalWrite(DIR_OUTA,LOW);
  pinMode(DIR_OUTA,OUTPUT);     // Then enable outputs.
  pinMode(DIR_OUTB,OUTPUT);
  
  XaxisPID.SetMode(AUTOMATIC);                  // Turn on the PID loop 
  XaxisPID.SetSampleTime(sampleRate);           // Sets the sample rate 
  XaxisPID.SetOutputLimits(-255,255);           // Set max speed for DC motors
}


void loop() {
  static char lastDir=2;
  char myDir;
  
  XaxisPos=myEnc.read();
  
  if (XaxisPos == XaxisSetpoint) {
    analogWrite(PIN_OUTPUT, 0);   // Stop the motor before new setpoint
    XaxisSetpoint = targets[ptr++];
    if (ptr==2) ptr=0;            // Cycle through the test positions
  }

  XaxisPID.Compute();             // Do the PID calculations
 
  if (XaxisSpd<0) myDir=0;        // Figure out the motor direction
  if (XaxisSpd>0) myDir=1;

  if (myDir!=lastDir) {
    digitalWrite(DIR_OUTA,myDir);
    digitalWrite(DIR_OUTB,!myDir);
    lastDir=myDir;
  }
  XaxisSpd = XaxisSpd/2;
  analogWrite(PWM_OUTPUT, abs(XaxisSpd));           // Apply PID speed to motor
}


This should result in the motor moving the carriage between two positions that are nominally 6000 and 3500 counts from wherever the start position was.

There's still some work to do on this code though. I need to add some code to send regular updates out over the serial port, so that I can generate graphs showing how the PID responds the a change in setpoint - I'm interested to see how the settling works. I should also add some code to receive setpoints as well as PID parameter updates. This should allow me to tune on the fly without having to recompile each time around. One of the really obvious improvements that could be made would be to ensure the carriage started from a known position too!

No comments:

Post a Comment