Introduction
In this lab, you’ll build an alternative computer mouse using an Arduino Leonardo using a joystick to move the mouse left, right, up and down. You’ll use the joystick’s select button to replace the mouse button as well. You’ll see how to scale the analog outputs of the joystick to a reasonable range using the map()
function.
A joystick is typically made up of two potentiometers, one for the X axis and one for the Y axis. The potentiometers are mounted so that when the joystick is at rest in the center, the potentiometers are at the middle of their range. Some joysticks like the Thumb Joystick used here also have a pushbutton that you can press by pushing down on the stick. (Note: SparkFun and Parallax have equivalent models as well.)
What You’ll Need to Know
To get the most out of this lab, you should be familiar with the following concepts. You can check how to do so in the links below:
- Digital Input with Arduino
- Analog Input with Arduino
- How to control a mouse
- Mouse control with pushbuttons
Things You’ll Need
Figures 1-6 show the parts you’ll need for this exercise. Click on any image for a larger view.
Click on any image for a larger view
About mouse control
NOTE: The sketches contained in this lab will cause the Arduino Leonardo to take control of your mouse. Make sure they’re working properly before you add the mouse commands. The example doesn’t introduce the mouse commands until the end of the lab. Instead, messages are printed to the serial monitor to tell you what should happen. When you’ve run this and seen the serial messages occurring when you think they should, then you can add the mouse commands safely.
The sketches here will work on an Uno until you add the mouse commands. So you can test this on an Uno simply by commenting out any line that says Mouse.begin() or Mouse.move().
Note on Mouse, Keyboard, and Serial Ports
The Mouse and Keyboard libraries on the SAMD boards (MKRZero, MKR1xxx, Nano 33IoT) have an unusual behavior: using them changes the serial port enumeration. When you include the Keyboard library in a sketch, your board’s serial port number will change. For example, on MacOS, if the port number is /dev/cu.usbmodem14101
, then adding the Keyboard library will change it to /dev/cu.usbmodem14102
. Removing the Keyboard library will change it back to /dev/cu.usbmodem14101
. Similarly, if you double-tap the reset button to put the board in bootloader mode, the serial port will re-enumerate to its original number.
Windows and HID Devices
You may have trouble getting these sketches to work on Windows. On Windows, the default Arduino drivers must be uninstalled so the system can recognize the Arduino as both serial device and HID device. Read this issue and follow the instructions there.
Recovering From a Runaway HID (Mouse or Keyboard) Sketch
Programs which control your keyboard and mouse can make development difficult or impossible if you make a mistake in your code. If you create a mouse or keyboard example that doesn’t do what you want it to, and you need to reprogram your microcontroller to stop the program, do this:
Open a new blank sketch.
This sketch is your recovery:
void setup() {
}
void loop() {
}
Programming the board with this blank sketch removes your mistaken sketch and gives you control again. To do this, however, you need the current sketch to stop running. So:
Put the microcontroller in bootloader mode and upload the blank sketch
On the SAMD boards, you can do this by double-tapping the reset button. The on-board LED will begin glowing, and the bootloader will start so that you get a serial port enumeration, but the sketch won’t run. On the Leonardo and other 32U4-based boards, hold the reset down until you’ve given the upload command. The 32U4 bootloader will take a few seconds before it starts the sketch, so the uploader can take command and load your blank sketch.
Once you’ve got the blank sketch on the board, review the logic of your mistaken Keyboard or Mouse sketch and find the problem before uploading again.
Prepare the breadboard
Connect power and ground on the breadboard to power and ground from the microcontroller. On the Arduino module, use the 5V and any of the ground connections as shown below in Figure 7:
Figure 8. Breadboard view of an Arduino Nano connected to a breadboard. The +3.3 volts and ground pins of the Arduino are connected by red and black wires, respectively, to the left side rows of the breadboard. +3.3 volts is connected to the left outer side row (the voltage bus) and ground is connected to the left inner side row (the ground bus). The side rows on the left are connected to the side rows on the right using red and black wires, respectively, creating a voltage bus and a ground bus on both sides of the board.
Made with Fritzing
Add a pushbutton
Attach a pushbutton to digital pin 2 as shown below in Figure 10. For Arduino Nano view, see Figure 11. For the schematic view, see Figure 9. Connect one side of the pushbutton to 5 volts, and the other side of the pushbutton to a 10-kilohm resistor. Connect the other end of the resistor to ground. Connect the junction where the pushbutton and the resistor meet to digital pin 2. (For more on this digital input circuit, see the Digital Input Lab).
Figure 11. Breadboard view of an Arduino Nano connected to a pushbutton. The +3.3 volts and ground pins of the Arduino are connected by red and black wires, respectively, to the left side rows of the breadboard. +3.3 volts is connected to the left outer side row (the voltage bus) and ground is connected to the left inner side row (the ground bus). The side rows on the left are connected to the side rows on the right using red and black wires, respectively, creating a voltage bus and a ground bus on both sides of the board. The pushbutton is mounted across the middle divide of the solderless breadboard. A 10-kilohm resistor connects from the same row as pushbutton’s bottom left pin to the ground bus on the breadboard. There is a wire connecting to digital pin 2 from the same row that connects the resistor and the pushbutton. The top left pin of the pushbutton is connected to +3.3V.
Add a thumb joystick
Add a thumb joystick, as shown below in Figure 13. For the Arduino Nano view, see Figure 14. For the schematic view, see Figure 11. Attach the X out to analog input 0, the Y out to analog input 1, and the select button to digital input 3.
Program the module to read the pushbutton
Follow the same steps as you did in the first Mouse Control lab to read when the pushbutton on pin 2 is pressed. Your code should only print out a message when the button changes state. Similarly, set up a global variable to track whether or not you’re controlling the mouse, called mouseIsActive. Each time the pushbutton on pin 2 is pressed, change the state of this variable from false to true, just like you did in the first mouse control lab.
// Global variables:
int lastButtonState = LOW; // state of the button last time you checked
boolean mouseIsActive = false; // whether or not the Arduino is controlling the mouse
void setup() {
// initialize serial communication:
Serial.begin(9600);
pinMode(2, INPUT);
}
void loop() {
// read the first pushbutton:
int buttonState = digitalRead(2);
// if it's changed and it's high, toggle the mouse state:
if (buttonState != lastButtonState) {
if (buttonState == HIGH) {
// if mouseIsActive is true, make it false;
// if it's false, make it true:
mouseIsActive = !mouseIsActive;
Serial.print("Mouse control state: ");
Serial.println(mouseIsActive);
}
}
// save button state for next comparison:
lastButtonState = buttonState;
}
Program the Leonardo to read the Joystick
Add code to the main loop to read the joystick X and Y outputs and print them.
// Global variables:
int lastButtonState = LOW; // state of the button last time you checked
boolean mouseIsActive = false; // whether or not the Arduino is controlling the mouse
void setup() {
// initialize serial communication:
Serial.begin(9600);
pinMode(2, INPUT);
}
void loop() {
// read the first pushbutton:
int buttonState = digitalRead(2);
// if it's changed and it's high, toggle the mouse state:
if (buttonState != lastButtonState) {
if (buttonState == HIGH) {
// if mouseIsActive is true, make it false;
// if it's false, make it true:
mouseIsActive = !mouseIsActive;
Serial.print("Mouse control state: ");
Serial.println(mouseIsActive);
}
}
// save button state for next comparison:
lastButtonState = buttonState;
// read the analog sensors:
int sensor1 = analogRead(A0);
delay(1);
int sensor2 = analogRead(A1);
// print their values. Remove this when you have things working:
Serial.print(sensor1);
Serial.print(" ");
Serial.println(sensor2);
}
Map the X and Y output readings
The Mouse.move()
command has three parameters: the horizontal movement, the vertical movement, and the scroll wheel movement. All movements are relative, so Mouse.move(1,0,0);
moves one pixel to the right; Mouse.move(-1,0,0); moves one pixel to the left; Mouse.move(0,1,0);
moves one pixel down; and Mouse.move(0,-1,0);
moves one pixel up. Moving the mouse more than about 5 pixels in any direction is a very fast move. So the ideal range for the joystick is if it can move the cursor 5 pixels in any direction.
In order to do this, you need to scale the X and Y outputs from the default range that they return (0 to 1023) to a range from -5 to 5. You can do this using the map() function. Map takes five parameters: the input value, the range of the input value, and the desired output range, like so:
result = map(inputValue, inputMinimum, inputMaximum, outputMinimum, outputMaximum);
So, if your input range is 0 to 1023, and your output range is -5 to 5, you might map like this:
result = map(sensorReading, 0, 1023, -5, 5);
Add code to the main loop to map the X and Y outputs to a range from -5 to 5. Print the mapped values instead of the original sensor values.
// read the analog sensors:
int sensor1 = analogRead(A0);
delay(1);
int sensor2 = analogRead(A1);
int xAxis = map(sensor1, 0, 1023, -5, 5);
int yAxis = map(sensor2, 0, 1023, -5, 5);
// print their values. Remove this when you have things working:
Serial.print(xAxis);
Serial.print(" ");
Serial.println(yAxis);
NOTE: If your joystick defaults to -1 at rest on one axis or both, try adding 1 to the result of the map command. Try different output ranges and see what they do.
When you run this sketch, you should see the Mouse Control State message once every time you press the first pushbutton, and the values of the X and Y axes of the joystick, mapped to a range of -5 to 5. You still need to add in the select button on the joystick, however.
Add code to listen for the Joystick Select Button
The joystick select button is a digital input, but it’s wired differently than the buttons you saw in the Digital Lab or the Mouse Control With Pushbuttons Lab.
Note on pullup vs. pulldown resistors
It is, in fact, wired to connect to ground when you press it. To read it, then, you’d still use digitalWrite(), but you’d expect it to go low when pressed instead of high. And instead of a pulldown resistor like you’ve used in those other two labs, you’d use a pullup resistor, so that it’s connected to 5V when the switch is not open.
You can see the schematic for a pullup resistor circuit below in Figure 12.
The Arduino has built-in pullup resistors that you can use on the digital inputs. Most microcontrollers have these. When you set the pin to be an input using the pinMode() command, use the parameter INPUT_PULLUP instead of INPUT. This will activate the built-in pullup resistor on that pin, so you only have to attach a switch between the pin and ground.
The advantage of connecting your pushbutton to ground instead of 5V is that you can use the internal pullup resistor instead of having to add an external resistor. The disadvantage is that your switch logic is inverted: HIGH means the switch is open, and LOW means it is closed. Most of the time you can wire your switches either way, but when you have a switch that’s already wired to ground like the select button in the joystick, you have to use the internal pullups and inverted logic.
Previously, you wired the select button to digital input 3. To read the select button in this sketch, add a pinMode command to the setup that makes it an INPUT_PULLUP. Then in the main loop, check if the mosueIsActive variable is true. If it is, then use digitalRead() to read the select button and store it in a local variable called button2State.
Add the following at the end of the setup command:
// make pin 3 an input, using the built-in pullup resistor:
pinMode(3, INPUT_PULLUP);
Then at the end of the loop, add the following code:
if (mouseIsActive == true) {
// read the second pushbutton:
int button2State = digitalRead(3);
}
The select button’s behavior should be like the mouse control pushbutton’s behavior: you only want something to happen when it changes. When the select button changes from off to on, you want it to perform a mouse click. When it changes from on to off, you want it to perform a mouse release. So you need to check for when the select button changes, just like you do with the other pushbutton.
To check for the select button to change, set up a global variable called lastButton2State to save its previous state. Then set up an if statement in the main loop after you read it to see if the current state and the previous state are different. If they are different, add another if statement to see if the current state is HIGH or LOW. If it’s LOW, then print “Mouse press” (remember,its logic is inverted). If the current state is HIGH, then print “mouse press”.
This block of code will look a lot like the code you used for Mouse Control to track the state change of the pushbutton on pin 2.
Add the following line before the setup command:
int lastButton2State = LOW; // state of the other button last time you checked
Then inside the if statement that you added to check the mouseIsActive variable, add the following code (the if statement is shown here too):
if (mouseIsActive == true) {
// read the second pushbutton:
int button2State = digitalRead(3);
// if it's changed and it's high, toggle the mouse state:
if (button2State != lastButton2State) {
if (button2State == LOW) {
Serial.println("mouse pressed");
}
else {
Serial.println("mouse released");
}
}
// save second button state for next comparison:
lastButton2State = button2State;
}
When you run this code, you should see the words “mouse pressed” once when you press the select button, and “mouse released” when you release the select button. If it prints continually, you have an error.
When you’ve got that working, you’re ready to take control of the mouse.
Add commands to control the mouse
The Mouse.begin() command is called in the setup. It initializes mouse control from your Leonardo.
Include the Mouse library at the top of your sketch. Modify the setup by adding the command Mouse.begin(). Then, in the loop where check if mouseIsActive is true, add Mouse.move commands to move as needed. Also add Mouse.press() when the select button is pressed, and Mouse.release() when it is released.
At the end of the setup()
, add the following:
#include "Mouse.h"
// initialize mouse control:
Mouse.begin();
Then in the main loop, add the following lines to the if statement that checks mouseIsActive:
if (mouseIsActive == true) {
Mouse.move(xAxis, yAxis, 0);
// read the second pushbutton:
int button2State = digitalRead(3);
// if it's changed and it's high, toggle the mouse state:
if (button2State != lastButton2State) {
if (button2State == LOW) {
Serial.println("mouse pressed");
Mouse.press();
}
else {
Serial.println("mouse released");
Mouse.release();
}
}
That’s the whole sketch. When you run this, press the mouse control pushbutton, then move the joystick and press the select button. You should have full mouse control.
The full sketch for this can be found on the phys comp github repository, called MouseMoveSimpleJoystick.
In this sketch, you can see the value of scaling an analog sensor’s readings to match the output range you need. Since the joystick potentiometers are at rest in the middle of their range, scaling them from -5 to 5 gives you easy control of the mouse movement, which is relative. You can also see how reading the state change of a digital input is important. You’re doing it for two different buttons in this sketch.