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
21 class PWMError(IOError):
22 """Base class for PWM errors."""
27 # pylint: enable=unnecessary-pass
31 """Pulse Width Modulation Output Class"""
33 # Number of retries to check for successful PWM export on open
35 # Delay between check for scucessful PWM export on open (100ms)
39 _sysfs_path = "/sys/class/pwm/"
40 _channel_path = "pwmchip{}"
43 _export_path = "export"
44 _unexport_path = "unexport"
48 _pin_period_path = "period"
49 _pin_duty_cycle_path = "duty_cycle"
50 _pin_polarity_path = "polarity"
51 _pin_enable_path = "enable"
53 def __init__(self, pin, *, frequency=500, duty_cycle=0, variable_frequency=False):
54 """Instantiate a PWM object and open the sysfs PWM corresponding to the
55 specified channel and pin.
58 pin (Pin): CircuitPython Pin object to output to
59 duty_cycle (int) : The fraction of each pulse which is high. 16-bit
60 frequency (int) : target frequency in Hertz (32-bit)
61 variable_frequency (bool) : True if the frequency will change over time
64 PWMOut: PWMOut object.
67 PWMError: if an I/O or OS error occurs.
68 TypeError: if `channel` or `pin` types are invalid.
69 ValueError: if PWM channel does not exist.
76 self._open(pin, duty_cycle, frequency, variable_frequency)
84 def __exit__(self, t, value, traceback):
87 def _open(self, pin, duty=0, freq=500, variable_frequency=False):
89 for pwmpair in pwmOuts:
91 self._channel = pwmpair[0][0]
92 self._pwmpin = pwmpair[0][1]
95 if self._channel is None:
96 raise RuntimeError("No PWM channel found for this Pin")
98 if variable_frequency:
99 print("Variable Frequency is not supported, continuing without it...")
101 channel_path = os.path.join(
102 self._sysfs_path, self._channel_path.format(self._channel)
104 if not os.path.isdir(channel_path):
106 "PWM channel does not exist, check that the required modules are loaded."
111 os.path.join(channel_path, self._unexport_path), "w", encoding="utf-8"
113 f_unexport.write("%d\n" % self._pwmpin)
115 pass # not unusual, it doesnt already exist
118 os.path.join(channel_path, self._export_path), "w", encoding="utf-8"
120 f_export.write("%d\n" % self._pwmpin)
122 raise PWMError(e.errno, "Exporting PWM pin: " + e.strerror) from IOError
124 # Loop until 'period' is writable, because application of udev rules
125 # after the above pin export is asynchronous.
126 # Without this loop, the following properties may not be writable yet.
127 for i in range(PWMOut.PWM_STAT_RETRIES):
131 channel_path, self._pin_path.format(self._pwmpin), "period"
138 if e.errno != EACCES or (
139 e.errno == EACCES and i == PWMOut.PWM_STAT_RETRIES - 1
141 raise PWMError(e.errno, "Opening PWM period: " + e.strerror) from e
142 sleep(PWMOut.PWM_STAT_DELAY)
144 # self._set_enabled(False) # This line causes a write error when trying to enable
146 # Look up the period, for fast duty cycle updates
147 self._period = self._get_period()
149 # self.duty_cycle = 0 # This line causes a write error when trying to enable
152 self.frequency = freq
154 self.duty_cycle = duty
156 self._set_enabled(True)
159 """Deinit the sysfs PWM."""
160 if self._channel is not None:
162 channel_path = os.path.join(
163 self._sysfs_path, self._channel_path.format(self._channel)
166 os.path.join(channel_path, self._unexport_path),
170 f_unexport.write("%d\n" % self._pwmpin)
173 e.errno, "Unexporting PWM pin: " + e.strerror
179 def _is_deinited(self):
180 if self._pwmpin is None:
182 "Object has been deinitialize and can no longer "
183 "be used. Create a new object."
186 def _write_pin_attr(self, attr, value):
187 # Make sure the pin is active
192 self._channel_path.format(self._channel),
193 self._pin_path.format(self._pwmpin),
197 with open(path, "w", encoding="utf-8") as f_attr:
199 f_attr.write(value + "\n")
201 def _read_pin_attr(self, attr):
202 # Make sure the pin is active
207 self._channel_path.format(self._channel),
208 self._pin_path.format(self._pwmpin),
212 with open(path, "r", encoding="utf-8") as f_attr:
213 return f_attr.read().strip()
217 def _get_period(self):
218 period_ns = self._read_pin_attr(self._pin_period_path)
220 period_ns = int(period_ns)
223 None, 'Unknown period value: "%s"' % period_ns
226 # Convert period from nanoseconds to seconds
227 period = period_ns / 1e9
229 # Update our cached period
230 self._period = period
234 def _set_period(self, period):
235 if not isinstance(period, (int, float)):
236 raise TypeError("Invalid period type, should be int or float.")
238 # Convert period from seconds to integer nanoseconds
239 period_ns = int(period * 1e9)
241 self._write_pin_attr(self._pin_period_path, "{}".format(period_ns))
243 # Update our cached period
244 self._period = float(period)
246 period = property(_get_period, _set_period)
248 """Get or set the PWM's output period in seconds.
251 PWMError: if an I/O or OS error occurs.
252 TypeError: if value type is not int or float.
257 def _get_duty_cycle(self):
258 duty_cycle_ns = self._read_pin_attr(self._pin_duty_cycle_path)
260 duty_cycle_ns = int(duty_cycle_ns)
263 None, 'Unknown duty cycle value: "%s"' % duty_cycle_ns
266 # Convert duty cycle from nanoseconds to seconds
267 duty_cycle = duty_cycle_ns / 1e9
269 # Convert duty cycle to ratio from 0.0 to 1.0
270 duty_cycle = duty_cycle / self._period
273 duty_cycle = int(duty_cycle * 65535)
276 def _set_duty_cycle(self, duty_cycle):
277 if not isinstance(duty_cycle, (int, float)):
278 raise TypeError("Invalid duty cycle type, should be int or float.")
280 # convert from 16-bit
281 duty_cycle /= 65535.0
282 if not 0.0 <= duty_cycle <= 1.0:
283 raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.")
285 # Convert duty cycle from ratio to seconds
286 duty_cycle = duty_cycle * self._period
288 # Convert duty cycle from seconds to integer nanoseconds
289 duty_cycle_ns = int(duty_cycle * 1e9)
291 self._write_pin_attr(self._pin_duty_cycle_path, "{}".format(duty_cycle_ns))
293 duty_cycle = property(_get_duty_cycle, _set_duty_cycle)
294 """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0.
297 PWMError: if an I/O or OS error occurs.
298 TypeError: if value type is not int or float.
299 ValueError: if value is out of bounds of 0.0 to 1.0.
304 def _get_frequency(self):
305 return 1.0 / self._get_period()
307 def _set_frequency(self, frequency):
308 if not isinstance(frequency, (int, float)):
309 raise TypeError("Invalid frequency type, should be int or float.")
311 self._set_period(1.0 / frequency)
313 frequency = property(_get_frequency, _set_frequency)
314 """Get or set the PWM's output frequency in Hertz.
317 PWMError: if an I/O or OS error occurs.
318 TypeError: if value type is not int or float.
323 def _get_enabled(self):
324 enabled = self._read_pin_attr(self._pin_enable_path)
331 raise PWMError(None, 'Unknown enabled value: "%s"' % enabled)
333 def _set_enabled(self, value):
334 """Get or set the PWM's output enabled state.
337 PWMError: if an I/O or OS error occurs.
338 TypeError: if value type is not bool.
342 if not isinstance(value, bool):
343 raise TypeError("Invalid enabled type, should be string.")
345 self._write_pin_attr(self._pin_enable_path, "1" if value else "0")
347 # String representation
350 return "PWM%d, pin %s (freq=%f Hz, duty_cycle=%f%%)" % (
354 self.duty_cycle * 100,