From b17356bff22c31bd15d28ca373acfa921f2853cc Mon Sep 17 00:00:00 2001 From: Neil McKechnie Date: Tue, 21 Mar 2023 15:12:51 +0000 Subject: [PATCH] CMRI RS485 support --- IO_CMRI.cpp | 136 +++++++++++++++++++++++++++++++++++++++++++++++++--- IO_CMRI.h | 125 +++++++++++++++-------------------------------- 2 files changed, 169 insertions(+), 92 deletions(-) diff --git a/IO_CMRI.cpp b/IO_CMRI.cpp index e442d26..9e3a9b3 100644 --- a/IO_CMRI.cpp +++ b/IO_CMRI.cpp @@ -19,6 +19,43 @@ #include "IO_CMRI.h" +/************************************************************ + * CMRIbus implementation + ************************************************************/ + +// Constructor for CMRIbus +CMRIbus::CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin) { + _busNo = busNo; + _serial = &serial; + _baud = baud; + _cycleTime = cycleTimeMS * 1000UL; // convert from milliseconds to microseconds. + _transmitEnablePin = transmitEnablePin; + if (_transmitEnablePin != VPIN_NONE) { + pinMode(_transmitEnablePin, OUTPUT); + ArduinoPins::fastWriteDigital(_transmitEnablePin, 0); // transmitter initially off + } + + // Max message length is 256+6=262 bytes. + // Each byte is one start bit, 8 data bits and 1 stop bit = 10 bits per byte. + // Calculate timeout based on double this time. + _timeoutPeriod = 2 * 10 * 262 * 1000UL / (_baud / 1000UL); + + // Calculate the time in microseconds to transmit one byte (10 bits). + _byteTransmitTime = 1000000UL * 10 / _baud; + // Postdelay is only required if we need to allow for data still being sent when + // we want to switch off the transmitter. The flush() method of HardwareSerial + // ensures that the data has completed being sent over the line. + _postDelay = 0; + + // Add device to HAL device chain + IODevice::addDevice(this); + + // Add bus to CMRIbus chain. + _nextBus = _busList; + _busList = this; +} + + // Main loop function for CMRIbus. // Work through list of nodes. For each node, in separate loop entries // send initialisation message (once only); then send @@ -37,7 +74,56 @@ void CMRIbus::_loop(unsigned long currentMicros) { } +// Send output data to the bus for nominated CMRInode +uint16_t CMRIbus::sendData(CMRInode *node) { + uint16_t numDataBytes = (node->getNumOutputs()+7)/8; + _serial->write(SYN); + _serial->write(SYN); + _serial->write(STX); + _serial->write(node->getAddress() + 65); + _serial->write('T'); // T for Transmit data message + uint16_t charsSent = 6; // include header and trailer + for (uint8_t index=0; indexgetOutputStates(index); + if (value == DLE || value == STX || value == ETX) { + _serial->write(DLE); + charsSent++; + } + _serial->write(value); + charsSent++; + } + _serial->write(ETX); + return charsSent; // number of characters sent +} + +// Send request for input data to nominated CMRInode. +uint16_t CMRIbus::requestData(CMRInode *node) { + _serial->write(SYN); + _serial->write(SYN); + _serial->write(STX); + _serial->write(node->getAddress() + 65); + _serial->write('P'); // P for Poll message + _serial->write(ETX); + return 6; // number of characters sent +} + +// Send initialisation message +uint16_t CMRIbus::sendInitialisation(CMRInode *node) { + _serial->write(SYN); + _serial->write(SYN); + _serial->write(STX); + _serial->write(node->getAddress() + 65); + _serial->write('I'); // I for initialise message + _serial->write(node->getType()); // NDP + _serial->write((uint8_t)0); // dH + _serial->write((uint8_t)0); // dL + _serial->write((uint8_t)0); // NS + _serial->write(ETX); + return 10; // number of characters sent +} + void CMRIbus::processOutgoing() { + uint16_t charsSent = 0; if (_currentNode == NULL) { // If we're between read/write cycles then don't do anything else. if (_currentMicros - _cycleStartTime < _cycleTime) return; @@ -50,19 +136,25 @@ void CMRIbus::processOutgoing() { switch (_transmitState) { case TD_IDLE: case TD_INIT: + enableTransmitter(); if (!_currentNode->isInitialised()) { - sendInitialisation(_currentNode); + charsSent = sendInitialisation(_currentNode); _currentNode->setInitialised(); _transmitState = TD_TRANSMIT; + delayUntil(_currentMicros+_byteTransmitTime*charsSent); break; } /* fallthrough */ case TD_TRANSMIT: - sendData(_currentNode); + charsSent = sendData(_currentNode); _transmitState = TD_PROMPT; + // Defer next entry for as long as it takes to transmit the characters, + // to allow output queue to empty. + delayUntil(_currentMicros+_byteTransmitTime*charsSent); break; case TD_PROMPT: - requestData(_currentNode); + charsSent = requestData(_currentNode); + disableTransmitter(); _transmitState = TD_RECEIVE; _timeoutStart = _currentMicros; // Start timeout on response break; @@ -84,7 +176,7 @@ void CMRIbus::processIncoming() { int data = _serial->read(); if (data < 0) return; // No characters to read - if (!_currentNode) return; // Not waiting for input, so ignore. + if (_transmitState != TD_RECEIVE || !_currentNode) return; // Not waiting for input, so ignore. uint8_t nextState = RD_SYN1; // default to resetting state machine switch(_receiveState) { @@ -128,6 +220,40 @@ void CMRIbus::processIncoming() { _receiveState = nextState; } +// If configured for half duplex RS485, switch RS485 interface +// into transmit mode. +void CMRIbus::enableTransmitter() { + if (_transmitEnablePin != VPIN_NONE) + ArduinoPins::fastWriteDigital(_transmitEnablePin, 1); + // Send an extra SYN character to ensure transmitter and + // remote receiver have stabilised before we start the packet. + _serial->write(SYN); +} + +// If configured for half duplex RS485, switch RS485 interface +// into receive mode. +void CMRIbus::disableTransmitter() { + // Wait until all data has been transmitted. On the standard + // AVR driver, this waits until the FIFO is empty and all + // data has been sent over the link. + _serial->flush(); + // If we don't trust the 'flush' function and think the + // data's still in transit, then wait a bit longer. + if (_postDelay > 0) + delayMicroseconds(_postDelay); + // Hopefully, we can now safely switch off the transmitter. + if (_transmitEnablePin != VPIN_NONE) + ArduinoPins::fastWriteDigital(_transmitEnablePin, 0); +} + +// Link to chain of CMRI bus instances +CMRIbus *CMRIbus::_busList = NULL; + + +/************************************************************ + * CMRInode implementation + ************************************************************/ + // Constructor for CMRInode object CMRInode::CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, char type, uint16_t inputs, uint16_t outputs) { _firstVpin = firstVpin; @@ -173,5 +299,3 @@ CMRInode::CMRInode(VPIN firstVpin, int nPins, uint8_t busNo, uint8_t address, ch } } -// Link to chain of CMRI bus instances -CMRIbus *CMRIbus::_busList = NULL; diff --git a/IO_CMRI.h b/IO_CMRI.h index 2e379f5..2ca03ad 100644 --- a/IO_CMRI.h +++ b/IO_CMRI.h @@ -18,13 +18,14 @@ */ /* - * To define a CMRI bus, - * CMRIbus::create(bus, Serial3, 19200, cycletime); + * To define a CMRI bus, example syntax: + * CMRIbus::create(bus, serial, baud[, cycletime[, pin]]); * * bus = 0-255 - * Serial3 = serial port to be used - * 19200 = baud rate (min 9600, max 115200) - * cycletime = minimum time between successive updates/reads of a node in millisecs + * serial = serial port to be used (e.g. Serial3) + * baud = baud rate (9600, 19200, 28800, 57600 or 115200) + * cycletime = minimum time between successive updates/reads of a node in millisecs (default 500ms) + * pin = pin number connected to RS485 module's DE and !RE terminals for half-duplex operation (default VPIN_NONE) * * Each bus must use a different serial port. * @@ -36,8 +37,8 @@ * type = 'M' for SMINI (fixed 24 inputs and 48 outputs) * 'C' for CPNODE (16 to 144 inputs/outputs in groups of 8) * (other types are not supported at this time). - * inputs = number of inputs (CPNODE) - * outputs = number of outputs (CPNODE) + * inputs = number of inputs (CPNODE only) + * outputs = number of outputs (CPNODE only) * * Reference: "LCS-9.10.1 * Layout Control Specification: CMRInet Protocol @@ -168,10 +169,12 @@ private: uint8_t _busNo; HardwareSerial *_serial; unsigned long _baud; + VPIN _transmitEnablePin = VPIN_NONE; CMRInode *_nodeListStart = NULL, *_nodeListEnd = NULL; CMRInode *_currentNode = NULL; + // Transmitter state machine states - enum {TD_IDLE, TD_INIT, TD_TRANSMIT, TD_PROMPT, TD_RECEIVE}; + enum {TD_IDLE, TD_PRETRANSMIT, TD_INIT, TD_TRANSMIT, TD_PROMPT, TD_RECEIVE}; uint8_t _transmitState = TD_IDLE; // Receiver state machine states. enum {RD_SYN1, RD_SYN2, RD_STX, RD_ADDR, RD_TYPE, @@ -182,8 +185,10 @@ private: unsigned long _cycleStartTime = 0; unsigned long _timeoutStart = 0; unsigned long _cycleTime; // target time between successive read/write cycles, microseconds - uint32_t _timeoutPeriod; // timeout on read responses, in microseconds. + unsigned long _timeoutPeriod; // timeout on read responses, in microseconds. unsigned long _currentMicros; // last value of micros() from _loop function. + unsigned long _postDelay; // delay time after transmission before switching off transmitter (in us) + unsigned long _byteTransmitTime; // time in us for transmission of one byte static CMRIbus *_busList; // linked list of defined bus instances @@ -196,53 +201,23 @@ private: }; public: - static void create(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS = 500) { - new CMRIbus(busNo, serial, baud, cycleTimeMS); + static void create(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS=500, VPIN transmitEnablePin=VPIN_NONE) { + new CMRIbus(busNo, serial, baud, cycleTimeMS, transmitEnablePin); } - // Add new CMRInode to the list of nodes for this bus. - void addNode(CMRInode *newNode) { - if (!_nodeListStart) - _nodeListStart = newNode; - if (!_nodeListEnd) - _nodeListEnd = newNode; - else - _nodeListEnd->setNext(newNode); - } - -protected: - CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS) { - _busNo = busNo; - _serial = &serial; - _baud = baud; - _cycleTime = cycleTimeMS * 1000UL; // convert from milliseconds to microseconds. - - // Max message length is 256+6=262 bytes. - // Each byte is one start bit, 8 data bits and 1 stop bit = 10 bits per byte. - // Calculate timeout based on double this time. - _timeoutPeriod = 2 * 10 * 262 * 1000UL / (_baud / 1000); - //DIAG(F("Timeout=%l"), _timeoutPeriod); - - // Add device to HAL device chain - IODevice::addDevice(this); - - // Add bus to CMRIbus chain. - _nextBus = _busList; - _busList = this; - } - // Device-specific initialisation void _begin() override { // Some sources quote one stop bit, some two. _serial->begin(_baud, SERIAL_8N1); -#if defined(DIAG_IO) + #if defined(DIAG_IO) _display(); -#endif + #endif } - + + // Loop function (overriding IODevice::_loop(unsigned long)) void _loop(unsigned long currentMicros) override; - // Display information about the device. + // Display information about the device void _display() override { DIAG(F("CMRIbus %d configured, speed=%d baud, cycle=%d ms"), _busNo, _baud, _cycleTime/1000); } @@ -256,53 +231,31 @@ protected: return NULL; } - // Send output data to the bus for nominated CMRInode - bool sendData(CMRInode *node) { - uint16_t numDataBytes = (node->getNumOutputs()+7)/8; - _serial->write(SYN); - _serial->write(SYN); - _serial->write(STX); - _serial->write(node->getAddress() + 65); - _serial->write('T'); // T for Transmit data message - for (uint8_t index=0; indexgetOutputStates(index); - if (value == DLE || value == STX || value == ETX) _serial->write(DLE); - _serial->write(value); - } - _serial->write(ETX); - return true; + // Add new CMRInode to the list of nodes for this bus. + void addNode(CMRInode *newNode) { + if (!_nodeListStart) + _nodeListStart = newNode; + if (!_nodeListEnd) + _nodeListEnd = newNode; + else + _nodeListEnd->setNext(newNode); } - // Send request for input data to nominated CMRInode. - bool requestData(CMRInode *node) { - _serial->write(SYN); - _serial->write(SYN); - _serial->write(STX); - _serial->write(node->getAddress() + 65); - _serial->write('P'); // P for Poll message - _serial->write(ETX); - return true; - } - - bool sendInitialisation(CMRInode *node) { - _serial->write(SYN); - _serial->write(SYN); - _serial->write(STX); - _serial->write(node->getAddress() + 65); - _serial->write('I'); // I for initialise message - _serial->write(node->getType()); // NDP - _serial->write((uint8_t)0); // dH - _serial->write((uint8_t)0); // dL - _serial->write((uint8_t)0); // NS - _serial->write(ETX); - return true; - } +protected: + CMRIbus(uint8_t busNo, HardwareSerial &serial, unsigned long baud, uint16_t cycleTimeMS, VPIN transmitEnablePin); + uint16_t sendData(CMRInode *node); + uint16_t requestData(CMRInode *node); + uint16_t sendInitialisation(CMRInode *node); // Process any data bytes received from a CMRInode. void processIncoming(); - // Process any outgoing traffic that is due. void processOutgoing(); + // Enable transmitter + void enableTransmitter(); + // Disable transmitter and enable receiver + void disableTransmitter(); + public: uint8_t getBusNumber() {