mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2024-12-25 13:41:23 +01:00
86c3020672
No functional change. VPINs are unsigned integers in the range 0-65535 (although the highest values are special, 65535=VPIN_NONE). Values above 32767 were erroneously being displayed as negative. This has been fixed, which is a pre-requisite for allowing VPINs above 32767 to be used.
170 lines
6.6 KiB
C++
170 lines
6.6 KiB
C++
/*
|
|
* © 2023, Neil McKechnie. All rights reserved.
|
|
*
|
|
* This file is part of DCC++EX API
|
|
*
|
|
* This is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* It 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 Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
* This driver performs the basic interface between the HAL and an
|
|
* I2C-connected PCA9685 16-channel PWM module. When requested, it
|
|
* commands the device to set the PWM mark-to-period ratio accordingly.
|
|
* The call to IODevice::writeAnalogue(vpin, value) specifies the
|
|
* desired value in the range 0-4095 (0=0% and 4095=100%).
|
|
*
|
|
* This driver can be used for simple servo control by writing values between
|
|
* about 102 and 450 (extremes of movement for 9g micro servos) or 150 to 250
|
|
* for a more restricted range (corresponding to 1.5ms to 2.5ms pulse length).
|
|
* A value of zero will switch off the servo. To create the device, use
|
|
* the following syntax:
|
|
*
|
|
* PCA9685_basic::create(vpin, npins, i2caddress);
|
|
*
|
|
* For LED control, a value of 0 is fully off, and 4095 is fully on. It is
|
|
* recommended, to reduce flicker of LEDs, that the frequency be configured
|
|
* to a value higher than the default of 50Hz. To do this, create the device
|
|
* as follows, for a frequency of 200Hz.:
|
|
*
|
|
* PCA9685_basic::create(vpin, npins, i2caddress, 200);
|
|
*
|
|
*/
|
|
|
|
#ifndef PCA9685_BASIC_H
|
|
#define PCA9685_BASIC_H
|
|
|
|
#include "IODevice.h"
|
|
#include "I2CManager.h"
|
|
#include "DIAG.h"
|
|
|
|
/*
|
|
* IODevice subclass for PCA9685 16-channel PWM module.
|
|
*/
|
|
|
|
class PCA9685pwm : public IODevice {
|
|
public:
|
|
// Create device driver instance.
|
|
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency = 50) {
|
|
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCA9685pwm(firstVpin, nPins, i2cAddress, frequency);
|
|
}
|
|
|
|
private:
|
|
|
|
// structures for setting up non-blocking writes to PWM controller
|
|
I2CRB requestBlock;
|
|
uint8_t outputBuffer[5];
|
|
uint16_t prescaler;
|
|
|
|
// REGISTER ADDRESSES
|
|
const uint8_t PCA9685_MODE1=0x00; // Mode Register
|
|
const uint8_t PCA9685_FIRST_SERVO=0x06; /** low uint8_t first PWM register ON*/
|
|
const uint8_t PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
|
|
// MODE1 bits
|
|
const uint8_t MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
|
|
const uint8_t MODE1_AI=0x20; /**< Auto-Increment enabled */
|
|
const uint8_t MODE1_RESTART=0x80; /**< Restart enabled */
|
|
|
|
const uint32_t FREQUENCY_OSCILLATOR=25000000; /** Accurate enough for our purposes */
|
|
const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
|
|
const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
|
|
|
|
// Constructor
|
|
PCA9685pwm(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {
|
|
_firstVpin = firstVpin;
|
|
_nPins = (nPins>16) ? 16 : nPins;
|
|
_I2CAddress = i2cAddress;
|
|
if (frequency > 1526) frequency = 1526;
|
|
else if (frequency < 24) frequency = 24;
|
|
prescaler = FREQUENCY_OSCILLATOR / 4096 / frequency;
|
|
addDevice(this);
|
|
|
|
// Initialise structure used for setting pulse rate
|
|
requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));
|
|
}
|
|
|
|
// Device-specific initialisation
|
|
void _begin() override {
|
|
I2CManager.begin();
|
|
I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C
|
|
// In reality, other devices including the Arduino will limit
|
|
// the clock speed to a lower rate.
|
|
|
|
// Initialise I/O module here.
|
|
if (I2CManager.exists(_I2CAddress)) {
|
|
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
|
|
writeRegister(_I2CAddress, PCA9685_PRESCALE, prescaler);
|
|
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_AI);
|
|
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
|
|
// In theory, we should wait 500us before sending any other commands to each device, to allow
|
|
// the PWM oscillator to get running. However, we don't do any specific wait, as there's
|
|
// plenty of other stuff to do before we will send a command.
|
|
#if defined(DIAG_IO)
|
|
_display();
|
|
#endif
|
|
} else
|
|
_deviceState = DEVSTATE_FAILED;
|
|
}
|
|
|
|
// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().
|
|
//
|
|
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override {
|
|
(void)param1; (void)param2; // suppress compiler warning
|
|
#ifdef DIAG_IO
|
|
DIAG(F("PCA9685pwm WriteAnalogue VPIN:%u Value:%d %S"),
|
|
vpin, value, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F(""));
|
|
#endif
|
|
if (_deviceState == DEVSTATE_FAILED) return;
|
|
int pin = vpin - _firstVpin;
|
|
if (value > 4095) value = 4095;
|
|
else if (value < 0) value = 0;
|
|
|
|
writeDevice(pin, value);
|
|
}
|
|
|
|
// Display details of this device.
|
|
void _display() override {
|
|
DIAG(F("PCA9685pwm I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), (int)_firstVpin,
|
|
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
|
|
}
|
|
|
|
// writeDevice (helper function) takes a pin in range 0 to _nPins-1 within the device, and a value
|
|
// between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.
|
|
void writeDevice(uint8_t pin, int value) {
|
|
#ifdef DIAG_IO
|
|
DIAG(F("PCA9685pwm I2C:%s WriteDevice Pin:%d Value:%d"), _I2CAddress.toString(), pin, value);
|
|
#endif
|
|
// Wait for previous request to complete
|
|
uint8_t status = requestBlock.wait();
|
|
if (status != I2C_STATUS_OK) {
|
|
_deviceState = DEVSTATE_FAILED;
|
|
DIAG(F("PCA9685pwm I2C:%s failed %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
|
|
} else {
|
|
// Set up new request.
|
|
outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;
|
|
outputBuffer[1] = 0;
|
|
outputBuffer[2] = (value == 4095 ? 0x10 : 0); // 4095=full on
|
|
outputBuffer[3] = value & 0xff;
|
|
outputBuffer[4] = value >> 8;
|
|
I2CManager.queueRequest(&requestBlock);
|
|
}
|
|
}
|
|
|
|
// Internal helper function for this device
|
|
static void writeRegister(I2CAddress address, uint8_t reg, uint8_t value) {
|
|
I2CManager.write(address, 2, reg, value);
|
|
}
|
|
|
|
};
|
|
|
|
#endif |