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

com.diozero.devices.PCA9685 Maven / Gradle / Ivy

package com.diozero.devices;

/*
 * #%L
 * Organisation: diozero
 * Project:      diozero - Core
 * Filename:     PCA9685.java
 * 
 * This file is part of the diozero project. More information about this project
 * can be found at https://www.diozero.com/.
 * %%
 * Copyright (C) 2016 - 2024 diozero
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */

import java.nio.ByteOrder;
import java.util.EnumSet;

import org.tinylog.Logger;

import com.diozero.api.DeviceInterface;
import com.diozero.api.DeviceMode;
import com.diozero.api.I2CConstants;
import com.diozero.api.I2CDevice;
import com.diozero.api.I2CDeviceInterface;
import com.diozero.api.PinInfo;
import com.diozero.api.RuntimeIOException;
import com.diozero.internal.spi.AbstractDevice;
import com.diozero.internal.spi.AbstractDeviceFactory;
import com.diozero.internal.spi.InternalPwmOutputDeviceInterface;
import com.diozero.internal.spi.InternalServoDeviceInterface;
import com.diozero.internal.spi.PwmOutputDeviceFactoryInterface;
import com.diozero.internal.spi.ServoDeviceFactoryInterface;
import com.diozero.sbc.BoardPinInfo;
import com.diozero.util.BitManipulation;
import com.diozero.util.RangeUtil;
import com.diozero.util.SleepUtil;

/**
 * PCA9685 I2C-bus controlled 16-channel 12-bit PWM controller as used in the popular
 * Adafruit PWM add-on board:
 * Datasheet
 */
@SuppressWarnings("unused")
public class PCA9685 extends AbstractDeviceFactory
		implements PwmOutputDeviceFactoryInterface, ServoDeviceFactoryInterface, DeviceInterface {
	public static final int DEFAULT_ADDRESS = 0x40;
	private static final String DEVICE_NAME = "PCA9685";
	private static final int NUM_CHANNELS = 16;
	// 4095
	private static final int RANGE = RangeUtil.resolutionEndInclusive(12);
	private static final int MAX_VALUE = RangeUtil.resolutionEndExclusive(12);

	private static final int CLOCK_FREQ = 25_000_000; // 25MHz default osc clock

	// Registers/etc.
	private static final int MODE1 = 0x00;
	private static final int MODE2 = 0x01;
	private static final int SUBADR1 = 0x02;
	private static final int SUBADR2 = 0x03;
	private static final int SUBADR3 = 0x04;
	private static final int ALL_CALL = 0x05; // LED All Call I2C-bus address
	private static final int LED0 = 0x06;
	private static final int LED0_ON_L = LED0;
	private static final int LED0_ON_H = 0x07;
	private static final int LED0_OFF_L = 0x08;
	private static final int LED0_OFF_H = 0x09;
	private static final int ALL_LED_ON_L = 0xFA; // Load all the LEDn_ON registers, byte 0 (turn 0-7 channels on)
	private static final int ALL_LED_ON_H = 0xFB; // Load all the LEDn_ON registers, byte 1 (turn 8-15 channels on)
	private static final int ALL_LED_OFF_L = 0xFC; // Load all the LEDn_OFF registers, byte 0 (turn 0-7 channels off)
	private static final int ALL_LED_OFF_H = 0xFD; // Load all the LEDn_OFF registers, byte 1 (turn 8-15 channels off)
	private static final int PRESCALE = 0xFE; // Prescaler for output frequency

	// MODE1 bits
	private static final byte RESTART_BIT = 7;
	private static final byte RESTART_MASK = BitManipulation.getBitMask(RESTART_BIT);
	private static final byte SLEEP_BIT = 4;
	private static final byte SLEEP_MASK = BitManipulation.getBitMask(SLEEP_BIT);
	private static final byte ALLCALL_BIT = 0;
	private static final byte ALLCALL_MASK = BitManipulation.getBitMask(ALLCALL_BIT);

	// MODE2 bits
	/**
	 * 0: Output logic state not inverted. Value to use when external driver used
	 *
	 * 1: Output logic state inverted. Value to use when no external driver used
	 */
	private static final byte INVRT_BIT = 4;
	private static final byte INVRT_MASK = BitManipulation.getBitMask(INVRT_BIT);
	/**
	 * 0: The 16 LED outputs are configured with an open-drain structure
	 *
	 * 1: The 16 LED outputs are configured with a totem pole structure
	 */
	private static final byte OUTDRV_BIT = 2;
	private static final byte OUTDRV_MASK = BitManipulation.getBitMask(OUTDRV_BIT);

	private static final int MIN_PWM_FREQUENCY = 40;
	private static final int MAX_PWM_FREQUENCY = 1000;
	public static final int DEFAULT_PWM_FREQUENCY = 50;

	private final I2CDeviceInterface device;
	private String keyPrefix;
	private int boardPwmFrequency = DEFAULT_PWM_FREQUENCY;
	private double periodUs = 1_000_000.0 / DEFAULT_PWM_FREQUENCY;
	private final BoardPinInfo boardPinInfo;

	/**
	 * Default I2C bus, address, and PWM frequency.
	 *
	 * @throws RuntimeIOException on error
	 */
	public PCA9685() throws RuntimeIOException {
		this(I2CConstants.CONTROLLER_1, DEFAULT_ADDRESS, DEFAULT_PWM_FREQUENCY);
	}

	/**
	 * Default I2C bus, address.
	 *
	 * @param pwmFrequency the board frequency in Hz
	 * @throws RuntimeIOException on error
	 */
	public PCA9685(int pwmFrequency) throws RuntimeIOException {
		this(I2CConstants.CONTROLLER_1, DEFAULT_ADDRESS, pwmFrequency);
	}

	/**
	 * Default address.
	 *
	 * @param controller   I2C bus/controller
	 * @param pwmFrequency the board frequency in Hz
	 * @throws RuntimeIOException
	 */
	public PCA9685(int controller, int pwmFrequency) throws RuntimeIOException {
		this(controller, DEFAULT_ADDRESS, pwmFrequency);
	}

	/**
	 * @param controller   I2C bus/controller
	 * @param address      the device address
	 * @param pwmFrequency the board frequency in Hz
	 * @throws RuntimeIOException on error
	 */
	public PCA9685(int controller, int address, int pwmFrequency) throws RuntimeIOException {
		this(I2CDevice.builder(address)
					 .setController(controller)
					 .setByteOrder(ByteOrder.BIG_ENDIAN)
					 .build(),
			 pwmFrequency);
	}

	/**
	 * Default PWM frequency.
	 *
	 * @param device I2C device to use
	 */
	public PCA9685(I2CDeviceInterface device) {
		this(device, DEFAULT_PWM_FREQUENCY);
	}

	/**
	 * @param device       I2C device to use
	 * @param pwmFrequency the board frequency in Hz
	 * @throws RuntimeIOException on error
	 */
	public PCA9685(I2CDeviceInterface device, int pwmFrequency) throws RuntimeIOException {
		super(DEVICE_NAME + "-" + device.getController() + "-" + device.getAddress());
		this.device = device;
		boardPinInfo = new PCA9685BoardPinInfo();

		reset();

		setPwmFreq(pwmFrequency);
	}

	private void reset() throws RuntimeIOException {
		device.writeByteData(MODE1, 0); // Normal mode
		device.writeByteData(MODE2, OUTDRV_MASK); // Set output driver to totem pole mode (rather than open drain)
	}

	/**
	 * Sets the PWM frequency
	 *
	 * @param pwmFrequency desired frequency. Valid range is 40Hz to 1,000Hz using internal
	 *                     25MHz oscillator
	 * @throws RuntimeIOException if an I/O error occurs
	 */
	private void setPwmFreq(int pwmFrequency) throws RuntimeIOException {
		if (pwmFrequency < MIN_PWM_FREQUENCY || pwmFrequency > MAX_PWM_FREQUENCY) {
			throw new IllegalArgumentException("Invalid PWM Frequency value (" + pwmFrequency + ") must be "
					+ MIN_PWM_FREQUENCY + ".." + MAX_PWM_FREQUENCY);
		}

		// TODO Check if there are any devices currently provisioned

		final float prescale_flt = (((float) CLOCK_FREQ) / RANGE / pwmFrequency) - 1;
		final int prescale_int = Math.round(prescale_flt);
		Logger.debug("Setting PWM frequency to {} Hz, float pre-scale: {0.##}, int prescale {}",
				Integer.valueOf(pwmFrequency), Float.valueOf(prescale_flt), Integer.valueOf(prescale_int));

		final byte oldmode = device.readByteData(MODE1);
		// Enter low power mode (set the sleep bit)
		device.writeByteData(MODE1, (byte) ((oldmode & 0x7F) | SLEEP_MASK));
		device.writeByteData(PRESCALE, (byte) (prescale_int));
		boardPwmFrequency = pwmFrequency;
		periodUs = 1_000_000.0 / pwmFrequency;
		// Restore the previous mode1 value
		device.writeByteData(MODE1, oldmode);
		// Wait min 500us for the oscillator to stabilise
		SleepUtil.sleepMillis(1);
		// Set restart enabled
		device.writeByteData(MODE1, (byte) (oldmode | RESTART_MASK));
	}

	private int[] getPwm(int channel) throws RuntimeIOException {
		validateChannel(channel);

		short on_l = device.readUByte(LED0_ON_L + 4 * channel);
		short on_h = device.readUByte(LED0_ON_H + 4 * channel);
		int on = (on_h << 8) | on_l;

		short off_l = device.readUByte(LED0_OFF_L + 4 * channel);
		short off_h = device.readUByte(LED0_OFF_H + 4 * channel);
		int off = (off_h << 8) | off_l;

		Logger.debug("channel={}, on={}, off={}", Integer.valueOf(channel), Integer.valueOf(on), Integer.valueOf(off));

		return new int[] { on, off };
	}

	/**
	 * Sets a single PWM channel
	 *
	 * Example 1: (assumes that the LED0 output is used and (delay time) + (PWM duty cycle)
	 * <= 100 %)
	 *
	 * 
	 * Delay time = 10 %;
	 * PWM duty cycle = 20 % (LED on time = 20 %;
	 * LED off time = 80 %).
	 * Delay time = 10 % = 409.6 ~ 410 counts
	 * 
* * Since the counter starts at 0 and ends at 4095, we will subtract 1, so: * *
	 * delay time = 409
	 * counts LED on time = 20 % = 819.2 ~ 819
	 * counts LED off time = (decimal 409 + 819 = 1228)
	 * 
* * @param channel PWM channel * @param on on time * @param off off time * @throws RuntimeIOException if an I/O error occurs */ private void setPwm(int channel, int on, int off) throws RuntimeIOException { validateChannel(channel); validateOnOff(on, off); // Logger.debug("channel: {}, on: {}, off: {}", Integer.valueOf(channel), // Integer.valueOf(on), // Integer.valueOf(off)); // TODO Replace with writeWordData()? // device.writeWordData(LED0_ON_L + 4 * channel, (short) on); // device.writeWordData(LED0_OFF_L + 4 * channel, (short) off); device.writeByteData(LED0_ON_L + 4 * channel, on & 0xFF); device.writeByteData(LED0_ON_H + 4 * channel, on >> 8); device.writeByteData(LED0_OFF_L + 4 * channel, off & 0xFF); device.writeByteData(LED0_OFF_H + 4 * channel, off >> 8); // SleepUtil.sleepMillis(50); } private static void validateOnOff(int on, int off) { if (on < 0 || on > MAX_VALUE) { throw new IllegalArgumentException(String.format("Error: on (" + on + ") must be 0.." + MAX_VALUE)); } if (off < 0 || off > MAX_VALUE) { throw new IllegalArgumentException(String.format("Error: off (" + off + ") must be 0.." + MAX_VALUE)); } // Off must be after on if (off < on) { throw new IllegalArgumentException("Off value (" + off + ") must be > on value (" + on + ")"); } // Total must be < 4096 if (on + off > MAX_VALUE) { throw new IllegalArgumentException(String.format("Error: on (%d) + off (%d) must be < %d", Integer.valueOf(on), Integer.valueOf(off), Integer.valueOf(MAX_VALUE))); } } private static void validateChannel(int channel) { if (channel < 0 || channel >= NUM_CHANNELS) { throw new IllegalArgumentException( "Invalid PWM channel number (" + channel + "); channel must be 0.." + (NUM_CHANNELS - 1)); } } /** * Sets a all PWM channels * * @param on on time * @param off off time * @throws RuntimeIOException if an I/O error occurs */ private void setAllPwm(int on, int off) throws RuntimeIOException { validateOnOff(on, off); // TODO Replace with writeShort()? // device.writeShort(ALL_LED_ON_L, (short)on); // device.writeShort(ALL_LED_OFF_L, (short)off); device.writeByteData(ALL_LED_ON_L, on & 0xFF); device.writeByteData(ALL_LED_ON_H, on >> 8); device.writeByteData(ALL_LED_OFF_L, off & 0xFF); device.writeByteData(ALL_LED_OFF_H, off >> 8); } @Override public String getName() { return DEVICE_NAME; } @Override public void close() throws RuntimeIOException { Logger.trace("close()"); if (isClosed()) { return; } // Close all open pins before closing the I2C device itself super.close(); device.close(); } public void closeChannel(int channel) throws RuntimeIOException { Logger.trace("closeChannel({})", Integer.valueOf(channel)); setPwm(channel, 0, 0); } @Override public InternalPwmOutputDeviceInterface createPwmOutputDevice(String key, PinInfo pinInfo, int pwmFrequency, float initialValue) throws RuntimeIOException { // Note pwmFrequency is ignored; make sure you setup the board's PWM frequency // first if (pwmFrequency != boardPwmFrequency) { Logger.warn( "Specified PWM frequency ({}) is different to that configured for the board ({})" + "; this device has a common PWM frequency that is used for all outputs" + " - the requested frequency will be ignored", Integer.valueOf(pwmFrequency), Integer.valueOf(boardPwmFrequency)); } return new PCA9685ServoOrPwmOutputDevice(this, key, DeviceMode.PWM_OUTPUT, pinInfo.getDeviceNumber(), initialValue); } @Override public InternalServoDeviceInterface createServoDevice(String key, PinInfo pinInfo, int frequency, int minPulseWidthUs, int maxPulseWidthUs, int initialPulseWidthUs) throws RuntimeIOException { // Note frequency is ignored; make sure you setup the board's PWM frequency // first if (frequency != boardPwmFrequency) { Logger.warn( "Specified Servo frequency ({}) is different to that configured for the board ({})" + "; this device has a common PWM frequency that is used for all outputs" + " - the requested frequency will be ignored", Integer.valueOf(frequency), Integer.valueOf(boardPwmFrequency)); } return new PCA9685ServoOrPwmOutputDevice(this, key, DeviceMode.SERVO, pinInfo.getDeviceNumber(), (float) (initialPulseWidthUs / periodUs)); } /** * Get the PWM output percentage for the specified channel, range 0..1 * * @param channel PWM channel * @return output PWM "on" percentage, range 0..1 * @throws RuntimeIOException if an I/O error occurs */ public float getValue(int channel) throws RuntimeIOException { int[] on_off = getPwm(channel); // return (on_off[1] - on_off[0]) / (float) MAX_VALUE; return RangeUtil.map(on_off[1] - on_off[0], MAX_VALUE, 1f); } /** * Set PWM output percentage for the specified channel, value must be 0..1 * * @param channel PWM channel * @param value Must be 0..1 * @throws RuntimeIOException if an I/O error occurs */ public void setValue(int channel, float value) throws RuntimeIOException { if (value < 0 || value > 1) { throw new IllegalArgumentException("PWM value must 0..1, you requested " + value); } // int off = Math.round(value * MAX_VALUE); setPwm(channel, 0, RangeUtil.map(value, 1, MAX_VALUE)); } public int getDutyUs(int channel) { return (int) Math.round(getValue(channel) * periodUs); } /** * Set PWM duty cycle output in microseconds for the specified channel. A standard servo * has a minimum range of 1-2 milliseconds. The actual range varies between devices. E.g. * My TowerPro SG90 has a pulse width range of 500-2,400 us. * * @param channel PWM channel * @param dutyUs New duty value in microseconds * @throws RuntimeIOException if an I/O error occurs */ public void setDutyUs(int channel, int dutyUs) throws RuntimeIOException { // TODO Bounds checking // int off = (int) Math.round((dutyUs / periodUs) * MAX_VALUE); int off = (int) Math.round(RangeUtil.map(dutyUs, periodUs, MAX_VALUE)); if (Logger.isDebugEnabled()) { Logger.debug("Requested duty value: {}, Scale: {0.####} microseconds per bit, Off: {}", Integer.valueOf(dutyUs), Double.valueOf(periodUs / MAX_VALUE), Integer.valueOf(off)); } setPwm(channel, 0, off); } @Override public int getBoardPwmFrequency() { return boardPwmFrequency; } @Override public void setBoardPwmFrequency(int pwmFrequency) { setPwmFreq(pwmFrequency); } @Override public int getBoardServoFrequency() { return boardPwmFrequency; } @Override public void setBoardServoFrequency(int pwmFrequency) { setPwmFreq(pwmFrequency); } @Override public BoardPinInfo getBoardPinInfo() { return boardPinInfo; } public static class PCA9685BoardPinInfo extends BoardPinInfo { public PCA9685BoardPinInfo() { for (int i = 0; i < 8; i++) { addGpioPinInfo(i, 6 + i, EnumSet.of(DeviceMode.PWM_OUTPUT, DeviceMode.SERVO)); } for (int i = 8; i < 16; i++) { addGpioPinInfo(i, 7 + i, EnumSet.of(DeviceMode.PWM_OUTPUT, DeviceMode.SERVO)); } } } private static class PCA9685ServoOrPwmOutputDevice extends AbstractDevice implements InternalPwmOutputDeviceInterface, InternalServoDeviceInterface { private final PCA9685 pca9685; private final DeviceMode mode; private final int channel; public PCA9685ServoOrPwmOutputDevice(PCA9685 pca9685, String key, DeviceMode mode, int channel, float initialValue) { super(key, pca9685); this.pca9685 = pca9685; this.mode = mode; this.channel = channel; setValue(initialValue); } @Override public DeviceMode getMode() { return mode; } @Override public int getGpio() { return channel; } @Override public int getPwmNum() { return channel; } @Override public int getServoNum() { return channel; } @Override public float getValue() throws RuntimeIOException { return pca9685.getValue(channel); } @Override public void setValue(float value) throws RuntimeIOException { pca9685.setValue(channel, value); } @Override public int getPulseWidthUs() throws RuntimeIOException { return pca9685.getDutyUs(channel); } @Override public void setPulseWidthUs(int pulseWidthUs) throws RuntimeIOException { pca9685.setDutyUs(channel, pulseWidthUs); } @Override protected void closeDevice() throws RuntimeIOException { Logger.trace("closeDevice() {}", getKey()); pca9685.closeChannel(channel); } @Override public int getPwmFrequency() { return pca9685.getBoardPwmFrequency(); } @Override public void setPwmFrequency(int frequency) throws RuntimeIOException { throw new UnsupportedOperationException( "The PCA9685 has a PWM output frequency that is shared across all channels hence " + "cannot be changed for individual channels"); } @Override public int getServoFrequency() { return pca9685.getBoardServoFrequency(); } @Override public void setServoFrequency(int frequency) throws RuntimeIOException { throw new UnsupportedOperationException( "The PCA9685 has a PWM output frequency that is shared across all channels hence " + "cannot be changed for individual channels"); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy