Custom Work: Stage Machinery Part 2 – Motion Control

A few months ago, we blogged about the winches we built for Grizzly Bear. We hinted at the clever motion control system we created for them, but we didn’t go into much detail in the original post. Here, for anyone who’s interested in the technicalities of motion control, are all the details:

The core problem we ran into when designing the winch system was that rather than being a cue-based system, where the instruction would be “move to position B at speed X”, we were dealing with a streaming DMX system. While the DMX system could send instructions like “target position B, speed X”, it was likely that we would encounter situations where we wanted both the target and the speed to change while in transit.

The preferred way of dealing with this, in general, is a PID loop. PID stands for Proportional Integral Differential and describes the three aspects of the control system that are implemented. PID loops are powerful, but they require a good bit of tuning to function properly. Given our limited timeline, we wanted to go with something a little simpler to implement. To some extent, we implemented a pure proportional controller, although we gave it knowledge of the system it was controlling to allow it to be a little smarter.

We ended up with a system that knew only a few variables: target speed (from DMX), target position (also from DMX), current speed (stored in program), and current position (read from sensor). The system ignored history, so it did not need to know anything about the past commands (much like a Buddhist, it lived in the Now). Here’s how we did it (what follows is a lot of code, so brace yourself).

In the code, two defined values determined the motion profiles:


#define DPC 0.00367 // Distance per Speed Unit (SU) to stop

#define ACCEL 3 // 3 SU per millisecond should do it.

To really understand this fully, we should step back a moment and explain the motor control hardware a little. For input we had a 10-turn potentiometer that read 0-1023 and was geared off the drive shaft at 3.5:1. Each tick of the pot was 0.43 inches of travel. For output, we sent a direction toggle and motor speed signal to the motor. For this system, a 3000Hz speed signal was full speed. In the control system, we kept everything in native units, so distance was calculated as potentiometer ticks (and we know that one tick was 0.43 inches), and speed was calculated as Speed Units (or SU – a unit we created for this system). We fixed acceleration at zero to full speed in 1 second, so if we were going to 1/4 speed, it would take 1/4 second. This was represented in the ACCEL value as speed units per millisecond.

Decelerating into a stop, however, required a particular calculation. Because of the fixed acceleration curve, we knew that we needed a certain distance to stop – this was expressed in distance units per speed unit. So, if we were going 1000 speed units, it would take 3.67 distance units (or about 1.6 inches) to stop. By expressing it this way, it made the calculations very simple.

Here’s the code, with comments:

// dist_to_target holds the absolute distance between
// the target and the current position.

dist_to_target = abs(mapped_target - (int)fpos);

// The first thing is whether we need to go up
// or down from our current position.
// Due to the way the pots wired up, lower position
// values are higher numerically.

if(mapped_target < (int)fpos) // If we need to go up
{

// The first step is to determine our target speed.
// DMX values are 0-255, our speed is 0-3000.
// This is done per-direction because we can have
// different up vs. down speeds
// map() does not constrain, so if set_speed were
// set to 1023, we could get very high (and wrong)
// values for mapped_speed. So we map and then constrain.

mapped_speed = map(set_speed,0,255,0,3000);
mapped_speed = constrain(mapped_speed,0,3000);

// cur_speed holds our current speed, but can vary
// from negative values (for going down) to positive
// values (for going up).
// We did this to simplify the math, and to avoid
// needing a separate variable for direction.

// This if statement checks to see if the distance
// to the target is larger than the distance required to stop
// That is: do we need to be decelerating?

if(dist_to_target > (int)(((float)abs(cur_speed))*DPC))
{

// This if statement means "do we need to be accelerating?"
// We don't need to check the signedness of
// cur_speed, since we're going upwards.

if(cur_speed < mapped_speed)
{

// last_update holds the time (millis()) of
// the last time we ran through this function.
// Smooth acceleration requires that the loop()
// function take no less than 1 millisecond to execute.

cur_speed += (ACCEL * (millis() - last_update));

// It is possible for last_update to be long
// enough ago that we overshoot top speed.
// This enforces the top speed.

if(cur_speed > 3000)
{
cur_speed = 3000;
}
}

// This handles the case that we are not
// close enough to the target to have to decelerate
// BUT we have been commanded
// a new (slower) speed, and need to slow down to it.

else if(cur_speed > mapped_speed)
{
cur_speed -= (ACCEL * (millis() - last_update));
if(cur_speed > 3000)
{
cur_speed = 3000;
}
}
}

// This handles the case of being close enough
// to a target position that we need to decelerate at it.

else
{

// Speeds below 30Hz are considered stops to prevent Zeno's paradox

if(cur_speed > 30)
{
cur_speed -= (ACCEL * (millis() - last_update));
}
if(cur_speed < 30)
{
cur_speed = 0;
}
}
}

We then ran a similar (but with signs flipped) set of functions for travel in a downward direction.

The actual output control to the motor is very simple:


if(cur_speed > 100 && cur_speed <= 3000)
{
digitalWrite(8,LOW); // Go up
tone(9,abs(cur_speed));
}
else if(cur_speed < -100 && cur_speed >= -3000)
{
digitalWrite(8,HIGH); // Go down
tone(9,abs(cur_speed));
}
else
{
noTone(9);
}

Because we were using frequency control for motor speed, we used the tone() function, which provides for frequency output on any pin. While the cur_speed <= 3000 statements are a little redundant, they provided safeties in case we tried to command the motor to go too fast.

This all worked well, with one exception: the potentiometer signal was noisy. We added some filtering by averaging values, but it wasn't quite enough - if speed was kept high, the motor would hunt around its target position. Since a target was 0.43" and the minimum speed was 30Hz, it could take 4 seconds for the winch to make it to the next tick - this led to the winch constantly hunting up and down as the noise in the potentiometer made it think it was a little bit off.

We did two things to solve this. The first was to change the minimum output speed to 100, while keeping the minimum speed in the controller at 30. This caused more of a tendency to stop, but it wasn't quite enough. Secondly, we added a "debounce" controller. Essentially, once the distance to target was below a certain threshold, the winch would time how long it had been that close. If the time exceeded a certain value (about 100ms), it would stop. This effectively decreased the target positioning by a fair bit, but caused the winches to come to a definitive stop when they reached their targets. Due to the needs of the rock tour, this was deemed acceptable.

The one thing we wanted to achieve with this controller but didn't have time to implement is a 16-bit "pan", having the winch track the position it's supposed to be going. This allows you to basically lock the winch off at full speed and write cues in the light board that take a defined amount of time, using that to control winch speed. In the next version (currently being designed!), we'll be using a more advanced motion control system that will make this possible.

Comments are closed.