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.