My code is written in Arduino IDE and is just my original code from my earlier clock project merged with a simple falling blocks game I made.
Simply paste this into the Arduino IDE, install all the libraries used, and upload to your board.
////////////////////ARDUINO UNO R4////////////////////
////////////////////LED MATRIX CLOCK////////////////////
////////////////////LIBRARIES////////////////////
#include "RTClib.h"
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <CtrlBtn.h>
#include <CtrlEnc.h>
#include <BlockNot.h>
////////////////////VARIABLES////////////////////
static char displayChar[9] = {0}; //stores the text for the display to show
char status = 'N';
// status variable — indicates the clock's current mode.
//
// Legend:
// 'N' - Normal display (time view)
// 'h' - Enter hour-edit menu
// 'm' - Enter minute-edit menu
// 'H' - Hour edit screen
// 'M' - Minute edit screen
// 's' - Set/confirm time
// 'e' - Exit menu / return to normal
// 'g' - Enter game menu
// 'G' - Game playing screen
uint8_t oldMin; //holds the last minute so screen updates when minute changes
int8_t setMinuteVar = 0; //holds the value when using the encoder to change the minute
int8_t setHourVar = 0; //holds the value when using the encoder to change the Hour
uint8_t gameBlocks[32] = {255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, };
int8_t gamePlayer = 0;
uint16_t gameScore = 0;
////////////////////PAROLA SETUP////////////////////
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
#define CS_PIN 8
MD_Parola P = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
MD_MAX72XX *mx;
////////////////////RTC SETUP////////////////////
RTC_DS1307 rtc;
////////////////////BLOCKNOT SETUP////////////////////
BlockNot rtcRefreshTimer(1000);
BlockNot menuTimer(5000);
BlockNot gameBlockTimer(200);
////////////////////CTRL SETUP AND MENU HANDLERS////////////////////
//Switch...Case is used to handle the status of the variables
// Define an onTurnleft handler.
void onTurnLeft() {
menuTimer.start(WITH_RESET);
switch (status) {
case 'm':
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'H':
setHourVar--;
if(setHourVar <= -1){setHourVar = 23;}
snprintf(displayChar, sizeof(displayChar), "%02d", setHourVar);
P.displayClear();
P.print(displayChar);
break;
case 'M':
setMinuteVar--;
if(setMinuteVar <= -1){setMinuteVar = 59;}
snprintf(displayChar, sizeof(displayChar), "%02d", setMinuteVar);
P.displayClear();
P.print(displayChar);
break;
case 's':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 'e':
status = 's';
P.displayClear();
P.print("Set");
break;
case 'g':
status = 'e';
P.displayClear();
P.print("Exit");
break;
case 'G':
if (gamePlayer + 1 != gameBlocks[0]) {
if(((gamePlayer == 7) && (gameBlocks[0] == 0)) == 0){
gamePlayer++;
if(gamePlayer > 7){
gamePlayer = 0;
}
}
}
gameUpdateScreen();
break;
default:
break;
}
}
// Define an onTurnRight handler.
void onTurnRight() {
menuTimer.start(WITH_RESET);
switch (status) {
case 'h':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 'm':
status = 's';
P.displayClear();
P.print("Set");
break;
case 'H':
setHourVar++;
if(setHourVar >= 24){setHourVar = 0;}
snprintf(displayChar, sizeof(displayChar), "%02d", setHourVar);
P.displayClear();
P.print(displayChar);
break;
case 'M':
setMinuteVar++;
if(setMinuteVar >= 60){setMinuteVar = 0;}
snprintf(displayChar, sizeof(displayChar), "%02d", setMinuteVar);
P.displayClear();
P.print(displayChar);
break;
case 's':
status = 'e';
P.displayClear();
P.print("Exit");
break;
case 'e':
status = 'g';
P.displayClear();
P.print("Game");
break;
case 'G':
if (gamePlayer - 1 != gameBlocks[0]) {
if(((gamePlayer == 0) && (gameBlocks[0] == 7)) == 0){
gamePlayer--;
if(gamePlayer < 0){
gamePlayer = 7;
}
}
}
gameUpdateScreen();
break;
default:
break;
}
}
// Define an onPress handler.
void onPress() {
menuTimer.start(WITH_RESET);
DateTime now = rtc.now();
switch (status) {
case 'N':
setHourVar = now.hour();
setMinuteVar = now.minute();
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'h':
status = 'H';
snprintf(displayChar, sizeof(displayChar), "%02d", setHourVar);
P.displayClear();
P.print(displayChar);
break;
case 'm':
status = 'M';
snprintf(displayChar, sizeof(displayChar), "%02d", setMinuteVar);
P.displayClear();
P.print(displayChar);
break;
case 'H':
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'M':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 's':
status = 'N';
setTime(setHourVar, setMinuteVar);
showTime();
break;
case 'e':
status = 'N';
showTime();
break;
case 'g':
status = 'G';
P.displayClear();
P.displaySuspend(true);
playGame();
break;
case 'G':
status = 'g';
gameScore = 0;
for (uint8_t i = 0; i <= 31; i++) {
gameBlocks[i] = 255;
}
P.displaySuspend(false);
P.displayClear();
P.print("Game");
break;
default:
break;
}
}
CtrlBtn button(2, 50, onPress);
CtrlEnc encoder(4, 3, onTurnLeft, onTurnRight);
////////////////////TIME SETTING FUNCTIONS////////////////////
void setTime(int8_t newHour, int8_t newMinute) {//take two variables and sets the time of the rtc to them
DateTime before = rtc.now();
DateTime updated(before.year(), before.month(), before.day(), newHour, newMinute, 0);
rtc.adjust(updated);
}
void showTime() {//updates the display and shows the current time.
DateTime now = rtc.now();
oldMin = now.minute();
if( status == 'N'){
snprintf(displayChar, sizeof(displayChar), "%02d:%02d", now.hour(), now.minute());
P.displayClear();
P.print(displayChar);
}
}
////////////////////SETUP CODE////////////////////
void setup () {
P.begin();
P.setIntensity(0);
mx = P.getGraphicObject(); // store pointer globally
if (! rtc.begin()) {
while (1) delay(10);
}
if (! rtc.isrunning()) {
// When time needs to be set on a new device, or after a power loss, the
// following line sets the RTC to the date & time this sketch was compiled
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
DateTime now = rtc.now();
oldMin = now.minute();
P.setTextAlignment(PA_CENTER);
P.print("Hello!");//Turns the screen on and says hello
delay(4000);
showTime();
menuTimer.stop();
rtcRefreshTimer.start(WITH_RESET);
}
void loop () {
if (menuTimer.triggered()) {//This exits the menu automatically
switch (status) {
case 'h':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'm':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'H':
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'M':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 's':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'e':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'g':
status = 'N';
showTime();
menuTimer.stop();
break;
default:
break;
}
}
if(rtcRefreshTimer.triggered()){//checks if time has changed
DateTime now = rtc.now();
if (oldMin != now.minute()) {
showTime();
}
}
encoder.process();
button.process();
}
void playGame(){
gameUpdateScreen();
while (status == 'G') {
encoder.process();
button.process();
if(gameBlockTimer.TRIGGERED){
gameScore++;
gameMoveBlocks();
}
}
}
void gameMoveBlocks() {
for (uint8_t i = 1; i <= 31; i++) {
if (gameBlocks[i] != 255) {
gameBlocks[i - 1] = gameBlocks[i];
}
}
gameBlocks[31] = rand() % 8;
if (gameBlocks[0] == gamePlayer){
delay(1000);
gamePrintScore();
gameScore = 0;
delay(1000);
for (uint8_t i = 0; i <= 31; i++) {
gameBlocks[i] = 255;
}
}
gameUpdateScreen();
}
void gameUpdateScreen() {
mx->clear();
mx->setPoint(gamePlayer, 0, true);
for(uint8_t i = 0; i <= 31; i++){
if(gameBlocks[i] != 255){
mx->setPoint(gameBlocks[i], i, true);
}
}
}
void gamePrintScore(){
P.displaySuspend(false);
snprintf(displayChar, sizeof(displayChar), "%05u", gameScore);
P.displayClear();
P.print(displayChar);
delay(4000);
P.displayClear();
P.displaySuspend(true);
}
Code Breakdown
This section just includes all libraries used in the code.
#include "RTClib.h"
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <CtrlBtn.h>
#include <CtrlEnc.h>
#include <BlockNot.h>
displayChar stores the text shown on the display.
static char displayChar[9] = {0};
status stores the mode the clock is in.
char status = 'N';
// status variable — indicates the clock's current mode.
//
// Legend:
// 'N' - Normal display (time view)
// 'h' - Enter hour-edit menu
// 'm' - Enter minute-edit menu
// 'H' - Hour edit screen
// 'M' - Minute edit screen
// 's' - Set/confirm time
// 'e' - Exit menu / return to normal
// 'g' - Enter game menu
// 'G' - Game playing screen
oldMin, setMinuteVar, and setHourVar store values used for setting and showing time
gameBlocks and gamePlayer store the positions of blocks and the player.
uint8_t gameBlocks[32] = {255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, };
int8_t gamePlayer = 0;
gameScore stores the current score for the game
This initializes the LED matrix and sets hardware type.
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
#define CS_PIN 8
MD_Parola P = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
MD_MAX72XX *mx;
Here it initializes the RTC.
This initializes timers to refresh the clock, exit the time, and move the blocks down.
BlockNot rtcRefreshTimer(1000);
BlockNot menuTimer(5000);
BlockNot gameBlockTimer(200);
This block of code initializes the CTRL library. It sets up handlers for turning the encoder left, turning it right, and pressing it. Each handler checks the current value of status and runs the code associated with that value.
void onTurnLeft() {
menuTimer.start(WITH_RESET);//resets the timer that automatically exits the menu
switch (status) {
case 'm':
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'H':
setHourVar--;
if(setHourVar <= -1){setHourVar = 23;}
snprintf(displayChar, sizeof(displayChar), "%02d", setHourVar);
P.displayClear();
P.print(displayChar);
break;
case 'M':
setMinuteVar--;
if(setMinuteVar <= -1){setMinuteVar = 59;}
snprintf(displayChar, sizeof(displayChar), "%02d", setMinuteVar);
P.displayClear();
P.print(displayChar);
break;
case 's':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 'e':
status = 's';
P.displayClear();
P.print("Set");
break;
case 'g':
status = 'e';
P.displayClear();
P.print("Exit");
break;
case 'G':
if (gamePlayer + 1 != gameBlocks[0]) {
if(((gamePlayer == 7) && (gameBlocks[0] == 0)) == 0){
gamePlayer++;
if(gamePlayer > 7){
gamePlayer = 0;
}
}
}
gameUpdateScreen();
break;
default:
break;
}
}
// Define an onTurnRight handler.
void onTurnRight() {
menuTimer.start(WITH_RESET);
switch (status) {
case 'h':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 'm':
status = 's';
P.displayClear();
P.print("Set");
break;
case 'H':
setHourVar++;
if(setHourVar >= 24){setHourVar = 0;}
snprintf(displayChar, sizeof(displayChar), "%02d", setHourVar);
P.displayClear();
P.print(displayChar);
break;
case 'M':
setMinuteVar++;
if(setMinuteVar >= 60){setMinuteVar = 0;}
snprintf(displayChar, sizeof(displayChar), "%02d", setMinuteVar);
P.displayClear();
P.print(displayChar);
break;
case 's':
status = 'e';
P.displayClear();
P.print("Exit");
break;
case 'e':
status = 'g';
P.displayClear();
P.print("Game");
break;
case 'G':
if (gamePlayer - 1 != gameBlocks[0]) {
if(((gamePlayer == 0) && (gameBlocks[0] == 7)) == 0){
gamePlayer--;
if(gamePlayer < 0){
gamePlayer = 7;
}
}
}
gameUpdateScreen();
break;
default:
break;
}
}
// Define an onPress handler.
void onPress() {
menuTimer.start(WITH_RESET);
DateTime now = rtc.now();
switch (status) {
case 'N':
setHourVar = now.hour();
setMinuteVar = now.minute();
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'h':
status = 'H';
snprintf(displayChar, sizeof(displayChar), "%02d", setHourVar);
P.displayClear();
P.print(displayChar);
break;
case 'm':
status = 'M';
snprintf(displayChar, sizeof(displayChar), "%02d", setMinuteVar);
P.displayClear();
P.print(displayChar);
break;
case 'H':
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'M':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 's':
status = 'N';
setTime(setHourVar, setMinuteVar);
showTime();
break;
case 'e':
status = 'N';
showTime();
break;
case 'g':
status = 'G';
P.displayClear();
P.displaySuspend(true);
playGame();
break;
case 'G':
status = 'g';
gameScore = 0;
for (uint8_t i = 0; i <= 31; i++) {
gameBlocks[i] = 255;
}
P.displaySuspend(false);
P.displayClear();
P.print("Game");
break;
default:
break;
}
}
CtrlBtn button(2, 50, onPress);
CtrlEnc encoder(4, 3, onTurnLeft, onTurnRight);
This function is used when changing time. It takes the time in hours and minutes and changes the time on the RTC accordingly.
void setTime(int8_t newHour, int8_t newMinute) {//take two variables and sets the time of the rtc to them
DateTime before = rtc.now();
DateTime updated(before.year(), before.month(), before.day(), newHour, newMinute, 0);
rtc.adjust(updated);
}
showTime updates the display with the current time. this is used whenever the time changes or the menu is exited.
void showTime() {//updates the display and shows the current time.
DateTime now = rtc.now();
oldMin = now.minute();
if( status == 'N'){
snprintf(displayChar, sizeof(displayChar), "%02d:%02d", now.hour(), now.minute());
P.displayClear();
P.print(displayChar);
}
}
Now we get to the setup code
This code starts the Parola library and sets the brightness.
P.begin();
P.setIntensity(0);
mx = P.getGraphicObject();
This sets up the RTC, makes sure it’s running, and sets the time if needed. It also save the current minute so the display updates correctly.
if (! rtc.begin()) {
while (1) delay(10);
}
if (! rtc.isrunning()) {
// When time needs to be set on a new device, or after a power loss, the
// following line sets the RTC to the date & time this sketch was compiled
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
DateTime now = rtc.now();
oldMin = now.minute();
This show the text "Hello!" for 4 seconds and then shows the time.
P.setTextAlignment(PA_CENTER);
P.print("Hello!");//Turns the screen on and says hello
delay(4000);
showTime();
This stops the timer for exiting the menu and starts the RTC refresh time which is used to check if the time has changed
menuTimer.stop();
rtcRefreshTimer.start(WITH_RESET);
Now we get to the loop. this runs repeatedly while the code runs
This automatically exits the menu if it hasn’t been used for a while, so the clock always returns to the normal time display.
if (menuTimer.triggered()) {//This exits the menu automatically
switch (status) {
case 'h':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'm':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'H':
status = 'h';
P.displayClear();
P.print("Hours");
break;
case 'M':
status = 'm';
P.displayClear();
P.print("Minutes");
break;
case 's':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'e':
status = 'N';
showTime();
menuTimer.stop();
break;
case 'g':
status = 'N';
showTime();
menuTimer.stop();
break;
default:
break;
}
}
This checks if the time has switched to the next minute once a second.
if(rtcRefreshTimer.triggered()){//checks if time has changed
DateTime now = rtc.now();
if (oldMin != now.minute()) {
showTime();
}
}
This checks for changes in rotary encoder state and triggers one of the handlers if the rotary encoder has been turned or pressed
encoder.process();
button.process();
Now we get to the functions that run the game
playGame updates the screen to be showing the game and then starts a loop while the status variable equals G. In the loop it procceses the encoder. if the gameBlockTimer is triggered it increments the score and moves the blocks down.
void playGame(){
gameUpdateScreen();
while (status == 'G') {
encoder.process();
button.process();
if(gameBlockTimer.TRIGGERED){
gameScore++;
gameMoveBlocks();
}
}
}
gameMoveBlocks moves the variable for the horisontal position of each blcok to position right below it to simulate falling blocks. If a block directly above the player collides with the player it resets the game.
void gameMoveBlocks() {
for (uint8_t i = 1; i <= 31; i++) {
if (gameBlocks[i] != 255) {
gameBlocks[i - 1] = gameBlocks[i];
}
}
gameBlocks[31] = rand() % 8;
if (gameBlocks[0] == gamePlayer){
delay(1000);
gamePrintScore();
gameScore = 0;
delay(1000);
for (uint8_t i = 0; i <= 31; i++) {
gameBlocks[i] = 255;
}
}
gameUpdateScreen();
}
void gameUpdateScreen() {
mx->clear();
mx->setPoint(gamePlayer, 0, true);
for(uint8_t i = 0; i <= 31; i++){
if(gameBlocks[i] != 255){
mx->setPoint(gameBlocks[i], i, true);
}
}
}
gameUpdateScreen draws dots for the position of each block and the player.
void gameUpdateScreen() {
mx->clear();
mx->setPoint(gamePlayer, 0, true);
for(uint8_t i = 0; i <= 31; i++){
if(gameBlocks[i] != 255){
mx->setPoint(gameBlocks[i], i, true);
}
}
}
gamePrintScore just prints the value of gameScore for four seconds.
void gamePrintScore(){
P.displaySuspend(false);
snprintf(displayChar, sizeof(displayChar), "%05u", gameScore);
P.displayClear();
P.print(displayChar);
delay(4000);
P.displayClear();
P.displaySuspend(true);
}