diff --git a/IODevice.cpp b/IODevice.cpp index 98584ca..a48564b 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -53,7 +53,9 @@ void IODevice::begin() { MCP23017::create(180, 16, 0x21); // Call the begin() methods of each configured device in turn + unsigned long currentMicros = micros(); for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) { + dev->_nextEntryTime = currentMicros; dev->_begin(); } _initPhase = false; @@ -69,8 +71,14 @@ void IODevice::loop() { unsigned long currentMicros = micros(); // Call every device's loop function in turn, one per entry. if (!_nextLoopDevice) _nextLoopDevice = _firstDevice; - if (_nextLoopDevice) { + // Check if device exists, and is due to run + if (_nextLoopDevice /* && ((long)(currentMicros-_nextLoopDevice->_nextEntryTime) >= 0) */ ) { + // Move _nextEntryTime on, so that we can guarantee that the device will continue to + // be serviced if it doesn't update _nextEntryTime. + _nextLoopDevice->_nextEntryTime = currentMicros; + // Invoke device's _loop function _nextLoopDevice->_loop(currentMicros); + // Move to next device. _nextLoopDevice = _nextLoopDevice->_nextDevice; } @@ -157,12 +165,13 @@ void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t dur #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; } diff --git a/IODevice.h b/IODevice.h index 58de64b..7e45816 100644 --- a/IODevice.h +++ b/IODevice.h @@ -129,7 +129,7 @@ public: static void write(VPIN vpin, int value); // write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs) - static void writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration=0); + static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0); // 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. @@ -178,7 +178,7 @@ protected: }; // Method to write an 'analogue' value (optionally implemented within device class) - virtual void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { + virtual void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) { (void)vpin; (void)value; (void) profile; (void)duration; }; @@ -203,13 +203,6 @@ protected: return 0; }; - // _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. Returns false unless overridden in sub class. - virtual bool _isBusy(VPIN vpin) { - (void)vpin; - return false; - } - // Method to perform updates on an ongoing basis (optionally implemented within device class) virtual void _loop(unsigned long currentMicros) { (void)currentMicros; // Suppress compiler warning. @@ -220,6 +213,11 @@ protected: // Destructor virtual ~IODevice() {}; + + // Non-virtual function + void delayUntil(unsigned long futureMicrosCount) { + _nextEntryTime = futureMicrosCount; + } // Common object fields. VPIN _firstVpin; @@ -242,6 +240,7 @@ private: static IODevice *findDevice(VPIN vpin); IODevice *_nextDevice = 0; + unsigned long _nextEntryTime; static IODevice *_firstDevice; static IODevice *_nextLoopDevice; @@ -276,7 +275,7 @@ private: // Device-specific write functions. void _write(VPIN vpin, int value) override; void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override; - bool _isBusy(VPIN vpin) override; + int _read(VPIN vpin) override; // returns the busy status of the device void _loop(unsigned long currentMicros) override; void updatePosition(uint8_t pin); void writeDevice(uint8_t pin, int value); diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h index 4c73329..7d24586 100644 --- a/IO_AnalogueInputs.h +++ b/IO_AnalogueInputs.h @@ -42,12 +42,18 @@ * * The ADS111x is set up as follows: * Single-shot scan - * Data rate 128 samples/sec (7.8ms/sample) + * 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. **********************************************************************************************/ @@ -63,6 +69,7 @@ public: 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)) { @@ -73,22 +80,25 @@ public: DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress); } } - void _loop(unsigned long currentMicros) { + void _loop(unsigned long currentMicros) override { if (currentMicros - _lastMicros >= scanInterval) { // Check that previous non-blocking write has completed, if not then wait - _i2crb.wait(); - - // If _currentPin is in the valid range, continue reading the pin values - if (_currentPin < _nPins) { - _outBuffer[0] = 0x00; // Conversion register address - uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, 1, _outBuffer); // Read register - if (status == I2C_STATUS_OK) { - _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 + uint8_t status = _i2crb.wait(); + if (status == I2C_STATUS_OK) { + // If _currentPin is in the valid range, continue reading the pin values + if (_currentPin < _nPins) { + _outBuffer[0] = 0x00; // Conversion register address + uint8_t status = I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1); // Read register + if (status == I2C_STATUS_OK) { + _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 + } } + if (status != I2C_STATUS_OK) + DIAG(F("ADS111x I2C:x%d Error:%d"), _i2cAddress, status); } // Move to next pin if (++_currentPin >= _nPins) _currentPin = 0; @@ -97,23 +107,23 @@ public: // of configuration register settings. _outBuffer[0] = 0x01; // Config register address _outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n - _outBuffer[2] = 0x83; // 128 samples/sec, comparator off + _outBuffer[2] = 0xA3; // 250 samples/sec, comparator off // Write command, without waiting for completion. I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); _lastMicros = currentMicros; } } - int _readAnalogue(VPIN vpin) { + int _readAnalogue(VPIN vpin) override { int pin = vpin - _firstVpin; return _value[pin]; } - void _display() { + void _display() override { DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1); } -protected: - // With ADC set to 128 samples/sec, that's 7.8ms/sample. So set the period between updates to 10ms + // 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 diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h new file mode 100644 index 0000000..4c133b5 --- /dev/null +++ b/IO_DFPlayer.h @@ -0,0 +1,229 @@ +/* + * © 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; + +public: + DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) { + _firstVpin = firstVpin; + _nPins = 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); + _display(); + } + + void _loop(unsigned long) 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(); +// DIAG(F("Received: %x"), c); + if (c == 0x7E) + _inputIndex = 1; + else if ((c==0xFF && _inputIndex==1) || (c==0x06 && _inputIndex==2) + || (c==0x3D && _inputIndex==3) || (_inputIndex >=4 && _inputIndex <= 8)) + _inputIndex++; + else if (c==0xEF && _inputIndex==9) { + // End of play + #ifdef DIAG_IO + DIAG(F("DFPlayer: Finished")); + #endif + _playing = false; + _inputIndex = 0; + } + } + } + + // 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"), _firstVpin, _firstVpin+_nPins-1); + } + +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_HCSR04.h b/IO_HCSR04.h index 98340ff..2df3733 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,11 +59,13 @@ 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 @@ -68,27 +76,27 @@ private: 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); + pinMode(_trigPin, OUTPUT); + pinMode(_echoPin, INPUT); + ArduinoPins::fastWriteDigital(_trigPin, 0); _lastExecutionTime = micros(); #if defined(DIAG_IO) _display(); @@ -101,18 +109,25 @@ 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 +142,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_PCA9685.cpp b/IO_PCA9685.cpp index ddc45d8..d8c9795 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -169,9 +169,9 @@ 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) { int pin = vpin - _firstVpin; struct ServoData *s = _servoData[pin]; if (s == NULL) diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h new file mode 100644 index 0000000..08de1aa --- /dev/null +++ b/IO_VL53L0X.h @@ -0,0 +1,249 @@ +/* + * © 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 any of the pins + * 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. + * + * 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 with a distinct I2C address. + * + * 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; + bool _initialising = true; + uint8_t _entryCount = 0; + unsigned long _lastEntryTime = 0; + bool _scanInProgress = false; + // 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 { + _initialising = true; + // 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. + _entryCount = 3; + } else { + _entryCount = 0; + } + } + void _loop(unsigned long currentMicros) override { + if (_initialising) { + switch (_entryCount++) { + case 0: + // 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); + break; + case 1: + // 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); + break; + case 3: + if (I2CManager.exists(_i2cAddress)) { + _display(); + // Set 2.8V mode + write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV, + read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01); + } + _initialising = false; + _entryCount = 0; + break; + default: + break; + } + } else if (_lastEntryTime - currentMicros > 10000UL) { + // Service device every 10ms + _lastEntryTime = currentMicros; + + if (!_scanInProgress) { + // Not scanning, so initiate a scan + write_reg(VL53L0X_REG_SYSRANGE_START, 0x01); + _scanInProgress = true; + + } else { + // Scan in progress, so check for completion. + uint8_t status = read_reg(VL53L0X_REG_RESULT_RANGE_STATUS); + if (status & 1) { + // Completed. Retrieve data + uint8_t inBuffer[12]; + read_registers(VL53L0X_REG_RESULT_RANGE_STATUS, inBuffer, 12); + 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; + } + _scanInProgress = false; + } + } + } + } + // 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 the same value for all pins. + int _read(VPIN) override { + return _value; + } + void _display() override { + DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm"), + _i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold); + } + + +private: + inline uint16_t makeuint16(byte lsb, byte msb) { + return (((uint16_t)msb) << 8) | lsb; + } + void write_reg(uint8_t reg, uint8_t data) { + // write byte to register + uint8_t outBuffer[2]; + outBuffer[0] = reg; + outBuffer[1] = data; + I2CManager.write(_i2cAddress, outBuffer, 2); + } + uint8_t read_reg(uint8_t reg) { + // read byte from register register + uint8_t inBuffer[1]; + I2CManager.read(_i2cAddress, inBuffer, 1, ®, 1); + return inBuffer[0]; + } + void read_registers(uint8_t reg, uint8_t buffer[], uint8_t size) { + I2CManager.read(_i2cAddress, buffer, size, ®, 1); + } +}; + +#endif // IO_VL53L0X_h 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