John Bradnam
Published © GPL3+

Electronic Game Buddy

A highly configurable electronic game buddy for games such as Yahtzee, Poker, Blackjack, Rock/Paper/Scissors, Coin Toss and many more.

IntermediateFull instructions provided8 hours737

Things used in this project

Hardware components

Microchip ATtiny1614 Microprocessor
×1
0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
I2C variant
×1
PTS 645 Series Switch
C&K Switches PTS 645 Series Switch
×1
Polymer Lithium Ion Battery (LiPo) 3.7V 120mAh
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

EightDiceSet_V3.ino

C/C++
/*============================================================================================================
  Eight Dice Set

  CPU:  ATtiny1614
  Display: 0.91" OLED
 
  ATtiny1614 Code: jbrad2089@gmail.com
  Concept: raul7321 (https://www.instructables.com/Fully-IR-Customizable-Eight-dice-Set/)

  Modes:
  COIN: 2 choices       - Thumbs Up or Thumbs down
  RPS: 3 choices        - Rock, Paper Sissors
  DICE: 6 choices       - Standard dice
  CARD_RED: 52 choices  - All red decks share a single deck that is there won't be any duplicate
	                        cards shown t the same time. Cards are put back in the eck for the next
                          draw.
  CAD_BLACK: 52 choices - All black decks are concatenated and black cards are drawn from that
	                        concatenated deck. Cards are not pur back into the deck. There is a
                          random marker that when encountered causes all the cards to go back
                          in the decks and decks are resuffled.
  
  BOARD: ATtiny1614/1604/814/804/414/404/214/204
  Chip: ATtiny1614
  Clock Speed: 20MHz
  Programmer: jtag2updi (megaTinyCore)

  ATTiny1614 Pins mapped to Ardunio Pins

             +--------+
         VCC + 1   14 + GND
 (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
       1 PA5 + 3   12 + PA2 9  (MISO)
 (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
       3 PA7 + 5   10 + PA0 11 (UPDI)
 (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
 (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
             +--------+

  PA0 to PA7 can be analog or digital
  PWM on D0, D1, D6, D7, D10
  
==============================================================================================================*/

#include <EEPROM.h>
#include "symbols.h"
#include "button.h"
#include "SSD1306_ATtiny1614.h"

#define ROLL 0    //(PA4)
#define RANDOM 1  //(PA5)

//Uncomment next line to clear out EEPROM and reset all high scores
//#define RESET_EEPROM

#define NO_CURSOR -1
int cursor;
bool inMenu;

enum MODES {MODE_OFF, COIN, RPS, DICE, CARD_RED, CARD_BLACK, MODE_LAST};

#define ELEMENTS 8

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
  uint32_t magic;
  MODES mode[ELEMENTS];
  int value[ELEMENTS];
  long seed;
  long blackSeed;
  long blackCount;
} EEPROM_DATA;

EEPROM_DATA EepromData;      //Current EEPROM settings
MODES lastMode[ELEMENTS];

bool blackDecks[52 * ELEMENTS];
int maxBlackCards;
long blackRandomNext;

/* Timing constants that ontrol how the elements animate */
#define START_DELAY_TIME 10
#define INCREMENT_DELAY_TIME 5
#define MAX_DELAY_BEFORE_STOP 100
#define MIN_SPIN_TIME 1000
#define MAX_SPIN_TIME 3000

/* spinElement holds the timing information for each element */
struct spinElement 
{
  unsigned long delayTime;
  unsigned long spinTime;
  unsigned long frameTime;
  bool stopped;
};

spinElement spin[ELEMENTS]; 

#define SLEEP_TIMEOUT 10000  //Amount of time no switch is pressed before going to sleep
uint32_t sleepTimeout = 0;   //Used to timeout mode switch function to sleep function

#define FLASH_TIMEOUT 300    //Flash time for cursor
uint32_t flashTimeout = 0;   //Used to timeout mode cursor flash
bool flashInvert = false;    //Invert state

#define LONG_PRESS_PERIOD 500
void rollButtonRaised(void);
Button* rollButton;
uint32_t rollLongPeriod;

//--------------------------------------------------------------------
//Handle pin change interrupt when button is pressed
//This wakes up the ATtiny1614 from its slumber
void SwitchInterrupt()
{
}

//--------------------------------------------------------------------
//Program Setup
void setup() 
{
  pinMode(ROLL, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ROLL),SwitchInterrupt,CHANGE);
  ssd1306_init();

  //Initialise button
  rollButton = new Button(ROLL);

  //Setup cards
  readEepromData();
  randomSeed(EepromData.seed);
  blackRandomNext = EepromData.blackSeed;
  createBlackCardDecks();

  //Finish setup
  setupAfterSleep();
}

//--------------------------------------------------------------------
//Reinitialise program after going to sleep
void setupAfterSleep()
{
  ssd1306_fillscreen(0x00);
  updateScreen(NO_CURSOR);
  sleepTimeout = millis() + SLEEP_TIMEOUT;
  inMenu = false;
}
  
//--------------------------------------------------------------------
//Program Loop
void loop() 
{
  if (inMenu)
  {
    doMenu(); 
  }
  else if (millis() >= sleepTimeout)
  {
    system_sleep();
  }
  else if (rollButton->Pressed())
  {
    if (rollButton->DownTime() >= LONG_PRESS_PERIOD)
    {
      //Enter menu mode
      cursor = 0;
      inMenu = true; 
      flashTimeout = millis() + FLASH_TIMEOUT;
      rollButton->Repeat(rollButtonPressed);
      for (int i = 0; i < ELEMENTS; i++)
      {
        lastMode[i] = EepromData.mode[i];   //Store each mode to test for changes
        switch (EepromData.mode[i])
        {
          case COIN: EepromData.value[i] = 1; break;
          case RPS: EepromData.value[i] = 2; break;
          case DICE: EepromData.value[i] = 2; break;
          case CARD_RED: EepromData.value[i] = 0; break; //Ace of Hearts
          case CARD_BLACK: EepromData.value[i] = 13 * 2; break; //Ace of Spades
        }
      }
    }
    else
    {
      //Perform a roll
      doRoll(); 
      sleepTimeout = millis() + SLEEP_TIMEOUT;
    }
  }
  else
  {
    delay(100);
  }
}

//--------------------------------------------------------------------
//Roll all items
void doRoll()
{
  //Reset wheels for the spin
  int stopCount = 0;  
  for (uint8_t j = 0; j < ELEMENTS; j++)
  {
    spin[j].stopped = (EepromData.mode[j] == MODE_OFF);
    if (spin[j].stopped)
    {
      stopCount++;
    }
    else
    {
      spin[j].delayTime = START_DELAY_TIME;
      spin[j].spinTime = random(MIN_SPIN_TIME, MAX_SPIN_TIME);
      spin[j].frameTime = millis() + spin[j].delayTime;
    }
  }

  bool hasBlackDecks = false;
  while (stopCount < ELEMENTS)
  {
    //Update each element
    for (uint8_t j = 0; j < ELEMENTS; j++)
    {
      hasBlackDecks |= (EepromData.mode[j] == CARD_BLACK);
      if (!spin[j].stopped && millis() > spin[j].frameTime)
      {
        spin[j].frameTime = millis() + spin[j].delayTime;
        switch (EepromData.mode[j])
        {
          case COIN: EepromData.value[j] = random(2); break;
          case RPS: EepromData.value[j] = random(3); break;
          case DICE: EepromData.value[j] = random(6); break;
          case CARD_RED: EepromData.value[j] = redRandom(j); break;
          case CARD_BLACK: EepromData.value[j] = random(52); break;
        }
        if (millis() > spin[j].spinTime)
        {
          //Stop if delayTime exceeds MAX_DELAY_BEFORE_STOP
          if (spin[j].delayTime > MAX_DELAY_BEFORE_STOP)
          {
            spin[j].stopped = true;
            stopCount++;
          }
          else
          {
            spin[j].delayTime = spin[j].delayTime + INCREMENT_DELAY_TIME;
          }
        }
      }
      updateScreen(NO_CURSOR);
    }
    yield();
  }

  //Black cards are special as they come from a concatenated deck and are not put back
  if (hasBlackDecks)
  {
    getBlackCards();
    //need to record cards removed from decks
    EepromData.seed = random(2147483647);
    writeEepromData();
  }
    
  updateScreen(NO_CURSOR);
}

//--------------------------------------------------------------------
//Ensure all red card picks are unique across every red pack
// element - current element to choose a card for
int redRandom(int element)
{
  int card;
  bool match = true;
  while (match)
  {
    card = random(52);
    match = false;
    for (int i = 0; i < ELEMENTS; i++)
    {
      if (i != element && EepromData.mode[i] == CARD_RED && EepromData.value[i] == card)
      {
        match = true;
        break;
      }
    }
  }
  return card;
}

//--------------------------------------------------------------------
//Populate all black cards from the black decks
void getBlackCards()
{
  //First clear out all black cards
  for (int i = 0; i < ELEMENTS; i++)
  {
    if (EepromData.mode[i] == CARD_BLACK)
    {
      EepromData.value[i] = -1;
    }
  }
  //Now assign the next card from the black decks
  for (int i = 0; i < ELEMENTS; i++)
  {
    if (EepromData.mode[i] == CARD_BLACK)
    {
      EepromData.value[i] = getNextBlackCard();
    }
  }
}

//--------------------------------------------------------------------
//Populate all black cards from the black decks
int getNextBlackCard()
{
  int card = 0;
  int tries = 0;
  do
  {
    card = getBlackRandom() % maxBlackCards;
    tries++;
  }
  while (blackDecks[card] && tries < 1000);

  //If we couldn't find a empty slot after 1000 attempts, shuffle the decks
  if (blackDecks[card])
  {
    //Shuffle deck
    shuffleDecks();
    card = getNextBlackCard();
  }
  else
  {
    //Stop card from being chosen again
    blackDecks[card] = true;
  }
  //card can be from any of the decks
  return card % 52;
}

//--------------------------------------------------------------------
//Black deck cards are drawn without putting back in the pack (eg blackjack)
void createBlackCardDecks()
{
  //Count how many actual black decks we have
  maxBlackCards = 0;
  for (int i = 0; i < ELEMENTS; i++)
  {
    if (EepromData.mode[i] == CARD_BLACK)
    {
      maxBlackCards = maxBlackCards + 52;
    }
  }
  if (maxBlackCards > 0)
  {
    //Clear out deck table
    for (int i = 0; i < (52 * ELEMENTS); i++)
    {
      blackDecks[i] = false;
    }
    //Fill up the table with cards already drawn from the pack
    long count = EepromData.blackCount;
    EepromData.blackCount = 0;
    for (long i = 0; i < count; i++)
    {
      blackDecks[getBlackRandom() % maxBlackCards] = true;
    }
  }
}

//--------------------------------------------------------------------
// Clear black deck tables ready for a new set of cards
void shuffleDecks()
{
  //Shuffle deck
  EepromData.blackCount = 0;
  EepromData.blackSeed = random(2147483647);
  blackRandomNext = EepromData.blackSeed;
  createBlackCardDecks();
}

//--------------------------------------------------------------------
//Random number generator taken from avr-libc-2.0.0\libc\stdlib\random.c
long getBlackRandom()
{
  /*
  * Compute x = (7^5 * x) mod (2^31 - 1)
  * wihout overflowing 31 bits:
  *      (2^31 - 1) = 127773 * (7^5) + 2836
  * From "Random number generators: good ones are hard to find",
  * Park and Miller, Communications of the ACM, vol. 31, no. 10,
  * October 1988, p. 1195.
  */
  long hi, lo, x;
  
  x = blackRandomNext;
  /* Can't be initialized with 0, so use another value. */
  if (x == 0)
      x = 123459876L;
  hi = x / 127773L;
  lo = x % 127773L;
  x = 16807L * lo - 2836L * hi;
  if (x < 0)
      x += 0x7fffffffL;
  blackRandomNext = x;
  EepromData.blackCount++;
  return x;
}

//--------------------------------------------------------------------
//Enter the menu
void doMenu()
{
  if (millis() >= flashTimeout)
  {
    flashInvert = !flashInvert;
    flashTimeout = millis() + FLASH_TIMEOUT;
    updateScreen((flashInvert) ? -1 : cursor);
  }
  rollLongPeriod = millis() + LONG_PRESS_PERIOD;
  if (rollButton->Pressed() && (millis() < rollLongPeriod))
  {
    //This is a short press - long presses are handled by the repeat function
    cursor = cursor + 1;
    if (cursor < ELEMENTS)
    {
      updateScreen((flashInvert) ? -1 : cursor);
    }
    else
    {
      //Exiting menu mode
      rollButton->Repeat(NULL);
      updateScreen(-1);
      inMenu = false;

      bool changed = false;
      bool blackChanged = false;
      bool hasBlackDecks = false;
      for (int i = 0; i < ELEMENTS; i++)
      {
        if (lastMode[i] != EepromData.mode[i])
        {
          blackChanged |= (lastMode[i] == CARD_BLACK);
          blackChanged |= (EepromData.mode[i] == CARD_BLACK);
          changed = true;
        }
      }

      if (changed)
      {
        if (blackChanged)
        {
          shuffleDecks();
        }
        //When modes have changed store it to EEPROM
        EepromData.seed = random(2147483647);
        writeEepromData();
      }
      
      sleepTimeout = millis() + SLEEP_TIMEOUT;
    }
  }
  delay(100);
}

//-----------------------------------------------------------------------------------
//Invoked via the repeat function of the roll button
void rollButtonPressed()
{
  if (millis() >= rollLongPeriod)
  {
    //Only do something on a long press
    EepromData.mode[cursor] = (EepromData.mode[cursor] == CARD_BLACK) ? MODE_OFF : (MODES)((int)EepromData.mode[cursor] + 1);
    switch (EepromData.mode[cursor])
    {
      case COIN: EepromData.value[cursor] = 1; break;
      case RPS: EepromData.value[cursor] = 2; break;
      case DICE: EepromData.value[cursor] = 2; break;
      case CARD_RED: EepromData.value[cursor] = 0; break; //Ace of Hearts
      case CARD_BLACK: EepromData.value[cursor] = 13 * 2; break; //Ace of Spades
    }
    updateScreen(cursor);
  }
}

//--------------------------------------------------------------------
//Update the screen with the current symbols
// c = -1 for no cursor, 0 to 7 to invert symbol
void updateScreen(int c)
{
  int x;
  int y;
  
  for (int i = 0; i < ELEMENTS; i++)
  {
    y = (i >= 4) ? 4 : 0;
    x = (i & 0x03) << 5;
    switch (EepromData.mode[i])
    {
      case MODE_OFF: drawEmptySpace(x, y, (i == c)); break;
      case COIN: drawThumbsUpDown(x, y, EepromData.value[i], (i == c)); break;
      case RPS: drawRockPaperSissors(x, y, EepromData.value[i], (i == c)); break;
      case DICE: drawDice(x, y, EepromData.value[i], (i == c)); break;
      case CARD_RED: 
      case CARD_BLACK: drawCard(x, y, EepromData.value[i], (i == c)); break;
    }
  }
}

//--------------------------------------------------------------------
//Draw thumbs up / thumbs down
// x = X position of mid point
// y = Y position of bottom point
void drawEmptySpace(int x, int y, bool invert)
{
  ssd1306_draw_bmp(x, y, 24, 3, space, invert);
}

//--------------------------------------------------------------------
//Draw thumbs up / thumbs down
// x = X position of mid point
// y = Y position of bottom point
// v = 0 or 1
void drawThumbsUpDown(int x, int y, int v, bool invert)
{
  ssd1306_draw_bmp(x, y, 24, 3, thumbs[v], invert);
}

//--------------------------------------------------------------------
//Draw rock / paper /sissors
// x = X position of mid point
// y = Y position of bottom point
// v = 0 or 1 or 2
void drawRockPaperSissors(int x, int y, int v, bool invert)
{
  ssd1306_draw_bmp(x, y, 24, 3, rps[v], invert);
}

//--------------------------------------------------------------------
//Draw standard dice
// x = X position of mid point
// y = Y position of bottom point
// v = 0 to 5
void drawDice(int x, int y, int v, bool invert)
{
  ssd1306_draw_bmp(x, y, 24, 3, dice[v], invert);
}  

//--------------------------------------------------------------------
//Draw card
// x = X position of mid point
// y = Y position of bottom point
// v = 0 to 51
void drawCard(int x, int y, int v, bool invert)
{
  int s = v / 13;
  int f = v % 13;
  ssd1306_draw_merge_bmp(x, y, 24, 3, cards[f], cards[s + 13], invert);
}

//--------------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData(void)
{
  //This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
  EEPROM.put(EEPROM_ADDRESS,EepromData);
}

//--------------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData(void)
{
#ifndef RESET_EEPROM
  EEPROM.get(EEPROM_ADDRESS,EepromData);
  if (EepromData.magic != EEPROM_MAGIC)
  {
#endif  
    EepromData.magic = EEPROM_MAGIC;
    EepromData.seed = analogRead(RANDOM);
    EepromData.blackSeed = analogRead(RANDOM);
    EepromData.blackCount = 0;
    /*
    for (int x = 0; x < 8; x++)
    {
      EepromData.mode[x] = CARD_BLACK;
      EepromData.value[x] = 13 * 3;
    }
    */
    EepromData.mode[0] = COIN; EepromData.value[0] = 0;
    EepromData.mode[1] = COIN; EepromData.value[1] = 1;
    EepromData.mode[2] = CARD_RED; EepromData.value[2] = 0;
    EepromData.mode[3] = CARD_BLACK; EepromData.value[3] = 13 * 3;  
    EepromData.mode[4] = RPS; EepromData.value[4] = 0;
    EepromData.mode[5] = RPS; EepromData.value[5] = 1;
    EepromData.mode[6] = RPS; EepromData.value[6] = 2;
    EepromData.mode[7] = DICE; EepromData.value[7] = 4;
    writeEepromData();
#ifndef RESET_EEPROM
  }
#endif  
}

//--------------------------------------------------------------------
//Shut down OLED and put ATtiny to sleep
//Will wake up when LEFT button is pressed
void system_sleep() 
{
  interrupts();
  ssd1306_sleep();
  //Wait until on button is released
  while (digitalRead(ROLL) == LOW)
  {
    delay(100);
  }
  setupAfterSleep();
}

symbols.h

C Header File
#pragma once

const uint8_t space[72] PROGMEM = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};

const uint8_t thumbs[2][72] PROGMEM = {{
0xF8,0x08,0x08,0x08,0x08,0x08,0xF8,0x10,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x48,0x48,0x58,0x60,0x80,0x00,
0x3F,0x20,0x20,0x20,0x20,0x20,0x3F,0x10,0x30,0x60,0xC0,0x80,0x00,0x00,0x00,0x80,0x80,0x92,0x92,0x92,0x92,0x92,0xFF,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x3E,0x20,0x20,0x1F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
},{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x7C,0x04,0x04,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0xFC,0x04,0x04,0x04,0x04,0x04,0xFC,0x08,0x0C,0x06,0x03,0x01,0x00,0x00,0x00,0x01,0x01,0x49,0x49,0x49,0x49,0x49,0xFF,0x00,
0x1F,0x10,0x10,0x10,0x10,0x10,0x1F,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x12,0x12,0x1A,0x06,0x01,0x00
}};

const uint8_t rps[3][72] PROGMEM = {{
0x80,0xE0,0x70,0xF0,0xF0,0x70,0x38,0x18,0x1C,0x0C,0x0E,0x06,0x66,0x66,0x6E,0xEC,0xDC,0xF8,0xF0,0xC0,0xC0,0x80,0x00,0x00,
0xFF,0xC1,0xFC,0xFF,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x18,0x18,0x18,0x18,0x38,0x3D,0xEF,0x00,
0x00,0x00,0x00,0x03,0x07,0x0E,0x0C,0x1C,0x18,0x38,0x30,0x30,0x30,0x30,0x30,0x31,0x31,0x33,0x33,0x1F,0x1F,0x07,0x01,0x00
},{
0x00,0xC0,0xE0,0x20,0x60,0xC0,0xC0,0x00,0x00,0x80,0xC0,0x70,0x38,0x1C,0x84,0xC4,0xE4,0xBC,0xC0,0x40,0x40,0xC0,0xC0,0x00,
0x00,0x81,0xFF,0x1C,0x00,0x00,0x0F,0x06,0x03,0x01,0x00,0x00,0x1C,0x0E,0x03,0x63,0x71,0x31,0x38,0xBC,0x9C,0x96,0xD3,0x70,
0x00,0x07,0x1D,0x33,0x2C,0x28,0x30,0x30,0x20,0x20,0x30,0x30,0x18,0x18,0x0C,0x06,0x06,0x03,0x03,0x01,0x01,0x00,0x00,0x00
},{
0x00,0x80,0xE0,0x30,0x90,0x70,0x30,0x30,0x10,0x10,0x10,0xD0,0x90,0xB0,0xE0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x0F,0x10,0x7E,0xC1,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x11,0x11,0x11,0x31,0x31,0x71,0xD1,0xB3,0xA2,0x26,0x3C,
0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x03,0x06,0x06,0x06,0x06,0x07,0x03,0x03,0x03,0x02,0x06,0x04,0x0C,0x08,0x09,0x0F,0x00
}};

const uint8_t dice[6][72] PROGMEM = {{
0x00,0xFC,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0xFC,
0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1C,0x3E,0x3E,0x3E,0x1C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x00,0x1F,0x20,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x20,0x1F
},{
0x00,0xFC,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x39,0x7D,0x7D,0x7D,0x39,0x02,0xFC,
0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x00,0x1F,0x20,0x4E,0x5F,0x5F,0x5F,0x4E,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x20,0x1F
},{
0x00,0xFC,0x02,0x39,0x7D,0x7D,0x7D,0x39,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x02,0xFC,
0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1C,0x3E,0x3E,0x3E,0x1C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x00,0x1F,0x20,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x4E,0x5F,0x5F,0x5F,0x4E,0x20,0x1F
},{
0x00,0xFC,0x02,0x39,0x7D,0x7D,0x7D,0x39,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x39,0x7D,0x7D,0x7D,0x39,0x02,0xFC,
0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x00,0x1F,0x20,0x4E,0x5F,0x5F,0x5F,0x4E,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x4E,0x5F,0x5F,0x5F,0x4E,0x20,0x1F
},{
0x00,0xFC,0x02,0x39,0x7D,0x7D,0x7D,0x39,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x39,0x7D,0x7D,0x7D,0x39,0x02,0xFC,
0x00,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1C,0x3E,0x3E,0x3E,0x1C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x00,0x1F,0x20,0x4E,0x5F,0x5F,0x5F,0x4E,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x4E,0x5F,0x5F,0x5F,0x4E,0x20,0x1F
},{
0x00,0xFC,0x02,0x39,0x7D,0x7D,0x7D,0x39,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x39,0x7D,0x7D,0x7D,0x39,0x02,0xFC,
0x00,0xFF,0x00,0x1C,0x3E,0x3E,0x3E,0x1C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1C,0x3E,0x3E,0x3E,0x1C,0x00,0xFF,
0x00,0x1F,0x20,0x4E,0x5F,0x5F,0x5F,0x4E,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x4E,0x5F,0x5F,0x5F,0x4E,0x20,0x1F
}};

#define HEART 13
#define DIAMOND 14
#define SPADE 15
#define CLUB 16

const uint8_t cards[17][72] PROGMEM = {{  
//Ace
0xFE,0x03,0xF1,0xF9,0x1D,0x0D,0x0D,0x0D,0x0D,0x1D,0xF9,0xF1,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0xFF,0xFF,0x03,0x03,0x03,0x03,0x03,0x03,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Two
0xFE,0x03,0x31,0x39,0x1D,0x0D,0x0D,0x0D,0x8D,0xDD,0xF9,0x71,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0xE0,0xF0,0xF8,0xDC,0xCE,0xC7,0xC3,0xC1,0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Three
0xFE,0x03,0x31,0x39,0x1D,0x0D,0x0D,0x0D,0x8D,0xDD,0xF9,0x71,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x30,0x70,0xE0,0xC3,0xC3,0xC3,0xC7,0xEF,0x7C,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Four
0xFE,0x03,0x01,0x81,0xC1,0xE1,0x71,0x39,0xFD,0xFD,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x0F,0x0F,0x0D,0x0C,0x0C,0x0C,0xFF,0xFF,0x0C,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Five
0xFE,0x03,0x7D,0xFD,0xCD,0x8D,0x8D,0x8D,0x8D,0x8D,0x0D,0x0D,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x30,0x70,0xE1,0xC1,0xC1,0xC1,0xC1,0xE3,0x7F,0x3E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Six
0xFE,0x03,0xF1,0xF9,0x1D,0x0D,0x0D,0x0D,0x0D,0x3D,0x79,0x71,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x3F,0x7F,0xEF,0xC6,0xC6,0xC6,0xC6,0xEE,0x7C,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Seven
0xFE,0x03,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0xCD,0xFD,0x3D,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x00,0x00,0x00,0x00,0xF8,0xFE,0x07,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Eight
0xFE,0x03,0x71,0xF9,0xDD,0x8D,0x0D,0x0D,0x8D,0xDD,0xF9,0x71,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x38,0x7C,0xEF,0xC7,0xC3,0xC3,0xC7,0xEF,0x7C,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Nine
0xFE,0x03,0x71,0xF9,0xDD,0x8D,0x8D,0x8D,0x8D,0xDD,0xF9,0xF1,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x38,0x78,0xF1,0xC1,0xC1,0xC1,0xC1,0xE3,0x7F,0x3F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Ten
0xFE,0x03,0x31,0x31,0xFD,0xFD,0x01,0x01,0xC1,0xC1,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x0C,0x0C,0x0F,0x0F,0x3F,0x3F,0xC0,0xC0,0x3F,0x3F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Jack
0xFE,0x03,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0xFD,0xFD,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x3C,0x7C,0xE0,0xC0,0xC0,0xC0,0xC0,0xE0,0x7F,0x3F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Queen
0xFE,0x03,0xF1,0xF9,0x1D,0x0D,0x0D,0x0D,0x0D,0x1D,0xF9,0xF1,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x0F,0x1F,0x38,0x30,0x30,0x38,0x38,0x78,0xFF,0xCF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//King
0xFE,0x03,0xFD,0xFD,0x81,0xC1,0xE1,0x71,0x39,0x1D,0x0D,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0xFF,0xFF,0x07,0x0F,0x1C,0x38,0x70,0xE0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,0x7F
},{
//Heart
0xFE,0x03,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xF8,0xFC,0xF8,0xF0,0xF0,0xF8,0xFC,0xF8,0xE0,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x81,0x83,0x8F,0x9F,0xBF,0xBF,0x9F,0x8F,0x83,0x81,0xC0,0x7F
},{
//Diamond
0xFE,0x03,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xC0,0xF0,0xF8,0xFC,0xFC,0xF8,0xF0,0xC0,0x80,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x81,0x83,0x8F,0x9F,0xBF,0xBF,0x9F,0x8F,0x83,0x81,0xC0,0x7F
},{
//Spade
0xFE,0x03,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0xC0,0xF0,0xF0,0xFC,0xFC,0xF0,0xF0,0xC0,0xC0,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x83,0xB3,0xB3,0xBF,0xBF,0xB3,0xB3,0x83,0x80,0xC0,0x7F
},{
//Club
0xFE,0x03,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x03,0xFE,
0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0xE0,0xC8,0x9C,0xFC,0xFC,0x9C,0xC8,0xE0,0xC0,0x00,0xFF,
0x7F,0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x83,0x87,0xB3,0xB1,0xBF,0xBF,0xB1,0xB3,0x87,0x83,0xC0,0x7F
}};

Button.h

C Header File
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#pragma once
#include "Arduino.h"

#define DEBOUNCE_DELAY 10

//Repeat speed - (slow with no increase)
#define REPEAT_START_SPEED 1000
#define REPEAT_INCREASE_SPEED 0
#define REPEAT_MAX_SPEED 50
/*
//Repeat speed
#define REPEAT_START_SPEED 500
#define REPEAT_INCREASE_SPEED 50
#define REPEAT_MAX_SPEED 50
 */

class Button
{
	public:
	//Simple constructor
	Button(int pin);
	Button(int name, int pin);
	Button(int name, int pin, int analogLow, int analogHigh, bool activeLow = true);

  //Background function called when in a wait or repeat loop
  void Background(void (*pBackgroundFunction)());
	//Repeat function called when button is pressed
  void Repeat(void (*pRepeatFunction)());
	//Test if button is pressed
	bool IsDown(void);
	//Test whether button is pressed and released
	//Will call repeat function if one is provided
	bool Pressed();
	//Return button state (HIGH or LOW) - LOW = Pressed
	int State();
  //Return length of time button was held down
  long DownTime();
  //Return button name
  int Name();

	private:
		int _name;
		int _pin;
		bool _range;
		int _low;
		int _high;
    long _downTime;
		bool _activeLow;
		void (*_repeatCallback)(void);
		void (*_backgroundCallback)(void);
};

Button.cpp

C/C++
/*
Class: Button
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Arduino library to handle buttons
*/
#include "Button.h"

Button::Button(int pin)
{
  _name = pin;
  _pin = pin;
  _range = false;
  _low = 0;
  _high = 0;
  _backgroundCallback = NULL;
  _repeatCallback = NULL;
  pinMode(_pin, INPUT_PULLUP);
}

Button::Button(int name, int pin)
{
  _name = name;
  _pin = pin;
  _range = false;
  _low = 0;
  _high = 0;
  _backgroundCallback = NULL;
  _repeatCallback = NULL;
  pinMode(_pin, INPUT_PULLUP);
}

Button::Button(int name, int pin, int analogLow, int analogHigh, bool activeLow)
{
  _name = name;
  _pin = pin;
  _range = true;
  _low = analogLow;
  _high = analogHigh;
  _activeLow = activeLow;
  _backgroundCallback = NULL;
  _repeatCallback = NULL;
  pinMode(_pin, INPUT);
}

//Set function to invoke in a delay or repeat loop
void Button::Background(void (*pBackgroundFunction)())
{
  _backgroundCallback = pBackgroundFunction;
}

//Set function to invoke if repeat system required
void Button::Repeat(void (*pRepeatFunction)())
{
  _repeatCallback = pRepeatFunction;
}

bool Button::IsDown()
{
	if (_range)
	{
		int value = analogRead(_pin);
		return (value >= _low && value < _high);
	}
	else
	{
		return (digitalRead(_pin) == LOW);
	}
}

//Tests if a button is pressed and released
//  returns true if the button was pressed and released
//	if repeat callback supplied, the callback is called while the key is pressed
bool Button::Pressed()
{
  bool pressed = false;
  if (IsDown())
  {
    unsigned long wait = millis() + DEBOUNCE_DELAY;
    while (millis() < wait)
    {
      if (_backgroundCallback != NULL)
      {
        _backgroundCallback();
      }
    }
    if (IsDown())
    {
  	  //Set up for repeat loop
  	  if (_repeatCallback != NULL)
  	  {
  	    _repeatCallback();
  	  }
  	  unsigned long speed = REPEAT_START_SPEED;
  	  unsigned long time = millis() + speed;
      unsigned long start = millis();
      while (IsDown())
      {
        if (_backgroundCallback != NULL)
        {
          _backgroundCallback();
        }
    		if (_repeatCallback != NULL && millis() >= time)
    		{
    		  _repeatCallback();
    		  unsigned long faster = speed - REPEAT_INCREASE_SPEED;
    		  if (faster >= REPEAT_MAX_SPEED)
    		  {
    			  speed = faster;
    		  }
    		  time = millis() + speed;
    		}
      }
      _downTime = millis() - start;
      pressed = true;
    }
  }
  return pressed;
}

//Return current button state
int Button::State()
{
	if (_range)
	{
		int value = analogRead(_pin);
		if (_activeLow)
		{
			return (value >= _low && value < _high) ? LOW : HIGH;
		}
		else 
		{
			return (value >= _low && value < _high) ? HIGH : LOW;
		}
	}
	else
	{
		return digitalRead(_pin);
	}
}

//Return time button was down
long Button::DownTime()
{
  return _downTime;
}

//Return current button name
int Button::Name()
{
	return _name;
}

SSD1306_ATtiny1614.h

C Header File
/*--------------------------------------------------------
 * I2C SSD1306 128x64 OLED support routines for ATtiny1614
 * 
 * @created: 2020-05-12
 * @author: John Bradnam
 *
 * Base on ATTiny85 source code available at: https://bitbucket.org/tinusaur/ssd1306xled
 */

#ifndef SSD1306_ATtiny1614_h
#define SSD1306_ATtiny1614_h

#include <avr/sleep.h>

//---------------------- ATtiny1614 ----------------------

#define SSD1306_SCL   PIN0_bm   // ----> [SCL]  Pin 3 on the SSD1306 display board
#define SSD1306_SDA   PIN1_bm   // ----> [SDA]  Pin 4 on the SSD1306 display board
#define SSD1306_SA    0x78  // Slave address

#define DIGITAL_WRITE_HIGH(PORT) PORTB.OUTSET = PORT
#define DIGITAL_WRITE_LOW(PORT) PORTB.OUTCLR = PORT

//----------------- Forward references -------------------

void ssd1306_init(void);
void ssd1306_xfer_start(void);
void ssd1306_xfer_stop(void);
void ssd1306_send_byte(uint8_t byte);
void ssd1306_send_command(uint8_t command);
void ssd1306_send_data_start(void);
void ssd1306_send_data_stop(void);
void ssd1306_setpos(uint8_t x, uint8_t y);
void ssd1306_fillscreen(uint8_t fill_Data);
void ssd1306_draw_bmp(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t bitmap[], bool invert);
void ssd1306_draw_merge_bmp(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t bitmap1[], const uint8_t bitmap2[], bool invert);

#ifdef INCLUDE_TEXT_FUNCTIONS
void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[]);
void ssd1306_char_font6x8(char ch) ;
void ssd1306_string_font6x8(char *s); 
void ssd1306_char_f8x16(uint8_t x, uint8_t y, const char ch[]);
#endif

//Extra routines commonly used in ATtiny Arcade Games
void ssd1306_beep(uint8_t pin, int bCount, int bDelay);
void ssd1306_sleep();

//----------------- Functions -------------------

// Some code based on "IIC_wtihout_ACK" by http://www.14blog.com/archives/1358

const uint8_t ssd1306_init_sequence [] PROGMEM = {  // Initialization Sequence
  0xAE,     // Display OFF (sleep mode)
  0x20, 0b00,   // Set Memory Addressing Mode
          // 00=Horizontal Addressing Mode; 01=Vertical Addressing Mode;
          // 10=Page Addressing Mode (RESET); 11=Invalid
  0xB0,     // Set Page Start Address for Page Addressing Mode, 0-7
  0xC8,     // Set COM Output Scan Direction
  0x00,     // ---set low column address
  0x10,     // ---set high column address
  0x40,     // --set start line address
  0x81, 0x3F,   // Set contrast control register
  0xA1,     // Set Segment Re-map. A0=address mapped; A1=address 127 mapped. 
  0xA6,     // Set display mode. A6=Normal; A7=Inverse
  0xA8, 0x3F,   // Set multiplex ratio(1 to 64)
  0xA4,     // Output RAM to Display
          // 0xA4=Output follows RAM content; 0xA5,Output ignores RAM content
  0xD3, 0x00,   // Set display offset. 00 = no offset
  0xD5,     // --set display clock divide ratio/oscillator frequency
  0xF0,     // --set divide ratio
  0xD9, 0x22,   // Set pre-charge period
  0xDA, 0x12,   // Set com pins hardware configuration    
  0xDB,     // --set vcomh
  0x20,     // 0x20,0.77xVcc
  0x8D, 0x14,   // Set DC-DC enable
  0xAF      // Display ON in normal mode
  
};

void ssd1306_init(void)
{
  PORTB.DIRSET = SSD1306_SDA; // Set port as output
  PORTB.DIRSET = SSD1306_SCL; // Set port as output
  
  for (uint8_t i = 0; i < sizeof (ssd1306_init_sequence); i++) 
  {
    ssd1306_send_command(pgm_read_byte(&ssd1306_init_sequence[i]));
  }
}

void ssd1306_xfer_start(void)
{
  DIGITAL_WRITE_HIGH(SSD1306_SCL);  // Set to HIGH
  DIGITAL_WRITE_HIGH(SSD1306_SDA);  // Set to HIGH
  DIGITAL_WRITE_LOW(SSD1306_SDA);   // Set to LOW
  DIGITAL_WRITE_LOW(SSD1306_SCL);   // Set to LOW
}

void ssd1306_xfer_stop(void)
{
  DIGITAL_WRITE_LOW(SSD1306_SCL);   // Set to LOW
  DIGITAL_WRITE_LOW(SSD1306_SDA);   // Set to LOW
  DIGITAL_WRITE_HIGH(SSD1306_SCL);  // Set to HIGH
  DIGITAL_WRITE_HIGH(SSD1306_SDA);  // Set to HIGH
}

void ssd1306_send_byte(uint8_t byte)
{
  uint8_t i;
  for(i=0; i<8; i++)
  {
    if((byte << i) & 0x80)
      DIGITAL_WRITE_HIGH(SSD1306_SDA);
    else
      DIGITAL_WRITE_LOW(SSD1306_SDA);
    
    DIGITAL_WRITE_HIGH(SSD1306_SCL);
    DIGITAL_WRITE_LOW(SSD1306_SCL);
  }
  DIGITAL_WRITE_HIGH(SSD1306_SDA);
  DIGITAL_WRITE_HIGH(SSD1306_SCL);
  DIGITAL_WRITE_LOW(SSD1306_SCL);
}

void ssd1306_send_command(uint8_t command)
{
  ssd1306_xfer_start();
  ssd1306_send_byte(SSD1306_SA);  // Slave address, SA0=0
  ssd1306_send_byte(0x00);  // write command
  ssd1306_send_byte(command);
  ssd1306_xfer_stop();
}

void ssd1306_send_data_start(void)
{
  ssd1306_xfer_start();
  ssd1306_send_byte(SSD1306_SA);
  ssd1306_send_byte(0x40);  //write data
}

void ssd1306_send_data_stop(void)
{
  ssd1306_xfer_stop();
}

void ssd1306_setpos(uint8_t x, uint8_t y)
{
  //X = 0..127, Y = 0..7
  ssd1306_xfer_start();
  ssd1306_send_byte(SSD1306_SA);  //Slave address,SA0=0
  ssd1306_send_byte(0x00);  //write command

  ssd1306_send_byte(0xb0+y);             //Set Page Start Address for Page Addressing Mode 10110xxx
  ssd1306_send_byte(((x&0xf0)>>4)|0x10); //Set Lower Column Start Address for Page Addressing Mode 0000xxxx
  ssd1306_send_byte((x&0x0f)|0x01);      //Set Higher Column Start Address for Page Addressing Mode 0001xxxx

  ssd1306_xfer_stop();
}

void ssd1306_fillscreen(uint8_t fill_Data)
{
  uint8_t m,n;
  for(m=0;m<8;m++)
  {
    ssd1306_send_command(0xb0+m);  //Set Page Start Address for Page Addressing Mode 10110xxx
    ssd1306_send_command(0x00);    //Set Lower Column Start Address for Page Addressing Mode 0000xxxx
    ssd1306_send_command(0x10);    //Set Higher Column Start Address for Page Addressing Mode 0001xxxx
    ssd1306_send_data_start();
    for(n=0;n<128;n++)
    {
      ssd1306_send_byte(fill_Data);
    }
    ssd1306_send_data_stop();
  }
}

void ssd1306_draw_bmp(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t bitmap[], bool invert)
{
  uint8_t x0 = x;
  uint8_t x1 = x + w;
  uint8_t y0 = y;
  uint8_t y1 = y + h;
  uint8_t mask = (invert) ? 0xFF : 0x00;
  
  uint16_t j = 0;
  for (y = y0; y < y1; y++)
  {
    ssd1306_setpos(min(max(x0,0),127),min(max(y,0),63));
    ssd1306_send_data_start();
    for (x = x0; x < x1; x++)
    {
      ssd1306_send_byte(pgm_read_byte(&bitmap[j++]) ^ mask);
    }
    ssd1306_send_data_stop();
  }
}

void ssd1306_draw_merge_bmp(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t bitmap1[], const uint8_t bitmap2[], bool invert)
{
  uint8_t x0 = x;
  uint8_t x1 = x + w;
  uint8_t y0 = y;
  uint8_t y1 = y + h;
  uint8_t b;
  uint8_t mask = (invert) ? 0xFF : 0x00;
  
  uint16_t j = 0;
  for (y = y0; y < y1; y++)
  {
    ssd1306_setpos(min(max(x0,0),127),min(max(y,0),63));
    ssd1306_send_data_start();
    for (x = x0; x < x1; x++)
    {
      b = pgm_read_byte(&bitmap1[j]);
      b |= pgm_read_byte(&bitmap2[j++]);
      ssd1306_send_byte(b ^ mask);
    }
    ssd1306_send_data_stop();
  }
}

//  ssd1306_draw_bmp_rect(0, 0, 128, 8, img1_128x64c1);
void ssd1306_draw_bmp_rect(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, const uint8_t bitmap[])
{
  uint16_t j = 0;
  uint8_t y, x;
  if (y1 % 8 == 0) 
    y = y1 / 8;
  else 
    y = y1 / 8 + 1;
  for (y = y0; y < y1; y++)
  {
    ssd1306_setpos(x0,y);
    ssd1306_send_data_start();
    for (x = x0; x < x1; x++)
    {
      ssd1306_send_byte(pgm_read_byte(&bitmap[j++]));
    }
    ssd1306_send_data_stop();
  }
}

#ifdef INCLUDE_TEXT_FUNCTIONS

void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[])
{
  uint8_t c,i,j=0;
  while(ch[j] != '\0')
  {
    c = ch[j] - 32;
    /*
    if (c >0) c = c - 12;
    if (c >15) c = c - 6;
    if (c>40) c=c-9;
    */
    if(x>126)
    {
      x=0;
      y++;
    }
    ssd1306_setpos(x,y);
    ssd1306_send_data_start();
    for(i=0;i<6;i++)
    {
      ssd1306_send_byte(pgm_read_byte(&ssd1306xled_font6x8[c*6+i]));
    }
    ssd1306_send_data_stop();
    x += 6;
    j++;
  }
}

void ssd1306_char_font6x8(char ch) 
{
  uint8_t i; 
  uint8_t c = ch - 32;
  ssd1306_send_data_start();
  for (i= 0; i < 6; i++)
  {
    ssd1306_send_byte(pgm_read_byte(&ssd1306xled_font6x8[c * 6 + i]));
  }
  ssd1306_send_data_stop();
}

void ssd1306_string_font6x8(char *s) 
{
  while (*s) 
  {
    ssd1306_char_font6x8(*s++);
  }
}

#ifdef ssd1306xled_font8x16

void ssd1306_char_f8x16(uint8_t x, uint8_t y, const char ch[])
{
  uint8_t c, j, i = 0;
  while (ch[j] != '\0')
  {
    c = ch[j] - 32;
    if (x > 120)
    {
      x = 0;
      y++;
    }
    ssd1306_setpos(x, y);
    ssd1306_send_data_start();
    for (i = 0; i < 8; i++)
    {
      ssd1306_send_byte(pgm_read_byte(&ssd1306xled_font8x16[c * 16 + i]));
    }
    ssd1306_send_data_stop();
    ssd1306_setpos(x, y + 1);
    ssd1306_send_data_start();
    for (i = 0; i < 8; i++)
    {
      ssd1306_send_byte(pgm_read_byte(&ssd1306xled_font8x16[c * 16 + i + 8]));
    }
    ssd1306_send_data_stop();
    x += 8;
    j++;
  }
}

#endif

#endif

//--------------------- Extra -------------------------------

//Shut down OLED and put ATtiny to sleep
//Will wake up when LEFT button is pressed
void ssd1306_sleep() 
{
  ssd1306_fillscreen(0x00);
  ssd1306_send_command(0xAE);
  //cbi(ADCSRA,ADEN);                   // switch Analog to Digitalconverter OFF
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // sleep mode is set here
  sleep_enable();
  sleep_mode();                         // System actually sleeps here
  sleep_disable();                      // System continues execution here when watchdog timed out
  //sbi(ADCSRA,ADEN);                   // switch Analog to Digitalconverter ON
  ssd1306_send_command(0xAF);
}
#endif

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to raul7321.

Comments