Skip to content

Instantly share code, notes, and snippets.

@julznc
Last active June 18, 2022 11:37

Revisions

  1. julznc revised this gist Jun 18, 2022. No changes.
  2. julznc revised this gist Jun 18, 2022. No changes.
  3. julznc created this gist Jun 18, 2022.
    584 changes: 584 additions & 0 deletions chess-clock.ino
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,584 @@

    #include <Wire.h>
    #include <Adafruit_GFX.h>
    #include <Adafruit_SSD1306.h>


    #define MAIN_BTN_PIN (2) // PA14
    #define P1_BTN_PIN (3) // PA9
    #define P2_BTN_PIN (5) // PA15
    #define LED_PIN LED_BUILTIN // (13)

    // 100ms timer interrupt
    #define K_TIMER_COMP (3277) // 32768 = 1 sec
    #define K_TIMER_CORR ((0<<7) | 0) // bit7 -sign; bit[6:0] magnitude

    #define SCREEN_WIDTH (128) // OLED display width, in pixels
    #define SCREEN_HEIGHT (32) // OLED display height, in pixels
    #define SCREEN_ADDRESS (0x3C)

    Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);


    enum state_et
    {
    STATE_INIT,
    STATE_RUN,
    STATE_PAUSE,
    STATE_CONFIG
    } e_state;

    enum mode_et
    {
    MODE_3min_2sec, // 3+2
    MODE_5min, // 5 min
    MODE_10min, // 10 min
    MODE_90min, // 1.5 hr
    } e_mode;

    enum config_state_et
    {
    CONFIG_RETURN,
    CONFIG_RESTART,
    CONFIG_3min_2sec, // 3+2
    CONFIG_5min, // 5 min
    CONFIG_10min, // 10 min
    CONFIG_90min, // 1.5 hr
    } e_config_state;

    typedef enum
    {
    BUTTON_IDLE = 0,
    BUTTON_SHORT_PRESSED = 1,
    BUTTON_LONG_PRESSED = 2,
    BUTTON_PRESSED = BUTTON_SHORT_PRESSED | BUTTON_LONG_PRESSED,
    } e_button_status;

    volatile uint32_t u32_100ms_counter;
    volatile uint32_t u32_last_active;
    volatile uint32_t u32_main_btn_released;
    volatile uint32_t u32_main_btn_pressed;
    volatile uint32_t u32_p1_btn_pressed;
    volatile uint32_t u32_p2_btn_pressed;

    e_button_status btn_main;
    e_button_status btn_p1;
    e_button_status btn_p2;

    struct stats_st {
    volatile uint32_t u32_time; // x 0.1s
    volatile bool b_running; // true = continue countdown
    uint16_t u16_moves;
    } s_p1_stats, s_p2_stats;


    static void mainBtnToggled(void)
    {
    if (LOW == digitalRead(MAIN_BTN_PIN)) {
    u32_main_btn_pressed = u32_100ms_counter;
    }
    else {
    u32_main_btn_released = u32_100ms_counter;
    }
    u32_last_active = u32_100ms_counter;
    }

    static void p1BtnPressed(void)
    {
    u32_p1_btn_pressed = u32_100ms_counter;
    u32_last_active = u32_100ms_counter;
    }

    static void p2BtnPressed(void)
    {
    u32_p2_btn_pressed = u32_100ms_counter;
    u32_last_active = u32_100ms_counter;
    }

    void RTC_Handler(void)
    {
    if (RTC->MODE0.INTFLAG.bit.CMP0)
    {
    //RTC->MODE0.INTFLAG.reg = 0xFF; // Clear all interrupts
    RTC->MODE0.INTFLAG.reg |= 0x81; // Clear CMP0 and OVF interrupts only

    u32_100ms_counter++;

    if (STATE_RUN == e_state)
    {
    if (s_p1_stats.b_running && s_p1_stats.u32_time) {
    s_p1_stats.u32_time--;
    }

    if (s_p2_stats.b_running && s_p2_stats.u32_time) {
    s_p2_stats.u32_time--;
    }
    }

    }
    }

    void getButtonStatus(void)
    {
    static bool b_long_pressed = false;

    // default status
    btn_main = BUTTON_IDLE;
    btn_p1 = BUTTON_IDLE;
    btn_p2 = BUTTON_IDLE;

    if (0 != u32_main_btn_released)
    {
    delay(10); // debounce?
    if (LOW == digitalRead(MAIN_BTN_PIN))
    {
    // false trigger?
    }
    else if ((0 == u32_main_btn_pressed) || (u32_main_btn_pressed > u32_main_btn_released))
    {
    // invalid ?
    }
    else if (true == b_long_pressed)
    {
    u32_main_btn_released = 0;
    }
    else if (u32_main_btn_released - u32_main_btn_pressed < 15)
    {
    btn_main = BUTTON_SHORT_PRESSED;
    u32_main_btn_pressed = 0;
    u32_main_btn_released = 0;
    }

    b_long_pressed = false;
    }
    else if (0 != u32_main_btn_pressed)
    {
    delay(10); // debounce?
    if (HIGH == digitalRead(MAIN_BTN_PIN))
    {
    // false trigger?
    }
    else if (u32_100ms_counter - u32_main_btn_pressed > 25)
    {
    btn_main = BUTTON_LONG_PRESSED;
    b_long_pressed = true;
    u32_main_btn_pressed = 0;
    u32_main_btn_released = 0;
    }
    }

    if ((0 != u32_p1_btn_pressed) || (0 != u32_p2_btn_pressed))
    {
    delay(10); // debounce?
    if ((0 != u32_p1_btn_pressed) && (LOW == digitalRead(P1_BTN_PIN)))
    {
    btn_p1 = BUTTON_PRESSED;
    }
    if ((0 != u32_p2_btn_pressed) && (LOW == digitalRead(P2_BTN_PIN)))
    {
    btn_p2 = BUTTON_PRESSED;
    }
    u32_p1_btn_pressed = 0;
    u32_p2_btn_pressed = 0;
    }
    }

    static inline void displayTime(struct stats_st *ps_stats, int xpos)
    {
    char str_time_buf[16];
    uint32_t u32_100ms;

    oled.setTextSize(2);
    oled.setCursor(xpos, 12);
    u32_100ms = ps_stats->u32_time;
    if (u32_100ms < 200)
    {
    #if 0
    snprintf(str_time_buf, sizeof(str_time_buf) - 1, "%3ld.%ld", u32_100ms / 10, u32_100ms % 10);
    oled.print(str_time_buf);
    #else
    snprintf(str_time_buf, sizeof(str_time_buf) - 1, "0:%02ld ", u32_100ms / 10);
    oled.print(str_time_buf);
    oled.setTextSize(1);
    oled.setCursor(oled.getCursorX() - 14, 10 + 7);
    snprintf(str_time_buf, sizeof(str_time_buf) - 1, ".%ld", u32_100ms % 10);
    oled.print(str_time_buf);
    #endif
    }
    else
    {
    u32_100ms /= 10;
    snprintf(str_time_buf, sizeof(str_time_buf) - 1, "%2ld:%02ld", u32_100ms / 60, u32_100ms % 60);
    oled.print(str_time_buf);
    }

    //oled.fillRect(xpos + 24, 30, 16, 1, ps_stats->b_running ? SSD1306_INVERSE : SSD1306_BLACK);
    oled.fillRect(xpos + 24, 30, 16, 2, ps_stats->b_running ? ((ps_stats->u32_time>>1) & SSD1306_INVERSE) : SSD1306_BLACK);
    }

    static inline void toggleConfigDisplay(config_state_et cfg)
    {
    switch(cfg)
    {
    case CONFIG_RETURN:
    oled.fillRect(0, 3, 44, 12, SSD1306_INVERSE);
    break;
    case CONFIG_RESTART:
    oled.fillRect(0, 18, 44, 12, SSD1306_INVERSE);
    break;
    case CONFIG_3min_2sec:
    oled.fillRect(62, 3, 22, 12, SSD1306_INVERSE);
    break;
    case CONFIG_5min:
    oled.fillRect(94, 3, 22, 12, SSD1306_INVERSE);
    break;
    case CONFIG_10min:
    oled.fillRect(62, 18, 22, 12, SSD1306_INVERSE);
    break;
    case CONFIG_90min:
    oled.fillRect(94, 18, 22, 12, SSD1306_INVERSE);
    break;
    }
    }

    static inline void applyMode(mode_et mode)
    {

    s_p1_stats.b_running = false;
    s_p2_stats.b_running = false;
    s_p1_stats.u16_moves = 0;
    s_p2_stats.u16_moves = 0;

    switch(mode)
    {
    case MODE_3min_2sec:
    s_p1_stats.u32_time = (3 * 60 * 10) + 20;
    s_p2_stats.u32_time = (3 * 60 * 10) + 20;
    break;
    case MODE_5min:
    s_p1_stats.u32_time = 5 * 60 * 10;
    s_p2_stats.u32_time = 5 * 60 * 10;
    break;
    case MODE_10min:
    s_p1_stats.u32_time = 10 * 60 * 10;
    s_p2_stats.u32_time = 10 * 60 * 10;
    break;
    case MODE_90min:
    s_p1_stats.u32_time = 90 * 60 * 10;
    s_p2_stats.u32_time = 90 * 60 * 10;
    break;
    }

    e_mode = mode;
    e_state = STATE_INIT;
    }

    void applyConfig(void)
    {
    switch(e_config_state)
    {
    case CONFIG_RESTART:
    applyMode(e_mode);
    break;
    case CONFIG_3min_2sec:
    applyMode(MODE_3min_2sec);
    break;
    case CONFIG_5min:
    applyMode(MODE_5min);
    break;
    case CONFIG_10min:
    applyMode(MODE_10min);
    break;
    case CONFIG_90min:
    applyMode(MODE_90min);
    break;
    }
    }

    void oledDisplay(void)
    {
    static state_et prev_state = STATE_INIT;
    static config_state_et prev_config = CONFIG_RETURN;

    if (STATE_CONFIG == e_state)
    {
    if (STATE_CONFIG != prev_state) {
    e_config_state = CONFIG_RETURN;
    prev_config = e_config_state;
    oled.clearDisplay();

    oled.setTextSize(1);
    oled.setCursor(4, 5);
    oled.print("RETURN");
    oled.setCursor(1, 20);
    oled.print("RESTART");

    oled.setCursor(64, 5);
    oled.print("3+2");
    oled.setCursor(96, 5);
    oled.print("5+0");
    oled.setCursor(64, 20);
    oled.print("10m");
    oled.setCursor(96, 20);
    oled.print("90m");

    toggleConfigDisplay(e_config_state);
    }
    else if (prev_config != e_config_state)
    {
    toggleConfigDisplay(prev_config);
    toggleConfigDisplay(e_config_state);
    }
    }
    else
    {
    if (STATE_CONFIG == prev_state) {
    oled.clearDisplay();
    }

    oled.drawLine(SCREEN_WIDTH/2, 10, SCREEN_WIDTH/2, SCREEN_HEIGHT - 1, SSD1306_WHITE);

    displayTime(&s_p1_stats, 0);
    displayTime(&s_p2_stats, SCREEN_WIDTH/2 + 4);

    oled.setTextSize(1);
    if (STATE_PAUSE == e_state)
    {
    oled.setCursor(SCREEN_WIDTH/2-17, 0);
    oled.print("PAUSED");
    }
    else
    {
    if (STATE_PAUSE == prev_state) {
    oled.fillRect(0, 0, SCREEN_WIDTH - 1, 10, SSD1306_BLACK);
    }
    uint16_t u16_moves = max(s_p1_stats.u16_moves, s_p2_stats.u16_moves);
    oled.setCursor(SCREEN_WIDTH / 2 - (u16_moves > 9 ? 4 : 2), 0);
    oled.print(u16_moves);
    }
    }

    oled.display();
    prev_state = e_state;
    prev_config = e_config_state;
    }

    void setup()
    {
    pinMode(LED_PIN, OUTPUT);
    pinMode(MAIN_BTN_PIN, INPUT_PULLUP);
    pinMode(P1_BTN_PIN, INPUT_PULLUP);
    pinMode(P2_BTN_PIN, INPUT_PULLUP);

    if (!oled.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS))
    {
    while (1) { __asm("nop"); } // halt!
    }

    oled.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
    oled.cp437(true);
    oled.clearDisplay();
    oled.display();

    setupRTC(K_TIMER_COMP, K_TIMER_CORR);

    attachInterrupt(digitalPinToInterrupt(MAIN_BTN_PIN), mainBtnToggled, CHANGE);
    attachInterrupt(digitalPinToInterrupt(P1_BTN_PIN), p1BtnPressed, FALLING);
    attachInterrupt(digitalPinToInterrupt(P2_BTN_PIN), p2BtnPressed, FALLING);

    e_state = STATE_INIT;
    e_config_state = CONFIG_RETURN;

    u32_100ms_counter = 0;
    u32_last_active = 0;
    u32_main_btn_released = 0;
    u32_main_btn_pressed = 0;
    u32_p1_btn_pressed = 0;
    u32_p2_btn_pressed = 0;

    applyMode(MODE_5min);

    }

    void loop()
    {
    static state_et prev_state_before_config;
    static uint32_t u32_prev_display = 0;

    getButtonStatus();

    #define PLAYER_MOVED(prev, next) do { \
    if (0 != s_##next##_stats.u32_time) { \
    if (!s_##prev##_stats.b_running) { \
    if ((MODE_3min_2sec == e_mode) && \
    (s_##next##_stats.u16_moves)) \
    s_##next##_stats.u32_time += 20; \
    s_##next##_stats.u16_moves++; \
    } \
    s_##next##_stats.b_running = false; \
    s_##prev##_stats.b_running = true; \
    } \
    } while (0)

    switch (e_state)
    {
    case STATE_INIT:
    if ((BUTTON_SHORT_PRESSED == btn_main) || (BUTTON_LONG_PRESSED == btn_main)) {
    prev_state_before_config = e_state;
    e_state = STATE_CONFIG;
    digitalWrite(LED_PIN, HIGH);
    }
    if (BUTTON_PRESSED == btn_p1) {
    PLAYER_MOVED(p2, p1);
    e_state = STATE_RUN;
    }
    if (BUTTON_PRESSED == btn_p2) {
    PLAYER_MOVED(p1, p2);
    e_state = STATE_RUN;
    }
    break;

    case STATE_RUN:
    if (BUTTON_SHORT_PRESSED == btn_main) {
    e_state = STATE_PAUSE;
    }
    else
    {
    if (BUTTON_PRESSED == btn_p1) {
    PLAYER_MOVED(p2, p1);
    }
    if (BUTTON_PRESSED == btn_p2) {
    PLAYER_MOVED(p1, p2);
    }
    }
    break;

    case STATE_PAUSE:
    if (BUTTON_SHORT_PRESSED == btn_main) {
    e_state = STATE_RUN;
    }
    if (BUTTON_LONG_PRESSED == btn_main) {
    prev_state_before_config = e_state;
    e_state = STATE_CONFIG;
    digitalWrite(LED_PIN, HIGH);
    }
    break;

    case STATE_CONFIG:
    if (BUTTON_SHORT_PRESSED == btn_main) {
    e_state = prev_state_before_config;
    applyConfig();
    digitalWrite(LED_PIN, LOW);
    }
    if (BUTTON_PRESSED == btn_p1) {
    if (e_config_state > CONFIG_RETURN) {
    e_config_state = (config_state_et)(e_config_state - 1);
    }
    }
    if (BUTTON_PRESSED == btn_p2) {
    if (e_config_state < CONFIG_90min) {
    e_config_state = (config_state_et)(e_config_state + 1);
    }
    }
    break;

    default:
    e_state = STATE_INIT;
    break;
    }

    if (u32_prev_display != u32_100ms_counter)
    {
    oledDisplay();
    u32_prev_display = u32_100ms_counter;
    }

    // sleep for 3 or 10 minutes
    if (u32_100ms_counter - u32_last_active > (STATE_RUN == e_state ? (10*60*10) : (3*60*10)))
    {
    digitalWrite(LED_PIN, LOW);
    oled.ssd1306_command(SSD1306_DISPLAYOFF);

    #if 1 // samd21g standby mode
    NVIC_DisableIRQ(RTC_IRQn);
    SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // disable systick interrupt

    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __DSB();
    __WFI(); // wait for any interrupt (any button press)

    SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; // re-enable systick interrupt
    NVIC_EnableIRQ(RTC_IRQn);
    #endif

    oled.ssd1306_command(SSD1306_DISPLAYON);
    e_state = STATE_INIT;
    }
    }

    // https://forum.arduino.cc/t/samd21-rtc-millisecond-timing/655161/8
    void setupRTC(uint32_t u32_compare, uint8_t u8_correction)
    {
    // configure the 32768 Hz oscillator
    SYSCTRL->XOSC32K.reg = SYSCTRL_XOSC32K_ONDEMAND |
    SYSCTRL_XOSC32K_RUNSTDBY |
    SYSCTRL_XOSC32K_EN32K |
    SYSCTRL_XOSC32K_XTALEN |
    SYSCTRL_XOSC32K_STARTUP(6) |
    SYSCTRL_XOSC32K_ENABLE;
    while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

    // attach peripheral clock to 32768 Hz oscillator (1 tick = 1/32768 second)
    GCLK->GENDIV.reg = GCLK_GENDIV_ID(2) | GCLK_GENDIV_DIV(0);
    while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

    GCLK->GENCTRL.reg = (GCLK_GENCTRL_GENEN | GCLK_GENCTRL_SRC_XOSC32K | GCLK_GENCTRL_ID(2));
    while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

    GCLK->CLKCTRL.reg = (uint32_t)((GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK2 | (RTC_GCLK_ID << GCLK_CLKCTRL_ID_Pos)));
    while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization

    // disable RTC if it is enabled, to allow reconfiguration
    RTC->MODE0.CTRL.reg &= ~RTC_MODE0_CTRL_ENABLE;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization

    // trigger RTC software reset
    RTC->MODE0.CTRL.reg |= RTC_MODE0_CTRL_SWRST;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization

    // configure RTC in mode 0 (32-bit)
    RTC->MODE0.CTRL.reg |= RTC_MODE0_CTRL_PRESCALER_DIV1 | RTC_MODE0_CTRL_MODE_COUNT32;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization

    // set match_clear bit(7) to clear counter for periodic interrupts
    // this will probably screw up anything else using the RTC
    // See Table 18-1. MODE0 - Mode Register Summary
    RTC->MODE0.CTRL.reg |= RTC_MODE0_CTRL_MATCHCLR;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization

    // enter freq correction here as sign (bit7) and magnitude (bits 6-0)
    RTC_FREQCORR_VALUE(u8_correction); // adjust if necessary

    // initialize counter & compare values
    RTC->MODE0.COUNT.reg = 0;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization
    RTC->MODE0.COMP[0].reg = u32_compare;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization

    // enable the CMP0 interrupt in the RTC
    RTC->MODE0.INTENSET.reg |= RTC_MODE0_INTENSET_CMP0;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization

    // re-enable RTC after reconfiguration and initial scheduling
    RTC->MODE0.CTRL.reg |= RTC_MODE0_CTRL_ENABLE;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY); // Wait for synchronization

    // enable continuous synchronization
    while (RTC->MODE0.STATUS.bit.SYNCBUSY);
    RTC->MODE0.READREQ.reg = RTC_READREQ_RREQ | RTC_READREQ_RCONT | 0x0010;
    while (RTC->MODE0.STATUS.bit.SYNCBUSY);

    // enable RTC interrupt in controller
    NVIC_SetPriority(RTC_IRQn, 0x00);
    NVIC_EnableIRQ(RTC_IRQn);
    }