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 object Int = NeoPixel number hexCol = 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.

Comments

© 2025 Swee, Savalio, and other SweeBlogs contributors.
Made with Jekyll