Last active
June 18, 2022 11:37
-
-
Save julznc/48da0ecbb0afa2e1ade0389dc1e09ca6 to your computer and use it in GitHub Desktop.
Arduino code for DIY mini-chess-clock (SAMD21 + OLED) https://projectproto.blogspot.com/2022/06/diy-mini-chess-clock.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment