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.