Project Overview & Difficulty Rating
Interfacing a Raspberry Pi Pico with a PCO (Pulse Count Output) sensor—such as a reed-switch water flow meter, gas meter, or anemometer—is a foundational embedded systems task. Unlike I2C or SPI sensors that hand you a formatted register value, a PCO module simply shorts a signal line to ground each time a mechanical or magnetic event occurs. Your microcontroller must count these pulses, filter out mechanical switch bounce, and calculate the physical metric (like liters per minute).
| Metric | Details |
|---|---|
| Difficulty | Intermediate (Requires interrupt logic and debouncing) |
| Time to Build | 45 minutes (Wiring + Code deployment) |
| Target Board | Raspberry Pi Pico W (RP2040) running MicroPython v1.23+ |
| Core Concept | Hardware Interrupts (IRQ), Software Debouncing, Edge Detection |
Parts List & Spec Sheet
To replicate this build exactly, source the following components. Do not substitute the pull-up resistor; floating GPIO pins on the RP2040 will trigger phantom interrupts from ambient electromagnetic noise.
- Microcontroller: Raspberry Pi Pico W (RP2040 dual-core ARM Cortex-M0+). The 'W' variant is recommended if you plan to log pulse data via MQTT/WiFi later.
- PCO Sensor: YF-S201 Water Flow Sensor (Brass body, Hall-effect PCO) OR a generic 2-wire reed-switch pulse module (commonly used on utility meters).
- Resistor: 10kΩ through-hole resistor (Brown-Black-Orange-Gold) for external pull-up redundancy.
- Wiring: 22 AWG solid core jumper wires.
- Power: 5V USB power supply (The YF-S201 requires 4.5V–5V to operate its internal Hall-effect op-amp cleanly, though the Pico's 3.3V logic is tolerant of the open-collector output).
Pin Mapping & Wiring Steps
The YF-S201 and most utility meter PCO modules use an open-collector or reed-switch output. This means the sensor does not output a high voltage; it only pulls the line to ground. We rely on the Pico's internal pull-up resistor (supplemented by an external 10kΩ for noise immunity) to hold the line HIGH until the pulse occurs.
| Pico W Pin | GPIO Number | Sensor Wire Color (YF-S201) | Function |
|---|---|---|---|
| VBUS (Pin 40) | 5V Output | Red | Sensor Power (4.5V - 5V) |
| GND (Pin 38) | Ground | Black | Common Ground |
| GP16 (Pin 21) | GPIO 16 | Yellow | PCO Pulse Signal (Interrupt Input) |
- Connect the sensor Red wire to the Pico W VBUS (5V). Do not use the 3V3 pin; the Hall sensor inside the YF-S201 will fail to trigger reliably below 4.0V.
- Connect the sensor Black wire to any Pico W GND pin.
- Insert the 10kΩ resistor between GP16 and 3V3 on your breadboard.
- Connect the sensor Yellow (signal) wire to GP16.
MicroPython Code with Interrupt Debouncing
This code targets the Raspberry Pi Pico W. It uses a hardware interrupt on the falling edge (when the switch closes and pulls the line to ground). Crucially, it includes a software debounce timer. Mechanical reed switches and Hall-effect circuits can 'ring' for 2 to 5 milliseconds during a single transition. Without debouncing, a single physical pulse will register as 10+ counts.
from machine import Pin
import time
# --- PIN DEFINITIONS ---
PCO_PIN = 16
LED_PIN = 25 # Onboard LED for visual pulse confirmation
# --- HARDWARE SETUP ---
# Enable internal pull-up, supplemented by external 10k resistor
pco_sensor = Pin(PCO_PIN, Pin.IN, Pin.PULL_UP)
onboard_led = Pin(LED_PIN, Pin.OUT)
# --- STATE VARIABLES ---
pulse_count = 0
last_pulse_time = 0
DEBOUNCE_MS = 50 # 50ms debounce window (adjust based on max RPM)
# --- INTERRUPT SERVICE ROUTINE (ISR) ---
def pco_interrupt_handler(pin):
global pulse_count, last_pulse_time
current_time = time.ticks_ms()
# Check if enough time has passed since the last valid pulse
if time.ticks_diff(current_time, last_pulse_time) > DEBOUNCE_MS:
pulse_count += 1
last_pulse_time = current_time
onboard_led.toggle()
# Attach the interrupt to the falling edge (High to Low transition)
pco_sensor.irq(trigger=Pin.IRQ_FALLING, handler=pco_interrupt_handler)
print('PCO Sensor Initialized. Waiting for pulses...')
# --- MAIN LOOP ---
try:
while True:
# Calculate Flow Rate (YF-S201 specific: 4.5 pulses/sec = 1 L/min)
# In a real application, you would measure pulses over a 1-second window
time.sleep(1.0)
print(f'Total Pulses: {pulse_count}')
except KeyboardInterrupt:
# Safely detach interrupt on exit to prevent memory leaks in REPL
pco_sensor.irq(handler=None)
print('Interrupt detached. Exiting safely.')
Debugging: Missed Pulses and ISR Errors
When working with PCO modules, the failure modes are almost always related to signal integrity or interrupt configuration. If your code crashes or your pulse count is wildly inaccurate, check these exact error strings and ranked causes.
The First Three Things to Check When It Fails
- Floating Pin State: Disconnect the sensor and run a multimeter in continuity mode between GP16 and GND. If it reads open, check your 10kΩ pull-up. A floating pin will oscillate randomly, maxing out the CPU and freezing the Pico.
- ISR Argument Signature: MicroPython requires the interrupt handler to accept the pin object as an argument. If you omit it, the interpreter will crash immediately upon the first pulse.
- Trigger Edge Selection: If your count is exactly double what it should be, you have likely set
trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING. Mechanical switches bounce on both edges. Stick toPin.IRQ_FALLINGfor open-collector PCO modules.
Error: TypeError: function takes 1 positional arguments but 0 were given
Ranked Causes:
- Missing
pinparameter in ISR: You defineddef pco_interrupt_handler():instead ofdef pco_interrupt_handler(pin):. The RP2040 hardware interrupt vector automatically passes the triggered pin object to the callback. Fix the function signature. - Accidental Class Method Binding: If your ISR is inside a class, you forgot the
selfparameter. It should bedef handler(self, pin):.
Error: Count is erratic, jumping by 5-20 per single physical rotation
Ranked Causes:
- Insufficient Debounce Time: Your
DEBOUNCE_MSis set too low (e.g., 5ms). Increase it to 50ms. Note: Do not exceed 100ms, or you will miss valid pulses at high flow rates. - Electromagnetic Interference (EMI): You are running the PCO signal wire parallel to a high-current AC line or a motor VFD. Route the signal wire away from noise sources and ensure the external 10kΩ pull-up is physically close to the Pico GPIO.
Extending or Simplifying the Build
To Simplify: If you are only measuring very slow pulses (like a residential gas meter that pulses once every few seconds), you can drop the interrupt entirely. Use a simple while True: loop with time.sleep(0.05) and poll the pin state. This removes the complexity of ISRs and global variable management, though it increases power consumption.
To Extend (High-Frequency PCO): MicroPython's software interrupts struggle with PCO signals above 2 kHz (e.g., high-speed rotary encoders or industrial flow meters). Python's garbage collection and interpreter overhead will cause missed pulses. For high-speed pulse counting on the RP2040, you must use the PIO (Programmable I/O) state machines. PIO runs independently of the main CPU cores and can count pulses at up to 125 MHz with zero jitter. Refer to the Raspberry Pi Pico Python SDK documentation for PIO assembly examples.
Frequently Asked Questions
Can I use a Raspberry Pi Pico PCO setup without an external pull-up resistor?
Yes, the RP2040 has internal pull-up resistors (activated via Pin.PULL_UP in the code). However, the internal pull-ups are relatively weak (around 50kΩ to 60kΩ). In environments with long wire runs (over 2 meters) or high EMI (like near water pumps), the line impedance will cause slow rise times, resulting in missed or double-counted edges. Adding a 4.7kΩ or 10kΩ external pull-up provides a 'stiff' logic HIGH that guarantees clean edges. For bench testing with 10cm jumper wires, the internal pull-up is sufficient.
Why is my Raspberry Pi Pico PCO pulse count double the actual flow?
This almost always happens because the interrupt is configured to trigger on both the rising and falling edges of the signal (Pin.IRQ_RISING | Pin.IRQ_FALLING). When a mechanical reed switch or Hall-effect transistor closes, it pulls the voltage from 3.3V to 0V (falling edge). When it opens, it returns to 3.3V (rising edge). If your code listens to both, it counts the close and the open as separate events. Change your trigger to Pin.IRQ_FALLING exclusively to count only the active pulse state.
How do I put the Raspberry Pi Pico to sleep between PCO pulses to save battery?
The RP2040 does not have a true deep-sleep mode like the ESP32, but it does support machine.lightsleep(). You can configure the PCO GPIO pin as a wake source. When the pulse pulls the line low, it wakes the Pico, increments the counter, and immediately goes back to sleep. Note that during lightsleep(), the USB interface drops, so you must rely on battery power and log data to an SD card or internal flash, transmitting via WiFi only at scheduled intervals. See the MicroPython machine.Pin documentation for wake-from-IRQ specifics.






