Smart blind stick on an Arduino Uno
An HC-SR04 sets a buzzer and a vibration motor pulsing faster as an obstacle gets closer, and a water sensor at the tip chirps for a puddle. The whole build, an Arduino Uno and 7 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 35 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $35. On Mokxi they're free.
What you'll learn
- Measuring distance with an ultrasonic echo
- Mapping distance to a pulse rate with
map - Switching a motor with a transistor and a flyback diode
- Timing pulses with
millisinstead ofdelay - Giving two different dangers two different sounds
- Difficulty: Intermediate
- Time: About 35 minutes
- Board: Arduino Uno R3
- Parts: 9
How it works
An HC-SR04 measures the distance ahead every 70 ms. Past 100 cm the stick stays quiet. Between 100 cm and 20 cm the buzzer and vibration motor pulse together, faster as the obstacle gets closer. Under 20 cm they stay on.
A transistor switches the motor, because it needs more current than a pin can give. The diode across the motor absorbs the voltage spike when the motor stops.
The rain sensor at the tip has its own alarm. When it reads wet, the buzzer plays three short chirps and a pause. The motor keeps showing the obstacle, so water never hides a wall.
Parts list
9 parts, plus the jumper wires. They retail for about $35; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Transistor (NPN or PNP)In LED Cube Kit.
- 1 × Resistor, 1k ΩIn Starter Kit, Claw Machine Kit, Halloween Prop Kit.
- 1 × DiodeIn Pinball Table Kit.
- 1 × Vibration motor
- 1 × BuzzerIn Starter Kit, Rocket Flight Computer Kit.
- 1 × Ultrasonic rangerIn Halloween Prop Kit.
- 1 × Rain sensor
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Ultrasonic ranger pin ECHO | Arduino Uno R3 pin 10 |
| Ultrasonic ranger pin TRIG | Arduino Uno R3 pin 9 |
| Buzzer pin 1 | Arduino Uno R3 pin 8 |
| Rain sensor pin DO | Arduino Uno R3 pin 7 |
| Resistor, 1k Ω pin 1 | Arduino Uno R3 pin 6 |
| Diode pin C | Arduino Uno R3 pin 3V3 |
| Vibration motor pin 1 | Arduino Uno R3 pin 3V3 |
| Ultrasonic ranger pin VCC | Arduino Uno R3 pin 5V |
| Rain sensor pin VCC | Arduino Uno R3 pin 5V |
| Transistor (NPN or PNP) pin E | Arduino Uno R3 pin GND |
| Buzzer pin 2 | Arduino Uno R3 pin GND |
| Ultrasonic ranger pin GND | Arduino Uno R3 pin GND |
| Rain sensor pin GND | Arduino Uno R3 pin GND |
| Rain sensor pin AO | Arduino Uno R3 pin A0 |
| Resistor, 1k Ω pin 2 | Transistor (NPN or PNP) pin B |
| Diode pin A | Transistor (NPN or PNP) pin C |
| Vibration motor pin 2 | Transistor (NPN or PNP) pin C |
The note on the canvas
Word for word, as it sits beside the circuit in the editor.
Try it
1. Press Run. Nothing sounds: the HC-SR04 sees nothing within 100 cm.
2. Drag its slider to 800 mm. The buzzer and motor pulse about every 0.8 s.
3. Bring it in to 300 mm: the pulses come about five times a second.
4. Under 200 mm they stay on. That means stop.
5. Drag the rain plate's slider up to wet it: three short chirps and a pause mean water ahead.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Smart blind stick: an HC-SR04 on the stick looks ahead, and a buzzer and a
// vibration motor in the handle pulse faster the closer the obstacle is. A
// water sensor at the tip gives its own alarm for a puddle or a wet floor.
//
// HC-SR04 TRIG pin 9, ECHO pin 10, VCC 5V, GND
// active buzzer + on pin 8, - to GND
// vibration motor from 3.3 V to a 2N2222's collector; the base from pin 6
// through 1k, the emitter to GND, a 1N4007 across the motor
// water sensor DO on pin 7 (LOW when wet), AO on A0, VCC 5V, GND
//
// THE OBSTACLE: the stick measures every 70 ms, a little over the HC-SR04's
// 60 ms cycle: trigger it sooner and the module ignores the trigger, so every
// other reading comes back empty. Past 100 cm there is nothing
// to say, so it is quiet. From 100 cm down to 20 cm the buzzer and the motor
// pulse together, once a second at 100 cm and ten times a second at 20 cm,
// in a straight line between. Under 20 cm both stay on: stop.
//
// distance 100 cm 80 cm 60 cm 40 cm 20 cm under 20 cm
// one pulse 1000 ms 775 ms 550 ms 325 ms 100 ms continuous
//
// THE WATER: when the plate at the tip gets wet the buzzer switches to three
// short chirps and a pause, a sound the obstacle never makes. The motor keeps
// telling the obstacle, so a wet floor never hides a wall.
//
// The motor needs about 75 mA, more than a pin can give, so a transistor
// switches it and the diode takes the kick when it stops. The active buzzer
// is a small enough load for the pin. Things to change: FAR_CM and NEAR_CM
// for the range, and PULSE_MS for the length of each pulse.
const int TRIG = 9;
const int ECHO = 10;
const int BUZZER = 8;
const int MOTOR = 6;
const int WET = 7;
const int WATER_LEVEL = A0;
const int FAR_CM = 100; // quiet past this
const int NEAR_CM = 20; // continuous under this
const int PULSE_MS = 50; // how long each pulse is on
long distanceCm = 999;
unsigned long lastMeasure = 0;
unsigned long lastPulse = 0;
unsigned long chirpStart = 0;
int zone = -1; // 0 clear, 1 pulsing, 2 very close: printed when it changes
bool wasWet = false;
// One reading in centimeters, or 999 when no echo comes back in 25 ms.
long measure() {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long us = pulseIn(ECHO, HIGH, 25000);
if (us == 0) return 999;
return us / 58;
}
// The time between pulses for a distance in the pulsing band.
unsigned long intervalFor(long cm) {
return map(cm, NEAR_CM, FAR_CM, 100, 1000);
}
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
pinMode(BUZZER, OUTPUT);
pinMode(MOTOR, OUTPUT);
pinMode(WET, INPUT);
Serial.begin(9600);
Serial.println("Smart blind stick ready");
}
void loop() {
unsigned long now = millis();
if (now - lastMeasure >= 70) {
lastMeasure = now;
distanceCm = measure();
int z = distanceCm > FAR_CM ? 0 : distanceCm >= NEAR_CM ? 1 : 2;
if (z != zone) {
zone = z;
if (z == 0) {
Serial.println("path clear");
} else if (z == 1) {
Serial.print("obstacle at ");
Serial.print(distanceCm);
Serial.print(" cm, a pulse every ");
Serial.print(intervalFor(distanceCm));
Serial.println(" ms");
} else {
Serial.print("STOP: obstacle at ");
Serial.print(distanceCm);
Serial.println(" cm");
}
}
}
// The obstacle's pulse, shared by the motor and (when dry) the buzzer.
bool pulse = false;
if (zone == 2) {
pulse = true;
} else if (zone == 1) {
if (now - lastPulse >= intervalFor(distanceCm)) lastPulse = now;
pulse = now - lastPulse < (unsigned long)PULSE_MS;
}
digitalWrite(MOTOR, pulse ? HIGH : LOW);
bool wet = digitalRead(WET) == LOW;
if (wet != wasWet) {
wasWet = wet;
chirpStart = now;
if (wet) {
Serial.print("WATER ahead (A0 reads ");
Serial.print(analogRead(WATER_LEVEL));
Serial.println(")");
} else {
Serial.println("dry again");
}
}
if (wet) {
// Three 100 ms chirps, 100 ms apart, then 700 ms of quiet: 1.2 s a round.
unsigned long t = (now - chirpStart) % 1200;
digitalWrite(BUZZER, (t < 500 && t % 200 < 100) ? HIGH : LOW);
} else {
digitalWrite(BUZZER, pulse ? HIGH : LOW);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
measure sketch.ino
Sends a 10 microsecond pulse on TRIG, then times the echo with pulseIn, waiting at most 25 ms. No echo returns 999. Otherwise it divides the microseconds by 58 to get centimeters.
// One reading in centimeters, or 999 when no echo comes back in 25 ms.
long measure() {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long us = pulseIn(ECHO, HIGH, 25000);
if (us == 0) return 999;
return us / 58;
}intervalFor sketch.ino
Returns the time between pulses. It maps the distance from NEAR_CM to FAR_CM onto 100 to 1000 ms, so closer obstacles pulse faster.
// The time between pulses for a distance in the pulsing band.
unsigned long intervalFor(long cm) {
return map(cm, NEAR_CM, FAR_CM, 100, 1000);
}loop sketch.ino
Every 70 ms it measures and sorts the distance into clear, pulsing or very close. It drives the motor from the pulse timing. The buzzer follows the same pulse when dry, or plays the three-chirp pattern when the water sensor reads LOW.
void loop() {
unsigned long now = millis();
if (now - lastMeasure >= 70) {
lastMeasure = now;
distanceCm = measure();
int z = distanceCm > FAR_CM ? 0 : distanceCm >= NEAR_CM ? 1 : 2;
if (z != zone) {
zone = z;
if (z == 0) {
Serial.println("path clear");
} else if (z == 1) {
Serial.print("obstacle at ");
Serial.print(distanceCm);
Serial.print(" cm, a pulse every ");
Serial.print(intervalFor(distanceCm));
Serial.println(" ms");
} else {
Serial.print("STOP: obstacle at ");
Serial.print(distanceCm);
Serial.println(" cm");
}
}
}
// The obstacle's pulse, shared by the motor and (when dry) the buzzer.
bool pulse = false;
if (zone == 2) {
// ... 28 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- Readings come back empty every other time This project
Do not measure faster than the sensor's cycle. The sketch waits 70 ms between triggers. - The motor never buzzes This project
Check the transistor: emitter to ground, base through the 1k resistor to pin 6, motor and diode on the collector side. The diode's stripe goes toward 3.3 V. - It pulses too early or too late This project
ChangeFAR_CMandNEAR_CMto set where pulsing starts and where it becomes continuous. - The load never turns on, or is always on, because the legs are in the wrong order. Transistor (NPN or PNP)
Check the pinout in the datasheet for your part. A 2N2222 or BC547 can have emitter, base, collector in a different order. Flat side orientation is easy to get wrong. - The base is wired straight to the pin with no resistor, or the motor has no flyback diode. Transistor (NPN or PNP)
Put about 1k between the pin and the base. For motors and relays, add a diode across the coil with the stripe toward the positive side. - The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure. - A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground. - The diode is in backwards, so nothing flows, or a flyback diode conducts all the time. Current flows from the anode to the cathode, the end with the stripe. Diode
Put the striped end toward the more positive side for a series diode. For flyback across a coil, the stripe goes to the positive supply side of the coil.
Questions and answers
Why does the vibration motor need a transistor?
A coin vibration motor draws about 75 mA, far more than the 20 mA an Arduino pin should give. A 2N2222 switches it, and the diode across the motor takes the voltage kick when it stops.
How does the stick tell water from an obstacle?
An obstacle makes the buzzer and motor pulse together, faster as it gets closer. Water at the tip makes the buzzer chirp three times and pause, and the motor keeps reporting the obstacle so a wet floor never hides a wall.
Why does it measure every 70 ms and not faster?
An HC-SR04 ignores a new trigger within 60 ms of the last one while its burst is still echoing, so pinging faster makes every other reading come back empty.
How do I change the warning range?
Change FAR_CM (quiet beyond it, 100 cm) and NEAR_CM (continuous under it, 20 cm). The pulse spacing scales between them, from 1000 ms to 100 ms.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.