The program centers on a chess timer program built on the 1ST Maker Frog board that simulates a real chess clock using embedded programming concepts. At its core, the program is a finite state machine that manages different game states. It tracks each player’s remaining time using a subtract-elapsed timing method, switches turns based on button release events, and supports features like pause/resume gestures and optional Fischer time increments.
Sample Code:
/**************************************************
* Program: Chess_Timer.ino
* Copyright (C) 成品人和精品人的区别在哪里, Inc. 2026
*
* Description:
* This program implements a chess timer using
* the 1ST Maker Frog hardware. On startup, the players
* choose the length of play by turning the
* potentiometer. Then the play is started by pressing
* switch 1 or switch 2. After the player's turn is over
* they press their button, which counts down their
* opponent's timer. The game can be paused by pressing
* both buttons.
*
* Hardware:
* - 1ST Maker Frog board
* - OLED display (SSD1306)
* - Two NeoPixel eyes
* - Two buttons (BUTTON_ONE, BUTTON_TWO)
* - Piezo buzzer (PIEZO)
**************************************************/
#include "1ST_Maker_Frog.h"
// ---------------------------------------------------------------------------
// Constants and configuration
// ---------------------------------------------------------------------------
const uint16_t DISPLAY_FPS_MS = 100; // refresh OLED about 10 times/second
const uint16_t DEBOUNCE_MS = 35; // simple button debounce
// Pause hold (pause happens while holding; resume happens after releasing)
const uint16_t PAUSE_HOLD_MS = 1200; // hold BOTH this long to pause / arm resume
// Time presets selected by the potentiometer (minutes)
const uint8_t PRESET_COUNT = 6;
const uint8_t PRESET_MINUTES[PRESET_COUNT] = {1, 3, 5, 10, 15, 20};
// Increment per move (seconds). Keep 0 for the first version; easy to change later.
const uint8_t INCREMENT_SECONDS = 0;
// Piezo tones (simple feedback)
const uint16_t BEEP_HZ = 1500;
const uint16_t BEEP_SHORT_MS = 40;
const uint16_t BEEP_ARM_MS = 70; // slightly longer so it's noticeable
// ---------------------------------------------------------------------------
// Data structures and globals
// ---------------------------------------------------------------------------
enum ClockState {
STATE_SET,
STATE_RUN_A,
STATE_RUN_B,
STATE_PAUSED,
STATE_OVER
};
struct ButtonTracker {
uint8_t pin;
bool stablePressed; // debounced "pressed" state
bool lastStablePressed;
uint32_t lastChangeMs; // for debounce timing
bool pressedEvent; // one-loop: transitioned to pressed
bool releasedEvent; // one-loop: transitioned to released
};
ClockState state = STATE_SET;
ClockState prevRunState = STATE_RUN_A; // remembers whether A or B was running before pause
uint32_t startTimeMs = 5UL * 60UL * 1000UL;
int32_t timeA_ms = 0;
int32_t timeB_ms = 0;
uint32_t lastTickMs = 0;
uint32_t lastDisplayMs = 0;
// BOTH-button hold tracking (one action per hold, no cycling)
uint32_t bothPressStartMs = 0;
bool bothGestureUsed = false;
// Resume is armed while holding BOTH, but only happens after BOTH are released.
bool resumePending = false;
// Prevent turn-switch release after a pause/resume hold
bool ignoreReleasesUntilBothUp = false;
// Button trackers
ButtonTracker btnA = {BUTTON_ONE, false, false, 0, false, false};
ButtonTracker btnB = {BUTTON_TWO, false, false, 0, false, false};
// ---------------------------------------------------------------------------
// Function prototypes
// ---------------------------------------------------------------------------
void initHardware();
void updateButtons(uint32_t nowMs);
void handleStateMachine(uint32_t nowMs);
uint8_t readPresetIndexFromPot();
void applyPreset(uint8_t presetIndex);
void tickActiveClock(uint32_t nowMs);
void switchTurnTo(ClockState nextState);
void renderUI(uint32_t nowMs);
void drawTimeBox(int16_t x, int16_t y, const char* label, int32_t msRemaining, bool isActive);
void formatTime(int32_t msRemaining, char* out, size_t outSize);
void updateEyes();
void beep(uint16_t hz, uint16_t durationMs);
void beepClick();
void beepArmed();
void beepTimeUp();
// ---------------------------------------------------------------------------
// Setup and main loop
// ---------------------------------------------------------------------------
void setup() {
initHardware();
state = STATE_SET;
applyPreset(readPresetIndexFromPot());
lastTickMs = millis();
lastDisplayMs = 0;
}
void loop() {
uint32_t nowMs = millis();
updateButtons(nowMs);
handleStateMachine(nowMs);
if (nowMs - lastDisplayMs >= DISPLAY_FPS_MS) {
lastDisplayMs = nowMs;
renderUI(nowMs);
}
updateEyes();
}
// ---------------------------------------------------------------------------
// Helper functions
// ---------------------------------------------------------------------------
/**************************************************
* Function: initHardware
* Purpose: Initializes Frog hardware (OLED, LEDs,
* buttons, NeoPixels).
**************************************************/
void initHardware() {
for (uint8_t i = 0; i < 4; i++) {
pinMode(LEDs[i], OUTPUT);
digitalWrite(LEDs[i], LOW);
}
// Buttons are active-LOW, so use pullups.
pinMode(BUTTON_ONE, INPUT_PULLUP);
pinMode(BUTTON_TWO, INPUT_PULLUP);
pinMode(PIEZO, OUTPUT);
// Constructors provided by 1ST_Maker_Frog.h
pixel.begin();
pixel.show();
Wire.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
while (true) {
for (uint8_t i = 0; i < 4; i++) digitalWrite(LEDs[i], HIGH);
delay(150);
for (uint8_t i = 0; i < 4; i++) digitalWrite(LEDs[i], LOW);
delay(150);
}
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.display();
}
/**************************************************
* Function: updateButtons
* Purpose: Debounces buttons and creates one-loop
* press/release events.
*
* Parameters:
* nowMs - current millis() time
**************************************************/
void updateButtons(uint32_t nowMs) {
ButtonTracker* buttons[2] = {&btnA, &btnB};
for (uint8_t i = 0; i < 2; i++) {
ButtonTracker* b = buttons[i];
b->pressedEvent = false;
b->releasedEvent = false;
bool rawPressed = (digitalRead(b->pin) == PRESSED);
if (rawPressed != b->stablePressed) {
if (nowMs - b->lastChangeMs >= DEBOUNCE_MS) {
b->lastStablePressed = b->stablePressed;
b->stablePressed = rawPressed;
b->lastChangeMs = nowMs;
if (b->stablePressed && !b->lastStablePressed) b->pressedEvent = true;
if (!b->stablePressed && b->lastStablePressed) b->releasedEvent = true;
}
} else {
b->lastChangeMs = nowMs;
}
}
}
/**************************************************
* Function: handleStateMachine
* Purpose: Runs the chess-clock logic.
* - Turn changes on button RELEASE.
* - Pause happens while BOTH are held.
* - Resume is ARMED while holding BOTH, and
* resumes AFTER both buttons are released.
*
* Parameters:
* nowMs - current millis() time
**************************************************/
void handleStateMachine(uint32_t nowMs) {
bool bothPressed = btnA.stablePressed && btnB.stablePressed;
// If we used a pause/resume hold, ignore releases until BOTH are fully up.
if (ignoreReleasesUntilBothUp) {
if (!btnA.stablePressed && !btnB.stablePressed) {
ignoreReleasesUntilBothUp = false;
// Clear any "button up" events caused by letting go after a hold.
btnA.pressedEvent = false;
btnA.releasedEvent = false;
btnB.pressedEvent = false;
btnB.releasedEvent = false;
} else {
return;
}
}
// -------------------------
// Resume pending (wait for BOTH released)
// -------------------------
if (resumePending) {
// Only resume when BOTH are fully released.
if (!btnA.stablePressed && !btnB.stablePressed) {
state = prevRunState;
lastTickMs = nowMs; // prevents time "jump" after pause
beepClick();
resumePending = false;
ignoreReleasesUntilBothUp = true; // blocks the release events from switching turns
}
return;
}
// -------------------------
// BOTH-button pause handling
// -------------------------
if (bothPressed) {
if (bothPressStartMs == 0) {
bothPressStartMs = nowMs;
bothGestureUsed = false;
}
uint32_t heldMs = nowMs - bothPressStartMs;
// One action per hold (prevents cycling).
if (!bothGestureUsed && heldMs >= PAUSE_HOLD_MS) {
if (state == STATE_RUN_A || state == STATE_RUN_B) {
// Pause immediately while holding.
prevRunState = state;
state = STATE_PAUSED;
beepClick();
bothGestureUsed = true;
ignoreReleasesUntilBothUp = true; // prevents player switch when releasing
return;
} else if (state == STATE_PAUSED) {
// ARM resume now, but do not resume until BOTH are released.
resumePending = true;
// Beep NOW to tell the user "okay, resume is armed."
beepArmed();
bothGestureUsed = true;
return;
}
}
// While both are held, do not tick and do not allow turn switching.
return;
}
// Not both pressed anymore: reset hold tracking for next time.
bothPressStartMs = 0;
bothGestureUsed = false;
// -------------------------
// Normal state machine
// -------------------------
switch (state) {
case STATE_SET: {
applyPreset(readPresetIndexFromPot());
// Start on RELEASE.
if (btnA.releasedEvent && !btnB.stablePressed) {
state = STATE_RUN_A;
prevRunState = STATE_RUN_A;
lastTickMs = nowMs;
beepClick();
} else if (btnB.releasedEvent && !btnA.stablePressed) {
state = STATE_RUN_B;
prevRunState = STATE_RUN_B;
lastTickMs = nowMs;
beepClick();
}
} break;
case STATE_RUN_A:
case STATE_RUN_B: {
tickActiveClock(nowMs);
if (state == STATE_RUN_A && btnA.releasedEvent && !btnB.stablePressed) {
timeA_ms += (int32_t)INCREMENT_SECONDS * 1000;
switchTurnTo(STATE_RUN_B);
prevRunState = STATE_RUN_B;
beepClick();
} else if (state == STATE_RUN_B && btnB.releasedEvent && !btnA.stablePressed) {
timeB_ms += (int32_t)INCREMENT_SECONDS * 1000;
switchTurnTo(STATE_RUN_A);
prevRunState = STATE_RUN_A;
beepClick();
}
if (timeA_ms <= 0 || timeB_ms <= 0) {
timeA_ms = max(timeA_ms, 0);
timeB_ms = max(timeB_ms, 0);
state = STATE_OVER;
beepTimeUp();
}
} break;
case STATE_PAUSED:
// Resume only via BOTH-button hold (arm) + release BOTH.
break;
case STATE_OVER:
// Use the board's reset button to restart.
break;
}
}
/**************************************************
* Function: readPresetIndexFromPot
* Purpose: Maps the potentiometer reading to one of
* the preset time options.
*
* Returns:
* Index into PRESET_MINUTES array.
**************************************************/
uint8_t readPresetIndexFromPot() {
int raw = analogRead(POT_PIN);
int binSize = 1024 / PRESET_COUNT;
uint8_t idx = raw / binSize;
if (idx >= PRESET_COUNT) idx = PRESET_COUNT - 1;
return idx;
}
/**************************************************
* Function: applyPreset
* Purpose: Sets both players' timers to the selected
* preset.
*
* Parameters:
* presetIndex - index in PRESET_MINUTES
**************************************************/
void applyPreset(uint8_t presetIndex) {
uint32_t minutes = PRESET_MINUTES[presetIndex];
startTimeMs = minutes * 60UL * 1000UL;
timeA_ms = (int32_t)startTimeMs;
timeB_ms = (int32_t)startTimeMs;
}
/**************************************************
* Function: tickActiveClock
* Purpose: Subtracts elapsed time from the active
* player using millis() (no delay()).
**************************************************/
void tickActiveClock(uint32_t nowMs) {
uint32_t dt = nowMs - lastTickMs;
lastTickMs = nowMs;
if (state == STATE_RUN_A) {
timeA_ms -= (int32_t)dt;
} else if (state == STATE_RUN_B) {
timeB_ms -= (int32_t)dt;
}
}
/**************************************************
* Function: switchTurnTo
* Purpose: Switches which player is active and
* resets the tick timer so no time "jumps."
**************************************************/
void switchTurnTo(ClockState nextState) {
state = nextState;
lastTickMs = millis();
}
/**************************************************
* Function: renderUI
* Purpose: Draws the clock and instructions on the OLED.
**************************************************/
void renderUI(uint32_t nowMs) {
(void)nowMs;
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
if (state == STATE_SET) {
display.print("Knob: time, 1/2 start");
} else if (state == STATE_PAUSED) {
if (resumePending) display.print("PAUSED");
else display.print("PAUSED");
} else if (state == STATE_OVER) {
display.print("TIME! Press board RESET");
} else {
display.print("RUN: Press SW1 or SW2");
}
bool aActive = (state == STATE_RUN_A);
bool bActive = (state == STATE_RUN_B);
drawTimeBox(0, 14, "A", timeA_ms, aActive);
drawTimeBox(0, 40, "B", timeB_ms, bActive);
display.display();
}
/**************************************************
* Function: drawTimeBox
* Purpose: Draws one player's label and time.
* A small filled marker shows whose turn it is.
**************************************************/
void drawTimeBox(int16_t x, int16_t y, const char* label, int32_t msRemaining, bool isActive) {
display.drawRect(x, y, 128, 22, SSD1306_WHITE);
if (isActive) display.fillRect(x + 2, y + 2, 6, 6, SSD1306_WHITE);
else display.drawRect(x + 2, y + 2, 6, 6, SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(x + 12, y + 3);
display.print("P");
display.print(label);
char buf[12];
formatTime(msRemaining, buf, sizeof(buf));
display.setTextSize(2);
display.setCursor(x + 52, y + 5);
display.print(buf);
}
/**************************************************
* Function: formatTime
* Purpose: Converts milliseconds to M:SS format.
**************************************************/
void formatTime(int32_t msRemaining, char* out, size_t outSize) {
if (msRemaining < 0) msRemaining = 0;
uint32_t totalSec = (uint32_t)(msRemaining / 1000);
uint16_t minutes = totalSec / 60;
uint8_t seconds = totalSec % 60;
snprintf(out, outSize, "%u:%02u", minutes, seconds);
}
/**************************************************
* Function: updateEyes
* Purpose: Shows the active player and state using
* the NeoPixel eyes.
**************************************************/
void updateEyes() {
uint8_t off = 0;
uint8_t dim = 20;
uint8_t on = 80;
uint8_t over = 120;
auto setEye = [&](uint8_t idx, uint8_t r, uint8_t g, uint8_t b) {
pixel.setPixelColor(idx, pixel.Color(r, g, b));
};
if (state == STATE_RUN_A) {
setEye(0, off, on, off);
setEye(1, off, off, off);
} else if (state == STATE_RUN_B) {
setEye(0, off, off, off);
setEye(1, off, on, off);
} else if (state == STATE_PAUSED) {
setEye(0, dim, dim, dim);
setEye(1, dim, dim, dim);
} else if (state == STATE_OVER) {
setEye(0, over, off, off);
setEye(1, over, off, off);
} else { // STATE_SET
setEye(0, off, off, dim);
setEye(1, off, off, dim);
}
pixel.show();
}
/**************************************************
* Function: beep
* Purpose: Plays a tone on the piezo for feedback.
**************************************************/
void beep(uint16_t hz, uint16_t durationMs) {
tone(PIEZO, hz, durationMs);
}
/**************************************************
* Function: beepClick
* Purpose: Short click for common button actions.
**************************************************/
void beepClick() {
beep(BEEP_HZ, BEEP_SHORT_MS);
}
/**************************************************
* Function: beepArmed
* Purpose: A slightly longer beep so the user knows
* the resume action is armed.
**************************************************/
void beepArmed() {
beep(BEEP_HZ, BEEP_ARM_MS);
}
/**************************************************
* Function: beepTimeUp
* Purpose: Simple alert when time runs out.
**************************************************/
void beepTimeUp() {
beep(900, 160);
delay(180);
beep(700, 240);
}
*If you’re copying and pasting the code, or typing from scratch, delete everything out of a new Arduino sketch and paste / type in the above text.