Monday, February 3, 2025

The Problem With delay()

     An issue that comes up over and over on Arduino forums supporting Arduinists trying to advance from beginner projects to building their own projects from what they have learned doing tutorials is the problem with delay(). The delay() function is introduced in blink, which is often the first lesson in beginner tutorials.  While the first lesson should be kept simple to keep from immediately confusing the audience, discussion of limitations of delay() are skipped for the same reason. This can lead to an intermediate micro-controller hobbyist attempting to perform some timing function in their project using delay, without ever digesting blink without delay (see also Multitasking the Arduino), leading to difficulties.

    The delay() function might as well be called do_nothing(), as when the controller reaches this instruction, it takes no further action until the specified time has elapsed. The behavior is referred to as 'blocking,' and is at the heart of the problem with delay(). The problem with delay() is that blocking behavior prevents code from being able to perform multiple tasks concurrently. 

 The following sketch illustrates the problem with delay in multiple ways.

/*
This sketch is original work by the author of westwoodtoys.blogspot.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
int leds[]={3,5,6,9,10,11};
int wait_time;
int speakerPin = 4;

void setup()
{
for(int i=0;i<6;i++)
{
pinMode(leds[i],OUTPUT);
}
pinMode(A0,INPUT);
pinMode(2,INPUT);
pinMode(speakerPin,OUTPUT);
}

void play_sounds()
{
for (int n=554; n>522; n--)
{
tone(speakerPin, n);
delay(3);
}
tone(speakerPin, 523);
delay(500);
noTone(speakerPin);
delay(500);
for (int n=554; n>522; n--)
{
tone(speakerPin, n);
delay(3);
}
tone(speakerPin, 523);
delay(1500);
noTone(speakerPin);
}

void loop()
{
for(int i=0;i<6;i++)
{
wait_time=5*analogRead(A0);
digitalWrite(leds[i],HIGH);
delay(wait_time);
digitalWrite(leds[i],LOW);
if(digitalRead(2))
play_sounds();
}
}

Here is a Tinkercad simulation.

The sketch takes input from a potentiometer wiper and button.  The potentiometer wiper position is used to set the time between switching from one LED illuminated to the next. The button causes the Arduino to play a tone on the buzzer that sounds a bit like a fire truck horn.  The problem with delay() is illustrated in that: 

  1. While the horn sound plays, the change in LEDs is paused.
  2. If the potentiometer is moved while one LED is illuminated, the change in duration for the LEDs to be illuminated is not updated until the call to delay() after illuminating the present LED is completed. That is, it would be nice if when the potentiometer is turned to a high resistance, a change in which LED is illuminated could be triggered immedately upon turning the knob to a position matching the current duration the LED has been illuminated rather than having to wait until the duration commanded at time of illumination is completed. 
  3. The button only triggers the horn sound in the period between one LED turning off and the next illuminating.
Of these problems, only #1 is much of a problem while the duration the LEDs are illuminated is small, but #s 2 &3 become more pronounced when the potentiometer wiper commands a longer delay() period. 

The following sketch dodges the problem with delay() by using the lessons learned from 'blink without delay' and Multitasking the Arduino.

/*
This sketch is original work by the author of westwoodtoys.blogspot.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
int leds[]={3,5,6,9,10,11};
int wait_time = 5*1028;
int speakerPin = 4;

//timers added to replace delay function calls
unsigned long this_loop_time;
unsigned long leds_timer;
unsigned long buzzer_timer;

//New variable to make button into a state machine
bool button_pressed = 0;

//New variables to make the buzzer into a state machine
//(keep track of what stage of the sound effect is needed)
int buzzer_counter = 0;
//(keep track of the tone, this replaces 'n' in the for-loop
//in the play_sounds() function the version using delay())
int buzzer_tone=554;
//make array of buzzer durations
int buzzer_duration[]={3,500,500,3,1500};

//new variable to keep track of LED to illuminate
//(this is replacing the for-loop increment 'i')
int leds_counter = 0;

void setup()
{
for(int i=0;i<6;i++)
{
pinMode(leds[i],OUTPUT);
}
pinMode(A0,INPUT);
pinMode(2,INPUT);
pinMode(speakerPin,OUTPUT);
//set the timer so that the first LED lights immediately on startup
leds_timer = millis()-wait_time;
}

void loop()
{
this_loop_time=millis();
wait_time=5*analogRead(A0);
if(this_loop_time-leds_timer>wait_time)
{
//this implementation of digitalWrite() is a little tricky:
//The modulo operator acts like if/else to turn LEDs on
//or off, using not to get the opposite value.
//The counter goes to 12, twice the number of LEDs
//and integer division is leveraged to write to the correct
//LED for even and subsequent odd counter values
digitalWrite(leds[leds_counter/2],!(leds_counter%2));
if(leds_counter%2==0)
//only resetting the timer when lights go on, otherwise
//would have equal duration of off time
leds_timer=this_loop_time;
//increment and reset counter when >=12
leds_counter=(leds_counter+1)%12;
}
if(digitalRead(2)&&button_pressed==0)
{
//the buttton_pressed state variable serves to debounce
//and keep from registering multiple button presses,
//which would interrupt the sound playing
button_pressed=1;
buzzer_timer=this_loop_time;
}
//coding the truck horn sound requires a timer, a counter
//and an array of tone durations. Writing this code is
//quite a bit more involved than the sequential approach
//used in the version using delay()
if(button_pressed)
{
switch(buzzer_counter)
{
case 0:
{
tone(speakerPin, buzzer_tone);
if(this_loop_time-buzzer_timer>buzzer_duration[buzzer_counter])
{
buzzer_tone--;
buzzer_timer=this_loop_time;
}
if(buzzer_tone==523)
buzzer_counter++;
break;
}
case 1:
{
tone(speakerPin, buzzer_tone);
if(this_loop_time-buzzer_timer>buzzer_duration[buzzer_counter])
{
buzzer_counter++;
buzzer_timer=this_loop_time;
}
break;
}
case 2:
{
noTone(speakerPin);
buzzer_tone=554;
if(this_loop_time-buzzer_timer>buzzer_duration[buzzer_counter])
{
buzzer_counter++;
buzzer_timer=this_loop_time;
}
break;
}
case 3:
{
tone(speakerPin, buzzer_tone);
if(this_loop_time-buzzer_timer>buzzer_duration[buzzer_counter])
{
buzzer_tone--;
buzzer_timer=this_loop_time;
}
if(buzzer_tone==523)
buzzer_counter++;
break;
}
case 4:
{
tone(speakerPin, buzzer_tone);
if(this_loop_time-buzzer_timer>buzzer_duration[buzzer_counter])
{
buzzer_counter++;
noTone(speakerPin);
}
break;
}
default:
{
buzzer_counter=0;
buzzer_tone=554;
button_pressed=0;
}
}
}
}

Here is a Tinkercad simulation of this one.

It is probably immediately obvious that this sketch is much longer than the one using delay.  Unfortunately, dodging the problem with delay() is done at cost of increased complexity.  However, when running this sketch, one would see that the three problems are now entirely resolved.  That is:

  1. LED illumination continues to change while playing the horn sound
  2. Changes in potentiometer wiper position are updated immediately
  3. The button will trigger the horn sound upon being depressed, irrespective of the state of the LEDs.
There are a few things to recognize in the difference between the two sketches.  For one, the function controlling the horn sound has been moved into the main loop. This isn't strictly necessary, but makes for easier coding. For another thing, the timing of the LEDs changing does appear to be affected somewhat by the playing of the horn sound.  This is likely because of the increase in loop time when processing the horn sound.  When not playing, a big 'if' condition is bypassed. Finally, if a reader is not familiar with state machines, then surely this code will be hard to parse.

The neophyte will find use of the modulo operator valuable in making state machines work, as demonstrated repeatedly in the code above. If the function of state machines is tricky for the reader, a suggestion for an example project to get a better understanding is to add a counter to 'blink without delay' similar to the use of leds_counter in the sketch above.  It could be used to make the LED blink a variable number of times between a single longer off interval. I call this "for looping without for looping", and this is a fairly crucial concept to understand when moving beyond beginner level micro-controller projects.  

Getting ESP-32 CAM with USB-C SD card to work

     Make this part 2 of a series, I suppose.  The high quality documentation of this device on the seller website marks 4 pins for use in the 'TF' column. TF stands for Trans-Flash, and apparently is a lesser used term for SD card. The savvy SD card user may note that no function is denoted for any of the 4 pins in the 'TF' column.


    A little searching finds that SPI pins can be reassigned.  The following snippet is from the SD card test example:

...

/* Uncomment and setup pins you want to use for the SPI communication #define REASSIGN_PINS int sck = -1; int miso = -1; int mosi = -1; int cs = -1; */

...

#ifdef REASSIGN_PINS SPI.begin(sck, miso, mosi, cs); if (!SD.begin(cs)) { #else if (!SD.begin()) {

    Sometimes brute force is the easiest way, and an approach can be made when realizing that there are only 4! = 24 possible combinations, then trying them one at a time. 

    Surely a programmatic approach could attempt to mount the card with each combination; this exercise is left to the reader.  I preferred just hand jamming one combination, compiling, checking if the card mount failed, and then moving to the next combination. But, failure upon trying the last combination brought me back to the seller provided documentation. The zip file provided by the seller contained a very slightly different pin mapping.

    While still leaving a little ambiguity, I guessed 'CLK' denotes clock (sck) and 'CMD' chip select (cs), just leaving the meanings of 'DAT0' and 'DAT3' to be identified.  I will stop holding dear readers in suspense and just reveal the working pin assignment.  

...
#define REASSIGN_PINS
int sck = 4;
int miso = 13;
int mosi = 21;
int cs = 19;
...
(Looks like it took only 4 tries into hand jamming before I got mixed up, skipping over the working pin mapping, whoops!)

***An update after first publication, it seems that the assumption that CMD maps to cs is NOT correct in the wider world of SD card SPI pin mappings.  Regardless, the pinout reported above does succeed in mounting the SD card and performing the read/write operations of the SD_test example, so maybe the documentation is wrong and I got lucky?

Sunday, February 2, 2025

Getting ESP32 CAM with USB-C Camera to work

    I picked up a different version of ESP32-CAM off Aliexpress.  It purports to support OV-5640 AF, a better camera than the typical ESP32-CAM, and supporting autofocus feature.


    The board has support level about as you would expect from Aliexpress sellers: there are some figures on the product write-up, the seller did send a couple zip files when asked for documentation, and the provided Arduino sketches do not work as written.  The camera fails to initialized, as the pin mapping is not correct.

    Thankfully, the provided documentation does include a schematic for the camera module, and a pin mapping between the camera module and the ESP32.




    Probably the most perplexing move was to provide a figure showing which of the pre-defined cameras to select in the CameraWebServer example sketch, then another image with the pins definitions of that camera overwritten. Thankfully the pin mappings overwritten in the figure are the correct mappings per the previous two figures, but the thought process there is a bit hard to grasp.

The left is the pin definitions in camera_pins.h that comes with the CameraWebServer example. The right is the figure on the seller web page.

    I went ahead and made a new camera definition instead, rather than overwriting the existing definition.  The seller description figures also had some input on how to configure the board.  These do seem necessary, as compile was crapping out without these changes.  


    Finally I tried uploading and met with success.  With corrected pin mapping this board works like any other ESP32 CAM board running the CameraWebServer example.  I guess autofocus works, I don't really remember having a problem with that with the older boards, but surely it is nice to have.

    Will this be the board to make ESP32-CAM a useful product?  Hard to say; it still seems you get what you pay for, and paying a little more gives a little improvement.