This project is a handheld blackjack game implemented on a custom through-hole PCB using an ATmega328P microcontroller. It is my second PCB design and focuses on embedded system integration, real-time user interaction, and digital display control.
The system features seven-segment multiplexing for score display, dedicated input buttons for gameplay (hit/stay), and status LEDs for player and dealer feedback. The firmware is written in embedded C for the AVR architecture and implements blackjack game logic with rule-based dealer decisions.
The design emphasizes reliable digital I/O handling, efficient display multiplexing, and time-based input filtering for responsive gameplay in a resource-constrained embedded environment.
The firmware targets the ATmega328P microcontroller and manages the blackjack round flow, button input, dealer logic, score tracking, LEDs, and multiplexed seven-segment displays using non-blocking timing.
void loop() {
// If the round is over, show the score animation instead of
// accepting new button presses.
if (showScore) {
handleBlink();
return;
}
// Update the seven-segment display often enough that all digits
// appear lit at the same time.
if (millis() - lastUpdateTime >= refreshRate) multiplex();
// If either side busts, pause briefly so the player can see it,
// then let the bust handler finish the round.
if (dHandleBust()) return;
if (pHandleBust()) return;
// Deal the starting cards once when a new turn begins.
if (beginTurn) initTurn();
// After the player stays, the dealer draws automatically.
if (dAllowHit) handleDealerHit();
// The hit button reads LOW when pressed. The time check keeps
// one press from being counted more than once.
if (digitalRead(hitPin) == LOW &&
(millis() - pLastHitTime) > debounceDelay) {
handlePlayerHit();
}
// The stay button uses the same debounce check before ending the player turn.
if (digitalRead(stayPin) == LOW &&
(millis() - lastStayTime) > debounceDelay) {
handleStay();
}
}
void handlePlayerHit() {
// Remember when the button was pressed for debounce timing.
pLastHitTime = millis();
if (pAllowHit) {
// Add a random card value to the player's hand.
int randNum = random(1, 11);
pTotal += randNum;
if (pTotal > 21) {
// If an ace-style value is used, reduce it before calling it a bust.
if (randNum == 11) pTotal--;
else {
// Otherwise, mark the player as busted and start the delay timer.
pAllowBust = 1;
pLastBustTime = millis();
}
}
// Convert the total into the two digits shown on the display.
pPreDigits(pTotal);
}
}
void handleDealerHit() {
// Wait between dealer draws so the automatic turn is visible.
if (millis() - dLastHitTime < 700) return;
if (dTotal < 17) {
// Dealer keeps drawing until reaching at least 17.
int randNum = random(1, 11);
dTotal += randNum;
if (dTotal > 21) {
// Apply the same ace-style adjustment before marking a dealer bust.
if (randNum == 11) dTotal--;
else {
// Stop dealer hits and let the bust handler end the round.
dLastBustTime = millis();
dAllowHit = 0;
dAllowBust = 1;
}
}
dPreDigits(dTotal);
dLastHitTime = millis();
} else {
// Once the dealer reaches 17, compare scores and end the round.
dAllowHit = 0;
results();
}
}
void multiplex() {
// Save the refresh time for the next display update.
lastUpdateTime = millis();
// Turn all digits off before changing the segment pins.
for (int i = 0; i < 4; i++) {
digitalWrite(commonPins[i], LOW);
}
// Pick the player or dealer digit for this refresh pass.
int digit = cDisplay == 0 ? pOnesPlace :
cDisplay == 1 ? pTensPlace :
cDisplay == 2 ? dOnesPlace :
dTensPlace;
// Write the segment pattern, then turn on only the selected digit.
sendDigit(digit);
digitalWrite(commonPins[cDisplay], HIGH);
// Move to the next digit for the next fast refresh.
cDisplay = (cDisplay + 1) % 4;
}