All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pi4j.gpio.extension.pca.PCA9685GpioProvider Maven / Gradle / Ivy

package pi4j.gpio.extension.pca;

/*
 * #%L
 * **********************************************************************
 * ORGANIZATION  :  Pi4J
 * PROJECT       :  Pi4J :: GPIO Extension
 * FILENAME      :  PCA9685GpioProvider.java
 *
 * This file is part of the Pi4J project. More information about
 * this project can be found here:  https://www.pi4j.com/
 * **********************************************************************
 * %%
 * Copyright (C) 2012 - 2021 Pi4J
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 *
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * .
 * #L%
 */
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;

import com.pi4j.io.gpio.*;
import com.pi4j.io.gpio.exception.InvalidPinException;
import com.pi4j.io.gpio.exception.InvalidPinModeException;
import com.pi4j.io.gpio.exception.ValidationException;
import com.pi4j.io.i2c.I2CBus;
import com.pi4j.io.i2c.I2CDevice;
import com.pi4j.io.i2c.I2CFactory;
import com.pi4j.io.i2c.I2CFactory.UnsupportedBusNumberException;

/**
 * 

* This GPIO provider implements the PCA9685 16-channel, 12-bit PWM I2C-bus LED/Servo controller as native Pi4J GPIO pins. * The PCA9685 is connected via I2C connection to the Raspberry Pi and provides 16 PWM pins that can be used for PWM output. *

*

* More information about the PCA9685 can be found here:
* PCA9685.pdf

* ...and especially about the board here:
* Adafruit 16-Channel 12-bit PWM/Servo Driver *

* * @author Christian Wehrli * @author Hanns Holger Rutz */ public class PCA9685GpioProvider extends GpioProviderBase implements GpioProvider { public static final String NAME = "com.pi4j.gpio.extension.pca.PCA9685GpioProvider"; public static final String DESCRIPTION = "PCA9685 PWM Provider"; public static final int INTERNAL_CLOCK_FREQ = 25 * 1000 * 1000; public static final BigDecimal MIN_FREQUENCY = new BigDecimal("40"); // 40 Hz public static final BigDecimal MAX_FREQUENCY = new BigDecimal("1000"); // 1 kHz /** * This would result in a period duration of ~22ms which is save for all type of servo */ public static final BigDecimal ANALOG_SERVO_FREQUENCY = new BigDecimal("45.454"); /** * This would result in a period duration of ~11ms which is recommended when using digital servos ONLY! */ public static final BigDecimal DIGITAL_SERVO_FREQUENCY = new BigDecimal("90.909"); public static final BigDecimal DEFAULT_FREQUENCY = ANALOG_SERVO_FREQUENCY; public static final int PWM_STEPS = 4096; // 12 Bit // Registers private static final int PCA9685A_MODE1 = 0x00; private static final int PCA9685A_PRESCALE = 0xFE; private static final int PCA9685A_LED0_ON_L = 0x06; private static final int PCA9685A_LED0_ON_H = 0x07; private static final int PCA9685A_LED0_OFF_L = 0x08; private static final int PCA9685A_LED0_OFF_H = 0x09; private boolean i2cBusOwner = false; private final I2CBus bus; private final I2CDevice device; private BigDecimal frequency; private int periodDurationMicros; // custom pin cache protected PCA9685GpioProviderPinCache[] cache = new PCA9685GpioProviderPinCache[16]; // support up to pin address 15 (16 pins) public PCA9685GpioProvider(int busNumber, int address) throws UnsupportedBusNumberException, IOException { // create I2C communications bus instance this(I2CFactory.getInstance(busNumber), address); i2cBusOwner = true; } public PCA9685GpioProvider(I2CBus bus, int address) throws IOException { this(bus, address, DEFAULT_FREQUENCY, BigDecimal.ONE); } public PCA9685GpioProvider(I2CBus bus, int address, BigDecimal targetFrequency) throws IOException { this(bus, address, targetFrequency, BigDecimal.ONE); } public PCA9685GpioProvider(I2CBus bus, int address, BigDecimal targetFrequency, BigDecimal frequencyCorrectionFactor) throws IOException { // create I2C communications bus instance this.bus = bus; // 1 // create I2C device instance device = bus.getDevice(address); // 0x40 device.write(PCA9685A_MODE1, (byte) 0); setFrequency(targetFrequency, frequencyCorrectionFactor); } @Override public void export(Pin pin, PinMode mode) { // make sure to set the pin mode super.export(pin, mode); setMode(pin, mode); } @Override public void unexport(Pin pin) { super.unexport(pin); setMode(pin, PinMode.PWM_OUTPUT); } /** * Target frequency (accuracy is around +/- 5%!) * * @param frequency desired PWM frequency * @see #setFrequency(BigDecimal, BigDecimal) */ public void setFrequency(BigDecimal frequency) { setFrequency(frequency, BigDecimal.ONE); } /** * The built-in Oscillator runs at ~25MHz. For better accuracy user can provide a correction * factor to meet desired frequency.

* Note: correction is limited to a certain degree because the calculated prescale value has to be * rounded to an integer value! *

* Example:
* target freq: 50Hz
* actual freq: 52.93Hz
* correction factor: 52.93 / 50 = 1.0586
* * @param targetFrequency desired frequency * @param frequencyCorrectionFactor 'actual frequency' / 'target frequency' */ public void setFrequency(BigDecimal targetFrequency, BigDecimal frequencyCorrectionFactor) { validateFrequency(targetFrequency); frequency = targetFrequency; periodDurationMicros = calculatePeriodDuration(); int prescale = calculatePrescale(frequencyCorrectionFactor); int oldMode; try { oldMode = device.read(PCA9685A_MODE1); int newMode = (oldMode & 0x7F) | 0x10; // sleep device.write(PCA9685A_MODE1 , (byte) newMode); // go to sleep device.write(PCA9685A_PRESCALE , (byte) prescale); device.write(PCA9685A_MODE1 , (byte) oldMode); Thread.sleep(1); // must wait at least 500 microseconds device.write(PCA9685A_MODE1 , (byte) (oldMode | 0x80)); // restart } catch (IOException e) { throw new RuntimeException("Unable to set prescale value [" + prescale + "]", e); } catch (InterruptedException e) { throw new RuntimeException(e); } } /** * Set pulse duration in microseconds.
* Make sure duration doesn't exceed period time(1'000'000/freq)! * * @param pin represents channel 0..15 * @param duration pulse duration in microseconds */ @Override public void setPwm(Pin pin, int duration) { int offPosition = calculateOffPositionForPulseDuration(duration); setPwm(pin, 0, offPosition); } /** * The LEDn_ON and LEDn_OFF counts can vary from 0 to 4095 max.
* The LEDn_ON and LEDn_OFF count registers should never be programmed with the same values. *

* Because the loading of the LEDn_ON and LEDn_OFF registers is via the I2C-bus, and * asynchronous to the internal oscillator, we want to ensure that we do not see any visual * artifacts of changing the ON and OFF values. This is achieved by updating the changes at * the end of the LOW cycle. * * @param pin represents channel 0..15 * @param onPosition value between 0 and 4095 * @param offPosition value between 0 and 4095 */ public void setPwm(Pin pin, int onPosition, int offPosition) { validatePin(pin, onPosition, offPosition); final int channel = pin.getAddress(); validatePwmValueInRange(onPosition); validatePwmValueInRange(offPosition); if (onPosition == offPosition) { throw new ValidationException("ON [" + onPosition + "] and OFF [" + offPosition + "] values must be different."); } // System.out.println("setPwm(_, " + onPosition + ", " + offPosition + ")"); try { device.write(PCA9685A_LED0_ON_L + 4 * channel, (byte) (onPosition & 0xFF)); device.write(PCA9685A_LED0_ON_H + 4 * channel, (byte) (onPosition >> 8)); device.write(PCA9685A_LED0_OFF_L + 4 * channel, (byte) (offPosition & 0xFF)); device.write(PCA9685A_LED0_OFF_H + 4 * channel, (byte) (offPosition >> 8)); } catch (IOException e) { throw new RuntimeException("Unable to write to PWM channel [" + channel + "] values for ON [" + onPosition + "] and OFF [" + offPosition + "] position.", e); } cachePinValues(pin, onPosition, offPosition); } /** * Permanently sets the output to High (no PWM anymore).
* The LEDn_ON_H output control bit 4, when set to logic 1, causes the output to be always ON. * * @param pin represents channel 0..15 */ public void setAlwaysOn(Pin pin) { final int pwmOnValue = 0x1000; final int pwmOffValue = 0x0000; validatePin(pin, pwmOnValue, pwmOffValue); final int channel = pin.getAddress(); try { device.write(PCA9685A_LED0_ON_L + 4 * channel, (byte) 0x00); device.write(PCA9685A_LED0_ON_H + 4 * channel, (byte) 0x10); // set bit 4 to high device.write(PCA9685A_LED0_OFF_L + 4 * channel, (byte) 0x00); device.write(PCA9685A_LED0_OFF_H + 4 * channel, (byte) 0x00); } catch (IOException e) { throw new RuntimeException("Error while trying to set channel [" + channel + "] always ON.", e); } cachePinValues(pin, pwmOnValue, pwmOffValue); } /** * Permanently sets the output to Low (no PWM anymore).
* The LEDn_OFF_H output control bit 4, when set to logic 1, causes the output to be always OFF. * In this case the values in the LEDn_ON registers are ignored. * * @param pin represents channel 0..15 */ public void setAlwaysOff(Pin pin) { final int pwmOnValue = 0x0000; final int pwmOffValue = 0x1000; final boolean live = !isShutdown(); if (live) validatePin(pin, pwmOnValue, pwmOffValue); final int channel = pin.getAddress(); try { // device.write(PCA9685A_LED0_ON_L + 4 * channel, (byte) 0x00); // device.write(PCA9685A_LED0_ON_H + 4 * channel, (byte) 0x00); // device.write(PCA9685A_LED0_OFF_L + 4 * channel, (byte) 0x00); device.write(PCA9685A_LED0_OFF_H + 4 * channel, (byte) 0x10); // set bit 4 to high } catch (IOException e) { throw new RuntimeException("Error while trying to set channel [" + channel + "] always OFF.", e); } if (live) cachePinValues(pin, pwmOnValue, pwmOffValue); } public BigDecimal getFrequency() { return frequency; } public int getPeriodDurationMicros() { return periodDurationMicros; } /** * Calculates the OFF position for a certain pulse duration. * * @param duration pulse duration in microseconds * @return OFF position(value between 1 and 4095) */ public int calculateOffPositionForPulseDuration(int duration) { validatePwmDuration(duration); int result = (PWM_STEPS - 1) * duration / periodDurationMicros; if (result < 1) { result = 1; } else if (result > PWM_STEPS - 1) { result = PWM_STEPS - 1; } return result; } private int calculatePeriodDuration() { return new BigDecimal("1000000").divide(frequency, 0, RoundingMode.HALF_UP).intValue(); } private int calculatePrescale(BigDecimal frequencyCorrectionFactor) { BigDecimal theoreticalPrescale = BigDecimal.valueOf(INTERNAL_CLOCK_FREQ); theoreticalPrescale = theoreticalPrescale.divide(BigDecimal.valueOf(PWM_STEPS), 3, RoundingMode.HALF_UP); theoreticalPrescale = theoreticalPrescale.divide(frequency, 0, RoundingMode.HALF_UP); theoreticalPrescale = theoreticalPrescale.subtract(BigDecimal.ONE); return theoreticalPrescale.multiply(frequencyCorrectionFactor).intValue(); } private void validateFrequency(BigDecimal frequency) { if (frequency.compareTo(MIN_FREQUENCY) < 0 || frequency.compareTo(MAX_FREQUENCY) > 0) { throw new ValidationException("Frequency [" + frequency + "] must be between 40.0 and 1000.0 Hz."); } } private void validatePwmValueInRange(int pwmValue) { if (pwmValue < 0 || pwmValue > 4095) { throw new ValidationException("PWM position value [" + pwmValue + "] must be between 0 and 4095."); } } private void validatePwmDuration(int duration) { if (duration < 1) { throw new ValidationException("Duration [" + duration + "] must be >= 1us."); } if (duration >= periodDurationMicros) { throw new ValidationException("Duration [" + duration + "] must be <= period duration [" + periodDurationMicros + "]."); } } private void validatePin(Pin pin, int onPosition, int offPosition) { if (!hasPin(pin)) { throw new InvalidPinException(pin); } PinMode mode = getMode(pin); if (mode != PinMode.PWM_OUTPUT) { throw new InvalidPinModeException(pin, "Invalid pin mode [" + mode.getName() + "]; unable to setPwm(" + onPosition + ", " + offPosition + ")"); } } private void cachePinValues(Pin pin, int onPosition, int offPosition) { getPinCache(pin).setPwmOnValue (onPosition ); getPinCache(pin).setPwmOffValue (offPosition); } /** * @param pin represents channel 0..15 * @return [0]: onValue, [1]: offValue */ public int[] getPwmOnOffValues(Pin pin) { if (!hasPin(pin)) { throw new InvalidPinException(pin); } int[] pwmValues = { getPinCache(pin).getPwmOnValue(), getPinCache(pin).getPwmOffValue() }; return pwmValues; } /** * Reset all outputs (set to always OFF) */ public void reset() { for (Pin pin : PCA9685Pin.ALL) { setAlwaysOff(pin); } } @Override protected PCA9685GpioProviderPinCache getPinCache(Pin pin) { PCA9685GpioProviderPinCache pc = cache[pin.getAddress()]; if (pc == null) { pc = cache[pin.getAddress()] = new PCA9685GpioProviderPinCache(pin); } return pc; } @Override public int getPwm(Pin pin) { throw new UnsupportedOperationException("Use getPwmOnOffValues() instead."); } @Override public String getName() { return NAME; } @Override public void shutdown() { if (isShutdown()) { return; } super.shutdown(); reset(); try { // if we are the owner of the I2C bus, then close it if(i2cBusOwner) { // close the I2C bus communication bus.close(); } } catch (IOException e) { throw new RuntimeException(e); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy