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
146 # Cache the period for fast duty cycle updates
147 self._period_ns = self._get_period_ns()
151 if self._channel is not None:
152 # Unexporting the PWM channel
154 unexport_fd = os.open(
155 os.path.join(self._chip_path, "unexport"), os.O_WRONLY
157 os.write(unexport_fd, "{:d}\n".format(self._channel).encode())
158 os.close(unexport_fd)
160 raise PWMError(e.errno, "Unexporting PWM: " + e.strerror) from OSError
165 def _write_channel_attr(self, attr, value):
167 os.path.join(self._channel_path, attr), "w", encoding="utf-8"
169 f_attr.write(value + "\n")
171 def _read_channel_attr(self, attr):
173 os.path.join(self._channel_path, attr), "r", encoding="utf-8"
175 return f_attr.read().strip()
180 """Enable the PWM output."""
184 """Disable the PWM output."""
189 def _get_period(self):
190 return float(self.period_ms) / 1000
192 def _set_period(self, period):
193 if not isinstance(period, (int, float)):
194 raise TypeError("Invalid period type, should be int.")
196 self.period_ms = int(period * 1000)
198 period = property(_get_period, _set_period)
199 """Get or set the PWM's output period in seconds.
202 PWMError: if an I/O or OS error occurs.
203 TypeError: if value type is not int.
208 def _get_period_ms(self):
209 return self.period_us / 1000
211 def _set_period_ms(self, period_ms):
212 if not isinstance(period_ms, (int, float)):
213 raise TypeError("Invalid period type, should be int or float.")
214 self.period_us = int(period_ms * 1000)
216 period_ms = property(_get_period_ms, _set_period_ms)
217 """Get or set the PWM's output period in milliseconds.
220 PWMError: if an I/O or OS error occurs.
221 TypeError: if value type is not int.
226 def _get_period_us(self):
227 return self.period_ns / 1000
229 def _set_period_us(self, period_us):
230 if not isinstance(period_us, int):
231 raise TypeError("Invalid period type, should be int.")
233 self.period_ns = int(period_us * 1000)
235 period_us = property(_get_period_us, _set_period_us)
236 """Get or set the PWM's output period in microseconds.
239 PWMError: if an I/O or OS error occurs.
240 TypeError: if value type is not int.
245 def _get_period_ns(self):
246 period_ns = self._read_channel_attr("period")
248 period_ns = int(period_ns)
251 None, 'Unknown period value: "%s".' % period_ns
254 self._period_ns = period_ns
258 def _set_period_ns(self, period_ns):
259 if not isinstance(period_ns, int):
260 raise TypeError("Invalid period type, should be int.")
262 self._write_channel_attr("period", str(period_ns))
264 # Update our cached period
265 self._period_ns = period_ns
267 period_ns = property(_get_period_ns, _set_period_ns)
268 """Get or set the PWM's output period in nanoseconds.
271 PWMError: if an I/O or OS error occurs.
272 TypeError: if value type is not int.
277 def _get_duty_cycle_ns(self):
278 duty_cycle_ns_str = self._read_channel_attr("duty_cycle")
281 duty_cycle_ns = int(duty_cycle_ns_str)
284 None, 'Unknown duty cycle value: "{:s}"'.format(duty_cycle_ns_str)
289 def _set_duty_cycle_ns(self, duty_cycle_ns):
290 if not isinstance(duty_cycle_ns, int):
291 raise TypeError("Invalid duty cycle type, should be int.")
293 self._write_channel_attr("duty_cycle", str(duty_cycle_ns))
295 duty_cycle_ns = property(_get_duty_cycle_ns, _set_duty_cycle_ns)
296 """Get or set the PWM's output duty cycle in nanoseconds.
299 PWMError: if an I/O or OS error occurs.
300 TypeError: if value type is not int.
305 def _get_duty_cycle(self):
306 return float(self.duty_cycle_ns) / self._period_ns
308 def _set_duty_cycle(self, duty_cycle):
309 if not isinstance(duty_cycle, (int, float)):
310 raise TypeError("Invalid duty cycle type, should be int or float.")
312 if not 0.0 <= duty_cycle <= 1.0:
313 raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.")
315 # Convert duty cycle from ratio to nanoseconds
316 self.duty_cycle_ns = int(duty_cycle * self._period_ns)
318 duty_cycle = property(_get_duty_cycle, _set_duty_cycle)
319 """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0.
321 PWMError: if an I/O or OS error occurs.
322 TypeError: if value type is not int or float.
323 ValueError: if value is out of bounds of 0.0 to 1.0.
327 def _get_frequency(self):
328 return 1.0 / self.period
330 def _set_frequency(self, frequency):
331 if not isinstance(frequency, (int, float)):
332 raise TypeError("Invalid frequency type, should be int or float.")
334 self.period = 1.0 / frequency
336 frequency = property(_get_frequency, _set_frequency)
337 """Get or set the PWM's output frequency in Hertz.
339 PWMError: if an I/O or OS error occurs.
340 TypeError: if value type is not int or float.
344 def _get_polarity(self):
345 return self._read_channel_attr("polarity")
347 def _set_polarity(self, polarity):
348 if not isinstance(polarity, str):
349 raise TypeError("Invalid polarity type, should be str.")
351 if polarity.lower() not in ["normal", "inversed"]:
352 raise ValueError('Invalid polarity, can be: "normal" or "inversed".')
354 self._write_channel_attr("polarity", polarity.lower())
356 polarity = property(_get_polarity, _set_polarity)
357 """Get or set the PWM's output polarity. Can be "normal" or "inversed".
359 PWMError: if an I/O or OS error occurs.
360 TypeError: if value type is not str.
361 ValueError: if value is invalid.
365 def _get_enabled(self):
366 enabled = self._read_channel_attr("enable")
373 raise PWMError(None, 'Unknown enabled value: "{:s}"'.format(enabled))
375 def _set_enabled(self, value):
376 if not isinstance(value, bool):
377 raise TypeError("Invalid enabled type, should be bool.")
379 self._write_channel_attr("enable", "1" if value else "0")
381 enabled = property(_get_enabled, _set_enabled)
382 """Get or set the PWM's output enabled state.
384 PWMError: if an I/O or OS error occurs.
385 TypeError: if value type is not bool.
389 # String representation
393 "PWM {:d}, chip {:d} (period={:f} sec, duty_cycle={:f}%,"
394 " polarity={:s}, enabled={:s})".format(
398 self.duty_cycle * 100,