diff --git a/IO_S88.h b/IO_S88.h new file mode 100644 index 0000000..d56d18e --- /dev/null +++ b/IO_S88.h @@ -0,0 +1,232 @@ +/* + * © 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 S88 Bus is essentially a shift register consisting of one or more + * S88 modules, connected one to another in one long chain. The register + * is read by the following steps: + * 1) The LOAD/PS line goes to HIGH, then the CLK line is pulsed HIGH. This + * tells the registers to acquire data from the latched parallel inputs. 2) With + * LOAD/PS still high, the RESET signal is pulsed HIGH, which clears the + * parallel input upstream latches. 3) With LOAD/PS LOW, the shift is performed + * by pulsing the CLK line high. On each pulse, the data in the shift register + * is presented, bit by bit, to the DATA-IN line. + * + * Example configuration in mySetup.cpp: + * #include "IO_S88.h" + * void mySetup() { + * S88bus::create(2500, 16, 40,41,42,43, 10); + * } + * + * This creates an S88 bus instance with 16 inputs (VPINs 2500-2515). + * The LOAD pin is 40, RESET is 41, CLK is 42 and DATA is 43. These four pins + * must be local GPIO pins (not on an I/O expander). + * The last parameter (bitTime=10) means that at least 10us is allowed for + * reading each bit in the shift register. + * + * Depending on the S88 modules being used and the cabling, the timing of the + * interface may have to be adjusted. This is done by the bit time parameter. + * The original S88 concept was based on 4014 shift register devices, which are + * easily capable of moderate speed transfers and would cope with a bit time + * around 2us. However, commercial S88 devices appear to be extremely slow in + * comparison and may need the order of tens or hundreds of microseconds. The + * symptom of a bit time that is too small is that, when you activate one input to + * the S88 module, the command station sees something other than the correct + * activation; e.g. no activations, multiple activations, the wrong pin + * activating etc. + * + * _acquireCycleTime determines the minimum time between successive acquire + * cycles of the inputs on the S88 bus. Bear in mind that the Sensor code + * includes anti-bounce logic which means that fleeting state changes may be + * ignored, so reducing the acquireCycleTime may not have the desired effect. + * Also, if the combination of bit time and number of pins is large, the + * specified cycle time may not be achieved. + * + */ + +#ifndef IO_S88_H +#define IO_S88_H + +#include "IODevice.h" + +class S88bus : public IODevice { + +private: + uint8_t _loadPin; + uint8_t _resetPin; + uint8_t _clockPin; + uint8_t _dataInPin; + uint8_t *_values; // Bit array, initialised in _begin method. + int _currentByteIndex; + unsigned long _lastAquireCycleStart; + uint16_t _pulseDelayTime; // Delay microsecs; can be tuned for the S88 hardware + uint16_t _shortDelayTime; + uint8_t _bitIndex; + unsigned long _lastPulseTime; + uint8_t _state; + uint8_t _maxBitsPerEntry; + + + // The acquire cycle time is a target maximum rate. If there are a lot of signals or the + // bit time is long, then the cycle time may be longer. + const unsigned long _acquireCycleTime = 20000; // target 20 milliseconds between acquire cycles + +public: + S88bus(VPIN firstVpin, int nPins, uint8_t loadPin, uint8_t resetPin, uint8_t clockPin, uint8_t dataInPin, uint16_t bitTime) : + IODevice(firstVpin, nPins), + _loadPin(loadPin), + _resetPin(resetPin), + _clockPin(clockPin), + _dataInPin(dataInPin), + _currentByteIndex(0), + _lastAquireCycleStart(0), + _pulseDelayTime((bitTime+1)/2) + { + // Allocate memory for input values. + _values = (uint8_t *)calloc((nPins+7)/8, 1); + _shortDelayTime = (_pulseDelayTime > 10) ? 10 : _pulseDelayTime; + _state = 0; + // The program typically manages 30 microseconds per clock cycle + // with no waiting, so limit the number of bits so that loop doesn't + // take much more than 200us. + _maxBitsPerEntry = 200 / (30+bitTime) + 1; + addDevice(this); + } + + static void create(VPIN firstVpin, int nPins, uint8_t loadPin, uint8_t resetPin, uint8_t clockPin, uint8_t dataInPin, uint16_t bitTime) { + new S88bus(firstVpin, nPins, loadPin, resetPin, clockPin, dataInPin, bitTime); + } + +protected: + void _begin() override { + pinMode(_loadPin, OUTPUT); + pinMode(_resetPin, OUTPUT); + pinMode(_clockPin, OUTPUT); + pinMode(_dataInPin, INPUT); + + #ifdef DIAG_IO + _display(); + #endif + } + + // Read method returns the latest aquired value for the nominated VPIN number. + int _read(VPIN vpin) override { + uint16_t pin = vpin - _firstVpin; + uint8_t mask = 1 << (pin % 8); + uint16_t byteIndex = pin / 8; + return (_values[byteIndex] & mask) ? 1 : 0; + } + + // Loop method acquires the input states from the shift register. + // At the beginning of each acquisition cycle, instruct the bus registers to acquire the + // input states from the latches, then reset the latches. On + // subsequent loop entries, some of the input states are shifted from the + // registers, until they have all been read. Then the whole process + // resumes for the next acquisition cycle. The operations are spread over consecutive + // loop entries to restrict the amount of time taken in each entry. + void _loop(unsigned long currentMicros) override { + // If just starting a new read, then latch the input values into the S88 + // registers. The active edge is the rising edge in each case, so we + // can use shorter delays for some transitions. + switch (_state) { + case 0: // Starting cycle. Set up LOAD and CLOCK pins. + _lastAquireCycleStart = currentMicros; + // Set LOAD pin + ArduinoPins::fastWriteDigital(_loadPin, HIGH); + _lastPulseTime = micros(); + pulseDelay(_shortDelayTime); + // Pulse CLOCK pin to read inputs into registers + ArduinoPins::fastWriteDigital(_clockPin, HIGH); + // Clear CLOCK, and set up RESET pin. + pulseDelay(_pulseDelayTime); + ArduinoPins::fastWriteDigital(_clockPin, LOW); + pulseDelay(_shortDelayTime); + // Pulse RESET pin to clear inputs ready for next acquisition period + ArduinoPins::fastWriteDigital(_resetPin, HIGH); + _state = 1; + delayUntil(_lastPulseTime + _pulseDelayTime); + return; + case 1: // Clear RESET and LOAD + ArduinoPins::fastWriteDigital(_resetPin, LOW); + pulseDelay(_shortDelayTime); + ArduinoPins::fastWriteDigital(_loadPin, LOW); + // Initialise variables used in reading bits. + _currentByteIndex = _bitIndex = 0; + _state = 2; + /* fallthrough */ + case 2: + // Subsequent loop entries, read each bit in turn from the shiftregister. + uint8_t bitCount = 0; + while (true) { + uint8_t mask = (1 << _bitIndex); + bool newValue = ArduinoPins::fastReadDigital(_dataInPin); +#ifdef DIAG_IO + bool oldValue = _values[_currentByteIndex] & mask; + if (newValue != oldValue) DIAG(F("S88 VPIN:%d Value:%d"), + _firstVpin+_currentByteIndex*8+_bitIndex, newValue); +#endif + if (newValue) + _values[_currentByteIndex] |= mask; + else + _values[_currentByteIndex] &= ~mask; + if (++_bitIndex == 8) { + // Byte completed, so move to next one. + _currentByteIndex++; + _bitIndex = 0; + } + + // Check if this cycle is complete. + if (_currentByteIndex*8 + _bitIndex >= _nPins) { + // All bits in the shift register have been read now, so + // don't read again until next acquisition cycle time + delayUntil(_lastAquireCycleStart + _acquireCycleTime); + _state = 0; + return; + } + + // Clock next bit in. + pulseDelay(_pulseDelayTime); + ArduinoPins::fastWriteDigital(_clockPin, HIGH); + pulseDelay(_pulseDelayTime); + ArduinoPins::fastWriteDigital(_clockPin, LOW); + + // See if we've done all we're allowed on this entry + if (++bitCount >= _maxBitsPerEntry) { + delayUntil(_lastPulseTime + _pulseDelayTime); + return; + } + } + } + } + + void _display() override { + DIAG(F("S88bus Configured on Vpins %d-%d, LOAD=%d RESET=%d CLK=%d DATAIN=%d"), + _firstVpin, _firstVpin+_nPins-1, _loadPin, _resetPin, _clockPin, _dataInPin); + } + + // Helper function to delay until a minimum number of microseconds have elapsed + // since _lastPulseTime. + void pulseDelay(uint16_t duration) { + delayMicroseconds(duration); + _lastPulseTime = micros(); + } + +}; + +#endif