9
\$\begingroup\$

I am looking for information about how to implement proper mixing of 2 analog joystick signals (X an Y axis) to control a dual differential motor drive ("tank like" drive) using a uC (ATMega328p in my case, but same should apply to any uC with ADC inputs and PWM outputs):

I have a analog stick, that gives 2 analog values:

(direction)X: 0 to 1023
(throttle)Y: 0 to 1023

enter image description here

Rest position is (direction and throttle neutral) 512,512
Throttle forward/direction left is 0,0
Full forward-full right is 1023,0
etc.

The motors are controlled by 2 H-bridge drivers, 2 PWM pins for each (Forward, backward), like so:
Left Motor: -255 to 255
Right Motor: -255 to 255
(positive values enable forward PWM pin, negative enable reverse PWM pin, 0 disables both)

The goal is to mix joystick analog signals to achive following response :

a)Throttle forward, direction neutral = vehicle moving forward
b)Throttle forward, direction left = vehicle moving forward and turning left
c)Throttle neutral, direction left = vehicle turning left IN PLACE that is right motor full forward, left motor full reverse

...and similarly for other combinations. Of course, the output should be "analog" that is, it should allow gradual transition from for example from option a) to b) to c).

The concept is:

http://www.lynxmotion.com/images/html/build123.htm

\$\endgroup\$
6
  • \$\begingroup\$ (1) Note that my basic algorithm allows speed control of 'turn in place' when joystick is pushed eg left by a % of full scale. (2) This requirement must have been re-solved many many times by now. The model community should have answers to this. (3) If the receiver translates the commands into track velocity using feedback the vehicle will behave about the same as ground conditions change. BUT if the commands are translated into motor power or drive voltage etc the vehicle performance will vary with ground conditions. - presumably 91) is preferable. \$\endgroup\$
    – Russell McMahon
    Sep 20, 2011 at 3:08
  • \$\begingroup\$ Russell, I googled a lot for the anwser and I have found a lot of ready to go motor controllers to connect directly to RC receiver but not much information about the algorithm inside. \$\endgroup\$ Sep 20, 2011 at 13:44
  • \$\begingroup\$ good day! renho a cousin who has been trying infantile paralysis and a construction of a wheelchair their programming worked well, but the output voltage is too low! help me! I'm using an arduino uno. \$\endgroup\$
    – user22423
    Apr 10, 2013 at 14:09
  • \$\begingroup\$ @Johnny welcome to Electronics.Stackexchange! Please look the FAQ to understand how this site works, and if you have a question to ask please use the specific button on the top-right corner of the page. \$\endgroup\$
    – clabacchio
    Apr 10, 2013 at 14:33
  • 1
    \$\begingroup\$ This is actually not possible as a continuous mapping, unless you force a stop or discontinuity between the spin in place mode and one of the forward or backward turning ones, ban spin-in-place, or turn "the wrong way" in reverse. In research of full sized earth moving machinery manuals, the only tracked one I found with a single joystick was stated as unable to spin in place with the tracks. \$\endgroup\$ Sep 21, 2016 at 5:06

3 Answers 3

4
\$\begingroup\$

"Proper" mixing is open to debate :-).

An issue is that you have to make decisions about how fast a track is moving under pure signals from a single pot and what to do when signals from the other pot are included. For example, if you push the FB (Forward-Backward pot fully forwards, and if both motors then run at full speed ahead, how do you deal with the addition of a small amount of LR (Left-Right) pot being added. To get rotation you have to have one track going faster that the other. So, if you are already running at maximum forwards speed on both motors you must decrease one or other track speed in order to turn. But, if you had been standing still you would have accelerated one or other track to achieve the same result.

So, all that said, here is a simple off-the-cuff starting solution out of my head which seems like good start.

If pots are mechanically independant then both can be at 100% simultaneously.
If both are on a joystick type arrangement, if Yaxis = 100% and Xaxis = 0%, then adding some B will usually reduce A. A joystick could be constructed where the above is not true, but these are unusual.
Assume that the joystick is of the type that increasing Y% when X = 100% will reduce X. Other assumptions can be made.

FB = front-back pot. Centre zero, +Ve for forward motion of pot

LR = Left right pot. Centre zero. +Ve for pot at right.

K is a scale factor initially 1.
If any result exceeds 100% then adjust K so result = 100% and use same K value for other motor also.

  • eg if Left motor result = 125 and Right motor result = 80 then.
    As 125 x 0.8 = 100, set K = 0.8. Then.
    Left = 125 x 0.8 = 100%. Right = 80 x 0.8 = 64%.

Then:

  • Left motor = K x (Front_Back + Left_Right)

  • Right motor = K x (Front_Back - Left_Right)

Sanity checks:

  • LR = 0 (centered), FB = full fwd -> Both motors run full forwards.

  • LR = full left, FB = 0 ->
    Left motor runs full backwards,
    Right motor runs full forwards.
    Vehicle rotates anti clockwise.

  • FB was 100%, Lr = 0%. Add 10% of LR to right.
    L = FB+LR = 100%- + 10% R = FB-LR = 100%- - 10%

If largest axis < 100%, scale until = 100%.
Then scale other axis by same amount.

\$\endgroup\$
8
  • \$\begingroup\$ Thank you Russell - I will try to implement this on my model setup. BTW, My joystick is able to keep full forward while panning it left to right and the other way around, it is very similar to this: static.sparkfun.com/images/products/09032-03-L_i_ma.jpg \$\endgroup\$ Sep 19, 2011 at 19:06
  • 1
    \$\begingroup\$ I am currently tasked with solving the same problem at work. I have a wii nunchuk 2-axis controller, and it needs to control 2 motors exactly as described in the question. I'm having a bit of trouble understanding the logic here. What exactly does k1/K1 refer to? One is lowercase and one is uppercase - are they different? What is +Ve? \$\endgroup\$
    – Tal
    Sep 20, 2016 at 22:53
  • 1
    \$\begingroup\$ Cool - thanks for the clarification. I needed this written in Python, so if I understand correctly, this should do it: pastebin.com/sWDakvLp. Does it look like I'm missing anything? Seems to work in my test environment - I'll need to actually connect it to the final motors I'll be using to know for sure. \$\endgroup\$
    – Tal
    Sep 21, 2016 at 17:17
  • 1
    \$\begingroup\$ 1) Motor speed is controlled by PWM, which only takes values from 0 - 100, which is why I used 100 as max value. 2) I use abs to find if scaling is needed (like you said) and to get the scale_factor. If I end up with a scale factor of 0.8 for example, and use it on a negative number, -125 * 0.8 = -100. Direction is maintained. I think it works, unless I am missing something. I still haven't had a chance to try it on the final motors - my boss will be building a test platform with motors attached that I'll be able to test on. \$\endgroup\$
    – Tal
    Sep 25, 2016 at 19:07
  • 1
    \$\begingroup\$ I wasn't sure if my code would actually work, so I set the previous pastebin link to expire after a week. Since it seems to work, here's a more permanent link with a few more comments if anyone comes across the problem again: pastebin.com/EKguJ1KP. I'd put this into an answer, but apparently I don't have enough rep to post an answer. All code is based on Russel McMahon's answer - credit goes to him - thanks Russel. \$\endgroup\$
    – Tal
    Oct 2, 2016 at 18:11
6
\$\begingroup\$

Here's a solution that doesn't require complicated if/else chains, doesn't reduce the power when moving full forward or rotating in place, and allows for smooth curves and transitions from moving to spinning.

The idea is simple. Assume the (x,y) joystick values are cartesian coordinates on a square plane. Now imagine a smaller square plane rotated 45º inside it.

example plane

The joystick coordinates give you a point in the larger square, and the same point superimposed in the smaller square gives you the motor values. You just need to convert coordinates from one square to the other, limiting the new (x,y) values to the sides of the smaller square.

There are many ways to do the conversion. My favorite method is:

  1. Convert the initial (x,y) coordinates to polar coordinates.
  2. Rotate them by 45 degrees.
  3. Convert the polar coordinates back to cartesian.
  4. Rescale the new coordinates to -1.0/+1.0.
  5. Clamp the new values to -1.0/+1.0.

This assumes the initial (x,y) coordinates are in the -1.0/+1.0 range. The side of the inner square will always be equal to l * sqrt(2)/2, so step 4 is just about multiplying the values by sqrt(2).

Here's an example Python implementation.

import math

def steering(x, y):
    # convert to polar
    r = math.hypot(x, y)
    t = math.atan2(y, x)

    # rotate by 45 degrees
    t += math.pi / 4

    # back to cartesian
    left = r * math.cos(t)
    right = r * math.sin(t)

    # rescale the new coords
    left = left * math.sqrt(2)
    right = right * math.sqrt(2)

    # clamp to -1/+1
    left = max(-1, min(left, 1))
    right = max(-1, min(right, 1))

    return left, right

The original idea for this method -- with a much more complicated transformation method -- came from this article.

\$\endgroup\$
3
  • \$\begingroup\$ this looks really easy, and i'm having a hell of a time validating it...can you take a look at my hacky code to try to validate? the left and right values seems discontinuous around 0. stackoverflow.com/q/66723576/3033594 \$\endgroup\$ Mar 25, 2021 at 18:35
  • \$\begingroup\$ @testname123 Unfortunately I can't help you with that, but the sample code I pasted here is copied directly from a working implementation, so I'm sure it works. github.com/pjwerneck/besiege-scripts/blob/master/bsg/… \$\endgroup\$ Mar 26, 2021 at 20:27
  • \$\begingroup\$ The calculation for atan2 is wrong. Should be t = math.atan2(x, y) \$\endgroup\$
    – d0n13
    Jul 28 at 21:21
1
\$\begingroup\$

Below is example of mixing algorithm implementation as described by Russel McMahon answer:

http://www.youtube.com/watch?v=sGpgWDIVsoE

//Atmega328p based Arduino code (should work withouth modifications with Atmega168/88), tested on RBBB Arduino clone by Modern Device:
const byte joysticYA = A0; //Analog Jostick Y axis
const byte joysticXA = A1; //Analog Jostick X axis

const byte controllerFA = 10; //PWM FORWARD PIN for OSMC Controller A (left motor)
const byte controllerRA = 9;  //PWM REVERSE PIN for OSMC Controller A (left motor)
const byte controllerFB = 6;  //PWM FORWARD PIN for OSMC Controller B (right motor)
const byte controllerRB = 5;  //PWM REVERSE PIN for OSMC Controller B (right motor)
const byte disablePin = 2; //OSMC disable, pull LOW to enable motor controller

int analogTmp = 0; //temporary variable to store 
int throttle, direction = 0; //throttle (Y axis) and direction (X axis) 

int leftMotor,leftMotorScaled = 0; //left Motor helper variables
float leftMotorScale = 0;

int rightMotor,rightMotorScaled = 0; //right Motor helper variables
float rightMotorScale = 0;

float maxMotorScale = 0; //holds the mixed output scaling factor

int deadZone = 10; //jostick dead zone 

void setup()  { 

  //initialization of pins  
  Serial.begin(19200);
  pinMode(controllerFA, OUTPUT);
  pinMode(controllerRA, OUTPUT);
  pinMode(controllerFB, OUTPUT);
  pinMode(controllerRB, OUTPUT);  

  pinMode(disablePin, OUTPUT);
  digitalWrite(disablePin, LOW);
} 

void loop()  { 
  //aquire the analog input for Y  and rescale the 0..1023 range to -255..255 range
  analogTmp = analogRead(joysticYA);
  throttle = (512-analogTmp)/2;

  delayMicroseconds(100);
  //...and  the same for X axis
  analogTmp = analogRead(joysticXA);
  direction = -(512-analogTmp)/2;

  //mix throttle and direction
  leftMotor = throttle+direction;
  rightMotor = throttle-direction;

  //print the initial mix results
  Serial.print("LIN:"); Serial.print( leftMotor, DEC);
  Serial.print(", RIN:"); Serial.print( rightMotor, DEC);

  //calculate the scale of the results in comparision base 8 bit PWM resolution
  leftMotorScale =  leftMotor/255.0;
  leftMotorScale = abs(leftMotorScale);
  rightMotorScale =  rightMotor/255.0;
  rightMotorScale = abs(rightMotorScale);

  Serial.print("| LSCALE:"); Serial.print( leftMotorScale,2);
  Serial.print(", RSCALE:"); Serial.print( rightMotorScale,2);

  //choose the max scale value if it is above 1
  maxMotorScale = max(leftMotorScale,rightMotorScale);
  maxMotorScale = max(1,maxMotorScale);

  //and apply it to the mixed values
  leftMotorScaled = constrain(leftMotor/maxMotorScale,-255,255);
  rightMotorScaled = constrain(rightMotor/maxMotorScale,-255,255);

  Serial.print("| LOUT:"); Serial.print( leftMotorScaled);
  Serial.print(", ROUT:"); Serial.print( rightMotorScaled);

  Serial.print(" |");

  //apply the results to appropriate uC PWM outputs for the LEFT motor:
  if(abs(leftMotorScaled)>deadZone)
  {

    if (leftMotorScaled > 0)
    {
      Serial.print("F");
      Serial.print(abs(leftMotorScaled),DEC);

      analogWrite(controllerRA,0);
      analogWrite(controllerFA,abs(leftMotorScaled));            
    }
    else 
    {
      Serial.print("R");
      Serial.print(abs(leftMotorScaled),DEC);

      analogWrite(controllerFA,0);
      analogWrite(controllerRA,abs(leftMotorScaled));  
    }
  }  
  else 
  {
  Serial.print("IDLE");
  analogWrite(controllerFA,0);
  analogWrite(controllerRA,0);
  } 

  //apply the results to appropriate uC PWM outputs for the RIGHT motor:  
  if(abs(rightMotorScaled)>deadZone)
  {

    if (rightMotorScaled > 0)
    {
      Serial.print("F");
      Serial.print(abs(rightMotorScaled),DEC);

      analogWrite(controllerRB,0);
      analogWrite(controllerFB,abs(rightMotorScaled));            
    }
    else 
    {
      Serial.print("R");
      Serial.print(abs(rightMotorScaled),DEC);

      analogWrite(controllerFB,0);
      analogWrite(controllerRB,abs(rightMotorScaled));  
    }
  }  
  else 
  {
  Serial.print("IDLE");
  analogWrite(controllerFB,0);
  analogWrite(controllerRB,0);
  } 

  Serial.println("");

  //To do: throttle change limiting, to avoid radical changes of direction for large DC motors

  delay(10);

}
\$\endgroup\$
4
  • \$\begingroup\$ Interesting, this code looks like it is feeding 2 analog pin to 2 different motor controller. I will try to adapt the code and modify for my settings. Arduino Uno + 1 Sabertooth driver board. 1 joystick to analog pinA0(x) pinA1(y) reading and passing values to PWM pin 10&3 going to S1&S2 of Sabertooth. I think I am close but I am getting confuse on how to setup the dipswitch on Sabertooth board. For now I am teting with switch setup to receive analog input, switch 4 is still in position for differential drive but will put it back to independant mode later for further testing. I think this orig \$\endgroup\$
    – user20514
    Mar 21, 2013 at 15:51
  • \$\begingroup\$ @user20514 welcome to electronics.stackexchange! As you might notice, this is not a forum but a Q&A site, therefore answers' space is not meant for discussion. Please be free to ask a new question if you have something to ask, or use comments to (indeed) comment about existing questions and answers. \$\endgroup\$
    – clabacchio
    Mar 21, 2013 at 16:03
  • 1
    \$\begingroup\$ @Kamil - The video is shown as private. Is it still avaiable ? youtube.com/watch?v=sGpgWDIVsoE \$\endgroup\$
    – Russell McMahon
    Sep 21, 2016 at 2:58
  • \$\begingroup\$ @RussellMcMahon reactivated :) \$\endgroup\$ Sep 28, 2016 at 21:21

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