Visitor counter
A motion sensor counts people walking past and chirps for each one. A 74HC595 shift register drives a 7-segment digit with just three wires, and the count survives a power cut.
Wiring
| From | To board | Note |
|---|---|---|
| PIR VCC | 5V | The HC-SR501 needs 5 V or more, but its OUT pin only goes to 3.3 V |
| PIR GND | GND | |
| PIR OUT | GPIO 27 | |
| 74HC595 VCC (pin 16) | 3V3 | Put the 0.1 µF capacitor from pin 16 to GND, close to the chip |
| 74HC595 GND (pin 8) | GND | |
| 74HC595 SER (pin 14) | GPIO 23 | Data in |
| 74HC595 SRCLK (pin 11) | GPIO 18 | Shift clock |
| 74HC595 RCLK (pin 12) | GPIO 19 | Latch: shows the new pattern |
| 74HC595 OE (pin 13) | GND | Outputs always on |
| 74HC595 MR (pin 10) | 3V3 | Never clear the register |
| 74HC595 Q0 to Q6 (pins 15, 1 to 6) | Digit segments a to g | Each through its own 330 Ω resistor |
| 74HC595 Q7 (pin 7) | Digit decimal point | Through 330 Ω |
| Digit common pin | GND or 3V3 | GND for common cathode, 3V3 for common anode (then set COMMON_ANODE) |
| Buzzer + | GPIO 25 | Buzzer - to GND |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32 Dev Module
// Visitor counter: a PIR sensor counts people walking past, a 74HC595 shift
// register drives a 7-segment digit, and a piezo chirps for each visitor.
// The digit shows the last digit of the count; after each visit it spells out
// the whole number, one digit at a time. The count survives a power cut.
#include <Preferences.h>
const int PIR_PIN = 27;
const int SER_PIN = 23; // 74HC595 SER (DS), pin 14
const int CLOCK_PIN = 18; // 74HC595 SRCLK (SHCP), pin 11
const int LATCH_PIN = 19; // 74HC595 RCLK (STCP), pin 12
const int BUZZER = 25;
const int BUTTON = 0; // the BOOT button: hold 3 s to reset the count
const bool COMMON_ANODE = false; // set true for a common-anode digit (common pin to 3V3)
const unsigned long WARMUP_MS = 30000; // a PIR needs about 30 s after power-up to settle
const unsigned long REARM_MS = 2000; // quiet time after a visit before the next one counts
// Segments a to g on the 595's outputs Q0 to Q6, the decimal point on Q7.
const uint8_t DIGITS[10] = {
0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110,
0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111,
};
const uint8_t DOT = 0b10000000;
Preferences prefs;
uint32_t count = 0;
bool wasHigh = false;
unsigned long lastLow = 0;
void show(uint8_t segments) {
if (COMMON_ANODE) segments = ~segments; // a common-anode segment lights when its pin is low
digitalWrite(LATCH_PIN, LOW);
shiftOut(SER_PIN, CLOCK_PIN, MSBFIRST, segments);
digitalWrite(LATCH_PIN, HIGH); // the new pattern appears on the outputs all at once
}
void showCount() {
show(DIGITS[count % 10]);
}
// Spell the whole count, one digit at a time, with the dot on the last one.
void spellCount() {
char text[12];
snprintf(text, sizeof(text), "%lu", (unsigned long)count);
for (int i = 0; text[i]; i++) {
uint8_t seg = DIGITS[text[i] - '0'];
if (!text[i + 1]) seg |= DOT;
show(0);
delay(120);
show(seg);
delay(450);
}
showCount();
}
void chirp() {
tone(BUZZER, 1800, 50);
delay(70);
tone(BUZZER, 2400, 50);
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(SER_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
prefs.begin("visitors", false);
count = prefs.getUInt("count", 0);
Serial.printf("Visitor counter. Warming up the PIR for %lu s.\n", WARMUP_MS / 1000);
// Warm-up: a spinning segment, so you know it is not counting yet.
const uint8_t spin[] = {0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00100000};
for (unsigned long start = millis(); millis() - start < WARMUP_MS;) {
show(spin[(millis() / 120) % 6]);
delay(20);
}
showCount();
lastLow = millis();
Serial.println("Counting.");
}
void loop() {
const bool motion = digitalRead(PIR_PIN) == HIGH;
const unsigned long now = millis();
if (!motion) {
if (wasHigh) lastLow = now;
wasHigh = false;
} else if (!wasHigh) {
wasHigh = true;
// A new visit only counts after REARM_MS of quiet, so one person is one count.
if (now - lastLow >= REARM_MS) {
count++;
prefs.putUInt("count", count);
Serial.printf("count = %lu\n", (unsigned long)count);
chirp();
spellCount();
}
}
// Hold BOOT for three seconds to start again from zero.
if (digitalRead(BUTTON) == LOW) {
const unsigned long pressed = now;
while (digitalRead(BUTTON) == LOW) {
if (millis() - pressed > 3000) {
count = 0;
prefs.putUInt("count", 0);
Serial.println("Count reset.");
showCount();
while (digitalRead(BUTTON) == LOW) delay(10);
break;
}
delay(10);
}
}
delay(20);
}Wiring
| From | To board | Note |
|---|---|---|
| PIR VCC | 5V | The HC-SR501 needs 5 V or more, but its OUT pin only goes to 3.3 V |
| PIR GND | GND | |
| PIR OUT | GPIO 4 | |
| 74HC595 VCC (pin 16) | 3V3 | Put the 0.1 µF capacitor from pin 16 to GND, close to the chip |
| 74HC595 GND (pin 8) | GND | |
| 74HC595 SER (pin 14) | GPIO 5 | Data in |
| 74HC595 SRCLK (pin 11) | GPIO 6 | Shift clock |
| 74HC595 RCLK (pin 12) | GPIO 7 | Latch: shows the new pattern |
| 74HC595 OE (pin 13) | GND | Outputs always on |
| 74HC595 MR (pin 10) | 3V3 | Never clear the register |
| 74HC595 Q0 to Q6 (pins 15, 1 to 6) | Digit segments a to g | Each through its own 330 Ω resistor |
| 74HC595 Q7 (pin 7) | Digit decimal point | Through 330 Ω |
| Digit common pin | GND or 3V3 | GND for common cathode, 3V3 for common anode (then set COMMON_ANODE) |
| Buzzer + | GPIO 10 | Buzzer - to GND |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32S3 Dev Module
// Visitor counter: a PIR sensor counts people walking past, a 74HC595 shift
// register drives a 7-segment digit, and a piezo chirps for each visitor.
// The digit shows the last digit of the count; after each visit it spells out
// the whole number, one digit at a time. The count survives a power cut.
#include <Preferences.h>
const int PIR_PIN = 4;
const int SER_PIN = 5; // 74HC595 SER (DS), pin 14
const int CLOCK_PIN = 6; // 74HC595 SRCLK (SHCP), pin 11
const int LATCH_PIN = 7; // 74HC595 RCLK (STCP), pin 12
const int BUZZER = 10;
const int BUTTON = 0; // the BOOT button: hold 3 s to reset the count
const bool COMMON_ANODE = false; // set true for a common-anode digit (common pin to 3V3)
const unsigned long WARMUP_MS = 30000; // a PIR needs about 30 s after power-up to settle
const unsigned long REARM_MS = 2000; // quiet time after a visit before the next one counts
// Segments a to g on the 595's outputs Q0 to Q6, the decimal point on Q7.
const uint8_t DIGITS[10] = {
0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110,
0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111,
};
const uint8_t DOT = 0b10000000;
Preferences prefs;
uint32_t count = 0;
bool wasHigh = false;
unsigned long lastLow = 0;
void show(uint8_t segments) {
if (COMMON_ANODE) segments = ~segments; // a common-anode segment lights when its pin is low
digitalWrite(LATCH_PIN, LOW);
shiftOut(SER_PIN, CLOCK_PIN, MSBFIRST, segments);
digitalWrite(LATCH_PIN, HIGH); // the new pattern appears on the outputs all at once
}
void showCount() {
show(DIGITS[count % 10]);
}
// Spell the whole count, one digit at a time, with the dot on the last one.
void spellCount() {
char text[12];
snprintf(text, sizeof(text), "%lu", (unsigned long)count);
for (int i = 0; text[i]; i++) {
uint8_t seg = DIGITS[text[i] - '0'];
if (!text[i + 1]) seg |= DOT;
show(0);
delay(120);
show(seg);
delay(450);
}
showCount();
}
void chirp() {
tone(BUZZER, 1800, 50);
delay(70);
tone(BUZZER, 2400, 50);
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(SER_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
prefs.begin("visitors", false);
count = prefs.getUInt("count", 0);
Serial.printf("Visitor counter. Warming up the PIR for %lu s.\n", WARMUP_MS / 1000);
// Warm-up: a spinning segment, so you know it is not counting yet.
const uint8_t spin[] = {0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00100000};
for (unsigned long start = millis(); millis() - start < WARMUP_MS;) {
show(spin[(millis() / 120) % 6]);
delay(20);
}
showCount();
lastLow = millis();
Serial.println("Counting.");
}
void loop() {
const bool motion = digitalRead(PIR_PIN) == HIGH;
const unsigned long now = millis();
if (!motion) {
if (wasHigh) lastLow = now;
wasHigh = false;
} else if (!wasHigh) {
wasHigh = true;
// A new visit only counts after REARM_MS of quiet, so one person is one count.
if (now - lastLow >= REARM_MS) {
count++;
prefs.putUInt("count", count);
Serial.printf("count = %lu\n", (unsigned long)count);
chirp();
spellCount();
}
}
// Hold BOOT for three seconds to start again from zero.
if (digitalRead(BUTTON) == LOW) {
const unsigned long pressed = now;
while (digitalRead(BUTTON) == LOW) {
if (millis() - pressed > 3000) {
count = 0;
prefs.putUInt("count", 0);
Serial.println("Count reset.");
showCount();
while (digitalRead(BUTTON) == LOW) delay(10);
break;
}
delay(10);
}
}
delay(20);
}Wiring
| From | To board | Note |
|---|---|---|
| PIR VCC | 5V | The HC-SR501 needs 5 V or more, but its OUT pin only goes to 3.3 V |
| PIR GND | GND | |
| PIR OUT | GPIO 10 | |
| 74HC595 VCC (pin 16) | 3V3 | Put the 0.1 µF capacitor from pin 16 to GND, close to the chip |
| 74HC595 GND (pin 8) | GND | |
| 74HC595 SER (pin 14) | GPIO 4 | Data in |
| 74HC595 SRCLK (pin 11) | GPIO 5 | Shift clock |
| 74HC595 RCLK (pin 12) | GPIO 6 | Latch: shows the new pattern |
| 74HC595 OE (pin 13) | GND | Outputs always on |
| 74HC595 MR (pin 10) | 3V3 | Never clear the register |
| 74HC595 Q0 to Q6 (pins 15, 1 to 6) | Digit segments a to g | Each through its own 330 Ω resistor |
| 74HC595 Q7 (pin 7) | Digit decimal point | Through 330 Ω |
| Digit common pin | GND or 3V3 | GND for common cathode, 3V3 for common anode (then set COMMON_ANODE) |
| Buzzer + | GPIO 7 | Buzzer - to GND |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32C3 Dev Module
// Visitor counter: a PIR sensor counts people walking past, a 74HC595 shift
// register drives a 7-segment digit, and a piezo chirps for each visitor.
// The digit shows the last digit of the count; after each visit it spells out
// the whole number, one digit at a time. The count survives a power cut.
#include <Preferences.h>
const int PIR_PIN = 10;
const int SER_PIN = 4; // 74HC595 SER (DS), pin 14
const int CLOCK_PIN = 5; // 74HC595 SRCLK (SHCP), pin 11
const int LATCH_PIN = 6; // 74HC595 RCLK (STCP), pin 12
const int BUZZER = 7;
const int BUTTON = 9; // the BOOT button: hold 3 s to reset the count
const bool COMMON_ANODE = false; // set true for a common-anode digit (common pin to 3V3)
const unsigned long WARMUP_MS = 30000; // a PIR needs about 30 s after power-up to settle
const unsigned long REARM_MS = 2000; // quiet time after a visit before the next one counts
// Segments a to g on the 595's outputs Q0 to Q6, the decimal point on Q7.
const uint8_t DIGITS[10] = {
0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110,
0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111,
};
const uint8_t DOT = 0b10000000;
Preferences prefs;
uint32_t count = 0;
bool wasHigh = false;
unsigned long lastLow = 0;
void show(uint8_t segments) {
if (COMMON_ANODE) segments = ~segments; // a common-anode segment lights when its pin is low
digitalWrite(LATCH_PIN, LOW);
shiftOut(SER_PIN, CLOCK_PIN, MSBFIRST, segments);
digitalWrite(LATCH_PIN, HIGH); // the new pattern appears on the outputs all at once
}
void showCount() {
show(DIGITS[count % 10]);
}
// Spell the whole count, one digit at a time, with the dot on the last one.
void spellCount() {
char text[12];
snprintf(text, sizeof(text), "%lu", (unsigned long)count);
for (int i = 0; text[i]; i++) {
uint8_t seg = DIGITS[text[i] - '0'];
if (!text[i + 1]) seg |= DOT;
show(0);
delay(120);
show(seg);
delay(450);
}
showCount();
}
void chirp() {
tone(BUZZER, 1800, 50);
delay(70);
tone(BUZZER, 2400, 50);
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(SER_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
prefs.begin("visitors", false);
count = prefs.getUInt("count", 0);
Serial.printf("Visitor counter. Warming up the PIR for %lu s.\n", WARMUP_MS / 1000);
// Warm-up: a spinning segment, so you know it is not counting yet.
const uint8_t spin[] = {0b00000001, 0b00000010, 0b00000100, 0b00001000, 0b00010000, 0b00100000};
for (unsigned long start = millis(); millis() - start < WARMUP_MS;) {
show(spin[(millis() / 120) % 6]);
delay(20);
}
showCount();
lastLow = millis();
Serial.println("Counting.");
}
void loop() {
const bool motion = digitalRead(PIR_PIN) == HIGH;
const unsigned long now = millis();
if (!motion) {
if (wasHigh) lastLow = now;
wasHigh = false;
} else if (!wasHigh) {
wasHigh = true;
// A new visit only counts after REARM_MS of quiet, so one person is one count.
if (now - lastLow >= REARM_MS) {
count++;
prefs.putUInt("count", count);
Serial.printf("count = %lu\n", (unsigned long)count);
chirp();
spellCount();
}
}
// Hold BOOT for three seconds to start again from zero.
if (digitalRead(BUTTON) == LOW) {
const unsigned long pressed = now;
while (digitalRead(BUTTON) == LOW) {
if (millis() - pressed > 3000) {
count = 0;
prefs.putUInt("count", 0);
Serial.println("Count reset.");
showCount();
while (digitalRead(BUTTON) == LOW) delay(10);
break;
}
delay(10);
}
}
delay(20);
}Parts
| Quantity | Part | Where to buy |
|---|---|---|
| 1× | ESP32 dev board | Buy at Amazon |
| 1× | PIR motion sensor (HC-SR501 type) | Buy at Adafruit |
| 1× | 74HC595 shift register | Buy at Adafruit |
| 1× | Single 7-segment LED digit, common cathode or common anode | |
| 8× | 330 Ω resistors, one per segment | |
| 1× | 0.1 µF ceramic capacitor | |
| 1× | Passive buzzer or piezo | Buy at Adafruit |
| 1× | Breadboard and jumper wires |
Amazon buy links may include an Associates tag, and this site may earn a commission from qualifying Amazon purchases at no extra cost to you. Adafruit links are direct.
Libraries
Preferences (built in)
Load it
- In the Arduino IDE, install the esp32 board package and pick the board named above the sketch.
- Paste the sketch, choose the board's port and upload. Hold BOOT if the upload stalls at Connecting.
- Close the IDE's serial monitor, then open the bench here to watch the output.
Field notes
- The shift register turns three ESP32 pins into eight outputs: the sketch clocks one byte into SER with SRCLK, then pulses RCLK so all eight segments change at once.
- Not sure whether your digit is common cathode or common anode? Wire the common pin to GND and run it. If everything except the right segments lights, move the common pin to 3V3 and set COMMON_ANODE to true.
- Use 330 Ω per segment, not one shared resistor: an 8 lights at once and the 74HC595 can only supply about 70 mA in total.
- On the HC-SR501, turn the time knob fully anticlockwise (shortest hold) and set the jumper to single trigger (L) if it has one. The sketch also waits 2 s of quiet before counting the next visitor.
- A PIR needs about 30 s after power-up to settle and may trigger at random until then. The digit spins while it warms up.
- Hold BOOT for three seconds to reset the count. Do not hold it while the board starts: that puts it into download mode.
- On the C3 the PIR uses GPIO 10 and the shift register GPIO 4, 5 and 6; GPIO 9 is the BOOT button itself.
Not tested yet
- Not tested on hardware yet. Compiles for every board listed with Arduino-ESP32 core 3.3.12.
Community builds
Loading build reports from people who made this.
Built it? Send a build report with your board and what the serial port printed. Working reports become the "Tested on this board" entries above.