The Joy of Python - Connecting a Joystick to the micro:bit!

The Joy of Python - Connecting a Joystick to the micro:bit!

November 22, 2019 by Kevin Banks

This installment of our micro:bit peripherals in Python series is an overview of analog joysticks, which are popular controller inputs for games, simulations, and more.

Check out the demonstration video here:

What's up with "Analog"? Why not just say "Joystick"?

Many early gaming systems had directional controls (UP/DOWN/LEFT/RIGHT) but they were digital in nature. For example, you could push the UP button and go upwards at full speed, or not push any direction and stand still, but there was no way to “go up slowly”.

These are technically called Digital Joysticks, and you can read more about using these sorts of digital inputs in the buttons and switches installment of this blog series.

An Analog Joystick is sensitive to how far you move the stick in any particular direction, and so you can go “a little up”, “a lot up”, and many other settings between those two extremes.

What's Inside an Analog Joystick?

If you read our Potentiometers article in this series, you already know that an analog joystick uses potentiometers internally. In fact, it uses two of them, one for left/right, and one for up/down.

The left/right direction is often referred to as the horizontal or X axis. The up/down direction is often referred to as the vertical or Y axis.

Possibly confusing matters, many of these joysticks include a “bonus” digital input – if you push “inward” on the joystick, you are actually pressing a tiny button.

If you are having trouble spotting the bonus button, look at Button A and Button B on your micro:bit and then look at the base of your joystick...you'll spot it!

Where can I get an Analog Joystick?

These little joysticks are readily available from multiple vendors, so it is hard to pick a single definitive source.

Probably the best thing to do is a web search (opens in a new tab).

This will bring up a results page with multiple sources you can choose from. With a little searching you can find them for under $5 each.

Quick Refresher - Reading Analog and Digital Signals on a Micro:bit

In our previous articles on Potentiometers and Buttons and Switches we learned how to read both of these types of inputs. Here is a quick refresher:

Any pin on the micro:bit that can take analog readings supports the function read_analog(), for example:

pin0.read_analog()

This function gives us a value between 0 and 1023. The value will be 0 if the voltage read on the pin is the same as GND. The value will be 1023 if the voltage read on the pin is 3.3 Volts. Any voltage in between GND and 3.3 Volts will return a value between 0 and 1023.

For example, if the voltage on pin0 was equal to 1.1 volts then you would expect pin0.read_analog() to return 341 ADC counts.

Any pin on the micro:bit that can take digital readings supports the function read_digital(), for example:

pin0.read_digital()

This function gives us a value of True or False, with True corresponding to a high voltage level, and False corresponding to a low voltage level. Which voltage level corresponds to “button is pressed” depends on how you wire the button up, but typically the button is connected so that pressing the button connects the pin to GND. This makes it so False corresponds to “pressed”, and True corresponds to “not pressed”.

Setting Up the Device

Assuming you want to use all of your joysticks capabilities, you will need to make 5 connections between the joystick and the micro:bit.

First, to use any potentiometer you must connect its two power pins to the micro:bit – the joystick controller is no exception. The joystick pin labelled GND will be connected to the micro:bit’s GND pin, and the joystick pin labelled +5V (or some might be labeled +3V, 3.3V, or even VCC) must be connected to the micro:bit 3V pin.

The joystick pin labelled VRx needs to go to a micro:bit pin capable of using read_analog(). In the example code provided with this blog post, we are assuming the VRx pin is connected to micro:bit pin0.

The joystick pin labelled VRy needs a similar analog input pin. We have chosen pin1 for this example.

The last connection is for that “bonus” switch we mentioned earlier. The joystick pin labelled SW (short for SWitch) needs to be connected to any micro:bit pin that supports read_digital(). We have chosen to connect to pin2 because it is easier to connect to than the other remaining pins, thanks to those big round holes on the micro:bit’s edge connector.

Making Sense of the Joystick Readings

The easiest way to understand how the read_analog() values correspond to different joystick positions is to simply print them from inside a while loop. Here is an example for examining horizontal movement:

from microbit import *
 
while True:
    val = pin0.read_analog()
    display.scroll(str(val))

Joysticks can vary between manufacturers, but typically you will see readings close to 0 when the joystick is moved all the way over to the left, and numbers very close to 1023 when the joystick is moved all the way to the right. When the joystick is in the middle (centered), the values will also be near the middle of the 0-1023 range, or around 511.

You can do similar experiments with vertical movement, by changing the code to use pin1.read_analog() instead of pin0.read_analog().

from microbit import *
 
while True:
    val = pin1.read_analog()
    display.scroll(str(val))

Typically you will see readings close to 0 when the joystick is moved all the way up (or “away from you” if the joystick is laying flat), and readings close to 1023 when the joystick is moved all the way down (or “towards you”). Once again, middle positions will correspond to middle values.

Of three joysticks tested at Firia Labs, two followed the “up gives you low values, down gives you high values” pattern and one was reversed: “down was low values, and up was high values”.

You will have to adapt your code to match the joystick you have.

Making the Readings Useful

This is somewhat dependent on how you want the controls in your game or simulation to work, but there are some common “recipes” that are typically used:

Recipe 1 – Emulation of a Joystick

Sometimes simple UP/DOWN/LEFT/RIGHT control is all you need. To accomplish this, it is usually sufficient to divide a given joystick axis into 3 intervals. Take a look at the following source code to see what I mean:

from microbit import *
 
while True:
    # X-Axis Left to Right is Low to High
    # Wiring connector is on left side of joystick
    value = pin0.read_analog()
    if value < 300:
        display.scroll("left")
    elif value < 600:
        display.scroll("right")
 
    # Y-Axis Up to Down is Low to High
    value = pin1.read_analog()
    if value < 300:
        display.scroll("up")
    elif value < 600:
        display.scroll("down")    

You can see in the code that the 0-1023 range is split up into three regions: 0-299, 300-600, and 601-1023.

If you examine the code closely, you will notice that directions are assigned to values from 0-299, and 601-1023, but not to the range 600-900. This middle region is referred to as a deadband or deadzone (it’s like a region where the “controls are dead”), and it is used to reduce the need for calibration, and to make the controls seem less “twitchy”. Consequently, if you want the controls to feel more “quick and responsive”, you might try reducing this deadband. Experiment with these thresholds and see which values give you the “feel” you want.

Recipe 2 - Finer-Grained Control

Of course, the whole point of using an analog joystick instead of a digital one is to be able to use more finesse in crushing your opponent moving your game character, so here is an example that divides the joystick’s range of motion into as many regions as there are rows and columns on the micro:bit’s 5x5 display.

from microbit import *
 
# Divide our movement range into sections
segment_size = 1024 / 5   # ADC range divided by screen range
 
while True:
     # X-Axis Left to Right is Low to High
    # Wiring connector is on left side of joystick
    value = pin0.read_analog()  
    col = int(value / segment_size)
 
    # Y-Axis Up to Down is Low to High
    value = pin1.read_analog()
    row = int (value / segment_size)
 
    display.set_pixel(col, row, 0)
    # allow time to SEE the pixel (otherwise looks "flickery")
    sleep(1)
    display.set_pixel(col, row, 0)

Here we are mapping joystick inputs directly into on-screen positions.

If you were controlling something like rocket thrusters you might want to divide the movement range into even more segments.

How to use the "Bonus" Button

Earlier we mentioned that when you pressed “inward” on the joystick, a small digital button got pressed.

Here is an expanded version of the earlier example, that shows reading all 3 inputs (2 analog inputs, 1 digital input).

from microbit import *
 
# Establish operating modes of the pins we will be using
 
pin0.read_analog()
 
pin1.read_analog()
 
pin2.read_digital()
# Since we are using a bare joystick, we have to add our own pull-up resistor
pin2.set_pull(pin2.PULL_UP)
 
while True:
    # X-Axis Left to Right is Low to High
    # Wiring connector is on left side of joystick
    value = pin0.read_analog()
    if value < 300:
        display.scroll("left")
    elif value < 600:
        display.scroll("right")
 
    # Y-Axis Up to Down is Low to High
    value = pin1.read_analog()
    if value < 300:
        display.scroll("up")
    elif value < 600:
        display.scroll("down") 
 
    # Pushing down on the joystick presses a single button
    value = pin2.read_digital()
    if value == 0:
        display.scroll("button")