Dual-Controller OLED Tic-Tac-Toe is a portable handheld gaming device specifically designed for two players or player versus bot. This project brings the classic game of Tic-Tac-Toe to a custom electronic platform built around an Arduino Uno board.
Project Goals:
The project consists of several integrated subsystems that handle input processing, game logic execution, and visual feedback:
Hardware Components:
Software Components:
This section describes the electronic components used to build the gaming console and their interconnections.
The hardware assembly has successfully transitioned from a conceptual layout to a fully wired, operational physical prototype on a breadboard.
All primary subsystems—including both analog controllers, the central microcontroller unit, the I2C communication display, and the PWM audio feedback module—are physically integrated.
Power distribution ($5\text{V}$ and $\text{GND}$) is stabilized across the breadboard rails, ensuring no voltage drops occur when the buzzer activates or the OLED screen renders full-white frames.
| Component | Quantity | Description / Role |
|---|---|---|
| Arduino Uno R3 | 1 | Central Processing Unit (ATmega328P microcontroller). |
| OLED Display (SSD1306) | 1 | 128×64 pixel monochrome screen, I2C interface, for rendering the game grid. |
| Joystick KY-023 | 2 | Analog modules for cursor control (X/Y) and selection (integrated button). |
| Passive Buzzer KY-006 | 1 | Provides audio feedback (PWM tones) for moves, errors, and victory. |
| Breadboard (400 points) | 1 | Platform for solderless electrical connections. |
| Dupont Wires | 1 set | Interconnecting wires for the components. |
To ensure optimal peripheral interaction without signal degradation, the ATmega328P pins were allocated based on their hardware-specific capabilities:
| Peripheral | Component Pin | Arduino Pin | Pin Type | Technical Justification |
|---|---|---|---|---|
| OLED Display | VCC | 5V | Power | Requires a stable 5V rail for the internal charge pump. |
| GND | GND | Power | Common ground reference for digital signals. | |
| SCL | A5 / SCL | Digital / I2C | Dedicated hardware clock line for I2C communication. | |
| SDA | A4 / SDA | Digital / I2C | Dedicated hardware data line for I2C communication. | |
| Joystick 1 (P1) | +5V | 5V | Power | Powers the internal potentiometers for full-scale reading. |
| GND | GND | Power | Ground reference for the voltage dividers. | |
| VRx | A0 | Analog Input | Connected to an ADC channel to read X-axis voltage ($0-5\text{V}$). | |
| VRy | A1 | Analog Input | Connected to an ADC channel to read Y-axis voltage ($0-5\text{V}$). | |
| SW | D4 | Digital Input | Configured with INPUT_PULLUP; triggers external reading on state change. | |
| Joystick 2 (P2) | +5V | 5V | Power | Parallel power line for the second user controller. |
| GND | GND | Power | Parallel ground line to prevent ground loops. | |
| VRx | A2 | Analog Input | Uses a distinct ADC channel to poll Player 2's X-axis data. | |
| VRy | A3 | Analog Input | Uses a distinct ADC channel to poll Player 2's Y-axis data. | |
| SW | D2 | Digital Input | Configured with INPUT_PULLUP. Can be mapped to hardware interrupt (INT0) if needed. | |
| Passive Buzzer | Signal (+) | D8 | Digital / PWM | Driven by the hardware timers using the tone() function for frequencies. |
| - / GND | GND | Power | Returns current safely back to the microcontroller ground rail. |
The electrical circuit is centered around the Arduino Uno R3, acting as the main master device on a shared bus architecture:
The I2C Bus Topology: The SSD1306 OLED display functions as an I2C slave device. It is tied to the dedicated hardware pini (A4 and A5). Pull-up resistors (typically integrated into the display board module) keep the lines at a logic $\text{HIGH}$ state when idle, enabling fast serial clock and data synchronization.
Analog Voltage Dividers: The Joysticks contain two internal $10\text{k}\Omega$ potentiometers arranged as voltage dividers. As the stick moves physically, the wiper terminal changes its position, altering the voltage routed to pins A0-A3. The ATmega328P's internal Successive Approximation ADC converts this analog voltage into a digital 10-bit integer ($0 - 1023$).
Digital Input Protection: The tactile switch (SW) outputs are tied to digital pins D2 and D4. By initializing them as INPUT_PULLUP, the microcontroller forces a default logic $\text{HIGH}$ state ($5\text{V}$), eliminating floating states. When a player presses down on the joystick, the circuit closes to ground, safely pulling the input to a logic $\text{LOW}$ ($0\text{V}$).
The physical setup is completely mounted on the prototype box console. Wiring paths have been tightly organized to mitigate electromagnetic noise feedback generated by the passive buzzer.
The firmware has reached $100\%$ functional completion. All states of the finite state machine (MENU, TTT_GAME, TTT_POST) are fully integrated. The game logic loop runs concurrently with background asynchronous tasks, such as handling debouncing via timing delays and generating non-blocking audio feedback frequencies via PWM timers.
Arduino IDE 2.3.x: Used for coding, compiling, and flashing the firmware onto the ATmega328P microcontroller. Serial Monitor: Utilized during the debugging phase to calibrate joystick thresholds and I2C address discovery.
Adafruit_SSD1306 & Adafruit_GFX: Writing a custom lightweight SSD1306 driver via direct register manipulation would consume significant development time and heavily limit geometric drawing capabilities. Using these well-optimized, open-source libraries provides an efficient screen-buffer rendering mechanism and ready-made pixel mapping arrays for alphanumeric text characters.
Wire.h: Chosen because it interfaces directly with the dedicated hardware I2C peripheral of the ATmega328P. This eliminates the need to implement complex bit-banging routines on standard GPIO pins, ensuring stable clock stretching and standard $400\text{kHz}$ data transfer speeds.
The novelty of this project lies in its software adaptation layer for symmetrical hardware physical assembly. Instead of restricting users to a fixed, awkward controller orientation, the firmware dynamically intercepts the raw ADC readings of Player 2 and applies an on-the-fly axis transform matrix. This creates a balanced, head-to-head console experience out of standard, asymmetrical DIY modules. Additionally, the inclusion of an automated single-player Bot opponent expands the scope beyond a basic two-wire connection.
This project serves as a practical unification of several laboratory concepts:
General Purpose Input/Output (GPIO) (Lab 0): The digital pins are utilized to handle binary user input events and configure peripheral states. Pini D2 and D4 are configured as digital inputs with internal pull-up resistors activated (INPUT_PULLUP), establishing a reliable default logic $\text{HIGH}$ state. This eliminates floating voltage nodes and ensures precise detection of a logic $\text{LOW}$ state when a player presses down on the joystick selection switch.
Timers and Pulse-Width Modulation (PWM) (Lab 3): Leveraged via hardware internal timers to toggle digital pin 8 at precise acoustic frequencies. This provides instantaneous sensory feedback without relying on primitive, blocking code execution delays.
Analog-to-Digital Conversion (ADC) (Lab 4): Used to continuously poll the analog voltage variance across pini A0-A3. This transforms physical mechanical movement into a digital range to guide game navigation.
I2C Serial Communication (Lab 7): Integrated to interface with the OLED screen over just two wires (SDA/SCL). This approach dramatically minimizes pin exploitation, leaving more digital pins available for hardware inputs.
The joysticks use $10\text{k}\Omega$ internal potentiometers which natively exhibit a baseline electrical drift when resting at the center dead-zone (yielding raw values roughly between $495 - 525$). Calibration was accomplished by mapping these thresholds mathematically into three discrete software bands:
Negative Range Vector: Any raw digital voltage drop below $300$ signifies an intentional physical push to the Left / Up.
Positive Range Vector: Any raw digital voltage rise above $700$ signifies an intentional physical push to the Right / Down.
Dead-zone Hysteresis: Raw values between $301$ and $699$ are filtered out and ignored. This completely eliminates “ghost drift” or unintentional structural movements on the game grid.
Where: Applied globally to all text rendering calls across drawMenu(), drawTTTGrid(), and drawPostGame().
How: Implemented by wrapping all hardcoded string literals inside the flash memory wrapper macro: display.print(F(“Text Here”));.
Why: By default, string constants are automatically loaded directly into the extremely scarce $2\text{KB}$ SRAM buffer during runtime initialization. Because the Adafruit display buffer alone permanently locks away $1\text{KB}$ ($1024$ bytes) of RAM just to drive the screen pixels, the remaining memory was highly vulnerable to stack overflow crashes. Utilizing the F() macro forces the compiler to keep these arrays inside the $32\text{KB}$ Flash memory space (PROGMEM), reducing SRAM consumption by over $35\%$ and ensuring console runtime stability.
The firmware architecture operates on a cyclic executive loop driven entirely by a Finite State Machine (FSM). The structural framework and subsystem interactions execute as follows:
The Orchestrating Loop: The native loop() function functions exclusively as a traffic controller, utilizing a switch-case block to continuously check the currentState variable. Based on this state, it evaluates only one of the three main execution branches (drawMenu(), tttGameLoop(), or drawPostGame()) per cycle.
Inter-Function Communication: When a match is active under tttGameLoop(), the loop handles state-dependent execution. If vsBot is active and it is Player 2's turn, it skips hardware scanning routines and hands code execution over to botMove().
Algorithmic Cascading: Once a joystick action or an automated bot choice alters the board[3][3] matrix array, a cascade of evaluation logic occurs. The game loop immediately suspends input pooling and runs checkWin() and checkDraw(). If either returns true, the score variables are incremented, the audio feedback engine is called to synthesize tone queues, and the global FSM variable is updated to TTT_POST, cleanly altering the rendering routine of the next execution cycle.
Turn State Management: To enhance the competitive user experience, the FSM implements a dynamic “winner-starts-first” rule. During the TTT_POST state evaluation, the system retains the previous winner's ID and automatically assigns them the first move for the subsequent match. Conversely, navigating back to the main menu or triggering a manual score reset forces the currentPlayer variable back to Player 1, and re-centers the cursor to the middle of the grid, ensuring a consistent and fair starting state.
To verify that all modular software components interact reliably without introducing runtime regressions, a progressive verification sequence was conducted:
Isolated Peripheral Verification: Prior to writing the complete game logic, a specialized diagnostic script was uploaded to poll raw voltage ranges across pins A0-A3 and display them directly as raw data integers on the SSD1306 display. This verified that the I2C pipeline and analog mapping bounds were physically synchronized.
Boundary Testing: The mathematical checkWin() matrix scanner was validated by manually forcing winning vector patterns (rows, columns, and edge-case diagonals) directly into the board array memory grid during initialization to confirm that state transitions triggered consistently.
Hysteresis and Debounce Stress Testing: Rapid, aggressive joystick movements were executed during testing to guarantee that the $200\text{ms}$ input decoupling delays worked properly, ensuring the cursor never skipped grid cells or caused double-trigger button actions.
Below is the embedded video demonstration displaying the complete end-to-end functionality of the hardware console, including both Single Player (VS Bot) and Multiplayer game loops, audio cue responses, and menu interactions.
Watch the Dual-Controller Tic-Tac-Toe Gameplay Demo on YouTube
The project resulted in a fully functional handheld console. The system successfully handles real-time input from two controllers without latency. The OLED display provides a clear interface, and the integrated Bot offers a challenging solo experience. The memory optimizations (using the F() macro) ensured the system remains stable during long play sessions.
The implementation of the Dual-Controller OLED Tic-Tac-Toe console highlights the importance of hardware-software codesign in embedded configurations. By using an FSM structure, software maintenance, debouncing routines, and turn-based mechanics (such as the winner-starts-first rule) were kept modular and reliable. Resolving physical alignment challenges for Player 2's joystick dynamically via software proved that smart firmware layers can seamlessly optimize raw physical component setups. Ultimately, this project demonstrates how memory optimization techniques (like the F macro) and clever state management can extract robust, real-time performance from constrained microcontrollers like the ATmega328P.
Adafruit GFX Library Documentation https://cdn-learn.adafruit.com/downloads/pdf/adafruit-gfx-graphics-library.pdf
Adafruit SSD1306 Library https://github.com/adafruit/Adafruit_SSD1306
Arduino Reference - Tone Function https://docs.arduino.cc/language-reference/en/functions/advanced-io/tone/
Arduino Reference - RandomSeed https://docs.arduino.cc/language-reference/en/functions/random-numbers/randomSeed/
Memory Optimization (F macro) https://docs.arduino.cc/language-reference/en/variables/utilities/PROGMEM/
Atmel ATmega328P Datasheet https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf
SSD1306 OLED Controller Datasheet https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
Joystick Module (KY-023) Guide https://arduinomodules.info/ky-023-joystick-dual-axis-module/