- C 82.1%
- CMake 11.7%
- Python 6.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| include | ||
| src | ||
| .gitignore | ||
| CMakeLists.txt | ||
| conanfile.py | ||
| example.c | ||
| pico_sdk_import.cmake | ||
| README.md | ||
lf-phototransistor
Reusable IR TX LED and RX Phototransistor sensor driver library for the Raspberry Pi Pico C SDK, packaged as a Conan package. Designed for discrete generic 5mm / 3mm IR emitter LEDs and phototransistors configured as reflective proximity, break-beam, or light-level sensors.
This is part of the Lost + Found suite of projects.
🛠️ Features
- ADC Integration: Compatible with Pico ADC pins (
GP26/ ADC0,GP27/ ADC1,GP28/ ADC2). - Multi-Sample Oversampling: Configurable hardware sample averaging (default 32 samples) to reject 50/60Hz mains flicker and electrical noise.
- Synchronous Ambient Light Cancellation: Optional TX LED GPIO modulation (
GP22) that measures background ambient room light and subtracts it from active reflection. - Hysteresis Triggering: Integrated threshold detector with hysteresis for stable break-beam or proximity switching.
- Flexible Circuit Topologies: Supports both active-low (common-emitter pull-up) and active-high (common-collector pull-down) dividers.
- Advanced HDR & Auto-Ranging Designs: Architectural patterns for 100+ dB optical dynamic range using staggered multi-PTX clusters or GPIO-switched auto-ranging resistor banks.
- Analytical Chemistry & Spectrophotometry: Beer-Lambert absorbance math and colorimetric assay automation for micro-spectrophotometry.
- Passive Ambient Lux & Flicker Meter: Continuous 7-decade ambient light tracking (0.01 to 120,000 lux) and high-speed microsecond lighting flicker analysis without an LED.
- High-Speed Optical Tachometry & Encoders: Non-contact RPM measurement up to 100,000+ RPM and rotary quadrature position decoding.
🔌 Standard Hardware Setup & Pin Mapping
Schematic (Single-Channel)
+3.3V (Pin 36) +3.3V (Pin 36)
| |
[220R] [10k]
| |
(Anode) | +-----> GP26 (ADC0 / Pin 31)
[IR LED] |
(Cathode) | (Collector)
| [RX PTX]
GND (Pin 38) (Emitter)
|
GND (Pin 38)
(Alternatively, connect the 220R resistor to GP22 instead of +3.3V to enable synchronous ambient light pulsing).
Recommended Pin Table
| Component Pin | Pico 2 GPIO Pin | Physical Pin | Description |
|---|---|---|---|
| Phototransistor (10k Node) | GP26 (ADC0) | Pin 31 | Analog input from voltage divider. |
| IR TX LED (Anode / 220R) | GP22 (or 3V3) | Pin 29 (or 36) | TX pulse control or continuous power. |
| Divider Pull-up | 3V3 (OUT) | Pin 36 | 3.3V power reference for voltage divider. |
| Ground (Cathode / Emitter) | GND | Pin 38 | System common ground. |
Tip
Underwhelming Sensitivity? Flip the Phototransistor! Unlike diodes which completely block current when reversed, an NPN phototransistor still functions if its Collector and Emitter leads are swapped, but operates in "Reverse Active" mode. In reverse mode, the transistor's gain drops from β_F ≈ 300+ down to β_R ≈ 1–3, producing only a weak ≈ 0.4V deflection even at point-blank range.
Because manufacturers frequently vary lead length conventions for dark-epoxy phototransistors: if light response seems weak or fails to saturate, simply swap the phototransistor pins 180° on your breadboard to restore full forward active gain and >3.1V dynamic swing.
📐 Phototransistor Pull-Up Resistor Selection (R_L)
The choice of the pull-up resistor (R_L) between 3V3 and the Phototransistor Collector determines the sensor's optical sensitivity, saturation threshold, and frequency response speed.
Physics & Math
- Light to Current: Incoming infrared photons generate base current I_base, which is amplified by transistor gain β:
I_C = β · I_base - Voltage Drop: Collector current pulls the ADC node down from 3.3V:
V_ADC = 3.3V - (I_C · R_L) - Saturation Point (I_sat): The minimum collector current required to pull V_ADC completely down to saturation (V_CE(sat) ≈ 0.1V):
I_sat ≈ 3.3V / R_L
Resistor Tuning Guide
| Pull-Up Resistor (R_L) | Saturation Current (I_sat) | Sensitivity | Response Time (t_r / t_f) | Ideal Application |
|---|---|---|---|---|
| 1 kΩ – 2.2 kΩ | 1.5 – 3.3 mA | Low | ≈ 2 – 5 µs | High-speed optical tachometers, high-RPM slotted encoders, high ambient room light immunity. |
| 10 kΩ (Standard) | ≈ 330 µA | Moderate | ≈ 15 – 30 µs | General-purpose break-beam, optical limit switches, short-range reflectance (3–8 cm). |
| 47 kΩ – 100 kΩ | 33 – 70 µA | High | ≈ 100 – 300 µs | Diffuse object reflection (dark/matte surfaces), low-power pulsed LED modes, longer range (10–20 cm). |
| 470 kΩ – 1 MΩ | 3.3 – 7 µA | Ultra-High | > 1 ms | Ambient light sensing, twilight detectors, fire/flame detection. |
🎚️ Advanced High Dynamic Range (HDR) Architectures
A single fixed resistor limits dynamic range to the 12-bit ADC span (72 dB). For extreme optical range—measuring faint diffuse reflections from dark objects without clipping under point-blank glare—two high-performance topologies are possible:
1. Single-PTX Auto-Ranging Transimpedance (GPIO-Switched Bank)
Use a single phototransistor on GP26 (ADC0) and connect three different pull-up resistors to standard GPIO output pins:
GP10 (Low-Gain Rail) ───[ 1k ]───┐
GP11 (Mid-Gain Rail) ───[ 10k ]───┼────► GP26 (ADC0)
GP12 (High-Gain Rail) ───[ 100k ]───┤ │
(Collector)
[PTX]
(Emitter)
│
GND
How It Works:
- Selecting a Range: To activate a resistor, configure its corresponding GPIO as
GPIO_OUTdriving HIGH (1/ 3.3V). Put the other unused resistor GPIOs into high-impedance input mode (GPIO_INwith internal pulls disabled). - Auto-Ranging Algorithm (DMM Style):
- Start at highest gain (100 kΩ on
GP12). - If the ADC reading approaches saturation (V_ADC < 0.2V), step down to 10 kΩ (
GP11). - If 10 kΩ also saturates, step down to 1 kΩ (
GP10). - If signal drops below 10% of scale, step gain back up.
- Start at highest gain (100 kΩ on
- Benefit: Over 100–120 dB of effective optical dynamic range using only 1 phototransistor and 3 passive resistors.
2. Multi-PTX Staggered-Gain Cluster (Concurrent 3-Channel HDR)
For applications requiring instant single-shot HDR without the settling latency of switching resistors, place three adjacent phototransistors on the 3 Pico ADC channels:
+3.3V
┌────────────────┼────────────────┐
│ │ │
[1k] [10k] [100k]
│ │ │
├─────► GP26 ├─────► GP27 ├─────► GP28
│ (ADC0) │ (ADC1) │ (ADC2)
│ (Low Gain) │ (Mid Gain) │ (High Gain)
[PTX #1] [PTX #2] [PTX #3]
│ │ │
GND GND GND
- Software HDR Fusion: In every ADC round-robin sweep, select the highest-gain channel that has not saturated, giving instantaneous continuous HDR across > 100,000:1 optical power ratios.
🏎️ Non-Contact Optical Tachometers & Quadrature Encoders
With a low-impedance pull-up resistor (1 kΩ – 2.2 kΩ), the phototransistor achieves microsecond response times (≈ 2–5 µs / >100 kHz bandwidth), enabling high-speed mechanical sensing with zero physical contact, drag, or friction.
1. 100,000+ RPM Optical Tachometer
Place a small mark of retro-reflective tape (or white paint) onto a motor shaft, drone propeller, or spinning spindle:
TX LED ───► [Reflective Mark on Shaft] ───► RX PTX ───► Pico GP26 (ADC / PWM)
- Zero Drag: No mechanical loading or magnetic eddy-current drag on high-speed micro-motors.
- Calculation:
RPM = (Pulses_Per_Second / Markers_Per_Rev) * 60 - Pico Hardware Counter: Use the RP2040/RP2350 hardware PWM slice configured in input frequency counter mode (
pwm_config_set_clkdiv_mode(&cfg, PWM_DIV_B_RISING)) to count pulses accurately up to millions of RPM without CPU intervention.
2. Optical Quadrature Encoder (Bidirectional Tracking)
Position two sensor pairs (Channel A and Channel B) over a slotted encoder disc with a 90° electrical phase offset:
Tooth Slot Tooth Slot
┌───────┐ ┌───────┐ ┌───────┐
│ █████ │ │ █████ │ │ █████ │
└───────┘ └───────┘ └───────┘
▲ ▲
│ │
[Sensor A] [Sensor B]
└──── (1.25 Teeth) ───┘
- Spacing Formula: Physical distance =
(N + 0.25) * Tooth_Pitch(e.g. 1.25 teeth apart), leaving room for discrete 5mm packages while providing 90° quadrature. - Direction Decoding:
- Clockwise (CW): Channel A leads Channel B (
00 → 10 → 11 → 01 → 00). - Counter-Clockwise (CCW): Channel B leads Channel A (
00 → 01 → 11 → 10 → 00).
- Clockwise (CW): Channel A leads Channel B (
- 4X Resolution: Counting all rising and falling edges multiplies wheel resolution 4-fold (a 25-slot wheel produces 100 Counts Per Revolution).
- Pico PIO Decoder: Run a state machine in hardware Programmable I/O (PIO) to decode quadrature at tens of millions of counts per second with zero CPU overhead.
☀️ Ultra-Wide-Range Ambient Light & Flicker Sensing (Passive Mode)
Without any emitting LED, a clear / full-spectrum phototransistor paired with the auto-ranging resistor bank acts as a high-performance ambient lux meter and optical oscilloscope.
1. Spanning 7 Decades of Real-World Light (0.01 to 120,000 Lux)
Real-world lighting spans over six orders of magnitude:
0.01 lux 1 lux 50 lux 500 lux 10,000 lux 120,000 lux
│ │ │ │ │ │
Starlight Moonlight Candlelight Office Overcast Day Direct Sun
(Night) (Twilight) (Dim Room) (Fluorescent) (Outdoors) (Midday Glare)
└─── 470kΩ ───┴─── 100kΩ ────┴──── 10kΩ ──────┴───── 1kΩ ────┴──── 220Ω ─────────┘
- High Gain (470 kΩ / 100 kΩ): Resolves nanoamp-level photocurrents, measuring twilight and moonlight down to 0.01 lux.
- Low Gain (1 kΩ / 220 Ω): Sinks several milliamps, measuring direct midday solar illumination (120,000+ lux) without clipping.
2. High-Speed Flicker & PWM Ripple Analysis
Unlike digital I2C light sensor chips (which integrate readings over 100–200 ms and miss all flicker), a phototransistor on a 1k–10kΩ pull-up responds in 2–15 µs (>50 kHz bandwidth).
- Pico Sampling (50–100 kHz): Sample the ADC continuously in a tight loop or via DMA to capture the waveform of ambient light.
- Diagnostics: Directly measure 100/120 Hz mains hum from cheap fluorescent/magnetic ballasts and calculate the PWM frequency and flicker index of LED lightbulbs, computer monitors, and phone screens.
3. Matching the Human Eye (Photopic Curve V_λ)
- Silicon vs. Human Vision: Bare silicon phototransistors detect light from 350 nm to 1100 nm (peaking in near-IR at ~850 nm), while human vision follows the photopic curve (peaking at 555 nm green and dropping to zero in IR).
- Calibrated Lux: To achieve true human-eye lux matching, place an IR-Cut optical filter (blocking >700 nm) in front of the clear phototransistor, or use a dedicated ambient-light phototransistor with internal optical filtering (e.g., TEMT6000 / TEPT4400).
🧪 Laboratory Spectrophotometry & Chemical Colorimetry
A benchtop laboratory UV-Vis spectrophotometer or ELISA microplate reader operates on the exact same transmissive optical physics as this circuit. By pairing a clear-lens phototransistor with specific visible/UV LEDs across a 1 cm cuvette or transparent flow-tube, you can automate analytical chemistry assays using the Beer-Lambert Law:
A = -log10(I_sample / I_blank) = ε · c · l
- I_blank: ADC reading through pure water / blank buffer (100% transmission baseline).
- I_sample: ADC reading through colored reaction mixture.
- A (Absorbance): Strictly linear with chemical/protein concentration (c).
COMMERCIAL BENCHTOP SPECTROPHOTOMETER ($10,000)
┌──────────────┐ ┌─────────────┐ ┌───────────────┐ ┌─────────────┐ ┌─────────────┐
│ Broad Lamp / │ ───► │ Diffraction │ ───► │ 1cm Cuvette │ ───► │ Silicon │ ───► │ Transimped. │ ───► ADC / MCU
│ Pulsed Xenon │ │ Grating │ │ Sample Holder │ │ Photodiode │ │ Amplifier │ (-log10 Math)
└──────────────┘ └─────────────┘ └───────────────┘ └─────────────┘ └─────────────┘
▲
│
YOUR PICO RIG DOES THIS:
┌──────────────┐ ┌───────────────┐ ┌─────────────┐ ┌─────────────┐
│ Monochromatic│ ────────────────────────► │ 1cm Cuvette │ ───► │ Silicon │ ───► │ 10k Pull-Up │ ───► Pico ADC
│ Narrow LED │ │ Slot │ │ Clear PTX │ │ Divider │ (-log10 Math)
└──────────────┘ └───────────────┘ └─────────────┘ └─────────────┘
Popular Automated Assays
| Assay Type | Target Analyte | Reagent Color Shift | Recommended LED |
|---|---|---|---|
| Bradford / BCA Assay | Total Protein Concentration | Brown → Deep Blue (595 nm) | Amber/Orange LED (590–600 nm) |
| Glucose Oxidase (Trinder) | Blood / Beverage Glucose | Clear → Pink/Magenta (505 nm) | Green LED (520 nm) |
| OD600 Turbidity | Bacterial / Yeast Cell Density | Clear → Scattering Broth | Orange/Red LED (600 nm) |
| Molybdenum Blue | Phosphate / Nutrient Runoff | Clear → Deep Blue (650–880 nm) | Red (660 nm) or 850nm IR LED |
| Griess Reagent | Nitrate / Nitrite in Water | Clear → Bright Pink (540 nm) | Green LED (525 nm) |
| Enzyme Kinetics (ONPG) | β-Galactosidase Activity | Clear → Yellow (420 nm) | Royal Blue LED (430 nm) |
Real-Time Reaction Kinetics
By polling absorbance once per second, firmware can track live reaction slopes (dA / dt), compute initial velocity (V_0), solve for Michaelis-Menten constants (K_m, V_max), and detect reaction endpoints automatically.
🛠️ Usage with CMake
Add the driver directory into your project:
add_subdirectory(lib/lf-phototransistor)
target_link_libraries(your_project PRIVATE
lf-phototransistor
)
Include the header in your C/C++ source:
#include "lf_phototransistor.h"
⚙️ Driver API Description
typedef struct {
uint adc_pin;
uint adc_channel;
int tx_led_pin;
bool active_low;
uint oversample_count;
uint settle_us;
uint16_t threshold_raw;
uint16_t hysteresis;
bool last_trigger_state;
} lf_phototransistor_t;
Main API Functions:
bool lf_phototransistor_init(lf_phototransistor_t *dev, uint adc_pin, int tx_led_pin, bool active_low)
Initializes ADC hardware, sets up optional TX LED GPIO, and sets default parameters.void lf_phototransistor_set_oversample(lf_phototransistor_t *dev, uint samples)
Sets the number of ADC samples averaged per reading (default: 32).uint16_t lf_phototransistor_read_raw(lf_phototransistor_t *dev)
Returns the raw averaged 12-bit ADC value (0..4095).float lf_phototransistor_read_voltage(lf_phototransistor_t *dev)
Returns the measured voltage (0.0V .. 3.3V).float lf_phototransistor_read_intensity(lf_phototransistor_t *dev)
Returns normalized light/reflectance intensity from 0.0f (dark) to 1.0f (saturated).int16_t lf_phototransistor_read_ambient_corrected(lf_phototransistor_t *dev)
Pulses the TX LED on/off to measure and subtract background ambient light.bool lf_phototransistor_is_triggered(lf_phototransistor_t *dev)
Checks if light/proximity exceeds threshold with hysteresis.
🚀 Standalone Build & Run (Demo)
-
Configure with CMake for Pico 2:
mkdir -p build && cd build cmake -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 .. -
Compile:
make -j$(nproc) -
Flash: Flash
phototransistor_demo.elfvia OpenOCD / SWD or loadphototransistor_demo.uf2in BOOTSEL mode.