One hit, one action · 09 of 10

Stop and back off

The bumper's job on a robot: on a hit, reverse for 400 ms, turn for 300 ms, and drive on. Three states and a clock, no delay(), so the bumper is read on every pass of loop() and a second hit in the middle of the escape is caught the moment it happens.

Three states and a clock

A robot that hits something has three jobs, one after the other: reverse away, turn, drive on. Each takes time, and the obvious way to write it is reverse(); delay(400); turnLeft(); delay(300);. For those 700 ms the sketch reads nothing, and a robot that reverses into a chair leg behind it keeps reversing.

So the sketch keeps a mode, DRIVE, BACK or TURN, and the time that mode began. Every pass of loop() asks three questions, in order:

  • Was there a hit? Then go to BACK, whatever the mode was.
  • Is it BACK, and has it been 400 ms? Then go to TURN.
  • Is it TURN, and has it been 300 ms? Then go to DRIVE.

Most passes, the answer to all three is no, and loop() goes round again straight away.

Stop and back off
Time
0 ms
State
DRIVE
Hits
0
DRIVE: the motors run forward and loop() reads the bumper tens of thousands of times a second. Nothing else happens until the lever closes.

Press Drive. The lever closes at the wall and the sketch changes mode on that same pass: BACK, the motors reverse, and the time is noted. Nothing waits. 400 ms later a pass finds the time is up and changes to TURN, and 300 ms after that, to DRIVE.

The hit, once

hit() is the debounce from counting hits, turned into a function that returns true once per hit: on the pass where the debounced reading becomes HIGH, and never again until the lever has been released and hit again. So a lever still held in while the robot starts reversing does not restart BACK on every pass.

Where the motors go

forward(), reverse() and turnLeft() only print. That lets you test the logic with the bumper wired and nothing else: push the lever, and the serial monitor shows reverse, turn left and forward at the right intervals. Then put your motor driver's calls in the three functions; for the DRV8833, its four states are the calls you need.

The timings are the part to tune. How far 400 ms of reversing and 300 ms of turning take a robot depends on its motors, its wheels and its battery, and only your robot can tell you.

The code

A robot's bumper logic as three states: DRIVE, BACK and TURN. A debounced hit in any state starts BACK; BACK becomes TURN after BACK_MS, and TURN becomes DRIVE after TURN_MS. The motor functions only print, so it runs with just the bumper wired.

collision_back_off.ino
/*
  Collision Sensor - stop and back off                   TK17 / /p/tk17

  Wiring. Count from the square pad on the TinkerBlock board, switch
  at the top, header at the bottom:

    GND    -> GND
    VCC    -> 5V on an Uno; 3V3 on an ESP32, ESP32-S3 or Pico
              (on a hit, SIGNAL gives your pin whatever VCC is)
    NC     -> nothing   (unconnected on the board)
    SIGNAL -> D2 on an Uno, GPIO 25 on an ESP32, GPIO 4 on an
              ESP32-S3, GP15 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.
*/

// The GPIO number SIGNAL is wired to.
// Uno: 2. ESP32: 25. ESP32-S3: 4. Pico: 15.
const int BUMPER_PIN = 4;
const unsigned long DEBOUNCE_MS = 20;
const unsigned long BACK_MS = 400;   // reverse for this long
const unsigned long TURN_MS = 300;   // then turn for this long

enum Mode { DRIVE, BACK, TURN };
Mode mode = DRIVE;
unsigned long modeSince = 0;    // millis() when this mode began

int lastReading = LOW;
int bumper = LOW;               // the debounced reading
unsigned long lastChange = 0;

// Your motor driver's calls go in these three. Here they only say
// what they would do, so the sketch runs with just the bumper wired.
void forward()  { Serial.println("forward"); }
void reverse()  { Serial.println("reverse"); }
void turnLeft() { Serial.println("turn left"); }

void enter(Mode next) {
  mode = next;
  modeSince = millis();
  if (next == DRIVE) forward();
  if (next == BACK) reverse();
  if (next == TURN) turnLeft();
}

// True once per hit: the debounced reading has just become HIGH.
bool hit() {
  int reading = digitalRead(BUMPER_PIN);
  unsigned long now = millis();
  if (reading != lastReading) {
    lastReading = reading;
    lastChange = now;
  }
  if (now - lastChange >= DEBOUNCE_MS && reading != bumper) {
    bumper = reading;
    return bumper == HIGH;
  }
  return false;
}

void setup() {
  Serial.begin(115200);
  pinMode(BUMPER_PIN, INPUT);   // the block has its own pull-down
  enter(DRIVE);
}

void loop() {
  unsigned long inMode = millis() - modeSince;

  if (hit()) {                  // in any mode: back off, now
    enter(BACK);
  } else if (mode == BACK && inMode >= BACK_MS) {
    enter(TURN);
  } else if (mode == TURN && inMode >= TURN_MS) {
    enter(DRIVE);
  }
}

modeSince is when the current state began, and millis() - modeSince is how long it has lasted: the same subtraction as the debounce, so it keeps working when millis() wraps. Put your driver's calls in forward(), reverse() and turnLeft().

When it does not work

The sketch only prints. Where do the motors go?

Into forward(), reverse() and turnLeft(). They print so the sketch runs with nothing but the bumper wired, and so you can watch the states change with a finger on the lever. Replace each print with your motor driver's calls; the DRV8833 handbook shows what those are for its four states.

It backs off, turns, and hits the same thing again.

Lengthen TURN_MS so it turns further, or make the turn direction change each time. The timings here are a starting point: how far 300 ms turns a robot depends on its motors, its wheels and its battery, and nobody can give you that number but your robot.

It reverses the instant it starts, with nothing touching it.

The sketch sees a hit at power-up, so the pin reads HIGH from the start. Look at the red LED: lit means the lever really is held in by something on the robot. Dark means the wiring: a SIGNAL wire on a supply pin, or a sketch testing for LOW.

Why not delay(400) in the hit branch? It is shorter.

It works until you add anything else. During a delay() nothing is read: not this bumper, not a second one on the other side, not a distance sensor. The state machine does the same job in the same number of lines and never goes deaf.

Where this goes next

The red LED first, then five symptoms and where to look for each.

When a hit does nothing

Edit this page — content/books/collision-sensor/stop-and-back-off.mdx

Community

Questions about this product

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

Ask a question ↗

Collision 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