Tic-Tac-Toe Game with Atmega 256 MicroController
Thilan Dissanayaka Hardware Hacking Mar 06, 2020

Tic-Tac-Toe Game with Atmega 256 MicroController

So guys, in this post I’ll walk you through how I built a Tic-Tac-Toe game using an AVR microcontroller, a 4x3 keypad, and a 3x3 grid of LEDs. This was a fun project that combines embedded programming, game logic, and hardware interaction into a complete system. Let’s get into it.

Project Overview

The idea here is pretty simple. Two players take turns pressing buttons on a keypad, and the board lights up with LEDs to show the game state. Red for one player, green for the other. When someone wins, the LEDs do a little celebration animation.

Hardware Components

Here’s what I used for the setup:

  • ATmega microcontroller (running at 8 MHz)
  • 4x3 Keypad for user input
  • 3x3 LED grid for game display
  • Resistors, wires, and breadboard for wiring
  • Power supply (USB/5V battery or programmer)

The keypad gives us 9 usable buttons (mapped to cells 1-9 on the board), and the LED grid has individually controllable LEDs that can show red or green. The microcontroller ties everything together, reading input and driving the LEDs based on the game state.

Software Workflow

Now let’s break down the code. I’ll go through each piece step by step so you can see how the hardware and software connect.

Step 1: Setting Up the Keypad

The keypad serves as the input device where each button represents a cell (1-9) on the Tic-Tac-Toe board. I used PORTK for rows and PINK for columns on the ATmega.

void initKeypad() {
    DDRK = 0x0F;    // Lower nibble (rows) as output, upper nibble (columns) as input
    PORTK |= 0xF0;  // Enable internal pull-ups for the inputs
}

What’s happening here? We set the lower 4 bits of PORTK as outputs (these drive the rows) and the upper 4 bits as inputs with pull-ups enabled (these read the columns). When a button is pressed, it connects a row to a column, and we can detect which one by scanning through each row.

The keypad() function handles:

  • Polling each row/column to detect key presses
  • Debouncing to eliminate accidental multiple presses
  • Mapping key values to grid positions (1-9)

Step 2: Initializing the LED Grid

The LED grid is controlled through multiple ports. Each cell corresponds to one LED, and different pins on PORTA, PORTC, PORTD, and PORTG are used.

void initGrid() {
    DDRA = 0xFF;
    DDRC = 0xFF;
    DDRD = 0xFF;
    DDRG = 0xFF;
    // Set all grid control ports as output
}

We set all the data direction registers to output mode so we can drive the LEDs directly. Each LED is controlled individually based on the game state using a helper function like controlLed(row, col, color, state). The color parameter determines whether the LED lights up red or green, and the state turns it on or off.

Step 3: Implementing Game Logic

This is where the actual game lives. The play() function is responsible for:

  • Keeping track of the player turn (RED or GREEN)
  • Preventing overwriting already marked cells
  • Updating the LED grid
void play(int index) {
    int row = (index - 1) / 3;
    int col = (index - 1) % 3;

    if (grid[row][col] == 0) {          // Check if the cell is empty
        grid[row][col] = turn;          // Mark the cell
        controlLed(row, col, turn, ON); // Light up the LED
        turn = (turn == RED) ? GREEN : RED; // Switch turns
    }
}

The index comes from the keypad (1-9), and we convert it to row and column coordinates. If the cell is already taken, the press is just ignored. Otherwise we mark it, light up the corresponding LED, and swap turns.

Step 4: Checking for a Winner

After every move, we check rows, columns, and diagonals to see if a player has won. This is classic Tic-Tac-Toe logic.

int checkWinner() {
    for (int i = 0; i < 3; i++) {
        // Check rows
        if (grid[i][0] != 0 &&
            grid[i][0] == grid[i][1] &&
            grid[i][1] == grid[i][2])
            return grid[i][0];

        // Check columns
        if (grid[0][i] != 0 &&
            grid[0][i] == grid[1][i] &&
            grid[1][i] == grid[2][i])
            return grid[0][i];
    }

    // Check diagonals
    if (grid[0][0] != 0 &&
        grid[0][0] == grid[1][1] &&
        grid[1][1] == grid[2][2])
        return grid[0][0];

    if (grid[0][2] != 0 &&
        grid[0][2] == grid[1][1] &&
        grid[1][1] == grid[2][0])
        return grid[0][2];

    return 0; // No winner
}

The function returns the winner’s color (RED or GREEN) if there’s a match, or 0 if no one has won yet. We check all 8 possible winning combinations: 3 rows, 3 columns, and 2 diagonals.

Step 5: Winning Animation

When a player wins, we want to make it obvious. A simple animation flashes the LEDs to celebrate.

void animation(int winner) {
    for (int i = 0; i < 3; i++) {
        controlLed(0, i, winner, ON);
        _delay_ms(300);
        controlLed(0, i, winner, OFF);
        _delay_ms(300);
    }
}

Step 6: The Main Loop

Now here’s where everything comes together. The main loop ties the keypad input, game logic, and winner checking into a continuous game cycle.

int main(void) {
    initKeypad();
    initGrid();
    turn = RED;  // Red goes first

    while (1) {
        int key = keypad();       // Wait for a keypress
        if (key >= 1 && key <= 9) {
            play(key);            // Make the move
            int winner = checkWinner();
            if (winner) {
                animation(winner); // Celebrate
                // Reset the game for a new round
                resetGrid();
                turn = RED;
            }
        }
    }
    return 0;
}

The loop is straightforward. We wait for a valid keypress, play the move, check if someone won, and if so, run the animation and reset. The while(1) keeps the game running forever, which is typical for embedded systems since there’s no operating system to return to.

Wrapping Up

This was a really fun project to put together. It’s a great way to get hands-on with AVR programming because it touches on a lot of the fundamentals: port I/O, input scanning, debouncing, and driving external components. And at the end of it, you have something you can actually play with.

If you want to take this further, you could add a buzzer for sound effects, implement a simple AI opponent, or even add a score counter using a 7-segment display. The ATmega has plenty of pins to spare.

Thanks for reading. See you in the next one.

ALSO READ
Remote Code Execution (RCE)
Jan 02 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.

Bypassing DEP with Return-to-libc
Apr 05 Exploit development

DEP makes the stack non-executable — our shellcode can't run. The simplest bypass? Don't inject code at all. Instead, call functions that already exist in libc. In this post, we exploit a stack overflow to call system('/bin/sh') without writing a single byte of shellcode.

 OWASP Top 10 explained - 2021
Feb 11 Application Security

The Open Worldwide Application Security Project (OWASP) is a nonprofit foundation focused on improving the security of software. It provides free, vendor neutral tools, resources, and standards that...

Boolean Based Blind SQL Injection
Feb 12 Application Security

In regular SQL injection, you can see the database output right there on the page. Blind SQL injection is different — the application gives you nothing. No errors, no data, no feedback. But with boolean-based blind SQLi, you can still extract the entire database — one true/false question at a time.

Exploiting a Stack Buffer Overflow on Windows
Apr 12 Exploit development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

How I built a web based CPU Simulator
May 07 Pet Projects

As someone passionate about computer engineering, reverse engineering, and system internals, I've always been fascinated by what happens "under the hood" of a computer. This curiosity led me to...