Joysticks

Almost everyone is familiar with joysticks. They have been ubiquitous on game controllers for 40+ years.

But how do they work? How are They wired? How do we use them in a project?

The joystick in our kit is from Seeed Studio’s (three e’s is on purpose) Grove line of components. You can check their demo and helps files for tips that go beyond this post.

How they Work

Electrically most joysticks are a combination of two potentiometers and a button. Full game controllers expand on this basic pattern considerably.

The potentiometers are permanently arranged at 90 degrees (right angles) to one another allowing for input on the X- and Y-axes. The button can be integrated with the joy stick control as it is with this grove device or positioned separately.

We have already seen how to read values from a POT with analogRead() and a BUTTON with digitalRead() — so the basic connections and code for using a joystick are really review!

The grove stick throws a clever bit of wiring at their joystick and the button is read in conjunction with the analogRead(). We will look at that in detail below.

Let’s explore.

Parts Required

For the basic setup we will connect the joystick directly to the Arduino.

  • joystick
  • connector ( grove plug to male headers)
  • Arduino

Video

in its way.

The Circuit

Grove uses a platform specific plug –called a grove plug — which has 4 pins. The cable that comes with the joystick packaging assumes you have a product called a grove shield which offers sockets for plugging devices. In our context the shield is overly constraining. So we will set the double plug connector that came with the joystick aside ( hang onto it, you never know when it will come in handy).

For this first test we will connect the joystick to the Arduino with the grove-to-header cable. These will be in the sensor bag — you should have several if these connectors.

Make the first connection. The grove plug inserts into the joystick with the lock facing out from the board.

Once connected flip the board over and look at the labels on the bottom. They are labelled X,Y,Vcc,Gnd.

The header pins are then connected as follows:
RED (Vcc) –> 5V
Black (GND) –> ground
Yellow (X) –> Ai
White (Y) –> Ai

Where:
Ai = any of the Analog input pins A0-A5

The power connections can go directly to the Arduino for quick setups. Or to a breadboard with power rails connected to 5V and GND for setup with multiple devices.

Add Code

Let’s begin by reading the analog inputs on X and Y and seeing how they react in the serial plotter.

As already indicated the X- and Y- axis are just potentiometers that are read with analogRead(). So the code that follows is just the basic analog input code set up for two circuits.

// tangible 2021, steve daniels

// simple read of grove joystick

int xPin = A1;
int yPin = A2;
int xVal, yVal;

void setup() {
  // put your setup code here, to run once:
  pinMode(xPin, INPUT);
  pinMode(yPin, INPUT);
  Serial.begin(9600);
}

void loop() {

  // put your main code here, to run repeatedly:
  xVal = analogRead(xPin);
  yVal = analogRead(yPin);

  Serial.print(xVal);
  Serial.print('\t');
  Serial.print(yVal);

  Serial.println();

}
What to Expect

Upload this code and open the serial plotter. Once you have scrolling traces move the joystick to it’s X (left -right) and Y (up-down) limits. Watch the traces.

Note: We can also refer to these positions by the cardinal directions of a compass rose: N,S,E,W.

You should see the traces for each axis extend to their limits. The exact shape will depend on how fast you are moving the joystick.

Physical orientation of the device will impact how you interpret the numbers. I am holding mine with the connector to the left.

In this case, moving my thumb right (away from the connector) causes the X-trace (blue) to go to max. Moving my thumb left — toward the connector causes the blue trace to go low.

Similarly, moving my moving my thumb up causes the Y-trace (red) to go to max. Moving my thumb down causes the trace to go low.

Seeking Corners
You can also move in two directions simultaneously — and sort out where you went by reading the two traces at the same time. Remember, X is blue; Y is red.

In the image above we can clearly see that we need to consider both traces simultaneously to determine the ‘corner’ we have pushed the joystick.

In code this could be sorted with if/and statements.

Interpreting With Thresholds

I find it easiest (at least initially) to interpret joystick data through the use of Thresholds.

So let’s look at 3 positions and the associated limits visible in the joystick traces. First notice that both axis are centered around roughly 512 — the midpoint of the analogRead() scale, not zero (0). Second notice that pushing the joystick to the cardinal directions leads to a max and min range of about 200 to 800. This range is narrower than a full analogRead() of 0-1023.

Given these boundaries, let’s set some thresholds for high and low values. As we did with thresholds previously we want to set the thresholds with in the range of the sensor — i.e. between min and max. Because the traces are very stable ( a characteristic of POTs ), we can set the thresholds closer to the max and min limits than we would if we averaged max or mix with the centre value. .

To add a threshold in code we have to add the variable declaration and the necessary Serial.print() lines. Since we want to see the output in the plotter, we will format our Serial outputs as print(), followed by tab (‘\t’), not println.

We will add the following above setup():

int maxThreshold = 700;
int minThreshold = 300;

Insert the following into loop(), as a part of the existing Serial.print statements, BEFORE the Serial.println():

Serial.print(maxThreshold);
Serial.print('\t');
Serial.print(minThreshold);
Serial.print('\t');

We can then make determinations of direction with a series of if statements.

  if ( xVal < minThreshold) Serial.print("LEFT");  
  if ( xVal > maxThreshold) Serial.print("RIGHT");
  if ( yVal > maxThreshold) Serial.print("UP");
  if ( yVal < minThreshold) Serial.print("DOWN");
  Serial.println();
Modified Code
// tangible 2021, steve daniels

// simple read with thresholds of grove joystick

int xPin = A1;
int yPin = A2;
int xVal, yVal;

int maxThreshold = 700;
int minThreshold = 300;

void setup() {
  // put your setup code here, to run once:
  pinMode(xPin, INPUT);
  pinMode(yPin, INPUT);
  Serial.begin(9600);
}

void loop() {

  // put your main code here, to run repeatedly:
  xVal = analogRead(xPin);
  yVal = analogRead(yPin);

  // for plotter 
  Serial.print(xVal);
  Serial.print('\t');
  Serial.print(yVal);
  Serial.print('\t');

  Serial.print(maxThreshold);
  Serial.print('\t');
  Serial.print(minThreshold);
  Serial.print('\t');

  Serial.println();

  // for monitor
  if ( xVal < minThreshold) Serial.print("LEFT");
  if ( xVal > maxThreshold) Serial.print("RIGHT");
  if ( yVal > maxThreshold) Serial.print("UP");
  if ( yVal < minThreshold) Serial.print("DOWN");
  
  Serial.println();


}
What to Expect

Upload the threshold code and this time open the serial monitor. You will see readings and labels mixed. If you want to only see the directions, comment out the blocks under the comment //for plotter that print the readings and thresholds. I am leaving them in because I still want to use the plotter.

In the serial plotter view you may see the direction labels in the top left corner. This is not readily controlled so it may not happen for you.

Read the BUTTON

The makers of this joystick added an interesting way to read the button. The button is wired such that when it is pressed the X-axis reading jumps to 1023; the maximum value of analogRead(). This means we can detect button presses with the basic wiring above and the addition of one more threshold.

Below is a trace of a the joystick when held in the RIGHT (East) position (small peaks) and when the button is pressed (tall peaks).

We can read this value directly since the button sets the analogRead() to 1023 — but let’s explore reading the button with thresholds.

We can add one more threshold to our code. This time we need the threshold between joystick max (800) and analogRead() max (1023). I am going with 950. We will also need one more if statement.

Above setup() add:

int buttonThreshold = 950;

In loop(), within the Serial statements add:

Serial.print(buttonThreshold);
Serial.print('\t');

And in loop() in the if statements:

  if ( xVal > buttonThreshold) Serial.print("BUTTON!");
Final code:
// tangible 2021, steve daniels 

// simple read, threholds and button  of grove joystick

int xPin = A1;
int yPin = A2;
int xVal, yVal;

int maxThreshold = 700;
int minThreshold = 300;

int buttonThreshold = 950;

void setup() {
  // put your setup code here, to run once:
  pinMode(xPin, INPUT);
  pinMode(yPin, INPUT);
  Serial.begin(9600);
}

void loop() {
  
  // put your main code here, to run repeatedly:
  xVal = analogRead(xPin);
  yVal = analogRead(yPin);

  // for plotter
  Serial.print(xVal);
  Serial.print('\t');
  Serial.print(yVal);
  Serial.print('\t');

  Serial.print(maxThreshold);
  Serial.print('\t');
  Serial.print(minThreshold);
  Serial.print('\t');
  Serial.print(buttonThreshold);
  Serial.print('\t');
  
  Serial.println();

  // for monitor
  if ( xVal < minThreshold) Serial.print("LEFT");
  if ( xVal > maxThreshold) Serial.print("RIGHT");
  if ( yVal > maxThreshold) Serial.print("UP");
  if ( yVal < minThreshold) Serial.print("DOWN");

  if ( xVal > buttonThreshold) Serial.print("BUTTON!");
  
  Serial.println();
  
}

What to Expect

Open both the plotter and the serial monitor to explore this variation.

In the plotter look for the new upper threshold and button presses exceeding that limit.

In the serial monitor, look for the word BUTTON when you press the joystick down (press firmly). It is nearly impossible to press perfectly straight down, so you will likely see some directions along with the BUTTON message.

Monitor view of button threshold code.

Notice here that you get both RIGHT and BUTTON messages printed. This indicates that two IF statements are true at the same time.

Let’s consider the trace one more time.

In this image the zones created by the thresholds are tinted. The area defined by the low threshold is orange, the center area is white, the max threshold region is yellow (and extends to the very top of the image) and the button threshold region is blue. The green area along the top arises because in this zone blue (button threshold) and yellow (max threshold) overlap.

Because of this overlap, when you press the button on the joystick the analogRead() value of the X-axis goes to 1023. It should be clear that 1023 is bigger than both the maxThreshold AND the buttonThreshold (green overlap in image above.) So both of the following IF statements are true.

if ( xVal > maxThreshold) Serial.print("RIGHT");
if ( xVal > buttonThreshold) Serial.print("BUTTON!");

So, with the code above, every button press triggers BUTTON AND RIGHT — even when you pushing the joystick in a different direction! This is common with multiple IF statements.

This needs a fix.

We need to constrain the yellow zone so that it does not cross into the blue region. This will ensure that the top section will be blue, not green.

Modify the IF statement that prints RIGHT such that is does not trigger when the sensor reading exceeds buttonThreshold.

if ( xVal > maxThreshold && xVal < buttonThreshold) Serial.print("RIGHT");

This line of code constrains the yellow zone to be between maxThreshold and buttonThreshold. When uploaded and viewed in the monitor, button presses only trigger the label BUTTON.

A color coded trace reflecting this newly constrained region is below.

Going Further

Advanced interpretation of joystick values will be dealt with in a later post.

Grove docs for this joystick