Hardware components | ||||||
| × | 1 | ||||
| × | 8 | ||||
| × | 16 | ||||
![]() |
| × | 7 | |||
![]() |
| × | 1 | |||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
| |||||
Hand tools and fabrication machines | ||||||
![]() |
| |||||
![]() |
|
A few years ago I built the 7-Segment Array Clock that used 36 4-Digit 7-Segment displays. To keep the cost down, I brought 60 to get them at around 50 cents each. Having a number left over, I decided to make an implementation of Gabriele Cirulli's 2048 puzzle game. It is played on a 4x4 grid so it used 16 4-Digit 7-Segment displays.
2048 puzzle game (source Wikipedia)2048 is a game created by Gabriele Cirulli. It is played on a plain 4×4 grid, with numbered tiles that slide when a player moves them using the four arrow keys. Every turn, a new tile randomly appears in an empty spot on the board with a value of either 2 or 4. Tiles slide as far as possible in the chosen direction until they are stopped by either another tile or the edge of the grid. If two tiles of the same number collide while moving, they will merge into a tile with the total value of the two tiles that collided. The resulting tile cannot merge with another tile again in the same move.
If a move causes three consecutive tiles of the same value to slide together, only the two tiles farthest along the direction of motion will combine. If all four spaces in a row or column are filled with tiles of the same value, a move parallel to that row/column will combine the first two and last two. The user's score starts at zero, and is increased whenever two tiles combine, by the value of the new tile.
The game is won when a tile with a value of 2048 appears on the board. When the player has no legal moves (there are no empty spaces and no adjacent tiles with the same value), the game ends.
DemonstrationThe files required for 3D printing of the case are attached.
2048 - Xpad.stl & 2048 - Buttons.stl - 0.15mm layer height, no supports, use a contrasting color from the color for the case.
2048 - Bottom.stl - 0.2mm layer height, no supports
2048 - Top.stl - 0.2mm layer height, no supports. Change to a contrasting color at the start of layer 7 and switch back at the start of layer 9.
ElectronicsThe Eagle files have been included should you wish to have the boards commercially made or do as I did and make them yourself. I used the Toner method when making mine.
Display board (2 required)Start by adding the SMD components.
Next add the links if you are using a single sided PCB
Add the MAX7219 ICs and 4-Digit 7-Segment displays to the component side of the PCB.
Add the pin headers to the copper side of the board. The second board only uses a single pin header rotated 180 degrees. See picture further down.
Screw each board onto the front of the case using six 6mm M3 screws.
CPU board (1 required)Start by adding the SMD components.
Add the switches and buzzer to the component side.
Add a 90 degree pin header to the copper side.
Screw onto front of case using 4 off 6mm M3 screws. (ignore my prototype - I stuffed up the measurements of the PCB mounting posts - it is fixed in the attached STL files).
To join the boards, I obtained a 5 way cable and a 6 way cable by splitting a 40 way female-female Dupont 20cm cable that I got from eBay.
I replaced the single Dupont shrouds with six way Dupont shrouds. This is optional and made for easier removal of the connections during the development process.
Wire the 5-way cable that connects the two display boards. Note the orange wire in the picture below. On the bottom board, it is connected to the 5th pin from the left and on the top board, it is connected to the far right pin or 6th pin from the left.
Add the cable from the bottom display board to the CPU board.
Unlike the earlier ATtiny series such as the ATtiny85, the ATtiny1614 uses the RESET pin to program the CPU. To program it you need a UPDI programmer. I made one using a Arduino Nano. You can find complete build instructions at Create Your Own UPDI Programmer. It also contains the instructions for adding the megaTinyCore boards to your IDE.
Screw in the DC panel socket and wire up to the board connection. Make sure you get the polarity correct.
SoftwareI developed the game logic using C# in Visual Studio because of the ease of testing and debugging. The compiled Windows version is attached if you just want to play 2048 without building this project. I warn you now that it can become quite addictive. You have to excuse the interface, it wasn't my primary goal and I didn't want to spend anymore time on it to polish it up.
/*============================================================================================================
2048 Game
Description (Soure wikipedia)
2048 is a game created by Gabriele Cirulli. It is played on a plain 44 grid, with numbered tiles that slide
when a player moves them using the four arrow keys. Every turn, a new tile randomly appears in an empty spot
on the board with a value of either 2 or 4. Tiles slide as far as possible in the chosen direction until they
are stopped by either another tile or the edge of the grid. If two tiles of the same number collide while
moving, they will merge into a tile with the total value of the two tiles that collided. The resulting tile
cannot merge with another tile again in the same move.
If a move causes three consecutive tiles of the same value to slide together, only the two tiles farthest
along the direction of motion will combine. If all four spaces in a row or column are filled with tiles of
the same value, a move parallel to that row/column will combine the first two and last two. The user's score
starts at zero, and is increased whenever two tiles combine, by the value of the new tile.
The game is won when a tile with a value of 2048 appears on the board. When the player has no legal moves
(there are no empty spaces and no adjacent tiles with the same value), the game ends.
------------------------------------------------------------------------------------------------------------\
CPU: ATtiny1614
Display: 8 x MAX7219 controlling 4 rows by 4 columns of 4 Digit displays
Code: jbrad2089@gmail.com
BOARD: ATtiny1614/1604/814/804/414/404/214/204
Chip: ATtiny1614
Clock Speed: 20MHz
Programmer: jtag2updi (megaTinyCore)
==============================================================================================================*/
#include "Game.h"
#include "Display.h"
#include "Switches.h"
#include "Memory.h"
#include "Sound.h"
#define RANDOM 1 //PA5
//--------------------------- Pin definitions -------------------------------
void setup()
{
//Read in data from EEPROM;
ee::read();
//Setup screen
srn::setup();
srn::clear(); //Clear the primary buffer
srn::refresh(); //Transfer to display buffer
//Setup switches
sw::setup();
//Setup sound
play::setup();
//Setup random generator
pinMode(RANDOM, INPUT);
randomSeed(analogRead(RANDOM));
//clear the board and variables
startGame(false);
//Show splash screen
splashState = true;
splashTimeOut = 0;
}
void loop()
{
gameLoop();
if (sw::pressed() == sw::BA)
{
//Start game
splashState = false; //turn off splash screen
startGame(true);
sw::waitForKeyUp();
}
else if (sw::pressed() == sw::BB)
{
//Equivlant to power on
srn::clear(); //Clear the primary buffer
srn::refresh(); //Transfer to display buffer
//clear the board and variables
startGame(false);
//Show splash screen
splashState = true;
splashTimeOut = 0;
sw::waitForKeyUp();
}
else if (sw::pressed() == sw::BC)
{
//I seem to have a faulty MAX7219 chip that sometimes go blank
//This resets all the MAX7219 chips which seems to address the problem
//The board is restored and the game can continue.
srn::reset();
transferBoard();
sw::waitForKeyUp();
}
}
/*
Namespace: game
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: 2048 game logic
*/
#pragma once
#include "Display.h"
#include "Switches.h"
#include "Memory.h"
#include "Sound.h"
#define ROW(i) (i >> 2)
#define COL(i) (i & 0x03)
#define INDEX(r,c) ((r << 2) | c)
#define NO_FREE_SQUARES 16
#define GRID_SIZE 4
#define GRID_TOTAL (GRID_SIZE * GRID_SIZE)
uint16_t grid[GRID_TOTAL];
uint32_t score;
bool gameOver = true;
uint16_t lastJoin;
uint8_t nextCell;
#define FLASH_TIMEOUT 200;
unsigned long flashTimeOut = 0;
bool flashState = false;
#define SCORE_TIMEOUT 2000;
unsigned long scoreTimeOut = 0;
bool scoreState = false;
#define SPLASH_TIMEOUT 100;
unsigned long splashTimeOut = 0;
bool splashState = false;
uint8_t splashRow = 0;
uint8_t splashCol = 0;
int8_t splashDir = 1;
//-------------------------------------------------------------------------
//Forward references
void startGame(bool addTiles);
void evaluateGame();
uint8_t getRandomFreeSquare();
void transferBoard();
void flashNewCell();
void flashScore();
void animateSplashScreen();
bool isLeft();
void moveLeft();
void removeEmptyCellsShiftLeft(int r, int s);
bool isRight();
void moveRight();
void removeEmptyCellsShiftRight(int r, int s);
bool isUp();
void moveUp();
void removeEmptyCellsShiftUp(int r, int s);
bool isDown();
void moveDown();
void removeEmptyCellsShiftDown(int r, int s);
//-------------------------------------------------------------------------
/// <summary>
/// Initialise the game
/// </summary>
/// <param name="addTiles">true to add two starting tiles</param>
void startGame(bool addTiles)
{
for (int i=0; i<GRID_TOTAL; i++)
{
grid[i] = 0; //Clear board
}
flashTimeOut = 0;
flashState = false;
score = 0;
lastJoin = 0;
nextCell = NO_FREE_SQUARES;
gameOver = !addTiles;
if (addTiles)
{
//Set two random squares with a value of either 2 or 4
grid[getRandomFreeSquare()] = (random(2) << 1) + 2;
grid[getRandomFreeSquare()] = (random(2) << 1) + 2;
}
//Update screen
transferBoard();
}
/// <summary>
/// Loop while game is in play
/// </summary>
void gameLoop()
{
if (!gameOver)
{
sw::BUTTONS b = sw::pressed();
if (b != sw::NONE)
{
switch (b)
{
case sw::LF:
if (isLeft())
{
play::buttonPressed();
moveLeft();
evaluateGame();
}
else
{
play::invalidResponse();
}
break;
case sw::RG:
if (isRight())
{
play::buttonPressed();
moveRight();
evaluateGame();
}
else
{
play::invalidResponse();
}
break;
case sw::UP:
if (isUp())
{
play::buttonPressed();
moveUp();
evaluateGame();
}
else
{
play::invalidResponse();
}
break;
case sw::DN:
if (isDown())
{
play::buttonPressed();
moveDown();
evaluateGame();
}
else
{
play::invalidResponse();
}
break;
}
sw::waitForKeyUp();
}
flashNewCell();
}
else if (splashState)
{
animateSplashScreen();
}
else
{
flashScore();
}
}
/// <summary>
/// Test for win or loss
/// </summary>
void evaluateGame()
{
if (!gameOver)
{
//labScore.Text = String.Format("{0}", score);
if (lastJoin == 2048)
{
nextCell = NO_FREE_SQUARES;
gameOver = true;
ee::data.wins++;
if (score > ee::data.high)
{
ee::data.high = score;
}
ee::write();
play::winSound();
}
else
{
//Generate a new random piece
nextCell = getRandomFreeSquare();
if (nextCell != NO_FREE_SQUARES)
{
grid[nextCell] = (random(2) << 1) + 2;
transferBoard(); //force update just incase there is no valid moves left
if (!isLeft() && !isRight() && !isUp() && !isDown())
{
nextCell = NO_FREE_SQUARES;
gameOver = true;
if (score > ee::data.high)
{
ee::data.high = score;
ee::write();
}
play::loseSound();
}
}
else
{
nextCell = NO_FREE_SQUARES;
gameOver = true;
if (score > ee::data.high)
{
ee::data.high = score;
ee::write();
}
play::loseSound();
}
}
//Update screen
transferBoard();
}
}
/// <summary>
/// Return the next available random square
/// </summary>
/// <returns>returns 0 to 15 otherwise NO_FREE_SQUARES if no free squares</returns>
uint8_t getRandomFreeSquare()
{
uint8_t freeCount = 0;
for (int i=0; i<GRID_TOTAL; i++)
{
if (grid[i] == 0)
{
freeCount = freeCount + 1;
}
}
if (freeCount != 0)
{
//Get ordinal value of random free square
freeCount = random(freeCount); //0 to freeCount
for (int i=0; i<GRID_TOTAL; i++)
{
if (grid[i] == 0)
{
if (freeCount == 0)
{
return i;
}
else
{
freeCount--;
}
}
}
}
return NO_FREE_SQUARES;
}
/// <summary>
/// Transfer board to the display
/// </summary>
void transferBoard()
{
for (int i=0; i<GRID_TOTAL; i++)
{
srn::displayNumber(ROW(i), COL(i), grid[i], true, true);
}
srn::refresh();
flashState = true;
flashTimeOut = millis() + FLASH_TIMEOUT;
}
/// <summary>
/// Flash the newest cell
/// </summary>
void flashNewCell()
{
if (nextCell != NO_FREE_SQUARES && millis() > flashTimeOut)
{
srn::displayNumber(ROW(nextCell), COL(nextCell), grid[nextCell], true, flashState);
srn::refresh();
flashState = !flashState;
flashTimeOut = millis() + FLASH_TIMEOUT;
}
}
/// <summary>
/// Switch between score and board
/// </summary>
void flashScore()
{
if (score != 0 && millis() > scoreTimeOut)
{
if (scoreState)
{
srn::clear();
srn::displayString(0, 1, "RESULTS");
srn::displayString(1, 0, " SCORE");
srn::displayNumberLong(1, 2, score);
srn::displayString(2, 0, " HIGH");
srn::displayNumberLong(2, 2, ee::data.high);
srn::displayString(3, 0, "TOT 2048");
srn::displayNumber(3, 3, ee::data.wins, false, true);
srn::refresh();
}
else
{
transferBoard();
}
scoreState = !scoreState;
scoreTimeOut = millis() + SCORE_TIMEOUT;
}
}
/// <summary>
/// Show splash screen animation
/// </summary>
void animateSplashScreen()
{
if (splashState && millis() > splashTimeOut)
{
srn::displayNumber(splashRow, splashCol, 0, true, false);
if (splashDir == 1)
{
if (splashCol != 3)
{
splashCol++;
}
else
{
splashDir = GRID_SIZE;
splashRow += 1;
}
}
else if (splashDir == -1)
{
if (splashCol != 0)
{
splashCol--;
}
else
{
splashDir = -GRID_SIZE;
splashRow -= 1;
}
}
else if (splashDir == GRID_SIZE)
{
if (splashRow != 3)
{
splashRow++;
}
else
{
splashDir = -1;
splashCol--;
}
}
else if (splashDir == -GRID_SIZE)
{
if (splashRow != 0)
{
splashRow--;
}
else
{
splashDir = 1;
splashCol++;
}
}
srn::displayNumber(splashRow, splashCol, 2048, true, true);
srn::displayString(1, 1, " 2 ");
srn::displayString(1, 2, " 0 ");
srn::displayString(2, 1, " 4 ");
srn::displayString(2, 2, " 8 ");
srn::refresh();
splashTimeOut = millis() + SPLASH_TIMEOUT;
}
}
//==========================================================================
// LEFT routines
//==========================================================================
/// <summary>
/// Test if there is a valid left move
/// </summary>
/// <returns>true if one or more left moves available</returns>
bool isLeft()
{
int i;
for (int r = 0; r < GRID_SIZE; r++)
{
bool leading = true;
for (int c = (GRID_SIZE - 1); c >= 0; c--)
{
i = INDEX(r, c);
if (grid[i] != 0 || !leading)
{
leading = false;
if (grid[i] == 0 || (c > 0 && grid[i] == grid[i - 1]))
{
//Cell is empty or the cell to the left is the same value
return true;
}
}
}
}
return false;
}
/// <summary>
/// Moves the tiles left
/// </summary>
void moveLeft()
{
int i;
for (int r = 0; r < GRID_SIZE; r++)
{
removeEmptyCellsShiftLeft(r, 0);
//Merge any cells that match
for (int c = 0; c < (GRID_SIZE - 1); c++)
{
i = INDEX(r, c);
if (grid[i] != 0 && grid[i] == grid[i + 1])
{
//Cell to the right matches so merge cell
grid[i] += grid[i + 1];
//Add combined total to score
lastJoin = grid[i];
score += lastJoin;
//Erase cell to the right
grid[i + 1] = 0;
//Because we just erased a cell, we need to shift
//any to the right to fill the hole
removeEmptyCellsShiftLeft(r, c + 1);
}
}
}
}
/// <summary>
/// Remove any empty cells to the right and shift non-empty cells left
/// </summary>
/// <param name="r">row to operate on</param>
/// <param name="s">starting column</param>
void removeEmptyCellsShiftLeft(int r, int s)
{
int i;
int j;
int e;
//Remove spaces and shift cells left
for (int c = s; c < (GRID_SIZE - 1); c++)
{
i = INDEX(r, c);
if (grid[i] == 0)
{
j = i;
e = 0;
while (grid[j] == 0 && COL(j) < (GRID_SIZE - 1))
{
grid[j] = grid[j + 1];
e += grid[j];
grid[j + 1] = 0;
j++;
}
if (grid[i] == 0 && e != 0)
{
c--; //still empty
}
}
}
}
//==========================================================================
// RIGHT routines
//==========================================================================
/// <summary>
/// Test if there is a valid right move
/// </summary>
/// <returns>true if one or more right moves available</returns>
bool isRight()
{
int i;
for (int r = 0; r < GRID_SIZE; r++)
{
bool leading = true;
for (int c = 0; c < GRID_SIZE; c++)
{
i = INDEX(r, c);
if (grid[i] != 0 || !leading)
{
leading = false;
if (grid[i] == 0 || (c < (GRID_SIZE - 1) && grid[i] == grid[i + 1]))
{
//Cell is empty or the cell to the right is the same value
return true;
}
}
}
}
return false;
}
/// <summary>
/// Moves the tiles right
/// </summary>
void moveRight()
{
int i;
for (int r = 0; r < GRID_SIZE; r++)
{
removeEmptyCellsShiftRight(r, 0);
//Merge any cells that match
for (int c = (GRID_SIZE - 1); c > 0; c--)
{
i = INDEX(r, c);
if (grid[i] != 0 && grid[i] == grid[i - 1])
{
//Cell to the right matches so merge cell
grid[i] += grid[i - 1];
//Add combined total to score
lastJoin = grid[i];
score += lastJoin;
//Erase cell to the right
grid[i - 1] = 0;
//Because we just erased a cell, we need to shift
//any to the left to fill the hole
removeEmptyCellsShiftRight(r, c - 1);
}
}
}
}
/// <summary>
/// Remove any empty cells to the right and shift non-empty cells left
/// </summary>
/// <param name="r">row to operate on</param>
/// <param name="s">starting column</param>
void removeEmptyCellsShiftRight(int r, int s)
{
int i;
int j;
int e;
//Remove spaces and shift cells right
for (int c = (GRID_SIZE - 1); c > 0; c--)
{
i = INDEX(r, c);
if (grid[i] == 0)
{
j = i;
e = 0;
while (grid[j] == 0 && COL(j) > 0)
{
grid[j] = grid[j - 1];
e += grid[j];
grid[j - 1] = 0;
j--;
}
if (grid[i] == 0 && e != 0)
{
c++; //still empty
}
}
}
}
//==========================================================================
// UP routines
//==========================================================================
/// <summary>
/// Test if there is a valid up move
/// </summary>
/// <returns>true if one or more up moves available</returns>
bool isUp()
{
int i;
for (int c = 0; c < GRID_SIZE; c++)
{
bool leading = true;
for (int r = (GRID_SIZE - 1); r >= 0; r--)
{
i = INDEX(r, c);
if (grid[i] != 0 || !leading)
{
leading = false;
if (grid[i] == 0 || (r > 0 && grid[i] == grid[i - GRID_SIZE]))
{
//Cell is empty or the cell above is the same value
return true;
}
}
}
}
return false;
}
/// <summary>
/// Moves the tiles up
/// </summary>
void moveUp()
{
int i;
for (int c = 0; c < GRID_SIZE; c++)
{
removeEmptyCellsShiftUp(0, c);
//Merge any cells that match
for (int r = 0; r < (GRID_SIZE - 1); r++)
{
i = INDEX(r, c);
if (grid[i] != 0 && grid[i] == grid[i + GRID_SIZE])
{
//Cell below matches so merge cell
grid[i] += grid[i + GRID_SIZE];
//Add combined total to score
lastJoin = grid[i];
score += lastJoin;
//Erase cell below
grid[i + GRID_SIZE] = 0;
//Because we just erased a cell, we need to shift
//any to the up to fill the hole
removeEmptyCellsShiftUp(r + 1, c);
}
}
}
}
/// <summary>
/// Remove any empty cells below and shift non-empty cells up
/// </summary>
/// <param name="s">starting row</param>
/// <param name="c">column to operate on</param>
void removeEmptyCellsShiftUp(int s, int c)
{
int i;
int j;
int e;
//Remove spaces and shift cells up
for (int r = 0; r < (GRID_SIZE - 1); r++)
{
i = INDEX(r, c);
if (grid[i] == 0)
{
j = i;
e = 0;
while (grid[j] == 0 && ROW(j) < (GRID_SIZE - 1))
{
grid[j] = grid[j + GRID_SIZE];
e += grid[j];
grid[j + GRID_SIZE] = 0;
j += GRID_SIZE;
}
if (grid[i] == 0 && e != 0)
{
r--; //still empty
}
}
}
}
//==========================================================================
// DOWN routines
//==========================================================================
/// <summary>
/// Test if there is a valid down move
/// </summary>
/// <returns>true if one or more down moves available</returns>
bool isDown()
{
int i;
for (int c = 0; c < GRID_SIZE; c++)
{
bool leading = true;
for (int r = 0; r < GRID_SIZE; r++)
{
i = INDEX(r, c);
if (grid[i] != 0 || !leading)
{
leading = false;
if (grid[i] == 0 || (r < (GRID_SIZE - 1) && grid[i] == grid[i + GRID_SIZE]))
{
//Cell is empty or the cell below is the same value
return true;
}
}
}
}
return false;
}
/// <summary>
/// Moves the tiles down
/// </summary>
void moveDown()
{
int i;
for (int c = 0; c < GRID_SIZE; c++)
{
removeEmptyCellsShiftDown(0, c);
//Merge any cells that match
for (int r = (GRID_SIZE - 1); r > 0; r--)
{
i = INDEX(r, c);
if (grid[i] != 0 && grid[i] == grid[i - GRID_SIZE])
{
//Cell above matches so merge cell
grid[i] += grid[i - GRID_SIZE];
//Add combined total to score
lastJoin = grid[i];
score += lastJoin;
//Erase cell above
grid[i - GRID_SIZE] = 0;
//Because we just erased a cell, we need to shift
//any to the down to fill the hole
removeEmptyCellsShiftDown(r - 1, c);
}
}
}
}
/// <summary>
/// Remove any empty cells to the above and shift non-empty cells down
/// </summary>
/// <param name="s">starting row</param>
/// <param name="c">column to operate on</param>
void removeEmptyCellsShiftDown(int s, int c)
{
int i;
int j;
int e;
//Remove spaces and shift cells down
for (int r = (GRID_SIZE - 1); r > 0; r--)
{
i = INDEX(r, c);
if (grid[i] == 0)
{
j = i;
e = 0;
while (grid[j] == 0 && ROW(j) > 0)
{
grid[j] = grid[j - GRID_SIZE];
e += grid[j];
grid[j - GRID_SIZE] = 0;
j -= GRID_SIZE;
}
if (grid[i] == 0 && e != 0)
{
r++; //still empty
}
}
}
}
/*
Namespace: srn
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Display routines
*/
#pragma once
#include <MD_MAX72xx.h>
#include <SPI.h>
namespace srn
{
//--------------------------- Pin definitions -------------------------------
#define LED_CLK 10 // PA3 or SCK
#define LED_DATA 8 // PA1 or MOSI
#define LED_CS 0 // PA4 or SS
//--------------------------- MAX7219 mappping ------------------------------
#define HARDWARE_TYPE MD_MAX72XX::GENERIC_HW
#define MAX_DEVICES 8
// SPI hardware interface
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, LED_CS, MAX_DEVICES);
//ASCII Character Set
//Numbers 0 - 9
//Letters A - Z
//Standard Segment order _ a b c d e f g
//2048 Segment order c b d e f g _ a
uint8_t ascii[] = {
B11111001, B11000000, B01110101, B11100101, B11001100, B10101101, B10111101, B11000001, B11111101, B11101101, B00000000, B00000000, B00000000, B00000100, B00000000, B00000000,
B00000000, B11011101, B10111100, B00111001, B11110100, B00111101, B00011101, B11101101, B11011100, B11000000, B11110000, B00000000, B00111000, B00000000, B10010100, B10110100,
B01011101, B00000000, B00010100, B10101101, B00111100, B10110000, B00000000, B00000000, B00000000, B11101100, B00000000, B10110001, B01010100, B11100001, B00000000, B00100000
};
//Digits table maps logical MAX7219 digit order to physical digit wiring on PCB
uint8_t digits[] = {0, 6, 5, 1, 4, 2, 3, 7};
//Devices table are the offsets to convert the vertical display layout per MAX7219 to a horziontal layout
int8_t devices[] = {0, 4, 8, 12, -12, -8, -4, 0};
//-------------------------------------------------------------------------
//Forward references
void setup();
void reset();
void refresh();
void clear();
void displayString(uint8_t r, uint8_t c, String s);
void displayNumber(uint8_t r, uint8_t c, uint16_t v, bool blankWhenZero, bool on);
void displayNumberLong(uint8_t r, uint8_t c, uint32_t v);
void setLogicalColumn(uint16_t c, uint8_t v);
//-------------------------------------------------------------------------
//Initialise Hardware
void setup()
{
reset();
}
//-------------------------------------------------------------------------
//Initialise the MAX7219 chips
void reset()
{
mx.begin();
mx.control(MD_MAX72XX::INTENSITY, 2); //0..MAX_INTENSITY
mx.update(MD_MAX72XX::OFF) ; //No Auto updating
}
//-----------------------------------------------------------------------------------------------
//Refresh display
void clear()
{
mx.clear(0, mx.getDeviceCount()-1);
}
//-----------------------------------------------------------------------------------------------
//Refresh display
void refresh()
{
mx.update();
}
//-----------------------------------------------------------------------------------------------
// Write string of characters
// r = row (0 to 3)
// c = column (0 to 3)
// v = 0 to 9999
void displayString(uint8_t r, uint8_t c, String s)
{
uint8_t p = (r << 4) + (c << 2);
for (int i = 0; i < s.length(); i++)
{
byte c = (byte)s.charAt(i);
if (c == 0x2D)
{
c = 0x3C;
}
if (c < 0x30 || c > 0x5F)
{
c = 0x3F; //Used as a space character
}
c = c - 0x30;
setLogicalColumn(p++, ascii[c]);
}
}
//-----------------------------------------------------------------------------------------------
//Displays number in array
// r = row (0 to 3)
// c = column (0 to 3)
// v = 0 to 9999
// blankWhenZero (true = zero not shown)
// on (true to show value)
void displayNumber(uint8_t r, uint8_t c, uint16_t v, bool blankWhenZero, bool on)
{
uint8_t p = (r << 4) + (c << 2) + 3;
for (int i = 0; i < 4; i++)
{
if (on && (v != 0 || (i == 0 && !blankWhenZero)))
{
setLogicalColumn(p, ascii[v % 10]);
v = v / 10;
}
else
{
setLogicalColumn(p, 0); //Space character
}
p--;
}
}
//-----------------------------------------------------------------------------------------------
//Displays number in array
// r = row (0 to 3)
// c = column (0 to 3) (Points to most significant column - number continues in column to the right)
// v = 0 to 9999
void displayNumberLong(uint8_t r, uint8_t c, uint32_t v)
{
//display in right matrix
uint8_t p = (r << 4) + ((c + 1) << 2) + 3;
for (int i = 0; i < 4; i++)
{
if (v != 0 || i == 0)
{
setLogicalColumn(p, ascii[v % 10]);
v = v / 10;
}
else
{
setLogicalColumn(p, 0); //Space character
}
p--;
}
//continue in left matrix
p = (r << 4) + (c << 2) + 3;
for (int i = 0; i < 4; i++)
{
if (v != 0)
{
setLogicalColumn(p, ascii[v % 10]);
v = v / 10;
}
else
{
setLogicalColumn(p, 0); //Space character
}
p--;
}
}
//-----------------------------------------------------------------------------------------------
//Displays segment bit mask v using logical column value (0 being top-left, 63 being bottom-right)
//to the physical device and digit
// c = 0 to 63, v = segs
void setLogicalColumn(uint16_t c, uint8_t v)
{
//Logical column and row layout
//[00 01 02 03] [04 05 06 07] [08 09 10 11] [12 13 14 15]
//[16 17 18 19] [20 21 22 23] [24 25 26 27] [28 29 30 31]
//[32 33 34 35] [36 37 38 39] [40 41 42 43] [44 45 46 47]
//[48 49 50 51] [52 53 54 55] [56 57 58 59] [60 61 62 63]
//[00 01 02 03] [04 05 06 07] [08 09 0A 0B] [0C 0D 0E 0F]
//[10 11 12 13] [14 15 16 17] [18 19 1A 1B] [1C 1D 1E 1F]
//[20 21 22 23] [24 25 26 27] [28 29 2A 2B] [2C 2D 2E 2F]
//[30 31 32 33] [34 35 36 37] [38 39 3A 1B] [3C 3D 3E 3F]
//Phyisc column and row layout
//[00 01 02 03] [08 09 0A 0B] [10 11 12 13] [18 19 1A 1B]
//[04 05 06 07] [0C 0D 0E 0F] [14 15 16 17] [1C 1D 1E 1F]
//[20 21 22 23] [28 29 2A 2B] [30 31 32 33] [38 39 3A 1B]
//[24 25 26 27] [2C 2D 2E 2F] [34 35 36 37] [3C 3D 3E 3F]
//Convert to horziontal layout
uint8_t p = c + devices[(c & 0x1F) >> 2];
//Fix up digit mappings
//uint8_t digits[] = {0, 6, 5, 1, 4, 2, 3, 7};
uint8_t tmp = ((p & 0xF8) ^ 0x20) | digits[p & 07];
//Display digit on MAX7219 array
mx.setColumn(tmp, v);
}
} //namespace
/*
Namespace: ee
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: EEPROM routines
*/
#pragma once
//Uncomment to reset EEPROM data
//#define RESET_EEPROM
#ifdef __AVR_ATtiny1614__
#include <avr/eeprom.h>
#else
#include <EEPROM.h>
#endif
namespace ee
{
//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
uint32_t magic;
uint32_t high;
uint16_t wins;
} EEPROM_DATA;
volatile EEPROM_DATA data; //Current EEPROM settings
//-----------------------------------------------------------------------------------
// Forward references
void write();
void read();
//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void write()
{
//This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
#ifdef __AVR_ATtiny1614__
eeprom_update_block (( void *) &data , ( const void *) EEPROM_ADDRESS, sizeof(data));
#else
EEPROM.put(EEPROM_ADDRESS,data);
#endif
}
//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void read()
{
//Eprom
#ifdef __AVR_ATtiny1614__
eeprom_read_block (( void *) &data , ( const void *) EEPROM_ADDRESS, sizeof(data));
#else
EEPROM.get(EEPROM_ADDRESS,EepromData);
#endif
#ifndef RESET_EEPROM
if (data.magic != EEPROM_MAGIC)
#endif
{
data.magic = EEPROM_MAGIC;
data.high = 0;
data.wins = 0;
write();
}
}
} //namespace
/*
Namespace: play
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Sound routines
*/
#pragma once
#include <TimerFreeTone.h> // https://bitbucket.org/teckel12/arduino-timer-free-tone/wiki/Home
namespace play
{
#define SPEAKER 2 // PA6
#define BUZZER_PORT PORTA
#define BUZZER_PIN PIN6_bm
//-------------------------------------------------------------------------
//Forward references
void setup();
void buttonPressed();
void invalidResponse();
void noteDecay(int start);
void noteAttack(int start);
void winSound();
void loseSound();
void beep();
//-------------------------------------------------------------------------
//Initialise Hardware
void setup()
{
pinMode(SPEAKER,OUTPUT);
}
//------------------------------------------------------------------
//Play button pressed sound
void buttonPressed()
{
TimerFreeTone(SPEAKER, 440, 150);
}
//------------------------------------------------------------------
//Play bad move sound
void invalidResponse()
{
TimerFreeTone(SPEAKER, 50, 150);
}
//-----------------------------------------------------------------------------------
//Play a decaying sound
void noteDecay(int start)
{
#define DECAY_NOTE 100 // Minimum delta time.
for (int note = start; note >= DECAY_NOTE; note -= 10)
{
TimerFreeTone(SPEAKER, note, 100);
}
}
//-----------------------------------------------------------------------------------
//Play a attack sound
void noteAttack(int start)
{
#define DECAY_NOTE 100 // Minimum delta time.
for (int note = DECAY_NOTE; note <= start; note += 10)
{
TimerFreeTone(SPEAKER, note, 100);
}
}
//------------------------------------------------------------------
//Play a high note as a sign you lost
void winSound()
{
//TimerFreeTone(SPEAKER,880,300);
TimerFreeTone(SPEAKER,880,100); //A5
TimerFreeTone(SPEAKER,988,100); //B5
TimerFreeTone(SPEAKER,523,100); //C5
TimerFreeTone(SPEAKER,988,100); //B5
TimerFreeTone(SPEAKER,523,100); //C5
TimerFreeTone(SPEAKER,587,100); //D5
TimerFreeTone(SPEAKER,523,100); //C5
TimerFreeTone(SPEAKER,587,100); //D5
TimerFreeTone(SPEAKER,659,100); //E5
TimerFreeTone(SPEAKER,587,100); //D5
TimerFreeTone(SPEAKER,659,100); //E5
TimerFreeTone(SPEAKER,659,100); //E5
delay(250);
}
//------------------------------------------------------------------------------------------------------------------
//Play wah wah wah wahwahwahwahwahwah
void loseSound()
{
delay(400);
//wah wah wah wahwahwahwahwahwah
for(double wah=0; wah<4; wah+=6.541)
{
TimerFreeTone(SPEAKER, 440+wah, 50);
}
TimerFreeTone(SPEAKER, 466.164, 100);
delay(80);
for(double wah=0; wah<5; wah+=4.939)
{
TimerFreeTone(SPEAKER, 415.305+wah, 50);
}
TimerFreeTone(SPEAKER, 440.000, 100);
delay(80);
for(double wah=0; wah<5; wah+=4.662)
{
TimerFreeTone(SPEAKER, 391.995+wah, 50);
}
TimerFreeTone(SPEAKER, 415.305, 100);
delay(80);
for(int j=0; j<7; j++)
{
TimerFreeTone(SPEAKER, 391.995, 70);
TimerFreeTone(SPEAKER, 415.305, 70);
}
delay(400);
}
//------------------------------------------------------------------------------------------------------------------
//Turn on and off buzzer quickly
void beep()
{ // Beep and flash LED green unless STATE_AUTO
BUZZER_PORT.OUTSET = BUZZER_PIN; // turn on buzzer
delay(5);
BUZZER_PORT.OUTCLR = BUZZER_PIN; // turn off the buzzer
}
} //namespace
/*
Namespace: sw
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Switch routines
*/
#pragma once
#include "Sound.h"
namespace sw
{
#define SWITCHES 7 // PB0
#define DEBOUNCE_DELAY 30
enum BUTTONS { NONE, BA, BB, BC, UP, LF, DN, RG };
typedef struct
{
int low;
int high;
} RANGE;
//This table maps the analog pins and voltages to the actual keys
//LF=733, RG=924, UP=617, DN=827, BA=0, BB=344, BC=515
RANGE ranges[8] = {
{ 1000, 1024 }, //NONE
{ 0, 99 }, //BA
{ 100, 419 }, //BB
{ 420, 559 }, //BC
{ 560, 649 }, //UP
{ 650, 779 }, //LF
{ 780, 869 }, //DN
{ 870, 999 } //RG
};
//-------------------------------------------------------------------------
//Forward references
void setup();
bool state(BUTTONS b);
BUTTONS pressed();
//-------------------------------------------------------------------------
//Initialise Hardware
void setup()
{
pinMode(SWITCHES,INPUT);
}
//-------------------------------------------------------------------
// Return the state of the given button
// b - BUTTONS constant to test
// returns TRUE if key is pressed, otherwise FALSE
bool state(BUTTONS b)
{
int16_t v = analogRead(SWITCHES);
return (v >= ranges[(int)b].low && v <= ranges[(int)b].high);
}
//-------------------------------------------------------------------
// Return the button being pressed. Wait until button is debounced and released
// returns BUTTONS constant of key being pressed (NONE if no key pressed)
BUTTONS pressed()
{
for (BUTTONS b = BA; b <= RG; b = (BUTTONS)((int)b + 1))
{
if (state(b))
{
delay(DEBOUNCE_DELAY);
if (state(b))
{
return b;
}
}
}
return NONE;
}
//-------------------------------------------------------------------
// Wait until any button pressed is released
void waitForKeyUp()
{
while (analogRead(SWITCHES) < 1000) ;
}
} //namespace
Comments