Joystick Module Calibration and Press Detection
by Trevor Lee in Circuits > Arduino
433 Views, 3 Favorites, 0 Comments
Joystick Module Calibration and Press Detection
In order for a sketch to accurately detect the motion of a joystick, it is necessary to calibrate, or more precisely, adapt to the joystick in the software (i.e. the sketch).
In this post I hope to show how to code in sketch, to detect the min / max positions of the joystick.
Concepturally, it is very simple. Hence, to add more fun to this post, I will also show how I use some classes to detect joystick "directional presses". Here, "press" means pushing the joystick to the very end of a direction. I guess in many use cases, you are more interested in those "directional presses", than the actual readings from the joystick.
The Connections
Here an Arduino is used; however, you can choose any microcontroller board supported by Arduino IDE.
- Connect 5V of Arduino Nano to +5V pin of Joystick.
- Connect GND of Arduino Nano to GND pin of Joystick.
- Connect A2 of Arduino Nano to VRX pin of Joystick. Note that the microcontroller pin must support reading analog data.
- Connect A1 of Arduino Nano to VRY pin of Joystick. Note that the microcontroller pin must support reading analog data.
- Connect A0 of Arduino Nano to SW pin of Joystick.
The Sketch
#include <limits.h>
#define TRACK_PRESS
#define TRACK_PRESSED_IS_2D
const int VRX = A2;
const int VRY = A1;
const int SW = A0;
const int XYPressThreshold = 100;
bool trackedXYPressed2D = true;
const long XYPressAutoRepeatMillis = 0; // 0 means no auto repeat
#if defined(TRACK_PRESS)
class ButtonPressTracker
{
public:
ButtonPressTracker(uint8_t pin)
{
this->pin = pin;
this->pressed = false;
this->blackOutMillis = 0;
this->nextRepeatMillis = 0;
}
bool checkPressed(int repeat = 0)
{
return setPressed(digitalRead(this->pin) == 0, repeat);
}
bool checkPressedBypass()
{
setPressed(digitalRead(this->pin) == 0);
return pressed;
}
private:
bool setPressed(bool pressed, int repeat = 0)
{
if (repeat == 0)
{
this->nextRepeatMillis = 0;
}
long nowMillis = millis();
if (this->blackOutMillis != 0)
{
long diff = this->blackOutMillis - nowMillis;
if (diff < 0)
{
this->blackOutMillis = 0;
}
}
if (this->blackOutMillis == 0)
{
if (pressed != this->pressed)
{
this->pressed = pressed;
this->blackOutMillis = nowMillis + 50;
if (repeat != 0 && this->pressed)
{
this->nextRepeatMillis = nowMillis + repeat;
}
else
{
this->nextRepeatMillis = 0;
}
return this->pressed;
}
}
if (this->nextRepeatMillis != 0)
{
long diff = this->nextRepeatMillis - nowMillis;
if (diff < 0)
{
this->nextRepeatMillis = nowMillis + repeat;
return true;
}
}
return false;
}
private:
uint8_t pin;
bool pressed;
long blackOutMillis;
long nextRepeatMillis;
};
class JoystickPressTracker
{
public:
JoystickPressTracker(uint8_t pin, int minReading = 10, int maxReading = 1013)
{
this->pin = pin;
setMinMax(minReading, maxReading, true);
}
public:
void setMinMax(int minReading, int maxReading, bool forceReset = false)
{
if (forceReset || this->minReading != minReading || this->maxReading != maxReading)
{
if (maxReading < minReading)
{
int temp = maxReading;
maxReading = minReading;
minReading = temp;
this->reverseDir = true;
}
else
{
this->reverseDir = false;
}
this->maxReading = maxReading;
this->minReading = minReading;
this->pressedDir = 0;
this->pressedMillis = 0;
this->needReset = false;
this->nextRepeatMillis = 0;
this->autoRepeatDir = 0;
}
}
public:
int8_t checkPressed(int repeat = 0)
{
return setReading(analogRead(this->pin), repeat);
}
int readPressedBypass()
{
int reading = analogRead(this->pin);
setReading(reading);
return readingToPressedDir(reading);
}
int readBypass()
{
int reading = analogRead(this->pin);
setReading(reading);
return reading;
}
private:
int readingToPressedDir(int reading)
{
if (reading <= this->minReading)
{
if (this->reverseDir)
{
return 1;
}
else
{
return -1;
}
}
if (reading >= this->maxReading)
{
if (this->reverseDir)
{
return -1;
}
else
{
return 1;
}
}
return 0;
}
int8_t setReading(int reading, int repeat = 0)
{
if (repeat == 0)
{
this->nextRepeatMillis = 0;
this->autoRepeatDir = 0;
}
long nowMillis = millis();
int8_t oriPressedDir = this->pressedDir;
int pressedDir = readingToPressedDir(reading);
if (pressedDir != 0)
{
this->pressedDir = pressedDir;
}
else
{
this->pressedDir = 0;
this->nextRepeatMillis = 0;
this->autoRepeatDir = 0;
}
if (!this->needReset && this->pressedMillis != 0 && (this->pressedDir == oriPressedDir))
{
long diffMillis = nowMillis - this->pressedMillis;
if (diffMillis > 50)
{
this->pressedDir = 0;
this->pressedMillis = 0;
this->needReset = true;
if (repeat != 0 && oriPressedDir != 0)
{
this->nextRepeatMillis = nowMillis + repeat;
this->autoRepeatDir = oriPressedDir;
}
else
{
this->nextRepeatMillis = 0;
this->autoRepeatDir = 0;
}
return oriPressedDir;
}
}
else
{
if (this->pressedDir != 0)
{
if (this->pressedMillis == 0)
{
this->pressedMillis = millis();
}
}
else
{
this->pressedMillis = 0;
this->needReset = false;
}
}
if (this->nextRepeatMillis != 0)
{
long diff = this->nextRepeatMillis - nowMillis;
if (diff < 0)
{
this->nextRepeatMillis = nowMillis + repeat;
return this->autoRepeatDir;
}
}
return 0;
}
private:
int maxReading;
int minReading;
bool reverseDir;
private:
uint8_t pin;
int pressedDir;
long pressedMillis;
bool needReset;
long nextRepeatMillis;
int autoRepeatDir;
};
class Joystick2DTracker
{
public:
Joystick2DTracker(JoystickPressTracker &xTracker, JoystickPressTracker &yTracker) : xTracker(xTracker), yTracker(yTracker)
{
this->lastPressedMillis = 0;
}
public:
bool checkPressed(int repeat = 0)
{
int xPressed = xTracker.checkPressed(repeat);
int yPressed = yTracker.checkPressed(repeat);
if (xPressed != 0 || yPressed != 0)
{
long nowMillis = millis();
long diffMillis = nowMillis - this->lastPressedMillis;
if (diffMillis > 50)
{
this->checkResultXPressed = xTracker.readPressedBypass();
this->checkResultYPressed = yTracker.readPressedBypass();
this->lastPressedMillis = nowMillis;
return true;
}
}
else
{
return 0;
}
}
private:
JoystickPressTracker &xTracker;
JoystickPressTracker &yTracker;
public:
int checkResultXPressed;
int checkResultYPressed;
long lastPressedMillis;
};
const char *JoystickPressedToStr(int xPressed, int yPressed)
{
if (xPressed == 0 && yPressed != 0)
{
return yPressed == -1 ? "↑" : "↓";
}
if (xPressed != 0 && yPressed == 0)
{
return xPressed == -1 ? "←" : "→";
}
if (xPressed != 0 && yPressed != 0)
{
if (xPressed == -1)
{
return yPressed == -1 ? "↖" : "↙";
}
else
{
return yPressed == -1 ? "↗" : "↘";
}
}
return NULL;
}
ButtonPressTracker *btnTracker = NULL;
JoystickPressTracker *xTracker = NULL;
JoystickPressTracker *yTracker = NULL;
Joystick2DTracker *xyTracker = NULL;
#endif
void setup()
{
Serial.begin(115200);
if (VRX != -1)
{
pinMode(VRX, INPUT);
}
if (VRY != -1)
{
pinMode(VRY, INPUT);
}
if (SW != -1)
{
pinMode(SW, INPUT_PULLUP);
}
#if defined(TRACK_PRESS)
if (VRX != -1)
{
xTracker = new JoystickPressTracker(VRX);
}
if (VRY != -1)
{
yTracker = new JoystickPressTracker(VRY);
}
#if defined(TRACK_PRESSED_IS_2D)
if (xTracker != NULL && yTracker != NULL)
{
xyTracker = new Joystick2DTracker(*xTracker, *yTracker);
}
#endif
if (SW != -1)
{
btnTracker = new ButtonPressTracker(SW);
}
#endif
}
int minXPos = INT_MAX;
int maxXPos = -1;
int minYPos = INT_MAX;
int maxYPos = -1;
long lastShowMillis = 0;
void loop()
{
int xPos = 0;
if (VRX != -1)
{
xPos = analogRead(VRX);
}
int yPos = 0;
if (VRY != -1)
{
yPos = analogRead(VRY);
}
int btnState = 0;
if (SW != -1)
{
btnState = digitalRead(SW);
}
if (xPos < minXPos)
{
minXPos = xPos;
}
if (xPos > maxXPos)
{
maxXPos = xPos;
}
if (yPos < minYPos)
{
minYPos = yPos;
}
if (yPos > maxYPos)
{
maxYPos = yPos;
}
long nowMillis = millis();
bool show = (nowMillis - lastShowMillis) >= 500;
#if defined(TRACK_PRESS)
if ((maxXPos - minXPos) > 800)
{
xTracker->setMinMax(minXPos + XYPressThreshold, maxXPos - XYPressThreshold);
}
if ((maxYPos - minYPos) > 800)
{
yTracker->setMinMax(minYPos + XYPressThreshold, maxYPos - XYPressThreshold);
}
bool btnPressed = btnTracker->checkPressed(XYPressAutoRepeatMillis);
if (btnPressed)
{
show = true;
}
int xPressed = 0;
int yPressed = 0;
if (xyTracker != NULL)
{
if (xyTracker->checkPressed())
{
xPressed = xyTracker->checkResultXPressed;
yPressed = xyTracker->checkResultYPressed;
show = true;
}
}
else
{
xPressed = xTracker->checkPressed(XYPressAutoRepeatMillis);
yPressed = yTracker->checkPressed(XYPressAutoRepeatMillis);
if (xPressed || yPressed)
{
show = true;
}
}
if (show)
{
Serial.print("[ ");
if (btnPressed)
{
Serial.print("#");
}
else
{
const char *pressedChar = JoystickPressedToStr(xPressed, yPressed);
Serial.print(pressedChar == NULL ? "." : pressedChar);
}
Serial.print(" ] -- ");
if (!show)
{
Serial.println();
}
}
#endif
if (show)
{
Serial.print("X: ");
Serial.print(xPos);
Serial.print(" (");
Serial.print(minXPos);
Serial.print("-");
Serial.print(maxXPos);
Serial.print(")");
Serial.print(" / Y: ");
Serial.print(yPos);
Serial.print(" (");
Serial.print(minYPos);
Serial.print("-");
Serial.print(maxYPos);
Serial.print(")");
Serial.print(" / BTN: ");
Serial.println(btnState);
lastShowMillis = nowMillis;
}
}
In fact, if you disregard the code in the "#if defined(TRACK_PRESS)" blocks, you will have a much shorter sketch.
I guess if you are interested in looking into the code, you will easily see which part is which.
Running the Sketch Without TRACK_PRESS
In case you are only interesting in finding the min / max positions, you can comment out the line that defines TRACK_PRESS, like
// #define TRACK_PRESS
The sketch will print output to serial port with baud rate 115200. So, in order to see the output of the sketch, you will need a serial monitor. And the output looks like
X: 511 (0-508) / Y: 508 (0-508) / BTN: 1
X: 511 (0-1023) / Y: 508 (0-508) / BTN: 1
X: 511 (0-1023) /Y: 508 (0-508) / BTN: 1
X: 838 (0-1023) / Y: 0 (0-508) / BTN: 1
X: 4 (0-1023) / Y: 0 (0-508) / BTN: 1
X: 126 (0-1023) / Y: 1022 (0-1023) / BTN: 1
For example, the last line
- X: 126 (0-1023) means that the joystick X position is 126, with min / max 0 to 1023
- Y: 1022 (0-1023) means that the joystick Y position is 1022, with min / max 0 to 1023
- BTN: 1 means that the button [of the joystick] is not pressed
Running the Sketch With TRACK_PRESS
If you keep the line that defines TRACK_PRESS uncommented out, you will get output like
[ ↖ ] -- X: 0 (0-1023) / Y: 0 (0-1023) / BTN: 1
[ . ] -- X: 1005 (0-1023) / Y: 508 (0-1023) / BTN: 1
[ → ] -- X: 1023 (0-1023) / Y: 806 (0-1023) / BTN: 1
[ ↘ ] -- X: 1023 (0-1023) / Y: 1023 (0-1023) / BTN: 1
[ ↙ ] -- X: 0 (0-1023) / Y: 1023 (0-1023) / BTN: 1
[ . ] -- X: 0 (0-1023) / Y: 177 (0-1023) / BTN: 1
[ ↖ ] -- X: 0 (0-1023) / Y: 0 (0-1023) / BTN: 1
[ . ] -- X: 511 (0-1023) / Y: 507 (0-1023) / BTN: 1
[ . ] -- X: 495 (0-1023) / Y: 507 (0-1023) / BTN: 1
[ # ] -- X: 471 (0-1023) / Y: 507 (0-1023) / BTN: 0
[ ↖ ] -- X: 0 (0-1023) / Y: 0 (0-1023) / BTN: 0
[ # ] -- X: 511 (0-1023) / Y: 507 (0-1023) / BTN: 0
The latter part of the output line is the same as previous run. Nevertheless, the fomer part is new. It shows what "action" is detected from the joystick.
The symbol inside the square brackets shows what is detected from the joystick
- . means nothing from the joystick
- # means the button [of the joystick] is pressed
- ↑, ↗, →, ↘, ↓, ↙, ←, ↖ refers to the detected "direction press"
It is important to note that the initial "directional press" detection may not be very accurate, since it needs to detect the joystick min / max ranges before the "directional press" detection can be more accurate. In order to achieve this "calibrate" purpose, you can simply make a few circles with the joystick.
One more thing. Actual button [of the joystick] press / click detection is done by a different class, which is similar to the one used, as mentioned in my previous post Button Click Counting With Virtual 7 Segment Display.
Enjoy!
Hope this little post can help someone somehow.
If you are interested, you may want to take a look at my post on an application of joystick "directional press" detection -- Adaptation of "Pocket Computer" With DumbDisplay. In fact, I am coming up with another one similar to this. Until then, enjoy!
Peace be with you! Jesus loves you! May God bless you!