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
Singleton Pattern explained simply
Jan 27 Software Architecture

Ever needed just one instance of a class in your application? Maybe a logger, a database connection, or a configuration manager? This is where the Singleton Pattern comes in — one of the simplest but...

Exploiting a heap buffer overflow in linux
Apr 12 Exploit development

In the [previous article](/heap-internals-how-glibc-malloc-works/), we dissected glibc's malloc — chunks, bins, tcache, coalescing, and the metadata that holds it all together. Now we break...

Out of Band SQL Injection
Feb 14 Application Security

Out of Band SQL Injection (OOB SQLi) is an advanced SQL injection technique where the attacker cannot retrieve data directly through the same communication channel used to send the injection payload....

Common Web Application Attacks
Feb 05 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Exploiting a format string vulnerebility on Linux
Apr 12 Exploit development

A misused printf can leak stack contents, read arbitrary memory, and write to arbitrary addresses. Format string vulnerabilities are one of the most powerful bug classes in C and they're the key to defeating ASLR. In this post, we exploit printf from leak to shell.

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...