optical sensor/Using it/08. A counter that learns
Using it · 08 of 9

A counter that learns

A counter for marks going past: black tape on a belt, a slot in a wheel, a hand waved over the block. At start it learns the background and the mark, then counts a mark once the reading has gone 60 % of the way between them and re-arms only when it comes back past 40 %. Two lines, not one, because an analog reading wobbles at every edge.

Learn the two readings

A threshold typed into a sketch is right for one surface, one height and one room. So this sketch measures instead. When it starts it asks you to hold the sensor over the background for three seconds, averages fifty readings, then asks for the mark and does the same. From then on it only asks one question of each reading: how far has it gone, from the background towards the mark? 0 is the background, 1 is the mark.

Mount the block face down, the domes 2 to 5 mm over whatever passes. Hold that height while it learns.

One mark, one count

One mark, one count
The sketch
Marks
0
Counted
0
Lines
1, at 50 %
One dark mark on a pale belt slides under the sensor. Press Play to watch the readings as the sketch sees them.

Press Play with each sketch. As the edge of a mark crosses the sensor, part of the receiver's view is dark and part is pale, and the reading wobbles on its way from one to the other. A single threshold in the middle catches every wobble that crosses it. Two lines do not: the sketch counts once the reading passes 60 %, and will not count again until it has fallen back below 40 %. The wobble at an edge never spans both. That gap between the lines is hysteresis. The trace is drawn to show the shape, not measured.

How the sketch does it

learn() prints its prompt, waits three seconds, and averages fifty readings. progress() turns a reading into the fraction of the way from background to mark, and because it divides by mark - background, it does not care which one is higher. loop() holds one flag, onMark: it counts and sets it on passing ENTER, and clears it on passing LEAVE. There is no delay() in loop(), so it reads as fast as the ADC allows.

If the two learnt readings are within 40 counts of each other, the sketch says so: there is not enough contrast to count on.

What you should see

At 115200: Hold it over the background ..., then Hold it over the mark to count ..., then the two readings it learnt. Each mark that passes after that prints count 1, count 2, and so on, once each however slowly it moves.

How fast it can count

The receiver can switch far faster than anything you will wave past it. The limit is on the board: when a mark leaves, SIGNAL climbs back through the 4.7 kΩ into the 100 nF, a time constant of 0.47 ms. It is 90 % of the way back after about 1.1 ms. A gap narrower than that, at the speed it passes, never gets the reading back to the line. For a disc with slots, that is still hundreds of slots a second.

The code

optical_counter.ino

Learns two readings at start, three seconds each: the background, then the mark. Each loop takes the mean of eight readings and works out how far it has gone from one to the other; it counts on passing ENTER and re-arms on falling back past LEAVE. No delay() in loop().

/*
  Reflective Optical Sensor - a counter                 TK57 / /p/tk57

  Wiring. Count from the square pad on the TinkerBlock board, parts
  up, header at the bottom:

    GND    -> GND
    VCC    -> 3V3 on an ESP32, ESP32-S3 or Pico; 5V on an Uno.
              SIGNAL rises to VCC over a dark surface, so VCC
              is the voltage your board's pins run at.
    NC     -> nothing   (unconnected on the board)
    SIGNAL -> GPIO 4 on an ESP32-S3, GPIO 34 on an ESP32,
              A0 on an Uno, GP26 on a Raspberry Pi Pico

  Arduino IDE
    Tools > Board                 your board, e.g. ESP32S3 Dev Module
    Tools > Port                  the one that appears when you plug in
    Tools > USB CDC On Boot       Enabled   (ESP32-S3 only)
    No library needed.

  Mount the block face down, 2 to 5 mm over the surface. It learns
  two readings at start: the background, then the mark to count.
*/

// The analog pin SIGNAL is wired to.
// Uno: A0. ESP32: 34. ESP32-S3: 4. Pico: 26.
const int SENSOR_PIN = 4;

// Two lines, not one: a mark counts when the reading passes
// ENTER of the way from background to mark, and the counter
// re-arms only when it falls back past LEAVE.
const float ENTER = 0.6;
const float LEAVE = 0.4;

int background;
int mark;
bool onMark = false;
unsigned long count = 0;

int readAverage() {
  long sum = 0;
  for (int i = 0; i < 8; i++) sum += analogRead(SENSOR_PIN);
  return sum / 8;
}

int learn(const char *what) {
  Serial.print("Hold it over ");
  Serial.print(what);
  Serial.println(" ...");
  delay(3000);
  long sum = 0;
  for (int i = 0; i < 50; i++) {
    sum += readAverage();
    delay(10);
  }
  return sum / 50;
}

// How far a reading has gone from background towards mark.
float progress(int level) {
  return float(level - background) / float(mark - background);
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  background = learn("the background");
  mark = learn("the mark to count");
  Serial.print("background ");
  Serial.print(background);
  Serial.print(", mark ");
  Serial.println(mark);
  if (abs(mark - background) < 40) {
    Serial.println("Too alike to tell apart: press reset.");
  }
}

void loop() {
  float p = progress(readAverage());

  if (!onMark && p >= ENTER) {
    onMark = true;
    count++;
    Serial.print("count ");
    Serial.println(count);
  } else if (onMark && p <= LEAVE) {
    onMark = false;   // back on the background: re-armed
  }
}

It works whichever way round the contrast is, a dark mark on a pale belt or a pale one on a dark belt, because it measures the way from background to mark. The sketch compiles for an ESP32-S3 and an Uno.

View on GitHub · blocks/tk57-reflective-optical-sensor/arduino/optical_counter/optical_counter.ino @ v1.5

When it does not work

It counts two or three for one mark.

The reading wobbled across the line as the mark's edge passed. Keep the two lines apart: ENTER at 0.6 and LEAVE at 0.4 of the way from background to mark. If it still double-counts, move them further apart, to 0.7 and 0.3.

It says the two readings are too alike.

The background and the mark look the same to the sensor in infrared. Bring it closer, 2 to 5 mm, and use matte black tape or a cut-out slot rather than printed ink. Then press reset and let it learn again.

Fast marks are missed.

SIGNAL climbs back through a 4.7 kΩ into 100 nF, a time constant of 0.47 ms: it needs about a millisecond to get most of the way. A gap that passes the sensor faster than that never reaches the line. Slow the belt, or widen the gaps.

The count drifts when the lights change.

The learnt readings include the room's infrared. A lamp switched on, or the sun moving, shifts both. Shade the sensor and run the calibration where the counter will work.

Where this goes next

Always at the top, always at the bottom, or wandering: where to look.

When the reading is wrong →

Edit this page — content/books/reflective-optical-sensor/a-counter-that-learns.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

Reflective Optical Sensor

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum →