From time to time I enjoy writing demo-codes. In this case you are the lucky guy that I took your code as the demo-code.
I have re-written the complete functionlity of your code in a non-blocking manner.
Your code is a really beautiful example how for-loops and delay - which are both blocking -
must be replaced by code that works non-blocking
There is a
fundamental difference
between sequential and blocking coding and non-blocking coding.
It is very important to
understand this difference as the first step.
Without understanding this difference most people try to see a sequential-blocking thing in non-blocking code. Which it really isn't. And trying to press non-blocking-code into a blocking-pattern does not work => Makes it very hard to understand.
I will use an everyday example to explain it.
Imagine cooking an egg. Most people use a short-time alarm-clock for beeing remembered at the right time to take the egg out of the cooking water.
If you don't have a short-time-alarm-clock handy you will take any kind of clock or watch.
heat up water until the water is boiling
put eggs into the water and take a look onto your watch.
Let's say it is 6:23 am. You want your egg smooth which means cooking-time is 4 to 5 minutes.
You start reading the newspaper but around every 30 seconds you take a new look onto your watch for checking how much time has passed by.
Your watch shows 6:25 am ehm when did I put the egg in 6:23 so 2 minutes not yet 4 minutes not yet time to take egg out of the water...
Your watch shows 6:26 am ehm when did I put the egg in 6:23 so 3 minutes not yet 4 minutes not yet time to take egg out of the water...
Your watch shows 6:28 am ehm when did I put the egg in 6:23 uups 5 minutes !
this is more than 4 minutes ! really time to take egg out of the water
You took an often repeated look onto your watch and calculated how much time has passed by.
You compared the result of the calculation with your timelimit cooking-time 4 minutes
You calculated
actualTime - CookingStartTime
and compared the result with a timelimit
if (actualTime - CookingStartTime >= 4_minutes) {
take_egg_out_of_the_water();
}
You could have looked onto your watch once every three seconds to get a more precise cooking-time. Though for a smooth egg it doesn't matter if the cooking-time is 4:00 or 4:17
Your microcontroller loves calculations. So give your microcontroller more fun through a very fast repeated calculation in combination with compairing
The basic principle is
void loop() {
if (actualTime - CookingStartTime >= 4_minutes) {
take_egg_out_of_the_water();
}
read_some_words_in_the_newsPaper();
take_a_sip_of_coffee();
checking how much time has passed by and only in case the timelimit is overdue
take action. If this is not yet the case do all the other stuff (almost) in parallel.
This is non-blocking timing
Back to your halloween-project:
Your code uses randomised numbers for times and speed of movements to get a more natural look-alike
But your device still does certain things in a well-defined sequence.
This sequence is:
start closing the eye with a certain speed
if eye is closed wait some time
start opening the eye with a certain speed
if eye is opened wait some time before a new blink of the eye starts
closing / opening the eye is a sub-sequence:
set servo to a start-position
wait a short time
set servo to a minimal proceeded position
wait a short time
repeat this until the eye-is_closed-position is reached
This is done by a for-loop
void closeEye(int spd) {
for (uint16_t pulseLen1 = topLidsOpen, pulseLen2 = botLidsOpen;
pulseLen1 < topLidsShut || pulseLen2 < botLidsShut; pulseLen1 += 10, pulseLen2 += 10) {
if (pulseLen1 < topLidsShut) {
topLidsServo.writeMicroseconds(pulseLen1);
}
if (pulseLen2 < botLidsShut) {
botLidsServo.writeMicroseconds(pulseLen2);
}
delay(spd);
}
}
The for-loop is blocking.
remember the sequence
step1: set servo to a start-position
step2: wait a short time
step3: set servo to a minimal proceeded position
if (servo-position is not yet in end-position) do again step2
if (servo-position has reached end-position do step4
step4: set servo to a position
step5: wait a short time
step6: set servo to a minimal proceeded position
if (servo-position is not yet in end-position) do again step5
if (servo-position has reached end-position do step7
step7: wait some time before starting over new
if waitingtime is over do step1
executing the steps one after the other could be coded
if (stepNo == 1) {
set servo to a start-position
}
if (stepNo == 2) {
wait a short time
}
if (stepNo == 3) {
set servo to a minimal proceeded position
if (servo position is not yet in end-position) {
stepNo = 2; // do again step2
}
if (servo - position has reached = end-position) {
stepNo = 4;
}
}
if (stepNo == 4) {
etc. etc.
But there is a variant which is more compact and better-suited if you have a lot of if-conditions that are similar but still different.
This is the switch-case-break-statement
The above functionality sketched as a switch-case-break-statemenet looks like this
switch (stepNo) {
case 1:
set servo to a start - position
break;
case 2:
wait a **short** time
break;
case 3:
set servo to a minimal proceeded position
if (servo position is not yet in end - position) {
stepNo = 2; // do again step2
}
if (servo - position has reached = end - position) {
stepNo = 4;
}
case 4:
etc. etc.
break;
} // end of switch-case-break-statement
This coding-technique is called a
step-chain
there is a chain of steps that gets executed
The more common but harder to understand term is state-machine
The "machine" runs through different "states" hence state-machine
The big advantage of a step-chain is that you need just a minimum of if-conditions
because the switch-variable does a pre-selection to only execute that part of the code that is of interest in this moment.
This pre-selection has another advantage:
you can let repeat the code a certain step over and over again until some special conditions become true (this is used for the non-blocking timing)
and last but not least this enables to do multiples things (almost) in parallel
void loop() {
if (actualTime - CookingStartTime >= 4_minutes) {
take_egg_out_of_the_water();
}
read_some_words_in_the_newsPaper();
take_a_sip_of_coffee();
because a longer actions like
- reading an 2 pages long article in the newspaper
- drinking a big cup of coffee
are all done in small steps with changing to the next action after a short time
if really all actions are divided into small steps this quickly
jump_in / jump_out enables to proceed with all actions in parallel.
And this is the fundamental difference between sequential coding and non-blocking coding.
If you look into the code after reading all this it will still take some time to re-recognise
"yes this is my code" or at least my functionality
If you have any questions about any detail just ask all your questions
#include<Servo.h>
#define topLidsShut 1883 // 1.88 mS
#define topLidsOpen 1233 // 1.23 mS
#define botLidsShut 1978 // 1.98 mS
#define botLidsOpen 1368 // 1.37 mS
int spd1; // speed eye-closing
int spd2; // speed eye-opening
int spd = 2; // Speed can range from 0(fast) => 15(slow)
int gap = 100; // non-blocking Delay between movements in milliseconds
int PauseBetweenBlinks; // non-blocking delay between blinkcycles
const int topLidsPin = 10; // Top lids servo output is on Pin #10
const int botLidsPin = 9; // Bottom lids servo ouptu is on Pin #9
const byte K1 = 12; // Assign pin numbers to FN-BC04 MP3 Kn inputs
const byte K2 = 4;
const byte K3 = 7;
const byte K4 = 8;
// Interrupt Variables
volatile boolean LowPulseActive = false;
volatile int gReq0 = 0; // Growl Request 0 connected to A0
volatile int gReq1 = 1; // Growl Request 1 connected to A1
volatile int mp3K0;
volatile int mp3K1;
Servo topLidsServo; // Name the top lids servo
Servo botLidsServo; // Name the bottom lids servo
const byte sc_InitClosing = 1;
const byte sc_CloseWriteServo = 2;
const byte sc_CloseWait = 3;
const byte sc_InitOpening = 4;
const byte sc_OpenWriteServo = 5;
const byte sc_OpenWait = 6;
const byte sc_WaitGapTime = 7;
const byte sc_WaitForNextBlink = 8;
byte BlinkStepNo = sc_InitClosing;
uint16_t pulseLen1;
uint16_t pulseLen2;
unsigned long WaitingTimer;
unsigned long LowPulseTimer;
// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
unsigned long currentMillis = millis();
if ( currentMillis - startOfPeriod >= TimePeriod ) {
// more time than TimePeriod has elapsed since last time if-condition was true
startOfPeriod = currentMillis; // a new period starts right here so set new starttime
return true;
}
else return false; // actual TimePeriod is NOT yet over
}
void stepChainBlink() {
switch (BlinkStepNo) {
case sc_InitClosing:
spd1 = random(15);
gap = random(100, 500);
pulseLen1 = topLidsOpen;
pulseLen2 = botLidsOpen;
topLidsServo.writeMicroseconds(pulseLen1);
botLidsServo.writeMicroseconds(pulseLen2);
WaitingTimer = millis(); // store snapshot of time when waiting starts
BlinkStepNo = sc_CloseWait; // do a FIRST waiting
break; // immidiately jump down to END-OF-SWITCH
case sc_CloseWriteServo:
if (pulseLen1 < topLidsShut) {
topLidsServo.writeMicroseconds(pulseLen1);
}
if (pulseLen2 < botLidsShut) {
botLidsServo.writeMicroseconds(pulseLen2);
}
BlinkStepNo = sc_CloseWait; // wait again
break; // immidiately jump down to END-OF-SWITCH
case sc_CloseWait:
if ( TimePeriodIsOver(WaitingTimer, spd1) ) {
// if the amount of milliseconds stored in spd1 have passed by
if (pulseLen1 < topLidsShut || pulseLen2 < botLidsShut) {
// if endpositions are not yet reached
pulseLen1 += 10; // set new positions
pulseLen2 += 10;
BlinkStepNo = sc_CloseWriteServo;
}
else { // endpositions ARE reached
WaitingTimer = millis(); // store snapshot of time when waiting starts
BlinkStepNo = sc_WaitGapTime;
}
}
break; // immidiately jump down to END-OF-SWITCH
case sc_WaitGapTime:
if ( TimePeriodIsOver(WaitingTimer, gap) ) {
BlinkStepNo = sc_InitOpening;
}
break; // immidiately jump down to END-OF-SWITCH
case sc_InitOpening:
spd2 = random(15);
pulseLen1 = topLidsShut;
pulseLen2 = botLidsShut;
topLidsServo.writeMicroseconds(pulseLen1);
botLidsServo.writeMicroseconds(pulseLen2);
WaitingTimer = millis(); // store snapshot of time when waiting starts
BlinkStepNo = sc_OpenWait; // do a FIRST waiting
break; // immidiately jump down to END-OF-SWITCH
case sc_OpenWait:
if ( TimePeriodIsOver(WaitingTimer, spd2) ) {
// if the amount of milliseconds stored in spd2 have passed by
if (pulseLen1 > topLidsOpen || pulseLen2 > botLidsOpen) {
// if endposition is not yet reached
pulseLen1 -= 10; // set new position
pulseLen2 -= 10;
BlinkStepNo = sc_OpenWriteServo;
}
else { // endpositions ARE reached
WaitingTimer = millis(); // store snapshot of time when waiting starts
PauseBetweenBlinks = random(100, 10000);
BlinkStepNo = sc_WaitForNextBlink; // one blink finished go idling until next blink starts
}
}
break; // immidiately jump down to END-OF-SWITCH
case sc_OpenWriteServo:
if (pulseLen1 > topLidsOpen) {
topLidsServo.writeMicroseconds(pulseLen1);
}
if (pulseLen2 > botLidsOpen) {
botLidsServo.writeMicroseconds(pulseLen2);
}
WaitingTimer = millis(); // store snapshot of time when waiting starts
BlinkStepNo = sc_OpenWait;
break; // immidiately jump down to END-OF-SWITCH
case sc_WaitForNextBlink:
if ( TimePeriodIsOver(WaitingTimer, PauseBetweenBlinks) ) {
// if the amount of milliseconds stored in PauseBetweenBlinks have passed by
BlinkStepNo = sc_InitClosing; // start next blink-cycle
}
break; // immidiately jump down to END-OF-SWITCH
} // END-OF-SWITCH
}
/******************* Initializing PWM and Serial *********************/
void setup() {
topLidsServo.attach(topLidsPin); // Attach the top lids servo to its pin
botLidsServo.attach(botLidsPin); // Attach the bottom lids servo to its pin
pinMode(K1, OUTPUT);
pinMode(K2, OUTPUT);
pinMode(K3, OUTPUT);
pinMode(K4, OUTPUT);
digitalWrite(K1, HIGH);
digitalWrite(K2, HIGH);
digitalWrite(K3, HIGH);
digitalWrite(K4, HIGH);
attachInterrupt (0, growlRequest, FALLING); // Vector to growlRequest upon falling pin #2
Serial.begin(115200); // Initialize the serial monitor for troubleshooting
Serial.println("Ready to Go!\r");
}
/************************** Growl Request Interrupt ****************/
void growlRequest() {
// millis() does not count up inside ISR but you can store the value it has
LowPulseTimer = millis();
LowPulseActive = true; // set flag true to indicate ISR was triggered
mp3K0 = analogRead(gReq0); // map(analogRead(gReq0), 0, 1023, 0, 1);
if (mp3K0 > 512) {
mp3K0 = 1;
}
else {
mp3K0 = 0;
}
mp3K1 = analogRead(gReq1); // map(analogRead(gReq1), 0, 1023, 0, 1);
if (mp3K1 > 512) {
mp3K1 = 1;
}
else {
mp3K1 = 0;
}
if ((mp3K0 == 0) && (mp3K1 == 0)) {
digitalWrite(K1, LOW);
// delay(50); // delayMillis(50UL);
}
else if ((mp3K0 == 1) && (mp3K1 == 0)) {
digitalWrite(K2, LOW);
//delay(50); // delayMillis(50UL);
}
else if ((mp3K0 == 0) && (mp3K1 == 1)) {
digitalWrite(K3, LOW);
//delay(50); // delayMillis(50UL);
}
else {
digitalWrite(K4, LOW);
//delay(50); // delayMillis(50UL);
// Serial.println("Pulsed K4");
}
}
void terminatePulseAfter50ms() {
if (LowPulseActive) { // is true if ISR was triggered
if ( TimePeriodIsOver(LowPulseTimer, 50) ) {
// if 50 milliseconds have passed by
digitalWrite(K1, HIGH); // terminate LOW-pulse through switching HIGH
digitalWrite(K2, HIGH);
digitalWrite(K3, HIGH);
digitalWrite(K4, HIGH);
LowPulseActive = false;
}
}
}
// *********************** Main Loop ******************************/
void loop() {
stepChainBlink(); // all the blinking is done through this function
terminatePulseAfter50ms();
}
best regards Stefan