Skip to content

Instantly share code, notes, and snippets.

@julznc
Last active June 18, 2022 11:37
Show Gist options
  • Save julznc/48da0ecbb0afa2e1ade0389dc1e09ca6 to your computer and use it in GitHub Desktop.
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
#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