Skip to content

Commit c04841d

Browse files
committed
added hardware pwm
1 parent 474ddd9 commit c04841d

2 files changed

Lines changed: 232 additions & 4 deletions

File tree

openscan_firmware/controllers/hardware/gpio.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from gpiozero import DigitalOutputDevice, PWMOutputDevice, Button
44
from typing import Dict, List, Optional, Callable
55

6+
# hardware PWM module
7+
from openscan_firmware.utils.pwm_hardware import hwpwm
8+
69
logger = logging.getLogger(__name__)
710

811
# Track pins and buttons
@@ -66,8 +69,14 @@ def initialize_pwm_pins(pins: List[int], freq: int):
6669
logger.error(f"Error: Cannot initialize pin {pin} as PWM. Already initialized as button.")
6770
else:
6871
try:
69-
_pwm_pins[pin] = PWMOutputDevice(pin, active_high=True, initial_value=0.0, frequency=freq)
70-
logger.debug(f"Initialized pin {pin} as PWM.")
72+
if hwpwm.supports(pin):
73+
_pwm_pins[pin] = pin
74+
hwpwm.setup(pin)
75+
hwpwm.set_frequency(pin, freq)
76+
logger.info(f"Initialized pin {pin} as hardware PWM.")
77+
else:
78+
_pwm_pins[pin] = PWMOutputDevice(pin, active_high=True, initial_value=0.0, frequency=freq)
79+
logger.info(f"Initialized pin {pin} as software PWM.")
7180
except Exception as e:
7281
logger.error(f"Error initializing PWM pin {pin}: {e}", exc_info=True)
7382
# Clean up if initialization failed partially
@@ -77,14 +86,30 @@ def initialize_pwm_pins(pins: List[int], freq: int):
7786
def set_pwm_pin(pin: int, value: float):
7887
"""Sets the value of a PWM pin."""
7988
if pin in _pwm_pins:
80-
_pwm_pins[pin].value = value
89+
dev = _pwm_pins[pin]
90+
91+
# on hw pwm we store just pin number here, not the device
92+
if isinstance(dev, int):
93+
# hw pwm
94+
hwpwm.set_duty_cycle(dev, value)
95+
else:
96+
# soft pwm
97+
_pwm_pins[pin].value = value
8198
else:
8299
logger.warning(f"Warning: Cannot set pin {pin}. Not initialized as PWM.")
83100

84101
def get_pwm_pin(pin: int):
85102
"""Returns the state of an output pin."""
86103
if pin in _pwm_pins:
87-
return _pwm_pins[pin].value
104+
dev = _pwm_pins[pin]
105+
106+
# on hw pwm we store just pin number here, not the device
107+
if isinstance(dev, int):
108+
# hw pwm
109+
return hwpwm.get_duty_cycle(dev)
110+
else:
111+
# soft pwm
112+
return _pwm_pins[pin].value
88113
else:
89114
logger.warning(f"Warning: Pin {pin} not initialized as PWM.")
90115
return None
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import subprocess
2+
from pathlib import Path
3+
4+
from dataclasses import dataclass
5+
6+
import atexit
7+
import signal
8+
import sys
9+
10+
@dataclass
11+
class _HwPWM:
12+
13+
_PWMCHIP = Path("/sys/class/pwm/pwmchip0")
14+
15+
_PIN_INFO = {
16+
12: {"channel": 0, "alt": "a0"},
17+
18: {"channel": 0, "alt": "a5"},
18+
13: {"channel": 1, "alt": "a0"},
19+
19: {"channel": 1, "alt": "a5"},
20+
}
21+
22+
_pins = {}
23+
24+
# register cleanup at exit
25+
def __init__(self):
26+
atexit.register(_HwPWM._cleanup)
27+
signal.signal(signal.SIGTERM, _HwPWM._signal_handler)
28+
signal.signal(signal.SIGINT, _HwPWM._signal_handler)
29+
30+
@staticmethod
31+
def _run(cmd):
32+
result = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout
33+
try:
34+
return result.split(":", 1)[1].split()[0]
35+
except:
36+
return ""
37+
38+
@staticmethod
39+
def _pwm_path(channel):
40+
return _HwPWM._PWMCHIP / f"pwm{channel}"
41+
42+
43+
@staticmethod
44+
def _write(path, value):
45+
path.write_text(str(value))
46+
47+
48+
@staticmethod
49+
def _export(channel):
50+
p = _HwPWM._pwm_path(channel)
51+
if not p.exists():
52+
(_HwPWM._PWMCHIP / "export").write_text(str(channel))
53+
54+
55+
@staticmethod
56+
def _unexport(channel):
57+
p = _HwPWM._pwm_path(channel)
58+
if p.exists():
59+
(_HwPWM._PWMCHIP / "unexport").write_text(str(channel))
60+
61+
62+
@staticmethod
63+
def supports(pin: int):
64+
# first check if pin is a supported one
65+
if not pin in _HwPWM._PIN_INFO:
66+
return False
67+
68+
# then check if its PWM is not already in use
69+
chan = _HwPWM._PIN_INFO[pin]["channel"]
70+
for p in _HwPWM._pins.keys():
71+
# harmless re-set already set pin
72+
if p == pin:
73+
return True
74+
# if using same channel as already setup pin don't accept it
75+
if chan == _HwPWM._PIN_INFO[p]["channel"]:
76+
return False
77+
78+
# available PWM pin and channel not used, ok
79+
return True
80+
81+
@staticmethod
82+
def setup(pin: int):
83+
if not _HwPWM.supports(pin):
84+
raise ValueError("unsupported pin or pwm channel in use")
85+
86+
info = _HwPWM._PIN_INFO[pin]
87+
ch = info["channel"]
88+
89+
# configure pin mux
90+
old_func = _HwPWM._run(["pinctrl", str(pin)])
91+
_HwPWM._run(["pinctrl", str(pin), info["alt"]])
92+
93+
# enable pwm channel
94+
_HwPWM._export(ch)
95+
96+
pwm = _HwPWM._pwm_path(ch)
97+
98+
# ensure disabled before configuration
99+
try:
100+
_HwPWM._write(pwm / "enable", 0)
101+
except:
102+
pass
103+
104+
_HwPWM._pins[pin] = { "freq": 20000.0, "duty": 1.0, "oldfunc": old_func }
105+
106+
107+
@staticmethod
108+
def release(pin: int):
109+
if not pin in _HwPWM._pins:
110+
return
111+
112+
info = _HwPWM._PIN_INFO[pin]
113+
ch = info["channel"]
114+
115+
pwm = _HwPWM._pwm_path(ch)
116+
117+
if pwm.exists():
118+
try:
119+
_HwPWM.write(pwm / "enable", 0)
120+
except:
121+
pass
122+
123+
# return pin to input
124+
_HwPWM._run(["pinctrl", str(pin), _HwPWM._pins[pin]["oldfunc"]])
125+
126+
del _HwPWM._pins[pin]
127+
128+
@staticmethod
129+
def _set_freq_duty(pin: int, freq: float, duty: float):
130+
131+
info = _HwPWM._PIN_INFO[pin]
132+
ch = info["channel"]
133+
134+
pwm = _HwPWM._pwm_path(ch)
135+
136+
period_ns = int(1_000_000_000 / freq)
137+
duty_val = int(period_ns * duty)
138+
139+
_HwPWM._write(pwm / "enable", 0)
140+
_HwPWM._write(pwm / "period", period_ns)
141+
_HwPWM._write(pwm / "duty_cycle", duty_val)
142+
_HwPWM._write(pwm / "enable", 1)
143+
144+
_HwPWM._pins[pin]["freq"] = freq
145+
_HwPWM._pins[pin]["duty"] = duty
146+
147+
@staticmethod
148+
def set_frequency(pin: int, freq: float):
149+
if not pin in _HwPWM._pins:
150+
raise ValueError("pwm pin not initialized")
151+
152+
info = _HwPWM._PIN_INFO[pin]
153+
ch = info["channel"]
154+
155+
duty = _HwPWM._pins[pin]["duty"]
156+
_HwPWM._set_freq_duty(pin, freq, duty)
157+
158+
@staticmethod
159+
def get_frequency(pin: int):
160+
if not pin in _HwPWM._pins:
161+
raise ValueError("pwm pin not initialized")
162+
163+
return _HwPWM._pins[pin]["freq"]
164+
165+
@staticmethod
166+
def set_duty_cycle(pin: int, duty: float):
167+
if not pin in _HwPWM._pins:
168+
raise ValueError("pwm pin not initialized")
169+
170+
info = _HwPWM._PIN_INFO[pin]
171+
ch = info["channel"]
172+
173+
freq = _HwPWM._pins[pin]["freq"]
174+
_HwPWM._set_freq_duty(pin, freq, duty)
175+
176+
177+
@staticmethod
178+
def get_duty_cycle(pin: int):
179+
if not pin in _HwPWM._pins:
180+
raise ValueError("pwm pin not initialized")
181+
182+
return _HwPWM._pins[pin]["duty"]
183+
184+
# cleanup routines -- resets PWM pins
185+
186+
@staticmethod
187+
def _cleanup():
188+
to_clean = []
189+
for pin in _HwPWM._pins.keys():
190+
to_clean.append(pin)
191+
for pin in to_clean:
192+
_HwPWM.release(pin)
193+
194+
def _signal_handler(signum, frame):
195+
_HwPWM._cleanup()
196+
197+
198+
# ==========================================================
199+
# SINGLETON
200+
# ==========================================================
201+
202+
# hardware pw, singleton
203+
hwpwm = _HwPWM()

0 commit comments

Comments
 (0)