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,