diff --git a/DCC.cpp b/DCC.cpp index 2268278..342c287 100644 --- a/DCC.cpp +++ b/DCC.cpp @@ -155,7 +155,7 @@ uint8_t DCC::getThrottleSpeed(int cab) { bool DCC::getThrottleDirection(int cab) { int reg=lookupSpeedTable(cab); - if (reg<0) return false ; + if (reg<0) return true; return (speedTable[reg].speedCode & 0x80) !=0; } @@ -371,8 +371,9 @@ const ackOp FLASH WRITE_BYTE_PROG[] = { const ackOp FLASH VERIFY_BYTE_PROG[] = { BASELINE, + BIV, // ackManagerByte initial value VB,WACK, // validate byte - ITCB, // if ok callback value + ITCB, // if ok callback value STARTMERGE, //clear bit and byte values ready for merge pass // each bit is validated against 0 and the result inverted in MERGE // this is because there tend to be more zeros in cv values than ones. @@ -390,7 +391,7 @@ const ackOp FLASH VERIFY_BYTE_PROG[] = { V0, WACK, MERGE, V0, WACK, MERGE, V0, WACK, MERGE, - VB, WACK, ITCB, // verify merged byte and return it if acked ok + VB, WACK, ITCBV, // verify merged byte and return it if acked ok - with retry report FAIL }; @@ -702,11 +703,13 @@ int DCC::nextLoco = 0; ackOp const * DCC::ackManagerProg; ackOp const * DCC::ackManagerProgStart; byte DCC::ackManagerByte; +byte DCC::ackManagerByteVerify; byte DCC::ackManagerStash; int DCC::ackManagerWord; byte DCC::ackManagerRetry; byte DCC::ackRetry = 2; int16_t DCC::ackRetrySum; +int16_t DCC::ackRetryPSum; int DCC::ackManagerCv; byte DCC::ackManagerBitNum; bool DCC::ackReceived; @@ -742,6 +745,7 @@ void DCC::ackManagerSetup(int cv, byte byteValueOrBitnum, ackOp const program[] ackManagerProgStart = program; ackManagerRetry = ackRetry; ackManagerByte = byteValueOrBitnum; + ackManagerByteVerify = byteValueOrBitnum; ackManagerBitNum=byteValueOrBitnum; ackManagerCallback = callback; } @@ -767,6 +771,7 @@ void DCC::ackManagerLoop() { // (typically waiting for a reset counter or ACK waiting, or when all finished.) switch (opcode) { case BASELINE: + if (DCCWaveform::progTrack.getPowerMode()==POWERMODE::OVERLOAD) return; if (checkResets(DCCWaveform::progTrack.autoPowerOff || ackManagerRejoin ? 20 : 3)) return; DCCWaveform::progTrack.setAckBaseline(); callbackState=READY; @@ -840,7 +845,18 @@ void DCC::ackManagerLoop() { return; } break; - + + case ITCBV: // If True callback(byte) - Verify + if (ackReceived) { + if (ackManagerByte == ackManagerByteVerify) { + ackRetrySum ++; + LCD(1, F("v %d %d Sum=%d"), ackManagerCv, ackManagerByte, ackRetrySum); + } + callback(ackManagerByte); + return; + } + break; + case ITCB7: // If True callback(byte & 0x7F) if (ackReceived) { callback(ackManagerByte & 0x7F); @@ -858,7 +874,11 @@ void DCC::ackManagerLoop() { case FAIL: // callback(-1) callback(-1); return; - + + case BIV: // ackManagerByte initial value + ackManagerByte = ackManagerByteVerify; + break; + case STARTMERGE: ackManagerBitNum=7; ackManagerByte=0; @@ -927,7 +947,7 @@ void DCC::callback(int value) { // check for automatic retry if (value == -1 && ackManagerRetry > 0) { ackRetrySum ++; - LCD(0, F("RETRY %d %d %d %d"), ackManagerCv, ackManagerRetry, ackRetry, ackRetrySum); + LCD(0, F("Retry %d %d Sum=%d"), ackManagerCv, ackManagerRetry, ackRetrySum); ackManagerRetry --; ackManagerProg = ackManagerProgStart; return; diff --git a/DCC.h b/DCC.h index cf1680f..8cb5d97 100644 --- a/DCC.h +++ b/DCC.h @@ -38,9 +38,11 @@ enum ackOp : byte ITC1, // If True Callback(1) (if prevous WACK got an ACK) ITC0, // If True callback(0); ITCB, // If True callback(byte) + ITCBV, // If True callback(byte) - end of Verify Byte ITCB7, // If True callback(byte &0x7F) NAKFAIL, // if false callback(-1) FAIL, // callback(-1) + BIV, // Set ackManagerByte to initial value for Verify retry STARTMERGE, // Clear bit and byte settings ready for merge pass MERGE, // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes) SETBIT, // sets bit number to next prog byte @@ -115,9 +117,11 @@ public: static inline void setGlobalSpeedsteps(byte s) { globalSpeedsteps = s; }; - static inline void setAckRetry(byte retry) { + static inline int16_t setAckRetry(byte retry) { ackRetry = retry; + ackRetryPSum = ackRetrySum; ackRetrySum = 0; // reset running total + return ackRetryPSum; }; private: @@ -149,11 +153,13 @@ private: static ackOp const *ackManagerProg; static ackOp const *ackManagerProgStart; static byte ackManagerByte; + static byte ackManagerByteVerify; static byte ackManagerBitNum; static int ackManagerCv; static byte ackManagerRetry; static byte ackRetry; static int16_t ackRetrySum; + static int16_t ackRetryPSum; static int ackManagerWord; static byte ackManagerStash; static bool ackReceived; diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index 91039f7..ed1f684 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -71,6 +71,8 @@ const int16_t HASH_KEYWORD_T=84; const int16_t HASH_KEYWORD_LCN = 15137; const int16_t HASH_KEYWORD_HAL = 10853; const int16_t HASH_KEYWORD_SHOW = -21309; +const int16_t HASH_KEYWORD_ANIN = -10424; +const int16_t HASH_KEYWORD_ANOUT = -26399; #ifdef HAS_ENOUGH_MEMORY const int16_t HASH_KEYWORD_WIFI = -5583; const int16_t HASH_KEYWORD_ETHERNET = -30767; @@ -736,15 +738,17 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[]) if (!VpinTurnout::create(p[0], p[2])) return false; } else if (params >= 3 && p[1] == HASH_KEYWORD_DCC) { - if (params==4 && p[2]>0 && p[2]<=512 && p[3]>=0 && p[3]<4) { // + // 0<=addr<=511, 0<=subadd<=3 (like command). + if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // if (!DCCTurnout::create(p[0], p[2], p[3])) return false; } else if (params==3 && p[2]>0 && p[2]<=512*4) { // , 1<=nn<=2048 + // Linearaddress 1 maps onto decoder address 1/0 (not 0/0!). if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false; } else return false; } else - if (params==3) { // legacy for DCC accessory - if (p[1]>0 && p[1]<=512 && p[2]>=0 && p[2]<4) { + if (params==3) { // legacy for DCC accessory + if (p[1]>=0 && p[1]<512 && p[2]>=0 && p[2]<4) { if (!DCCTurnout::create(p[0], p[1], p[2])) return false; } else return false; @@ -820,8 +824,7 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) LCD(0, F("Ack Max=%dus"), p[2]); // } else if (p[1] == HASH_KEYWORD_RETRY) { if (p[2] >255) p[2]=3; - DCC::setAckRetry(p[2]); - LCD(0, F("Ack Retry=%d"), p[2]); // + LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCC::setAckRetry(p[2])); // } } else { StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off")); @@ -878,9 +881,14 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[]) return true; case HASH_KEYWORD_SERVO: // + case HASH_KEYWORD_ANOUT: // IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0); break; + case HASH_KEYWORD_ANIN: // Display analogue input value + DIAG(F("VPIN=%d value=%d"), p[1], IODevice::readAnalogue(p[1])); + break; + #if !defined(IO_MINIMAL_HAL) case HASH_KEYWORD_HAL: if (p[1] == HASH_KEYWORD_SHOW) diff --git a/DCCWaveform.cpp b/DCCWaveform.cpp index 97ae944..f1b5571 100644 --- a/DCCWaveform.cpp +++ b/DCCWaveform.cpp @@ -116,6 +116,7 @@ void DCCWaveform::setPowerMode(POWERMODE mode) { powerMode = mode; bool ison = (mode == POWERMODE::ON); motorDriver->setPower( ison); + sentResetsSincePacket=0; } diff --git a/GITHUB_SHA.h b/GITHUB_SHA.h index 6ffec91..4338846 100644 --- a/GITHUB_SHA.h +++ b/GITHUB_SHA.h @@ -1 +1 @@ -#define GITHUB_SHA "50fcbc0" +#define GITHUB_SHA "37904b5" diff --git a/I2CManager.cpp b/I2CManager.cpp index 94c4baf..a3ea611 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -179,7 +179,7 @@ I2CManagerClass I2CManager = I2CManagerClass(); /*************************************************************************** * Block waiting for request block to complete, and return completion status. * Since such a loop could potentially last for ever if the RB status doesn't - * change, we set a high limit (0.1sec, 100ms) on the wait time and, if it + * change, we set a high limit (1sec, 1000ms) on the wait time and, if it * hasn't changed by that time we assume it's not going to, and just return * a timeout status. This means that CS will not lock up. ***************************************************************************/ @@ -187,8 +187,8 @@ uint8_t I2CRB::wait() { unsigned long waitStart = millis(); do { I2CManager.loop(); - // Rather than looping indefinitely, let's set a very high timeout (100ms). - if ((millis() - waitStart) > 100UL) { + // Rather than looping indefinitely, let's set a very high timeout (1s). + if ((millis() - waitStart) > 1000UL) { DIAG(F("I2C TIMEOUT I2C:x%x I2CRB:x%x"), i2cAddress, this); status = I2C_STATUS_TIMEOUT; // Note that, although the timeout is posted, the request may yet complete. diff --git a/I2CManager_AVR.h b/I2CManager_AVR.h index 310afa2..6492e00 100644 --- a/I2CManager_AVR.h +++ b/I2CManager_AVR.h @@ -62,7 +62,18 @@ * Set I2C clock speed register. ***************************************************************************/ void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) { - TWBR = ((F_CPU / i2cClockSpeed) - 16) / 2; + unsigned long temp = ((F_CPU / i2cClockSpeed) - 16) / 2; + for (uint8_t preScaler = 0; preScaler<=3; preScaler++) { + if (temp <= 255) { + TWBR = temp; + TWSR = (TWSR & 0xfc) | preScaler; + return; + } else + temp /= 4; + } + // Set slowest speed ~= 500 bits/sec + TWBR = 255; + TWSR |= 0x03; } /*************************************************************************** diff --git a/IODevice.cpp b/IODevice.cpp index e19ddef..1f9f53f 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -67,34 +67,54 @@ void IODevice::begin() { // doesn't need to invoke it. void IODevice::loop() { unsigned long currentMicros = micros(); - // Call every device's loop function in turn, one per entry. - if (!_nextLoopDevice) _nextLoopDevice = _firstDevice; - if (_nextLoopDevice) { - _nextLoopDevice->_loop(currentMicros); - _nextLoopDevice = _nextLoopDevice->_nextDevice; - } + + IODevice *lastLoopDevice = _nextLoopDevice; // So we know when to stop... + // Loop through devices until we find one ready to be serviced. + do { + if (!_nextLoopDevice) _nextLoopDevice = _firstDevice; + if (_nextLoopDevice) { + if (_nextLoopDevice->_deviceState != DEVSTATE_FAILED + && ((long)(currentMicros - _nextLoopDevice->_nextEntryTime)) >= 0) { + // Found one ready to run, so invoke its _loop method. + _nextLoopDevice->_nextEntryTime = currentMicros; + _nextLoopDevice->_loop(currentMicros); + _nextLoopDevice = _nextLoopDevice->_nextDevice; + break; + } + // Not this one, move to next one + _nextLoopDevice = _nextLoopDevice->_nextDevice; + } + } while (_nextLoopDevice != lastLoopDevice); // Stop looking when we've done all. // Report loop time if diags enabled #if defined(DIAG_LOOPTIMES) static unsigned long lastMicros = 0; - static unsigned long maxElapsed = 0; + // Measure time since loop() method started. + unsigned long halElapsed = micros() - currentMicros; + // Measure time between loop() method entries. + unsigned long elapsed = currentMicros - lastMicros; + static unsigned long maxElapsed = 0, maxHalElapsed = 0; static unsigned long lastOutputTime = 0; + static unsigned long halTotal = 0, total = 0; static unsigned long count = 0; const unsigned long interval = (unsigned long)5 * 1000 * 1000; // 5 seconds in microsec - unsigned long elapsed = currentMicros - lastMicros; + // Ignore long loop counts while message is still outputting if (currentMicros - lastOutputTime > 3000UL) { if (elapsed > maxElapsed) maxElapsed = elapsed; + if (halElapsed > maxHalElapsed) maxHalElapsed = halElapsed; + halTotal += halElapsed; + total += elapsed; + count++; } - count++; if (currentMicros - lastOutputTime > interval) { if (lastOutputTime > 0) - LCD(1,F("Loop=%lus,%lus max"), interval/count, maxElapsed); - maxElapsed = 0; - count = 0; + DIAG(F("Loop Total:%lus (%lus max) HAL:%lus (%lus max)"), + total/count, maxElapsed, halTotal/count, maxHalElapsed); + maxElapsed = maxHalElapsed = total = halTotal = count = 0; lastOutputTime = currentMicros; } - lastMicros = micros(); + lastMicros = currentMicros; #endif } @@ -114,12 +134,13 @@ bool IODevice::exists(VPIN vpin) { bool IODevice::hasCallback(VPIN vpin) { IODevice *dev = findDevice(vpin); if (!dev) return false; - return dev->_hasCallback(vpin); + return dev->_hasCallback; } // Display (to diagnostics) details of the device. void IODevice::_display() { - DIAG(F("Unknown device Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1); + DIAG(F("Unknown device Vpins:%d-%d %S"), + (int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState==DEVSTATE_FAILED ? F("OFFLINE") : F("")); } // Find device associated with nominated Vpin and pass configuration values on to it. @@ -139,30 +160,36 @@ void IODevice::write(VPIN vpin, int value) { return; } #ifdef DIAG_IO - //DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin); + DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin); #endif } -// Write analogue value to virtual pin(s). If multiple devices are allocated the same pin -// then only the first one found will be used. Duration is the time that the -// operation is to be performed over (e.g. as an animation) in deciseconds (0-3276 sec) -void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { +// Write analogue value to virtual pin(s). If multiple devices are allocated +// the same pin then only the first one found will be used. +// +// The significance of param1 and param2 may vary from device to device. +// For servo controllers, param1 is the profile of the transition and param2 +// the duration, i.e. the time that the operation is to be animated over +// in deciseconds (0-3276 sec) +// +void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) { IODevice *dev = findDevice(vpin); if (dev) { - dev->_writeAnalogue(vpin, value, profile, duration); + dev->_writeAnalogue(vpin, value, param1, param2); return; } #ifdef DIAG_IO - //DIAG(F("IODevice::writeAnalogue(): Vpin ID %d not found!"), (int)vpin); + DIAG(F("IODevice::writeAnalogue(): Vpin ID %d not found!"), (int)vpin); #endif } -// isBusy returns true if the device is currently in an animation of some sort, e.g. is changing -// the output over a period of time. +// isBusy, when called for a device pin is always a digital output or analogue output, +// returns input feedback state of the pin, i.e. whether the pin is busy performing +// an animation or fade over a period of time. bool IODevice::isBusy(VPIN vpin) { IODevice *dev = findDevice(vpin); if (dev) - return dev->_isBusy(vpin); + return dev->_read(vpin); else return false; } @@ -194,10 +221,12 @@ void IODevice::addDevice(IODevice *newDevice) { newDevice->_begin(); } -// Private helper function to locate a device by VPIN. Returns NULL if not found +// Private helper function to locate a device by VPIN. Returns NULL if not found. +// This is performance-critical, so minimises the calculation and function calls necessary. IODevice *IODevice::findDevice(VPIN vpin) { for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) { - if (dev->owns(vpin)) + VPIN firstVpin = dev->_firstVpin; + if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins) return dev; } return NULL; @@ -236,7 +265,19 @@ int IODevice::read(VPIN vpin) { return dev->_read(vpin); } #ifdef DIAG_IO - //DIAG(F("IODevice::read(): Vpin %d not found!"), (int)vpin); + DIAG(F("IODevice::read(): Vpin %d not found!"), (int)vpin); +#endif + return false; +} + +// Read analogue value from virtual pin. +int IODevice::readAnalogue(VPIN vpin) { + for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) { + if (dev->owns(vpin)) + return dev->_readAnalogue(vpin); + } +#ifdef DIAG_IO + DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin); #endif return false; } @@ -247,7 +288,17 @@ int IODevice::read(VPIN vpin) { // Minimal implementations of public HAL interface, to support Arduino pin I/O and nothing more. void IODevice::begin() { DIAG(F("NO HAL CONFIGURED!")); } -bool IODevice::configure(VPIN, ConfigTypeEnum, int, int []) { return true; } +bool IODevice::configure(VPIN pin, ConfigTypeEnum, int, int p[]) { + #ifdef DIAG_IO + DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, p[0]); + #endif + if (p[0]) { + pinMode(pin, INPUT_PULLUP); + } else { + pinMode(pin, INPUT); + } + return true; +} void IODevice::write(VPIN vpin, int value) { digitalWrite(vpin, value); pinMode(vpin, OUTPUT); @@ -256,9 +307,15 @@ void IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {} bool IODevice::isBusy(VPIN) { return false; } bool IODevice::hasCallback(VPIN) { return false; } int IODevice::read(VPIN vpin) { - pinMode(vpin, INPUT_PULLUP); return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1) } +int IODevice::readAnalogue(VPIN vpin) { + pinMode(vpin, INPUT); + noInterrupts(); + int value = analogRead(vpin); + interrupts(); + return value; +} void IODevice::loop() {} void IODevice::DumpAll() { DIAG(F("NO HAL CONFIGURED!")); @@ -279,12 +336,14 @@ IONotifyCallback *IONotifyCallback::first = 0; ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) { _firstVpin = firstVpin; _nPins = nPins; - uint8_t arrayLen = (_nPins+7)/8; - _pinPullups = (uint8_t *)calloc(2, arrayLen); + int arrayLen = (_nPins+7)/8; + _pinPullups = (uint8_t *)calloc(3, arrayLen); _pinModes = (&_pinPullups[0]) + arrayLen; + _pinInUse = (&_pinPullups[0]) + 2*arrayLen; for (int i=0; i. + */ + +#ifndef io_analogueinputs_h +#define io_analogueinputs_h + +// Uncomment following line to slow the scan cycle down to 1second ADC samples, with +// diagnostic output of scanned values. +//#define IO_ANALOGUE_SLOW + +#include "IODevice.h" +#include "I2CManager.h" +#include "DIAG.h" +#include "FSH.h" + +/********************************************************************************************** + * ADS111x class for I2C-connected analogue input modules ADS1113, ADS1114 and ADS1115. + * + * ADS1113 and ADS1114 are restricted to 1 input. ADS1115 has a multiplexer which allows + * any of four input pins to be read by its ADC. + * + * The driver polls the device in accordance with the constant 'scanInterval' below. On first loop + * entry, the multiplexer is set to pin A0 and the ADC is triggered. On second and subsequent + * entries, the analogue value is read from the conversion register and then the multiplexer and + * ADC are set up to read the next pin. + * + * The ADS111x is set up as follows: + * Single-shot scan + * Data rate 128 samples/sec (7.8ms/sample, but scanned every 10ms) + * Comparator off + * Gain FSR=6.144V + * The gain means that the maximum input voltage of 5V (when Vss=5V) gives a reading + * of 32767*(5.0/6.144) = 26666. + * + * A device is configured by the following: + * ADS111x::create(firstVpin, nPins, i2cAddress); + * for example + * ADS111x::create(300, 1, 0x48); // single-input ADS1113 + * ADS111x::create(300, 4, 0x48); // four-input ADS1115 + * + * Note: The device is simple and does not need initial configuration, so it should recover from + * temporary loss of communications or power. + **********************************************************************************************/ +class ADS111x: public IODevice { +public: + ADS111x(VPIN firstVpin, int nPins, uint8_t i2cAddress) { + _firstVpin = firstVpin; + _nPins = min(nPins,4); + _i2cAddress = i2cAddress; + _currentPin = 0; + for (int8_t i=0; i<_nPins; i++) + _value[i] = -1; + addDevice(this); + } + static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) { + new ADS111x(firstVpin, nPins, i2cAddress); + } +private: + void _begin() { + // Initialise ADS device + if (I2CManager.exists(_i2cAddress)) { + _nextState = STATE_STARTSCAN; +#ifdef DIAG_IO + _display(); +#endif + } else { + DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress); + _deviceState = DEVSTATE_FAILED; + } + } + void _loop(unsigned long currentMicros) override { + + // Check that previous non-blocking write has completed, if not then wait + uint8_t status = _i2crb.status; + if (status == I2C_STATUS_PENDING) return; // Busy, so don't do anything. + if (status == I2C_STATUS_OK) { + switch (_nextState) { + case STATE_STARTSCAN: + // Configure ADC and multiplexer for next scan. See ADS111x datasheet for details + // of configuration register settings. + _outBuffer[0] = 0x01; // Config register address + _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n + _outBuffer[2] = 0xA3; // 250 samples/sec, comparator off + // Write command, without waiting for completion. + I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); + + delayUntil(currentMicros + scanInterval); + _nextState = STATE_STARTREAD; + break; + + case STATE_STARTREAD: + // Reading the pin value + _outBuffer[0] = 0x00; // Conversion register address + I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register + _nextState = STATE_GETVALUE; + break; + + case STATE_GETVALUE: + _value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1]; + #ifdef IO_ANALOGUE_SLOW + DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]); + #endif + + // Move to next pin + if (++_currentPin >= _nPins) _currentPin = 0; + _nextState = STATE_STARTSCAN; + break; + + default: + break; + } + } else { // error status + DIAG(F("ADS111x I2C:x%d Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status)); + _deviceState = DEVSTATE_FAILED; + } + } + + int _readAnalogue(VPIN vpin) override { + int pin = vpin - _firstVpin; + return _value[pin]; + } + + void _display() override { + DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d %S"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1, + _deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F("")); + } + + // ADC conversion rate is 250SPS, or 4ms per conversion. Set the period between updates to 10ms. + // This is enough to allow the conversion to reliably complete in time. + #ifndef IO_ANALOGUE_SLOW + const unsigned long scanInterval = 10000UL; // Period between successive ADC scans in microseconds. + #else + const unsigned long scanInterval = 1000000UL; // Period between successive ADC scans in microseconds. + #endif + enum : uint8_t { + STATE_STARTSCAN, + STATE_STARTREAD, + STATE_GETVALUE, + }; + uint16_t _value[4]; + uint8_t _i2cAddress; + uint8_t _outBuffer[3]; + uint8_t _inBuffer[2]; + uint8_t _currentPin; // ADC pin currently being scanned + I2CRB _i2crb; + uint8_t _nextState; +}; + +#endif // io_analogueinputs_h \ No newline at end of file diff --git a/IO_DCCAccessory.cpp b/IO_DCCAccessory.cpp index 5e1c8f4..e40198b 100644 --- a/IO_DCCAccessory.cpp +++ b/IO_DCCAccessory.cpp @@ -22,17 +22,9 @@ #include "DIAG.h" #include "defines.h" -// Note: For DCC Accessory Decoders, a particular output can be specified by -// a linear address, or by an address/subaddress pair, where the subaddress is -// in the range 0 to 3 and specifies an output within a group of 4. -// NMRA and DCC++EX accepts addresses in the range 0-511. Linear addresses -// are not specified by the NMRA and so different manufacturers may calculate them -// in different ways. DCC++EX uses a range of 1-2044 which excludes decoder address 0. -// Linear address 1 corresponds to address 1 subaddress 0. - -#define LINEARADDRESS(addr, subaddr) (((addr-1) << 2) + subaddr + 1) -#define ADDRESS(linearaddr) (((linearaddr-1) >> 2) + 1) -#define SUBADDRESS(linearaddr) ((linearaddr-1) % 4) +#define PACKEDADDRESS(addr, subaddr) (((addr) << 2) + (subaddr)) +#define ADDRESS(packedaddr) ((packedaddr) >> 2) +#define SUBADDRESS(packedaddr) ((packedaddr) % 4) void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) { new DCCAccessoryDecoder(vpin, nPins, DCCAddress, DCCSubaddress); @@ -42,7 +34,7 @@ void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSu DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) { _firstVpin = vpin; _nPins = nPins; - _packedAddress = LINEARADDRESS(DCCAddress, DCCSubaddress); + _packedAddress = PACKEDADDRESS(DCCAddress, DCCSubaddress); addDevice(this); } @@ -66,8 +58,7 @@ void DCCAccessoryDecoder::_write(VPIN id, int state) { void DCCAccessoryDecoder::_display() { int endAddress = _packedAddress + _nPins - 1; - DIAG(F("DCCAccessoryDecoder Configured on Vpins:%d-%d Linear Address:%d-%d (%d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1, - _packedAddress, _packedAddress+_nPins-1, + DIAG(F("DCCAccessoryDecoder Configured on Vpins:%d-%d Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1, ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress)); } diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h new file mode 100644 index 0000000..bdf0626 --- /dev/null +++ b/IO_DFPlayer.h @@ -0,0 +1,255 @@ +/* + * © 2021, 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 . + */ + +/* + * DFPlayer is an MP3 player module with an SD card holder. It also has an integrated + * amplifier, so it only needs a power supply and a speaker. + * + * This driver allows the device to be controlled through IODevice::write() and + * IODevice::writeAnalogue() calls. + * + * The driver is configured as follows: + * + * DFPlayer::create(firstVpin, nPins, Serialn); + * + * Where firstVpin is the first vpin reserved for reading the device, + * nPins is the number of pins to be allocated (max 5) + * and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial1). + * + * Example: + * In mySetup function within mySetup.cpp: + * DFPlayer::create(3500, 5, Serial1); + * + * Writing an analogue value 0-2999 to the first pin will select a numbered file from the SD card; + * Writing an analogue value 0-30 to the second pin will set the volume of the output; + * Writing a digital value to the first pin will play or stop the file; + * Reading a digital value from any pin will return true(1) if the player is playing, false(0) otherwise. + * + * From EX-RAIL, the following commands may be used: + * SET(3500) -- starts playing the first file on the SD card + * SET(3501) -- starts playing the second file on the SD card + * etc. + * RESET(3500) -- stops all playing on the player + * WAITFOR(3500) -- wait for the file currently being played by the player to complete + * SERVO(3500,23,0) -- plays file 23 at current volume + * SERVO(3500,23,30) -- plays file 23 at volume 30 (maximum) + * SERVO(3501,20,0) -- Sets the volume to 20 + * + * NB The DFPlayer's serial lines are not 5V safe, so connecting the Arduino TX directly + * to the DFPlayer's RX terminal will cause lots of noise over the speaker, or worse. + * A 1k resistor in series with the module's RX terminal will alleviate this. + */ + +#ifndef IO_DFPlayer_h +#define IO_DFPlayer_h + +#include "IODevice.h" + +class DFPlayer : public IODevice { +private: + HardwareSerial *_serial; + bool _playing = false; + uint8_t _inputIndex = 0; + unsigned long _commandSendTime; // Allows timeout processing + +public: + // Constructor + DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) : + IODevice(firstVpin, nPins), + _serial(&serial) + { + addDevice(this); + } + + static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) { + new DFPlayer(firstVpin, nPins, serial); + } + +protected: + void _begin() override { + _serial->begin(9600); + _deviceState = DEVSTATE_INITIALISING; + + // Send a query to the device to see if it responds + sendPacket(0x42); + _commandSendTime = micros(); + } + + void _loop(unsigned long currentMicros) override { + // Check for incoming data on _serial, and update busy flag accordingly. + // Expected message is in the form "7F FF 06 3D xx xx xx xx xx EF" + while (_serial->available()) { + int c = _serial->read(); + if (c == 0x7E) + _inputIndex = 1; + else if ((c==0xFF && _inputIndex==1) + || (c==0x3D && _inputIndex==3) + || (_inputIndex >=4 && _inputIndex <= 8)) + _inputIndex++; + else if (c==0x06 && _inputIndex==2) { + // Valid message prefix, so consider the device online + if (_deviceState==DEVSTATE_INITIALISING) { + _deviceState = DEVSTATE_NORMAL; + #ifdef DIAG_IO + _display(); + #endif + } + _inputIndex++; + } else if (c==0xEF && _inputIndex==9) { + // End of play + if (_playing) { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Finished")); + #endif + _playing = false; + } + _inputIndex = 0; + } else + _inputIndex = 0; // Unrecognised character sequence, start again! + } + // Check if the initial prompt to device has timed out. Allow 1 second + if (_deviceState == DEVSTATE_INITIALISING && currentMicros - _commandSendTime > 1000000UL) { + DIAG(F("DFPlayer device not responding on serial port")); + _deviceState = DEVSTATE_FAILED; + } + delayUntil(currentMicros + 10000); // Only enter every 10ms + } + + // Write with value 1 starts playing a song. The relative pin number is the file number. + // Write with value 0 stops playing. + void _write(VPIN vpin, int value) override { + int pin = vpin - _firstVpin; + if (value) { + // Value 1, start playing + #ifdef DIAG_IO + DIAG(F("DFPlayer: Play %d"), pin+1); + #endif + sendPacket(0x03, pin+1); + _playing = true; + } else { + // Value 0, stop playing + #ifdef DIAG_IO + DIAG(F("DFPlayer: Stop")); + #endif + sendPacket(0x16); + _playing = false; + } + } + + // WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0. + // Volume may be specified as second parameter to writeAnalogue. + // If value is zero, the player stops playing. + // WriteAnalogue on second pin sets the output volume. + void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override { + uint8_t pin = vpin - _firstVpin; + + // Validate parameter. + volume = min(30,volume); + + if (pin == 0) { + // Play track + if (value > 0) { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Play %d"), value); + #endif + sendPacket(0x03, value); // Play track + _playing = true; + if (volume > 0) { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Volume %d"), volume); + #endif + sendPacket(0x06, volume); // Set volume + } + } else { + #ifdef DIAG_IO + DIAG(F("DFPlayer: Stop")); + #endif + sendPacket(0x16); // Stop play + _playing = false; + } + } else if (pin == 1) { + // Set volume (0-30) + if (value > 30) value = 30; + else if (value < 0) value = 0; + #ifdef DIAG_IO + DIAG(F("DFPlayer: Volume %d"), value); + #endif + sendPacket(0x06, value); + } + } + + // A read on any pin indicates whether the player is still playing. + int _read(VPIN) override { + return _playing; + } + + void _display() override { + DIAG(F("DFPlayer Configured on Vpins:%d-%d %S"), _firstVpin, _firstVpin+_nPins-1, + (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); + } + +private: + // 7E FF 06 0F 00 01 01 xx xx EF + // 0 -> 7E is start code + // 1 -> FF is version + // 2 -> 06 is length + // 3 -> 0F is command + // 4 -> 00 is no receive + // 5~6 -> 01 01 is argument + // 7~8 -> checksum = 0 - ( FF+06+0F+00+01+01 ) + // 9 -> EF is end code + + void sendPacket(uint8_t command, uint16_t arg = 0) + { + uint8_t out[] = { 0x7E, + 0xFF, + 06, + command, + 00, + static_cast(arg >> 8), + static_cast(arg & 0x00ff), + 00, + 00, + 0xEF }; + + setChecksum(out); + + _serial->write(out, sizeof(out)); + } + + uint16_t calcChecksum(uint8_t* packet) + { + uint16_t sum = 0; + for (int i = 1; i < 7; i++) + { + sum += packet[i]; + } + return -sum; + } + + void setChecksum(uint8_t* out) + { + uint16_t sum = calcChecksum(out); + + out[7] = (sum >> 8); + out[8] = (sum & 0xff); + } +}; + +#endif // IO_DFPlayer_h diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 4ca8cce..4269a70 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -45,10 +45,6 @@ protected: int _read(VPIN vpin) override; void _display() override; void _loop(unsigned long currentMicros) override; - bool _hasCallback(VPIN vpin) { - (void)vpin; // suppress compiler warning - return true; // Enable callback if caller wants to use it. - } // Data fields uint8_t _I2CAddress; @@ -57,9 +53,9 @@ protected: T _portOutputState; T _portMode; T _portPullup; + T _portInUse; // Interval between refreshes of each input port static const int _portTickTime = 4000; - unsigned long _lastLoopEntry = 0; // Virtual functions for interfacing with I2C GPIO Device virtual void _writeGpioPort() = 0; @@ -80,12 +76,13 @@ protected: // Constructor template -GPIOBase::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) { +GPIOBase::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) : + IODevice(firstVpin, nPins) +{ _deviceName = deviceName; - _firstVpin = firstVpin; - _nPins = nPins; _I2CAddress = I2CAddress; _gpioInterruptPin = interruptPin; + _hasCallback = true; // Add device to list of devices. addDevice(this); } @@ -104,11 +101,14 @@ void GPIOBase::_begin() { #endif _portMode = 0; // default to input mode _portPullup = -1; // default to pullup enabled - _portInputState = -1; + _portInputState = -1; + _portInUse = 0; + _setupDevice(); + _deviceState = DEVSTATE_NORMAL; + } else { + DIAG(F("%S I2C:x%x Device not detected"), _deviceName, _I2CAddress); + _deviceState = DEVSTATE_FAILED; } - _setupDevice(); - _deviceState = DEVSTATE_NORMAL; - _lastLoopEntry = micros(); } // Configuration parameters for inputs: @@ -128,11 +128,15 @@ bool GPIOBase::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCoun _portPullup |= mask; else _portPullup &= ~mask; + // Mark that port has been accessed + _portInUse |= mask; + // Set input mode + _portMode &= ~mask; // Call subclass's virtual function to write to device + _writePortModes(); _writePullups(); - // Re-read port following change - _readGpioPort(); + // Port change will be notified on next loop entry. return true; } @@ -151,6 +155,8 @@ void GPIOBase::_loop(unsigned long currentMicros) { I2CManager.getErrorMessage(status)); } _processCompletion(status); + // Set unused pin and write mode pin value to 1 + _portInputState |= ~_portInUse | _portMode; // Scan for changes in input states and invoke callback (if present) T differences = lastPortStates ^ _portInputState; @@ -172,27 +178,25 @@ void GPIOBase::_loop(unsigned long currentMicros) { #endif } - // Check if interrupt configured. If so, and pin is not pulled down, finish. - if (_gpioInterruptPin >= 0) { - if (digitalRead(_gpioInterruptPin)) return; - } else - // No interrupt pin. Check if tick has elapsed. If not, finish. - if (currentMicros - _lastLoopEntry < (unsigned long)_portTickTime) return; + // Check if interrupt configured. If not, or if it is active (pulled down), then + // initiate a scan. + if (_gpioInterruptPin < 0 || !digitalRead(_gpioInterruptPin)) { + // TODO: Could suppress reads if there are no pins configured as inputs! - // TODO: Could suppress reads if there are no pins configured as inputs! - - // Read input - _lastLoopEntry = currentMicros; - if (_deviceState == DEVSTATE_NORMAL) { - _readGpioPort(false); // Initiate non-blocking read - _deviceState= DEVSTATE_SCANNING; + // Read input + if (_deviceState == DEVSTATE_NORMAL) { + _readGpioPort(false); // Initiate non-blocking read + _deviceState= DEVSTATE_SCANNING; + } } + // Delay next entry until tick elapsed. + delayUntil(currentMicros + _portTickTime); } template void GPIOBase::_display() { - DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d"), _deviceName, _I2CAddress, - _firstVpin, _firstVpin+_nPins-1); + DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d %S"), _deviceName, _I2CAddress, + _firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); } template @@ -203,8 +207,9 @@ void GPIOBase::_write(VPIN vpin, int value) { DIAG(F("%S I2C:x%x Write Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, value); #endif - // Set port mode output + // Set port mode output if currently not output mode if (!(_portMode & mask)) { + _portInUse |= mask; _portMode |= mask; _writePortModes(); } @@ -224,12 +229,16 @@ int GPIOBase::_read(VPIN vpin) { int pin = vpin - _firstVpin; T mask = 1 << pin; - // Set port mode to input - if (_portMode & mask) { + // Set port mode to input if currently output or first use + if ((_portMode | ~_portInUse) & mask) { _portMode &= ~mask; + _portInUse |= mask; + _writePullups(); _writePortModes(); // Port won't have been read yet, so read it now. _readGpioPort(); + // Set unused pin and write mode pin value to 1 + _portInputState |= ~_portInUse | _portMode; #ifdef DIAG_IO DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState); #endif diff --git a/IO_HCSR04.h b/IO_HCSR04.h index 98340ff..9bbd2f8 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -17,31 +17,37 @@ * along with CommandStation. If not, see . */ -/* +/* * The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver. - * It is operated through two signal pins. When the transmit pin is set to 1 for - * 10us, on the falling edge the transmitter sends a short transmission of + * It is operated through two signal pins. When the transmit pin is set to 1 + * for 10us, on the falling edge the transmitter sends a short transmission of * 8 pulses (like a sonar 'ping'). This is reflected off objects and received * by the receiver. A pulse is sent on the receive pin whose length is equal * to the delay between the transmission of the pulse and the detection of * its echo. The distance of the reflecting object is calculated by halving * the time (to allow for the out and back distance), then multiplying by the * speed of sound (assumed to be constant). - * + * * This driver polls the HC-SR04 by sending the trigger pulse and then measuring - * the length of the received pulse. If the calculated distance is less than the - * threshold, the output changes to 1. If it is greater than the threshold plus - * a hysteresis margin, the output changes to 0. - * - * The measurement would be more reliable if interrupts were disabled while the - * pulse is being timed. However, this would affect other functions in the CS - * so the measurement is being performed with interrupts enabled. Also, we could - * use an interrupt pin in the Arduino for the timing, but the same consideration - * applies. - * - * Note: The timing accuracy required by this means that the pins have to be - * direct Arduino pins; GPIO pins on an IO Extender cannot provide the required - * accuracy. + * the length of the received pulse. If the calculated distance is less than + * the threshold, the output state returned by a read() call changes to 1. If + * the distance is greater than the threshold plus a hysteresis margin, the + * output changes to 0. The device also supports readAnalogue(), which returns + * the measured distance in cm, or 32767 if the distance exceeds the + * offThreshold. + * + * It might be thought that the measurement would be more reliable if interrupts + * were disabled while the pulse is being timed. However, this would affect + * other functions in the CS so the measurement is being performed with + * interrupts enabled. Also, we could use an interrupt pin in the Arduino for + * the timing, but the same consideration applies. In any case, the DCC + * interrupt occurs once every 58us, so any IRC code is much faster than that. + * And 58us corresponds to 1cm in the calculation, so the effect of + * interrupts is negligible. + * + * Note: The timing accuracy required for measuring the pulse length means that + * the pins have to be direct Arduino pins; GPIO pins on an IO Extender cannot + * provide the required accuracy. */ #ifndef IO_HCSR04_H @@ -53,43 +59,42 @@ class HCSR04 : public IODevice { private: // pins must be arduino GPIO pins, not extender pins or HAL pins. - int _transmitPin = -1; - int _receivePin = -1; + int _trigPin = -1; + int _echoPin = -1; // Thresholds for setting active state in cm. uint8_t _onThreshold; // cm uint8_t _offThreshold; // cm + // Last measured distance in cm. + uint16_t _distance; // Active=1/inactive=0 state uint8_t _value = 0; - // Time of last loop execution - unsigned long _lastExecutionTime; // Factor for calculating the distance (cm) from echo time (ms). // Based on a speed of sound of 345 metres/second. const uint16_t factor = 58; // ms/cm public: // Constructor perfroms static initialisation of the device object - HCSR04 (VPIN vpin, int transmitPin, int receivePin, uint16_t onThreshold, uint16_t offThreshold) { + HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { _firstVpin = vpin; _nPins = 1; - _transmitPin = transmitPin; - _receivePin = receivePin; + _trigPin = trigPin; + _echoPin = echoPin; _onThreshold = onThreshold; _offThreshold = offThreshold; addDevice(this); } // Static create function provides alternative way to create object - static void create(VPIN vpin, int transmitPin, int receivePin, uint16_t onThreshold, uint16_t offThreshold) { - new HCSR04(vpin, transmitPin, receivePin, onThreshold, offThreshold); + static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { + new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold); } protected: // _begin function called to perform dynamic initialisation of the device void _begin() override { - pinMode(_transmitPin, OUTPUT); - pinMode(_receivePin, INPUT); - ArduinoPins::fastWriteDigital(_transmitPin, 0); - _lastExecutionTime = micros(); + pinMode(_trigPin, OUTPUT); + pinMode(_echoPin, INPUT); + ArduinoPins::fastWriteDigital(_trigPin, 0); #if defined(DIAG_IO) _display(); #endif @@ -101,18 +106,21 @@ protected: return _value; } + int _readAnalogue(VPIN vpin) override { + (void)vpin; // avoid compiler warning + return _distance; + } + // _loop function - read HC-SR04 once every 50 milliseconds. void _loop(unsigned long currentMicros) override { - if (currentMicros - _lastExecutionTime > 50000UL) { - _lastExecutionTime = currentMicros; - - _value = read_HCSR04device(); - } + read_HCSR04device(); + // Delay next loop entry until 50ms have elapsed. + delayUntil(currentMicros + 50000UL); } void _display() override { DIAG(F("HCSR04 Configured on Vpin:%d TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"), - _firstVpin, _transmitPin, _receivePin, _onThreshold, _offThreshold); + _firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold); } private: @@ -127,51 +135,52 @@ private: // measured distance is less than the onThreshold, and is set to 0 if the measured distance is // greater than the offThreshold. // - uint8_t read_HCSR04device() { + void read_HCSR04device() { // uint16 enough to time up to 65ms uint16_t startTime, waitTime, currentTime, maxTime; // If receive pin is still set on from previous call, abort the read. - if (ArduinoPins::fastReadDigital(_receivePin)) return _value; + if (ArduinoPins::fastReadDigital(_echoPin)) + return; // Send 10us pulse to trigger transmitter - ArduinoPins::fastWriteDigital(_transmitPin, 1); + ArduinoPins::fastWriteDigital(_trigPin, 1); delayMicroseconds(10); - ArduinoPins::fastWriteDigital(_transmitPin, 0); + ArduinoPins::fastWriteDigital(_trigPin, 0); // Wait for receive pin to be set startTime = currentTime = micros(); maxTime = factor * _offThreshold * 2; - while (!ArduinoPins::fastReadDigital(_receivePin)) { + while (!ArduinoPins::fastReadDigital(_echoPin)) { // lastTime = currentTime; currentTime = micros(); waitTime = currentTime - startTime; if (waitTime > maxTime) { // Timeout waiting for pulse start, abort the read - return _value; + return; } } // Wait for receive pin to reset, and measure length of pulse startTime = currentTime = micros(); maxTime = factor * _offThreshold; - while (ArduinoPins::fastReadDigital(_receivePin)) { + while (ArduinoPins::fastReadDigital(_echoPin)) { currentTime = micros(); waitTime = currentTime - startTime; // If pulse is too long then set return value to zero, // and finish without waiting for end of pulse. if (waitTime > maxTime) { // Pulse length longer than maxTime, reset value. - return 0; + _value = 0; + _distance = 32767; + return; } } // Check if pulse length is below threshold, if so set value. //DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, distance); - uint16_t distance = waitTime / factor; // in centimetres - if (distance < _onThreshold) - return 1; - - return _value; + _distance = waitTime / factor; // in centimetres + if (_distance < _onThreshold) + _value = 1; } }; diff --git a/IO_MCP23008.h b/IO_MCP23008.h index 3557b49..18ff12f 100644 --- a/IO_MCP23008.h +++ b/IO_MCP23008.h @@ -42,14 +42,18 @@ private: I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState); } void _writePullups() override { - I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup); + // Set pullups only for in-use pins. This prevents pullup being set for a pin that + // is intended for use as an output but hasn't been written to yet. + I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup & _portInUse); } void _writePortModes() override { - // Each bit is 1 for an input, 0 for an output, i.e. inverted. - I2CManager.write(_I2CAddress, 2, REG_IODIR, ~_portMode); - // Enable interrupt-on-change for pins that are inputs (_portMode=0) + // Write 0 to IODIR for in-use pins that are outputs, 1 for others. + uint8_t temp = ~(_portMode & _portInUse); + I2CManager.write(_I2CAddress, 2, REG_IODIR, temp); + // Enable interrupt-on-change for in-use pins that are inputs (_portMode=0) + temp = ~_portMode & _portInUse; I2CManager.write(_I2CAddress, 2, REG_INTCON, 0x00); - I2CManager.write(_I2CAddress, 2, REG_GPINTEN, ~_portMode); + I2CManager.write(_I2CAddress, 2, REG_GPINTEN, temp); } void _readGpioPort(bool immediate) override { if (immediate) { diff --git a/IO_MCP23017.h b/IO_MCP23017.h index d7c27ce..930b051 100644 --- a/IO_MCP23017.h +++ b/IO_MCP23017.h @@ -48,14 +48,19 @@ private: I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8); } void _writePullups() override { - I2CManager.write(_I2CAddress, 3, REG_GPPUA, _portPullup, _portPullup>>8); + // Set pullups only for in-use pins. This prevents pullup being set for a pin that + // is intended for use as an output but hasn't been written to yet. + uint16_t temp = _portPullup & _portInUse; + I2CManager.write(_I2CAddress, 3, REG_GPPUA, temp, temp>>8); } void _writePortModes() override { - // Write 1 to IODIR for pins that are inputs, 0 for outputs (i.e. _portMode inverted) - I2CManager.write(_I2CAddress, 3, REG_IODIRA, ~_portMode, (~_portMode)>>8); - // Enable interrupt for those pins which are inputs (_portMode=0) + // Write 0 to IODIR for in-use pins that are outputs, 1 for others. + uint16_t temp = ~(_portMode & _portInUse); + I2CManager.write(_I2CAddress, 3, REG_IODIRA, temp, temp>>8); + // Enable interrupt for in-use pins which are inputs (_portMode=0) + temp = ~_portMode & _portInUse; I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00); - I2CManager.write(_I2CAddress, 3, REG_GPINTENA, ~_portMode, (~_portMode)>>8); + I2CManager.write(_I2CAddress, 3, REG_GPINTENA, temp, temp>>8); } void _readGpioPort(bool immediate) override { if (immediate) { diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index ddc45d8..009ff63 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -107,12 +107,14 @@ void PCA9685::_begin() { #if defined(DIAG_IO) _display(); #endif - } + } else + _deviceState = DEVSTATE_FAILED; } // Device-specific write function, invoked from IODevice::write(). // For this function, the configured profile is used. void PCA9685::_write(VPIN vpin, int value) { + if (_deviceState == DEVSTATE_FAILED) return; #ifdef DIAG_IO DIAG(F("PCA9685 Write Vpin:%d Value:%d"), vpin, value); #endif @@ -137,6 +139,7 @@ void PCA9685::_write(VPIN vpin, int value) { // 4 (Bounce) Servo 'bounces' at extremes. // void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { + if (_deviceState == DEVSTATE_FAILED) return; #ifdef DIAG_IO DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d"), vpin, value, profile, duration); @@ -169,9 +172,10 @@ void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur s->fromPosition = s->currentPosition; } -// _isBusy returns true if the device is currently in executing an animation, +// _read returns true if the device is currently in executing an animation, // changing the output over a period of time. -bool PCA9685::_isBusy(VPIN vpin) { +int PCA9685::_read(VPIN vpin) { + if (_deviceState == DEVSTATE_FAILED) return 0; int pin = vpin - _firstVpin; struct ServoData *s = _servoData[pin]; if (s == NULL) @@ -181,12 +185,10 @@ bool PCA9685::_isBusy(VPIN vpin) { } void PCA9685::_loop(unsigned long currentMicros) { - if (currentMicros - _lastRefreshTime >= refreshInterval * 1000) { - for (int pin=0; pin<_nPins; pin++) { - updatePosition(pin); - } - _lastRefreshTime = currentMicros; + for (int pin=0; pin<_nPins; pin++) { + updatePosition(pin); } + delayUntil(currentMicros + refreshInterval * 1000UL); } // Private function to reposition servo @@ -238,20 +240,25 @@ void PCA9685::writeDevice(uint8_t pin, int value) { DIAG(F("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value); #endif // Wait for previous request to complete - requestBlock.wait(); - // 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); + 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); + } } // Display details of this device. void PCA9685::_display() { - DIAG(F("PCA9685 I2C:x%x Configured on Vpins:%d-%d"), _I2CAddress, (int)_firstVpin, - (int)_firstVpin+_nPins-1); + 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("")); } // Internal helper function for this device diff --git a/IO_PCF8574.h b/IO_PCF8574.h index 2a8d363..dea5e2c 100644 --- a/IO_PCF8574.h +++ b/IO_PCF8574.h @@ -17,6 +17,24 @@ * along with CommandStation. If not, see . */ +/* + * The PCF8574 is a simple device; it only has one register. The device + * input/output mode and pullup are configured through this, and the + * output state is written and the input state read through it too. + * + * This is accomplished by having a weak resistor in series with the output, + * and a read-back of the other end of the resistor. As an output, the + * pin state is set to 1 or 0, and the output voltage goes to +5V or 0V + * (through the weak resistor). + * + * In order to use the pin as an input, the output is written as + * a '1' in order to pull up the resistor. Therefore the input will be + * 1 unless the pin is pulled down externally, in which case it will be 0. + * + * As a consequence of this approach, it is not possible to use the device for + * inputs without pullups. + */ + #ifndef IO_PCF8574_H #define IO_PCF8574_H @@ -70,7 +88,7 @@ private: if (status == I2C_STATUS_OK) _portInputState = ((uint16_t)inputBuffer[0]) & 0xff; else - _portInputState = 0xff; + _portInputState = 0xff; } // Set up device ports diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h new file mode 100644 index 0000000..bcdbc49 --- /dev/null +++ b/IO_VL53L0X.h @@ -0,0 +1,299 @@ +/* + * © 2021, 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 . + */ + +/* + * The VL53L0X Time-Of-Flight sensor operates by sending a short laser pulse and detecting + * the reflection of the pulse. The time between the pulse and the receipt of reflections + * is measured and used to determine the distance to the reflecting object. + * + * For economy of memory and processing time, this driver includes only part of the code + * that ST provide in their API. Also, the API code isn't very clear and it is not easy + * to identify what operations are useful and what are not. + * The operation shown here doesn't include any calibration, so is probably not as accurate + * as using the full driver, but it's probably accurate enough for the purpose. + * + * The device driver allocates up to 3 vpins to the device. A digital read on the first pin + * will return a value that indicates whether the object is within the threshold range (1) + * or not (0). An analogue read on the first pin returns the last measured distance (in mm), + * the second pin returns the signal strength, and the third pin returns detected + * ambient light level. By default the device takes around 60ms to complete a ranging + * operation, so we do a 100ms cycle (10 samples per second). + * + * The VL53L0X is initially set to respond to I2C address 0x29. If you only have one module, + * you can use this address. However, the address can be modified by software. If + * you select another address, that address will be written to the device and used until the device is reset. + * + * If you have more than one module, then you will need to specify a digital VPIN (Arduino + * digital output or I/O extender pin) which you connect to the module's XSHUT pin. Now, + * when the device driver starts, the XSHUT pin is set LOW to turn the module off. Once + * all VL53L0X modules are turned off, the driver works through each module in turn by + * setting XSHUT to HIGH to turn the module on,, then writing the module's desired I2C address. + * In this way, many VL53L0X modules can be connected to the one I2C bus, each one + * using a distinct I2C address. + * + * WARNING: If the device's XSHUT pin is not connected, then it is very prone to noise, + * and the device may even reset when handled. If you're not using XSHUT, then it's + * best to tie it to +5V. + * + * The driver is configured as follows: + * + * Single VL53L0X module: + * VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold); + * Where firstVpin is the first vpin reserved for reading the device, + * nPins is 1, 2 or 3, + * i2cAddress is the address of the device (normally 0x29), + * lowThreshold is the distance at which the digital vpin state is set to 1 (in mm), + * and highThreshold is the distance at which the digital vpin state is set to 0 (in mm). + * + * Multiple VL53L0X modules: + * VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold, xshutPin); + * ... + * Where firstVpin is the first vpin reserved for reading the device, + * nPins is 1, 2 or 3, + * i2cAddress is the address of the device (any valid address except 0x29), + * lowThreshold is the distance at which the digital vpin state is set to 1 (in mm), + * highThreshold is the distance at which the digital vpin state is set to 0 (in mm), + * and xshutPin is the VPIN number corresponding to a digital output that is connected to the + * XSHUT terminal on the module. + * + * Example: + * In mySetup function within mySetup.cpp: + * VL53L0X::create(4000, 3, 0x29, 200, 250); + * Sensor::create(4000, 4000, 0); // Create a sensor + * + * When an object comes within 200mm of the sensor, a message + * + * will be sent over the serial USB, and when the object moves more than 250mm from the sensor, + * a message + * + * will be sent. + * + */ + +#ifndef IO_VL53L0X_h +#define IO_VL53L0X_h + +#include "IODevice.h" + +class VL53L0X : public IODevice { +private: + uint8_t _i2cAddress; + uint16_t _ambient; + uint16_t _distance; + uint16_t _signal; + uint16_t _onThreshold; + uint16_t _offThreshold; + VPIN _xshutPin; + bool _value; + uint8_t _nextState = 0; + I2CRB _rb; + uint8_t _inBuffer[12]; + uint8_t _outBuffer[2]; + // State machine states. + enum : uint8_t { + STATE_INIT = 0, + STATE_CONFIGUREADDRESS = 1, + STATE_SKIP = 2, + STATE_CONFIGUREDEVICE = 3, + STATE_INITIATESCAN = 4, + STATE_CHECKSTATUS = 5, + STATE_GETRESULTS = 6, + STATE_DECODERESULTS = 7, + }; + + // Register addresses + enum : uint8_t { + VL53L0X_REG_SYSRANGE_START=0x00, + VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13, + VL53L0X_REG_RESULT_RANGE_STATUS=0x14, + VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV=0x89, + VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A, + }; + const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29; + +public: + VL53L0X(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) { + _firstVpin = firstVpin; + _nPins = min(nPins, 3); + _i2cAddress = i2cAddress; + _onThreshold = onThreshold; + _offThreshold = offThreshold; + _xshutPin = xshutPin; + _value = 0; + addDevice(this); + } + static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) { + new VL53L0X(firstVpin, nPins, i2cAddress, onThreshold, offThreshold, xshutPin); + } + +protected: + void _begin() override { + if (_xshutPin == VPIN_NONE) { + // Check if device is already responding on the nominated address. + if (I2CManager.exists(_i2cAddress)) { + // Yes, it's already on this address, so skip the address initialisation. + _nextState = STATE_CONFIGUREDEVICE; + } else { + _nextState = STATE_INIT; + } + } + } + + void _loop(unsigned long currentMicros) override { + uint8_t status; + switch (_nextState) { + case STATE_INIT: + // On first entry to loop, reset this module by pulling XSHUT low. All modules + // will be reset in turn. + if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0); + _nextState = STATE_CONFIGUREADDRESS; + break; + case STATE_CONFIGUREADDRESS: + // On second entry, set XSHUT pin high to allow the module to restart. + // On the module, there is a diode in series with the XSHUT pin to + // protect the low-voltage pin against +5V. + if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1); + // Allow the module time to restart + delay(10); + // Then write the desired I2C address to the device, while this is the only + // module responding to the default address. + I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress); + _nextState = STATE_SKIP; + break; + case STATE_SKIP: + // Do nothing on the third entry. + _nextState = STATE_CONFIGUREDEVICE; + break; + case STATE_CONFIGUREDEVICE: + // On next entry, check if device address has been set. + if (I2CManager.exists(_i2cAddress)) { + #ifdef DIAG_IO + _display(); + #endif + // Set 2.8V mode + write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV, + read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01); + } else { + DIAG(F("VL53L0X I2C:x%x device not responding"), _i2cAddress); + _deviceState = DEVSTATE_FAILED; + } + _nextState = STATE_INITIATESCAN; + break; + case STATE_INITIATESCAN: + // Not scanning, so initiate a scan + _outBuffer[0] = VL53L0X_REG_SYSRANGE_START; + _outBuffer[1] = 0x01; + I2CManager.write(_i2cAddress, _outBuffer, 2, &_rb); + _nextState = STATE_CHECKSTATUS; + break; + case STATE_CHECKSTATUS: + status = _rb.status; + if (status == I2C_STATUS_PENDING) return; // try next time + if (status != I2C_STATUS_OK) { + DIAG(F("VL53L0X I2C:x%x Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status)); + _deviceState = DEVSTATE_FAILED; + _value = false; + } else + _nextState = 2; + delayUntil(currentMicros + 95000); // wait for 95 ms before checking. + _nextState = STATE_GETRESULTS; + break; + case STATE_GETRESULTS: + // Ranging completed. Request results + _outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS; + I2CManager.read(_i2cAddress, _inBuffer, 12, _outBuffer, 1, &_rb); + _nextState = 3; + delayUntil(currentMicros + 5000); // Allow 5ms to get data + _nextState = STATE_DECODERESULTS; + break; + case STATE_DECODERESULTS: + // If I2C write still busy, return. + status = _rb.status; + if (status == I2C_STATUS_PENDING) return; // try again next time + if (status == I2C_STATUS_OK) { + if (!(_inBuffer[0] & 1)) return; // device still busy + uint8_t deviceRangeStatus = ((_inBuffer[0] & 0x78) >> 3); + if (deviceRangeStatus == 0x0b) { + // Range status OK, so use data + _ambient = makeuint16(_inBuffer[7], _inBuffer[6]); + _signal = makeuint16(_inBuffer[9], _inBuffer[8]); + _distance = makeuint16(_inBuffer[11], _inBuffer[10]); + if (_distance <= _onThreshold) + _value = true; + else if (_distance > _offThreshold) + _value = false; + } + } + // Completed. Restart scan on next loop entry. + _nextState = STATE_INITIATESCAN; + break; + default: + break; + } + } + + // For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level. + int _readAnalogue(VPIN vpin) override { + int pin = vpin - _firstVpin; + switch (pin) { + case 0: + return _distance; + case 1: + return _signal; + case 2: + return _ambient; + default: + return -1; + } + } + + // For digital read, return zero for all but first pin. + int _read(VPIN vpin) override { + if (vpin == _firstVpin) + return _value; + else + return 0; + } + + void _display() override { + DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"), + _i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold, + (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F("")); + } + + +private: + inline uint16_t makeuint16(byte lsb, byte msb) { + return (((uint16_t)msb) << 8) | lsb; + } + uint8_t write_reg(uint8_t reg, uint8_t data) { + // write byte to register + uint8_t outBuffer[2]; + outBuffer[0] = reg; + outBuffer[1] = data; + return I2CManager.write(_i2cAddress, outBuffer, 2); + } + uint8_t read_reg(uint8_t reg) { + // read byte from register and return value + I2CManager.read(_i2cAddress, _inBuffer, 1, ®, 1); + return _inBuffer[0]; + } +}; + +#endif // IO_VL53L0X_h diff --git a/MotorDrivers.h b/MotorDrivers.h index 1087090..e6316fb 100644 --- a/MotorDrivers.h +++ b/MotorDrivers.h @@ -87,5 +87,9 @@ #define IBT_2_WITH_ARDUINO F("IBT_2_WITH_ARDUINO_SHIELD"), \ new MotorDriver(4, 5, 6, UNUSED_PIN, A5, 41.54, 5000, UNUSED_PIN), \ new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN) +// YFROBOT Motor Shield (V3.1) +#define YFROBOT_MOTOR_SHIELD F("YFROBOT_MOTOR_SHIELD"), \ + new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 2000, UNUSED_PIN), \ + new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN) #endif diff --git a/RMFT2.cpp b/RMFT2.cpp index 842b460..5a91541 100644 --- a/RMFT2.cpp +++ b/RMFT2.cpp @@ -19,6 +19,7 @@ #include #include "RMFT2.h" #include "DCC.h" +#include "DCCWaveform.h" #include "DIAG.h" #include "WiThrottle.h" #include "DCCEXParser.h" @@ -43,6 +44,7 @@ const int16_t HASH_KEYWORD_ROUTES=-3702; // The thrrads exist in a ring, each time through loop() the next thread in the ring is serviced. // Statics +const int16_t LOCO_ID_WAITING=-99; // waiting for loco id from prog track int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prograck bool RMFT2::diag=false; // RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring. @@ -64,6 +66,16 @@ byte RMFT2::flags[MAX_FLAGS]; byte opcode=GET_OPCODE; if (opcode==OPCODE_ENDEXRAIL) break; + switch (opcode) { + case OPCODE_AT: + case OPCODE_AFTER: + case OPCODE_IF: + case OPCODE_IFNOT: + int16_t pin = (int16_t)GET_OPERAND(0); + if (pin<0) pin = -pin; + IODevice::configureInput((VPIN)pin,true); + } + if (opcode==OPCODE_SIGNAL) { VPIN red=GET_OPERAND(0); VPIN amber=GET_OPERAND(1); @@ -326,6 +338,10 @@ int RMFT2::locateRouteStart(int16_t _route) { void RMFT2::driveLoco(byte speed) { if (loco<=0) return; // Prevent broadcast! if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert); + if (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::OFF) { + DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON); + Serial.println(F("")); // tell JMRI + } DCC::setThrottle(loco,speed, forward^invert); speedo=speed; } @@ -477,6 +493,13 @@ void RMFT2::loop2() { if (loco) DCC::writeCVByteMain(loco, operand, GET_OPERAND(1)); break; + case OPCODE_POWEROFF: + DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF); + DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF); + DCC::setProgTrackSyncMain(false); + Serial.println(F("")); // Tell JMRI + break; + case OPCODE_RESUME: pausingTask=NULL; driveLoco(speedo); @@ -572,7 +595,10 @@ void RMFT2::loop2() { return; case OPCODE_JOIN: + DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON); + DCCWaveform::progTrack.setPowerMode(POWERMODE::ON); DCC::setProgTrackSyncMain(true); + Serial.println(F("")); // Tell JMRI break; case OPCODE_UNJOIN: @@ -580,14 +606,20 @@ void RMFT2::loop2() { break; case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes + progtrackLocoId=LOCO_ID_WAITING; // Nothing found yet DCC::getLocoId(readLocoCallback); break; case OPCODE_READ_LOCO2: - if (progtrackLocoId<0) { + if (progtrackLocoId==LOCO_ID_WAITING) { delayMe(100); return; // still waiting for callback } + if (progtrackLocoId<0) { + kill(F("No Loco Found"),progtrackLocoId); + return; // still waiting for callback + } + loco=progtrackLocoId; speedo=0; forward=true; diff --git a/RMFT2.h b/RMFT2.h index a254070..90080b7 100644 --- a/RMFT2.h +++ b/RMFT2.h @@ -40,7 +40,7 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE, OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN, OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,OPCODE_POM, OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO, - OPCODE_PAUSE, OPCODE_RESUME, + OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF, OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT, OPCODE_PRINT, OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,OPCODE_ENDTASK,OPCODE_ENDEXRAIL diff --git a/RMFTMacros.h b/RMFTMacros.h index fc5140f..01bfb46 100644 --- a/RMFTMacros.h +++ b/RMFTMacros.h @@ -19,8 +19,9 @@ #ifndef RMFTMacros_H #define RMFTMacros_H -// remove normal code LCD macro (will be restored later) +// remove normal code LCD & SERIAL macros (will be restored later) #undef LCD +#undef SERIAL // This file will include and build the EXRAIL script and associated helper tricks. @@ -89,6 +90,7 @@ #define PAUSE #define PRINT(msg) #define POM(cv,value) +#define POWEROFF #define READ_LOCO #define RED(signal_id) #define RESERVE(blockid) @@ -147,7 +149,7 @@ const int StringMacroTracker1=__COUNTER__; #define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break; #define SERIAL(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial,F(msg));break; #define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break; -#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(L&Serial2,F(msg));break; +#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial2,F(msg));break; #define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break; #define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break; #include "myAutomation.h" @@ -187,6 +189,7 @@ const int StringMacroTracker1=__COUNTER__; #undef ONTHROW #undef PAUSE #undef POM +#undef POWEROFF #undef PRINT #undef READ_LOCO #undef RED @@ -264,6 +267,7 @@ const int StringMacroTracker1=__COUNTER__; #define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id), #define PAUSE OPCODE_PAUSE,NOP, #define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value), +#define POWEROFF OPCODE_POWEROFF,NOP, #define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2), #define READ_LOCO OPCODE_READ_LOCO1,NOP,OPCODE_READ_LOCO2,NOP, #define RED(signal_id) OPCODE_RED,V(signal_id), @@ -299,8 +303,9 @@ const int StringMacroTracker1=__COUNTER__; const int StringMacroTracker2=__COUNTER__; #include "myAutomation.h" -// Restore normal code LCD macro +// Restore normal code LCD & SERIAL macro #undef LCD #define LCD StringFormatter::lcd - +#undef SERIAL +#define SERIAL 0x0 #endif diff --git a/Sensors.cpp b/Sensors.cpp index 4d3283a..d6d3d81 100644 --- a/Sensors.cpp +++ b/Sensors.cpp @@ -103,12 +103,6 @@ void Sensor::checkAll(Print *stream){ // Required time elapsed since last read cycle started, // so initiate new scan through the sensor list readingSensor = firstSensor; -#ifdef USE_NOTIFY - if (firstSensor == firstPollSensor) - pollSignalPhase = true; - else - pollSignalPhase = false; -#endif lastReadCycle = thisTime; } } @@ -117,12 +111,6 @@ void Sensor::checkAll(Print *stream){ bool pause = false; while (readingSensor != NULL && !pause) { -#ifdef USE_NOTIFY - // Check if we have reached the start of the polled portion of the sensor list. - if (readingSensor == firstPollSensor) - pollSignalPhase = true; -#endif - // Where the sensor is attached to a pin, read pin status. For sources such as LCN, // which don't have an input pin to read, the LCN class calls setState() to update inputState when // a message is received. The IODevice::read() call returns 1 for active pins (0v) and 0 for inactive (5v). @@ -130,10 +118,8 @@ void Sensor::checkAll(Print *stream){ // routine when an input signal change is detected, and this updates the inputState directly, // so these inputs don't need to be polled here. VPIN pin = readingSensor->data.pin; -#ifdef USE_NOTIFY - if (pollSignalPhase) -#endif - if (pin!=VPIN_NONE) readingSensor->inputState = IODevice::read(pin); + if (readingSensor->pollingRequired && pin != VPIN_NONE) + readingSensor->inputState = IODevice::read(pin); // Check if changed since last time, and process changes. if (readingSensor->inputState == readingSensor->active) { @@ -156,22 +142,12 @@ void Sensor::checkAll(Print *stream){ // Move to next sensor in list. readingSensor = readingSensor->nextSensor; - // Currently process max of 16 sensors per entry for polled sensors, and - // 16 per entry for sensors notified by callback. - // Performance measurements taken during development indicate that, with 64 sensors configured - // on 8x 8-pin PCF8574 GPIO expanders, all inputs can be read within 1.4ms (400Mhz I2C bus speed), and a - // full cycle of scanning 64 sensors for changes takes between 1.9 and 3.2 milliseconds. + // Currently process max of 16 sensors per entry. + // Performance measurements taken during development indicate that, with 128 sensors configured + // on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices + // within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond. sensorCount++; -#ifdef USE_NOTIFY - if (pollSignalPhase) { -#endif - if (sensorCount >= 16) pause = true; -#ifdef USE_NOTIFY - } else - { - if (sensorCount >= 16) pause = true; - } -#endif + if (sensorCount >= 16) pause = true; } } // Sensor::checkAll @@ -223,23 +199,18 @@ Sensor *Sensor::create(int snum, VPIN pin, int pullUp){ tt = (Sensor *)calloc(1,sizeof(Sensor)); if (!tt) return tt; // memory allocation failure -#ifdef USE_NOTIFY - if (pin == VPIN_NONE || IODevice::hasCallback(pin)) { - // Callback available, or no pin to read, so link sensor on to the start of the list - tt->nextSensor = firstSensor; - firstSensor = tt; - if (lastSensor == NULL) lastSensor = tt; // This is only item in list. - } else { - // No callback, so add to end of list so it's polled. - if (lastSensor != NULL) lastSensor->nextSensor = tt; - lastSensor = tt; - if (!firstSensor) firstSensor = tt; - if (!firstPollSensor) firstPollSensor = tt; - } -#else + if (pin == VPIN_NONE) + tt->pollingRequired = false; + #ifdef USE_NOTIFY + else if (IODevice::hasCallback(pin)) + tt->pollingRequired = false; + #endif + else + tt->pollingRequired = true; + + // Add to the start of the list tt->nextSensor = firstSensor; firstSensor = tt; -#endif tt->data.snum = snum; tt->data.pin = pin; @@ -248,9 +219,8 @@ Sensor *Sensor::create(int snum, VPIN pin, int pullUp){ tt->inputState = 0; tt->latchDelay = minReadCount; - int params[] = {pullUp}; if (pin != VPIN_NONE) - IODevice::configure(pin, IODevice::CONFIGURE_INPUT, 1, params); + IODevice::configureInput(pin, pullUp); // Generally, internal pull-up resistors are not, on their own, sufficient // for external infrared sensors --- each sensor must have its own 1K external pull-up resistor @@ -343,6 +313,5 @@ unsigned long Sensor::lastReadCycle=0; #ifdef USE_NOTIFY Sensor *Sensor::firstPollSensor = NULL; Sensor *Sensor::lastSensor = NULL; -bool Sensor::pollSignalPhase = false; bool Sensor::inputChangeCallbackRegistered = false; #endif \ No newline at end of file diff --git a/Sensors.h b/Sensors.h index 60e414f..7547784 100644 --- a/Sensors.h +++ b/Sensors.h @@ -45,14 +45,6 @@ struct SensorData { class Sensor{ // The sensor list is a linked list where each sensor's 'nextSensor' field points to the next. // The pointer is null in the last on the list. - // To partition the sensor into those sensors which require polling through cyclic calls - // to 'IODevice::read(vpin)', and those which support callback on change, 'firstSensor' - // points to the start of the overall list, and 'lastSensor' points to the end of the list - // (the last sensor object). This structure allows sensors to be added to the start or the - // end of the list easily. So if an input pin supports change notification, it is placed at the - // end of the list. If not, it is placed at the beginning. And the pointer 'firstPollSensor' - // is set to the first of the sensor objects that requires scanning. Thus, we can iterate - // through the whole list, or just through the part that requires scanning. public: SensorData data; @@ -74,6 +66,7 @@ public: // Constructor Sensor(); Sensor *nextSensor; + void setState(int state); static void load(); static void store(); @@ -88,9 +81,9 @@ public: static const unsigned int minReadCount = 1; // number of additional scans before acting on change // E.g. 1 means that a change is ignored for one scan and actioned on the next. // Max value is 63 + bool pollingRequired = true; #ifdef USE_NOTIFY - static bool pollSignalPhase; static void inputChangeCallback(VPIN vpin, int state); static bool inputChangeCallbackRegistered; #endif diff --git a/Turnouts.cpp b/Turnouts.cpp index 6dbc402..dcbff73 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -340,7 +340,8 @@ DCCTurnout::DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) : Turnout(id, TURNOUT_DCC, false) { - _dccTurnoutData.address = ((address-1) << 2) + subAdd + 1; + _dccTurnoutData.address = address; + _dccTurnoutData.subAddress = subAdd; } // Create function @@ -351,7 +352,8 @@ if (tt->isType(TURNOUT_DCC)) { // Yes, so set parameters DCCTurnout *dt = (DCCTurnout *)tt; - dt->_dccTurnoutData.address = ((add-1) << 2) + subAdd + 1; + dt->_dccTurnoutData.address = add; + dt->_dccTurnoutData.subAddress = subAdd; // Don't touch the _closed parameter, retain the original value. return tt; } else { @@ -371,27 +373,24 @@ EEStore::advance(sizeof(dccTurnoutData)); // Create new object - DCCTurnout *tt = new DCCTurnout(turnoutData->id, (((dccTurnoutData.address-1) >> 2)+1), ((dccTurnoutData.address-1) & 3)); + DCCTurnout *tt = new DCCTurnout(turnoutData->id, dccTurnoutData.address, dccTurnoutData.subAddress); return tt; } void DCCTurnout::print(Print *stream) { StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - !_turnoutData.closed); + _dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed); // Also report using classic DCC++ syntax for DCC accessory turnouts, since JMRI expects this. StringFormatter::send(stream, F("\n"), _turnoutData.id, - (((_dccTurnoutData.address-1) >> 2)+1), ((_dccTurnoutData.address-1) & 3), - !_turnoutData.closed); + _dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed); } bool DCCTurnout::setClosedInternal(bool close) { // DCC++ Classic behaviour is that Throw writes a 1 in the packet, // and Close writes a 0. // RCN-213 specifies that Throw is 0 and Close is 1. - DCC::setAccessory((((_dccTurnoutData.address-1) >> 2) + 1), - ((_dccTurnoutData.address-1) & 3), close ^ !rcn213Compliant); + DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant); _turnoutData.closed = close; return true; } diff --git a/Turnouts.h b/Turnouts.h index 9c14089..6a55c15 100644 --- a/Turnouts.h +++ b/Turnouts.h @@ -213,9 +213,11 @@ private: // DCCTurnoutData contains data specific to this subclass that is // written to EEPROM when the turnout is saved. struct DCCTurnoutData { - // DCC address (Address in bits 15-2, subaddress in bits 1-0 - uint16_t address; // CS currently supports linear address 1-2048 - // That's DCC accessory address 1-512 and subaddress 0-3. + // DCC address (Address in bits 15-2, subaddress in bits 1-0) + struct { + uint16_t address : 14; + uint8_t subAddress : 2; + }; } _dccTurnoutData; // 2 bytes // Constructor diff --git a/config.example.h b/config.example.h index 9de54b0..056f1b4 100644 --- a/config.example.h +++ b/config.example.h @@ -119,11 +119,11 @@ The configuration file for DCC-EX Command Station // DEFINE LCD SCREEN USAGE BY THE BASE STATION // // Note: This feature requires an I2C enabled LCD screen using a Hitachi HD44780 -// controller and a PCF8574 based I2C 'backpack'. +// controller and a commonly available PCF8574 based I2C 'backpack'. // To enable, uncomment one of the #define lines below -// define LCD_DRIVER for I2C LCD address 0x3f,16 cols, 2 rows -// #define LCD_DRIVER 0x3F,16,2 +// define LCD_DRIVER for I2C address 0x27, 16 cols, 2 rows +// #define LCD_DRIVER 0x27,16,2 //OR define OLED_DRIVER width,height in pixels (address auto detected) // 128x32 or 128x64 I2C SSD1306-based devices are supported. diff --git a/mySetup.cpp_example.txt b/mySetup.cpp_example.txt index 949088a..0efc771 100644 --- a/mySetup.cpp_example.txt +++ b/mySetup.cpp_example.txt @@ -13,6 +13,7 @@ #include "Turnouts.h" #include "Sensors.h" #include "IO_HCSR04.h" +#include "IO_VL53L0X.h" // The #if directive prevent compile errors for Uno and Nano by excluding the @@ -23,8 +24,9 @@ // Examples of statically defined HAL directives (alternative to the create() call). // These have to be outside of the mySetup() function. - +//======================================================================= // The following directive defines a PCA9685 PWM Servo driver module. +//======================================================================= // The parameters are: // First Vpin=100 // Number of VPINs=16 (numbered 100-115) @@ -33,13 +35,15 @@ //PCA9685 pwmModule1(100, 16, 0x40); +//======================================================================= // The following directive defines an MCP23017 16-port I2C GPIO Extender module. +//======================================================================= // The parameters are: -// First Vpin=164 -// Number of VPINs=16 (numbered 164-179) -// I2C address of module=0x20 +// First Vpin=196 +// Number of VPINs=16 (numbered 196-211) +// I2C address of module=0x22 -//MCP23017 gpioModule2(164, 16, 0x20); +//MCP23017 gpioModule2(196, 16, 0x22); // Alternative form, which allows the INT pin of the module to request a scan @@ -47,19 +51,23 @@ // all the time, only when a change takes place. Multiple modules' INT pins // may be connected to the same Arduino pin. -//MCP23017 gpioModule2(164, 16, 0x20, 40); +//MCP23017 gpioModule2(196, 16, 0x22, 40); +//======================================================================= // The following directive defines an MCP23008 8-port I2C GPIO Extender module. +//======================================================================= // The parameters are: // First Vpin=300 // Number of VPINs=8 (numbered 300-307) // I2C address of module=0x22 -//MCP23017 gpioModule3(300, 8, 0x22); +//MCP23008 gpioModule3(300, 8, 0x22); +//======================================================================= // The following directive defines a PCF8574 8-port I2C GPIO Extender module. +//======================================================================= // The parameters are: // First Vpin=200 // Number of VPINs=8 (numbered 200-207) @@ -73,7 +81,9 @@ //PCF8574 gpioModule4(200, 8, 0x23, 40); -// The following directive defines an HCSR04 ultrasonic module. +//======================================================================= +// The following directive defines an HCSR04 ultrasonic ranging module. +//======================================================================= // The parameters are: // Vpin=2000 (only one VPIN per directive) // Number of VPINs=1 @@ -90,20 +100,48 @@ //HCSR04 sonarModule2(2001, 30, 32, 20, 25); +//======================================================================= +// The following directive defines a single VL53L0X Time-of-Flight range sensor. +//======================================================================= +// The parameters are: +// VPIN=5000 +// Number of VPINs=1 +// I2C address=0x29 (default for this chip) +// Minimum trigger range=200mm (VPIN goes to 1 when <20cm) +// Maximum trigger range=250mm (VPIN goes to 0 when >25cm) + +//VL53L0X tofModule1(5000, 1, 0x29, 200, 250); + +// For multiple VL53L0X modules, add another parameter which is a VPIN connected to the +// module's XSHUT pin. This allows the modules to be configured, at start, +// with distinct I2C addresses. In this case, the address 0x29 is only used during +// initialisation to configure each device in turn with the desired unique I2C address. +// The examples below have the modules' XSHUT pins connected to the first two pins of +// the first MCP23017 module (164 and 165), but Arduino pins may be used instead. +// The first module here is given I2C address 0x30 and the second is 0x31. + +//VL53L0X tofModule1(5000, 1, 0x30, 200, 250, 164); +//VL53L0X tofModule2(5001, 1, 0x31, 200, 250, 165); + + +//======================================================================= // The function mySetup() is invoked from CS if it exists within the build. // It is called just before mysetup.h is executed, so things set up within here can be // referenced by commands in mySetup.h. +//======================================================================= void mySetup() { - // Alternative way of creating MCP23017, which has to be within the mySetup() function + // Alternative way of creating a module driver, which has to be within the mySetup() function // The other devices can also be created in this way. The parameter lists for the // create() function are identical to the parameter lists for the declarations. - //MCP23017::create(180, 16, 0x21); + //MCP23017::create(196, 16, 0x22); + //======================================================================= // Creating a Turnout + //======================================================================= // Parameters: same as command for Servo turnouts // ID and VPIN are 100, sonar moves between positions 102 and 490 with slow profile. // Profile may be Instant, Fast, Medium, Slow or Bounce. @@ -111,7 +149,9 @@ void mySetup() { //ServoTurnout::create(100, 100, 490, 102, PCA9685::Slow); + //======================================================================= // DCC Accessory turnout + //======================================================================= // Parameters: same as command for DCC Accessory turnouts // ID=3000 // Decoder address=23 @@ -120,7 +160,9 @@ void mySetup() { //DCCTurnout::create(3000, 23, 1); + //======================================================================= // Creating a Sensor + //======================================================================= // Parameters: As for the command, // id = 164, // Vpin = 164 (configured above as pin 0 of an MCP23017) @@ -129,11 +171,44 @@ void mySetup() { //Sensor::create(164, 164, 1); + //======================================================================= // Way of creating lots of identical sensors in a range + //======================================================================= //for (int i=165; i<180; i++) // Sensor::create(i, i, 1); + + //======================================================================= + // Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module. + //======================================================================= + // Parameters: + // 10000 = first VPIN allocated. + // 10 = number of VPINs allocated. + // Serial1 = name of serial port (usually Serial1 or Serial2). + // With these parameters, up to 10 files may be played on pins 10000-10009. + // Play is started from EX-RAIL with SET(10000) for first mp3 file, SET(10001) + // for second file, etc. Play may also be initiated by writing an analogue + // value to the first pin, e.g. SERVO(10000,23,0) will play the 23rd mp3 file. + // SERVO(10000,23,30) will do the same thing, as well as setting the volume to + // 30 (maximum value). + // Play is stopped by RESET(10000) (or any other allocated VPIN). + // Volume may also be set by writing an analogue value to the second pin for the player, + // e.g. SERVO(10001,30,0) sets volume to maximum (30). + // The EX-RAIL script may check for completion of play by calling WAITFOR(pin), which will only proceed to the + // following line when the player is no longer busy. + // E.g. + // SEQUENCE(1) + // AT(164) // Wait for sensor attached to pin 164 to activate + // SET(10003) // Play fourth MP3 file + // LCD(4, "Playing") // Display message on LCD/OLED + // WAITFOR(10003) // Wait for playing to finish + // LCD(4, " ") // Clear LCD/OLED line + // FOLLOW(1) // Go back to start + + // DFPlayer::create(10000, 10, Serial1); + + } #endif diff --git a/platformio.ini b/platformio.ini index 953a658..8bba297 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,6 +16,7 @@ default_envs = unowifiR2 nano src_dir = . +include_dir = . [env] build_flags = -Wall -Wextra @@ -41,7 +42,7 @@ lib_deps = SPI monitor_speed = 115200 monitor_flags = --echo -build_flags = -DDIAG_IO +build_flags = -DDIAG_IO -DDIAG_LOOPTIMES [env:mega2560-no-HAL] platform = atmelavr diff --git a/version.h b/version.h index 09211fa..617673c 100644 --- a/version.h +++ b/version.h @@ -3,8 +3,26 @@ #include "StringFormatter.h" - -#define VERSION "3.1.6" +#define VERSION "3.2.0 rc2" +// 3.2.0 Major functional and non-functional changes. +// New HAL added for I/O (digital and analogue inputs and outputs, servos etc). +// Support for MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules. +// Support for PCA9685 PWM (servo) control modules. +// Support for analogue inputs on Arduino pins and on ADS111x I2C modules. +// Support for MP3 sound playback via DFPlayer module. +// Support for HC-SR04 Ultrasonic range sensor module. +// Support for VL53L0X Laser range sensor module (Time-Of-Flight). +// Native non-blocking I2C drivers for AVR and Nano architectures (fallback +// to blocking Wire library for other platforms). +// EEPROM layout change - deletes EEPROM contents on first start following upgrade. +// New EX-RAIL automation capability. +// Turnout class revised to expand turnout capabilities, new commands added. +// Output class now allows ID > 255. +// Configuration options to globally flip polarity of DCC Accessory states when driven +// from command and command. +// Increased use of display for showing loco decoder programming information. +// ... +// 3.1.7 Bugfix: Unknown locos should have speed forward // 3.1.6 Make output ID two bytes and guess format/size of registered outputs found in EEPROM // 3.1.5 Fix LCD corruption on power-up // 3.1.4 Refactor OLED and LCD drivers and remove unused code