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

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

The newest version!
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.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.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;
	private static final int RANGE = (int) Math.pow(2, 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 I2CDevice i2cDevice;
	private String keyPrefix;
	private int boardPwmFrequency = DEFAULT_PWM_FREQUENCY;
	private double periodUs = 1_000_000.0 / DEFAULT_PWM_FREQUENCY;
	private final BoardPinInfo boardPinInfo;

	public PCA9685() throws RuntimeIOException {
		this(I2CConstants.CONTROLLER_1, DEFAULT_ADDRESS, DEFAULT_PWM_FREQUENCY);
	}

	public PCA9685(int pwmFrequency) throws RuntimeIOException {
		this(I2CConstants.CONTROLLER_1, DEFAULT_ADDRESS, pwmFrequency);
	}

	public PCA9685(int controller, int pwmFrequency) throws RuntimeIOException {
		this(controller, DEFAULT_ADDRESS, pwmFrequency);
	}

	public PCA9685(int controller, int address, int pwmFrequency) throws RuntimeIOException {
		super(DEVICE_NAME + "-" + controller + "-" + address);

		i2cDevice = I2CDevice.builder(address).setController(controller).setByteOrder(ByteOrder.BIG_ENDIAN).build();
		boardPinInfo = new PCA9685BoardPinInfo();

		reset();

		setPwmFreq(pwmFrequency);
	}

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

	/**
	 * Sets the PWM frequency
	 *
	 * @param pwmFrequency desired frequency. 40Hz to 1000Hz 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

		float prescale_flt = (((float) CLOCK_FREQ) / RANGE / pwmFrequency) - 1;
		int prescale_int = Math.round(prescale_flt);
		Logger.debug("Setting PWM frequency to {} Hz, float pre-scale: {}, int prescale {}", pwmFrequency,
				String.format("%.2f", prescale_flt), prescale_int);

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

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

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

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

		Logger.debug("channel={}, on={}, off={}", channel, on, 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()? // i2cDevice.writeWordData(LED0_ON_L + 4 * channel, (short) on); // i2cDevice.writeWordData(LED0_OFF_L + 4 * channel, (short) off); i2cDevice.writeByteData(LED0_ON_L + 4 * channel, on & 0xFF); i2cDevice.writeByteData(LED0_ON_H + 4 * channel, on >> 8); i2cDevice.writeByteData(LED0_OFF_L + 4 * channel, off & 0xFF); i2cDevice.writeByteData(LED0_OFF_H + 4 * channel, off >> 8); // SleepUtil.sleepMillis(50); } private static void validateOnOff(int on, int off) { if (on < 0 || on >= RANGE) { throw new IllegalArgumentException(String.format("Error: on (" + on + ") must be 0.." + (RANGE - 1))); } if (off < 0 || off >= RANGE) { throw new IllegalArgumentException(String.format("Error: off (" + off + ") must be 0.." + (RANGE - 1))); } // 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 >= RANGE) { throw new IllegalArgumentException(String.format("Error: on (%d) + off (%d) must be < %d", on, off, RANGE)); } } 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()? // i2cDevice.writeShort(ALL_LED_ON_L, (short)on); // i2cDevice.writeShort(ALL_LED_OFF_L, (short)off); i2cDevice.writeByteData(ALL_LED_ON_L, on & 0xFF); i2cDevice.writeByteData(ALL_LED_ON_H, on >> 8); i2cDevice.writeByteData(ALL_LED_OFF_L, off & 0xFF); i2cDevice.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(); i2cDevice.close(); } public void closeChannel(int channel) throws RuntimeIOException { Logger.trace("closeChannel({})", 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", pwmFrequency, 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", frequency, 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) RANGE; } /** * 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 * RANGE); setPwm(channel, 0, off); } 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) * RANGE); if (Logger.isDebugEnabled()) { Logger.debug("Requested duty value: {}, Scale: {} microseconds per bit, Off: {}", String.format("%.2f", (double) dutyUs), String.format("%.4f", periodUs / RANGE), 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 - 2024 Weber Informatics LLC | Privacy Policy