From a17c02444d4157b8ce9a657af43b1663a9ddcefc Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 01/52] Refactored, analogue tested --- IO_EXIOExpander.h | 97 ++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index d54feac..724ea42 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -54,22 +54,18 @@ */ class EXIOExpander : public IODevice { public: - static void create(VPIN vpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { - if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress, numDigitalPins, numAnaloguePins); + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor - EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { + EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; - _numDigitalPins = numDigitalPins; - _numAnaloguePins = numAnaloguePins; - _digitalPinBytes = (numDigitalPins+7)/8; - _analoguePinBytes = numAnaloguePins * 2; + _digitalPinBytes = (nPins+7)/8; _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); - _analogueInputStates=(byte*) calloc(_analoguePinBytes,1); addDevice(this); } @@ -77,20 +73,26 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { - _digitalOutBuffer[0] = EXIOINIT; - _digitalOutBuffer[1] = _numDigitalPins; - _digitalOutBuffer[2] = _numAnaloguePins; - // Send config, if EXIORDY returned, we're good, otherwise go offline - I2CManager.read(_i2cAddress, _commandBuffer, 1, _digitalOutBuffer, 3); - if (_commandBuffer[0] != EXIORDY) { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; return; } + // We now need to retrieve the analogue pin map + _command1Buffer[0] = EXIOINITA; + I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline - // Using digital in buffer in reverse to save RAM - _commandBuffer[0] = EXIOVER; - I2CManager.read(_i2cAddress, _versionBuffer, 3, _commandBuffer, 1); + _command1Buffer[0] = EXIOVER; + I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); _majorVer = _versionBuffer[0]; _minorVer = _versionBuffer[1]; _patchVer = _versionBuffer[2]; @@ -105,13 +107,10 @@ private: } } + // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (configType != CONFIGURE_INPUT) return false; if (paramCount != 1) return false; - if (vpin >= _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is an analogue pin, cannot use as a digital pin"), vpin); - return false; - } bool pullup = params[0]; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIODPUP; @@ -121,37 +120,39 @@ private: return true; } - // We only use this to detect incorrect use of analogue pins + // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is a digital pin, cannot use as an analogue pin"), vpin); - return false; - } int pin = vpin - _firstVpin; - _analogueOutBuffer[0] = EXIOENAN; - _analogueOutBuffer[1] = pin; - I2CManager.write(_i2cAddress, _analogueOutBuffer, 2); + _command2Buffer[0] = EXIOENAN; + _command2Buffer[1] = pin; + I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } + // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning - _commandBuffer[0] = EXIORDD; - I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _commandBuffer, 1); - _commandBuffer[0] = EXIORDAN; - I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _commandBuffer, 1); + _command1Buffer[0] = EXIORDD; + I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDAN; + I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } + // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) return false; - int pin = vpin - _firstVpin - _numDigitalPins; - uint8_t _pinLSBByte = pin * 2; + int pin = vpin - _firstVpin; + uint8_t _pinLSBByte; + for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { + if (_analoguePinMap[aPin] == pin) { + _pinLSBByte = aPin * 2; + } + } uint8_t _pinMSBByte = _pinLSBByte + 1; return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } + // Obtain the correct digital input value int _read(VPIN vpin) override { - if (vpin >= _firstVpin + _numDigitalPins) return false; int pin = vpin - _firstVpin; uint8_t pinByte = pin / 8; bool value = _digitalInputStates[pinByte] >> (pin - pinByte * 8); @@ -159,7 +160,6 @@ private: } void _write(VPIN vpin, int value) override { - if (vpin >= _firstVpin + _numDigitalPins) return; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIOWRD; _digitalOutBuffer[1] = pin; @@ -168,25 +168,14 @@ private: } void _display() override { - int _firstAnalogue, _lastAnalogue; - if (_numAnaloguePins == 0) { - _firstAnalogue = 0; - _lastAnalogue = 0; - } else { - _firstAnalogue = _firstVpin + _numDigitalPins; - _lastAnalogue = _firstVpin + _nPins - 1; - } - DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d: %d Digital Vpins %d-%d, %d Analogue Vpins %d-%d %S"), + DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, - _numDigitalPins, _firstVpin, _firstVpin + _numDigitalPins - 1, - _numAnaloguePins, _firstAnalogue, _lastAnalogue, + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } uint8_t _i2cAddress; - uint8_t _numDigitalPins; uint8_t _numAnaloguePins; - byte _analogueOutBuffer[2]; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -196,7 +185,10 @@ private: byte* _analogueInputStates; uint8_t _digitalPinBytes = 0; uint8_t _analoguePinBytes = 0; - byte _commandBuffer[1]; + byte _command1Buffer[1]; + byte _command2Buffer[2]; + byte _receive2Buffer[2]; + uint8_t* _analoguePinMap; enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure @@ -207,6 +199,7 @@ private: EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input EXIOENAN = 0xE7, // Flag eo enable an analogue pin + EXIOINITA = 0xE8, // Flag we're receiving analogue pin info }; }; From d8a1bcaf34b6a66b015045e3e02ff3e62876e470 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 02/52] Refactored, analogue tested --- IO_EXIOExpander.h | 97 ++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index d54feac..724ea42 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -54,22 +54,18 @@ */ class EXIOExpander : public IODevice { public: - static void create(VPIN vpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { - if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress, numDigitalPins, numAnaloguePins); + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor - EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { + EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; - _numDigitalPins = numDigitalPins; - _numAnaloguePins = numAnaloguePins; - _digitalPinBytes = (numDigitalPins+7)/8; - _analoguePinBytes = numAnaloguePins * 2; + _digitalPinBytes = (nPins+7)/8; _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); - _analogueInputStates=(byte*) calloc(_analoguePinBytes,1); addDevice(this); } @@ -77,20 +73,26 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { - _digitalOutBuffer[0] = EXIOINIT; - _digitalOutBuffer[1] = _numDigitalPins; - _digitalOutBuffer[2] = _numAnaloguePins; - // Send config, if EXIORDY returned, we're good, otherwise go offline - I2CManager.read(_i2cAddress, _commandBuffer, 1, _digitalOutBuffer, 3); - if (_commandBuffer[0] != EXIORDY) { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; return; } + // We now need to retrieve the analogue pin map + _command1Buffer[0] = EXIOINITA; + I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline - // Using digital in buffer in reverse to save RAM - _commandBuffer[0] = EXIOVER; - I2CManager.read(_i2cAddress, _versionBuffer, 3, _commandBuffer, 1); + _command1Buffer[0] = EXIOVER; + I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); _majorVer = _versionBuffer[0]; _minorVer = _versionBuffer[1]; _patchVer = _versionBuffer[2]; @@ -105,13 +107,10 @@ private: } } + // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (configType != CONFIGURE_INPUT) return false; if (paramCount != 1) return false; - if (vpin >= _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is an analogue pin, cannot use as a digital pin"), vpin); - return false; - } bool pullup = params[0]; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIODPUP; @@ -121,37 +120,39 @@ private: return true; } - // We only use this to detect incorrect use of analogue pins + // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is a digital pin, cannot use as an analogue pin"), vpin); - return false; - } int pin = vpin - _firstVpin; - _analogueOutBuffer[0] = EXIOENAN; - _analogueOutBuffer[1] = pin; - I2CManager.write(_i2cAddress, _analogueOutBuffer, 2); + _command2Buffer[0] = EXIOENAN; + _command2Buffer[1] = pin; + I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } + // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning - _commandBuffer[0] = EXIORDD; - I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _commandBuffer, 1); - _commandBuffer[0] = EXIORDAN; - I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _commandBuffer, 1); + _command1Buffer[0] = EXIORDD; + I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDAN; + I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } + // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) return false; - int pin = vpin - _firstVpin - _numDigitalPins; - uint8_t _pinLSBByte = pin * 2; + int pin = vpin - _firstVpin; + uint8_t _pinLSBByte; + for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { + if (_analoguePinMap[aPin] == pin) { + _pinLSBByte = aPin * 2; + } + } uint8_t _pinMSBByte = _pinLSBByte + 1; return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } + // Obtain the correct digital input value int _read(VPIN vpin) override { - if (vpin >= _firstVpin + _numDigitalPins) return false; int pin = vpin - _firstVpin; uint8_t pinByte = pin / 8; bool value = _digitalInputStates[pinByte] >> (pin - pinByte * 8); @@ -159,7 +160,6 @@ private: } void _write(VPIN vpin, int value) override { - if (vpin >= _firstVpin + _numDigitalPins) return; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIOWRD; _digitalOutBuffer[1] = pin; @@ -168,25 +168,14 @@ private: } void _display() override { - int _firstAnalogue, _lastAnalogue; - if (_numAnaloguePins == 0) { - _firstAnalogue = 0; - _lastAnalogue = 0; - } else { - _firstAnalogue = _firstVpin + _numDigitalPins; - _lastAnalogue = _firstVpin + _nPins - 1; - } - DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d: %d Digital Vpins %d-%d, %d Analogue Vpins %d-%d %S"), + DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, - _numDigitalPins, _firstVpin, _firstVpin + _numDigitalPins - 1, - _numAnaloguePins, _firstAnalogue, _lastAnalogue, + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } uint8_t _i2cAddress; - uint8_t _numDigitalPins; uint8_t _numAnaloguePins; - byte _analogueOutBuffer[2]; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -196,7 +185,10 @@ private: byte* _analogueInputStates; uint8_t _digitalPinBytes = 0; uint8_t _analoguePinBytes = 0; - byte _commandBuffer[1]; + byte _command1Buffer[1]; + byte _command2Buffer[2]; + byte _receive2Buffer[2]; + uint8_t* _analoguePinMap; enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure @@ -207,6 +199,7 @@ private: EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input EXIOENAN = 0xE7, // Flag eo enable an analogue pin + EXIOINITA = 0xE8, // Flag we're receiving analogue pin info }; }; From 28caa9e8d3cc4c067bd92c75a8c3bb2da4cc84b6 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 19:26:33 +1000 Subject: [PATCH 03/52] Brief PWM start --- IO_EXIOExpander.h | 76 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 724ea42..f284bc1 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -1,5 +1,5 @@ /* - * © 2021, Peter Cole. All rights reserved. + * © 2022, Peter Cole. All rights reserved. * * This file is part of EX-CommandStation * @@ -26,19 +26,14 @@ * (Note the device driver is included by default) * * void halSetup() { -* // EXIOExpander::create(vpin, num_vpins, i2c_address, digitalPinCount, analoguePinCount); -* EXIOExpander::create(800, 18, 0x65, 12, 8); +* // EXIOExpander::create(vpin, num_vpins, i2c_address); +* EXIOExpander::create(800, 18, 0x65); * } * -* Note when defining the number of digital and analogue pins, there is no way to sanity check -* this from the device driver, and it is up to the user to define the correct values here. -* -* All pins available on the EX-IOExpander device must be accounted for. -* -* Vpins are allocated to digital pins first, and then analogue pins, so digital pins will -* populate the first part of the specified vpin range, with the analogue pins populating the -* last part of the vpin range. -* Eg. for a default Nano, 800 - 811 are digital (D2 - D13), 812 to 817 are analogue (A0 - A3, A6/A7). +* All pins on an EX-IOExpander device are allocated according to the pin map for the specific +* device in use. There is no way for the device driver to sanity check pins are used for the +* correct purpose, however the EX-IOExpander device's pin map will prevent pins being used +* incorrectly (eg. A6/7 on Nano cannot be used for digital input/output). */ #ifndef IO_EX_IOEXPANDER_H @@ -76,12 +71,15 @@ private: _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive3Buffer[1]; + _numPWMPins = _receive3Buffer[2]; _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + _servoData = (struct ServoData*) calloc(_numPWMPins, 14); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -109,15 +107,24 @@ private: // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { - if (configType != CONFIGURE_INPUT) return false; if (paramCount != 1) return false; - bool pullup = params[0]; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + if (configType == CONFIGURE_INPUT) { + bool pullup = params[0]; + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; + } else if (configType == CONFIGURE_SERVO) { + DIAG(F("Configure servo at pin %d"), (int)pin); + for (int i = 0; i < paramCount; i++) { + DIAG(F("Param %d is %x"), (int)i, params[i]); + } + return true; + } else { + return false; + } } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -175,7 +182,7 @@ private: } uint8_t _i2cAddress; - uint8_t _numAnaloguePins; + uint8_t _numAnaloguePins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -187,8 +194,29 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; + uint8_t _numPWMPins = 0; + + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + } ServoData; // 14 bytes per element, i.e. per pin in use + + struct ServoData* _servoData; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const byte FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From 7f19a92d2a0ca04b68af756329d5781e95cf1424 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 04/52] Refactored, analogue tested --- IO_EXIOExpander.h | 97 ++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index d54feac..724ea42 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -54,22 +54,18 @@ */ class EXIOExpander : public IODevice { public: - static void create(VPIN vpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { - if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress, numDigitalPins, numAnaloguePins); + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor - EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { + EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; - _numDigitalPins = numDigitalPins; - _numAnaloguePins = numAnaloguePins; - _digitalPinBytes = (numDigitalPins+7)/8; - _analoguePinBytes = numAnaloguePins * 2; + _digitalPinBytes = (nPins+7)/8; _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); - _analogueInputStates=(byte*) calloc(_analoguePinBytes,1); addDevice(this); } @@ -77,20 +73,26 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { - _digitalOutBuffer[0] = EXIOINIT; - _digitalOutBuffer[1] = _numDigitalPins; - _digitalOutBuffer[2] = _numAnaloguePins; - // Send config, if EXIORDY returned, we're good, otherwise go offline - I2CManager.read(_i2cAddress, _commandBuffer, 1, _digitalOutBuffer, 3); - if (_commandBuffer[0] != EXIORDY) { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; return; } + // We now need to retrieve the analogue pin map + _command1Buffer[0] = EXIOINITA; + I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline - // Using digital in buffer in reverse to save RAM - _commandBuffer[0] = EXIOVER; - I2CManager.read(_i2cAddress, _versionBuffer, 3, _commandBuffer, 1); + _command1Buffer[0] = EXIOVER; + I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); _majorVer = _versionBuffer[0]; _minorVer = _versionBuffer[1]; _patchVer = _versionBuffer[2]; @@ -105,13 +107,10 @@ private: } } + // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (configType != CONFIGURE_INPUT) return false; if (paramCount != 1) return false; - if (vpin >= _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is an analogue pin, cannot use as a digital pin"), vpin); - return false; - } bool pullup = params[0]; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIODPUP; @@ -121,37 +120,39 @@ private: return true; } - // We only use this to detect incorrect use of analogue pins + // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is a digital pin, cannot use as an analogue pin"), vpin); - return false; - } int pin = vpin - _firstVpin; - _analogueOutBuffer[0] = EXIOENAN; - _analogueOutBuffer[1] = pin; - I2CManager.write(_i2cAddress, _analogueOutBuffer, 2); + _command2Buffer[0] = EXIOENAN; + _command2Buffer[1] = pin; + I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } + // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning - _commandBuffer[0] = EXIORDD; - I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _commandBuffer, 1); - _commandBuffer[0] = EXIORDAN; - I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _commandBuffer, 1); + _command1Buffer[0] = EXIORDD; + I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDAN; + I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } + // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) return false; - int pin = vpin - _firstVpin - _numDigitalPins; - uint8_t _pinLSBByte = pin * 2; + int pin = vpin - _firstVpin; + uint8_t _pinLSBByte; + for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { + if (_analoguePinMap[aPin] == pin) { + _pinLSBByte = aPin * 2; + } + } uint8_t _pinMSBByte = _pinLSBByte + 1; return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } + // Obtain the correct digital input value int _read(VPIN vpin) override { - if (vpin >= _firstVpin + _numDigitalPins) return false; int pin = vpin - _firstVpin; uint8_t pinByte = pin / 8; bool value = _digitalInputStates[pinByte] >> (pin - pinByte * 8); @@ -159,7 +160,6 @@ private: } void _write(VPIN vpin, int value) override { - if (vpin >= _firstVpin + _numDigitalPins) return; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIOWRD; _digitalOutBuffer[1] = pin; @@ -168,25 +168,14 @@ private: } void _display() override { - int _firstAnalogue, _lastAnalogue; - if (_numAnaloguePins == 0) { - _firstAnalogue = 0; - _lastAnalogue = 0; - } else { - _firstAnalogue = _firstVpin + _numDigitalPins; - _lastAnalogue = _firstVpin + _nPins - 1; - } - DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d: %d Digital Vpins %d-%d, %d Analogue Vpins %d-%d %S"), + DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, - _numDigitalPins, _firstVpin, _firstVpin + _numDigitalPins - 1, - _numAnaloguePins, _firstAnalogue, _lastAnalogue, + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } uint8_t _i2cAddress; - uint8_t _numDigitalPins; uint8_t _numAnaloguePins; - byte _analogueOutBuffer[2]; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -196,7 +185,10 @@ private: byte* _analogueInputStates; uint8_t _digitalPinBytes = 0; uint8_t _analoguePinBytes = 0; - byte _commandBuffer[1]; + byte _command1Buffer[1]; + byte _command2Buffer[2]; + byte _receive2Buffer[2]; + uint8_t* _analoguePinMap; enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure @@ -207,6 +199,7 @@ private: EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input EXIOENAN = 0xE7, // Flag eo enable an analogue pin + EXIOINITA = 0xE8, // Flag we're receiving analogue pin info }; }; From 1d27eb67e41b3a99b4438f5d34b11cf5c9ad420b Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 19:26:33 +1000 Subject: [PATCH 05/52] Brief PWM start --- IO_EXIOExpander.h | 76 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 724ea42..f284bc1 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -1,5 +1,5 @@ /* - * © 2021, Peter Cole. All rights reserved. + * © 2022, Peter Cole. All rights reserved. * * This file is part of EX-CommandStation * @@ -26,19 +26,14 @@ * (Note the device driver is included by default) * * void halSetup() { -* // EXIOExpander::create(vpin, num_vpins, i2c_address, digitalPinCount, analoguePinCount); -* EXIOExpander::create(800, 18, 0x65, 12, 8); +* // EXIOExpander::create(vpin, num_vpins, i2c_address); +* EXIOExpander::create(800, 18, 0x65); * } * -* Note when defining the number of digital and analogue pins, there is no way to sanity check -* this from the device driver, and it is up to the user to define the correct values here. -* -* All pins available on the EX-IOExpander device must be accounted for. -* -* Vpins are allocated to digital pins first, and then analogue pins, so digital pins will -* populate the first part of the specified vpin range, with the analogue pins populating the -* last part of the vpin range. -* Eg. for a default Nano, 800 - 811 are digital (D2 - D13), 812 to 817 are analogue (A0 - A3, A6/A7). +* All pins on an EX-IOExpander device are allocated according to the pin map for the specific +* device in use. There is no way for the device driver to sanity check pins are used for the +* correct purpose, however the EX-IOExpander device's pin map will prevent pins being used +* incorrectly (eg. A6/7 on Nano cannot be used for digital input/output). */ #ifndef IO_EX_IOEXPANDER_H @@ -76,12 +71,15 @@ private: _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive3Buffer[1]; + _numPWMPins = _receive3Buffer[2]; _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + _servoData = (struct ServoData*) calloc(_numPWMPins, 14); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -109,15 +107,24 @@ private: // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { - if (configType != CONFIGURE_INPUT) return false; if (paramCount != 1) return false; - bool pullup = params[0]; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + if (configType == CONFIGURE_INPUT) { + bool pullup = params[0]; + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; + } else if (configType == CONFIGURE_SERVO) { + DIAG(F("Configure servo at pin %d"), (int)pin); + for (int i = 0; i < paramCount; i++) { + DIAG(F("Param %d is %x"), (int)i, params[i]); + } + return true; + } else { + return false; + } } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -175,7 +182,7 @@ private: } uint8_t _i2cAddress; - uint8_t _numAnaloguePins; + uint8_t _numAnaloguePins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -187,8 +194,29 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; + uint8_t _numPWMPins = 0; + + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + } ServoData; // 14 bytes per element, i.e. per pin in use + + struct ServoData* _servoData; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const byte FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From a18c06d021ce989ef3e957698488c5a9df40ad9a Mon Sep 17 00:00:00 2001 From: peteGSX Date: Tue, 31 Jan 2023 19:29:39 +1000 Subject: [PATCH 06/52] Cleaned up PWM start --- IO_EXIOExpander.h | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index f284bc1..34ece1b 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -78,7 +78,6 @@ private: _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - _servoData = (struct ServoData*) calloc(_numPWMPins, 14); } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); @@ -198,26 +197,6 @@ private: uint8_t* _analoguePinMap; uint8_t _numPWMPins = 0; - struct ServoData { - uint16_t activePosition : 12; // Config parameter - uint16_t inactivePosition : 12; // Config parameter - uint16_t currentPosition : 12; - uint16_t fromPosition : 12; - uint16_t toPosition : 12; - uint8_t profile; // Config parameter - uint16_t stepNumber; // Index of current step (starting from 0) - uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. - uint8_t currentProfile; // profile being used for current animation. - uint16_t duration; // time (tenths of a second) for animation to complete. - } ServoData; // 14 bytes per element, i.e. per pin in use - - struct ServoData* _servoData; - - static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const byte FLASH _bounceProfile[30]; - - const unsigned int refreshInterval = 50; // refresh every 50ms - enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup From 1073e142e6fe40e503cf4292a6dc5e4e9884615d Mon Sep 17 00:00:00 2001 From: peteGSX Date: Tue, 31 Jan 2023 19:32:12 +1000 Subject: [PATCH 07/52] Add new drivers --- IODevice.h | 5 +- IO_PCA9685_basic.h | 149 ++++++++++++++++++++++++ IO_Servo.h | 277 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 IO_PCA9685_basic.h create mode 100644 IO_Servo.h diff --git a/IODevice.h b/IODevice.h index 72beb9e..e7906c6 100644 --- a/IODevice.h +++ b/IODevice.h @@ -242,11 +242,12 @@ protected: // Current state of device DeviceStateEnum _deviceState = DEVSTATE_DORMANT; + // Method to find device handling Vpin + static IODevice *findDevice(VPIN vpin); + private: // Method to check whether the vpin corresponds to this device bool owns(VPIN vpin); - // Method to find device handling Vpin - static IODevice *findDevice(VPIN vpin); IODevice *_nextDevice = 0; unsigned long _nextEntryTime; static IODevice *_firstDevice; diff --git a/IO_PCA9685_basic.h b/IO_PCA9685_basic.h new file mode 100644 index 0000000..4f809aa --- /dev/null +++ b/IO_PCA9685_basic.h @@ -0,0 +1,149 @@ +/* + * © 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 . + */ + +/* + * 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%). + */ + +#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 PCA9685_basic : public IODevice { +public: + // Create device driver instance. + static void create(VPIN firstVpin, int nPins, uint8_t I2CAddress) { + if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new PCA9685_basic(firstVpin, nPins, I2CAddress); + } + +private: + + // structures for setting up non-blocking writes to servo controller + I2CRB requestBlock; + uint8_t outputBuffer[5]; + + // REGISTER ADDRESSES + const byte PCA9685_MODE1=0x00; // Mode Register + const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/ + const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */ + // MODE1 bits + const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */ + const byte MODE1_AI=0x20; /**< Auto-Increment enabled */ + const byte MODE1_RESTART=0x80; /**< Restart enabled */ + + const float FREQUENCY_OSCILLATOR=25000000.0; /** 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 + PCA9685_basic(VPIN firstVpin, int nPins, uint8_t I2CAddress) { + _firstVpin = firstVpin; + _nPins = min(nPins, 16); + _I2CAddress = I2CAddress; + 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, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period. + 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 profile, uint16_t duration) override { + #ifdef DIAG_IO + DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (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("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value); + #endif + // Wait for previous request to complete + uint8_t status = requestBlock.wait(); + if (status != I2C_STATUS_OK) { + _deviceState = DEVSTATE_FAILED; + DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, 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(byte address, byte reg, byte value) { + I2CManager.write(address, 2, reg, value); + } + +}; + +#endif \ No newline at end of file diff --git a/IO_Servo.h b/IO_Servo.h new file mode 100644 index 0000000..bd475fb --- /dev/null +++ b/IO_Servo.h @@ -0,0 +1,277 @@ +/* + * © 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 . + */ +#ifndef IO_SERVO_H + +#include "IODevice.h" +#include "I2CManager.h" +#include "DIAG.h" + +class Servo : IODevice { + +public: + enum ProfileType : uint8_t { + Instant = 0, // Moves immediately between positions (if duration not specified) + UseDuration = 0, // Use specified duration + Fast = 1, // Takes around 500ms end-to-end + Medium = 2, // 1 second end-to-end + Slow = 3, // 2 seconds end-to-end + Bounce = 4, // For semaphores/turnouts with a bit of bounce!! + NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. + }; + + // Create device driver instance. + static void create(VPIN firstVpin, int nPins, VPIN firstSlavePin) { + if (checkNoOverlap(firstVpin, nPins)) new Servo(firstVpin, nPins, firstSlavePin); + } + +private: + VPIN _firstSlavePin; + IODevice *_slaveDevice = NULL; + + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + }; // 14 bytes per element, i.e. per pin in use + + struct ServoData *_servoData [16]; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const uint8_t FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms + + + // Configure a port on the Servo. + bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { + if (configType != CONFIGURE_SERVO) return false; + if (paramCount != 5) return false; + #ifdef DIAG_IO + DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + vpin, params[0], params[1], params[2], params[3], params[4]); + #endif + + int8_t pin = vpin - _firstVpin; + VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); + s = _servoData[pin]; + if (!s) return false; // Check for failed memory allocation + } + + s->activePosition = params[0]; + s->inactivePosition = params[1]; + s->profile = params[2]; + s->duration = params[3]; + int state = params[4]; + + if (state != -1) { + // Position servo to initial state + IODevice::writeAnalogue(slavePin, state ? s->activePosition : s->inactivePosition, 0, 0); + } + return true; + } + + // Constructor + Servo(VPIN firstVpin, int nPins, VPIN firstSlavePin) { + _firstVpin = firstVpin; + _nPins = (nPins > 16) ? 16 : nPins; + _firstSlavePin = firstSlavePin; + + // To save RAM, space for servo configuration is not allocated unless a pin is used. + // Initialise the pointers to NULL. + for (int i=0; i<_nPins; i++) + _servoData[i] = NULL; + + addDevice(this); + } + + // Device-specific initialisation + void _begin() override { + // Get reference to slave device to make accesses faster. + _slaveDevice = this->findDevice(_firstSlavePin); + // Check firstSlavePin is actually allocated to a device + if (!_slaveDevice) { + DIAG(F("Servo: Slave device not found on pins %d-%d"), + _firstSlavePin, _firstSlavePin+_nPins-1); + _deviceState = DEVSTATE_FAILED; + } + // Check that the last slave pin is allocated to the same device. + if (_slaveDevice != this->findDevice(_firstSlavePin+_nPins-1)) { + DIAG(F("Servo: Slave device does not cover all pins %d-%d"), + _firstSlavePin, _firstSlavePin+_nPins-1); + _deviceState = DEVSTATE_FAILED; + } + #if defined(DIAG_IO) + _display(); + #endif + } + + // Device-specific write function, invoked from IODevice::write(). + // For this function, the configured profile is used. + void _write(VPIN vpin, int value) override { + if (_deviceState == DEVSTATE_FAILED) return; + #ifdef DIAG_IO + DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); + #endif + int pin = vpin - _firstVpin; + VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + if (value) value = 1; + + struct ServoData *s = _servoData[pin]; + if (s != NULL) { + // Use configured parameters + this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); + } else { + /* simulate digital pin on PWM */ + this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); + } + } + + // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue(). + // Profile is as follows: + // Bit 7: 0=Set PWM to 0% to power off servo motor when finished + // 1=Keep PWM pulses on (better when using PWM to drive an LED) + // Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds) + // 1 (Fast) Move servo in 0.5 seconds + // 2 (Medium) Move servo in 1.0 seconds + // 3 (Slow) Move servo in 2.0 seconds + // 4 (Bounce) Servo 'bounces' at extremes. + // + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { + #ifdef DIAG_IO + DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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; + + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + // Servo pin not configured, so configure now using defaults + s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); + if (s == NULL) return; // Check for memory allocation failure + s->activePosition = 4095; + s->inactivePosition = 0; + s->currentPosition = value; + s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) + } + + // Animated profile. Initiate the appropriate action. + s->currentProfile = profile; + uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. + s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds + profileValue==Medium ? 20 : // 1.0 seconds + profileValue==Slow ? 40 : // 2.0 seconds + profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds + duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) + s->stepNumber = 0; + s->toPosition = value; + s->fromPosition = s->currentPosition; + } + + // _read returns true if the device is currently in executing an animation, + // changing the output over a period of time. + int _read(VPIN vpin) override { + if (_deviceState == DEVSTATE_FAILED) return 0; + int pin = vpin - _firstVpin; + struct ServoData *s = _servoData[pin]; + if (s == NULL) + return false; // No structure means no animation! + else + return (s->stepNumber < s->numSteps); + } + + void _loop(unsigned long currentMicros) override { + if (_deviceState == DEVSTATE_FAILED) return; + for (int pin=0; pin<_nPins; pin++) { + updatePosition(pin); + } + delayUntil(currentMicros + refreshInterval * 1000UL); + } + + // Private function to reposition servo + // TODO: Could calculate step number from elapsed time, to allow for erratic loop timing. + void updatePosition(uint8_t pin) { + struct ServoData *s = _servoData[pin]; + if (s == NULL) return; // No pin configuration/state data + + if (s->numSteps == 0) return; // No animation in progress + + if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { + // Go straight to end of sequence, output final position. + s->stepNumber = s->numSteps-1; + } + + if (s->stepNumber < s->numSteps) { + // Animation in progress, reposition servo + s->stepNumber++; + if ((s->currentProfile & ~NoPowerOff) == Bounce) { + // Retrieve step positions from array in flash + uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); + s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); + } else { + // All other profiles - calculate step by linear interpolation between from and to positions. + s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); + } + // Send servo command + _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); + } else if (s->stepNumber < s->numSteps + _catchupSteps) { + // We've finished animation, wait a little to allow servo to catch up + s->stepNumber++; + } else if (s->stepNumber == s->numSteps + _catchupSteps + && s->currentPosition != 0) { + #ifdef IO_SWITCH_OFF_SERVO + if ((s->currentProfile & NoPowerOff) == 0) { + // Wait has finished, so switch off PWM to prevent annoying servo buzz + _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); + } + #endif + s->numSteps = 0; // Done now. + } + } + + // Display details of this device. + void _display() override { + DIAG(F("Servo Configured on Vpins:%d-%d, slave pins:%d-%d %S"), + (int)_firstVpin, (int)_firstVpin+_nPins-1, + (int)_firstSlavePin, (int)_firstSlavePin+_nPins-1, + (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); + } +}; + +// Profile for a bouncing signal or turnout +// The profile below is in the range 0-100% and should be combined with the desired limits +// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, +// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. +const byte FLASH Servo::_bounceProfile[30] = + {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; + + +#endif \ No newline at end of file From 052256e2ed803cae8d371bed5478b6d5ed568200 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 08/52] Refactored, analogue tested --- IO_EXIOExpander.h | 75 +++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index f284bc1..c962623 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -71,15 +71,12 @@ private: _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); - if (_receive3Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive3Buffer[1]; - _numPWMPins = _receive3Buffer[2]; + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - _servoData = (struct ServoData*) calloc(_numPWMPins, 14); - } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -105,26 +102,17 @@ private: } } + // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; + bool pullup = params[0]; int pin = vpin - _firstVpin; - if (configType == CONFIGURE_INPUT) { - bool pullup = params[0]; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; - } else if (configType == CONFIGURE_SERVO) { - DIAG(F("Configure servo at pin %d"), (int)pin); - for (int i = 0; i < paramCount; i++) { - DIAG(F("Param %d is %x"), (int)i, params[i]); - } - return true; - } else { - return false; - } + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -133,9 +121,13 @@ private: _command2Buffer[0] = EXIOENAN; _command2Buffer[1] = pin; I2CManager.write(_i2cAddress, _command2Buffer, 2); + _command2Buffer[0] = EXIOENAN; + _command2Buffer[1] = pin; + I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } + // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning @@ -143,10 +135,22 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDD; + I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDAN; + I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } + // Obtain the correct analogue input value // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { + int pin = vpin - _firstVpin; + uint8_t _pinLSBByte; + for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { + if (_analoguePinMap[aPin] == pin) { + _pinLSBByte = aPin * 2; + } + } int pin = vpin - _firstVpin; uint8_t _pinLSBByte; for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { @@ -158,6 +162,7 @@ private: return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } + // Obtain the correct digital input value // Obtain the correct digital input value int _read(VPIN vpin) override { int pin = vpin - _firstVpin; @@ -178,11 +183,12 @@ private: DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, (int)_firstVpin, (int)_firstVpin+_nPins-1, + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } uint8_t _i2cAddress; - uint8_t _numAnaloguePins = 0; + uint8_t _numAnaloguePins; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -194,29 +200,8 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive3Buffer[3]; + byte _receive2Buffer[2]; uint8_t* _analoguePinMap; - uint8_t _numPWMPins = 0; - - struct ServoData { - uint16_t activePosition : 12; // Config parameter - uint16_t inactivePosition : 12; // Config parameter - uint16_t currentPosition : 12; - uint16_t fromPosition : 12; - uint16_t toPosition : 12; - uint8_t profile; // Config parameter - uint16_t stepNumber; // Index of current step (starting from 0) - uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. - uint8_t currentProfile; // profile being used for current animation. - uint16_t duration; // time (tenths of a second) for animation to complete. - } ServoData; // 14 bytes per element, i.e. per pin in use - - struct ServoData* _servoData; - - static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const byte FLASH _bounceProfile[30]; - - const unsigned int refreshInterval = 50; // refresh every 50ms enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From bdc8aec9a638ee35d13bfc9e6b327ba189cdf503 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 09/52] Refactored, analogue tested --- IO_EXIOExpander.h | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index c962623..5c10d88 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -49,12 +49,15 @@ */ class EXIOExpander : public IODevice { public: + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor + EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; @@ -68,6 +71,16 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline @@ -85,9 +98,14 @@ private: // We now need to retrieve the analogue pin map _command1Buffer[0] = EXIOINITA; I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); + // We now need to retrieve the analogue pin map + _command1Buffer[0] = EXIOINITA; + I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline _command1Buffer[0] = EXIOVER; I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); + _command1Buffer[0] = EXIOVER; + I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); _majorVer = _versionBuffer[0]; _minorVer = _versionBuffer[1]; _patchVer = _versionBuffer[2]; @@ -102,7 +120,6 @@ private: } } - // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; @@ -115,19 +132,16 @@ private: return true; } + // Analogue input pin configuration, used to enable on EX-IOExpander device // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { int pin = vpin - _firstVpin; _command2Buffer[0] = EXIOENAN; _command2Buffer[1] = pin; I2CManager.write(_i2cAddress, _command2Buffer, 2); - _command2Buffer[0] = EXIOENAN; - _command2Buffer[1] = pin; - I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } - // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning @@ -135,22 +149,10 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); - _command1Buffer[0] = EXIORDD; - I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); - _command1Buffer[0] = EXIORDAN; - I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } - // Obtain the correct analogue input value // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { - int pin = vpin - _firstVpin; - uint8_t _pinLSBByte; - for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { - if (_analoguePinMap[aPin] == pin) { - _pinLSBByte = aPin * 2; - } - } int pin = vpin - _firstVpin; uint8_t _pinLSBByte; for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { @@ -162,7 +164,6 @@ private: return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } - // Obtain the correct digital input value // Obtain the correct digital input value int _read(VPIN vpin) override { int pin = vpin - _firstVpin; @@ -180,10 +181,10 @@ private: } void _display() override { + DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, (int)_firstVpin, (int)_firstVpin+_nPins-1, - (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } @@ -202,6 +203,10 @@ private: byte _command2Buffer[2]; byte _receive2Buffer[2]; uint8_t* _analoguePinMap; + byte _command1Buffer[1]; + byte _command2Buffer[2]; + byte _receive2Buffer[2]; + uint8_t* _analoguePinMap; enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From e76197faa97f2aa0d6a488823456a5fcaba5ad7b Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 19:26:33 +1000 Subject: [PATCH 10/52] Brief PWM start --- IO_EXIOExpander.h | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 5c10d88..5476a41 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -90,6 +90,18 @@ private: _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive3Buffer[1]; + _numPWMPins = _receive3Buffer[2]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -123,13 +135,23 @@ private: // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; - bool pullup = params[0]; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + if (configType == CONFIGURE_INPUT) { + bool pullup = params[0]; + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; + } else if (configType == CONFIGURE_SERVO) { + DIAG(F("Configure servo at pin %d"), (int)pin); + for (int i = 0; i < paramCount; i++) { + DIAG(F("Param %d is %x"), (int)i, params[i]); + } + return true; + } else { + return false; + } } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -189,7 +211,7 @@ private: } uint8_t _i2cAddress; - uint8_t _numAnaloguePins; + uint8_t _numAnaloguePins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -201,7 +223,7 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; byte _command1Buffer[1]; byte _command2Buffer[2]; From 84431d1841a472f892aa1125e28242f6cc7f781a Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Wed, 1 Feb 2023 07:49:31 +1000 Subject: [PATCH 11/52] Fix mess after rebase and conflicts --- IO_EXIOExpander.h | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 5476a41..519d2eb 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -49,15 +49,12 @@ */ class EXIOExpander : public IODevice { public: - static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { - if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor - EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; @@ -80,28 +77,6 @@ private: _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { - _command2Buffer[0] = EXIOINIT; - _command2Buffer[1] = _nPins; - // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; - _analoguePinBytes = _numAnaloguePins * 2; - _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); - _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { - _command2Buffer[0] = EXIOINIT; - _command2Buffer[1] = _nPins; - // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); - if (_receive3Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive3Buffer[1]; - _numPWMPins = _receive3Buffer[2]; - _analoguePinBytes = _numAnaloguePins * 2; - _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); - _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -110,9 +85,6 @@ private: // We now need to retrieve the analogue pin map _command1Buffer[0] = EXIOINITA; I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); - // We now need to retrieve the analogue pin map - _command1Buffer[0] = EXIOINITA; - I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline _command1Buffer[0] = EXIOVER; I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); @@ -143,12 +115,6 @@ private: _digitalOutBuffer[2] = pullup; I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); return true; - } else if (configType == CONFIGURE_SERVO) { - DIAG(F("Configure servo at pin %d"), (int)pin); - for (int i = 0; i < paramCount; i++) { - DIAG(F("Param %d is %x"), (int)i, params[i]); - } - return true; } else { return false; } @@ -203,7 +169,6 @@ private: } void _display() override { - DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, (int)_firstVpin, (int)_firstVpin+_nPins-1, @@ -212,6 +177,7 @@ private: uint8_t _i2cAddress; uint8_t _numAnaloguePins = 0; + uint8_t numDigitalPins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -223,10 +189,6 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive3Buffer[3]; - uint8_t* _analoguePinMap; - byte _command1Buffer[1]; - byte _command2Buffer[2]; byte _receive2Buffer[2]; uint8_t* _analoguePinMap; From a7366b42c13bbef2671c73d1a42dfa8a48ee10dc Mon Sep 17 00:00:00 2001 From: peteGSX Date: Tue, 31 Jan 2023 19:32:12 +1000 Subject: [PATCH 12/52] Add new drivers --- IODevice.h | 5 +- IO_PCA9685_basic.h | 149 ++++++++++++++++++++++++ IO_Servo.h | 277 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 IO_PCA9685_basic.h create mode 100644 IO_Servo.h diff --git a/IODevice.h b/IODevice.h index 72beb9e..e7906c6 100644 --- a/IODevice.h +++ b/IODevice.h @@ -242,11 +242,12 @@ protected: // Current state of device DeviceStateEnum _deviceState = DEVSTATE_DORMANT; + // Method to find device handling Vpin + static IODevice *findDevice(VPIN vpin); + private: // Method to check whether the vpin corresponds to this device bool owns(VPIN vpin); - // Method to find device handling Vpin - static IODevice *findDevice(VPIN vpin); IODevice *_nextDevice = 0; unsigned long _nextEntryTime; static IODevice *_firstDevice; diff --git a/IO_PCA9685_basic.h b/IO_PCA9685_basic.h new file mode 100644 index 0000000..4f809aa --- /dev/null +++ b/IO_PCA9685_basic.h @@ -0,0 +1,149 @@ +/* + * © 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 . + */ + +/* + * 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%). + */ + +#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 PCA9685_basic : public IODevice { +public: + // Create device driver instance. + static void create(VPIN firstVpin, int nPins, uint8_t I2CAddress) { + if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new PCA9685_basic(firstVpin, nPins, I2CAddress); + } + +private: + + // structures for setting up non-blocking writes to servo controller + I2CRB requestBlock; + uint8_t outputBuffer[5]; + + // REGISTER ADDRESSES + const byte PCA9685_MODE1=0x00; // Mode Register + const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/ + const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */ + // MODE1 bits + const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */ + const byte MODE1_AI=0x20; /**< Auto-Increment enabled */ + const byte MODE1_RESTART=0x80; /**< Restart enabled */ + + const float FREQUENCY_OSCILLATOR=25000000.0; /** 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 + PCA9685_basic(VPIN firstVpin, int nPins, uint8_t I2CAddress) { + _firstVpin = firstVpin; + _nPins = min(nPins, 16); + _I2CAddress = I2CAddress; + 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, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period. + 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 profile, uint16_t duration) override { + #ifdef DIAG_IO + DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (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("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value); + #endif + // Wait for previous request to complete + uint8_t status = requestBlock.wait(); + if (status != I2C_STATUS_OK) { + _deviceState = DEVSTATE_FAILED; + DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, 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(byte address, byte reg, byte value) { + I2CManager.write(address, 2, reg, value); + } + +}; + +#endif \ No newline at end of file diff --git a/IO_Servo.h b/IO_Servo.h new file mode 100644 index 0000000..bd475fb --- /dev/null +++ b/IO_Servo.h @@ -0,0 +1,277 @@ +/* + * © 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 . + */ +#ifndef IO_SERVO_H + +#include "IODevice.h" +#include "I2CManager.h" +#include "DIAG.h" + +class Servo : IODevice { + +public: + enum ProfileType : uint8_t { + Instant = 0, // Moves immediately between positions (if duration not specified) + UseDuration = 0, // Use specified duration + Fast = 1, // Takes around 500ms end-to-end + Medium = 2, // 1 second end-to-end + Slow = 3, // 2 seconds end-to-end + Bounce = 4, // For semaphores/turnouts with a bit of bounce!! + NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. + }; + + // Create device driver instance. + static void create(VPIN firstVpin, int nPins, VPIN firstSlavePin) { + if (checkNoOverlap(firstVpin, nPins)) new Servo(firstVpin, nPins, firstSlavePin); + } + +private: + VPIN _firstSlavePin; + IODevice *_slaveDevice = NULL; + + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + }; // 14 bytes per element, i.e. per pin in use + + struct ServoData *_servoData [16]; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const uint8_t FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms + + + // Configure a port on the Servo. + bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { + if (configType != CONFIGURE_SERVO) return false; + if (paramCount != 5) return false; + #ifdef DIAG_IO + DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + vpin, params[0], params[1], params[2], params[3], params[4]); + #endif + + int8_t pin = vpin - _firstVpin; + VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); + s = _servoData[pin]; + if (!s) return false; // Check for failed memory allocation + } + + s->activePosition = params[0]; + s->inactivePosition = params[1]; + s->profile = params[2]; + s->duration = params[3]; + int state = params[4]; + + if (state != -1) { + // Position servo to initial state + IODevice::writeAnalogue(slavePin, state ? s->activePosition : s->inactivePosition, 0, 0); + } + return true; + } + + // Constructor + Servo(VPIN firstVpin, int nPins, VPIN firstSlavePin) { + _firstVpin = firstVpin; + _nPins = (nPins > 16) ? 16 : nPins; + _firstSlavePin = firstSlavePin; + + // To save RAM, space for servo configuration is not allocated unless a pin is used. + // Initialise the pointers to NULL. + for (int i=0; i<_nPins; i++) + _servoData[i] = NULL; + + addDevice(this); + } + + // Device-specific initialisation + void _begin() override { + // Get reference to slave device to make accesses faster. + _slaveDevice = this->findDevice(_firstSlavePin); + // Check firstSlavePin is actually allocated to a device + if (!_slaveDevice) { + DIAG(F("Servo: Slave device not found on pins %d-%d"), + _firstSlavePin, _firstSlavePin+_nPins-1); + _deviceState = DEVSTATE_FAILED; + } + // Check that the last slave pin is allocated to the same device. + if (_slaveDevice != this->findDevice(_firstSlavePin+_nPins-1)) { + DIAG(F("Servo: Slave device does not cover all pins %d-%d"), + _firstSlavePin, _firstSlavePin+_nPins-1); + _deviceState = DEVSTATE_FAILED; + } + #if defined(DIAG_IO) + _display(); + #endif + } + + // Device-specific write function, invoked from IODevice::write(). + // For this function, the configured profile is used. + void _write(VPIN vpin, int value) override { + if (_deviceState == DEVSTATE_FAILED) return; + #ifdef DIAG_IO + DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); + #endif + int pin = vpin - _firstVpin; + VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + if (value) value = 1; + + struct ServoData *s = _servoData[pin]; + if (s != NULL) { + // Use configured parameters + this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); + } else { + /* simulate digital pin on PWM */ + this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); + } + } + + // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue(). + // Profile is as follows: + // Bit 7: 0=Set PWM to 0% to power off servo motor when finished + // 1=Keep PWM pulses on (better when using PWM to drive an LED) + // Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds) + // 1 (Fast) Move servo in 0.5 seconds + // 2 (Medium) Move servo in 1.0 seconds + // 3 (Slow) Move servo in 2.0 seconds + // 4 (Bounce) Servo 'bounces' at extremes. + // + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { + #ifdef DIAG_IO + DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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; + + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + // Servo pin not configured, so configure now using defaults + s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); + if (s == NULL) return; // Check for memory allocation failure + s->activePosition = 4095; + s->inactivePosition = 0; + s->currentPosition = value; + s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) + } + + // Animated profile. Initiate the appropriate action. + s->currentProfile = profile; + uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. + s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds + profileValue==Medium ? 20 : // 1.0 seconds + profileValue==Slow ? 40 : // 2.0 seconds + profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds + duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) + s->stepNumber = 0; + s->toPosition = value; + s->fromPosition = s->currentPosition; + } + + // _read returns true if the device is currently in executing an animation, + // changing the output over a period of time. + int _read(VPIN vpin) override { + if (_deviceState == DEVSTATE_FAILED) return 0; + int pin = vpin - _firstVpin; + struct ServoData *s = _servoData[pin]; + if (s == NULL) + return false; // No structure means no animation! + else + return (s->stepNumber < s->numSteps); + } + + void _loop(unsigned long currentMicros) override { + if (_deviceState == DEVSTATE_FAILED) return; + for (int pin=0; pin<_nPins; pin++) { + updatePosition(pin); + } + delayUntil(currentMicros + refreshInterval * 1000UL); + } + + // Private function to reposition servo + // TODO: Could calculate step number from elapsed time, to allow for erratic loop timing. + void updatePosition(uint8_t pin) { + struct ServoData *s = _servoData[pin]; + if (s == NULL) return; // No pin configuration/state data + + if (s->numSteps == 0) return; // No animation in progress + + if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { + // Go straight to end of sequence, output final position. + s->stepNumber = s->numSteps-1; + } + + if (s->stepNumber < s->numSteps) { + // Animation in progress, reposition servo + s->stepNumber++; + if ((s->currentProfile & ~NoPowerOff) == Bounce) { + // Retrieve step positions from array in flash + uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); + s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); + } else { + // All other profiles - calculate step by linear interpolation between from and to positions. + s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); + } + // Send servo command + _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); + } else if (s->stepNumber < s->numSteps + _catchupSteps) { + // We've finished animation, wait a little to allow servo to catch up + s->stepNumber++; + } else if (s->stepNumber == s->numSteps + _catchupSteps + && s->currentPosition != 0) { + #ifdef IO_SWITCH_OFF_SERVO + if ((s->currentProfile & NoPowerOff) == 0) { + // Wait has finished, so switch off PWM to prevent annoying servo buzz + _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); + } + #endif + s->numSteps = 0; // Done now. + } + } + + // Display details of this device. + void _display() override { + DIAG(F("Servo Configured on Vpins:%d-%d, slave pins:%d-%d %S"), + (int)_firstVpin, (int)_firstVpin+_nPins-1, + (int)_firstSlavePin, (int)_firstSlavePin+_nPins-1, + (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); + } +}; + +// Profile for a bouncing signal or turnout +// The profile below is in the range 0-100% and should be combined with the desired limits +// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, +// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. +const byte FLASH Servo::_bounceProfile[30] = + {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; + + +#endif \ No newline at end of file From 73e1dfc1929838bc3770fc94e0ec5550cd8f2e0d Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Wed, 1 Feb 2023 08:13:23 +1000 Subject: [PATCH 13/52] Remove duplicate comment --- IO_EXIOExpander.h | 1 - 1 file changed, 1 deletion(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 519d2eb..ac66fd9 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -120,7 +120,6 @@ private: } } - // Analogue input pin configuration, used to enable on EX-IOExpander device // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { int pin = vpin - _firstVpin; From 4e32c707b984f353c1039741e8964e14f11ab851 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Wed, 1 Feb 2023 14:53:46 +1000 Subject: [PATCH 14/52] Brief start on PWM --- IO_EXIOExpander.h | 25 ++++++++++++++++--------- IO_Servo.h | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index ac66fd9..718cf1c 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -59,8 +59,6 @@ private: _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; - _digitalPinBytes = (nPins+7)/8; - _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); addDevice(this); } @@ -70,10 +68,13 @@ private: if (I2CManager.exists(_i2cAddress)) { _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; - // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; + // Send config, if EXIOPINS returned, we're good, setup pin buffers, otherwise go offline + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOPINS) { + _numDigitalPins = _receive3Buffer[1]; + _numAnaloguePins = _receive3Buffer[2]; + _digitalPinBytes = (_numDigitalPins + 7)/8; + _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); @@ -167,6 +168,11 @@ private: I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); } + void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { + int pin = vpin - _firstVpin; + DIAG(F("Write %d to pin %d, param 1 %d, param 2 %d"), value, pin, param1, param2); + } + void _display() override { DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, @@ -175,8 +181,8 @@ private: } uint8_t _i2cAddress; + uint8_t _numDigitalPins = 0; uint8_t _numAnaloguePins = 0; - uint8_t numDigitalPins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -188,7 +194,7 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; enum { @@ -200,7 +206,8 @@ private: EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input EXIOENAN = 0xE7, // Flag eo enable an analogue pin - EXIOINITA = 0xE8, // Flag we're receiving analogue pin info + EXIOINITA = 0xE8, // Flag we're receiving analogue pin mappings + EXIOPINS = 0xE9, // Flag we're receiving pin counts for buffers }; }; diff --git a/IO_Servo.h b/IO_Servo.h index bd475fb..b1935b6 100644 --- a/IO_Servo.h +++ b/IO_Servo.h @@ -139,7 +139,7 @@ private: DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); #endif int pin = vpin - _firstVpin; - VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + // VPIN slavePin = vpin - _firstVpin + _firstSlavePin; if (value) value = 1; struct ServoData *s = _servoData[pin]; From ec83a345dcfa44b2781d6a6c2cd4c765df80300d Mon Sep 17 00:00:00 2001 From: peteGSX Date: Wed, 1 Feb 2023 19:46:08 +1000 Subject: [PATCH 15/52] Basic PWM working --- IO_EXIOExpander.h | 19 ++++++++++++++----- version.h | 4 +++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 718cf1c..46abc50 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -66,10 +66,12 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { - _command2Buffer[0] = EXIOINIT; - _command2Buffer[1] = _nPins; + _command4Buffer[0] = EXIOINIT; + _command4Buffer[1] = _nPins; + _command4Buffer[2] = _firstVpin & 0xFF; + _command4Buffer[3] = _firstVpin >> 8; // Send config, if EXIOPINS returned, we're good, setup pin buffers, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command4Buffer, 4); if (_receive3Buffer[0] == EXIOPINS) { _numDigitalPins = _receive3Buffer[1]; _numAnaloguePins = _receive3Buffer[2]; @@ -170,7 +172,11 @@ private: void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { int pin = vpin - _firstVpin; - DIAG(F("Write %d to pin %d, param 1 %d, param 2 %d"), value, pin, param1, param2); + _command4Buffer[0] = EXIOWRAN; + _command4Buffer[1] = pin; + _command4Buffer[2] = value & 0xFF; + _command4Buffer[3] = value >> 8; + I2CManager.write(_i2cAddress, _command4Buffer, 4); } void _display() override { @@ -194,6 +200,7 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; + byte _command4Buffer[4]; byte _receive3Buffer[3]; uint8_t* _analoguePinMap; @@ -205,9 +212,11 @@ private: EXIORDAN = 0xE4, // Flag to read an analogue input EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input - EXIOENAN = 0xE7, // Flag eo enable an analogue pin + EXIOENAN = 0xE7, // Flag to enable an analogue pin EXIOINITA = 0xE8, // Flag we're receiving analogue pin mappings EXIOPINS = 0xE9, // Flag we're receiving pin counts for buffers + EXIOWRAN = 0xEA, // Flag we're sending an analogue write (PWM) + EXIOERR = 0xEF, // Flag we've received an error }; }; diff --git a/version.h b/version.h index 6788645..c37a1c4 100644 --- a/version.h +++ b/version.h @@ -4,7 +4,9 @@ #include "StringFormatter.h" -#define VERSION "4.2.14" +#define VERSION "4.2.15" +// 4.2.15 Separate Servo from PCA9685 +// Add PWM support to EX-IOExpander // 4.2.14 STM32F4xx fast ADC read implementation // 4.2.13 Broadcast power for again // 4.2.12 Bugfix for issue #299 TurnoutDescription NULL From abe79b854e73a01a71d94019cb1c696f2abfebc5 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sat, 4 Feb 2023 09:19:32 +1000 Subject: [PATCH 16/52] Fix digital read bug --- IO_EXIOExpander.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 46abc50..59684cd 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -158,7 +158,7 @@ private: int _read(VPIN vpin) override { int pin = vpin - _firstVpin; uint8_t pinByte = pin / 8; - bool value = _digitalInputStates[pinByte] >> (pin - pinByte * 8); + bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); return value; } From 754639c7e3af71d448c6817fecb6315d79dc5678 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Mon, 6 Feb 2023 19:39:25 +1000 Subject: [PATCH 17/52] Update version --- version.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/version.h b/version.h index c37a1c4..d95e01d 100644 --- a/version.h +++ b/version.h @@ -5,8 +5,9 @@ #define VERSION "4.2.15" -// 4.2.15 Separate Servo from PCA9685 -// Add PWM support to EX-IOExpander +// 4.2.15 Add Servo device driver with PCA9685_basic driver +// Add basic experimental PWM support to EX-IOExpander +// EX-IOExpander 0.0.14 minimum required // 4.2.14 STM32F4xx fast ADC read implementation // 4.2.13 Broadcast power for again // 4.2.12 Bugfix for issue #299 TurnoutDescription NULL From c870940ddefb5e4f2ff58e09717b95b2d225f5d8 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Tue, 7 Feb 2023 07:32:16 +1000 Subject: [PATCH 18/52] Add extra error checking --- IO_EXIOExpander.h | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 59684cd..8ec1f48 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -116,8 +116,13 @@ private: _digitalOutBuffer[0] = EXIODPUP; _digitalOutBuffer[1] = pin; _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); + if (_command1Buffer[0] == EXIORDY) { + return true; + } else { + DIAG(F("Vpin %d cannot be used as a digital input pin"), (int)vpin); + return false; + } } else { return false; } @@ -128,7 +133,13 @@ private: int pin = vpin - _firstVpin; _command2Buffer[0] = EXIOENAN; _command2Buffer[1] = pin; - I2CManager.write(_i2cAddress, _command2Buffer, 2); + I2CManager.read(_i2cAddress, _command1Buffer, 1, _command2Buffer, 2); + if (_command1Buffer[0] == EXIORDY) { + return true; + } else { + DIAG(F("Vpin %d cannot be used as an analogue input pin"), (int)vpin); + return false; + } return true; } @@ -167,7 +178,10 @@ private: _digitalOutBuffer[0] = EXIOWRD; _digitalOutBuffer[1] = pin; _digitalOutBuffer[2] = value; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); + if (_command1Buffer[0] != EXIORDY) { + DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + } } void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { From 57292c2250f8ad60e84ef85af9b00f76febfc1e9 Mon Sep 17 00:00:00 2001 From: Harald Barth Date: Tue, 7 Feb 2023 20:44:03 +0100 Subject: [PATCH 19/52] installer.sh script bug fix and enhancements --- installer.sh | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/installer.sh b/installer.sh index 70a7858..98de771 100755 --- a/installer.sh +++ b/installer.sh @@ -37,6 +37,10 @@ function need () { need git +if test -d `basename "$DCCEXGITURL"` ; then + : assume we are almost there + cd `basename "$DCCEXGITURL"` || exit 255 +fi if test -d .git ; then : assume we are right here git pull @@ -44,6 +48,21 @@ else git clone "$DCCEXGITURL" cd `basename "$DCCEXGITURL"` || exit 255 fi + +# prepare versions +VERSIONS=/tmp/versions.$$ +git tag --sort=v:refname | grep Prod | tail -1 > $VERSIONS +echo master >> $VERSIONS +git tag --sort=v:refname | grep Devel | tail -1 >> $VERSIONS +echo devel >> $VERSIONS + +# ask user what version to use +echo "What version to use? (give line number) If in doubt, use 1" +cat -n $VERSIONS +echo -n "> " +LINE=`awk 'BEGIN {getline A < "/dev/tty"} ; A == NR {print}' $VERSIONS` +git checkout $LINE + if test -f config.h ; then : all well else @@ -63,7 +82,14 @@ $ACLI core update-index || exit 255 # Board discovery BOARDS=/tmp/boards.$$ -$ACLI board list | grep serial > $BOARDS +$ACLI board list > /dev/null # download missing components +$ACLI board list | grep serial > $BOARDS # real run +if test -s $BOARDS ; then + : all well +else + echo "$ACLI: No boards found" + exit 255 +fi if test x`< $BOARDS wc -l` = 'x1' ; then LINE=`cat $BOARDS` else @@ -96,6 +122,6 @@ echo FQBN is $FQBN # Install phase $ACLI core install `echo $FQBN | sed 's,:[^:]*$,,1'` # remove last component to get package -$ACLI board attach -p $PORT --fqbn $FQBN $PWD -$ACLI compile --fqbn $FQBN $PWD -$ACLI upload -v -t -p $PORT $PWD +$ACLI board attach -p $PORT --fqbn $FQBN "$PWD" +$ACLI compile --fqbn $FQBN "$PWD" +$ACLI upload -v -t -p $PORT "$PWD" From 3d480ee9ef378e32b03947ab342179f1b83c04fb Mon Sep 17 00:00:00 2001 From: peteGSX Date: Thu, 9 Feb 2023 05:32:27 +1000 Subject: [PATCH 20/52] Start adding servo to EX-IO --- IO_EXIOExpander.h | 142 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 8ec1f48..6549873 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -49,6 +49,17 @@ */ class EXIOExpander : public IODevice { public: + + enum ProfileType : uint8_t { + Instant = 0, // Moves immediately between positions (if duration not specified) + UseDuration = 0, // Use specified duration + Fast = 1, // Takes around 500ms end-to-end + Medium = 2, // 1 second end-to-end + Slow = 3, // 2 seconds end-to-end + Bounce = 4, // For semaphores/turnouts with a bit of bounce!! + NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. + }; + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } @@ -59,6 +70,11 @@ private: _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; + // To save RAM, space for servo configuration is not allocated unless a pin is used. + // Initialise the pointers to NULL. + for (int i=0; i<_nPins; i++) { + _servoData[i] = NULL; + } addDevice(this); } @@ -123,6 +139,28 @@ private: DIAG(F("Vpin %d cannot be used as a digital input pin"), (int)vpin); return false; } + } else if (configType == CONFIGURE_SERVO) { + if (paramCount != 5) return false; +#ifdef DIAG_IO + DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + vpin, params[0], params[1], params[2], params[3], params[4]); +#endif + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); + s = _servoData[pin]; + if (!s) return false; // Check for failed memory allocation + } + s->activePosition = params[0]; + s->inactivePosition = params[1]; + s->profile = params[2]; + s->duration = params[3]; + int state = params[4]; + if (state != -1) { + // Position servo to initial state + IODevice::writeAnalogue(pin, state ? s->activePosition : s->inactivePosition, 0, 0); + } + return true; } else { return false; } @@ -184,13 +222,86 @@ private: } } - void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { + // void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { int pin = vpin - _firstVpin; + /* Initial _writeAnalogue here _command4Buffer[0] = EXIOWRAN; _command4Buffer[1] = pin; _command4Buffer[2] = value & 0xFF; _command4Buffer[3] = value >> 8; I2CManager.write(_i2cAddress, _command4Buffer, 4); + */ + #ifdef DIAG_IO + DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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; + + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + // Servo pin not configured, so configure now using defaults + s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); + if (s == NULL) return; // Check for memory allocation failure + s->activePosition = 4095; + s->inactivePosition = 0; + s->currentPosition = value; + s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) + } + + // Animated profile. Initiate the appropriate action. + s->currentProfile = profile; + uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. + s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds + profileValue==Medium ? 20 : // 1.0 seconds + profileValue==Slow ? 40 : // 2.0 seconds + profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds + duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) + s->stepNumber = 0; + s->toPosition = value; + s->fromPosition = s->currentPosition; + } + + void updatePosition(uint8_t pin) { + struct ServoData *s = _servoData[pin]; + if (s == NULL) return; // No pin configuration/state data + + if (s->numSteps == 0) return; // No animation in progress + + if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { + // Go straight to end of sequence, output final position. + s->stepNumber = s->numSteps-1; + } + + if (s->stepNumber < s->numSteps) { + // Animation in progress, reposition servo + s->stepNumber++; + if ((s->currentProfile & ~NoPowerOff) == Bounce) { + // Retrieve step positions from array in flash + uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); + s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); + } else { + // All other profiles - calculate step by linear interpolation between from and to positions. + s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); + } + // Send servo command + // _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); + } else if (s->stepNumber < s->numSteps + _catchupSteps) { + // We've finished animation, wait a little to allow servo to catch up + s->stepNumber++; + } else if (s->stepNumber == s->numSteps + _catchupSteps + && s->currentPosition != 0) { + #ifdef IO_SWITCH_OFF_SERVO + if ((s->currentProfile & NoPowerOff) == 0) { + // Wait has finished, so switch off PWM to prevent annoying servo buzz + // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); + } + #endif + s->numSteps = 0; // Done now. + } } void _display() override { @@ -218,6 +329,35 @@ private: byte _receive3Buffer[3]; uint8_t* _analoguePinMap; + // Servo specific + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + }; // 14 bytes per element, i.e. per pin in use + + struct ServoData *_servoData [16]; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const uint8_t FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms + +// Profile for a bouncing signal or turnout +// The profile below is in the range 0-100% and should be combined with the desired limits +// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, +// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. +const byte FLASH EXIOExpander::_bounceProfile[30] = + {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; + + // EX-IOExpander protocol flags enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup From 2848ba616b7dcc3fde1c976ab5b8b8a3301c085a Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 07:38:00 +1000 Subject: [PATCH 21/52] Some success --- IO_EXIOExpander.h | 86 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 6549873..9912fe6 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -184,10 +184,14 @@ private: // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning + if (_deviceState == DEVSTATE_FAILED) return; _command1Buffer[0] = EXIORDD; I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); + for (int pin=0; pin<_nPins; pin++) { + updatePosition(pin); + } } // Obtain the correct analogue input value @@ -205,20 +209,43 @@ private: // Obtain the correct digital input value int _read(VPIN vpin) override { + if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; - uint8_t pinByte = pin / 8; - bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); - return value; + if (_servoData[pin] == NULL) { + uint8_t pinByte = pin / 8; + bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); + return value; + } else { + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + return false; // No structure means no animation! + } else { + return (s->stepNumber < s->numSteps); + } + } } void _write(VPIN vpin, int value) override { + if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIOWRD; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = value; - I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); - if (_command1Buffer[0] != EXIORDY) { - DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + if (_servoData[pin] == NULL) { + _digitalOutBuffer[0] = EXIOWRD; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = value; + I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); + if (_command1Buffer[0] != EXIORDY) { + DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + } + } else { + if (value) value = 1; + struct ServoData *s = _servoData[pin]; + if (s != NULL) { + // Use configured parameters + this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); + } else { + /* simulate digital pin on PWM */ + this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); + } } } @@ -232,12 +259,11 @@ private: _command4Buffer[3] = value >> 8; I2CManager.write(_i2cAddress, _command4Buffer, 4); */ - #ifdef DIAG_IO +#ifdef DIAG_IO DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); - #endif +#endif if (_deviceState == DEVSTATE_FAILED) return; - int pin = vpin - _firstVpin; if (value > 4095) value = 4095; else if (value < 0) value = 0; @@ -288,22 +314,30 @@ private: s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); } // Send servo command - // _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); + this->writePWM(pin, s->currentPosition); } else if (s->stepNumber < s->numSteps + _catchupSteps) { // We've finished animation, wait a little to allow servo to catch up s->stepNumber++; } else if (s->stepNumber == s->numSteps + _catchupSteps && s->currentPosition != 0) { - #ifdef IO_SWITCH_OFF_SERVO - if ((s->currentProfile & NoPowerOff) == 0) { - // Wait has finished, so switch off PWM to prevent annoying servo buzz - // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); - } - #endif + // #ifdef IO_SWITCH_OFF_SERVO + // if ((s->currentProfile & NoPowerOff) == 0) { + // // Wait has finished, so switch off PWM to prevent annoying servo buzz + // // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); + // } + // #endif s->numSteps = 0; // Done now. } } + void writePWM(int pin, uint16_t value) { + _command4Buffer[0] = EXIOWRAN; + _command4Buffer[1] = pin; + _command4Buffer[2] = value & 0xFF; + _command4Buffer[3] = value >> 8; + I2CManager.write(_i2cAddress, _command4Buffer, 4); + } + void _display() override { DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, @@ -343,18 +377,18 @@ private: uint16_t duration; // time (tenths of a second) for animation to complete. }; // 14 bytes per element, i.e. per pin in use - struct ServoData *_servoData [16]; + struct ServoData *_servoData[256]; static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const uint8_t FLASH _bounceProfile[30]; + // static const uint8_t FLASH _bounceProfile[30]; const unsigned int refreshInterval = 50; // refresh every 50ms -// Profile for a bouncing signal or turnout -// The profile below is in the range 0-100% and should be combined with the desired limits -// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, -// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. -const byte FLASH EXIOExpander::_bounceProfile[30] = + // Profile for a bouncing signal or turnout + // The profile below is in the range 0-100% and should be combined with the desired limits + // of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, + // i.e. the bounce is the same on the down action as on the up action. First entry isn't used. + const byte FLASH _bounceProfile[30] = {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; // EX-IOExpander protocol flags From afc94a75bbc8b152d5305b195733cd022d540173 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 07:39:58 +1000 Subject: [PATCH 22/52] Remove excess drivers --- IO_PCA9685_basic.h | 149 ------------------------ IO_Servo.h | 277 --------------------------------------------- 2 files changed, 426 deletions(-) delete mode 100644 IO_PCA9685_basic.h delete mode 100644 IO_Servo.h diff --git a/IO_PCA9685_basic.h b/IO_PCA9685_basic.h deleted file mode 100644 index 4f809aa..0000000 --- a/IO_PCA9685_basic.h +++ /dev/null @@ -1,149 +0,0 @@ -/* - * © 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 . - */ - -/* - * 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%). - */ - -#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 PCA9685_basic : public IODevice { -public: - // Create device driver instance. - static void create(VPIN firstVpin, int nPins, uint8_t I2CAddress) { - if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new PCA9685_basic(firstVpin, nPins, I2CAddress); - } - -private: - - // structures for setting up non-blocking writes to servo controller - I2CRB requestBlock; - uint8_t outputBuffer[5]; - - // REGISTER ADDRESSES - const byte PCA9685_MODE1=0x00; // Mode Register - const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/ - const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */ - // MODE1 bits - const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */ - const byte MODE1_AI=0x20; /**< Auto-Increment enabled */ - const byte MODE1_RESTART=0x80; /**< Restart enabled */ - - const float FREQUENCY_OSCILLATOR=25000000.0; /** 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 - PCA9685_basic(VPIN firstVpin, int nPins, uint8_t I2CAddress) { - _firstVpin = firstVpin; - _nPins = min(nPins, 16); - _I2CAddress = I2CAddress; - 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, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period. - 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 profile, uint16_t duration) override { - #ifdef DIAG_IO - DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), - vpin, value, profile, duration, _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("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (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("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value); - #endif - // Wait for previous request to complete - uint8_t status = requestBlock.wait(); - if (status != I2C_STATUS_OK) { - _deviceState = DEVSTATE_FAILED; - DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, 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(byte address, byte reg, byte value) { - I2CManager.write(address, 2, reg, value); - } - -}; - -#endif \ No newline at end of file diff --git a/IO_Servo.h b/IO_Servo.h deleted file mode 100644 index b1935b6..0000000 --- a/IO_Servo.h +++ /dev/null @@ -1,277 +0,0 @@ -/* - * © 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 . - */ -#ifndef IO_SERVO_H - -#include "IODevice.h" -#include "I2CManager.h" -#include "DIAG.h" - -class Servo : IODevice { - -public: - enum ProfileType : uint8_t { - Instant = 0, // Moves immediately between positions (if duration not specified) - UseDuration = 0, // Use specified duration - Fast = 1, // Takes around 500ms end-to-end - Medium = 2, // 1 second end-to-end - Slow = 3, // 2 seconds end-to-end - Bounce = 4, // For semaphores/turnouts with a bit of bounce!! - NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. - }; - - // Create device driver instance. - static void create(VPIN firstVpin, int nPins, VPIN firstSlavePin) { - if (checkNoOverlap(firstVpin, nPins)) new Servo(firstVpin, nPins, firstSlavePin); - } - -private: - VPIN _firstSlavePin; - IODevice *_slaveDevice = NULL; - - struct ServoData { - uint16_t activePosition : 12; // Config parameter - uint16_t inactivePosition : 12; // Config parameter - uint16_t currentPosition : 12; - uint16_t fromPosition : 12; - uint16_t toPosition : 12; - uint8_t profile; // Config parameter - uint16_t stepNumber; // Index of current step (starting from 0) - uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. - uint8_t currentProfile; // profile being used for current animation. - uint16_t duration; // time (tenths of a second) for animation to complete. - }; // 14 bytes per element, i.e. per pin in use - - struct ServoData *_servoData [16]; - - static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const uint8_t FLASH _bounceProfile[30]; - - const unsigned int refreshInterval = 50; // refresh every 50ms - - - // Configure a port on the Servo. - bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { - if (configType != CONFIGURE_SERVO) return false; - if (paramCount != 5) return false; - #ifdef DIAG_IO - DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), - vpin, params[0], params[1], params[2], params[3], params[4]); - #endif - - int8_t pin = vpin - _firstVpin; - VPIN slavePin = vpin - _firstVpin + _firstSlavePin; - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); - s = _servoData[pin]; - if (!s) return false; // Check for failed memory allocation - } - - s->activePosition = params[0]; - s->inactivePosition = params[1]; - s->profile = params[2]; - s->duration = params[3]; - int state = params[4]; - - if (state != -1) { - // Position servo to initial state - IODevice::writeAnalogue(slavePin, state ? s->activePosition : s->inactivePosition, 0, 0); - } - return true; - } - - // Constructor - Servo(VPIN firstVpin, int nPins, VPIN firstSlavePin) { - _firstVpin = firstVpin; - _nPins = (nPins > 16) ? 16 : nPins; - _firstSlavePin = firstSlavePin; - - // To save RAM, space for servo configuration is not allocated unless a pin is used. - // Initialise the pointers to NULL. - for (int i=0; i<_nPins; i++) - _servoData[i] = NULL; - - addDevice(this); - } - - // Device-specific initialisation - void _begin() override { - // Get reference to slave device to make accesses faster. - _slaveDevice = this->findDevice(_firstSlavePin); - // Check firstSlavePin is actually allocated to a device - if (!_slaveDevice) { - DIAG(F("Servo: Slave device not found on pins %d-%d"), - _firstSlavePin, _firstSlavePin+_nPins-1); - _deviceState = DEVSTATE_FAILED; - } - // Check that the last slave pin is allocated to the same device. - if (_slaveDevice != this->findDevice(_firstSlavePin+_nPins-1)) { - DIAG(F("Servo: Slave device does not cover all pins %d-%d"), - _firstSlavePin, _firstSlavePin+_nPins-1); - _deviceState = DEVSTATE_FAILED; - } - #if defined(DIAG_IO) - _display(); - #endif - } - - // Device-specific write function, invoked from IODevice::write(). - // For this function, the configured profile is used. - void _write(VPIN vpin, int value) override { - if (_deviceState == DEVSTATE_FAILED) return; - #ifdef DIAG_IO - DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); - #endif - int pin = vpin - _firstVpin; - // VPIN slavePin = vpin - _firstVpin + _firstSlavePin; - if (value) value = 1; - - struct ServoData *s = _servoData[pin]; - if (s != NULL) { - // Use configured parameters - this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); - } else { - /* simulate digital pin on PWM */ - this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); - } - } - - // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue(). - // Profile is as follows: - // Bit 7: 0=Set PWM to 0% to power off servo motor when finished - // 1=Keep PWM pulses on (better when using PWM to drive an LED) - // Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds) - // 1 (Fast) Move servo in 0.5 seconds - // 2 (Medium) Move servo in 1.0 seconds - // 3 (Slow) Move servo in 2.0 seconds - // 4 (Bounce) Servo 'bounces' at extremes. - // - void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { - #ifdef DIAG_IO - DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), - vpin, value, profile, duration, _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; - - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - // Servo pin not configured, so configure now using defaults - s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); - if (s == NULL) return; // Check for memory allocation failure - s->activePosition = 4095; - s->inactivePosition = 0; - s->currentPosition = value; - s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) - } - - // Animated profile. Initiate the appropriate action. - s->currentProfile = profile; - uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. - s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds - profileValue==Medium ? 20 : // 1.0 seconds - profileValue==Slow ? 40 : // 2.0 seconds - profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds - duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) - s->stepNumber = 0; - s->toPosition = value; - s->fromPosition = s->currentPosition; - } - - // _read returns true if the device is currently in executing an animation, - // changing the output over a period of time. - int _read(VPIN vpin) override { - if (_deviceState == DEVSTATE_FAILED) return 0; - int pin = vpin - _firstVpin; - struct ServoData *s = _servoData[pin]; - if (s == NULL) - return false; // No structure means no animation! - else - return (s->stepNumber < s->numSteps); - } - - void _loop(unsigned long currentMicros) override { - if (_deviceState == DEVSTATE_FAILED) return; - for (int pin=0; pin<_nPins; pin++) { - updatePosition(pin); - } - delayUntil(currentMicros + refreshInterval * 1000UL); - } - - // Private function to reposition servo - // TODO: Could calculate step number from elapsed time, to allow for erratic loop timing. - void updatePosition(uint8_t pin) { - struct ServoData *s = _servoData[pin]; - if (s == NULL) return; // No pin configuration/state data - - if (s->numSteps == 0) return; // No animation in progress - - if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { - // Go straight to end of sequence, output final position. - s->stepNumber = s->numSteps-1; - } - - if (s->stepNumber < s->numSteps) { - // Animation in progress, reposition servo - s->stepNumber++; - if ((s->currentProfile & ~NoPowerOff) == Bounce) { - // Retrieve step positions from array in flash - uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); - s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); - } else { - // All other profiles - calculate step by linear interpolation between from and to positions. - s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); - } - // Send servo command - _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); - } else if (s->stepNumber < s->numSteps + _catchupSteps) { - // We've finished animation, wait a little to allow servo to catch up - s->stepNumber++; - } else if (s->stepNumber == s->numSteps + _catchupSteps - && s->currentPosition != 0) { - #ifdef IO_SWITCH_OFF_SERVO - if ((s->currentProfile & NoPowerOff) == 0) { - // Wait has finished, so switch off PWM to prevent annoying servo buzz - _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); - } - #endif - s->numSteps = 0; // Done now. - } - } - - // Display details of this device. - void _display() override { - DIAG(F("Servo Configured on Vpins:%d-%d, slave pins:%d-%d %S"), - (int)_firstVpin, (int)_firstVpin+_nPins-1, - (int)_firstSlavePin, (int)_firstSlavePin+_nPins-1, - (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); - } -}; - -// Profile for a bouncing signal or turnout -// The profile below is in the range 0-100% and should be combined with the desired limits -// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, -// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. -const byte FLASH Servo::_bounceProfile[30] = - {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; - - -#endif \ No newline at end of file From 5cb216dd79b7cbfabbdd632a3ed45e0c365d9f01 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 08:41:50 +1000 Subject: [PATCH 23/52] Servo functional --- IO_EXIOExpander.h | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 9912fe6..f7190ee 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -189,8 +189,13 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); - for (int pin=0; pin<_nPins; pin++) { - updatePosition(pin); + if ((currentMicros - _lastRefresh) / 1000UL > refreshInterval) { + _lastRefresh = currentMicros; + for (int pin=0; pin<_nPins; pin++) { + if (_servoData[pin] != NULL) { + updatePosition(pin); + } + } } } @@ -249,16 +254,8 @@ private: } } - // void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { int pin = vpin - _firstVpin; - /* Initial _writeAnalogue here - _command4Buffer[0] = EXIOWRAN; - _command4Buffer[1] = pin; - _command4Buffer[2] = value & 0xFF; - _command4Buffer[3] = value >> 8; - I2CManager.write(_i2cAddress, _command4Buffer, 4); - */ #ifdef DIAG_IO DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); @@ -320,12 +317,6 @@ private: s->stepNumber++; } else if (s->stepNumber == s->numSteps + _catchupSteps && s->currentPosition != 0) { - // #ifdef IO_SWITCH_OFF_SERVO - // if ((s->currentProfile & NoPowerOff) == 0) { - // // Wait has finished, so switch off PWM to prevent annoying servo buzz - // // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); - // } - // #endif s->numSteps = 0; // Done now. } } @@ -380,9 +371,9 @@ private: struct ServoData *_servoData[256]; static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - // static const uint8_t FLASH _bounceProfile[30]; - + const unsigned int refreshInterval = 50; // refresh every 50ms + unsigned long _lastRefresh = 0; // Profile for a bouncing signal or turnout // The profile below is in the range 0-100% and should be combined with the desired limits From deb49f294356cba8aea36d11af0b16541f98c30a Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 09:31:09 +1000 Subject: [PATCH 24/52] Fix dynamic RAM allocation --- IO_EXIOExpander.h | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index f7190ee..afe6457 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -72,6 +72,7 @@ private: _i2cAddress = i2cAddress; // To save RAM, space for servo configuration is not allocated unless a pin is used. // Initialise the pointers to NULL. + _servoData = (ServoData**) calloc(_nPins, sizeof(ServoData*)); for (int i=0; i<_nPins; i++) { _servoData[i] = NULL; } @@ -139,28 +140,6 @@ private: DIAG(F("Vpin %d cannot be used as a digital input pin"), (int)vpin); return false; } - } else if (configType == CONFIGURE_SERVO) { - if (paramCount != 5) return false; -#ifdef DIAG_IO - DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), - vpin, params[0], params[1], params[2], params[3], params[4]); -#endif - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); - s = _servoData[pin]; - if (!s) return false; // Check for failed memory allocation - } - s->activePosition = params[0]; - s->inactivePosition = params[1]; - s->profile = params[2]; - s->duration = params[3]; - int state = params[4]; - if (state != -1) { - // Position servo to initial state - IODevice::writeAnalogue(pin, state ? s->activePosition : s->inactivePosition, 0, 0); - } - return true; } else { return false; } @@ -368,7 +347,8 @@ private: uint16_t duration; // time (tenths of a second) for animation to complete. }; // 14 bytes per element, i.e. per pin in use - struct ServoData *_servoData[256]; + // struct ServoData *_servoData[256]; + ServoData** _servoData; static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off From 93ac1b6d611f3dd8e422e29d84e06e988c92485a Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 12:45:34 +1000 Subject: [PATCH 25/52] Revert IODevice.h change --- IODevice.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IODevice.h b/IODevice.h index e7906c6..72beb9e 100644 --- a/IODevice.h +++ b/IODevice.h @@ -242,12 +242,11 @@ protected: // Current state of device DeviceStateEnum _deviceState = DEVSTATE_DORMANT; - // Method to find device handling Vpin - static IODevice *findDevice(VPIN vpin); - private: // Method to check whether the vpin corresponds to this device bool owns(VPIN vpin); + // Method to find device handling Vpin + static IODevice *findDevice(VPIN vpin); IODevice *_nextDevice = 0; unsigned long _nextEntryTime; static IODevice *_firstDevice; From d8cbdb24e154766d16ccbeb00f94c3645e17cf47 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 26/52] Refactored, analogue tested --- IO_EXIOExpander.h | 97 ++++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index d54feac..724ea42 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -54,22 +54,18 @@ */ class EXIOExpander : public IODevice { public: - static void create(VPIN vpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { - if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress, numDigitalPins, numAnaloguePins); + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor - EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress, int numDigitalPins, int numAnaloguePins) { + EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; - _numDigitalPins = numDigitalPins; - _numAnaloguePins = numAnaloguePins; - _digitalPinBytes = (numDigitalPins+7)/8; - _analoguePinBytes = numAnaloguePins * 2; + _digitalPinBytes = (nPins+7)/8; _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); - _analogueInputStates=(byte*) calloc(_analoguePinBytes,1); addDevice(this); } @@ -77,20 +73,26 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { - _digitalOutBuffer[0] = EXIOINIT; - _digitalOutBuffer[1] = _numDigitalPins; - _digitalOutBuffer[2] = _numAnaloguePins; - // Send config, if EXIORDY returned, we're good, otherwise go offline - I2CManager.read(_i2cAddress, _commandBuffer, 1, _digitalOutBuffer, 3); - if (_commandBuffer[0] != EXIORDY) { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; return; } + // We now need to retrieve the analogue pin map + _command1Buffer[0] = EXIOINITA; + I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline - // Using digital in buffer in reverse to save RAM - _commandBuffer[0] = EXIOVER; - I2CManager.read(_i2cAddress, _versionBuffer, 3, _commandBuffer, 1); + _command1Buffer[0] = EXIOVER; + I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); _majorVer = _versionBuffer[0]; _minorVer = _versionBuffer[1]; _patchVer = _versionBuffer[2]; @@ -105,13 +107,10 @@ private: } } + // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (configType != CONFIGURE_INPUT) return false; if (paramCount != 1) return false; - if (vpin >= _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is an analogue pin, cannot use as a digital pin"), vpin); - return false; - } bool pullup = params[0]; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIODPUP; @@ -121,37 +120,39 @@ private: return true; } - // We only use this to detect incorrect use of analogue pins + // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) { - DIAG(F("EX-IOExpander ERROR: Vpin %d is a digital pin, cannot use as an analogue pin"), vpin); - return false; - } int pin = vpin - _firstVpin; - _analogueOutBuffer[0] = EXIOENAN; - _analogueOutBuffer[1] = pin; - I2CManager.write(_i2cAddress, _analogueOutBuffer, 2); + _command2Buffer[0] = EXIOENAN; + _command2Buffer[1] = pin; + I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } + // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning - _commandBuffer[0] = EXIORDD; - I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _commandBuffer, 1); - _commandBuffer[0] = EXIORDAN; - I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _commandBuffer, 1); + _command1Buffer[0] = EXIORDD; + I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDAN; + I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } + // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { - if (vpin < _firstVpin + _numDigitalPins) return false; - int pin = vpin - _firstVpin - _numDigitalPins; - uint8_t _pinLSBByte = pin * 2; + int pin = vpin - _firstVpin; + uint8_t _pinLSBByte; + for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { + if (_analoguePinMap[aPin] == pin) { + _pinLSBByte = aPin * 2; + } + } uint8_t _pinMSBByte = _pinLSBByte + 1; return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } + // Obtain the correct digital input value int _read(VPIN vpin) override { - if (vpin >= _firstVpin + _numDigitalPins) return false; int pin = vpin - _firstVpin; uint8_t pinByte = pin / 8; bool value = _digitalInputStates[pinByte] >> (pin - pinByte * 8); @@ -159,7 +160,6 @@ private: } void _write(VPIN vpin, int value) override { - if (vpin >= _firstVpin + _numDigitalPins) return; int pin = vpin - _firstVpin; _digitalOutBuffer[0] = EXIOWRD; _digitalOutBuffer[1] = pin; @@ -168,25 +168,14 @@ private: } void _display() override { - int _firstAnalogue, _lastAnalogue; - if (_numAnaloguePins == 0) { - _firstAnalogue = 0; - _lastAnalogue = 0; - } else { - _firstAnalogue = _firstVpin + _numDigitalPins; - _lastAnalogue = _firstVpin + _nPins - 1; - } - DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d: %d Digital Vpins %d-%d, %d Analogue Vpins %d-%d %S"), + DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, - _numDigitalPins, _firstVpin, _firstVpin + _numDigitalPins - 1, - _numAnaloguePins, _firstAnalogue, _lastAnalogue, + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } uint8_t _i2cAddress; - uint8_t _numDigitalPins; uint8_t _numAnaloguePins; - byte _analogueOutBuffer[2]; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -196,7 +185,10 @@ private: byte* _analogueInputStates; uint8_t _digitalPinBytes = 0; uint8_t _analoguePinBytes = 0; - byte _commandBuffer[1]; + byte _command1Buffer[1]; + byte _command2Buffer[2]; + byte _receive2Buffer[2]; + uint8_t* _analoguePinMap; enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure @@ -207,6 +199,7 @@ private: EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input EXIOENAN = 0xE7, // Flag eo enable an analogue pin + EXIOINITA = 0xE8, // Flag we're receiving analogue pin info }; }; From d41b5e09387e1a5514ca372cf77cb9c17dea892f Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 19:26:33 +1000 Subject: [PATCH 27/52] Brief PWM start --- IO_EXIOExpander.h | 76 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 724ea42..f284bc1 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -1,5 +1,5 @@ /* - * © 2021, Peter Cole. All rights reserved. + * © 2022, Peter Cole. All rights reserved. * * This file is part of EX-CommandStation * @@ -26,19 +26,14 @@ * (Note the device driver is included by default) * * void halSetup() { -* // EXIOExpander::create(vpin, num_vpins, i2c_address, digitalPinCount, analoguePinCount); -* EXIOExpander::create(800, 18, 0x65, 12, 8); +* // EXIOExpander::create(vpin, num_vpins, i2c_address); +* EXIOExpander::create(800, 18, 0x65); * } * -* Note when defining the number of digital and analogue pins, there is no way to sanity check -* this from the device driver, and it is up to the user to define the correct values here. -* -* All pins available on the EX-IOExpander device must be accounted for. -* -* Vpins are allocated to digital pins first, and then analogue pins, so digital pins will -* populate the first part of the specified vpin range, with the analogue pins populating the -* last part of the vpin range. -* Eg. for a default Nano, 800 - 811 are digital (D2 - D13), 812 to 817 are analogue (A0 - A3, A6/A7). +* All pins on an EX-IOExpander device are allocated according to the pin map for the specific +* device in use. There is no way for the device driver to sanity check pins are used for the +* correct purpose, however the EX-IOExpander device's pin map will prevent pins being used +* incorrectly (eg. A6/7 on Nano cannot be used for digital input/output). */ #ifndef IO_EX_IOEXPANDER_H @@ -76,12 +71,15 @@ private: _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive3Buffer[1]; + _numPWMPins = _receive3Buffer[2]; _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + _servoData = (struct ServoData*) calloc(_numPWMPins, 14); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -109,15 +107,24 @@ private: // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { - if (configType != CONFIGURE_INPUT) return false; if (paramCount != 1) return false; - bool pullup = params[0]; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + if (configType == CONFIGURE_INPUT) { + bool pullup = params[0]; + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; + } else if (configType == CONFIGURE_SERVO) { + DIAG(F("Configure servo at pin %d"), (int)pin); + for (int i = 0; i < paramCount; i++) { + DIAG(F("Param %d is %x"), (int)i, params[i]); + } + return true; + } else { + return false; + } } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -175,7 +182,7 @@ private: } uint8_t _i2cAddress; - uint8_t _numAnaloguePins; + uint8_t _numAnaloguePins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -187,8 +194,29 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; + uint8_t _numPWMPins = 0; + + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + } ServoData; // 14 bytes per element, i.e. per pin in use + + struct ServoData* _servoData; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const byte FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From 53215b496e71b07541fcde0fa165654370e00fcc Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 28/52] Refactored, analogue tested --- IO_EXIOExpander.h | 75 +++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index f284bc1..c962623 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -71,15 +71,12 @@ private: _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); - if (_receive3Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive3Buffer[1]; - _numPWMPins = _receive3Buffer[2]; + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - _servoData = (struct ServoData*) calloc(_numPWMPins, 14); - } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -105,26 +102,17 @@ private: } } + // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; + bool pullup = params[0]; int pin = vpin - _firstVpin; - if (configType == CONFIGURE_INPUT) { - bool pullup = params[0]; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; - } else if (configType == CONFIGURE_SERVO) { - DIAG(F("Configure servo at pin %d"), (int)pin); - for (int i = 0; i < paramCount; i++) { - DIAG(F("Param %d is %x"), (int)i, params[i]); - } - return true; - } else { - return false; - } + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -133,9 +121,13 @@ private: _command2Buffer[0] = EXIOENAN; _command2Buffer[1] = pin; I2CManager.write(_i2cAddress, _command2Buffer, 2); + _command2Buffer[0] = EXIOENAN; + _command2Buffer[1] = pin; + I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } + // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning @@ -143,10 +135,22 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDD; + I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); + _command1Buffer[0] = EXIORDAN; + I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } + // Obtain the correct analogue input value // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { + int pin = vpin - _firstVpin; + uint8_t _pinLSBByte; + for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { + if (_analoguePinMap[aPin] == pin) { + _pinLSBByte = aPin * 2; + } + } int pin = vpin - _firstVpin; uint8_t _pinLSBByte; for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { @@ -158,6 +162,7 @@ private: return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } + // Obtain the correct digital input value // Obtain the correct digital input value int _read(VPIN vpin) override { int pin = vpin - _firstVpin; @@ -178,11 +183,12 @@ private: DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, (int)_firstVpin, (int)_firstVpin+_nPins-1, + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } uint8_t _i2cAddress; - uint8_t _numAnaloguePins = 0; + uint8_t _numAnaloguePins; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -194,29 +200,8 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive3Buffer[3]; + byte _receive2Buffer[2]; uint8_t* _analoguePinMap; - uint8_t _numPWMPins = 0; - - struct ServoData { - uint16_t activePosition : 12; // Config parameter - uint16_t inactivePosition : 12; // Config parameter - uint16_t currentPosition : 12; - uint16_t fromPosition : 12; - uint16_t toPosition : 12; - uint8_t profile; // Config parameter - uint16_t stepNumber; // Index of current step (starting from 0) - uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. - uint8_t currentProfile; // profile being used for current animation. - uint16_t duration; // time (tenths of a second) for animation to complete. - } ServoData; // 14 bytes per element, i.e. per pin in use - - struct ServoData* _servoData; - - static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const byte FLASH _bounceProfile[30]; - - const unsigned int refreshInterval = 50; // refresh every 50ms enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From 0c2f8428dfdf324014654f6e4862c1cd3614c8e0 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 29/52] Refactored, analogue tested --- IO_EXIOExpander.h | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index c962623..5c10d88 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -49,12 +49,15 @@ */ class EXIOExpander : public IODevice { public: + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { + if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor + EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; @@ -68,6 +71,16 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline @@ -85,9 +98,14 @@ private: // We now need to retrieve the analogue pin map _command1Buffer[0] = EXIOINITA; I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); + // We now need to retrieve the analogue pin map + _command1Buffer[0] = EXIOINITA; + I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline _command1Buffer[0] = EXIOVER; I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); + _command1Buffer[0] = EXIOVER; + I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); _majorVer = _versionBuffer[0]; _minorVer = _versionBuffer[1]; _patchVer = _versionBuffer[2]; @@ -102,7 +120,6 @@ private: } } - // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; @@ -115,19 +132,16 @@ private: return true; } + // Analogue input pin configuration, used to enable on EX-IOExpander device // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { int pin = vpin - _firstVpin; _command2Buffer[0] = EXIOENAN; _command2Buffer[1] = pin; I2CManager.write(_i2cAddress, _command2Buffer, 2); - _command2Buffer[0] = EXIOENAN; - _command2Buffer[1] = pin; - I2CManager.write(_i2cAddress, _command2Buffer, 2); return true; } - // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning @@ -135,22 +149,10 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); - _command1Buffer[0] = EXIORDD; - I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); - _command1Buffer[0] = EXIORDAN; - I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); } - // Obtain the correct analogue input value // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { - int pin = vpin - _firstVpin; - uint8_t _pinLSBByte; - for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { - if (_analoguePinMap[aPin] == pin) { - _pinLSBByte = aPin * 2; - } - } int pin = vpin - _firstVpin; uint8_t _pinLSBByte; for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { @@ -162,7 +164,6 @@ private: return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte]; } - // Obtain the correct digital input value // Obtain the correct digital input value int _read(VPIN vpin) override { int pin = vpin - _firstVpin; @@ -180,10 +181,10 @@ private: } void _display() override { + DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, (int)_firstVpin, (int)_firstVpin+_nPins-1, - (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); } @@ -202,6 +203,10 @@ private: byte _command2Buffer[2]; byte _receive2Buffer[2]; uint8_t* _analoguePinMap; + byte _command1Buffer[1]; + byte _command2Buffer[2]; + byte _receive2Buffer[2]; + uint8_t* _analoguePinMap; enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From cf2817d7c41aca8eeea1e0846ebca5e43c68f6f6 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 19:26:33 +1000 Subject: [PATCH 30/52] Brief PWM start --- IO_EXIOExpander.h | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 5c10d88..5476a41 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -90,6 +90,18 @@ private: _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { + _command2Buffer[0] = EXIOINIT; + _command2Buffer[1] = _nPins; + // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive3Buffer[1]; + _numPWMPins = _receive3Buffer[2]; + _analoguePinBytes = _numAnaloguePins * 2; + _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); + _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -123,13 +135,23 @@ private: // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; - bool pullup = params[0]; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + if (configType == CONFIGURE_INPUT) { + bool pullup = params[0]; + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; + } else if (configType == CONFIGURE_SERVO) { + DIAG(F("Configure servo at pin %d"), (int)pin); + for (int i = 0; i < paramCount; i++) { + DIAG(F("Param %d is %x"), (int)i, params[i]); + } + return true; + } else { + return false; + } } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -189,7 +211,7 @@ private: } uint8_t _i2cAddress; - uint8_t _numAnaloguePins; + uint8_t _numAnaloguePins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -201,7 +223,7 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; byte _command1Buffer[1]; byte _command2Buffer[2]; From 984ef6feadfbe99e5e4b1869749b98ce0eee653a Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 10:06:01 +1000 Subject: [PATCH 31/52] Refactored, analogue tested --- IO_EXIOExpander.h | 60 ++++++----------------------------------------- 1 file changed, 7 insertions(+), 53 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 5476a41..1b0d23f 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -49,15 +49,12 @@ */ class EXIOExpander : public IODevice { public: - static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { - if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } private: // Constructor - EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { EXIOExpander(VPIN firstVpin, int nPins, uint8_t i2cAddress) { _firstVpin = firstVpin; _nPins = nPins; @@ -80,28 +77,6 @@ private: _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { - _command2Buffer[0] = EXIOINIT; - _command2Buffer[1] = _nPins; - // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; - _analoguePinBytes = _numAnaloguePins * 2; - _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); - _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { - _command2Buffer[0] = EXIOINIT; - _command2Buffer[1] = _nPins; - // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); - if (_receive3Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive3Buffer[1]; - _numPWMPins = _receive3Buffer[2]; - _analoguePinBytes = _numAnaloguePins * 2; - _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); - _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -110,14 +85,9 @@ private: // We now need to retrieve the analogue pin map _command1Buffer[0] = EXIOINITA; I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); - // We now need to retrieve the analogue pin map - _command1Buffer[0] = EXIOINITA; - I2CManager.read(_i2cAddress, _analoguePinMap, _numAnaloguePins, _command1Buffer, 1); // Attempt to get version, if we don't get it, we don't care, don't go offline _command1Buffer[0] = EXIOVER; I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); - _command1Buffer[0] = EXIOVER; - I2CManager.read(_i2cAddress, _versionBuffer, 3, _command1Buffer, 1); _majorVer = _versionBuffer[0]; _minorVer = _versionBuffer[1]; _patchVer = _versionBuffer[2]; @@ -135,26 +105,15 @@ private: // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; + bool pullup = params[0]; int pin = vpin - _firstVpin; - if (configType == CONFIGURE_INPUT) { - bool pullup = params[0]; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; - } else if (configType == CONFIGURE_SERVO) { - DIAG(F("Configure servo at pin %d"), (int)pin); - for (int i = 0; i < paramCount; i++) { - DIAG(F("Param %d is %x"), (int)i, params[i]); - } - return true; - } else { - return false; - } + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; } - // Analogue input pin configuration, used to enable on EX-IOExpander device // Analogue input pin configuration, used to enable on EX-IOExpander device int _configureAnalogIn(VPIN vpin) override { int pin = vpin - _firstVpin; @@ -203,7 +162,6 @@ private: } void _display() override { - DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, (int)_firstVpin, (int)_firstVpin+_nPins-1, @@ -211,7 +169,7 @@ private: } uint8_t _i2cAddress; - uint8_t _numAnaloguePins = 0; + uint8_t _numAnaloguePins; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -223,10 +181,6 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive3Buffer[3]; - uint8_t* _analoguePinMap; - byte _command1Buffer[1]; - byte _command2Buffer[2]; byte _receive2Buffer[2]; uint8_t* _analoguePinMap; From fa38583772b0286768e20de3c6d0d6835aa34112 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 29 Jan 2023 19:26:33 +1000 Subject: [PATCH 32/52] Brief PWM start --- IO_EXIOExpander.h | 56 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 1b0d23f..f284bc1 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -71,12 +71,15 @@ private: _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive3Buffer[1]; + _numPWMPins = _receive3Buffer[2]; _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); + _servoData = (struct ServoData*) calloc(_numPWMPins, 14); + } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -105,13 +108,23 @@ private: // Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if in use bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override { if (paramCount != 1) return false; - bool pullup = params[0]; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIODPUP; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + if (configType == CONFIGURE_INPUT) { + bool pullup = params[0]; + _digitalOutBuffer[0] = EXIODPUP; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = pullup; + I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + return true; + } else if (configType == CONFIGURE_SERVO) { + DIAG(F("Configure servo at pin %d"), (int)pin); + for (int i = 0; i < paramCount; i++) { + DIAG(F("Param %d is %x"), (int)i, params[i]); + } + return true; + } else { + return false; + } } // Analogue input pin configuration, used to enable on EX-IOExpander device @@ -169,7 +182,7 @@ private: } uint8_t _i2cAddress; - uint8_t _numAnaloguePins; + uint8_t _numAnaloguePins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -181,8 +194,29 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; + uint8_t _numPWMPins = 0; + + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + } ServoData; // 14 bytes per element, i.e. per pin in use + + struct ServoData* _servoData; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const byte FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From d375723a137219192d36baea6b135eec76ae1a4b Mon Sep 17 00:00:00 2001 From: peteGSX Date: Tue, 31 Jan 2023 19:29:39 +1000 Subject: [PATCH 33/52] Cleaned up PWM start --- IO_EXIOExpander.h | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index f284bc1..34ece1b 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -78,7 +78,6 @@ private: _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - _servoData = (struct ServoData*) calloc(_numPWMPins, 14); } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); @@ -198,26 +197,6 @@ private: uint8_t* _analoguePinMap; uint8_t _numPWMPins = 0; - struct ServoData { - uint16_t activePosition : 12; // Config parameter - uint16_t inactivePosition : 12; // Config parameter - uint16_t currentPosition : 12; - uint16_t fromPosition : 12; - uint16_t toPosition : 12; - uint8_t profile; // Config parameter - uint16_t stepNumber; // Index of current step (starting from 0) - uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. - uint8_t currentProfile; // profile being used for current animation. - uint16_t duration; // time (tenths of a second) for animation to complete. - } ServoData; // 14 bytes per element, i.e. per pin in use - - struct ServoData* _servoData; - - static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const byte FLASH _bounceProfile[30]; - - const unsigned int refreshInterval = 50; // refresh every 50ms - enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup From 6031a0fb7fc841fbc02cf4e8f53aac59c0500da4 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Wed, 1 Feb 2023 07:49:31 +1000 Subject: [PATCH 34/52] Fix mess after rebase and conflicts --- IO_EXIOExpander.h | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 34ece1b..69fb6af 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -71,14 +71,12 @@ private: _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); - if (_receive3Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive3Buffer[1]; - _numPWMPins = _receive3Buffer[2]; + I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); + if (_receive2Buffer[0] == EXIOINITA) { + _numAnaloguePins = _receive2Buffer[1]; _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); - } else { DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress); _deviceState = DEVSTATE_FAILED; @@ -115,12 +113,6 @@ private: _digitalOutBuffer[2] = pullup; I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); return true; - } else if (configType == CONFIGURE_SERVO) { - DIAG(F("Configure servo at pin %d"), (int)pin); - for (int i = 0; i < paramCount; i++) { - DIAG(F("Param %d is %x"), (int)i, params[i]); - } - return true; } else { return false; } @@ -182,6 +174,7 @@ private: uint8_t _i2cAddress; uint8_t _numAnaloguePins = 0; + uint8_t numDigitalPins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -193,9 +186,8 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive3Buffer[3]; + byte _receive2Buffer[2]; uint8_t* _analoguePinMap; - uint8_t _numPWMPins = 0; enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From 4d31cd64a54f2c2f2386bc40cb6c6e24858c8351 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Tue, 31 Jan 2023 19:32:12 +1000 Subject: [PATCH 35/52] Add new drivers --- IODevice.h | 5 +- IO_PCA9685_basic.h | 149 ++++++++++++++++++++++++ IO_Servo.h | 277 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 IO_PCA9685_basic.h create mode 100644 IO_Servo.h diff --git a/IODevice.h b/IODevice.h index 72beb9e..e7906c6 100644 --- a/IODevice.h +++ b/IODevice.h @@ -242,11 +242,12 @@ protected: // Current state of device DeviceStateEnum _deviceState = DEVSTATE_DORMANT; + // Method to find device handling Vpin + static IODevice *findDevice(VPIN vpin); + private: // Method to check whether the vpin corresponds to this device bool owns(VPIN vpin); - // Method to find device handling Vpin - static IODevice *findDevice(VPIN vpin); IODevice *_nextDevice = 0; unsigned long _nextEntryTime; static IODevice *_firstDevice; diff --git a/IO_PCA9685_basic.h b/IO_PCA9685_basic.h new file mode 100644 index 0000000..4f809aa --- /dev/null +++ b/IO_PCA9685_basic.h @@ -0,0 +1,149 @@ +/* + * © 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 . + */ + +/* + * 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%). + */ + +#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 PCA9685_basic : public IODevice { +public: + // Create device driver instance. + static void create(VPIN firstVpin, int nPins, uint8_t I2CAddress) { + if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new PCA9685_basic(firstVpin, nPins, I2CAddress); + } + +private: + + // structures for setting up non-blocking writes to servo controller + I2CRB requestBlock; + uint8_t outputBuffer[5]; + + // REGISTER ADDRESSES + const byte PCA9685_MODE1=0x00; // Mode Register + const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/ + const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */ + // MODE1 bits + const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */ + const byte MODE1_AI=0x20; /**< Auto-Increment enabled */ + const byte MODE1_RESTART=0x80; /**< Restart enabled */ + + const float FREQUENCY_OSCILLATOR=25000000.0; /** 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 + PCA9685_basic(VPIN firstVpin, int nPins, uint8_t I2CAddress) { + _firstVpin = firstVpin; + _nPins = min(nPins, 16); + _I2CAddress = I2CAddress; + 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, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period. + 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 profile, uint16_t duration) override { + #ifdef DIAG_IO + DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (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("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value); + #endif + // Wait for previous request to complete + uint8_t status = requestBlock.wait(); + if (status != I2C_STATUS_OK) { + _deviceState = DEVSTATE_FAILED; + DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, 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(byte address, byte reg, byte value) { + I2CManager.write(address, 2, reg, value); + } + +}; + +#endif \ No newline at end of file diff --git a/IO_Servo.h b/IO_Servo.h new file mode 100644 index 0000000..bd475fb --- /dev/null +++ b/IO_Servo.h @@ -0,0 +1,277 @@ +/* + * © 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 . + */ +#ifndef IO_SERVO_H + +#include "IODevice.h" +#include "I2CManager.h" +#include "DIAG.h" + +class Servo : IODevice { + +public: + enum ProfileType : uint8_t { + Instant = 0, // Moves immediately between positions (if duration not specified) + UseDuration = 0, // Use specified duration + Fast = 1, // Takes around 500ms end-to-end + Medium = 2, // 1 second end-to-end + Slow = 3, // 2 seconds end-to-end + Bounce = 4, // For semaphores/turnouts with a bit of bounce!! + NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. + }; + + // Create device driver instance. + static void create(VPIN firstVpin, int nPins, VPIN firstSlavePin) { + if (checkNoOverlap(firstVpin, nPins)) new Servo(firstVpin, nPins, firstSlavePin); + } + +private: + VPIN _firstSlavePin; + IODevice *_slaveDevice = NULL; + + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + }; // 14 bytes per element, i.e. per pin in use + + struct ServoData *_servoData [16]; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const uint8_t FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms + + + // Configure a port on the Servo. + bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { + if (configType != CONFIGURE_SERVO) return false; + if (paramCount != 5) return false; + #ifdef DIAG_IO + DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + vpin, params[0], params[1], params[2], params[3], params[4]); + #endif + + int8_t pin = vpin - _firstVpin; + VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); + s = _servoData[pin]; + if (!s) return false; // Check for failed memory allocation + } + + s->activePosition = params[0]; + s->inactivePosition = params[1]; + s->profile = params[2]; + s->duration = params[3]; + int state = params[4]; + + if (state != -1) { + // Position servo to initial state + IODevice::writeAnalogue(slavePin, state ? s->activePosition : s->inactivePosition, 0, 0); + } + return true; + } + + // Constructor + Servo(VPIN firstVpin, int nPins, VPIN firstSlavePin) { + _firstVpin = firstVpin; + _nPins = (nPins > 16) ? 16 : nPins; + _firstSlavePin = firstSlavePin; + + // To save RAM, space for servo configuration is not allocated unless a pin is used. + // Initialise the pointers to NULL. + for (int i=0; i<_nPins; i++) + _servoData[i] = NULL; + + addDevice(this); + } + + // Device-specific initialisation + void _begin() override { + // Get reference to slave device to make accesses faster. + _slaveDevice = this->findDevice(_firstSlavePin); + // Check firstSlavePin is actually allocated to a device + if (!_slaveDevice) { + DIAG(F("Servo: Slave device not found on pins %d-%d"), + _firstSlavePin, _firstSlavePin+_nPins-1); + _deviceState = DEVSTATE_FAILED; + } + // Check that the last slave pin is allocated to the same device. + if (_slaveDevice != this->findDevice(_firstSlavePin+_nPins-1)) { + DIAG(F("Servo: Slave device does not cover all pins %d-%d"), + _firstSlavePin, _firstSlavePin+_nPins-1); + _deviceState = DEVSTATE_FAILED; + } + #if defined(DIAG_IO) + _display(); + #endif + } + + // Device-specific write function, invoked from IODevice::write(). + // For this function, the configured profile is used. + void _write(VPIN vpin, int value) override { + if (_deviceState == DEVSTATE_FAILED) return; + #ifdef DIAG_IO + DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); + #endif + int pin = vpin - _firstVpin; + VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + if (value) value = 1; + + struct ServoData *s = _servoData[pin]; + if (s != NULL) { + // Use configured parameters + this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); + } else { + /* simulate digital pin on PWM */ + this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); + } + } + + // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue(). + // Profile is as follows: + // Bit 7: 0=Set PWM to 0% to power off servo motor when finished + // 1=Keep PWM pulses on (better when using PWM to drive an LED) + // Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds) + // 1 (Fast) Move servo in 0.5 seconds + // 2 (Medium) Move servo in 1.0 seconds + // 3 (Slow) Move servo in 2.0 seconds + // 4 (Bounce) Servo 'bounces' at extremes. + // + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { + #ifdef DIAG_IO + DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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; + + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + // Servo pin not configured, so configure now using defaults + s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); + if (s == NULL) return; // Check for memory allocation failure + s->activePosition = 4095; + s->inactivePosition = 0; + s->currentPosition = value; + s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) + } + + // Animated profile. Initiate the appropriate action. + s->currentProfile = profile; + uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. + s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds + profileValue==Medium ? 20 : // 1.0 seconds + profileValue==Slow ? 40 : // 2.0 seconds + profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds + duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) + s->stepNumber = 0; + s->toPosition = value; + s->fromPosition = s->currentPosition; + } + + // _read returns true if the device is currently in executing an animation, + // changing the output over a period of time. + int _read(VPIN vpin) override { + if (_deviceState == DEVSTATE_FAILED) return 0; + int pin = vpin - _firstVpin; + struct ServoData *s = _servoData[pin]; + if (s == NULL) + return false; // No structure means no animation! + else + return (s->stepNumber < s->numSteps); + } + + void _loop(unsigned long currentMicros) override { + if (_deviceState == DEVSTATE_FAILED) return; + for (int pin=0; pin<_nPins; pin++) { + updatePosition(pin); + } + delayUntil(currentMicros + refreshInterval * 1000UL); + } + + // Private function to reposition servo + // TODO: Could calculate step number from elapsed time, to allow for erratic loop timing. + void updatePosition(uint8_t pin) { + struct ServoData *s = _servoData[pin]; + if (s == NULL) return; // No pin configuration/state data + + if (s->numSteps == 0) return; // No animation in progress + + if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { + // Go straight to end of sequence, output final position. + s->stepNumber = s->numSteps-1; + } + + if (s->stepNumber < s->numSteps) { + // Animation in progress, reposition servo + s->stepNumber++; + if ((s->currentProfile & ~NoPowerOff) == Bounce) { + // Retrieve step positions from array in flash + uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); + s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); + } else { + // All other profiles - calculate step by linear interpolation between from and to positions. + s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); + } + // Send servo command + _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); + } else if (s->stepNumber < s->numSteps + _catchupSteps) { + // We've finished animation, wait a little to allow servo to catch up + s->stepNumber++; + } else if (s->stepNumber == s->numSteps + _catchupSteps + && s->currentPosition != 0) { + #ifdef IO_SWITCH_OFF_SERVO + if ((s->currentProfile & NoPowerOff) == 0) { + // Wait has finished, so switch off PWM to prevent annoying servo buzz + _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); + } + #endif + s->numSteps = 0; // Done now. + } + } + + // Display details of this device. + void _display() override { + DIAG(F("Servo Configured on Vpins:%d-%d, slave pins:%d-%d %S"), + (int)_firstVpin, (int)_firstVpin+_nPins-1, + (int)_firstSlavePin, (int)_firstSlavePin+_nPins-1, + (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); + } +}; + +// Profile for a bouncing signal or turnout +// The profile below is in the range 0-100% and should be combined with the desired limits +// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, +// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. +const byte FLASH Servo::_bounceProfile[30] = + {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; + + +#endif \ No newline at end of file From e53ed7b46d4fbf41595186b56e0e16acdc83607f Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Wed, 1 Feb 2023 14:53:46 +1000 Subject: [PATCH 36/52] Brief start on PWM --- IO_EXIOExpander.h | 25 ++++++++++++++++--------- IO_Servo.h | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 69fb6af..f653a80 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -59,8 +59,6 @@ private: _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; - _digitalPinBytes = (nPins+7)/8; - _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); addDevice(this); } @@ -70,10 +68,13 @@ private: if (I2CManager.exists(_i2cAddress)) { _command2Buffer[0] = EXIOINIT; _command2Buffer[1] = _nPins; - // Send config, if EXIOINITA returned, we're good, setup analogue input buffer, otherwise go offline - I2CManager.read(_i2cAddress, _receive2Buffer, 2, _command2Buffer, 2); - if (_receive2Buffer[0] == EXIOINITA) { - _numAnaloguePins = _receive2Buffer[1]; + // Send config, if EXIOPINS returned, we're good, setup pin buffers, otherwise go offline + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + if (_receive3Buffer[0] == EXIOPINS) { + _numDigitalPins = _receive3Buffer[1]; + _numAnaloguePins = _receive3Buffer[2]; + _digitalPinBytes = (_numDigitalPins + 7)/8; + _digitalInputStates=(byte*) calloc(_digitalPinBytes,1); _analoguePinBytes = _numAnaloguePins * 2; _analogueInputStates = (byte*) calloc(_analoguePinBytes, 1); _analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1); @@ -165,6 +166,11 @@ private: I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); } + void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { + int pin = vpin - _firstVpin; + DIAG(F("Write %d to pin %d, param 1 %d, param 2 %d"), value, pin, param1, param2); + } + void _display() override { DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, @@ -173,8 +179,8 @@ private: } uint8_t _i2cAddress; + uint8_t _numDigitalPins = 0; uint8_t _numAnaloguePins = 0; - uint8_t numDigitalPins = 0; byte _digitalOutBuffer[3]; uint8_t _versionBuffer[3]; uint8_t _majorVer = 0; @@ -186,7 +192,7 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; - byte _receive2Buffer[2]; + byte _receive3Buffer[3]; uint8_t* _analoguePinMap; enum { @@ -198,7 +204,8 @@ private: EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input EXIOENAN = 0xE7, // Flag eo enable an analogue pin - EXIOINITA = 0xE8, // Flag we're receiving analogue pin info + EXIOINITA = 0xE8, // Flag we're receiving analogue pin mappings + EXIOPINS = 0xE9, // Flag we're receiving pin counts for buffers }; }; diff --git a/IO_Servo.h b/IO_Servo.h index bd475fb..b1935b6 100644 --- a/IO_Servo.h +++ b/IO_Servo.h @@ -139,7 +139,7 @@ private: DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); #endif int pin = vpin - _firstVpin; - VPIN slavePin = vpin - _firstVpin + _firstSlavePin; + // VPIN slavePin = vpin - _firstVpin + _firstSlavePin; if (value) value = 1; struct ServoData *s = _servoData[pin]; From 5efb0c50135ce93aff36cbc8cafd4e77dece0e83 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Wed, 1 Feb 2023 19:46:08 +1000 Subject: [PATCH 37/52] Basic PWM working --- IO_EXIOExpander.h | 19 ++++++++++++++----- version.h | 4 +++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index f653a80..cb31528 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -66,10 +66,12 @@ private: // Initialise EX-IOExander device I2CManager.begin(); if (I2CManager.exists(_i2cAddress)) { - _command2Buffer[0] = EXIOINIT; - _command2Buffer[1] = _nPins; + _command4Buffer[0] = EXIOINIT; + _command4Buffer[1] = _nPins; + _command4Buffer[2] = _firstVpin & 0xFF; + _command4Buffer[3] = _firstVpin >> 8; // Send config, if EXIOPINS returned, we're good, setup pin buffers, otherwise go offline - I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command2Buffer, 2); + I2CManager.read(_i2cAddress, _receive3Buffer, 3, _command4Buffer, 4); if (_receive3Buffer[0] == EXIOPINS) { _numDigitalPins = _receive3Buffer[1]; _numAnaloguePins = _receive3Buffer[2]; @@ -168,7 +170,11 @@ private: void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { int pin = vpin - _firstVpin; - DIAG(F("Write %d to pin %d, param 1 %d, param 2 %d"), value, pin, param1, param2); + _command4Buffer[0] = EXIOWRAN; + _command4Buffer[1] = pin; + _command4Buffer[2] = value & 0xFF; + _command4Buffer[3] = value >> 8; + I2CManager.write(_i2cAddress, _command4Buffer, 4); } void _display() override { @@ -192,6 +198,7 @@ private: uint8_t _analoguePinBytes = 0; byte _command1Buffer[1]; byte _command2Buffer[2]; + byte _command4Buffer[4]; byte _receive3Buffer[3]; uint8_t* _analoguePinMap; @@ -203,9 +210,11 @@ private: EXIORDAN = 0xE4, // Flag to read an analogue input EXIOWRD = 0xE5, // Flag for digital write EXIORDD = 0xE6, // Flag to read digital input - EXIOENAN = 0xE7, // Flag eo enable an analogue pin + EXIOENAN = 0xE7, // Flag to enable an analogue pin EXIOINITA = 0xE8, // Flag we're receiving analogue pin mappings EXIOPINS = 0xE9, // Flag we're receiving pin counts for buffers + EXIOWRAN = 0xEA, // Flag we're sending an analogue write (PWM) + EXIOERR = 0xEF, // Flag we've received an error }; }; diff --git a/version.h b/version.h index 6788645..c37a1c4 100644 --- a/version.h +++ b/version.h @@ -4,7 +4,9 @@ #include "StringFormatter.h" -#define VERSION "4.2.14" +#define VERSION "4.2.15" +// 4.2.15 Separate Servo from PCA9685 +// Add PWM support to EX-IOExpander // 4.2.14 STM32F4xx fast ADC read implementation // 4.2.13 Broadcast power for again // 4.2.12 Bugfix for issue #299 TurnoutDescription NULL From 2a3d48dc009625e1b4384b2c025d6628d160be2d Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sat, 4 Feb 2023 09:19:32 +1000 Subject: [PATCH 38/52] Fix digital read bug --- IO_EXIOExpander.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index cb31528..d7d73ae 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -156,7 +156,7 @@ private: int _read(VPIN vpin) override { int pin = vpin - _firstVpin; uint8_t pinByte = pin / 8; - bool value = _digitalInputStates[pinByte] >> (pin - pinByte * 8); + bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); return value; } From 938b4cfbd6acaa2ebd558ab5ed1f377d25c93b5c Mon Sep 17 00:00:00 2001 From: peteGSX Date: Mon, 6 Feb 2023 19:39:25 +1000 Subject: [PATCH 39/52] Update version --- version.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/version.h b/version.h index c37a1c4..d95e01d 100644 --- a/version.h +++ b/version.h @@ -5,8 +5,9 @@ #define VERSION "4.2.15" -// 4.2.15 Separate Servo from PCA9685 -// Add PWM support to EX-IOExpander +// 4.2.15 Add Servo device driver with PCA9685_basic driver +// Add basic experimental PWM support to EX-IOExpander +// EX-IOExpander 0.0.14 minimum required // 4.2.14 STM32F4xx fast ADC read implementation // 4.2.13 Broadcast power for again // 4.2.12 Bugfix for issue #299 TurnoutDescription NULL From ad97260055187bfacd4b4e6d634c012e1d297c81 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Tue, 7 Feb 2023 07:32:16 +1000 Subject: [PATCH 40/52] Add extra error checking --- IO_EXIOExpander.h | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index d7d73ae..43a6155 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -114,8 +114,13 @@ private: _digitalOutBuffer[0] = EXIODPUP; _digitalOutBuffer[1] = pin; _digitalOutBuffer[2] = pullup; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); - return true; + I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); + if (_command1Buffer[0] == EXIORDY) { + return true; + } else { + DIAG(F("Vpin %d cannot be used as a digital input pin"), (int)vpin); + return false; + } } else { return false; } @@ -126,7 +131,13 @@ private: int pin = vpin - _firstVpin; _command2Buffer[0] = EXIOENAN; _command2Buffer[1] = pin; - I2CManager.write(_i2cAddress, _command2Buffer, 2); + I2CManager.read(_i2cAddress, _command1Buffer, 1, _command2Buffer, 2); + if (_command1Buffer[0] == EXIORDY) { + return true; + } else { + DIAG(F("Vpin %d cannot be used as an analogue input pin"), (int)vpin); + return false; + } return true; } @@ -165,7 +176,10 @@ private: _digitalOutBuffer[0] = EXIOWRD; _digitalOutBuffer[1] = pin; _digitalOutBuffer[2] = value; - I2CManager.write(_i2cAddress, _digitalOutBuffer, 3); + I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); + if (_command1Buffer[0] != EXIORDY) { + DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + } } void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { From c768bdc36101b536822b01c53ca5a66bfc06ae1f Mon Sep 17 00:00:00 2001 From: peteGSX Date: Thu, 9 Feb 2023 05:32:27 +1000 Subject: [PATCH 41/52] Start adding servo to EX-IO --- IO_EXIOExpander.h | 142 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 43a6155..f29dad3 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -49,6 +49,17 @@ */ class EXIOExpander : public IODevice { public: + + enum ProfileType : uint8_t { + Instant = 0, // Moves immediately between positions (if duration not specified) + UseDuration = 0, // Use specified duration + Fast = 1, // Takes around 500ms end-to-end + Medium = 2, // 1 second end-to-end + Slow = 3, // 2 seconds end-to-end + Bounce = 4, // For semaphores/turnouts with a bit of bounce!! + NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. + }; + static void create(VPIN vpin, int nPins, uint8_t i2cAddress) { if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress); } @@ -59,6 +70,11 @@ private: _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; + // To save RAM, space for servo configuration is not allocated unless a pin is used. + // Initialise the pointers to NULL. + for (int i=0; i<_nPins; i++) { + _servoData[i] = NULL; + } addDevice(this); } @@ -121,6 +137,28 @@ private: DIAG(F("Vpin %d cannot be used as a digital input pin"), (int)vpin); return false; } + } else if (configType == CONFIGURE_SERVO) { + if (paramCount != 5) return false; +#ifdef DIAG_IO + DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), + vpin, params[0], params[1], params[2], params[3], params[4]); +#endif + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); + s = _servoData[pin]; + if (!s) return false; // Check for failed memory allocation + } + s->activePosition = params[0]; + s->inactivePosition = params[1]; + s->profile = params[2]; + s->duration = params[3]; + int state = params[4]; + if (state != -1) { + // Position servo to initial state + IODevice::writeAnalogue(pin, state ? s->activePosition : s->inactivePosition, 0, 0); + } + return true; } else { return false; } @@ -182,13 +220,86 @@ private: } } - void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { + // void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { int pin = vpin - _firstVpin; + /* Initial _writeAnalogue here _command4Buffer[0] = EXIOWRAN; _command4Buffer[1] = pin; _command4Buffer[2] = value & 0xFF; _command4Buffer[3] = value >> 8; I2CManager.write(_i2cAddress, _command4Buffer, 4); + */ + #ifdef DIAG_IO + DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _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; + + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + // Servo pin not configured, so configure now using defaults + s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); + if (s == NULL) return; // Check for memory allocation failure + s->activePosition = 4095; + s->inactivePosition = 0; + s->currentPosition = value; + s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) + } + + // Animated profile. Initiate the appropriate action. + s->currentProfile = profile; + uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. + s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds + profileValue==Medium ? 20 : // 1.0 seconds + profileValue==Slow ? 40 : // 2.0 seconds + profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds + duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) + s->stepNumber = 0; + s->toPosition = value; + s->fromPosition = s->currentPosition; + } + + void updatePosition(uint8_t pin) { + struct ServoData *s = _servoData[pin]; + if (s == NULL) return; // No pin configuration/state data + + if (s->numSteps == 0) return; // No animation in progress + + if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { + // Go straight to end of sequence, output final position. + s->stepNumber = s->numSteps-1; + } + + if (s->stepNumber < s->numSteps) { + // Animation in progress, reposition servo + s->stepNumber++; + if ((s->currentProfile & ~NoPowerOff) == Bounce) { + // Retrieve step positions from array in flash + uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); + s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); + } else { + // All other profiles - calculate step by linear interpolation between from and to positions. + s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); + } + // Send servo command + // _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); + } else if (s->stepNumber < s->numSteps + _catchupSteps) { + // We've finished animation, wait a little to allow servo to catch up + s->stepNumber++; + } else if (s->stepNumber == s->numSteps + _catchupSteps + && s->currentPosition != 0) { + #ifdef IO_SWITCH_OFF_SERVO + if ((s->currentProfile & NoPowerOff) == 0) { + // Wait has finished, so switch off PWM to prevent annoying servo buzz + // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); + } + #endif + s->numSteps = 0; // Done now. + } } void _display() override { @@ -216,6 +327,35 @@ private: byte _receive3Buffer[3]; uint8_t* _analoguePinMap; + // Servo specific + struct ServoData { + uint16_t activePosition : 12; // Config parameter + uint16_t inactivePosition : 12; // Config parameter + uint16_t currentPosition : 12; + uint16_t fromPosition : 12; + uint16_t toPosition : 12; + uint8_t profile; // Config parameter + uint16_t stepNumber; // Index of current step (starting from 0) + uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. + uint8_t currentProfile; // profile being used for current animation. + uint16_t duration; // time (tenths of a second) for animation to complete. + }; // 14 bytes per element, i.e. per pin in use + + struct ServoData *_servoData [16]; + + static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off + static const uint8_t FLASH _bounceProfile[30]; + + const unsigned int refreshInterval = 50; // refresh every 50ms + +// Profile for a bouncing signal or turnout +// The profile below is in the range 0-100% and should be combined with the desired limits +// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, +// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. +const byte FLASH EXIOExpander::_bounceProfile[30] = + {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; + + // EX-IOExpander protocol flags enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup From f59fe6e83bcd8684832117cc05c4d885d937ef9f Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 07:38:00 +1000 Subject: [PATCH 42/52] Some success --- IO_EXIOExpander.h | 86 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index f29dad3..84beb4b 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -182,10 +182,14 @@ private: // Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads) void _loop(unsigned long currentMicros) override { (void)currentMicros; // remove warning + if (_deviceState == DEVSTATE_FAILED) return; _command1Buffer[0] = EXIORDD; I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); + for (int pin=0; pin<_nPins; pin++) { + updatePosition(pin); + } } // Obtain the correct analogue input value @@ -203,20 +207,43 @@ private: // Obtain the correct digital input value int _read(VPIN vpin) override { + if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; - uint8_t pinByte = pin / 8; - bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); - return value; + if (_servoData[pin] == NULL) { + uint8_t pinByte = pin / 8; + bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); + return value; + } else { + struct ServoData *s = _servoData[pin]; + if (s == NULL) { + return false; // No structure means no animation! + } else { + return (s->stepNumber < s->numSteps); + } + } } void _write(VPIN vpin, int value) override { + if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; - _digitalOutBuffer[0] = EXIOWRD; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = value; - I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); - if (_command1Buffer[0] != EXIORDY) { - DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + if (_servoData[pin] == NULL) { + _digitalOutBuffer[0] = EXIOWRD; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = value; + I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); + if (_command1Buffer[0] != EXIORDY) { + DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + } + } else { + if (value) value = 1; + struct ServoData *s = _servoData[pin]; + if (s != NULL) { + // Use configured parameters + this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); + } else { + /* simulate digital pin on PWM */ + this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); + } } } @@ -230,12 +257,11 @@ private: _command4Buffer[3] = value >> 8; I2CManager.write(_i2cAddress, _command4Buffer, 4); */ - #ifdef DIAG_IO +#ifdef DIAG_IO DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); - #endif +#endif if (_deviceState == DEVSTATE_FAILED) return; - int pin = vpin - _firstVpin; if (value > 4095) value = 4095; else if (value < 0) value = 0; @@ -286,22 +312,30 @@ private: s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); } // Send servo command - // _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); + this->writePWM(pin, s->currentPosition); } else if (s->stepNumber < s->numSteps + _catchupSteps) { // We've finished animation, wait a little to allow servo to catch up s->stepNumber++; } else if (s->stepNumber == s->numSteps + _catchupSteps && s->currentPosition != 0) { - #ifdef IO_SWITCH_OFF_SERVO - if ((s->currentProfile & NoPowerOff) == 0) { - // Wait has finished, so switch off PWM to prevent annoying servo buzz - // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); - } - #endif + // #ifdef IO_SWITCH_OFF_SERVO + // if ((s->currentProfile & NoPowerOff) == 0) { + // // Wait has finished, so switch off PWM to prevent annoying servo buzz + // // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); + // } + // #endif s->numSteps = 0; // Done now. } } + void writePWM(int pin, uint16_t value) { + _command4Buffer[0] = EXIOWRAN; + _command4Buffer[1] = pin; + _command4Buffer[2] = value & 0xFF; + _command4Buffer[3] = value >> 8; + I2CManager.write(_i2cAddress, _command4Buffer, 4); + } + void _display() override { DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, @@ -341,18 +375,18 @@ private: uint16_t duration; // time (tenths of a second) for animation to complete. }; // 14 bytes per element, i.e. per pin in use - struct ServoData *_servoData [16]; + struct ServoData *_servoData[256]; static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const uint8_t FLASH _bounceProfile[30]; + // static const uint8_t FLASH _bounceProfile[30]; const unsigned int refreshInterval = 50; // refresh every 50ms -// Profile for a bouncing signal or turnout -// The profile below is in the range 0-100% and should be combined with the desired limits -// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, -// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. -const byte FLASH EXIOExpander::_bounceProfile[30] = + // Profile for a bouncing signal or turnout + // The profile below is in the range 0-100% and should be combined with the desired limits + // of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, + // i.e. the bounce is the same on the down action as on the up action. First entry isn't used. + const byte FLASH _bounceProfile[30] = {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; // EX-IOExpander protocol flags From 06827a42b73db07fb66cb452b48813efd3f72d07 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 07:39:58 +1000 Subject: [PATCH 43/52] Remove excess drivers --- IO_PCA9685_basic.h | 149 ------------------------ IO_Servo.h | 277 --------------------------------------------- 2 files changed, 426 deletions(-) delete mode 100644 IO_PCA9685_basic.h delete mode 100644 IO_Servo.h diff --git a/IO_PCA9685_basic.h b/IO_PCA9685_basic.h deleted file mode 100644 index 4f809aa..0000000 --- a/IO_PCA9685_basic.h +++ /dev/null @@ -1,149 +0,0 @@ -/* - * © 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 . - */ - -/* - * 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%). - */ - -#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 PCA9685_basic : public IODevice { -public: - // Create device driver instance. - static void create(VPIN firstVpin, int nPins, uint8_t I2CAddress) { - if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new PCA9685_basic(firstVpin, nPins, I2CAddress); - } - -private: - - // structures for setting up non-blocking writes to servo controller - I2CRB requestBlock; - uint8_t outputBuffer[5]; - - // REGISTER ADDRESSES - const byte PCA9685_MODE1=0x00; // Mode Register - const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/ - const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */ - // MODE1 bits - const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */ - const byte MODE1_AI=0x20; /**< Auto-Increment enabled */ - const byte MODE1_RESTART=0x80; /**< Restart enabled */ - - const float FREQUENCY_OSCILLATOR=25000000.0; /** 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 - PCA9685_basic(VPIN firstVpin, int nPins, uint8_t I2CAddress) { - _firstVpin = firstVpin; - _nPins = min(nPins, 16); - _I2CAddress = I2CAddress; - 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, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period. - 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 profile, uint16_t duration) override { - #ifdef DIAG_IO - DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), - vpin, value, profile, duration, _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("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (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("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value); - #endif - // Wait for previous request to complete - uint8_t status = requestBlock.wait(); - if (status != I2C_STATUS_OK) { - _deviceState = DEVSTATE_FAILED; - DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, 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(byte address, byte reg, byte value) { - I2CManager.write(address, 2, reg, value); - } - -}; - -#endif \ No newline at end of file diff --git a/IO_Servo.h b/IO_Servo.h deleted file mode 100644 index b1935b6..0000000 --- a/IO_Servo.h +++ /dev/null @@ -1,277 +0,0 @@ -/* - * © 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 . - */ -#ifndef IO_SERVO_H - -#include "IODevice.h" -#include "I2CManager.h" -#include "DIAG.h" - -class Servo : IODevice { - -public: - enum ProfileType : uint8_t { - Instant = 0, // Moves immediately between positions (if duration not specified) - UseDuration = 0, // Use specified duration - Fast = 1, // Takes around 500ms end-to-end - Medium = 2, // 1 second end-to-end - Slow = 3, // 2 seconds end-to-end - Bounce = 4, // For semaphores/turnouts with a bit of bounce!! - NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move. - }; - - // Create device driver instance. - static void create(VPIN firstVpin, int nPins, VPIN firstSlavePin) { - if (checkNoOverlap(firstVpin, nPins)) new Servo(firstVpin, nPins, firstSlavePin); - } - -private: - VPIN _firstSlavePin; - IODevice *_slaveDevice = NULL; - - struct ServoData { - uint16_t activePosition : 12; // Config parameter - uint16_t inactivePosition : 12; // Config parameter - uint16_t currentPosition : 12; - uint16_t fromPosition : 12; - uint16_t toPosition : 12; - uint8_t profile; // Config parameter - uint16_t stepNumber; // Index of current step (starting from 0) - uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. - uint8_t currentProfile; // profile being used for current animation. - uint16_t duration; // time (tenths of a second) for animation to complete. - }; // 14 bytes per element, i.e. per pin in use - - struct ServoData *_servoData [16]; - - static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - static const uint8_t FLASH _bounceProfile[30]; - - const unsigned int refreshInterval = 50; // refresh every 50ms - - - // Configure a port on the Servo. - bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { - if (configType != CONFIGURE_SERVO) return false; - if (paramCount != 5) return false; - #ifdef DIAG_IO - DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), - vpin, params[0], params[1], params[2], params[3], params[4]); - #endif - - int8_t pin = vpin - _firstVpin; - VPIN slavePin = vpin - _firstVpin + _firstSlavePin; - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); - s = _servoData[pin]; - if (!s) return false; // Check for failed memory allocation - } - - s->activePosition = params[0]; - s->inactivePosition = params[1]; - s->profile = params[2]; - s->duration = params[3]; - int state = params[4]; - - if (state != -1) { - // Position servo to initial state - IODevice::writeAnalogue(slavePin, state ? s->activePosition : s->inactivePosition, 0, 0); - } - return true; - } - - // Constructor - Servo(VPIN firstVpin, int nPins, VPIN firstSlavePin) { - _firstVpin = firstVpin; - _nPins = (nPins > 16) ? 16 : nPins; - _firstSlavePin = firstSlavePin; - - // To save RAM, space for servo configuration is not allocated unless a pin is used. - // Initialise the pointers to NULL. - for (int i=0; i<_nPins; i++) - _servoData[i] = NULL; - - addDevice(this); - } - - // Device-specific initialisation - void _begin() override { - // Get reference to slave device to make accesses faster. - _slaveDevice = this->findDevice(_firstSlavePin); - // Check firstSlavePin is actually allocated to a device - if (!_slaveDevice) { - DIAG(F("Servo: Slave device not found on pins %d-%d"), - _firstSlavePin, _firstSlavePin+_nPins-1); - _deviceState = DEVSTATE_FAILED; - } - // Check that the last slave pin is allocated to the same device. - if (_slaveDevice != this->findDevice(_firstSlavePin+_nPins-1)) { - DIAG(F("Servo: Slave device does not cover all pins %d-%d"), - _firstSlavePin, _firstSlavePin+_nPins-1); - _deviceState = DEVSTATE_FAILED; - } - #if defined(DIAG_IO) - _display(); - #endif - } - - // Device-specific write function, invoked from IODevice::write(). - // For this function, the configured profile is used. - void _write(VPIN vpin, int value) override { - if (_deviceState == DEVSTATE_FAILED) return; - #ifdef DIAG_IO - DIAG(F("Servo Write Vpin:%d Value:%d"), vpin, value); - #endif - int pin = vpin - _firstVpin; - // VPIN slavePin = vpin - _firstVpin + _firstSlavePin; - if (value) value = 1; - - struct ServoData *s = _servoData[pin]; - if (s != NULL) { - // Use configured parameters - this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); - } else { - /* simulate digital pin on PWM */ - this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); - } - } - - // Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue(). - // Profile is as follows: - // Bit 7: 0=Set PWM to 0% to power off servo motor when finished - // 1=Keep PWM pulses on (better when using PWM to drive an LED) - // Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds) - // 1 (Fast) Move servo in 0.5 seconds - // 2 (Medium) Move servo in 1.0 seconds - // 3 (Slow) Move servo in 2.0 seconds - // 4 (Bounce) Servo 'bounces' at extremes. - // - void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { - #ifdef DIAG_IO - DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), - vpin, value, profile, duration, _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; - - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - // Servo pin not configured, so configure now using defaults - s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); - if (s == NULL) return; // Check for memory allocation failure - s->activePosition = 4095; - s->inactivePosition = 0; - s->currentPosition = value; - s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) - } - - // Animated profile. Initiate the appropriate action. - s->currentProfile = profile; - uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. - s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds - profileValue==Medium ? 20 : // 1.0 seconds - profileValue==Slow ? 40 : // 2.0 seconds - profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds - duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) - s->stepNumber = 0; - s->toPosition = value; - s->fromPosition = s->currentPosition; - } - - // _read returns true if the device is currently in executing an animation, - // changing the output over a period of time. - int _read(VPIN vpin) override { - if (_deviceState == DEVSTATE_FAILED) return 0; - int pin = vpin - _firstVpin; - struct ServoData *s = _servoData[pin]; - if (s == NULL) - return false; // No structure means no animation! - else - return (s->stepNumber < s->numSteps); - } - - void _loop(unsigned long currentMicros) override { - if (_deviceState == DEVSTATE_FAILED) return; - for (int pin=0; pin<_nPins; pin++) { - updatePosition(pin); - } - delayUntil(currentMicros + refreshInterval * 1000UL); - } - - // Private function to reposition servo - // TODO: Could calculate step number from elapsed time, to allow for erratic loop timing. - void updatePosition(uint8_t pin) { - struct ServoData *s = _servoData[pin]; - if (s == NULL) return; // No pin configuration/state data - - if (s->numSteps == 0) return; // No animation in progress - - if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { - // Go straight to end of sequence, output final position. - s->stepNumber = s->numSteps-1; - } - - if (s->stepNumber < s->numSteps) { - // Animation in progress, reposition servo - s->stepNumber++; - if ((s->currentProfile & ~NoPowerOff) == Bounce) { - // Retrieve step positions from array in flash - uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); - s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); - } else { - // All other profiles - calculate step by linear interpolation between from and to positions. - s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); - } - // Send servo command - _slaveDevice->writeAnalogue(_firstSlavePin+pin, s->currentPosition); - } else if (s->stepNumber < s->numSteps + _catchupSteps) { - // We've finished animation, wait a little to allow servo to catch up - s->stepNumber++; - } else if (s->stepNumber == s->numSteps + _catchupSteps - && s->currentPosition != 0) { - #ifdef IO_SWITCH_OFF_SERVO - if ((s->currentProfile & NoPowerOff) == 0) { - // Wait has finished, so switch off PWM to prevent annoying servo buzz - _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); - } - #endif - s->numSteps = 0; // Done now. - } - } - - // Display details of this device. - void _display() override { - DIAG(F("Servo Configured on Vpins:%d-%d, slave pins:%d-%d %S"), - (int)_firstVpin, (int)_firstVpin+_nPins-1, - (int)_firstSlavePin, (int)_firstSlavePin+_nPins-1, - (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); - } -}; - -// Profile for a bouncing signal or turnout -// The profile below is in the range 0-100% and should be combined with the desired limits -// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, -// i.e. the bounce is the same on the down action as on the up action. First entry isn't used. -const byte FLASH Servo::_bounceProfile[30] = - {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; - - -#endif \ No newline at end of file From 6874ddca9bfc1cd704dd508de36f6128b11e10a0 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 08:41:50 +1000 Subject: [PATCH 44/52] Servo functional --- IO_EXIOExpander.h | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 84beb4b..aedb1ce 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -187,8 +187,13 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); - for (int pin=0; pin<_nPins; pin++) { - updatePosition(pin); + if ((currentMicros - _lastRefresh) / 1000UL > refreshInterval) { + _lastRefresh = currentMicros; + for (int pin=0; pin<_nPins; pin++) { + if (_servoData[pin] != NULL) { + updatePosition(pin); + } + } } } @@ -247,16 +252,8 @@ private: } } - // void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override { void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { int pin = vpin - _firstVpin; - /* Initial _writeAnalogue here - _command4Buffer[0] = EXIOWRAN; - _command4Buffer[1] = pin; - _command4Buffer[2] = value & 0xFF; - _command4Buffer[3] = value >> 8; - I2CManager.write(_i2cAddress, _command4Buffer, 4); - */ #ifdef DIAG_IO DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); @@ -318,12 +315,6 @@ private: s->stepNumber++; } else if (s->stepNumber == s->numSteps + _catchupSteps && s->currentPosition != 0) { - // #ifdef IO_SWITCH_OFF_SERVO - // if ((s->currentProfile & NoPowerOff) == 0) { - // // Wait has finished, so switch off PWM to prevent annoying servo buzz - // // _slaveDevice->writeAnalogue(_firstSlavePin+pin, 0); - // } - // #endif s->numSteps = 0; // Done now. } } @@ -378,9 +369,9 @@ private: struct ServoData *_servoData[256]; static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - // static const uint8_t FLASH _bounceProfile[30]; - + const unsigned int refreshInterval = 50; // refresh every 50ms + unsigned long _lastRefresh = 0; // Profile for a bouncing signal or turnout // The profile below is in the range 0-100% and should be combined with the desired limits From 6b67760db18fa9d9402f7590d824eb16ef1ba8da Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 09:31:09 +1000 Subject: [PATCH 45/52] Fix dynamic RAM allocation --- IO_EXIOExpander.h | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index aedb1ce..d6e0dde 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -72,6 +72,7 @@ private: _i2cAddress = i2cAddress; // To save RAM, space for servo configuration is not allocated unless a pin is used. // Initialise the pointers to NULL. + _servoData = (ServoData**) calloc(_nPins, sizeof(ServoData*)); for (int i=0; i<_nPins; i++) { _servoData[i] = NULL; } @@ -137,28 +138,6 @@ private: DIAG(F("Vpin %d cannot be used as a digital input pin"), (int)vpin); return false; } - } else if (configType == CONFIGURE_SERVO) { - if (paramCount != 5) return false; -#ifdef DIAG_IO - DIAG(F("Servo: Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"), - vpin, params[0], params[1], params[2], params[3], params[4]); -#endif - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - _servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData)); - s = _servoData[pin]; - if (!s) return false; // Check for failed memory allocation - } - s->activePosition = params[0]; - s->inactivePosition = params[1]; - s->profile = params[2]; - s->duration = params[3]; - int state = params[4]; - if (state != -1) { - // Position servo to initial state - IODevice::writeAnalogue(pin, state ? s->activePosition : s->inactivePosition, 0, 0); - } - return true; } else { return false; } @@ -366,7 +345,8 @@ private: uint16_t duration; // time (tenths of a second) for animation to complete. }; // 14 bytes per element, i.e. per pin in use - struct ServoData *_servoData[256]; + // struct ServoData *_servoData[256]; + ServoData** _servoData; static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off From d36ac7dcfd7d60ba6de4a334d6b1697eea2f8f01 Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 12:45:34 +1000 Subject: [PATCH 46/52] Revert IODevice.h change --- IODevice.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/IODevice.h b/IODevice.h index e7906c6..72beb9e 100644 --- a/IODevice.h +++ b/IODevice.h @@ -242,12 +242,11 @@ protected: // Current state of device DeviceStateEnum _deviceState = DEVSTATE_DORMANT; - // Method to find device handling Vpin - static IODevice *findDevice(VPIN vpin); - private: // Method to check whether the vpin corresponds to this device bool owns(VPIN vpin); + // Method to find device handling Vpin + static IODevice *findDevice(VPIN vpin); IODevice *_nextDevice = 0; unsigned long _nextEntryTime; static IODevice *_firstDevice; From f1c17c360620ae0aa2e0546df7c865220c4fdc1c Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 13:03:00 +1000 Subject: [PATCH 47/52] Add more state checking --- IO_EXIOExpander.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index d6e0dde..ad48a3a 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -178,6 +178,7 @@ private: // Obtain the correct analogue input value int _readAnalogue(VPIN vpin) override { + if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; uint8_t _pinLSBByte; for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) { @@ -232,6 +233,7 @@ private: } void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { + if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; #ifdef DIAG_IO DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), @@ -345,7 +347,6 @@ private: uint16_t duration; // time (tenths of a second) for animation to complete. }; // 14 bytes per element, i.e. per pin in use - // struct ServoData *_servoData[256]; ServoData** _servoData; static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off From acadf241e6838bc2517c3af9d94dd83f47e9a05b Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 13:15:04 +1000 Subject: [PATCH 48/52] Update version --- version.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/version.h b/version.h index d95e01d..abfa9df 100644 --- a/version.h +++ b/version.h @@ -5,8 +5,7 @@ #define VERSION "4.2.15" -// 4.2.15 Add Servo device driver with PCA9685_basic driver -// Add basic experimental PWM support to EX-IOExpander +// 4.2.15 Add basic experimental PWM support to EX-IOExpander // EX-IOExpander 0.0.14 minimum required // 4.2.14 STM32F4xx fast ADC read implementation // 4.2.13 Broadcast power for again From d8d785877e4f4aec334ab013cd5711445b77ad6e Mon Sep 17 00:00:00 2001 From: peteGSX <97784652+peteGSX@users.noreply.github.com> Date: Thu, 9 Feb 2023 13:38:06 +1000 Subject: [PATCH 49/52] Fix myHal example for EX-IOExpander --- myHal.cpp_example.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/myHal.cpp_example.txt b/myHal.cpp_example.txt index 64adac1..d7afe15 100644 --- a/myHal.cpp_example.txt +++ b/myHal.cpp_example.txt @@ -179,19 +179,17 @@ void halSetup() { //======================================================================= // The following directive defines an EX-IOExpander instance. //======================================================================= - // EXIOExpander::create(VPIN, Number of VPINs, I2C Address, Digital pin count, Analogue pin count) + // EXIOExpander::create(VPIN, Number of VPINs, I2C Address) // // The parameters are: // VPIN=an available Vpin - // Number of VPINs=Digital pin count + Analogue pin count (must match device in use as per documentation) + // Number of VPINs=pin count (must match device in use as per documentation) // I2C address=an available I2C address (default 0x65) // // Note that the I2C address is defined in the EX-IOExpander code, and 0x65 is the default. - // The first example is for an Arduino Nano with the default pin allocations. - // The second example is for an Arduino Uno using all pins as digital only. + // The example is for an Arduino Nano. - //EXIOExpander::create(800, 18, 0x65, 12, 6); - //EXIOExpander::create(820, 16, 0x66, 16, 0); + //EXIOExpander::create(800, 18, 0x65); //======================================================================= From 47cda832102068236f9d9d0fd22d16bc245ca99b Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 12 Feb 2023 10:36:26 +1000 Subject: [PATCH 50/52] Disabled servo animations --- IO_EXIOExpander.h | 93 +++++++++++++++++++++++++++++++---------------- version.h | 3 +- 2 files changed, 63 insertions(+), 33 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index ad48a3a..289f282 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -72,10 +72,10 @@ private: _i2cAddress = i2cAddress; // To save RAM, space for servo configuration is not allocated unless a pin is used. // Initialise the pointers to NULL. - _servoData = (ServoData**) calloc(_nPins, sizeof(ServoData*)); - for (int i=0; i<_nPins; i++) { - _servoData[i] = NULL; - } + // _servoData = (ServoData**) calloc(_nPins, sizeof(ServoData*)); + // for (int i=0; i<_nPins; i++) { + // _servoData[i] = NULL; + // } addDevice(this); } @@ -166,14 +166,14 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); - if ((currentMicros - _lastRefresh) / 1000UL > refreshInterval) { - _lastRefresh = currentMicros; - for (int pin=0; pin<_nPins; pin++) { - if (_servoData[pin] != NULL) { - updatePosition(pin); - } - } - } + // if ((currentMicros - _lastRefresh) / 1000UL > refreshInterval) { + // _lastRefresh = currentMicros; + // for (int pin=0; pin<_nPins; pin++) { + // if (_servoData[pin] != NULL) { + // updatePosition(pin); + // } + // } + // } } // Obtain the correct analogue input value @@ -194,24 +194,24 @@ private: int _read(VPIN vpin) override { if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; - if (_servoData[pin] == NULL) { + // if (_servoData[pin] == NULL) { uint8_t pinByte = pin / 8; bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); return value; - } else { - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - return false; // No structure means no animation! - } else { - return (s->stepNumber < s->numSteps); - } - } + // } else { + // struct ServoData *s = _servoData[pin]; + // if (s == NULL) { + // return false; // No structure means no animation! + // } else { + // return (s->stepNumber < s->numSteps); + // } + // } } void _write(VPIN vpin, int value) override { if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; - if (_servoData[pin] == NULL) { + // if (_servoData[pin] == NULL) { _digitalOutBuffer[0] = EXIOWRD; _digitalOutBuffer[1] = pin; _digitalOutBuffer[2] = value; @@ -219,19 +219,40 @@ private: if (_command1Buffer[0] != EXIORDY) { DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); } - } else { - if (value) value = 1; - struct ServoData *s = _servoData[pin]; - if (s != NULL) { - // Use configured parameters - this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); - } else { - /* simulate digital pin on PWM */ - this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); - } + // } else { + // if (value) value = 1; + // struct ServoData *s = _servoData[pin]; + // if (s != NULL) { + // // Use configured parameters + // this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); + // } else { + // /* simulate digital pin on PWM */ + // this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); + // } + // } + } + + void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { + if (_deviceState == DEVSTATE_FAILED) return; + int pin = vpin - _firstVpin; +#ifdef DIAG_IO + DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), + vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); +#endif + _servoBuffer[0] = EXIOWRAN; + _servoBuffer[1] = pin; + _servoBuffer[2] = value & 0xFF; + _servoBuffer[3] = value >> 8; + _servoBuffer[4] = profile; + _servoBuffer[5] = duration & 0xFF; + _servoBuffer[6] = duration >> 8; + I2CManager.read(_i2cAddress, _command1Buffer, 1, _servoBuffer, 7); + if (_command1Buffer[0] != EXIORDY) { + DIAG(F("Vpin %d cannot be used as a servo/PWM pin"), (int)vpin); } } +/* void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; @@ -266,7 +287,9 @@ private: s->toPosition = value; s->fromPosition = s->currentPosition; } +*/ +/* void updatePosition(uint8_t pin) { struct ServoData *s = _servoData[pin]; if (s == NULL) return; // No pin configuration/state data @@ -299,7 +322,9 @@ private: s->numSteps = 0; // Done now. } } +*/ +/* void writePWM(int pin, uint16_t value) { _command4Buffer[0] = EXIOWRAN; _command4Buffer[1] = pin; @@ -307,6 +332,7 @@ private: _command4Buffer[3] = value >> 8; I2CManager.write(_i2cAddress, _command4Buffer, 4); } +*/ void _display() override { DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), @@ -331,8 +357,10 @@ private: byte _command2Buffer[2]; byte _command4Buffer[4]; byte _receive3Buffer[3]; + byte _servoBuffer[7]; uint8_t* _analoguePinMap; +/* // Servo specific struct ServoData { uint16_t activePosition : 12; // Config parameter @@ -360,6 +388,7 @@ private: // i.e. the bounce is the same on the down action as on the up action. First entry isn't used. const byte FLASH _bounceProfile[30] = {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; +*/ // EX-IOExpander protocol flags enum { diff --git a/version.h b/version.h index abfa9df..3a0fa9e 100644 --- a/version.h +++ b/version.h @@ -4,7 +4,8 @@ #include "StringFormatter.h" -#define VERSION "4.2.15" +#define VERSION "4.2.16" +// 4.2.16 Move EX-IOExpander servo support to the EX-IOExpander software // 4.2.15 Add basic experimental PWM support to EX-IOExpander // EX-IOExpander 0.0.14 minimum required // 4.2.14 STM32F4xx fast ADC read implementation From 9c95eb69054718e04b34ba38facd6a4e560dc8a0 Mon Sep 17 00:00:00 2001 From: peteGSX Date: Sun, 12 Feb 2023 19:06:46 +1000 Subject: [PATCH 51/52] Servo animation moved to EX-IO --- IO_EXIOExpander.h | 167 +++------------------------------------------- 1 file changed, 10 insertions(+), 157 deletions(-) diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index 289f282..a782942 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -70,12 +70,6 @@ private: _firstVpin = firstVpin; _nPins = nPins; _i2cAddress = i2cAddress; - // To save RAM, space for servo configuration is not allocated unless a pin is used. - // Initialise the pointers to NULL. - // _servoData = (ServoData**) calloc(_nPins, sizeof(ServoData*)); - // for (int i=0; i<_nPins; i++) { - // _servoData[i] = NULL; - // } addDevice(this); } @@ -166,14 +160,6 @@ private: I2CManager.read(_i2cAddress, _digitalInputStates, _digitalPinBytes, _command1Buffer, 1); _command1Buffer[0] = EXIORDAN; I2CManager.read(_i2cAddress, _analogueInputStates, _analoguePinBytes, _command1Buffer, 1); - // if ((currentMicros - _lastRefresh) / 1000UL > refreshInterval) { - // _lastRefresh = currentMicros; - // for (int pin=0; pin<_nPins; pin++) { - // if (_servoData[pin] != NULL) { - // updatePosition(pin); - // } - // } - // } } // Obtain the correct analogue input value @@ -194,42 +180,21 @@ private: int _read(VPIN vpin) override { if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; - // if (_servoData[pin] == NULL) { - uint8_t pinByte = pin / 8; - bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); - return value; - // } else { - // struct ServoData *s = _servoData[pin]; - // if (s == NULL) { - // return false; // No structure means no animation! - // } else { - // return (s->stepNumber < s->numSteps); - // } - // } + uint8_t pinByte = pin / 8; + bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8); + return value; } void _write(VPIN vpin, int value) override { if (_deviceState == DEVSTATE_FAILED) return; int pin = vpin - _firstVpin; - // if (_servoData[pin] == NULL) { - _digitalOutBuffer[0] = EXIOWRD; - _digitalOutBuffer[1] = pin; - _digitalOutBuffer[2] = value; - I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); - if (_command1Buffer[0] != EXIORDY) { - DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); - } - // } else { - // if (value) value = 1; - // struct ServoData *s = _servoData[pin]; - // if (s != NULL) { - // // Use configured parameters - // this->_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration); - // } else { - // /* simulate digital pin on PWM */ - // this->_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0); - // } - // } + _digitalOutBuffer[0] = EXIOWRD; + _digitalOutBuffer[1] = pin; + _digitalOutBuffer[2] = value; + I2CManager.read(_i2cAddress, _command1Buffer, 1, _digitalOutBuffer, 3); + if (_command1Buffer[0] != EXIORDY) { + DIAG(F("Vpin %d cannot be used as a digital output pin"), (int)vpin); + } } void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { @@ -252,88 +217,6 @@ private: } } -/* - void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override { - if (_deviceState == DEVSTATE_FAILED) return; - int pin = vpin - _firstVpin; -#ifdef DIAG_IO - DIAG(F("Servo: WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"), - vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F("")); -#endif - if (_deviceState == DEVSTATE_FAILED) return; - if (value > 4095) value = 4095; - else if (value < 0) value = 0; - - struct ServoData *s = _servoData[pin]; - if (s == NULL) { - // Servo pin not configured, so configure now using defaults - s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1); - if (s == NULL) return; // Check for memory allocation failure - s->activePosition = 4095; - s->inactivePosition = 0; - s->currentPosition = value; - s->profile = Instant | NoPowerOff; // Use instant profile (but not this time) - } - - // Animated profile. Initiate the appropriate action. - s->currentProfile = profile; - uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit. - s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds - profileValue==Medium ? 20 : // 1.0 seconds - profileValue==Slow ? 40 : // 2.0 seconds - profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds - duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms) - s->stepNumber = 0; - s->toPosition = value; - s->fromPosition = s->currentPosition; - } -*/ - -/* - void updatePosition(uint8_t pin) { - struct ServoData *s = _servoData[pin]; - if (s == NULL) return; // No pin configuration/state data - - if (s->numSteps == 0) return; // No animation in progress - - if (s->stepNumber == 0 && s->fromPosition == s->toPosition) { - // Go straight to end of sequence, output final position. - s->stepNumber = s->numSteps-1; - } - - if (s->stepNumber < s->numSteps) { - // Animation in progress, reposition servo - s->stepNumber++; - if ((s->currentProfile & ~NoPowerOff) == Bounce) { - // Retrieve step positions from array in flash - uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]); - s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition); - } else { - // All other profiles - calculate step by linear interpolation between from and to positions. - s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition); - } - // Send servo command - this->writePWM(pin, s->currentPosition); - } else if (s->stepNumber < s->numSteps + _catchupSteps) { - // We've finished animation, wait a little to allow servo to catch up - s->stepNumber++; - } else if (s->stepNumber == s->numSteps + _catchupSteps - && s->currentPosition != 0) { - s->numSteps = 0; // Done now. - } - } -*/ - -/* - void writePWM(int pin, uint16_t value) { - _command4Buffer[0] = EXIOWRAN; - _command4Buffer[1] = pin; - _command4Buffer[2] = value & 0xFF; - _command4Buffer[3] = value >> 8; - I2CManager.write(_i2cAddress, _command4Buffer, 4); - } -*/ - void _display() override { DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"), _i2cAddress, _majorVer, _minorVer, _patchVer, @@ -360,36 +243,6 @@ private: byte _servoBuffer[7]; uint8_t* _analoguePinMap; -/* - // Servo specific - struct ServoData { - uint16_t activePosition : 12; // Config parameter - uint16_t inactivePosition : 12; // Config parameter - uint16_t currentPosition : 12; - uint16_t fromPosition : 12; - uint16_t toPosition : 12; - uint8_t profile; // Config parameter - uint16_t stepNumber; // Index of current step (starting from 0) - uint16_t numSteps; // Number of steps in animation, or 0 if none in progress. - uint8_t currentProfile; // profile being used for current animation. - uint16_t duration; // time (tenths of a second) for animation to complete. - }; // 14 bytes per element, i.e. per pin in use - - ServoData** _servoData; - - static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off - - const unsigned int refreshInterval = 50; // refresh every 50ms - unsigned long _lastRefresh = 0; - - // Profile for a bouncing signal or turnout - // The profile below is in the range 0-100% and should be combined with the desired limits - // of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here, - // i.e. the bounce is the same on the down action as on the up action. First entry isn't used. - const byte FLASH _bounceProfile[30] = - {0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100}; -*/ - // EX-IOExpander protocol flags enum { EXIOINIT = 0xE0, // Flag to initialise setup procedure From 2ada89f9180b750a0a15c7df9a9a63c9112d005c Mon Sep 17 00:00:00 2001 From: Harald Barth Date: Sun, 12 Feb 2023 20:35:57 +0100 Subject: [PATCH 52/52] LCN bugfix --- GITHUB_SHA.h | 2 +- LCN.cpp | 2 +- version.h | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/GITHUB_SHA.h b/GITHUB_SHA.h index 919895a..98ac27a 100644 --- a/GITHUB_SHA.h +++ b/GITHUB_SHA.h @@ -1 +1 @@ -#define GITHUB_SHA "devel-202301290750Z" +#define GITHUB_SHA "devel-202302121935Z" diff --git a/LCN.cpp b/LCN.cpp index efb49ff..d1e1228 100644 --- a/LCN.cpp +++ b/LCN.cpp @@ -43,7 +43,7 @@ void LCN::loop() { while (stream->available()) { int ch = stream->read(); - if (ch >= 0 && ch <= '9') { // accumulate id value + if (ch >= '0' && ch <= '9') { // accumulate id value id = 10 * id + ch - '0'; } else if (ch == 't' || ch == 'T') { // Turnout opcodes diff --git a/version.h b/version.h index 3a0fa9e..21c7ad2 100644 --- a/version.h +++ b/version.h @@ -4,7 +4,8 @@ #include "StringFormatter.h" -#define VERSION "4.2.16" +#define VERSION "4.2.17" +// 4.2.17 LCN bugfix // 4.2.16 Move EX-IOExpander servo support to the EX-IOExpander software // 4.2.15 Add basic experimental PWM support to EX-IOExpander // EX-IOExpander 0.0.14 minimum required