top of page

Quiz Buzzer Strike Back

  • Writer: Philippe Chretien
    Philippe Chretien
  • 5 days ago
  • 4 min read

Updated: 2 days ago

Fourteen years ago I built my first quiz buzzer system. It worked, but the hardware was — let's say — a product of its constraints. I used every single IO pin on the Arduino, plus a 74HC595 shift register and two 74LS32 OR gate chips just to drive the LEDs. It was held together by necessity and a certain stubbornness that I think is just part of making things. I always told myself I'd revisit it. This year, I finally did.



The Brief I Set Myself

The original build worked, but it was sprawling — three chips doing what I wanted two wires to handle. So for version two, I gave myself a clear goal: same functionality, minimal Arduino IO, fewer chips.


The core concept is unchanged. Two teams. Four buttons each. First team to press a button lights up their LED and locks the other team out. A host button resets everything for the next question. What I wanted to improve was what's underneath — and that meant thinking carefully about how "first press wins" actually works at the silicon level.


Software polling introduces timing ambiguity. If both teams press within the same polling cycle, who wins? It depends on which line of code runs first, and that's not a quiz result, that's a coin flip. The right answer was interrupts: when a button goes LOW, the microcontroller drops everything and handles it immediately. That's the kind of determinism a quiz night deserves.


Enter the MCP23017

This is where the original design fell apart at the seams. I needed sixteen GPIO pins — eight buttons and eight LEDs — and the Arduino doesn't have that many. The v1 answer was a 74HC595 shift register to expand the outputs, and two 74LS32 chips to handle the logic. It worked, but it was a lot of silicon for a quiz buzzer.


The v2 answer is a single MCP23017. It's an I2C I/O expander that gives you 16 extra pins over just two wires — SDA and SCL. That's it. Two wires instead of three chips. And crucially, the MCP23017 has its own interrupt outputs, INTA and INTB, one for each bank of eight pins. That maps perfectly to "Team A" and "Team B" — the hardware architecture and the game logic line up naturally.



The wiring philosophy was straightforward: even-numbered pins on the expander get buttons (with internal pull-ups), odd-numbered pins get LEDs. So pin 0 is a button, pin 1 is its LED. Pin 2 is a button, pin 3 is its LED. Eight pairs, split across the two banks. INTA goes to Arduino pin 2, INTB goes to Arduino pin 3.

Pin 4 on the Arduino is the host reset. Pull it LOW and you start a new question.


The Part That Required Actual Thought

Here's where it got genuinely interesting: mutual exclusion. The two interrupt callbacks are tiny:

void callbackTeamA() {
  if(!awakenByInterruptB)
    awakenByInterruptA = true;
}

Team A's callback only fires if Team B hasn't already buzzed in. And vice versa. That single if check is what makes the whole system fair. Whichever interrupt fires first sets its flag, and the other team's interrupt — even if it arrives microseconds later — finds the flag already set and does nothing. It sounds almost too simple. But that's kind of the beauty of interrupt-driven design. The hardware serializes things that feel simultaneous to humans.


The other piece that needed care was debouncing and cleanup. Mechanical buttons are noisy — they bounce, meaning a single physical press can register as multiple rapid transitions. The cleanInt() function handles this by waiting 100ms after an interrupt fires, then blocking until every button reads HIGH (fully released), then clearing the Arduino's interrupt flag register. Only then does it re-enable interrupts for the next round.


Without that cleanup step, a slightly slow button release could trigger a phantom buzz on the next question. Tested that one the hard way.



Does It Actually Work?

Yes, and it's satisfying in a way that's hard to describe until you try it. There's something deeply pleasing about pressing a button and watching a light come on within a couple milliseconds. No lag, no debate. The LED is on. That team buzzed first. Next question.



The serial output is handy during development and testing — it prints which pin triggered the interrupt, so you can verify that the right button is registering. In a real game you'd probably ignore the serial monitor, but it's there if something weird happens and you want to debug.


What's Next

Silence is not a great buzzer experience. The v1 had sound — I used an Adafruit shield to play pre-recorded audio when a team buzzed in. This time I want to add it back using basic parts, without the cost of a dedicated shield.


After that: a proper scoreboard with operator controls, then everything packed into a real enclosure with handheld buzzer controllers. The longer-term goal is a kit — a PCB, a 3D-printable enclosure, something anyone with basic experience could build themselves.


There's a bit of road between here and there. But that's what makes it interesting.


The Code

The full code is on GitHub if you want to follow along or build your own starting point. The MCP23017 library from Adafruit handles all the low-level I2C communication, so the sketch stays readable.


Comments


  • Facebook
  • Twitter
  • LinkedIn

©2022 by Basbrun. Proudly created with Wix.com

bottom of page