Threshold an Analog Sensor

Many times when using an analog sensor you will simply want to know if the sensor has changed enough to trigger a different event or behaviour.

For example you may want to know when a room changes from bright (lights on) to dark (lights off). Or you may want to know if something is near or far away from your project.

In these cases the exact value of the reading is less important than whether the reading (and therefore either something in the environment, or your user) has crossed the imaginary line in which you are interested.

Defining such imaginary lines formally and finding out if someone or something cross those lines is called thresholding.

It is very easy to set up thresholds, but it can be difficult and verbose to explain formally in text.

I will try to keep it clear.

Thought Exercise :: PhotoCell

Let’s begin by considering the value trace from a photocell. Recall that a photocell is a sensor that responds to changes in ambient (room) light levels. We read these changes with analogInput.

pot schematic AI

With this circuit assembled, we can upload analogRead code and open the Arduino Serial Plotter to generate a trace like the one below. The blue line shows the value of analogRead()s from a photocell.

In this case, my room was initially dark. I then turned the lights on and off quickly two times in a row. And returned the room to darkness.

The first bump in the trace is the light being turned on the first time. And the second bump corresponds to the second light flash.

If we understand that pattern (shape) in the graph it is very easy, as humans, to see when the light was ON and when it was OFF. If I asked you to label the diagram you would all succeed.

Getting a computer to figure out light and dark is not intuitive but it is reasonably simple.

Basic Code

From our analogRead() Building Block we know that the following code will render a similar trace when a photocell is connected to pin A0 on our Arduino and the serial plotter is opened.

int sensorPin = A0;
int state = 0;

void setup() {
  pinMode(sensorPin, INPUT);
  Serial.begin(9600);
}
void loop() {
  state = analogRead( sensorPin );
  Serial.println(state);
  delay(50);
}
Bright or Dark?

But, how do we get the Arduino to know its bright and dark?

The red line in the traces above represent a threshold (it is set to 600). I picked it. A threshold can have any value. If your environment is stable, the threshold should be the same all the time (a constant). The correct value for a threshold depends on the context (the specific room or environment in which the sensor lives).

It is necessary to realize that a photocell in my room may give totally different set of actual numbers than a photocell in your room. But our photocell traces should give similar shapes when in light and dark. A photocell outside may give totally different numbers than a photocell in a factory beside a welding machine that is throwing sparks. A day with clouds scuttling across the sky will give different readings when the sun is hidden compared to when it is out. A photocell pointed at a night sky during a fireworks show will give totally different numbers than a photocell beside a candle. But they will all indicate fluctuations between light and dark.

So what does light and dark mean in all of these contexts?

There is a human answer and a technical answer. The human answer is: it depends. For humans it may depend on things like: are you scared of the dark? Are candles romantic when there are 3, but dangerous when there are 20?).

The technical answer is: it depends on the threshold.

In all of the above cases, we would be able to identify bumps in the plotter traces. Bright moments (high values) and dark moments (low values). These values are relative, candle bright may be dimmer than sunlight with a cloud, but in its context it is still bright. Despite all of these variations, if we pay attention to the plotter SHAPE we can set a threshold that falls between the bright moments and the dark moments of ANY context. But we have to pay attention to what is bright, what is dark, and what is in between.

Thresholding requires us to make decisions based on the relative values of the current reading compared to the threshold value. Specifically we can ask if a current reading is bigger than the threshold, or we can ask if its below the threshold.

Defining Your Threshold

Let’s take a moment to define a threshold a little more formally. A threshold is just a number — in the range of our sensor in its context — that divides our readings into two parts. The parts do not have to be equal — but it is easier if we start there.

In other words, we want a number, that we will call threshold that will divide our in context readings into two roughly equal halves so that half the sensor range is above the threshold and half is below.

We could start by saying, well analogRead() goes from 0-1023. So half way is 512 (=1023/2). That is true, but it ignores context. It would only be true if your sensor covers the whole range of analogRead. Your sensor probably does not cover this range.

Let’s go back to the trace from above.

In this trace we identified levels associated with a DARK room and a BRIGHT room. It will not always be this easy or clearcut. We can also see that our sensor covers some of the possible analogInput range (0-1023) — but not all of it.

The scale on the left tells us that DARK room values are just below 500 and the LIGHT room values are half way between 750 and 1000 — or about 875.

So we have sensor minimum of 500 and maximum of 875. Our threshold should fall between these. We could take the average, which is 687.5 == or 690. This would work fine.

In the trace I set my threshold (red line) to 600 — I guess I didn’t bother averaging. This value is a little lower than the average but well inside the peaks and valleys of my environment and the corresponding readings.

The take away here is that there is no right or wrong value for a threshold, as long as it allows you to create the experience you want.

Values, Thresholds, States

Once you have a threshold selected you can create code that will identify the state of your environment — light or dark, near of far, loud or quite.

To do this, use a simple if statement that compares the current reading to the threshold you defined. If your reading is bigger than (>=) the threshold, set STATE = 1. If your reading is smaller than (<=) the threshold set STATE = 0.

You may be surprise that we just went to all the trouble of getting an analog reading and just reduced it to a digital outcome. It is an interesting result. One worth pondering. It is a helpful tactic because sometimes analog readings simply have way more data than you need.

Code Sample :: PhotoCell

Upload the full code below.

// sensor thresholding :: photocell

int photocell = A0; // where is the circuit ?

int currentBrightness;
int lightThreshold = 0; // reset manually in code below
int LIGHT_STATE = 0;


void setup() {
  pinMode(photocell, INPUT );
  Serial.begin(9600);
}

void loop() {
  // GET LIGHT READING
  currentBrightness  = analogRead(photocell) ;
  Serial.print(currentBrightness);
  Serial.print('\t');

  // DETERMINE THRESHOLD

    // use serial plotter
    // look at your sensor max and min, pick a value between that makes sense
    // change the 512 to the value you determine

  lightThreshold = 512; // 512 is a start point -- it will be wrong
  Serial.print(lightThreshold);
  Serial.print('\t');

  // THRESHOLD COMPARE -- is light above or below threshold
  if (currentBrightness < lightThreshold) {
    LIGHT_STATE = 0;
    Serial.print("dark");
  } else {
    LIGHT_STATE = 1;
    Serial.print("light");
  }

  Serial.print('\t');
  Serial.print(LIGHT_STATE);
  Serial.println();

} // end loop

Get the threshold code on tangible github.

  • Open the serial plotter and cover and uncover your light sensor.
  • Look at the high and low reading values as you change the light falling on the sensor — use plotter and monitor to sort out the value.
  • pick a threshold between YOUR high and low

Once you have determined your threshold, change the following line in the loop() of the above code :

lightThreshold = 512;

such that the 512 is replaced with YOUR threshold for YOUR space and YOUR sensor. In the hypothetical trace below I would start around 460 and then refine if I wasn’t happy with outcomes.

  • Re-upload the code.
  • Cover and uncover your sensor while the serial plotter and monitor are open.
What to Expect

In the Plotter
If you have set your threshold successfully, you should see your light value trace crossing the threshold line as you change the light falling on the sensor.

In the Monitor
You should see four columns printed. The first is your raw light value. The second is your threshold value. The third is the ambient light state. The forth column is LIGHT_STATE value (0/1). If you have set your threshold successfully, the last two columns will say dark 0 when the sensor is covered, and light 1 when its bright.

Code Sample :: Sonar

The above concept can be applied to any analog sensor. Let’s look at one more case: sonar distance sensors. Check their building block for library and hookup details.

Upload the code below.

// File :: 3-sonarThreshold
// tangible
// steve daniels
// created Aug 2019
// updated Oct 2021

// sonar threshold example


#include <Ultrasonic.h>

//Ultra sonic sensor pin definitions
int trigpin = 12;//appoint trigger pin
int echopin = 13;//appoint echo pin

Ultrasonic ultrasonic(trigpin,echopin); // create object

int currentDistance;
int distanceThreshold = 0;
int DISTANCE_STATE = 0;


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
   // GET DISTANCE READING
   int currentDistance = averageUltraSonic(5);   // average (X) readings
   Serial.print( currentDistance );
   Serial.print('\t');

   // DETERMINE THRESHOLD

    // use serial plotter
    // look at your sensor max and min, pick amid-value that makes sense
    // change the 30 to the value you determine

   distanceThreshold = 30; // 30 is a start point -- it will be wrong
   Serial.print(distanceThreshold);
   Serial.print('\t');

  // THRESHOLD COMPARE -- is light above or below threshold
  if (currentDistance < distanceThreshold) {
    DISTANCE_STATE = 1;
    Serial.print("near"); // like grover on seasame street
  } else {
    DISTANCE_STATE = 0;
    Serial.print("far");
  }

  Serial.print('\t');
  Serial.print(DISTANCE_STATE);
  Serial.println();

}


// this function reads the distance 'howMany' times
// it then returns the average of those readings

int averageUltraSonic( int howMany ){
  long dist = 0;
  for (int i = 0; i < howMany; i++){
      dist += ultrasonic.read();
  }
  return dist = (int) dist / howMany;
}
  • Open the serial plotter and move an object towards and away from your sonar sensor.
  • Look at the high and low reading values as you move the object — use plotter and monitor to sort out the value.
  • You should use the averaging strategy.
  • pick a threshold between YOUR high and low

Once you have determined your threshold, change the following line in the loop() :

distanceThreshold = 30;

such that the 30 is replaced with YOUR threshold for YOUR space and YOUR sensor.

In this case, I set the start threshold to 30 because these readings will be much smaller.

  • Re-upload the code.
  • move towards and away from your sensor while serial plotter and monitor are open.
What to Expect

Overall, the outcome will be similar to the photocell example above. The range of numbers will be much lower.

You will likely notice that Ultrasonic sensors throw A LOT of spikes (big values) — especially when an object leaves the sensor’s field-of-view. Play with the number of readings to average. Too many will slow your code down.

Finally you may notice the code also gets slow when the sensor doesn’t see anything. This sensor is relying on the time until an echo returns to determine distance. It waits for a long time when distances get large.

The ambitious among you can minimize this delay using the sensor timeout. See the intro to sonar building block (last section of that post) or check the example files that came with the library for details.