Skip to content

Commit 09f5343

Browse files
moffa90claude
andcommitted
fix: correct PWM output mode register logic and add RPM noise filtering
- Fix PWM output config (0x2B): 0=open-drain (default), 1=push-pull Previous implementation had this inverted - Add MIN_VALID_RPM_READING constant (200 RPM) to filter tach noise - Filter RPM readings below threshold as stopped fan (return 0) - Add PWM output mode API: set_pwm_output_mode(), set_all_pwm_output_mode(), get_pwm_output_mode() - Add PWM polarity API: set_pwm_polarity(), get_pwm_polarity() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 9e5dfb3 commit 09f5343

File tree

4 files changed

+200
-4
lines changed

4 files changed

+200
-4
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Nothing yet
1212

13+
## [1.1.0] - 2025-11-25
14+
15+
### Added
16+
- `set_pwm_output_mode()` - Configure PWM output as open-drain or push-pull per channel
17+
- `set_all_pwm_output_mode()` - Configure PWM output mode for all channels at once
18+
- `get_pwm_output_mode()` - Read current PWM output mode for a channel
19+
- `set_pwm_polarity()` - Configure PWM polarity (normal/inverted) per channel
20+
- `get_pwm_polarity()` - Read current PWM polarity for a channel
21+
- `MIN_VALID_RPM_READING` constant (200 RPM) for tachometer noise filtering
22+
23+
### Fixed
24+
- **Critical**: PWM output mode register (0x2B) logic was inverted. Per datasheet: 0=open-drain (default), 1=push-pull. Previous implementation had this backwards.
25+
- RPM readings below 200 RPM now return 0 (filters tachometer noise when fan is stopped)
26+
27+
## [1.0.0] - 2025-11-25
28+
29+
### Added
30+
- PyPI publishing workflow with trusted publishing (OIDC)
31+
- Donation section in README
32+
33+
### Changed
34+
- Package published to PyPI as `microchip-emc2305`
35+
- Removed phase/roadmap references from documentation
36+
1337
## [0.1.0] - 2025-11-24
1438

1539
### Added
@@ -64,5 +88,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6488
- Contributing guidelines
6589
- MIT License
6690

67-
[Unreleased]: https://github.com/moffa90/python-emc2305/compare/v0.1.0...HEAD
91+
[Unreleased]: https://github.com/moffa90/python-emc2305/compare/v1.1.0...HEAD
92+
[1.1.0]: https://github.com/moffa90/python-emc2305/compare/v1.0.0...v1.1.0
93+
[1.0.0]: https://github.com/moffa90/python-emc2305/compare/v0.1.0...v1.0.0
6894
[0.1.0]: https://github.com/moffa90/python-emc2305/releases/tag/v0.1.0

emc2305/driver/constants.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,16 @@
9797
DEFAULT_PWM_POLARITY = 0x00
9898
"""Default PWM polarity configuration (normal/non-inverted for all channels)"""
9999

100-
DEFAULT_PWM_OUTPUT_CONFIG = 0x1F
101-
"""Default PWM output configuration (open-drain for all channels) - Recommended by electronics engineer for better signal integrity"""
100+
DEFAULT_PWM_OUTPUT_CONFIG = 0x00
101+
"""Default PWM output configuration (open-drain for all channels, per datasheet default)"""
102+
103+
# PWM Output Type Constants (Register 0x2B bit values)
104+
# Datasheet: '0' (default) = open drain, '1' = push-pull
105+
PWM_OUTPUT_OPEN_DRAIN = 0
106+
"""Open-drain output mode (default, requires external pull-up resistor)"""
107+
108+
PWM_OUTPUT_PUSH_PULL = 1
109+
"""Push-pull output mode (active drive high and low)"""
102110

103111
FAN_INTERRUPT_ENABLE_ALL_FANS = 0x1F
104112
"""Enable interrupts for all 5 fan channels (bits 4-0 set)"""
@@ -444,6 +452,9 @@
444452
MIN_RPM = 500
445453
"""Minimum supported RPM (hardware limit)"""
446454

455+
MIN_VALID_RPM_READING = 200
456+
"""RPM readings below this threshold are considered noise (fan stopped)"""
457+
447458
MAX_RPM = 16000
448459
"""Maximum supported RPM with internal clock (500-16k range)"""
449460

emc2305/driver/emc2305.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,161 @@ def set_pwm_duty_cycle_verified(
936936

937937
return (False, actual)
938938

939+
def set_pwm_output_mode(self, channel: int, open_drain: bool = True) -> None:
940+
"""
941+
Set PWM output mode for a specific fan channel.
942+
943+
Args:
944+
channel: Fan channel number (1-5)
945+
open_drain: True for open-drain (default), False for push-pull
946+
947+
Note:
948+
- Open-drain (default): Requires external pull-up resistor. Better for
949+
level shifting and shared bus scenarios.
950+
- Push-pull: Active drive high and low. No external pull-up needed.
951+
952+
Example:
953+
>>> fan_controller.set_pwm_output_mode(1, open_drain=True) # Open-drain
954+
>>> fan_controller.set_pwm_output_mode(1, open_drain=False) # Push-pull
955+
"""
956+
self._validate_channel(channel)
957+
958+
with self._lock:
959+
try:
960+
# Read current config
961+
current = self.i2c_bus.read_byte(self.address, const.REG_PWM_OUTPUT_CONFIG)
962+
963+
# Per datasheet: 0 = open-drain (default), 1 = push-pull
964+
bit_mask = 1 << (channel - 1)
965+
966+
if open_drain:
967+
new_config = current & ~bit_mask # Clear bit for open-drain
968+
else:
969+
new_config = current | bit_mask # Set bit for push-pull
970+
971+
self.i2c_bus.write_byte(self.address, const.REG_PWM_OUTPUT_CONFIG, new_config)
972+
logger.debug(
973+
f"Fan {channel} PWM output set to {'open-drain' if open_drain else 'push-pull'}"
974+
)
975+
976+
except I2CError as e:
977+
raise EMC2305Error(f"Failed to set PWM output mode for fan {channel}: {e}")
978+
979+
def set_all_pwm_output_mode(self, open_drain: bool = True) -> None:
980+
"""
981+
Set PWM output mode for all fan channels.
982+
983+
Args:
984+
open_drain: True for open-drain (default), False for push-pull
985+
986+
Example:
987+
>>> fan_controller.set_all_pwm_output_mode(open_drain=True) # All open-drain
988+
>>> fan_controller.set_all_pwm_output_mode(open_drain=False) # All push-pull
989+
"""
990+
with self._lock:
991+
try:
992+
# Per datasheet: 0 = open-drain, 1 = push-pull
993+
config = 0x00 if open_drain else 0x1F
994+
self.i2c_bus.write_byte(self.address, const.REG_PWM_OUTPUT_CONFIG, config)
995+
logger.debug(
996+
f"All PWM outputs set to {'open-drain' if open_drain else 'push-pull'}"
997+
)
998+
999+
except I2CError as e:
1000+
raise EMC2305Error(f"Failed to set PWM output mode: {e}")
1001+
1002+
def get_pwm_output_mode(self, channel: int) -> bool:
1003+
"""
1004+
Get PWM output mode for a specific fan channel.
1005+
1006+
Args:
1007+
channel: Fan channel number (1-5)
1008+
1009+
Returns:
1010+
True if open-drain, False if push-pull
1011+
1012+
Example:
1013+
>>> is_open_drain = fan_controller.get_pwm_output_mode(1)
1014+
>>> print("Open-drain" if is_open_drain else "Push-pull")
1015+
"""
1016+
self._validate_channel(channel)
1017+
1018+
with self._lock:
1019+
try:
1020+
config = self.i2c_bus.read_byte(self.address, const.REG_PWM_OUTPUT_CONFIG)
1021+
bit_mask = 1 << (channel - 1)
1022+
# Per datasheet: 0 = open-drain, 1 = push-pull
1023+
# Return True for open-drain (bit NOT set)
1024+
return not bool(config & bit_mask)
1025+
1026+
except I2CError as e:
1027+
raise EMC2305Error(f"Failed to read PWM output mode for fan {channel}: {e}")
1028+
1029+
def set_pwm_polarity(self, channel: int, inverted: bool = False) -> None:
1030+
"""
1031+
Set PWM polarity for a specific fan channel.
1032+
1033+
Args:
1034+
channel: Fan channel number (1-5)
1035+
inverted: True for inverted (100% = fan off), False for normal (100% = fan max)
1036+
1037+
Note:
1038+
- Normal polarity (default): 0% duty = fan off, 100% duty = fan max speed
1039+
- Inverted polarity: 0% duty = fan max speed, 100% duty = fan off
1040+
Check your fan's datasheet to determine the correct polarity.
1041+
1042+
Example:
1043+
>>> fan_controller.set_pwm_polarity(1, inverted=False) # Normal
1044+
>>> fan_controller.set_pwm_polarity(1, inverted=True) # Inverted
1045+
"""
1046+
self._validate_channel(channel)
1047+
1048+
with self._lock:
1049+
try:
1050+
# Read current config
1051+
current = self.i2c_bus.read_byte(self.address, const.REG_PWM_POLARITY_CONFIG)
1052+
1053+
# Each channel is 1 bit: 0 = normal, 1 = inverted
1054+
bit_mask = 1 << (channel - 1)
1055+
1056+
if inverted:
1057+
new_config = current | bit_mask
1058+
else:
1059+
new_config = current & ~bit_mask
1060+
1061+
self.i2c_bus.write_byte(self.address, const.REG_PWM_POLARITY_CONFIG, new_config)
1062+
logger.debug(
1063+
f"Fan {channel} PWM polarity set to {'inverted' if inverted else 'normal'}"
1064+
)
1065+
1066+
except I2CError as e:
1067+
raise EMC2305Error(f"Failed to set PWM polarity for fan {channel}: {e}")
1068+
1069+
def get_pwm_polarity(self, channel: int) -> bool:
1070+
"""
1071+
Get PWM polarity for a specific fan channel.
1072+
1073+
Args:
1074+
channel: Fan channel number (1-5)
1075+
1076+
Returns:
1077+
True if inverted, False if normal
1078+
1079+
Example:
1080+
>>> is_inverted = fan_controller.get_pwm_polarity(1)
1081+
>>> print("Inverted" if is_inverted else "Normal")
1082+
"""
1083+
self._validate_channel(channel)
1084+
1085+
with self._lock:
1086+
try:
1087+
config = self.i2c_bus.read_byte(self.address, const.REG_PWM_POLARITY_CONFIG)
1088+
bit_mask = 1 << (channel - 1)
1089+
return bool(config & bit_mask)
1090+
1091+
except I2CError as e:
1092+
raise EMC2305Error(f"Failed to read PWM polarity for fan {channel}: {e}")
1093+
9391094
# =============================================================================
9401095
# Public API - RPM Control (FSC Mode)
9411096
# =============================================================================
@@ -1060,6 +1215,10 @@ def get_current_rpm(self, channel: int) -> int:
10601215
config = self._fan_configs.get(channel, FanConfig())
10611216
rpm = self._tach_count_to_rpm(tach_count, config.edges)
10621217

1218+
# Filter noise - readings below threshold mean fan is stopped
1219+
if rpm < const.MIN_VALID_RPM_READING:
1220+
return 0
1221+
10631222
return rpm
10641223

10651224
except I2CError as e:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "microchip-emc2305"
7-
version = "1.0.0"
7+
version = "1.1.0"
88
description = "Python driver for Microchip EMC2305 5-channel PWM fan controller"
99
readme = "README.md"
1010
authors = [

0 commit comments

Comments
 (0)