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

SQL Injection Login Bypass
Feb 10 Application Security

SQL Injection (SQLi) is one of the oldest and most fundamental web application vulnerabilities. While modern frameworks have made it harder to introduce, understanding SQL injection is essential for anyone learning web security. In this post, we'll break it down from the ground up using a classic login bypass.

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01 Exploit development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Identity and Access Management (IAM)
May 11 Identity & Access Management

Who are you — and what are you allowed to do? That's the fundamental question every secure system must answer. And it's exactly what Identity and Access Management (IAM) is built to solve.

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

Access Control Models
Apr 08 Identity & Access Management

Access control is one of the most fundamental concepts in security. Every time you set file permissions, assign user roles, or restrict access to a resource, you're implementing some form of access control. But not all access control is created equal...