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:
163 channel_path = os.path.join(
164 self._sysfs_path, self._channel_path.format(self._channel)
167 os.path.join(channel_path, self._unexport_path),
171 f_unexport.write("%d\n" % self._pwmpin)
174 e.errno, "Unexporting PWM pin: " + e.strerror
180 def _is_deinited(self):
181 if self._pwmpin is None:
183 "Object has been deinitialize and can no longer "
184 "be used. Create a new object."
187 def _write_pin_attr(self, attr, value):
188 # Make sure the pin is active
193 self._channel_path.format(self._channel),
194 self._pin_path.format(self._pwmpin),
198 with open(path, "w", encoding="utf-8") as f_attr:
200 f_attr.write(value + "\n")
202 def _read_pin_attr(self, attr):
203 # Make sure the pin is active
208 self._channel_path.format(self._channel),
209 self._pin_path.format(self._pwmpin),
213 with open(path, "r", encoding="utf-8") as f_attr:
214 return f_attr.read().strip()
218 def _get_period(self):
219 period_ns = self._read_pin_attr(self._pin_period_path)
221 period_ns = int(period_ns)
224 None, 'Unknown period value: "%s"' % period_ns
227 # Convert period from nanoseconds to seconds
228 period = period_ns / 1e9
230 # Update our cached period
231 self._period = period
235 def _set_period(self, period):
236 if not isinstance(period, (int, float)):
237 raise TypeError("Invalid period type, should be int or float.")
239 # Convert period from seconds to integer nanoseconds
240 period_ns = int(period * 1e9)
242 self._write_pin_attr(self._pin_period_path, "{}".format(period_ns))
244 # Update our cached period
245 self._period = float(period)
247 period = property(_get_period, _set_period)
249 """Get or set the PWM's output period in seconds.
252 PWMError: if an I/O or OS error occurs.
253 TypeError: if value type is not int or float.
258 def _get_duty_cycle(self):
259 duty_cycle_ns = self._read_pin_attr(self._pin_duty_cycle_path)
261 duty_cycle_ns = int(duty_cycle_ns)
264 None, 'Unknown duty cycle value: "%s"' % duty_cycle_ns
267 # Convert duty cycle from nanoseconds to seconds
268 duty_cycle = duty_cycle_ns / 1e9
270 # Convert duty cycle to ratio from 0.0 to 1.0
271 duty_cycle = duty_cycle / self._period
274 duty_cycle = int(duty_cycle * 65535)
277 def _set_duty_cycle(self, duty_cycle):
278 if not isinstance(duty_cycle, (int, float)):
279 raise TypeError("Invalid duty cycle type, should be int or float.")
281 # convert from 16-bit
282 duty_cycle /= 65535.0
283 if not 0.0 <= duty_cycle <= 1.0:
284 raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.")
286 # Convert duty cycle from ratio to seconds
287 duty_cycle = duty_cycle * self._period
289 # Convert duty cycle from seconds to integer nanoseconds
290 duty_cycle_ns = int(duty_cycle * 1e9)
292 self._write_pin_attr(self._pin_duty_cycle_path, "{}".format(duty_cycle_ns))
294 duty_cycle = property(_get_duty_cycle, _set_duty_cycle)
295 """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0.
298 PWMError: if an I/O or OS error occurs.
299 TypeError: if value type is not int or float.
300 ValueError: if value is out of bounds of 0.0 to 1.0.
305 def _get_frequency(self):
306 return 1.0 / self._get_period()
308 def _set_frequency(self, frequency):
309 if not isinstance(frequency, (int, float)):
310 raise TypeError("Invalid frequency type, should be int or float.")
312 self._set_period(1.0 / frequency)
314 frequency = property(_get_frequency, _set_frequency)
315 """Get or set the PWM's output frequency in Hertz.
318 PWMError: if an I/O or OS error occurs.
319 TypeError: if value type is not int or float.
324 def _get_enabled(self):
325 enabled = self._read_pin_attr(self._pin_enable_path)
332 raise PWMError(None, 'Unknown enabled value: "%s"' % enabled)
334 def _set_enabled(self, value):
335 """Get or set the PWM's output enabled state.
338 PWMError: if an I/O or OS error occurs.
339 TypeError: if value type is not bool.
343 if not isinstance(value, bool):
344 raise TypeError("Invalid enabled type, should be string.")
346 self._write_pin_attr(self._pin_enable_path, "1" if value else "0")
348 # String representation
351 return "PWM%d, pin %s (freq=%f Hz, duty_cycle=%f%%)" % (
355 self.duty_cycle * 100,