Introduction

Having two motors to drive on a bot, how can one make it turn left? Or right? The premisse is quite simple:

  • To turn right while going forward, the left motor should drive faster than the right one.
  • To turn left while going forward, the right motor should drive faster than the left one.

The inverse happens when the bot is driving backwards and the same can be acheived, just in a different perspective, by driving one motor slower than the other.

The most basic approach to the premisse exposed above would be to, when aiming to turn the bot, drive one motor and stop the other one. In pseudo code would be something like:

// To turn right
motor_left(ON);
motor_right(OFF);
 
// To turn left
motor_left(OFF);
motor_right(ON);

The above idea could somehow work, but it would be extremely unlikely to produce any satisfatory outcome. The driving wouldn’t be smooth and it would highly depend on the time the bot has configured to turn.

Smoother driving

To achieve a smoother driving, the bot would have to be able to turn slowly or quickly, depending on the controller’s input.

From now on, let’s use a scale of 0 to 100 to materialize the velocity of the motors, where 0 is when a motor is not spinning and 100 is when it’s spinning at maximum speed.

Changing just the speed of each motor, it could be possible to take the last pseudo code example and tweak it a bit:

// To turn right
motor_left(100);
motor_right(50);
 
// To turn left
motor_left(50);
motor_right(100);

Instead of turning off the motor, it could remain powered, but spinning less than the other. However, this would result in the same conclusion as in the previous section.

Using a 2 dimensions axis direction

Ideally, one would want to be able to change the speed of motor_left and motor_right based on a input. Conceptually speaking, a two axis controller, such a joystick, would fit within the requirements. Both axis, X and Y could be used to tweak the spinning speed of each motor.

Let’s take a look at the 2D X and Y axis:

2D axis

Following what has been said, the small circle in red should lead to a slow forward right turn and the circle in blue should lead to a faster forward right turn.

Essentially, the one in red could have the left motor at full speed and the right one at slightly less speed.

AxisRedExample

By contrast, the blue example would still have the left motor at full speed, but the right one would be slower. This results in a quicker turn to the right.

Axis Blue Example

Slow down

The goal has always been to slow down one of the motors, but where should the speed be set from?

By looking at the 2 axis scenario, when the right motor speed decreases, so does the value of Y. Essentially, the speed of the right motor, can be based on something that relies on the axis Y.

Both arrows, the red and blue, materialize the maximum speed of the left motor and can be interpreted as hypotenuses. When one of the hypotenuses has a angle of 90 degress, both motors spin at the same speed. Therefore, the speed of the right motor should be based on the opposite side of a triangle:

Opposite side of the triangle

Opposite site turn

The desired opposite side Y value can be measured by:

#define SPEED_VALUE 100
 
uint8_t motor_left_speed = SPEED_VALUE;
 
double z = atan2(y, x);
uint8_t motor_right_speed = SPEED_VALUE * sin(z); // Always eq or less than 100

Info

The double variable z is in radians, not degrees.

This ensures the bot has a smoother turn and its behaviour is based on a Joystick controller, the direction of the bot is pretty much figured out. However, this is not enough — there is something missing.

Same direction, different speed

Instead of focusing only on one motor, the focus should be on both. The direction is set: the bot will turn left or right based on a turn angle. But, what if the same turn needs to performed quicker? Or slower?

There is a need for one more Joystick – one which only controllers how fast a angle based turn should be done.

Rad Speed Example

The speed should be based on this newly added joystick, while the direction (how the bot turns) should be managed by the second joystick.

int speed = analogRead(JOYSTICK_1_SPEED);
int direction_y = analogRead(JOYSTICK_2_Y);
int direction_x = analogRead(JOYSTICK_2_X);
 
double z = atan2(y, x);
uint8_t speed_sin_y = speed * sin(z);
// To turn right
motor_left(speed);
motor_right(speed_sin_y);
 
// To turn left
motor_left(speed_sin_y);
motor_right(speed);

Building a bot controller

With all that being said, I jumped into turning all this theory into reality by building a bot controller (e.g. “RC”) with two joysticks.

Top view of the controller

Other view of the controller

Hardware

The controller is battery (LiPo) powered and uses the ESPNow wireless protocol as its communication protocol. Here is the list of components:

Software

As stated previously, the joysticks are not the best. Their read analog voltage, from both axis, differs from one to another. To mitigate the diference, I’ve introduced a CALIBRATION_DELTA.

#include <Arduino.h>
#include <stdint.h>
#include <esp_now.h>
#include <WiFi.h>
 
#define GPIO_SPEED GPIO_NUM_36
#define GPIO_DIR_X GPIO_NUM_39
#define GPIO_DIR_Y GPIO_NUM_35
#define GPIO_LED GPIO_NUM_2
 
#define ADC_RESOLUTION 4096
 
#define CALIBRATION_DELTA (25) // The lower the Vref, the bigger should be this number.
#define UPDATE_DELTA (5)
 
struct coor
{
  int8_t speed;
  int8_t y;
  int8_t x;
} __attribute__((packed));
 
// Note: Attention: the bit 0 of the first byte of MAC address can not be 1.
// For example, the MAC address can set to be “1a:XX:XX:XX:XX:XX”, but can not be “15:XX:XX:XX:XX:XX”.
esp_now_peer_info_t peer_info = {
    .peer_addr = {0x1A, 0xFF, 0x00, 0xFF, 0x00, 0xFF},
    .channel = 0,
    .encrypt = false};
 
static coor coord;
 
void setup()
{
  WiFi.mode(WIFI_STA);
 
  if (esp_now_init() != ESP_OK || esp_now_add_peer(&peer_info) != ESP_OK)
  {
    return;
  }
 
  pinMode(GPIO_SPEED, INPUT);
  pinMode(GPIO_DIR_Y, INPUT);
  pinMode(GPIO_DIR_X, INPUT);
  pinMode(GPIO_LED, OUTPUT);
 
  digitalWrite(GPIO_LED, 0);
}
 
void loop()
{
  int speed = analogRead(GPIO_SPEED);
  int y = analogRead(GPIO_DIR_Y);
  int x = analogRead(GPIO_DIR_X);
 
  speed = map(speed, 0, ADC_RESOLUTION, -100, 100);
  y = map(y, 0, ADC_RESOLUTION, -100, 100);
  x = map(x, 0, ADC_RESOLUTION, -100, 100);
 
  // To deal with calibration issues.
  if ((0 < speed && speed < CALIBRATION_DELTA) || (0 > speed && speed > -CALIBRATION_DELTA))
  {
    speed = 0;
  }
  if ((0 < y && y < CALIBRATION_DELTA) || (0 > y && y > -CALIBRATION_DELTA))
  {
    y = 0;
  }
  if ((0 < x && x < CALIBRATION_DELTA) || (0 > x && x > -CALIBRATION_DELTA))
  {
    x = 0;
  }
 
  if ((abs(coord.speed - speed) > UPDATE_DELTA) || (abs(coord.y - y) > UPDATE_DELTA) || (abs(coord.x - x) > UPDATE_DELTA))
  {
    coord.speed = speed;
    coord.y = y;
    coord.x = x;
 
    esp_err_t result = esp_now_send(peer_info.peer_addr, (uint8_t *)&coord, sizeof(struct coor));
    if (result != ERR_OK)
    {
      if (result == ESP_ERR_ESPNOW_NO_MEM)
      {
        delay(10);
      }
      digitalWrite(GPIO_LED, 1);
    }
  }
}

I’ve also mapped the read analog voltage from 0 until 4096 to -100 until 100. The mapped values help materialize the value of each read value into a 2 axis (X and Y) graph, but they are not necessarly mandatory for the end goal.

Design

The enclosure to glue everything together is a 12cm x 9cm PLA printed design with a border radius of 2cm.

Final Design

In case you are interested in the project, please check the project’s repository and feel free to build your own controller!

That’s all folks! Have safe drives ;)