/ 52 weeks of pianaloganalog to digitaljoystick

Connecting an Analog Joystick to the Raspberry Pi (and using it with an RGB LED to simulate a color wheel)

One of the coolest things about the Raspberry Pi is its GPIO pins. They’re just sitting there, waiting to be connected to all kinds of useful peripherals so your Pi can interact with the world around it.

Power an LED to signal the userPlace a button in the path of a circuit and detect when a user presses it. Attach sensors to read temperature and humidity, and plug other cards like the Sense HAT over top of the pins.

A few months ago, I got a set of 37 sensor modules on Amazon. I knew they wouldn’t directly interface with the Pi, but that it was entirely possible to do it, so I put them aside for later.

Well, I finally decided to pull one out, and thought the mini-joystick might offer some interesting… opportunities. :)

Materials

There are a few things you’ll need on hand before doing this. I’ll list them here, but read the entire page before bothering to buy anything… make sure it sounds like something you want to try!

CanaKit Raspberry Pi 3 Ultimate Starter Kit – 32 GB Edition



The Pi comes with nothing else, not even an adapter. If you don’t have a Pi yet, buy a kit that has everything you’ll need to get started, including a case, adapter, LEDs (including an RGB LED) and wires, a breadboard, memory card, etc. If you buy a second Pi in the future, you won’t need a kit like this again, since you’ll already have the basic components.

Long Breadboard, 3pk

Optional, but once you see the pics of my setup below, you’ll understand why a longer board makes things easier. Most kits come with one half that size. I linked to this one because I bought it and the boards work well. Be aware that if you want to use a T-Cobbler, like the one in the CanaKit kit, it won’t work on these boards without modifying them slightly. (I had to detach the side rails, file down the small notches that snap them into the center board, and then glue the rails back on in the position I needed them.)

Kuman 37 Sensor Module Kit for Arduino Uno

I bought this kit and it’s working well so far. It comes with a joystick control, as well as a load of other sensors and input devices. You can also look for one by itself and it may be cheaper. My guess is that most of these little joystick controls are made very similarly, and that you’ll be able to use any of them with a few minor adjustments, but I can only attest to what I did with what I’ve got.

The only issue is that it comes with zero documentation, and you may be able to find a kit that comes with a nice manual. Otherwise, you’ll be doing some googling to figure out the wiring for each control. I found a link to instructions for each module in the Q&A section of the Amazon page (it’s a zip file), but they don’t provide a large amount of detail.

Phantom YoYo Jumper wire M/F male to female 200mm length (50pcs/pack)

You’ll need a few of these to connect the joystick to the breadboard. I pulled 5 random wires out of the set to use on this project, and they all worked great.

Adafruit MCP3008 – 8-Channel 10-Bit ADC With SPI Interface [ADA856]

This tiny chip bridges the gap between our analog control and the Pi. It’s cheaper directly from Adafruit for only a few bucks, but shipping ends up making it more expensive, especially if you have Amazon Prime. If you’re buying several instead of just one like me, consider Adafruit’s site. (And here’s the MCP3008 Datasheet if you’re interested.)

Interfacing with Analog Controls

The joystick is an analog control, consisting of two potentiometers that send a variable voltage depending on the position of the joystick (here’s a video that shows how they work), and it won’t just connect directly to the GPIO pins on the Pi. If your joystick can be pressed down like mine can, then *that *button just has an on/off state and can be connected directly to any regular GPIO pin. But I’ll wire it up same as the potentiometers, since that’s what the articles linked below do as well.

To get it to work, you’ll need to learn about the SPI bus protocol and how to enable it on the Pi, then wire up a small chip that uses SPI to bridge the gap between analog controls and the Pi. Fortunately, there’s a set of very helpful tutorials for doing just this, and you’ll find them on the Raspberry Pi Spy website.

First, you’ll learn how to enable the Serial Peripheral Interface (SPI) bus on certain GPIO pins. Method 1 worked fine for me – you just open up a config screen in Raspbian and select the SPI option.

Then you’ll need to wire up the MCP3008 chip correctly. That’ll provide the bridge between the joystick and your Pi. The “using a joystick” article linked above will walk you through it. You don’t *need *to read the “analogue sensors” article, but it’s got some helpful info in it that’s not in the other one. I suggest reading both.

Also, for changing colors on the RGB LED, you may want to learn more about pulse-width modulation (PWM).

Here are some pictures and a diagram of my setup, which hopefully will help if you get stuck, although Matt provides a good set of pics in his article too. There’s some additional stuff in my circuit that’s not in his, namely the RGB LED and resistors/wires to make it work. I used a 100Ω resistor for red and 220Ω for green and blue, same as here.















Fritzing Diagram

joystick-color-wheel

If you’d like, you can download the original Fritzing file and play around with it. The Fritzing site also has loads of diagrams that people have shared, which you can check out too. If you’re trying to connect a particular component, you might find something in there to help you out.

Reading Input

You should’ve already verified that Python Spidev (pi-spydev) was installed after you enabled SPI. We’ll need that for reading input from the analog device.

Since I’ve been messing with an RGB LED lately, I thought it’d be interesting to map the position of the joystick to the RGB color wheel and then light up the LED appropriately. Imagine the X-axis running horizontal above Blue and Green, and the Y-axis running vertical through Red.

rgb_color_wheel_400px

Here’s the code in its entirety, or check it out on GitHub.

import math
import RPi.GPIO as GPIO
import spidev
 
# Open SPI bus
spi = spidev.SpiDev()
spi.open(0, 0)
 
# Define sensor channels (3 to 7 are unused)
mcp3008_switch_channel = 0
mcp3008_x_voltage_channel = 1
mcp3008_y_voltage_channel = 2
 
# Define RGB channels
red_led = 36
green_led = 31
blue_led = 37
 
 
def read_spi_data_channel(channel):
    """
    Read in SPI data from the channel and return a coordinate position
    :param channel: integer, between 0-7
    :return: integer, between 0-1023 indicating joystick position
    """
 
    adc = spi.xfer2([1, (8+channel) << 4, 0])
    return ((adc[1] & 3) << 8) + adc[2]
 
 
def convert_coordinates_to_angle(x, y, center_x_pos, center_y_pos):
    """
    Convert an x,y coordinate pair representing joystick position,
    and convert it to an angle relative to the joystick center (resting) position
    :param x: integer, between 0-1023 indicating position on x-axis
    :param y: integer, between 0-1023 indicating position on y-axis
    :param center_x_pos: integer, indicating resting position of joystick along x-axis
    :param center_y_pos: integer, indicating resting position of joystick along y-axis
    :return: integer, between 0-359 indicating angle in degrees
    """
 
    dx = x - center_x_pos
    dy = y - center_y_pos
    rads = math.atan2(-dy, dx)
    rads %= 2 * math.pi
    return math.degrees(rads)
 
 
def adjust_angle_for_perspective_of_current_led(angle, led):
    """
    Take the current LED into account, and rotate the coordinate plane 360 deg to make PWM calculations easier
    :param angle: integer, between 0-359 indicating current angle of joystick position
    :param led: 'R', 'G', 'B', indicating the LED we're interested in
    :return: integer, between 0-359 indicating new angle relative to the current LED under consideration
    """
 
    led_peak_angle = 90 if led == 'R' else (210 if led == 'B' else 330)
    return ((angle - led_peak_angle) + 360) % 360
 
 
def calculate_next_pwm_duty_cycle_for_led(angle, led):
    """
    Calculate the next PWM duty cycle value for the current LED and joystick position (angle)
    :param angle: integer, between 0-359 indicating current angle of joystick position
    :param led: 'R', 'G', 'B', indicating the LED we're interested in
    :return: integer, between 0-100 indicating the next PWM duty cycle value for the LED
    """
 
    angle = adjust_angle_for_perspective_of_current_led(angle, led)
    if 120 < angle < 240:
        return 0
    elif angle <= 120:
        return 100 - (angle * (100 / 120.0))
    else:
        return 100 - ((360 - angle) * (100 / 120.0))
 
 
def is_joystick_near_center(x, y, center_x_pos, center_y_pos):
    """
    Compare the current joystick position to resting position and decide if it's close enough to be considered "center"
    :param x: integer, between 0-1023 indicating position on x-axis
    :param y: integer, between 0-1023 indicating position on y-axis
    :param center_x_pos: integer, indicating resting position of joystick along x-axis
    :param center_y_pos: integer, indicating resting position of joystick along y-axis
    :return: boolean, indicating whether or not the joystick is near the center (resting) position
    """
 
    dx = math.fabs(x - center_x_pos)
    dy = math.fabs(y - center_y_pos)
    return dx < 20 and dy < 20
 
 
def main():
    """
    Initializes GPIO and PWM, then sets up a loop to continually read the joystick position and calculate the next set
    of PWM value for the RGB LED. When user hits ctrl^c, everything is cleaned up (see 'finally' block)
    :return: None
    """
 
    # Center positions when joystick is at rest
    center_x_pos = 530
    center_y_pos = 504
 
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup([red_led, green_led, blue_led], GPIO.OUT, initial=GPIO.LOW)
 
    pwm_r = GPIO.PWM(red_led, 300)
    pwm_g = GPIO.PWM(green_led, 300)
    pwm_b = GPIO.PWM(blue_led, 300)
 
    pwm_instances = [pwm_r, pwm_g, pwm_b]
 
    for p in pwm_instances:
        p.start(0)
 
    try:
        while True:
            # If joystick switch is pressed down, turn off LEDs
            switch = read_spi_data_channel(mcp3008_switch_channel)
            if switch == 0:
                for p in pwm_instances:
                    p.ChangeDutyCycle(0)
                continue
 
            # Read the joystick position data
            x_pos = read_spi_data_channel(mcp3008_x_voltage_channel)
            y_pos = read_spi_data_channel(mcp3008_y_voltage_channel)
 
            # If joystick is at rest in center, turn on all LEDs at max
            if is_joystick_near_center(x_pos, y_pos, center_x_pos, center_y_pos):
                for p in pwm_instances:
                    p.ChangeDutyCycle(100)
                continue
 
            # Adjust duty cycle of LEDs based on joystick position
            angle = convert_coordinates_to_angle(x_pos, y_pos, center_x_pos, center_y_pos)
            pwm_r.ChangeDutyCycle(calculate_next_pwm_duty_cycle_for_led(angle, 'R'))
            pwm_g.ChangeDutyCycle(calculate_next_pwm_duty_cycle_for_led(angle, 'G'))
            pwm_b.ChangeDutyCycle(calculate_next_pwm_duty_cycle_for_led(angle, 'B'))
 
            # print("Position : ({},{})  --  Angle : {}".format(x_pos, y_pos, round(angle, 2)))
 
    except KeyboardInterrupt:
        pass
 
    finally:
        for p in pwm_instances:
            p.stop()
        spi.close()
        GPIO.cleanup()
 
 
if __name__ == '__main__':
    main()

I’ve done more work than I usually do to comment the code, so the inputs, outputs, and purpose of these functions are as clear as possible.

Always Write Tests!

If you check out my joystick project repo on GitHub, you’ll see a separate file with tests in it. Testing your code is vitally important. I found a bug in calculate_next_pwm_duty_cycle_for_led where I was performing integer division by accident, unintentionally discarding the fractional part of the result, which would’ve thrown everything off and been tough to track down.

If you’re an aspiring developer, get in the habit now. To run the tests in this project, you’ll need to install the DDT (Data-Driven Tests) package for Python unit testing via pip.

General Comments

I wrote the adjust_angle_for_perspective_of_current_led function to make calculations easier. Imagine a 360 degree circle overlaying the color wheel. Each color (red, blue, green) is separated by 120 degrees. So if red is at 90 (the top), then blue is at 210 and green is at 330. That function rotates the imaginary circle, placing the LED we’re concerned about at 0 degrees.

The is_joystick_near_center function was necessary because the joystick is not that accurate. Even when it’s sitting still, the readings coming off it fluctuate a bit. That’s not a huge deal when the joystick is positioned far away from the center, but imagine what happens when the position is near center and the X and Y coordinates keep jumping around the vertex of our “angle”. The angle varies wildly, so that when the joystick is “at rest”, the color flickers all over the place on the LED.

So instead, I just display white if you’re near center.

See it in Action

If you have a question about any of the code, leave a comment below and I’ll try to clarify. There’s one tricky piece in the read_spi_data_channel() function, and that’s the call to spi.xfer2(). Suffice to say, that’s the pi-spydev module doing work.

If you want, check out the spidev_module.c file and do a search for “xfer2″. It’s roughly 100 lines of C code.


Grant Winney

Grant Winney

I write when I've got something to share - a personal project, a solution to a difficult problem, or just an idea. We learn by doing and sharing. We've all got something to contribute.

Read More