Ultrasonic Ranging Module and 4-digit LED odd behavior

Hi,

I've completed a range sensor project, and have noticed an odd behavior I can't fix. When the sensor is active and taking readings, the farther things are from it, the more volatile the 4-digit seven-segment display is. I mean, at close ranges, the display looks fine, solid on. As the distances measured get longer, the lcd starts to flicker until it starts to look like it is having a very slow refresh rate.

I'd appreciate any guidance- I'm new to the arduino hardware and programming. As of now, I'm assuming this is the best I can do with the parts I have.

How it works:

  1. Turn a potentiometer to adjust the range at which anything closer will trigger an alarm.
  2. See the distance selected on a 4-digit seven-segment display.
  3. press a button to arm the sensor and begin readings.
  4. when button is pressed, an LED turns on, and the sensor begins taking measurements.
  5. when an object in front of the sense moves to a range closer than that which was selected, a buzzer sounds.

Parts List:

  1. Arduino (Elegoo) Mega2560
  2. Ultrasonic Ranging Module HC-SR04
  3. 4-digit seven-segment display (5641-AS)
  4. 5 220 ohm resisters
  5. 1 red led
    6 Potentiometer (see attached)
  6. Small button
  7. Active Buzzer (see attached)

Code:

#include <SevSeg.h>
SevSeg sevseg;  //Instantiate a seven segment object

#define btnpin 3    //button
#define ledpin 10   //led
#define buzzpin 9   //buzzer
#define pingPin 11  //range finder
#define echoPin 12  //range finder
#define potpin A0   //pin used to read the potentiometer

// Declare variables
int ledState = LOW;         // Current led state
int lastButtonState;        // The previous state of the button
int currentButtonState;     // The current state of the button
bool systemActive = false;  // Flag to track system activation
const int startDist = 10;

//DECLARE VARIBLES FOR THE MILLI TIMER
unsigned long previousMillis = 0;  // will store last time LED was updated
// constants won't change:
const long interval = 0;   // interval at which to REFRESH THE 7-SEG DISPLAY (milliseconds)
const long interval2 = 0;  // interval at which the measurement is taken

void setup() {

  //SEVSEG SETUP VARIABLES
  byte numDigits = 4;
  byte digitPins[] = { 32, 29, 28, 26 };
  byte segmentPins[] = { 31, 27, 24, 22, 34, 36, 25 };
  bool resistorsOnSegments = false;      // 'false' means resistors are on digit pins
  byte hardwareConfig = COMMON_CATHODE;  // See README.md for options
  bool updateWithDelays = false;         // Default 'false' is Recommended
  bool leadingZeros = false;             // Use 'true' if you'd like to keep the leading zeros
  bool disableDecPoint = true;           // Use 'true' if your decimal point doesn't exist or isn't connected. Then, you only need to specify 7 segmentPins[]
  sevseg.begin(hardwareConfig, numDigits, digitPins, segmentPins, resistorsOnSegments,
               updateWithDelays, leadingZeros, disableDecPoint);
  sevseg.setBrightness(1);
  //END SEVSEG SETUP


  pinMode(btnpin, INPUT_PULLUP);
  pinMode(ledpin, OUTPUT);
  pinMode(buzzpin, OUTPUT);
  Serial.begin(9600);
  // Initialization of currentButtonState and lastButtonState
  currentButtonState = digitalRead(btnpin);
  lastButtonState = currentButtonState;
}

void loop() {

  // Update currentButtonState - MOVED FROM SETUP
  currentButtonState = digitalRead(btnpin);  //TAKES ACTUAL PIN READING (HIGH OR LOW) AND SAVES VALUE TO THE VARIABLE, 'CURRENTBUTTONSTATE'.
  int potval = analogRead(potpin);           //identifies a variable to hold the voltage value of the potentiometer
  int potinch = map(potval, 0, 1023, 1, 72);
  // Serial.print(potinch);  //PRINTS THE NUMBER OF INCHES SET VIA THE POTENTIOMETER.
  // Serial.println(" USER SET inches");

  /* check to see if it's time to REFRESH THE 7-SEG; that is, if the difference
  between the current time and last time you blinked the LED is bigger than
  the interval at which you want to blink the LED.
  */
  unsigned long currentMillis = millis();



  //code for timing the writing to the display
  if (currentMillis - previousMillis >= interval) {  //ALLOWS A TIMER TO CONTROL FREQUENCY OF DISPLAY BEING UPDATED.
    // save the last time you blinked the LED
    previousMillis = currentMillis;

    //PRINTS 'POTINCH' TO THE 7 SEG DISPLAY
    sevseg.setNumber(potinch, 2);  //prints the 'POTINCH' VARIABLE TO THE 7-SEGMENT DISPLAY
  }
  sevseg.refreshDisplay();  //REFRESHES THE DISPLAY


  // Toggle system activation upon button press
  if (lastButtonState == HIGH && currentButtonState == LOW) {
    systemActive = !systemActive;

    // Control RED LED based on system activation
    if (systemActive) {
      digitalWrite(ledpin, HIGH);  // Turn on the LED when system is activated
    } else {
      digitalWrite(ledpin, LOW);   // Turn off the LED when system is deactivated
      digitalWrite(buzzpin, LOW);  // Turn off the buzzer when system is deactivated
    }
  }

  lastButtonState = currentButtonState;  // Update the last button state

  if (systemActive) {
    unsigned long distanceMeasurementStartTime = millis();

    long duration, inches, cm;
    // The PING))) is triggered by a HIGH pulse of 2 or more microseconds.
    // Give a short LOW pulse beforehand to ensure a clean HIGH pulse:
    pinMode(pingPin, OUTPUT);
    digitalWrite(pingPin, LOW);
    delayMicroseconds(2);
    digitalWrite(pingPin, HIGH);
    delayMicroseconds(5);
    digitalWrite(pingPin, LOW);

    pinMode(echoPin, INPUT);  //THIS SETS UP THE PIN THAT SENDS TIMING DATA TO THE ARDUINO.
    duration = pulseIn(echoPin, HIGH);

    // convert the time into a distance -these call to the functions at the bottom of this sketch.
    inches = microsecondsToInches(duration);
    cm = microsecondsToCentimeters(duration);

    // Calculate time taken for distance measurement
    unsigned long distanceMeasurementDuration = millis() - distanceMeasurementStartTime;

    // Subtract time taken for distance measurement from interval
    if (distanceMeasurementDuration < interval2) {
      delay(interval2 - distanceMeasurementDuration);
    }

    // Code to activate buzzer based on distance
    if (inches < potinch) {         //compares measured distance against the desired triggering distance.
      digitalWrite(buzzpin, HIGH);  // Activate buzzer if distance is less than 10ft
    } else {
      digitalWrite(buzzpin, LOW);  // Deactivate buzzer if distance is greater than 10ft
    }
  }
}

//FUNCTIONS BELOW

long microsecondsToInches(long microseconds) {
  // According to Parallax's datasheet for the PING))), there are 73.746
  // microseconds per inch (i.e. sound travels at 1130 feet per second).
  // This gives the distance travelled by the ping, outbound and return,
  // so we divide by 2 to get the distance of the obstacle.
  // See: https://www.parallax.com/package/ping-ultrasonic-distance-sensor-downloads/
  return microseconds / 74 / 2;
}

long microsecondsToCentimeters(long microseconds) {
  // The speed of sound is 340 m/s or 29 microseconds per centimeter.
  // The ping travels out and back, so to find the distance of the object we
  // take half of the distance travelled.
  return microseconds / 29 / 2;
}

Lesson 5 Button.pdf (130.5 KB)
Lesson 6 Active buzzer.pdf (125.2 KB)

Look at how You use this variable in the code. It is 0 and creates no window at all.
Not getting all down in the code the feeling is that the responce time for the ultra sonic sets the loop around.

pulseIn() is a blocking function - while your program is in that function waiting for the pulse echo it can not refresh the delay.

First of all you should add a timeout to pulseIn(), some 20,000 µs is enough. That will improve a lot.

Next you can set a sensible interval for distance measurements, if you do it super frequently you may get wrong measurements due to echoes of your previous measurement.

Finally to get around the blocking of pulseIn() you can use a timer interrupt to call sevseg.refreshDisplay() at a reasonable interval, something like 500 Hz is probably enough. You do run the risk of a slight measurement error if the return of pulseIn() is delayed when the display update happens to be at the time the echo comes in.

Perhaps that is because pulseln() is taking longer and longer to find the echo. Perhaps you could try to refresh the display ONLY when the data changes.

1 Like

Thanks for the suggestion. I tried 10, 100, 1000, and higher with no affect ms in there, and there was no affect. since i found that to be the case, I deleted the lines relating to the millis timer.

The only thing that changes how good the display looks when the system is armed is how close an object is to the sensor.

Hi - thanks for the guidance. I adjusted the timeout for the pulsein. makes the display a little more stable, but not perfect.

can you tell me more about what you mean by an 'interval'? I assume you mean to only take measurements periodically. if that's right, can you give me a hint on how to do that? would it be by using millis() or a delay()?

Use the millis() function, like in the Blink without Delay example (it's in the IDE), otherwise you're still blocking.

Something like this:

  if (millis() - lastUpdate > interval) {
    lastUpdate = millis();

    // Measure distance
    digitalWrite(pingPin, HIGH);
    delayMicroseconds(2);
    digitalWrite(pingPin, LOW);
    duration = pulseIn(echoPin, HIGH, 20000);
  }

Reading the segsev code I don't see an easy solution. It relies on being called very frequently; you can do that using a timer interrupt, calling the refreshDisplay() function every 50 us or so.

Even not pinging the sensor every single time you run loop will be a big improvement, as you get to update the display so much more frequently. Probably 5-10 times a second is more than enough, that is instant response on the display from a typical slow human perspective.

I started with some code for a non-blocking read of an ultrasonic using Timer1 input capture. It's not only non-blocking but also greatly improves accuracy over timing with micros or pulseIn. I never got around to buttoning it up into a proper library. You'll have to make sure the pins work for whatever board you're on. There are comments for what to use on UNO and on a ATMega-1284P. It would work on probably any AVR if you fix the pins and ports.

Another option to avoid the flickering problem is to use a separate chip/module to drive the display such as max7219 or HT16K33.

Of course, driver chips. They do make life easy.

I like the cheap Chinese-made TM1637, or for bigger displays its bigger brother TM1627. Lots of ready-made 4-digit display modules with that chip on board and Arduino libraries are available.

The same chip can also be used to read button matrices.

Nice approach. Main downside is that there's only one pin that has this function.
Maybe it's worth generalising it by using pin change interrupts? Then all pins on an Uno/Nano and most pins on a Mega can be used in this manner. Timing using either micros() or for higher accuracy a TCNT1 count.

True it's only good for one sensor. But it gives one sensor some real advantages. And if you only have one...

Then it would have to count micros and it would lose resolution. I'm using input capture and counting with the timer directly. I'm counting with Timer1 running at full throttle so I have 62.5nS resolution on my echo times. You can't do that with a PCI and micros.

Plus, the timer stops on the echo, so even if I read it late I still get the right result. If a PCI hits and gets delayed then I get the time when I actually enter the handler and read micros, not the actual instant that the echo happened.

Now the UNO-R4 has a GPT timer behind every digital pin and each one of them can do multiple input capture events. So it would be easier to extend the concept there.

I greatly appreciate all the guidance I've gotten and will look into each bit. I've realized that i'm sorley not knowledgeable about the millis() function. I found a great youtube series about that I'm watching now, that really dumbs it down well for a noob like me.

here's the link if anyone else needs it: https://www.youtube.com/watch?v=qn8SP93L3iQ&t=1s

That that sounds like an add for the vid, but i have nothing to do with that content creator, and I receive no money from the millis() function for sending people to that video. :slight_smile:

Count timer ticks instead. Read (or reset) TCNT1 when sending the ping; read it again when the interrupt is called.

Interrupts get normally delayed by something like 2-3 clock ticks (unless another interrupt happens to run), that's some 150 ns. Not as good as your timer capture method but still pretty darn good.

micros() has a 4µ resolution; that's 1.32 mm travelled by sound, so up to 0.66 mm error in the distance. Good enough for most general cases.

That 150 ns is about 50 µm of travel for sound, or 25µm error in distance. For that to be significant, the sensor and the object it senses should not be vibrating too much or it'd be more.

Maybe I should really try and give pin change interrupts a go. I've always seriously disliked the blocking pulseIn() function. It does mean the sensor effectively consumes the timer (messing up the PWM outputs), as I'd add a timeout as well in the form of a timer overflow interrupt.

If I needed more than one sensor.

Do the same.

Just use TCNT1 values instead of micros() values. That way you can even ping multiple at the same time, wait for echos to come back. Though that obviously gives issues with cross talk between sensors and with PC interrupts figure out which one triggered it.

Or: enable the PC interrupt for one sensor, ping that sensor, set TCNT1=0, and record TCNT1 when the interrupt happens. Wait a bit for echoes to die out and repeat with another sensor.

If I need more than one sensor.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.