Python Driver for Microchip EMC2305 5-Channel PWM Fan Controller
A hardware-agnostic, production-ready Python driver for the Microchip EMC2305 fan controller with comprehensive feature support and robust I2C communication.
- Chip: Microchip EMC2305-1, EMC2305-2, EMC2305-3, EMC2305-4 (5-channel variants)
- Interface: I2C/SMBus with cross-process locking
- Platform: Any Linux system with I2C support (Raspberry Pi, Banana Pi, x86, etc.)
- ✅ 5 independent PWM channels - Control up to 5 fans simultaneously
- ✅ Dual control modes:
- PWM Mode: Direct duty cycle control (0-100%)
- FSC Mode: Closed-loop RPM control with PID (500-32,000 RPM)
- ✅ Per-fan PWM frequency - Individual frequency control per channel
- ✅ Configurable spin-up - Aggressive start for high-inertia fans
- ✅ RPM monitoring - Real-time tachometer reading
- ✅ Fault detection: Stall, spin failure, aging fan detection
- ✅ SMBus Alert (ALERT#): Hardware interrupt support
- ✅ Software configuration lock - Protect settings in production (race-condition safe)
- ✅ Watchdog timer - Automatic failsafe
- ✅ Hardware capability detection - Auto-detect chip features
- ✅ Thread-safe operation - Concurrent access protection with atomic operations
- ✅ Comprehensive validation - I2C addresses (0x00-0x7F), registers (0x00-0xFF), SMBus block limits (32 bytes), and RPM bounds checking
- ✅ Full type hints (PEP 561)
- ✅ Comprehensive documentation
- ✅ Hardware-validated
- ✅ MIT licensed
pip install microchip-emc2305git clone https://github.com/moffa90/python-emc2305.git
cd emc2305-python
pip install -e .# For YAML configuration file support
pip install microchip-emc2305[config]
# For development
pip install microchip-emc2305[dev]from emc2305.driver.i2c import I2CBus
from emc2305.driver.emc2305 import EMC2305, FanConfig
# Initialize I2C bus (with cross-process locking)
i2c_bus = I2CBus(bus_number=0)
# Initialize EMC2305 at default address 0x4D
fan_controller = EMC2305(i2c_bus, device_address=0x4D)
# Configure tachometer for your fan type (IMPORTANT for accurate RPM!)
# edges=3 for 1-pulse/rev, edges=5 for 2-pulse/rev (default), edges=9 for 4-pulse/rev
config = FanConfig(edges=3) # Adjust based on your fan's tachometer
fan_controller.configure_fan(channel=1, config=config)
# Set fan 1 to 75% duty cycle
fan_controller.set_pwm_duty_cycle(channel=1, percent=75.0)
# Read current RPM
rpm = fan_controller.get_current_rpm(channel=1)
print(f"Fan 1 speed: {rpm} RPM")from emc2305.driver.emc2305 import EMC2305, ControlMode, FanConfig
# Configure for FSC mode
config = FanConfig(
control_mode=ControlMode.FSC,
min_rpm=1000,
max_rpm=4000,
pid_gain_p=4, # Proportional gain
pid_gain_i=2, # Integral gain
pid_gain_d=1, # Derivative gain
)
fan_controller.configure_fan(channel=1, config=config)
fan_controller.set_target_rpm(channel=1, rpm=3000)
# Hardware PID will maintain 3000 RPM automaticallyfrom emc2305.driver.emc2305 import FanStatus
# Check fan status
status = fan_controller.get_fan_status(channel=1)
if status == FanStatus.STALLED:
print("Fan 1 is stalled!")
elif status == FanStatus.DRIVE_FAILURE:
print("Fan 1 is aging (drive failure)")
elif status == FanStatus.OK:
print("Fan 1 is operating normally")# Enable alerts for fan 1
fan_controller.configure_fan_alerts(channel=1, enabled=True)
# Check if any alerts are active
if fan_controller.is_alert_active():
# Get which fans have alerts
alerts = fan_controller.get_alert_status()
for channel, has_alert in alerts.items():
if has_alert:
print(f"Fan {channel} has an alert condition")
# Clear alert status
fan_controller.clear_alert_status()The edges parameter is critical for accurate RPM readings. It must match your fan's tachometer signal:
| Fan Type | Pulses/Revolution | edges Setting |
|---|---|---|
| 1-pole | 1 | edges=3 |
| 2-pole | 2 | edges=5 (default) |
| 3-pole | 3 | edges=7 |
| 4-pole | 4 | edges=9 |
How to determine your fan's pulse count:
- Check the fan datasheet for "FG Signal" or "Tachometer" specification
- Or use trial and error: the correct setting gives RPM readings that scale linearly with PWM
from emc2305.driver.emc2305 import EMC2305, FanConfig
from emc2305.driver.i2c import I2CBus
bus = I2CBus(bus_number=0)
controller = EMC2305(i2c_bus=bus, device_address=0x4D)
# For a 1-pulse-per-revolution fan (common in high-speed fans)
config_1pole = FanConfig(edges=3)
controller.configure_fan(1, config_1pole)
# For a standard 2-pulse-per-revolution fan
config_2pole = FanConfig(edges=5)
controller.configure_fan(2, config_2pole)
# For a 4-pulse-per-revolution fan (some server fans)
config_4pole = FanConfig(edges=9)
controller.configure_fan(3, config_4pole)If your RPM readings seem wrong (too high, too low, or not scaling with PWM):
# Test different edges configurations
import time
controller.set_pwm_duty_cycle(1, 100) # Set to full speed
time.sleep(2) # Wait for fan to stabilize
for edges in [3, 5, 7, 9]:
config = FanConfig(edges=edges)
controller.configure_fan(1, config)
time.sleep(0.5)
rpm = controller.get_current_rpm(1)
print(f"edges={edges}: {rpm} RPM")
# The correct setting will show a reasonable RPM that matches
# your fan's rated speed at 100% PWM- Pull-up resistor: EMC2305 TACH pins are open-drain and require a 10kΩ pull-up to 3.3V
- Signal voltage: TACH signal should swing from 0V to VDD (typically 3.3V)
- Wiring: Connect fan's TACH wire to EMC2305's TACHx pin for the corresponding channel
The EMC2305 I2C address is configurable via the ADDR_SEL pin:
| ADDR_SEL | Address |
|---|---|
| GND | 0x4C |
| VDD | 0x4D |
| SDA | 0x5C |
| SCL | 0x5D |
| Float | 0x5E/0x5F |
Default in this driver: 0x61 (adjust for your hardware)
Ensure your user has I2C access:
# Add user to i2c group
sudo usermod -aG i2c $USER
# Or set permissions
sudo chmod 666 /dev/i2c-*# Install i2c-tools
sudo apt-get install i2c-tools
# Scan I2C bus 0
i2cdetect -y 0
# You should see your EMC2305 at its configured addressOptional YAML configuration support:
# ~/.config/emc2305/emc2305.yaml
i2c:
bus: 0
lock_enabled: true
emc2305:
address: 0x61
pwm_frequency_hz: 26000
fans:
1:
name: "CPU Fan"
control_mode: "fsc"
min_rpm: 1000
max_rpm: 4500
default_target_rpm: 3000
pid_gain_p: 4
pid_gain_i: 2
pid_gain_d: 1
2:
name: "Case Fan"
control_mode: "pwm"
default_duty_percent: 50Load configuration:
from emc2305.settings import ConfigManager
config_mgr = ConfigManager()
config = config_mgr.load()
# Use loaded configuration
fan_controller = EMC2305(
i2c_bus,
device_address=config.emc2305.address,
pwm_frequency=config.emc2305.pwm_frequency_hz
)┌─────────────────────────────────────┐
│ Application Code │
├─────────────────────────────────────┤
│ EMC2305 Driver (emc2305.py) │ ← High-level API
│ - Fan control │
│ - RPM monitoring │
│ - Fault detection │
├─────────────────────────────────────┤
│ I2C Communication (i2c.py) │ ← Low-level I/O
│ - SMBus operations │
│ - Cross-process locking │
├─────────────────────────────────────┤
│ Hardware (EMC2305 chip) │
└─────────────────────────────────────┘
Main driver class for fan control.
Methods:
set_pwm_duty_cycle(channel, percent)- Set PWM duty cycleset_target_rpm(channel, rpm)- Set target RPM (FSC mode)get_current_rpm(channel)- Read current RPMget_fan_status(channel)- Get fault statusconfigure_fan(channel, config)- Apply full configurationlock_configuration()- Lock settings (irreversible until reset)get_product_features()- Read hardware capabilities
Configuration dataclass for fan channels.
Fields:
control_mode: PWM or FSCmin_rpm,max_rpm: RPM limitsmin_drive_percent: Minimum PWM percentagepid_gain_p/i/d: PID tuning parametersspin_up_level_percent,spin_up_time_ms: Spin-up configurationpwm_divide: Per-fan PWM frequency divider
Low-level I2C communication with locking.
Methods:
read_byte(address, register)write_byte(address, register, value)read_block(address, register, length)write_block(address, register, data)
See examples/python/ directory:
test_fan_control.py- Basic PWM controltest_rpm_monitor.py- RPM monitoringtest_fsc_mode.py- Closed-loop controltest_fault_detection.py- Fault handling
# Run all tests
pytest tests/
# Run with coverage
pytest tests/ --cov=emc2305 --cov-report=html
# Run specific test
pytest tests/test_emc2305_init.py -vNote: Most tests require actual EMC2305 hardware.
- Python 3.9+
- Python 3.10+
- Python 3.11+
- Python 3.12+
- Linux (any distribution with I2C support)
- Raspberry Pi OS
- Banana Pi
- Generic embedded Linux
- I2C bus interface
- Microchip EMC2305 (any variant: EMC2305-1/2/3/4)
- Appropriate fan connectors and power supply
Contributions are welcome! This project aims to provide a comprehensive, hardware-agnostic driver for the EMC2305.
git clone https://github.com/moffa90/python-emc2305.git
cd emc2305-python
pip install -e ".[dev]"- Follow PEP 8
- Use type hints (PEP 484)
- Document all public APIs
- Run tests before submitting
MIT License - see LICENSE file for details.
Copyright (c) 2025 Contributors to the microchip-emc2305 project
- Issues: GitHub Issues
- Documentation: GitHub Wiki
- Discussions: GitHub Discussions
If you find this project useful, consider supporting its development:
This driver implements the complete EMC2305 register map and feature set as documented in the Microchip datasheet. Special thanks to the community contributors who helped validate and improve this driver.