1 # SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
5 Much code from https://github.com/vsergeev/python-periphery/blob/master/periphery/pwm.py
6 Copyright (c) 2015-2016 vsergeev / Ivan (Vanya) A. Sergeev
11 from time import sleep
12 from errno import EACCES
15 from microcontroller.pin import pwmOuts
17 raise RuntimeError("No PWM outputs defined for this board.") from ImportError
20 # pylint: disable=unnecessary-pass, too-many-instance-attributes
23 class PWMError(IOError):
24 """Base class for PWM errors."""
29 # pylint: enable=unnecessary-pass
33 """Pulse Width Modulation Output Class"""
35 # Number of retries to check for successful PWM export on open
37 # Delay between check for successful PWM export on open (100ms)
41 _chip_path = "pwmchip{}"
42 _channel_path = "pwm{}"
44 def __init__(self, pwm, *, frequency=500, duty_cycle=0, variable_frequency=False):
45 """Instantiate a PWM object and open the sysfs PWM corresponding to the
46 specified chip and channel.
49 frequency (int, float): target frequency in Hertz (32-bit).
50 duty_cycle (int, float): The fraction of each pulse which is high (16-bit).
51 variable_frequency (bool): True if the frequency will change over time.
55 PWMError: if an I/O or OS error occurs.
56 TypeError: if `chip` or `channel` types are invalid.
57 LookupError: if PWM chip does not exist.
58 TimeoutError: if waiting for PWM export times out.
64 self._open(pwm, frequency, duty_cycle, variable_frequency)
72 def __exit__(self, exc_type, exc_val, exc_tb):
75 def _open(self, pwm, frequency, duty_cycle, variable_frequency):
76 for pwmout in pwmOuts:
78 self._chip = pwmout[0][0]
79 self._channel = pwmout[0][1]
81 self._chip_path = os.path.join(
82 "/sys/class/pwm", self._chip_path.format(self._chip)
84 self._channel_path = os.path.join(
85 self._chip_path, self._channel_path.format(self._channel)
88 if variable_frequency:
89 print("Variable Frequency is not supported, continuing without it...")
91 if not os.path.isdir(self._chip_path):
92 raise LookupError("Opening PWM: PWM chip {} not found.".format(self._chip))
94 if not os.path.isdir(self._channel_path):
98 os.path.join(self._chip_path, "export"), "w", encoding="utf-8"
100 f_export.write("{:d}\n".format(self._channel))
103 e.errno, "Exporting PWM channel: " + e.strerror
106 # Loop until PWM is exported
108 for i in range(PWMOut.PWM_STAT_RETRIES):
109 if os.path.isdir(self._channel_path):
113 sleep(PWMOut.PWM_STAT_DELAY)
117 'Exporting PWM: waiting for "{:s}" timed out.'.format(
122 # Loop until 'period' is writable, This could take some time after
123 # export as application of the udev rules after export is asynchronous.
124 # Without this loop, the following properties may not be writable yet.
125 for i in range(PWMOut.PWM_STAT_RETRIES):
128 os.path.join(self._channel_path, "period"),
134 if e.errno != EACCES or (
135 e.errno == EACCES and i == PWMOut.PWM_STAT_RETRIES - 1
138 e.errno, "Opening PWM period: " + e.strerror
141 sleep(PWMOut.PWM_STAT_DELAY)
143 self.frequency = frequency
144 self.duty_cycle = duty_cycle
145 self.polarity = "normal"
148 # Cache the period for fast duty cycle updates
149 self._period_ns = self._get_period_ns()
153 if self._channel is not None:
154 # Unexporting the PWM channel
156 unexport_fd = os.open(
157 os.path.join(self._chip_path, "unexport"), os.O_WRONLY
159 os.write(unexport_fd, "{:d}\n".format(self._channel).encode())
160 os.close(unexport_fd)
162 raise PWMError(e.errno, "Unexporting PWM: " + e.strerror) from OSError
167 def _write_channel_attr(self, attr, value):
169 os.path.join(self._channel_path, attr), "w", encoding="utf-8"
171 f_attr.write(value + "\n")
173 def _read_channel_attr(self, attr):
175 os.path.join(self._channel_path, attr), "r", encoding="utf-8"
177 return f_attr.read().strip()
182 """Enable the PWM output."""
186 """Disable the PWM output."""
191 def _get_period(self):
192 return float(self.period_ms) / 1000
194 def _set_period(self, period):
195 if not isinstance(period, (int, float)):
196 raise TypeError("Invalid period type, should be int.")
198 self.period_ms = int(period * 1000)
200 period = property(_get_period, _set_period)
201 """Get or set the PWM's output period in seconds.
204 PWMError: if an I/O or OS error occurs.
205 TypeError: if value type is not int.
210 def _get_period_ms(self):
211 return self.period_us / 1000
213 def _set_period_ms(self, period_ms):
214 if not isinstance(period_ms, (int, float)):
215 raise TypeError("Invalid period type, should be int or float.")
216 self.period_us = int(period_ms * 1000)
218 period_ms = property(_get_period_ms, _set_period_ms)
219 """Get or set the PWM's output period in milliseconds.
222 PWMError: if an I/O or OS error occurs.
223 TypeError: if value type is not int.
228 def _get_period_us(self):
229 return self.period_ns / 1000
231 def _set_period_us(self, period_us):
232 if not isinstance(period_us, int):
233 raise TypeError("Invalid period type, should be int.")
235 self.period_ns = int(period_us * 1000)
237 period_us = property(_get_period_us, _set_period_us)
238 """Get or set the PWM's output period in microseconds.
241 PWMError: if an I/O or OS error occurs.
242 TypeError: if value type is not int.
247 def _get_period_ns(self):
248 period_ns = self._read_channel_attr("period")
250 period_ns = int(period_ns)
253 None, 'Unknown period value: "%s".' % period_ns
256 self._period_ns = period_ns
260 def _set_period_ns(self, period_ns):
261 if not isinstance(period_ns, int):
262 raise TypeError("Invalid period type, should be int.")
264 self._write_channel_attr("period", str(period_ns))
266 # Update our cached period
267 self._period_ns = period_ns
269 period_ns = property(_get_period_ns, _set_period_ns)
270 """Get or set the PWM's output period in nanoseconds.
273 PWMError: if an I/O or OS error occurs.
274 TypeError: if value type is not int.
279 def _get_duty_cycle_ns(self):
280 duty_cycle_ns_str = self._read_channel_attr("duty_cycle")
283 duty_cycle_ns = int(duty_cycle_ns_str)
286 None, 'Unknown duty cycle value: "{:s}"'.format(duty_cycle_ns_str)
291 def _set_duty_cycle_ns(self, duty_cycle_ns):
292 if not isinstance(duty_cycle_ns, int):
293 raise TypeError("Invalid duty cycle type, should be int.")
295 self._write_channel_attr("duty_cycle", str(duty_cycle_ns))
297 duty_cycle_ns = property(_get_duty_cycle_ns, _set_duty_cycle_ns)
298 """Get or set the PWM's output duty cycle in nanoseconds.
301 PWMError: if an I/O or OS error occurs.
302 TypeError: if value type is not int.
307 def _get_duty_cycle(self):
308 return float(self.duty_cycle_ns) / self._period_ns
310 def _set_duty_cycle(self, duty_cycle):
311 if not isinstance(duty_cycle, (int, float)):
312 raise TypeError("Invalid duty cycle type, should be int or float.")
314 if not 0.0 <= duty_cycle <= 1.0:
315 raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.")
317 # Convert duty cycle from ratio to nanoseconds
318 self.duty_cycle_ns = int(duty_cycle * self._period_ns)
320 duty_cycle = property(_get_duty_cycle, _set_duty_cycle)
321 """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0.
323 PWMError: if an I/O or OS error occurs.
324 TypeError: if value type is not int or float.
325 ValueError: if value is out of bounds of 0.0 to 1.0.
329 def _get_frequency(self):
330 return 1.0 / self.period
332 def _set_frequency(self, frequency):
333 if not isinstance(frequency, (int, float)):
334 raise TypeError("Invalid frequency type, should be int or float.")
336 self.period = 1.0 / frequency
338 frequency = property(_get_frequency, _set_frequency)
339 """Get or set the PWM's output frequency in Hertz.
341 PWMError: if an I/O or OS error occurs.
342 TypeError: if value type is not int or float.
346 def _get_polarity(self):
347 return self._read_channel_attr("polarity")
349 def _set_polarity(self, polarity):
350 if not isinstance(polarity, str):
351 raise TypeError("Invalid polarity type, should be str.")
353 if polarity.lower() not in ["normal", "inversed"]:
354 raise ValueError('Invalid polarity, can be: "normal" or "inversed".')
356 self._write_channel_attr("polarity", polarity.lower())
358 polarity = property(_get_polarity, _set_polarity)
359 """Get or set the PWM's output polarity. Can be "normal" or "inversed".
361 PWMError: if an I/O or OS error occurs.
362 TypeError: if value type is not str.
363 ValueError: if value is invalid.
367 def _get_enabled(self):
368 enabled = self._read_channel_attr("enable")
375 raise PWMError(None, 'Unknown enabled value: "{:s}"'.format(enabled))
377 def _set_enabled(self, value):
378 if not isinstance(value, bool):
379 raise TypeError("Invalid enabled type, should be bool.")
381 self._write_channel_attr("enable", "1" if value else "0")
383 enabled = property(_get_enabled, _set_enabled)
384 """Get or set the PWM's output enabled state.
386 PWMError: if an I/O or OS error occurs.
387 TypeError: if value type is not bool.
391 # String representation
395 "PWM {:d}, chip {:d} (period={:f} sec, duty_cycle={:f}%,"
396 " polarity={:s}, enabled={:s})".format(
400 self.duty_cycle * 100,