Explore Power Factor Correction (PFC) with this experimental Homatica project by Claudio Cabete. Using Raspberry Pi Pico W, learn zero-cross detection, PWM shaping, and voltage feedback to optimize AC power draw in a DIY setup.
Power supplies often draw electricity in short, inefficient bursts, stressing the grid and requiring oversized wiring. I wanted to understand Power Factor Correction (PFC)—a technique to smooth power draw—by building a controller from scratch. This project is a learning journey, using the Pico W’s capabilities to experiment with real-time AC synchronization and efficient power management. It’s part of Homatica’s mission to explore DIY home automation with curiosity and open-source tools.
Power Factor Correction (PFC) makes electrical devices draw power smoothly, reducing waste and grid stress. Instead of erratic bursts, PFC aligns power draw with the AC waveform, lowering peak current and enabling thinner wires. Think of it as turning chaotic water splashes into a steady stream—better for efficiency and infrastructure. This project experiments with PFC using a Pico W to control AC power draw in real time.
The system detects the AC voltage’s zero-crossing point using an opto-isolator circuit, syncing the Pico W with the AC sine wave. A precomputed Lookup Table (LUT) shapes PWM output to mimic a sinusoidal power draw, reducing harmonic distortion. An interrupt-driven update_pwm()
function, optimized with @micropython.viper
, updates the PWM duty cycle in real time. A secondary PWM on Pin(19) acts as a sampling clock for precise timing.
A voltage feedback loop adjusts the duty cycle dynamically, ensuring efficient power draw. Explore the code on GitHub.
@micropython.viper
for sub-millisecond control.import machine import utime import math import array import micropython # Zero-cross detection zc_pin = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_DOWN) last_zc_time = 0 def zc_callback(pin): global last_zc_time last_zc_time = utime.ticks_us() print("Zero-cross detected!") zc_pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=zc_callback) # Lookup table for PWM shaping lut_items = 200 lut = array.array('H', [0] * lut_items) for n in range(lut_items): theta = n * math.pi / (lut_items / 2) duty = 0.25 + (0.5 - 0.25) * (1 - math.sin(theta)) lut[n] = int(duty * 65535) # PWM setup pwm = machine.PWM(machine.Pin(19)) pwm.freq(20000) # 20 kHz sampling clock lut_index = 0 @micropython.viper def update_pwm(pin): global lut_index pwm.duty_u16(lut[lut_index]) lut_index = (lut_index + 1) % len(lut) # IRQ setup pfc_sense_pin = machine.Pin(16, machine.Pin.IN) pfc_sense_pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=update_pwm) # Voltage feedback fbl = machine.ADC(machine.Pin(26)) target_voltage = 3.3 k_p = 0.1 duty_scale = 100 def feedback_loop(): voltage = 3.3 - (fbl.read_u16() * 3.3 / 65535) error = target_voltage - voltage global duty_scale duty_scale += int(k_p * error) duty_scale = max(10, min(duty_scale, 1000))
I’m refining the feedback loop for better stability and planning to add Home Assistant integration for real-time monitoring. A full schematic and tutorial are coming—stay tuned on my blog!
Want to try this? Visit the GitHub repo or discuss on r/homeautomation.