Weekly tutorial: Make a simple games console with Arduino
By Savalio
Sorry for not posting for a week! I got kinda busy after the winter break was over. In this tutorial I’ll teach you how to make a super simple games console.
1. Supplies
You will need the following components:
- An Arduino-compatible, I2C inclusive dev board (Xiao ESP32-S3 Sense in my case)
- An I2C OLED that is SSD1306 compatible: get on Adafruit
- Neokey board: get on Adafruit
- MX-compatible mechanical switches: get on Adafruit
- Keycaps for the switches (optional if you’re on a VERY tight budget or you’re a psycho): get on Adafruit
- Arduino IDE
- Some prior Arduino IDE experience.
This project was not optimized for the parts from other manufacturers, so you might need to do some debugging, like changing a library or changing an I2C address.
2. Setup
This project will require some simple wiring if you’re using Stemma QT, but you will need to solder if you want to use pins. Here’s a quick list of steps to connect:
- Get your dev board’s I2C pin position
- Place the key caps on the switches
- Insert the switches on the flatter side of the PCB by pushing the switches into the sockets
- Insert the Stemma QT cable into the Stemma QT socket (if using Stemma QT)
- Connect the other end of the cable into the I2C pins of the dev board (blue -> SDA; yellow -> SCL)
- Repeat the last 2 steps for the OLED
3. Code
Insert this code into your Arduino IDE:
#include "Adafruit_NeoKey_1x4.h"
#include "seesaw_neopixel.h"
#include "Adafruit_GFX.h"
#include "Adafruit_SSD1306.h"
Adafruit_NeoKey_1x4 neokey; // Creates the NeoKey object
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// The pins for I2C are defined by the Wire-library.
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3D ///< See datasheet for Address
#define NEOKEY_ADDRESS 0x30 // Change this as needed
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
int x = 2; // Integers for the sprite position
int y = 2;
void writeTextReset(String text, int x, int y) { // Function to save some time coding
display.clearDisplay(); // Clears the display
display.setCursor(x, y); // Sets the cursor to the x and y parameters
display.println(text); // Writes the text to the display
}
void setup() {
delay(1000); // Delay to initialize I2C
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); // Starts the display
// Show initial display buffer contents on the screen --
// the library initializes this with an Adafruit splash screen.
display.display();
delay(2000); // Pause for 2 seconds
// Clear the buffer
display.clearDisplay();
display.setTextSize(1); // Sets the text size
display.setTextColor(SSD1306_WHITE); // Draw white text
writeTextReset("Awaiting connection...", x, y);
display.display(); // Displays the text. This function is used to show the graphics that were prepared.
Serial.begin(115200); // Begins searial
//while (! Serial) delay(10); // Uncomment if you want to prevent the game to run without serial
delay(100);
if (! neokey.begin(NEOKEY_ADDRESS)) { // begin with I2C address, default is 0x30
Serial.println("Could not start NeoKey, check wiring?");
writeTextReset("Could not connect", x, y);
display.display();
while(1) delay(10);
}
Serial.println("NeoKey started!");
writeTextReset("Key's animation playing...", x, y); // Tells the user that the NeoKey animation is playing
display.display();
// Pulse all the LEDs on to show they're working
for (uint16_t i=0; i<neokey.pixels.numPixels(); i++) { // The animation itself
if (i == 0) {
neokey.pixels.setPixelColor(i, 0xFF0000); // Changes the NeoPixel's color
} else if (i == 1) {
neokey.pixels.setPixelColor(i, 0xFFFFFF);
} else if (i == 2) {
neokey.pixels.setPixelColor(i, 0x00FF00);
} else {
neokey.pixels.setPixelColor(3, 0x00FFFF);
}
neokey.pixels.show();
if (i == 0 | i == 1) {
delay(250);
} else {
delay(500);
}
neokey.pixels.setPixelColor(i, 0);
}
for (uint16_t i=0; i<neokey.pixels.numPixels(); i++) {
neokey.pixels.setPixelColor(i, 0x000000); // Turns off the LEDs
neokey.pixels.show();
delay(50);
}
writeTextReset("Key started! Key key: \n A = red/Go left \n B = white/Go up \n C = green/Go down \n D = blue/Go right", x, y); // Displays the controls on the display
display.display();
neokey.pixels.setPixelColor(0, 0xFF0000); // Shows the user which key is which
neokey.pixels.setPixelColor(1, 0xFFFFFF);
neokey.pixels.setPixelColor(2, 0x00FF00);
neokey.pixels.setPixelColor(3, 0x00FFFF);
neokey.pixels.show();
delay(5000);
}
void loop() {
uint8_t buttons = neokey.read();
// Check each button, if pressed, light the matching neopixel
if (buttons & (1<<0)) { // If the button 0 (A) is pressed, then...
Serial.println("Button A"); // Tell the Serial
neokey.pixels.setPixelColor(0, 0xFF0000); // Set the NeoKey color to red
neokey.pixels.show(); // Show the color
x -= 1; // Move the character in the right direction
display.display(); // Display is used in here and not at the end to remove flicker
} else { // If not...
neokey.pixels.setPixelColor(0, 0); // make sure that the neokey is off
neokey.pixels.show();
}
if (buttons & (1<<1)) {
Serial.println("Button B");
neokey.pixels.setPixelColor(1, 0xFFFFFF); // walter heisenberg white
neokey.pixels.show();
y -= 1;
display.display();
} else {
neokey.pixels.setPixelColor(1, 0);
neokey.pixels.show();
}
if (buttons & (1<<2)) {
Serial.println("Button C");
neokey.pixels.setPixelColor(2, 0x00FF00); // green
neokey.pixels.show();
y += 1;
display.display();
} else {
neokey.pixels.setPixelColor(2, 0);
neokey.pixels.show();
}
if (buttons & (1<<3)) {
Serial.println("Button D");
neokey.pixels.setPixelColor(3, 0x00FFFF); // blue
neokey.pixels.show();
x += 1;
display.display();
} else {
neokey.pixels.setPixelColor(3, 0);
neokey.pixels.show();
}
writeTextReset(":)", x, y);
delay(10); // Gives the dev board some time to show the NeoPixels
}
Add this chunk of code if you want to translate RGB into HEX:
// Input a value 0 to 255 to get a color value.
// The colors are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
if(WheelPos < 85) {
return seesaw_NeoPixel::Color(WheelPos * 3, 255 - WheelPos * 3, 0);
} else if(WheelPos < 170) {
WheelPos -= 85;
return seesaw_NeoPixel::Color(255 - WheelPos * 3, 0, WheelPos * 3);
} else {
WheelPos -= 170;
return seesaw_NeoPixel::Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
return 0;
}
What should happen:
- The display shows the Adafruit splash screen
- The same display shows text saying “Key’s animation playing…”
- After it finishes, the OLED shows the controls and NeoKey shows the color code for each button
- When the NeoPixels eventually turn off and you press the NeoKeys, a moving smiley face will appear.
- Try pressing different keys and key combos and see how the smiley moves.
Now we’re gonna go into the juicy stuff, which is
4. Code Explanation
First, We’ll dive into the code outside of the functions. If we scroll for a bit, you can see this line of code:
#define SCREEN_ADDRESS 0x3D
This defines the screen’s address, which is required if you want to use the module with I2C. This info is usually in:
- Product description
- Official guide
- Datasheet
Usually, the I2C have these weird solder pads.
You might think, “what is this for?”. The answer is changing the I2C address.
This is useful when you want to use multiple I2C modules at once, but one of the modules has the same I2C address as another module you want to use.
For example, you want to use an I2C time of flight sensor, but you’re also using a screen, and both of them have the 0x3D address, and with those solder pads, you can take any one of the modules and your all-mighty soldering iron and solder it on.
The solder pads I was talking about
Next, we have this:
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
This makes the screen object. Now let’s talk about our first function in the program:
void writeTextReset(String text, int x, int y) { // Function to save some time coding
display.clearDisplay();
display.setCursor(x, y);
display.println(text);
}
I made this function to save some time, and I think this is a good way to explain the display’s basic functions and text functions. First, we have two most important functions, these are:
display.display();
display.clearDisplay();
The first function displays EVERYTHING you put on the canvas after the boot or last clearDisplay call, which clears the display.
If you call the first function after the boot, it will show the Adafruit splash screen. Here’s the syntax for the functions:
dispObj.display();
dispObj.clearDisplay();
dispObj
: display object name
Now, let’s come back and talk about the display object. The display object is created with this syntax:
Adafruit_SSD1306 dispObjName(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, RESET_PIN);
Now, we’re writing. Literally! The functions for writing to the display are the same as if you were using serial.
The cursor function changes the position of the text and syntax looks like this:
dispObj.setCursor(x, y);
The function for changing the text color applies to all Adafruit display libraries and looks like this:
dispObj.setTextColor(str);
str
= String variable
And last but not least, we have the function that changes the size, the syntax looks like this:
dispObj.setTextSize(Int);
Int
= int variable
Now, that we finished with the display functions, we can talk about our dear friend: NeoKey! The first NeoKey’s function used is this:
Adafruit_NeoKey_1x4 neokey;
This defines the NeoKey object. Next we have this:
neokey.pixels.setPixelColor(i, 0xFF0000);
This changes the NeoKey’s color. The syntax looks like this:
neokey.pixels.setPixelColor(Int, hexCol);
neokey
= NeoKey objectInt
= NeoPixel numberhexCol
= Color (HEX)
Then, we have this:
neokey.pixels.show();
This shows the NeoPixel’s color that you set previously. To turn off the NeoPixels, you can just use the setPixelColor and write 0 in the color parameter.
And it looks like we came to the end! See ya next week.