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]
82 self._chip_path = os.path.join(
83 "/sys/class/pwm", self._chip_path.format(self._chip)
85 self._channel_path = os.path.join(
86 self._chip_path, self._channel_path.format(self._channel)
89 if variable_frequency:
90 print("Variable Frequency is not supported, continuing without it...")
92 if not os.path.isdir(self._chip_path):
93 raise LookupError("Opening PWM: PWM chip {} not found.".format(self._chip))
95 if not os.path.isdir(self._channel_path):
99 os.path.join(self._chip_path, "export"), "w", encoding="utf-8"
101 f_export.write("{:d}\n".format(self._channel))
104 e.errno, "Exporting PWM channel: " + e.strerror
107 # Loop until PWM is exported
109 for i in range(PWMOut.PWM_STAT_RETRIES):
110 if os.path.isdir(self._channel_path):
114 sleep(PWMOut.PWM_STAT_DELAY)
118 'Exporting PWM: waiting for "{:s}" timed out.'.format(
123 # Loop until 'period' is writable, This could take some time after
124 # export as application of the udev rules after export is asynchronous.
125 # Without this loop, the following properties may not be writable yet.
126 for i in range(PWMOut.PWM_STAT_RETRIES):
129 os.path.join(self._channel_path, "period"),
135 if e.errno != EACCES or (
136 e.errno == EACCES and i == PWMOut.PWM_STAT_RETRIES - 1
139 e.errno, "Opening PWM period: " + e.strerror
142 sleep(PWMOut.PWM_STAT_DELAY)
144 self.frequency = frequency
145 self.duty_cycle = duty_cycle
146 self.polarity = "normal"
149 # Cache the period for fast duty cycle updates
150 self._period_ns = self._get_period_ns()
154 if self._channel is not None:
155 # Unexporting the PWM channel
157 unexport_fd = os.open(
158 os.path.join(self._chip_path, "unexport"), os.O_WRONLY
160 os.write(unexport_fd, "{:d}\n".format(self._channel).encode())
161 os.close(unexport_fd)
163 raise PWMError(e.errno, "Unexporting PWM: " + e.strerror) from OSError
168 def _write_channel_attr(self, attr, value):
170 os.path.join(self._channel_path, attr), "w", encoding="utf-8"
172 f_attr.write(value + "\n")
174 def _read_channel_attr(self, attr):
176 os.path.join(self._channel_path, attr), "r", encoding="utf-8"
178 return f_attr.read().strip()
183 """Enable the PWM output."""
187 """Disable the PWM output."""
192 def _get_period(self):
193 return float(self.period_ms) / 1000
195 def _set_period(self, period):
196 if not isinstance(period, (int, float)):
197 raise TypeError("Invalid period type, should be int.")
199 self.period_ms = int(period * 1000)
201 period = property(_get_period, _set_period)
202 """Get or set the PWM's output period in seconds.
205 PWMError: if an I/O or OS error occurs.
206 TypeError: if value type is not int.
211 def _get_period_ms(self):
212 return self.period_us / 1000
214 def _set_period_ms(self, period_ms):
215 if not isinstance(period_ms, (int, float)):
216 raise TypeError("Invalid period type, should be int or float.")
217 self.period_us = int(period_ms * 1000)
219 period_ms = property(_get_period_ms, _set_period_ms)
220 """Get or set the PWM's output period in milliseconds.
223 PWMError: if an I/O or OS error occurs.
224 TypeError: if value type is not int.
229 def _get_period_us(self):
230 return self.period_ns / 1000
232 def _set_period_us(self, period_us):
233 if not isinstance(period_us, int):
234 raise TypeError("Invalid period type, should be int.")
236 self.period_ns = int(period_us * 1000)
238 period_us = property(_get_period_us, _set_period_us)
239 """Get or set the PWM's output period in microseconds.
242 PWMError: if an I/O or OS error occurs.
243 TypeError: if value type is not int.
248 def _get_period_ns(self):
249 period_ns = self._read_channel_attr("period")
251 period_ns = int(period_ns)
254 None, 'Unknown period value: "%s".' % period_ns
257 self._period_ns = period_ns
261 def _set_period_ns(self, period_ns):
262 if not isinstance(period_ns, int):
263 raise TypeError("Invalid period type, should be int.")
265 self._write_channel_attr("period", str(period_ns))
267 # Update our cached period
268 self._period_ns = period_ns
270 period_ns = property(_get_period_ns, _set_period_ns)
271 """Get or set the PWM's output period in nanoseconds.
274 PWMError: if an I/O or OS error occurs.
275 TypeError: if value type is not int.
280 def _get_duty_cycle_ns(self):
281 duty_cycle_ns_str = self._read_channel_attr("duty_cycle")
284 duty_cycle_ns = int(duty_cycle_ns_str)
287 None, 'Unknown duty cycle value: "{:s}"'.format(duty_cycle_ns_str)
292 def _set_duty_cycle_ns(self, duty_cycle_ns):
293 if not isinstance(duty_cycle_ns, int):
294 raise TypeError("Invalid duty cycle type, should be int.")
296 self._write_channel_attr("duty_cycle", str(duty_cycle_ns))
298 duty_cycle_ns = property(_get_duty_cycle_ns, _set_duty_cycle_ns)
299 """Get or set the PWM's output duty cycle in nanoseconds.
302 PWMError: if an I/O or OS error occurs.
303 TypeError: if value type is not int.
308 def _get_duty_cycle(self):
309 return float(self.duty_cycle_ns) / self._period_ns
311 def _set_duty_cycle(self, duty_cycle):
312 if not isinstance(duty_cycle, (int, float)):
313 raise TypeError("Invalid duty cycle type, should be int or float.")
315 if not 0.0 <= duty_cycle <= 1.0:
316 raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.")
318 # Convert duty cycle from ratio to nanoseconds
319 self.duty_cycle_ns = int(duty_cycle * self._period_ns)
321 duty_cycle = property(_get_duty_cycle, _set_duty_cycle)
322 """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0.
324 PWMError: if an I/O or OS error occurs.
325 TypeError: if value type is not int or float.
326 ValueError: if value is out of bounds of 0.0 to 1.0.
330 def _get_frequency(self):
331 return 1.0 / self.period
333 def _set_frequency(self, frequency):
334 if not isinstance(frequency, (int, float)):
335 raise TypeError("Invalid frequency type, should be int or float.")
337 self.period = 1.0 / frequency
339 frequency = property(_get_frequency, _set_frequency)
340 """Get or set the PWM's output frequency in Hertz.
342 PWMError: if an I/O or OS error occurs.
343 TypeError: if value type is not int or float.
347 def _get_polarity(self):
348 return self._read_channel_attr("polarity")
350 def _set_polarity(self, polarity):
351 if not isinstance(polarity, str):
352 raise TypeError("Invalid polarity type, should be str.")
354 if polarity.lower() not in ["normal", "inversed"]:
355 raise ValueError('Invalid polarity, can be: "normal" or "inversed".')
357 self._write_channel_attr("polarity", polarity.lower())
359 polarity = property(_get_polarity, _set_polarity)
360 """Get or set the PWM's output polarity. Can be "normal" or "inversed".
362 PWMError: if an I/O or OS error occurs.
363 TypeError: if value type is not str.
364 ValueError: if value is invalid.
368 def _get_enabled(self):
369 enabled = self._read_channel_attr("enable")
376 raise PWMError(None, 'Unknown enabled value: "{:s}"'.format(enabled))
378 def _set_enabled(self, value):
379 if not isinstance(value, bool):
380 raise TypeError("Invalid enabled type, should be bool.")
382 self._write_channel_attr("enable", "1" if value else "0")
384 enabled = property(_get_enabled, _set_enabled)
385 """Get or set the PWM's output enabled state.
387 PWMError: if an I/O or OS error occurs.
388 TypeError: if value type is not bool.
392 # String representation
396 "PWM {:d}, chip {:d} (period={:f} sec, duty_cycle={:f}%,"
397 " polarity={:s}, enabled={:s})".format(
401 self.duty_cycle * 100,