You can browse the source code on the GitHub repository here

Click the Download: Project Zip below to just download the whole thing in a zip file. Uncompress and be sure to change the name of the folder to Mini_LED_Gamer after download.

The code for this project doesn't use any external libraries. Instead of using Wire.h to talk to the HT16K33 chip on the LED Matrix Backpack, it uses the i2c.h for the I2C interface.

The reason is that a timer interrupt is used to both refresh the LED matrix and read the state of all the buttons at approximately 50Hz. However, Wire.h uses the timer interrupt for the I2C interface, and you can't have two timer interrupts running inside of each other. The i2c.h does not use any timer interrupt, thus avoiding the conflict. 

It's a little more advanced but works well!

HT16K33.cpp is similar to the Adafruit_LEDBackpack library, but it works with i2c.h instead of Wire.h, and it not only drives the LED matrix but also checks the state of the pushbuttons.

Tetris.cpp, Snake.cpp, and Paint.cpp are each responsible for an individual program that runs on the Tiny Tetris.

// SPDX-FileCopyrightText: 2019 Anne Barela for Adafruit Industries
//
// SPDX-License-Identifier: MIT

#include "HT16K33.h"
#include "Tetris.h"
#include "Snake.h"
#include "Paint.h"

// ADC clock configurations
#define ADPS_16 _BV(ADPS2)
#define ADPS_32 _BV(ADPS2)|_BV(ADPS0)
#define ADPS_128 _BV(ADPS2)|_BV(ADPS1)|_BV(ADPS0)

// Buttons: bits 0-6 in the first byte of the key-scan matrix
#define LEFT 6
#define SELECT 1
#define DOWN 2
#define UP 5
#define RIGHT 0
#define BRIGHTNESS_UP 4
#define BRIGHTNESS_DOWN 3

// Objects
HT16K33 ht(0x70);  // I2C address of the HT16K33 IC
Tetris tetris;
Snake snake;
Paint paint(3,3);  // initilize cursor position to (3,3)

uint8_t* getMenu();
void changeOption(int8_t i);
void changeMode();

// Mode: 0-Undecided; 1-Tetris; 2-Snake; 3-Paint
uint8_t mode=0;

// Interrupt Variables
uint8_t interruptCounter2Hz = 0;

ISR(TIMER1_OVF_vect){  
  // things that need to be done at 50Hz
  ht.readButtons();
  processButtons();
  switch(mode) {
    case 0:
      ht.storeToBuffer(getMenu());
      break;
    case 1:
      ht.storeToBuffer(tetris.getActiveBoard());
      break;
    case 2:
      ht.storeToBuffer(snake.getActiveBoard());
      break;
    case 3: 
      ht.storeToBuffer(paint.getActiveCanvas());
  }
  ht.refreshDisplay();
  
  // things that happen less frequently
  interruptCounter2Hz++;
  if (interruptCounter2Hz==25) {
    interruptCounter2Hz=0;
    paint.flashCursor();
  }
}

void processButtons() {
  // These two buttons are fixed
  if (ht.allowToMove(BRIGHTNESS_DOWN,25,6) && ht.getButtonHoldTime(SELECT)==0) ht.decreaseBrightness();
  if (ht.allowToMove(BRIGHTNESS_UP,25,6) && ht.getButtonHoldTime(SELECT)==0) ht.increaseBrightness();
  
  // change to the previous/next program by pressing holding select and press the brightness control buttons
  if (mode!=0 && ht.getButtonHoldTime(SELECT)>10) {
    if (ht.getButtonFirstPress(BRIGHTNESS_UP)) {
      changeOption(-1);
      changeMode();
    }
    else if (ht.getButtonFirstPress(BRIGHTNESS_DOWN)){
      changeOption(1);
      changeMode();
    }
  }
  
  // control for different programs
  switch(mode) {
    case 0:
      if (ht.getButtonFirstPress(LEFT)) changeOption(-1);
      if (ht.getButtonFirstPress(RIGHT)) changeOption(1);
      if (ht.getButtonFirstPress(SELECT)) changeMode();
      break;
    case 1:
      if (tetris.gameRunning) {
        if (ht.allowToMove(UP,100,4)) tetris.rotatePiece();
        if (ht.allowToMove(DOWN,15,2)) tetris.movePiece(0,1);
        if (ht.allowToMove(LEFT,15,2)) tetris.movePiece(-1,0);
        if (ht.allowToMove(RIGHT,15,2)) tetris.movePiece(1,0);
        if (ht.getButtonFirstPress(SELECT)) tetris.dropPiece();
      }
      else {
        if (ht.getButtonFirstPress(SELECT)) tetris.init();
      }
      break;
    case 2:
      if (snake.gameRunning) {
        if (ht.getButtonFirstPress(UP)) snake.changeDirection(0,-1);
        if (ht.getButtonFirstPress(DOWN)) snake.changeDirection(0,1);
        if (ht.getButtonFirstPress(LEFT)) snake.changeDirection(-1,0);
        if (ht.getButtonFirstPress(RIGHT)) snake.changeDirection(1,0);
      }
      else {
        if (ht.getButtonFirstPress(SELECT)) snake.init();
      }
      break;
    case 3: 
        if (ht.allowToMove(UP,25,2)) paint.moveCursor(0,-1);
        if (ht.allowToMove(DOWN,25,2)) paint.moveCursor(0,1);
        if (ht.allowToMove(LEFT,25,2)) paint.moveCursor(-1,0);
        if (ht.allowToMove(RIGHT,25,2)) paint.moveCursor(1,0);
        if (ht.getButtonFirstPress(SELECT)) paint.draw();
        if (ht.getButtonHoldTime(SELECT)==150) paint.clearCanvas();    
  }
}

void setup(){    
  // Set randomSeed; the first few random() are not used because they never change
  randomSeed(analogRead(A0));
  for (uint8_t i=0;i<4;i++) random();
  
  // Initiate objects
  Serial.begin(115200);
  ht.init();
  tetris.init();
  snake.init();
  
  //ADC: 16MHz/16=1MHz
  ADCSRA &= ~ADPS_128;              // clean out ADPS
  ADCSRA |= ADPS_32;                // set Division Factor
  
  // Timer1 overflow interrupt setup
  TCNT1 = 0;                        // reset the counter
  ICR1 = 19999;                     // top value of the counter (16*10^6Hz/2000000*20000us/8-1) -> 1/20000us = 50Hz
  TCCR1A = 0;                       // clear TCCR1A (not used)
  TCCR1B = _BV(WGM13) | _BV(CS11);  // mode 8(PWM), set prescalar=8;
  TIMSK1 = _BV(TOIE1);              // Enable Overflow Interrupt 
}

void loop(){
  // For both the tetris and snake, they have a function called "run" as the main game loop.
  // For the paint program, all the actions are handled in the timer interrupt, so there is nothing to do in the main loop
  switch(mode) {
    case 0:
      break;
    case 1:
      tetris.run(); break;
    case 2:
      snake.run(); break;
    case 3: 
      ;    
  }
}

This guide was first published on Sep 04, 2014. It was last updated on Sep 04, 2014.

This page (Source Code) was last updated on Mar 20, 2023.

Text editor powered by tinymce.