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,