Introduction: Understanding Channel Mixing

About: I work at RobotZone ( the folks behind Actobotics and ServoCity.com ) in Winfield, KS. I love working on projects with my kids and seeing what they create.

If you’ve ever driven a remote control chassis, there’s a good chance you’ve used mixing, even if you did not know it. Specifically, if you’ve used a single joystick or gimble to control a vehicle which uses skid steering or differential steering, you’ve used mixing.

Mixing is simply how the data from your joystick is used to determine how much power should be supplied to each side of the chassis.

If you open up a joystick, generally you will see two potentiometers inside. One to measure your current position along the Y axis (up and down), and the other to measure where you are at along the X axis (side to side).

Although I have no formal training on the subject I have had to do mixing in code before and recently I wanted to dive a little deeper into the subject.

First I want to note that most RC transmitters have mixing capability as do many motor controllers. This information is going to be most useful if you have to do the mixing yourself in your code. Say for example if you are using an Arduino to read unmixed data from an RC receiver, or you’re reading analog data from the pots in a joystick, or if you are reading the coordinates from a digital joystick on a mobile app.

Let's take a look at some different mixing approaches.

Step 1: Mix Method » None

First let's take a look at what happens if you don't use mixing at all. If you just send the data from one axis to one side of the chassis and the other axis to the other side, your vehicle would not respond the way you want it to.

For example if you push the joystick all the way straight forward, the Y axis is at full throttle and the X axis is at 0. So you would be driving in circles instead of going straight.

Step 2: Method Method » Rotate

A co-worker once pointed out to me that, in a pinch you can rotate your transmitter 45 degrees for a poor man’s mix. If you think of the values from the two potentiometers in a joystick as being the x an y axis on a grid (with both axis spanning -100 to +100) this makes a lot of sense because you are going to +100 on both axis as you push the joystick up and to the right. So if this maps directly to your two chassis channels (the left and right sides of your robot) it would make your robot go forward.

So the first method of mixing I ever tried was to mathematically rotate the x and y coordinate 45 degrees about the center point of the grid.

This works ok, however I cannot go forward with 100% power because when you are rotating, the overall movement is constrained to a circle within the grid, which means you can never really get into that top right corner.

This also results in the corners of the grid not being utilized. This is not a problem if you are using a joystick/gimple which limits your movement so those areas are never reached anyways, but otherwise you will want that portion of the grid to do something so that your movements feel completely proportional.

If you are a visual learner like myself this concept might be easier to grok by watching the video at the beginning of this instructable.

Let's look at some code examples.

NOTES ABOUT MY CODE EXAMPLES:
I am leaving out how you get the joystick_x and joystick_y values as it would change depending on your project. Also I will be mapping/constraining to ±100 but you might likely need to map to 1000 - 2000 for PWM or 0 - 255 for analog output etc. I always constrain... just in case.

Arduino Example:

//mathematically rotate
double rad = -45*M_PI/180;  
int leftThrottle = joystick_x * cos(rad) - joystick_y * sin(rad);
int rightThrottle = joystick_y * cos(rad) + joystick_x * sin(rad); //constrain leftThrottle = constrain(leftThrottle, -100, 100 ); rightThrottle = constrain(rightThrottle, -100, 100 );

JavaScript Example:

//mathematically rotate
var rad = -45*Math.PI/180;
leftThrottle = joystick_x * Math.cos(rad) - joystick_y * Math.sin(rad);
rightThrottle = joystick_y * Math.cos(rad) + joystick_x * Math.sin(rad);

//constrain
leftThrottle = constrain(leftThrottle, -100, 100);
rightThrottle = constrain(rightThrottle, -100, 100);

//helper function
var constrain = function(num,min,max){
return Math.min(Math.max(num, min), max);
};

Step 3: Method Method » Simple

Next up we have a very simple equation which I first picked up from one of Shawn Hymel’s Adventures in Science SparkFun videos where he happened to be working a very similar project to the one I was working on.

This equation does let you get to full speed when going forward but much like the rotate method, it disregards the corner areas of the grid. This is because in some cases the maximum is 100 and in some cases the maximum is 200. So the you would use a constrain function to disregard anything after 100.

And by the way I don't call this simple derogatorily... there is a beauty in simplicity.

Arduino Example:

int leftThrottle = joystick_y + joystick_x;
int rightThrottle = joystick_y - joystick_x;

//constrain 
leftThrottle = constrain(leftThrottle, -100, 100 );
rightThrottle = constrain(rightThrottle, -100, 100 );

JavaScript Example:

var leftChannel = joystick_y + joystick_x;
var rightChannel = joystick_y - joystick_x;

//constrain leftChannel = constrain(leftChannel, -100, 100); rightChannel = constrain(rightChannel, -100, 100); //helper function
var constrain = function(num,min,max){ return Math.min(Math.max(num, min), max); };

Step 4: Method Method » Proportional

I spring-boarded off of the simple method hoping make a best of both worlds equation. The idea here is to be fully proportional in all directions even diagonally despite the fact though you are moving a greater distance it has the same range as when you move vertically which is a smaller distance.

You end up with a scale of -200 to +200 in all directions in my examples I map that to ±100 because it represents the percentage of power going to each channel - however you will want to map it to whatever works in your use-case for your motor controller. For example if you are sending a PWM signal you might map that to 1000 to 2000 or if you are sending an analog signal you might map it to 0-255 and set the direction as boolean etc.

Arduino Example:

int leftThrottle = joystick_y + joystick_x; 
int rightThrottle = joystick_y - joystick_x;

// in some cases the max is 100, in some cases it is 200
// let's factor in the difference so the max is always 200
int diff = abs( abs(joystick_y) - abs(joystick_x) );
leftThrottle = leftThrottle < 0 ? leftThrottle - diff : leftThrottle + diff;
rightThrottle = rightThrottle < 0 ? rightThrottle - diff : rightThrottle + diff;

//Map from ±200 to ± 100 or whatever range you need
leftThrottle = map( leftThrottle, 0, 200, -100, 100);
rightThrottle = map( rightThrottle, 0, 200, -100, 100); //constrain
leftThrottle = constrain(leftThrottle, -100, 100);
rightThrottle = constrain(rightThrottle, -100, 100);

JavaScript Example:

var leftThrottle = joystick_y + joystick_x;
var rightThrottle = joystick_y - joystick_x;

// in some cases the max is 100, in some cases it is 200,
// let's factor in the difference so the max is always 200
var diff = Math.abs( Math.abs(joystick_y) - Math.abs(joystick_x) );
leftThrottle = leftThrottle < 0 ? leftThrottle - diff : leftThrottle + diff;
rightThrottle = rightThrottle < 0 ? rightThrottle - diff : rightThrottle + diff;

//Map from ±200 back down to ±100 or whatever you need
leftThrottle = map(leftThrottle, -200, 200, -100, 100);
rightThrottle = map(rightThrottle, -200, 200, -100, 100);
//constrain leftThrottle = constrain(leftThrottle, -100, 100);
rightThrottle = constrain(rightThrottle, -100, 100);

//some helper functions
var constrain = function(num,min,max){ return Math.min(Math.max(num, min), max); };
var map = function(num,inMin,inMax,outMin,outMax){
var p, inSpan, outSpan, mapped;
inMin = inMin + inMax;
num = num + inMax;
inMax = inMax + inMax;
inSpan = Math.abs(inMax-inMin);
p = (num/inSpan)*100;
outMin = outMin + outMax;
outMax = outMax + outMax;
outSpan = Math.abs(outMax - outMin);
mapped = outSpan*(p/100) - (outMax/2);
return mapped;
};