Count the pulses
The acceptor says which coin it took by sending that many pulses, one after another. The coin probe counts them and decides a coin has ended after 200 ms of silence, so it depends on two timings agreeing: yours and the acceptor's.
A coin is a burst
The acceptor does not send a number. It sends pulses, and the count is the message: a coin taught to three pulses pulls the COIN line low three times in quick succession. The probe's job is to count them and to know when one coin has finished and the next has not yet started.
It uses two times, both in the code:
- 60 ms lockout. An edge within 60 ms of the last one counted is ignored, so a noisy edge is not counted twice.
- 200 ms group gap. When 200 ms pass with no new edge, the coin is over and the probe prints the count.
When the timings disagree
The figure's pulse spacings are examples, not measurements of the kit's acceptor: the spacing is an acceptor setting and varies between models. What matters is the order. The acceptor's spacing has to be longer than the lockout and shorter than the group gap, and the gap has to be shorter than the pause between two coins.
These are the lines that apply both rules, once per line the probe watches:
for (uint8_t i=0; i<CHANNELS; ++i) {
if (pulses[i] && ms-lastEdge[i] >= GROUP_MS) {
Serial.print(COIN_NAMES[i]); Serial.print(": ");
Serial.print(pulses[i]); Serial.println(" pulse(s)");
pulses[i] = 0;
}
bool low = digitalRead(COIN_PINS[i]) == LOW;
if (low && !lastLow[i] && (!seen[i] || ms-lastEdge[i] >= LOCKOUT_MS)) {
seen[i] = true;
lastEdge[i] = ms;
if (pulses[i] < 65535) ++pulses[i];
}
lastLow[i] = low;
}Read it five times
Drop the same taught coin five times, waiting a second between them. Five lines with the same count means the timings agree. Then drop a coin the acceptor was not taught: it should come back out of the return chute and print nothing.
COIN GPIO14: 1 pulse(s)On a Zero 2 W
The same rules, with gpiozero's edge callback doing the counting. It watches COIN on BCM26 and COUNTER on BCM24:
"""Watch COIN (BCM26) and COUNTER (BCM24) and print each pulse group.
See which line your acceptor pulses. Never add the two counts together.
"""
from collections import deque
from threading import Lock
from time import monotonic, sleep
from gpiozero import DigitalInputDevice
class CoinProbe:
def __init__(self, bcm):
self.bcm = bcm
self.lock = Lock()
self.last = None
self.count = 0
self.ready = deque()
self.device = DigitalInputDevice(bcm, pull_up=True, bounce_time=None)
self.device.when_activated = self.edge
def edge(self):
with self.lock:
now = monotonic()
if self.last is not None and now - self.last < 0.060:
return
if self.count and now - self.last >= 0.200:
self.ready.append(self.count)
self.count = 0
self.last = now
self.count += 1
def poll(self):
with self.lock:
if self.ready:
return self.ready.popleft()
if self.count and monotonic() - self.last >= 0.200:
count, self.count = self.count, 0
return count
return 0
def close(self):
self.device.when_activated = None
self.device.close()
def main():
probes = []
try:
for bcm in (26, 24):
probes.append(CoinProbe(bcm))
print("Coin probe ready. Insert one learned coin, then wait.")
while True:
for probe in probes:
count = probe.poll()
if count:
print("%s BCM%d: %d pulse(s)" % ("COIN" if probe.bcm == 26 else "COUNTER", probe.bcm, count))
sleep(0.005)
except KeyboardInterrupt:
pass
finally:
for probe in probes:
probe.close()
if __name__ == "__main__":
main()The code
For the Nano and ESP32-S3 adapters. Power the mainboard from its USB-C, keep the board's USB in for the serial monitor, open it at 115200 and drop one taught coin.
/* HotSwap Arcade Kit - coin probe: count the pulses one coin sends
Nano: Tools > Board Arduino Nano · Processor ATmega328P
ESP32-S3: Tools > Board ESP32S3 Dev Module · USB CDC On Boot Enabled
when the cable is in the board's native USB port
Power the mainboard from its USB-C: the acceptor's 12 V comes from there.
Serial Monitor at 115200. A pulse is the line pulled LOW.
Guide: https://learn.lonelybinary.com/manuals/arcade/count-the-pulses */
#include <Arduino.h>
#if defined(ARDUINO_AVR_NANO)
const uint8_t COIN_PINS[] = {A0};
const char *COIN_NAMES[] = {"COIN A0"};
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
// COIN (header pin 19) lands on GPIO14, COUNTER (pin 18) on GPIO21.
// Watch both, and see which one your acceptor pulses. Never add them up.
const uint8_t COIN_PINS[] = {14,21};
const char *COIN_NAMES[] = {"COIN GPIO14", "COUNTER GPIO21"};
#else
#error "Select classic Arduino Nano or ESP32S3 Dev Module."
#endif
// Diagnostic only. Keep loop fast; no display or long delays here.
const uint8_t CHANNELS = sizeof(COIN_PINS) / sizeof(COIN_PINS[0]);
const unsigned long LOCKOUT_MS = 60, GROUP_MS = 200;
bool lastLow[CHANNELS], seen[CHANNELS];
unsigned long lastEdge[CHANNELS];
uint16_t pulses[CHANNELS];
void setup() {
Serial.begin(115200);
delay(1000);
for (uint8_t i=0; i<CHANNELS; ++i) {
pinMode(COIN_PINS[i], INPUT_PULLUP);
lastLow[i] = digitalRead(COIN_PINS[i]) == LOW;
}
Serial.println("Coin probe ready. Insert one learned coin, then wait.");
}
void loop() {
unsigned long ms = millis();
for (uint8_t i=0; i<CHANNELS; ++i) {
if (pulses[i] && ms-lastEdge[i] >= GROUP_MS) {
Serial.print(COIN_NAMES[i]); Serial.print(": ");
Serial.print(pulses[i]); Serial.println(" pulse(s)");
pulses[i] = 0;
}
bool low = digitalRead(COIN_PINS[i]) == LOW;
if (low && !lastLow[i] && (!seen[i] || ms-lastEdge[i] >= LOCKOUT_MS)) {
seen[i] = true;
lastEdge[i] = ms;
if (pulses[i] < 65535) ++pulses[i];
}
lastLow[i] = low;
}
delay(1);
}On the ESP32-S3 it watches COIN and COUNTER side by side. If both print, note which count matches the coin; never add the two together.
View on GitHub · examples/arduino/CoinProbe/CoinProbe.ino @ v1.0For the Pico and Pico 2, with arcade.py saved on the board beside it. It watches COIN on GP28.
"""Observe GP28 after configuring the acceptor. No currency assumptions."""
from time import sleep_ms
from arcade import CoinInput
coin = CoinInput(28)
print("Coin probe ready. Insert one learned coin, then wait.")
try:
while True:
pulses = coin.poll()
if pulses == -1:
print("Input queue overflow; group discarded. Repeat the test.")
elif pulses:
print("GP28:", pulses if pulses <= 10 else ">10", "pulse(s)")
sleep_ms(1)
finally:
coin.close()The pulse counting happens in arcade.py's CoinInput, in an interrupt, so a slow loop cannot miss an edge. This file only prints each finished group.
View on GitHub · examples/pico/coin_probe.py @ v1.0When it does not work
Check the mainboard's USB-C is plugged in, since the acceptor has no 12 V without it. Then check how the acceptor's output is set: most acceptors can be set to pulse their output or to hold it, and the probe needs pulses. The manual that came with it says how.
The acceptor's pulses are further apart than the 200 ms group gap. Lengthen GROUP_MS in the Arduino probe, or the 200 in arcade.py, to more than the spacing, or set the acceptor to its faster pulse setting.
The group gap is longer than the pause between the coins. It rarely happens at 200 ms; if you have lengthened the gap, shorten it until one coin is one group and two coins are two.
Pulses are arriving closer together than the 60 ms lockout and the probe is throwing them away. Set the acceptor to a slower pulse, or lower LOCKOUT_MS.
Both are wired, and on most acceptors both pulse: COIN with the coin's count, COUNTER with one per coin. Use COIN, on GPIO14, for money; use COUNTER, on GPIO21, when you only need to know that a coin arrived.
Credits, a start button and a ten-second round, on a Pico.
A coin-operated game →Edit this page — content/books/arcade/count-the-pulses.mdx
Questions about this product
See what other owners have asked, and read their solutions.
HotSwap Arcade Kit: Buttons, Joystick and Coin Acceptor for ESP32-S3, Nano, Pico and Zero 2 W
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.