From: Drew Fustini Date: Sat, 26 Oct 2019 19:03:09 +0000 (+0200) Subject: Add PWMOut for PocketBeagle #159 X-Git-Tag: 3.0.1~1^2 X-Git-Url: https://git.ayoreis.com/Adafruit_Blinka-hackapet.git/commitdiff_plain/824d525493acfb26728ca74e183417e4a58aaa23?hp=-c Add PWMOut for PocketBeagle #159 Add PWMOut for PocketBeagle which has the AM3358 SoC. Note that the generic linux PWMOut implementation will not work as BeagleBoard.org kernel has a patch to allow udev to be able to set the ownership and permissions in /sys/class/pwm when channels are exported. However, this naming scheme is different than what is used in the mainline kernel. The PocketBeagle has these PWM outputs: /sys/class/pwm/pwmchip0/pwm-0:0/period /sys/class/pwm/pwmchip0/pwm-0:1/period /sys/class/pwm/pwmchip2/pwm-2:0/period /sys/class/pwm/pwmchip2/pwm-2:1/period /sys/class/pwm/pwmchip4/pwm-4:0/period /sys/class/pwm/pwmchip4/pwm-4:1/period Here is the kernel patch that is being used: https://github.com/RobertCNelson/bb-kernel/blob/am33x-v4.11/patches/drivers/pwm/0001-pwm-Create-device-class-for-pwm-channels.p$ --- 824d525493acfb26728ca74e183417e4a58aaa23 diff --git a/src/adafruit_blinka/board/beaglebone_pocketbeagle.py b/src/adafruit_blinka/board/beaglebone_pocketbeagle.py index a56e5f7..fc0bb3a 100644 --- a/src/adafruit_blinka/board/beaglebone_pocketbeagle.py +++ b/src/adafruit_blinka/board/beaglebone_pocketbeagle.py @@ -40,10 +40,10 @@ P1_29 = pin.P1_29 # GPIO3_21 - GPIO_117 P1_30 = pin.P1_30 # UART0_TXD - GPIO_43 P1_31 = pin.P1_31 # GPIO3_18 - GPIO_114 P1_32 = pin.P1_32 # UART0_RXD - GPIO_42 -P1_33 = pin.P1_33 # GPIO3_15 - GPIO_111 +P1_33 = pin.P1_33 # GPIO3_15 - GPIO_111 - EHRPWM0B (ehrpwm.0:1) P1_34 = pin.P1_34 # GPIO0_26 - GPIO_26 P1_35 = pin.P1_35 # GPIO2_24 - GPIO_88 -P1_36 = pin.P1_36 # EHRPWM0A - GPIO_110 +P1_36 = pin.P1_36 # EHRPWM0A - GPIO_110 - EHRPWM0A (ehrpwm.0:0) P2_1 = pin.P2_1 # EHRPWM1A - GPIO_50 diff --git a/src/adafruit_blinka/microcontroller/am335x/pin.py b/src/adafruit_blinka/microcontroller/am335x/pin.py index e73457d..ca9d027 100644 --- a/src/adafruit_blinka/microcontroller/am335x/pin.py +++ b/src/adafruit_blinka/microcontroller/am335x/pin.py @@ -343,3 +343,10 @@ i2cPorts = ( (1, I2C1_SCL, I2C1_SDA), (2, I2C2_SCL, I2C2_SDA), ) + +PWM1 = P1_36 +PWM2 = P1_33 +PWM3 = P2_1 +PWM4 = P2_3 + +pwmOuts = ( ((0, 0), PWM1), ((0, 1), PWM2), ((2, 0), PWM3), ((4, 1), PWM4) ) diff --git a/src/adafruit_blinka/microcontroller/am335x/sysfs_pwmout.py b/src/adafruit_blinka/microcontroller/am335x/sysfs_pwmout.py new file mode 100644 index 0000000..70bab33 --- /dev/null +++ b/src/adafruit_blinka/microcontroller/am335x/sysfs_pwmout.py @@ -0,0 +1,287 @@ +# Much code from https://github.com/vsergeev/python-periphery/blob/master/periphery/pwm.py +# Copyright (c) 2015-2016 vsergeev / Ivan (Vanya) A. Sergeev +# License: MIT + +import os +import digitalio + +try: + from microcontroller.pin import pwmOuts +except ImportError: + raise RuntimeError("No PWM outputs defined for this board") + +class PWMError(IOError): + """Base class for PWM errors.""" + pass + + +class PWMOut(object): + # Sysfs paths + _sysfs_path = "/sys/class/pwm/" + _channel_path = "pwmchip{}" + + # Channel paths + _export_path = "export" + _unexport_path = "unexport" + _pin_path = "pwm-{}:{}" + + # Pin attribute paths + _pin_period_path = "period" + _pin_duty_cycle_path = "duty_cycle" + _pin_polarity_path = "polarity" + _pin_enable_path = "enable" + + def __init__(self, pin, *, frequency=500, duty_cycle=0, variable_frequency=False): + """Instantiate a PWM object and open the sysfs PWM corresponding to the + specified channel and pin. + + Args: + pin (Pin): CircuitPython Pin object to output to + duty_cycle (int) : The fraction of each pulse which is high. 16-bit + frequency (int) : target frequency in Hertz (32-bit) + variable_frequency (bool) : True if the frequency will change over time + + Returns: + PWMOut: PWMOut object. + + Raises: + PWMError: if an I/O or OS error occurs. + TypeError: if `channel` or `pin` types are invalid. + ValueError: if PWM channel does not exist. + + """ + + self._pwmpin = None + self._open(pin, duty_cycle, frequency, variable_frequency) + + def __del__(self): + self.deinit() + + def __enter__(self): + return self + + def __exit__(self, t, value, traceback): + self.deinit() + + def _open(self, pin, duty=0, freq=500, variable_frequency=False): + self._channel = None + for pwmpair in pwmOuts: + if pwmpair[1] == pin: + self._channel = pwmpair[0][0] + self._pwmpin = pwmpair[0][1] + + self._pin = pin + if self._channel is None: + raise RuntimeError("No PWM channel found for this Pin") + + channel_path = os.path.join(self._sysfs_path, self._channel_path.format(self._channel)) + if not os.path.isdir(channel_path): + raise ValueError("PWM channel does not exist, check that the required modules are loaded.") + + pin_path = os.path.join(channel_path, self._pin_path.format(self._channel,self._pwmpin)) + if not os.path.isdir(pin_path): + try: + with open(os.path.join(channel_path, self._export_path), "w") as f_export: + f_export.write("%d\n" % self._pwmpin) + except IOError as e: + raise PWMError(e.errno, "Exporting PWM pin: " + e.strerror) + + # Look up the period, for fast duty cycle updates + self._period = self._get_period() + + # set frequency + self.frequency = freq + # set duty + self.duty_cycle = duty + + self._set_enabled(True) + + def deinit(self): + try: + """Deinit the sysfs PWM.""" + channel_path = os.path.join(self._sysfs_path, self._channel_path.format(self._channel)) + pin_path = os.path.join(channel_path, self._pin_path.format(self._channel,self._pwmpin)) + + if self._channel is not None: + #self.duty_cycle = 0 + self._set_enabled(False) # make to disable before unexport + try: + #unexport_path = os.path.join(channel_path, self._unexport_path) + with open(os.path.join(channel_path, self._unexport_path), "w") as f_unexport: + f_unexport.write("%d\n" % self._pwmpin) + except IOError as e: + raise PWMError(e.errno, "Unexporting PWM pin: " + e.strerror) + except Exception as e: + # due to a race condition for which I have not yet been + # able to find the root cause, deinit() often fails + # but it does not effect future usage of the pwm pin + print("warning: failed to deinitialize pwm pin {0}:{1} due to: {2}\n".format(self._channel, self._pwmpin, type(e).__name__)) + finally: + self._channel = None + self._pwmpin = None + + def _is_deinited(self): + if self._pwmpin is None: + raise ValueError("Object has been deinitialize and can no longer " + "be used. Create a new object.") + + def _write_pin_attr(self, attr, value): + # Make sure the pin is active + self._is_deinited() + + path = os.path.join( + self._sysfs_path, + self._channel_path.format(self._channel), + self._pin_path.format(self._channel,self._pwmpin), + attr) + + with open(path, 'w') as f_attr: + f_attr.write(value + "\n") + + def _read_pin_attr(self, attr): + # Make sure the pin is active + self._is_deinited() + + path = os.path.join( + self._sysfs_path, + self._channel_path.format(self._channel), + self._pin_path.format(self._channel,self._pwmpin), + attr) + + with open(path, 'r') as f_attr: + return f_attr.read().strip() + + # Mutable properties + + def _get_period(self): + period_ns = self._read_pin_attr(self._pin_period_path) + try: + period_ns = int(period_ns) + except ValueError: + raise PWMError(None, "Unknown period value: \"%s\"" % period_ns) + + # Convert period from nanoseconds to seconds + period = period_ns / 1e9 + + # Update our cached period + self._period = period + + return period + + def _set_period(self, period): + if not isinstance(period, (int, float)): + raise TypeError("Invalid period type, should be int or float.") + + # Convert period from seconds to integer nanoseconds + period_ns = int(period * 1e9) + + self._write_pin_attr(self._pin_period_path, "{}".format(period_ns)) + + # Update our cached period + self._period = float(period) + + period = property(_get_period, _set_period) + + """Get or set the PWM's output period in seconds. + + Raises: + PWMError: if an I/O or OS error occurs. + TypeError: if value type is not int or float. + + :type: int, float + """ + + def _get_duty_cycle(self): + duty_cycle_ns = self._read_pin_attr(self._pin_duty_cycle_path) + try: + duty_cycle_ns = int(duty_cycle_ns) + except ValueError: + raise PWMError(None, "Unknown duty cycle value: \"%s\"" % duty_cycle_ns) + + # Convert duty cycle from nanoseconds to seconds + duty_cycle = duty_cycle_ns / 1e9 + + # Convert duty cycle to ratio from 0.0 to 1.0 + duty_cycle = duty_cycle / self._period + + # convert to 16-bit + duty_cycle = int(duty_cycle * 65535) + return duty_cycle + + def _set_duty_cycle(self, duty_cycle): + if not isinstance(duty_cycle, (int, float)): + raise TypeError("Invalid duty cycle type, should be int or float.") + + # convert from 16-bit + duty_cycle /= 65535.0 + if not 0.0 <= duty_cycle <= 1.0: + raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.") + + # Convert duty cycle from ratio to seconds + duty_cycle = duty_cycle * self._period + + # Convert duty cycle from seconds to integer nanoseconds + duty_cycle_ns = int(duty_cycle * 1e9) + + self._write_pin_attr(self._pin_duty_cycle_path, "{}".format(duty_cycle_ns)) + + duty_cycle = property(_get_duty_cycle, _set_duty_cycle) + """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0. + + Raises: + PWMError: if an I/O or OS error occurs. + TypeError: if value type is not int or float. + ValueError: if value is out of bounds of 0.0 to 1.0. + + :type: int, float + """ + + def _get_frequency(self): + return 1.0 / self._get_period() + + def _set_frequency(self, frequency): + if not isinstance(frequency, (int, float)): + raise TypeError("Invalid frequency type, should be int or float.") + + self._set_period(1.0 / frequency) + + frequency = property(_get_frequency, _set_frequency) + """Get or set the PWM's output frequency in Hertz. + + Raises: + PWMError: if an I/O or OS error occurs. + TypeError: if value type is not int or float. + + :type: int, float + """ + + def _get_enabled(self): + enabled = self._read_pin_attr(self._pin_enable_path) + + if enabled == "1": + return True + elif enabled == "0": + return False + + raise PWMError(None, "Unknown enabled value: \"%s\"" % enabled) + + def _set_enabled(self, value): + if not isinstance(value, bool): + raise TypeError("Invalid enabled type, should be string.") + + self._write_pin_attr(self._pin_enable_path, "1" if value else "0") + + """Get or set the PWM's output enabled state. + + Raises: + PWMError: if an I/O or OS error occurs. + TypeError: if value type is not bool. + + :type: bool + """ + + # String representation + + def __str__(self): + return "PWM%d, pin %s (freq=%f Hz, duty_cycle=%f%%)" % \ + (self._channel, self._pin, self.frequency, self.duty_cycle * 100,) diff --git a/src/pulseio.py b/src/pulseio.py index 42524fc..bb8a04b 100644 --- a/src/pulseio.py +++ b/src/pulseio.py @@ -6,3 +6,5 @@ if detector.board.any_coral_board: from adafruit_blinka.microcontroller.generic_linux.sysfs_pwmout import PWMOut if detector.board.any_giant_board: from adafruit_blinka.microcontroller.generic_linux.sysfs_pwmout import PWMOut +if detector.board.any_beaglebone: + from adafruit_blinka.microcontroller.am335x.sysfs_pwmout import PWMOut