#include <EEPROM.h>
// Look ma no LED lib's
// Fully manual digital 8x8 led matrix, now with multiple directions of animation from button input
/* -- // -- CURRENT THOUGHTS ABOUT THIS PROJECT -- // --
Currently uses polling timers to control animation update
I would like to experiment with software timer interrupts for this, but theres quite a few iterations to do
each interrupt so its not entirely feasable
is to possible to get a hardware interrupt from serial information coming to the arduino?
i believe the issue with serial input being mush comes from the arduino doing other things
and then parsing the serial data mid-transmission and getting junk
if we could run the serial read function as an interrupt then this issue shouldn't happen
same goes for the button, but recording how long a button is being pressed for through only interrupts
doesn't seem to be possible, so there will still be polling in loop()
using software interrupt to perform the display update is a possibility, but theres quite a lot to loop through
since every pixel is being edited once and then read once, per cycle of loop()
*/
// -- PIN DEFINITONS -- //
const uint8_t ROW_PINS[] = {22,24,26,28,30,32,34,36}; // Connected to Anode
const uint8_t COL_PINS[] = {23,25,27,29,31,33,35,37}; // Connected to Cathode
#define BUTTON_PIN 2 // anim change button pin
#define POTENTIOMETER_PIN A0
bool pixels[8][8] = {
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0},
{0,0,0,0,0,0,0,0}
}; // The array of pixels which determines which LED's should be on or off
// We're only using 59 chracters, which is all characters from space to uppercase Z
// This was painful to make, but im sure you knew that already
const byte ASCII_DECODER[][8] = { // Gargantuan array which converts character input into readable display patterns
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // NULL
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // SOH
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // STX
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // ETX
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // EOT
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // ENQ
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // ACK
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // BEL
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // BS (ok how would this appear anywhere?)
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // TAB
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // LF (could be "\n")
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // VT
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // FF
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // CR
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // SO
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // SI
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // DLE
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // DC1
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // DC2
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // DC3
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // DC4
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // NAK
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // SYN
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // ETB
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // CAN
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // EM
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // SUB
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // ESC (again, how does this appear)
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // FS
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // GS
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // RS
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // US
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // Space
{B00011000,B00011000,B00011000,B00011000,B00011000,B00000000,B00011000,B00011000}, // !
{B00110110,B01101100,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // "
{B00100100,B00100100,B11111111,B00100100,B00100100,B11111111,B00100100,B00100100}, // #
{B00001000,B00111100,B01001000,B01011000,B00111100,B00010010,B01111100,B00010000}, // $
{B00000001,B01100010,B01100100,B00001000,B00010000,B00100110,B01000110,B10000000}, // %
{B00110000,B01001000,B01001000,B00110000,B01011000,B01001000,B01001100,B00110010}, // &
{B00011000,B00011000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000000}, // '
{B00000010,B00000100,B00001000,B00001000,B00001000,B00001000,B00000100,B00000010}, // (
{B01000000,B00100000,B00010000,B00010000,B00010000,B00010000,B00100000,B01000000}, // )
{B00010000,B00111000,B00010000,B00000000,B00000000,B00000000,B00000000,B00000000}, // *
{B00000000,B00011000,B00011000,B01111110,B01111110,B00011000,B00011000,B00000000}, // +
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00000110,B00001110}, // ,
{B00000000,B00000000,B00000000,B01111110,B01111110,B00000000,B00000000,B00000000}, // -
{B00000000,B00000000,B00000000,B00000000,B00000000,B00000000,B00001100,B00001100}, // .
{B00000001,B00000010,B00000100,B00001000,B00010000,B00100000,B01000000,B10000000}, // /
{B00011000,B00100100,B01000010,B01001110,B01110010,B01000010,B00100100,B00011000}, // 0
{B00001000,B00011000,B00101000,B00001000,B00001000,B00001000,B00001000,B01111110}, // 1
{B00111000,B01101100,B00000110,B00000110,B00011100,B00111000,B01100000,B01111110}, // 2
{B01111100,B00001110,B00001110,B01111100,B01111100,B00001110,B00001110,B01111100}, // 3
{B00000100,B00001100,B00010100,B00100100,B01000100,B01111110,B00000100,B00000100}, // 4
{B01111110,B01000000,B01000000,B01000000,B01111100,B00000110,B00000110,B01111100}, // 5
{B00011100,B00111000,B01110000,B01100000,B01111100,B01000010,B01000010,B00111100}, // 6
{B01111110,B00000110,B00000110,B00000110,B00001100,B00001100,B00011000,B00011000}, // 7
{B00111100,B01000010,B01000010,B01000010,B00111100,B01000010,B01000010,B00111100}, // 8
{B00111100,B01000010,B01000010,B01000010,B00111110,B00000010,B00000010,B00000010}, // 9
{B00000000,B00011000,B00011000,B00000000,B00000000,B00011000,B00011000,B00000000}, // :
{B00000000,B00011000,B00011000,B00000000,B00000000,B00011000,B00011000,B00110000}, // ;
{B00000000,B00001111,B00111100,B11100000,B11100000,B00111100,B00001111,B00000000}, // <
{B00000000,B01111110,B01111110,B00000000,B00000000,B01111110,B01111110,B00000000}, // =
{B00000000,B11110000,B00111100,B00001111,B00001111,B00111100,B11110000,B00000000}, // >
{B01111100,B11000110,B10000011,B00000111,B00001110,B00000000,B00001100,B00001100}, // ?
{B00111100,B01000010,B10011101,B10100101,B10100101,B10011110,B01000000,B00111110}, // @
{B00011000,B00100100,B01000010,B01111110,B01000010,B01000010,B01000010,B01000010}, // A
{B01111100,B01000010,B01000010,B01000010,B01111100,B01000010,B01000010,B01111100}, // B
{B00111100,B01000010,B01000000,B01000000,B01000000,B01000000,B01000010,B00111100}, // C
{B01111100,B01000010,B01000010,B01000010,B01000010,B01000010,B01000010,B01111100}, // D
{B01111110,B01000000,B01000000,B01111110,B01000000,B01000000,B01000000,B01111110}, // E
{B01111110,B01000000,B01000000,B01111110,B01000000,B01000000,B01000000,B01000000}, // F
{B00111100,B01000010,B01000000,B01000000,B01000000,B01001110,B01000010,B00111110}, // G
{B01000010,B01000010,B01000010,B01111110,B01000010,B01000010,B01000010,B01000010}, // H
{B01111110,B00011000,B00011000,B00011000,B00011000,B00011000,B00011000,B01111110}, // I
{B01111110,B00000100,B00000100,B00000100,B00000100,B00000100,B01000100,B00111000}, // J
{B01000110,B01001100,B01011000,B01110000,B01110000,B01011000,B01001100,B01000110}, // K
{B01000000,B01000000,B01000000,B01000000,B01000000,B01000000,B01000000,B01111110}, // L
{B01100110,B01111110,B01011010,B01011010,B01011010,B01011010,B01000010,B01000010}, // M
{B01100010,B01110010,B01010010,B01011010,B01011010,B01001110,B01001110,B01000110}, // N
{B00111100,B01000010,B01000010,B01000010,B01000010,B01000010,B01000010,B00111100}, // O
{B01111100,B01000010,B01000010,B01000010,B01111100,B01000000,B01000000,B01000000}, // P
{B00111100,B01000010,B01000010,B01000010,B01000010,B01000010,B00111100,B00000110}, // Q
{B01111100,B01000010,B01000010,B01000010,B01111100,B01111000,B01011100,B01001110}, // R
{B00111100,B01000010,B01000000,B01000000,B00111100,B00000010,B01000010,B00111100}, // S
{B01111110,B01111110,B00011000,B00011000,B00011000,B00011000,B00011000,B00011000}, // T
{B01000010,B01000010,B01000010,B01000010,B01000010,B01000010,B01000010,B00111100}, // U
{B01000010,B01000010,B01000010,B01000010,B00100100,B00100100,B00100100,B00011000}, // V
{B01000010,B01000010,B01000010,B01000010,B01011010,B01011010,B01100110,B01100110}, // W
{B01000010,B01100110,B00100100,B00011000,B00011000,B00100100,B01100110,B01000010}, // X
{B01000010,B01000010,B00100100,B00100100,B00011000,B00011000,B00011000,B00011000}, // Y
{B01111110,B00000010,B00000100,B00001000,B00010000,B00100000,B01000000,B01111110} // Z
};
// Animation counter values
unsigned long prevTime = 0;
int16_t animationCharacter = 0;
uint16_t animationTimer = 0;
uint8_t animationBit = 0;
// Serial input vars
char message[256]; // The current message to display, with a healthy sized buffer
#define MESSAGE_ADDRESS 1 // EEPROM index which holds the number of bytes stored as the message
// Button logic vars
#define BUTTON_DELAY 2000 // Time to change animation from holding the button down in miliseconds
uint32_t buttonTimer = 0; // current button held duration, 0 if not being held
bool isBackward = false; // current Anim State
// SETUP AND LOOP //
void setup() {
Serial.begin(115200);
// setup pinmodes
pinMode(BUTTON_PIN, INPUT_PULLUP);
for (uint8_t i = 0; i < 8; i++) {
pinMode(ROW_PINS[i], OUTPUT);
pinMode(COL_PINS[i], OUTPUT);
digitalWrite(COL_PINS[i], HIGH); // Set all cathodes to high turn off every LED
}
/* Debug thing to read my pixels array
Serial.println("pixels readout");
for (uint8_t i = 0; i < 8; i++) {
for (uint8_t j = 0; j < 8; j++) {
Serial.print(pixels[i][j]);
}
Serial.println("");
}
*/
// readEEPROM(); // this would set the current message[] to what's on the EEPROM, but wokwi no likey that
Serial.println("// -- Ready for input, note that the display only supports CAPITAL letters, numbers and a few symbols -- //");
}
void loop() {
// get the time since the last call of loop()
unsigned long currentTime = millis(); // Set the current time
uint8_t deltaTime = currentTime - prevTime; // Calculate the change in time from last call
// Get input from the Slide Potentiometer and convert it to a more appropriate speed value
// minimum speed of 10 miliseconds
uint16_t displaySpeed = analogRead(POTENTIOMETER_PIN)/4 + 10;
// Serial input logic
if (Serial.available() != 0) { // Check if there is data in the serial bus
uint8_t byteCount = Serial.readBytes(message,255); // Write serial contents to the buffer
resetCounters(); // Reset the counters for the new message
writeToEEPROM(message,byteCount); // Write new contents to the EEPROM
Serial.print("Changed message to: ");
Serial.print(message); // Since message ends with "/n", we dont need println
}
// Button logic
if (digitalRead(BUTTON_PIN) == HIGH) { // If the button ISN'T being pushed, check the timer value
switch (buttonTimer) {
case 74 ... BUTTON_DELAY: // needs to be held for more than 74 miliseconds as bounce protection
isBackward = false;
buttonTimer = 0;
break;
case BUTTON_DELAY + 1 ... BUTTON_DELAY * 100:
isBackward = true;
buttonTimer = 0;
break;
default: // Bounce protection engaged
buttonTimer = 0;
break;
}
} else { // If the button IS being pushed, incriment timer value
buttonTimer += deltaTime;
}
// Animation selection from button
switch (isBackward) {
case false:
printMessage(message,displaySpeed,deltaTime);
break;
case true:
printReverseMessage(message,displaySpeed,deltaTime);
break;
}
updateDisplayFromPixels(); // Update the display
prevTime = currentTime; // set the next "last" time to the current time
}
// STATIC/BASIC DISPLAY DRAWING FUNCTIONS //
// Cycle the display normally
void updateDisplayFromPixels() {
for (uint8_t currentRow = 0; currentRow < 8; currentRow++) {
digitalWrite(ROW_PINS[currentRow], HIGH); // set the current row (anode) to HIGH
for (uint8_t currentCol = 0; currentCol < 8; currentCol++) {
uint8_t currentPix = !(pixels[currentRow][currentCol]); // Read the current pixel
digitalWrite(COL_PINS[currentCol],currentPix);
if (currentPix == LOW) {
digitalWrite(COL_PINS[currentCol], HIGH); // Turn the led off after turning it on
}
}
digitalWrite(ROW_PINS[currentRow], LOW);
}
}
// This was actually the first function i made before realising it inverted the colours
void updateDisplayFromPixels_Inverted() {
for (uint8_t currentRow = 0; currentRow < 8; currentRow++) {
// set the current anode to HIGH
digitalWrite(ROW_PINS[currentRow], HIGH);
// We need to read the byte from right to left and turn the rows on from left to right
for (uint8_t currentCol = 0; currentCol < 8; currentCol++) {
// When the row is HIGH and the col is LOW, the corresponding LED will turn on
digitalWrite(COL_PINS[currentCol],pixels[currentRow][currentCol]);
// Turn the led off after turning it on
if (pixels[currentRow][currentCol] == LOW) {
digitalWrite(COL_PINS[currentCol], HIGH);
}
}
digitalWrite(ROW_PINS[currentRow], LOW);
}
}
// Function to set the display from an array of bytes
void row_to_col_byte(byte input[]) {
for (uint8_t currentRow = 0; currentRow < 8; currentRow++) {
digitalWrite(ROW_PINS[currentRow], HIGH);
for (uint8_t currentCol = 0; currentCol < 8; currentCol++) {
uint8_t currentPix = !getBitFromByte(input[currentRow],currentCol);
digitalWrite(COL_PINS[currentCol], currentPix);
if (currentPix == LOW) { // Turn off the current LED now that we're done with it
digitalWrite(COL_PINS[currentCol], HIGH);
}
// Quick debugging
Serial.print(!currentPix);
}
Serial.println("");
digitalWrite(ROW_PINS[currentRow], LOW);
}
}
// PRIMARY DISPLAY ANIMATION FUNCTIONS //
// Display a message on the screen, currently only supports CAPITAL letters
// returns true once the message is completed, false otherwise
bool printMessage(char currentMessage[],uint16_t anim_delay, uint8_t delta) {
animationTimer += delta; // Update the timer
if (animationTimer > anim_delay) { // Commence the magic
animationTimer = 0; // Reset the timer
shiftPixelsLeft(ASCII_DECODER[currentMessage[animationCharacter]],animationBit);// Where the magic happens
// Boring cycle over stuff i cant do in a loop because it breaky the arduino
if (animationBit > 7) { // Reset/Iterate the Bit counter
animationBit = 0;
if (animationCharacter >= strlen(currentMessage)) { // Reset/Iterate the Char counter
animationCharacter = 0;
return true; // Tell the main loop the message is complete
} else {
animationCharacter++;
}
} else {
animationBit++;
}
}
return false;
/* This was the initial plan but it didnt work because the hardware needs to iterate over loop() properly
So i had to make a buncha global counter variables to iterate over, dang
for (uint16_t currentChar = 0; currentChar < strlen(message); currentChar++) { // iterate over the message
for (uint8_t currentBit = 0; currentBit < 8; currentBit++) { // Iterate over each bit
shiftPixels(ASCII_DECODER[currentMessage[currentChar]],currentBit); // where the magic happens
updateDisplayFromPixels(); // cycle the display
}
}
*/
}
// almost identical to printMessage but cycles the animationCharacter BACKWARDS and shifts pixels RIGHT
bool printReverseMessage(char currentMessage[], uint16_t anim_delay, uint8_t delta) {
animationTimer += delta;
if (animationTimer > anim_delay) {
animationTimer = 0;
shiftPixelsRight(ASCII_DECODER[currentMessage[animationCharacter]],animationBit);// Commence the magic, IN REVERSE
if (animationBit > 7) {
animationBit = 0;
if (animationCharacter < 0) { // We iterate backwards rather than forwards in the message
animationCharacter = strlen(currentMessage) - 1; // current message length - 1, last character is "/n" which is blank
} else {
animationCharacter--;
}
} else {
animationBit++;
}
}
}
// Called when the message is changed to prevent weirdness
void resetCounters() {
animationTimer = 0; // Reset the counters
animationBit = 0;
animationCharacter = 0;
writeBytesToPixel(ASCII_DECODER[0]); // At index 0 is a space, which clears the display
updateDisplayFromPixels(); // Update the display
}
// translates the raw text data into the pixels array
void writeBytesToPixel(byte bytes[8]) {
for (uint8_t currentByte = 0; currentByte < 8; currentByte++) { // Iterate over rows
for (uint8_t currentBit = 7; currentBit < 8; currentBit--) { // Iterate over Columns
// Need to put the LAST bit in the FIRST column and so on so it displays as it does in the decoder
// This means i dont need to flip everything over in the decoder which makes life MUCH easier
pixels[currentByte][7-currentBit] = getBitFromByte(bytes[currentByte],currentBit);
//Serial.print(getBitFromByte(bytes[currentByte],currentBit));
}
//Serial.println("");
}
}
// Function to shift the contents of the pixels array to the left by one pixel and
// writes the contents of each input's index bit into the 8th column of pixels
void shiftPixelsLeft(byte input[], uint8_t index) {
for (uint8_t pixelRow = 0; pixelRow < 8; pixelRow++) { // Iterate over rows
for (uint8_t pixelCol = 1; pixelCol < 8; pixelCol++) { // Iterate over columns except the first
pixels[pixelRow][pixelCol-1] = pixels[pixelRow][pixelCol]; // Write the current Col onto the Col behind
}
pixels[pixelRow][7] = getBitFromByte(input[pixelRow],7-index);// Write the index bit of each input to the last col
}
}
// Function to shift the contents of the pixels array to the right by one pixel and write byte contents to col 1
void shiftPixelsRight(byte input[], uint8_t index) {
for (uint8_t pixelRow = 0; pixelRow < 8; pixelRow++) { // Iterate over rows
for (int8_t pixelCol = 6; pixelCol >= 0; pixelCol--) { // Iterate from the 7th column to the first
pixels[pixelRow][pixelCol+1] = pixels[pixelRow][pixelCol]; // Write the current Col onto the Col in front
}
pixels[pixelRow][0] = getBitFromByte(input[pixelRow],index); // Write the index bit of each input to the first col
}
}
// OTHER FUNCTIONS //
// Function that returns a bit at a certain index of a byte
bool getBitFromByte(byte input, uint8_t index) {
index = 1 << index; // turn the index value into a 1 at bit position "index"
return bool(index & input); // compare if a bit exists at the index's bit position
}
// Function to write the contents of message[] to the EEPROM, can only store one message at a time
void writeToEEPROM(char input[],uint8_t byteCount) {
EEPROM.write(MESSAGE_ADDRESS,byteCount);
for (uint16_t i = MESSAGE_ADDRESS + 1; i < byteCount + 2; i++) { // Iterate over every character/byte
EEPROM.write(i,input[i-2]); // Write to the EEPROM, i is +2 from the array index
}
}
// read contents of EEPROM to message[] global
// currently unused since the simulator doesn't save EEPROM status between simulations
void readEEPROM() {
uint8_t bytesToRead = EEPROM.read(MESSAGE_ADDRESS); // Get the number of bytes stored
for (uint8_t i = MESSAGE_ADDRESS + 1; i < bytesToRead + 2; i++) { // iterate over every byte
message[i-2] = EEPROM.read(i); // Read at i to index - 2
}
}