I was a bit naughty and got to work right after posting
Here’s the updated code. I’ve fiddled and played with it, tweaking the looks and trying to break it on purpose. So far it has withstood some “abuse” on the input pin and has run stable.
The input pin A7 has no digital pin IO hardware on it, but can be MUXed to the ADC. It can only be an INPUT, but not an INPUT_PULLUP. @lookmumnocomputer make sure to connect a button pulling the pin HIGH (to VCC) with a Pull-Down resistor (to GND).
The new finite state machine may look somewhat complicated at first, but ensures smooth and maintainable transitions between modes.
//Big VFD 7 segment display animation for Museum Of Everything Else
//Written by Ennar 24.Jun.2021
//V1.2
//Additional d10 die feature idea contributed by Caustic
//Plays back animations on the seven segment VFD featured in the Museum Of Everything Else display case.
//If INPUT_PIN is held high, it will roll a d10 die. Release to display the number. After some time it
//will resume playing animations
//********** ANIMATION **********
#define CHANGEOVER_SECONDS 8 /*after this many seconds, the animation is changed out to another*/
//uncomment either one of these to activate the mode
#define CHANGEOVER_MODE_CONSECUTIVE /* makes sure every animation has an appearance on stream */
//#define CHANGEOVER_MODE_RANDOM /* selects animation randomly, better for displaying in the museum */
//********** D10 DIE **********
#define DIE_MIN_SPEED 32/* minimum speed for dice spinner animation (dwellSteps value!)*/
#define DIE_MAX_SPEED 7/* maximum speed for dice spinner animation (dwellSteps value!) */
#define DIE_DWELLSTEPS_SHOW 255 /*how long the resulting number will be shown*/
#define DIE_DWELLSTEPS_FADE 120 /*how long the resulting number will be faded before resuming animation player*/
unsigned char dieDwellSteps = DIE_MIN_SPEED;
unsigned char dieRolledNumber = 0;
//********** MISC **********
#define INPUT_PIN A7 /* universal (analog!) input pin, currently used for dice feature*/
#define MAX_BRIGHTNESS 52 /*upper limit for Software PWM. Higher means finer control, but runs slower*/
#define DELAY_US 46 /*global delay inside software PWM loop*/
const unsigned char pins[7] = {14, 13, 18, 17, 16, 15, 19}; //abcdefg (display like in the video!)
//const unsigned char pins[7] = {17, 16, 15, 14, 13, 18, 19}; //abcdefg (in case the numbers display upside down)
//const unsigned char pins[7] = {13, 14, 17, 18, 15, 16, 19}; //randomized segments
unsigned char brightness[7] = {}; //max is MAX_BRIGHTNESS
//********** MODE LOGIC **********
//since the animation is no longer the sole mode, this sketch also needs a state machine to determine its mode
enum stateMachineStates {startUp, playAnimation, dieSpinUp, dieSpinDown, dieShow, dieFade};
stateMachineStates currentState, nextState = startUp;
//input pin is assumed to be an analog pin, give it some helper variables to deal with noisy input
//A function that reads the button will act as its own two-state state machine
#define BUTTON_HYSTERESIS 100 /*hysteresis for button around ADC_MAX/2 (usually 511) */
unsigned char buttonState, buttonLastState = 0; //used as binary state for logic
double buttonVal; //Holds last saved value from button
double buttonInfluence = 0.2; //determines how much influence each ADC reading has on buttonVal
//********** ANIMATIONS **********
const unsigned char heartbeatAllSegments[] = {3, 0xFF, 10, 0xFF, 40, 0xFF, 150}; //length(once), frame data, dwell steps (interleaved)
const unsigned char individualSegments[] =
{ 7,
0b00000001, 15,
0b00000010, 15,
0b00000100, 15,
0b00001000, 15,
0b00010000, 15,
0b00100000, 15,
0b01000000, 120
};
const unsigned char insideOutDown[] =
{ 5,
0b01000000, 15,
0b00000001, 15,
0b00100010, 15,
0b00010100, 15,
0b00001000, 15
};
const unsigned char sparklyBits[] =
{ 6,
0b01100000, 16,
0b00000100, 16,
0b00010000, 16,
0b00000010, 16,
0b01001000, 16,
0b00000001, 16
};
//draw out numbers
//these two are constants that would be very awful to replace in every line
#define NUMBERS_SPEED1 8
#define NUMBERS_SPEED2 60
const unsigned char animatedNumbers[] =
{ 44,
0b00000001, NUMBERS_SPEED1,
0b00000010, NUMBERS_SPEED1,
0b00000100, NUMBERS_SPEED1,
0b00001000, NUMBERS_SPEED1,
0b00010000, NUMBERS_SPEED1,
0b00100000, NUMBERS_SPEED1,
0b00000001, NUMBERS_SPEED2,
0b00000010, NUMBERS_SPEED1,
0b00000100, NUMBERS_SPEED2,
0b00000001, NUMBERS_SPEED1,
0b00000010, NUMBERS_SPEED1,
0b01000000, NUMBERS_SPEED1,
0b00010000, NUMBERS_SPEED1,
0b00001000, NUMBERS_SPEED2,
0b01000000, NUMBERS_SPEED1,
0b00000110, NUMBERS_SPEED1,
0b00001001, NUMBERS_SPEED2,
0b00100000, NUMBERS_SPEED1,
0b01000000, NUMBERS_SPEED1,
0b00000010, NUMBERS_SPEED1,
0b00000100, NUMBERS_SPEED2,
0b00000001, NUMBERS_SPEED1,
0b00100000, NUMBERS_SPEED1,
0b01000000, NUMBERS_SPEED1,
0b00000100, NUMBERS_SPEED1,
0b00001000, NUMBERS_SPEED2,
0b00000001, NUMBERS_SPEED1,
0b00100000, NUMBERS_SPEED1,
0b00010000, NUMBERS_SPEED1,
0b00001000, NUMBERS_SPEED1,
0b00000100, NUMBERS_SPEED1,
0b01000000, NUMBERS_SPEED2,
0b00000001, NUMBERS_SPEED1,
0b00000010, NUMBERS_SPEED1,
0b00000100, NUMBERS_SPEED2,
0b01000000, NUMBERS_SPEED1,
0b00110110, NUMBERS_SPEED1,
0b00001001, NUMBERS_SPEED2,
0b01000000, NUMBERS_SPEED1,
0b00100000, NUMBERS_SPEED1,
0b00000001, NUMBERS_SPEED1,
0b00000010, NUMBERS_SPEED1,
0b00000100, NUMBERS_SPEED1,
0b00001000, NUMBERS_SPEED2 * 2,
};
const unsigned char snakeEight[] =
{ 8,
0b00000001, 15,
0b00000010, 15,
0b01000000, 15,
0b00010000, 15,
0b00001000, 15,
0b00000100, 15,
0b01000000, 15,
0b00100000, 15
};
const unsigned char scannerEight[] =
{ 10,
0b01000000, 15,
0b00000110, 15,
0b00001001, 15,
0b00110000, 15,
0b01000000, 85,
0b01000000, 15,
0b00110000, 15,
0b00001001, 15,
0b00000110, 15,
0b01000000, 85
};
//spinner animation for die, dwellsteps are ignored and overwritten by #define'd values
const unsigned char spinner[] =
{ 6,
0b00000001, 0,
0b00000010, 0,
0b00000100, 0,
0b00001000, 0,
0b00010000, 0,
0b00100000, 0,
};
//not an animation, just numbers for the die roll
const unsigned char numbers[] {
10,
0b00111111, 0,
0b00000110, 0,
0b01011011, 0,
0b01001111, 0,
0b01100110, 0,
0b01101101, 0,
0b01111101, 0,
0b00000111, 0,
0b01111111, 0,
0b01101111, 0
};
const unsigned char* animations[] = {insideOutDown, heartbeatAllSegments, individualSegments, snakeEight, animatedNumbers, sparklyBits, scannerEight, numbers}; //pointers to animations in memory
unsigned char* animation = animations[0]; //holds pointer to currently selected animation
unsigned char animFrame = 0; //holds Currently active frame
unsigned char currentAnimNumber = 0; //Helper variable to prevent double consecutive showings
unsigned char lastAnimNumber = 0; //Helper variable to prevent double consecutive showings
unsigned char dwellSteps = 0; //holds currently active frame's remaining dwell steps. DwellSteps results in a delay of DELAY_US*MAX_BRIGHTNESS µs
void setup() {
//Set all the pins required for the segments to OUT
for (int i = 0; i < 7; i++) {
pinMode(pins[i], OUTPUT);
}
pinMode(INPUT_PIN, INPUT); //reserved input pin
getAnimationFrame(0, 0); //load first animation frame
//Serial.begin(9600);
//debugMsg();
}
void loop() {
//read button and decide its state
buttonVal = (1 - buttonInfluence) * buttonVal + (buttonInfluence * analogRead(INPUT_PIN));
if (buttonVal > (511 + BUTTON_HYSTERESIS) && buttonLastState == 0) {
buttonState = 1;
} else if (buttonVal < (511 - BUTTON_HYSTERESIS) && buttonLastState == 1) {
buttonState = 0;
}
buttonLastState = buttonState;
//Main finite state machine for mode selection
//enum stateMachineStates {startUp, playAnimation, dieSpinUp, dieSpinDown, dieShow, dieFade};
//1/2: State jumping and condition-bound actions like resetting something to default values
//Imagine all states as a tree of connected bubbles. Every bubble must lead to another, each path
//"blocked" by a condition that must be met (in this case pressed/released button, waiting times)
switch (currentState) {
case startUp:
if (buttonState == 1) nextState = dieSpinUp;
else nextState = playAnimation;
break;
case playAnimation:
if (buttonState == 1) {
dieDwellSteps = DIE_MIN_SPEED * 0.7; //Reset to starting speed
dwellSteps = dieDwellSteps;
nextState = dieSpinUp;
}
else nextState = playAnimation;
break;
case dieSpinUp:
if (buttonState == 1) nextState = dieSpinUp; //resume spinning WITHOUT resetting the speed
else nextState = dieSpinDown;
break;
case dieSpinDown:
if (buttonState == 1) nextState = dieSpinUp; //resume spinning WITHOUT resetting the speed
else if (buttonState == 0 && dieDwellSteps < DIE_MIN_SPEED) nextState = dieSpinDown;
else {
dieRolledNumber = random(0, 9);
nextState = dieShow;
}
break;
case dieShow:
if (buttonState == 1) {
dieDwellSteps = DIE_MIN_SPEED * 0.7; //Reset to starting speed
dwellSteps = dieDwellSteps;
nextState = dieSpinUp;
}
else if (buttonState == 0 && dieDwellSteps < DIE_DWELLSTEPS_SHOW) nextState = dieShow;
else {
dieDwellSteps = 0; //dieDwellsteps will be used for a timeout in the following state, so reset it!
nextState = dieFade;
}
break;
case dieFade:
if (buttonState == 1) {
dieDwellSteps = DIE_MIN_SPEED * 0.7; //Reset to starting speed
nextState = dieSpinUp;
}
else if (buttonState == 0 && dieDwellSteps > DIE_DWELLSTEPS_FADE) {
dieDwellSteps = 0; //dieDwellsteps will be used for a timeout in the following state, so reset it!
dwellSteps = 0;
nextState = playAnimation;
}
break;
default: //state machine should never reach this under normal conditions, but will be provided anyway
nextState = startUp;
break;
};
//2/2 state-bound actions
//These actions are performed as long as its corresponding state is active
//enum stateMachineStates {startUp, playAnimation, dieSpinUp, dieSpinDown, dieShow, dieFade};
switch (currentState) {
case startUp:
break;
case playAnimation:
//This consists of two nested loops, forming a software PWM and animation player
// Animation specific code
//switch animations automatically every CHANGEOVER_SECONDS seconds.
//Keeping this active for one second jumbles the display up a bit to make it more interesting
if ((millis() / 1000) % CHANGEOVER_SECONDS == 0) {
#ifdef CHANGEOVER_MODE_RANDOM
while (currentAnimNumber == lastAnimNumber) {
currentAnimNumber = random(0, 6);
}
#endif
#ifdef CHANGEOVER_MODE_CONSECUTIVE
currentAnimNumber = lastAnimNumber + 1;
#endif
animFrame = 0;
dwellSteps = 0;
randomBrightness();
} else {
animation = animations[currentAnimNumber];
if (currentAnimNumber > 6) currentAnimNumber = 0;
lastAnimNumber = currentAnimNumber;
//if (animFrame == 0) Serial.println(nextAnim); //Sometimes there was a glitch happening, trying to find out which animation number causes that
//Simple fade-out type animations
fadeOutAll();
if (dwellSteps > 0) {
dwellSteps--;
} else {
animFrame++;
if (animFrame >= animation[0]) animFrame = 0;
getAnimationFrame(animFrame, 1); //mode 1 writes HIGH bits from animation byte only
//debugMsg();
}
}
break;
case dieSpinUp:
animation = spinner;
if (dwellSteps > 0) {
dwellSteps--;
} else {
if (dieDwellSteps > DIE_MAX_SPEED)dieDwellSteps--;
animFrame++;
if (animFrame >= animation[0]) animFrame = 0;
getAnimationFrame(animFrame, 1); //mode 1 writes HIGH bits from animation byte only
dwellSteps = dieDwellSteps; //Overwrite dwellSteps from animation
//debugMsg();
}
fadeOutAll();
break;
case dieSpinDown:
animation = spinner;
if (dwellSteps > 0) {
dwellSteps--;
} else {
if (dieDwellSteps < DIE_MIN_SPEED)dieDwellSteps++;
animFrame++;
if (animFrame >= animation[0]) animFrame = 0;
getAnimationFrame(animFrame, 1); //mode 1 writes HIGH bits from animation byte only
dwellSteps = dieDwellSteps; //Overwrite dwellSteps from animation
//debugMsg();
}
fadeOutAll();
break;
case dieShow:
animation = numbers;
fadeInAll();
getAnimationFrame(dieRolledNumber, 2); //mode 2 writes LOW bits from animation byte only, overwriting the fade-ins
dieDwellSteps++;
break;
case dieFade:
fadeOutAll();
dieDwellSteps++;
break;
default:
break;
};
currentState = nextState;
// Software PWM
for (unsigned char softPWM = 0; softPWM < MAX_BRIGHTNESS; softPWM++) {
for (int segment = 0; segment < 7; segment++) {
if (brightness[segment] > softPWM) {
digitalWrite(pins[segment], HIGH);
} else {
digitalWrite(pins[segment], LOW);
}
}
delayMicroseconds(DELAY_US);
}
}
//Loads animation frames from array to segment brightness buffer, and gets new value for dwellSteps
//mode 0: overwrite every bit
//mode 1: overwrite only ones (fade-out animation types)
//mode 2: overwrite only zeroes (fade-in animation types)
void getAnimationFrame(unsigned char frameNumber, unsigned char mode) {
switch (mode) {
case 0:
for (int i = 0; i < 7; i++) {
brightness[i] = ((animation[(frameNumber * 2) + 1] & (1 << i)) >> i) * MAX_BRIGHTNESS; //uses bits from animation array
}
break;
case 1:
for (int i = 0; i < 7; i++) {
if (animation[(frameNumber * 2) + 1] & (1 << i)) brightness[i] = MAX_BRIGHTNESS; //uses bits from animation array
}
break;
case 2:
for (int i = 0; i < 7; i++) {
if (!(animation[(frameNumber * 2) + 1] & (1 << i))) brightness[i] = 0; //uses bits from animation array
}
}
dwellSteps = animation[(frameNumber * 2) + 2];
}
//Fades every segment out by 1 if it is still lit (brightness >0)
void fadeOutAll() {
for (int i = 0; i < 7; i++) {
if (brightness[i] > 0) brightness[i]--;
}
}
//Fades every segment in by 1 if it is still below MAX_BRIGHTNESS
void fadeInAll() {
for (int i = 0; i < 7; i++) {
if (brightness[i] < MAX_BRIGHTNESS) brightness[i]++;
}
}
//Write random brightness values to all segments
void randomBrightness() {
for (int i = 0; i < 7; i++) {
brightness[i] = (random(0, MAX_BRIGHTNESS) / 16) * 16; //more contrast
//brightness[i] = random(0, MAX_BRIGHTNESS);
}
}
//A leftover function to print out all brightness values
void debugMsg() {
for (int i = 0; i < 7; i++) {
Serial.print(brightness[i]); Serial.print(' ');
} Serial.println();
}
I hope this will perform as intended
Ennar