From 2ed578821ffec6103342285e6b8a2ebf132a2fad Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Sat, 11 Sep 2021 13:35:11 +0100 Subject: [PATCH] Add analogue inputs to HAL. Add ability to read analogue inputs on arduino and on external ADS1115 I2C modules. --- IODevice.cpp | 54 +++++++++++++++++- IODevice.h | 14 ++++- IO_AnalogueInputs.h | 131 ++++++++++++++++++++++++++++++++++++++++++++ platformio.ini | 3 +- 4 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 IO_AnalogueInputs.h diff --git a/IODevice.cpp b/IODevice.cpp index e19ddef..98584ca 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -241,6 +241,18 @@ int IODevice::read(VPIN vpin) { 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; +} + #else // !defined(IO_NO_HAL) @@ -259,6 +271,13 @@ 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!")); @@ -330,7 +349,7 @@ void ArduinoPins::_write(VPIN vpin, int value) { } } -// Device-specific read function. +// Device-specific read function (digital input). int ArduinoPins::_read(VPIN vpin) { int pin = vpin; uint8_t mask = 1 << ((pin-_firstVpin) % 8); @@ -352,6 +371,39 @@ int ArduinoPins::_read(VPIN vpin) { return value; } +// Device-specific readAnalogue function (analogue input) +int ArduinoPins::_readAnalogue(VPIN vpin) { + int pin = vpin; + uint8_t mask = 1 << ((pin-_firstVpin) % 8); + uint8_t index = (pin-_firstVpin) / 8; + if (_pinModes[index] & mask) { + // Currently in write mode, change to read mode + _pinModes[index] &= ~mask; + // Since mode changes should be infrequent, use standard pinMode function + if (_pinPullups[index] & mask) + pinMode(pin, INPUT_PULLUP); + else + pinMode(pin, INPUT); + } + // Since AnalogRead is also called from interrupt code, disable interrupts + // while we're using it. There's only one ADC shared by all analogue inputs + // on the Arduino, so we don't want interruptions. + //****************************************************************************** + // NOTE: If the HAL is running on a computer without the DCC signal generator, + // then interrupts needn't be disabled. Also, the DCC signal generator puts + // the ADC into fast mode, so if it isn't present, analogueRead calls will be much + // slower!! + //****************************************************************************** + noInterrupts(); + int value = analogRead(pin); + interrupts(); + + #ifdef DIAG_IO + //DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value); + #endif + return value; +} + void ArduinoPins::_display() { DIAG(F("Arduino Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1); } diff --git a/IODevice.h b/IODevice.h index 65f5d35..58de64b 100644 --- a/IODevice.h +++ b/IODevice.h @@ -141,6 +141,9 @@ public: // read invokes the IODevice instance's _read method. static int read(VPIN vpin); + // read invokes the IODevice instance's _readAnalogue method. + static int readAnalogue(VPIN vpin); + // loop invokes the IODevice instance's _loop method. static void loop(); @@ -188,12 +191,18 @@ protected: return false; } - // Method to read pin state (optionally implemented within device class) + // Method to read digital pin state (optionally implemented within device class) virtual int _read(VPIN vpin) { (void)vpin; return 0; }; + // Method to read analogue pin state (optionally implemented within device class) + virtual int _readAnalogue(VPIN vpin) { + (void)vpin; + 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) { @@ -343,8 +352,9 @@ private: bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override; // Device-specific write function. void _write(VPIN vpin, int value) override; - // Device-specific read function. + // Device-specific read functions. int _read(VPIN vpin) override; + int _readAnalogue(VPIN vpin) override; void _display() override; diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h new file mode 100644 index 0000000..4c73329 --- /dev/null +++ b/IO_AnalogueInputs.h @@ -0,0 +1,131 @@ +/* + * © 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 . + */ + +#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) + * 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. + * + * 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 = _nPins; // Suppress read on first loop entry. + addDevice(this); + } + static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) { + new ADS111x(firstVpin, nPins, i2cAddress); + } + void _begin() { + // Initialise ADS device + if (I2CManager.exists(_i2cAddress)) { +#ifdef DIAG_IO + _display(); +#endif + } else { + DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress); + } + } + void _loop(unsigned long currentMicros) { + + 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 + } + } + // Move to next pin + if (++_currentPin >= _nPins) _currentPin = 0; + + // 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] = 0x83; // 128 samples/sec, comparator off + // Write command, without waiting for completion. + I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb); + + _lastMicros = currentMicros; + } + } + int _readAnalogue(VPIN vpin) { + int pin = vpin - _firstVpin; + return _value[pin]; + } + void _display() { + 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 + #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 + uint16_t _value[4]; + uint8_t _i2cAddress; + uint8_t _outBuffer[3]; + uint8_t _inBuffer[2]; + uint8_t _currentPin; // ADC pin currently being scanned + unsigned long _lastMicros = 0; + I2CRB _i2crb; +}; + +#endif // io_analogueinputs_h \ No newline at end of file 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