mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2024-11-22 23:56:13 +01:00
468 lines
16 KiB
C++
468 lines
16 KiB
C++
/*
|
|
* © 2021 M Steve Todd
|
|
* © 2021 Mike S
|
|
* © 2021 Fred Decker
|
|
* © 2020-2021 Harald Barth
|
|
* © 2020-2022 Chris Harlow
|
|
* All rights reserved.
|
|
*
|
|
* This file is part of CommandStation-EX
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "DCCACK.h"
|
|
#include "DIAG.h"
|
|
#include "DCC.h"
|
|
#include "DCCWaveform.h"
|
|
#include "TrackManager.h"
|
|
|
|
unsigned int DCCACK::minAckPulseDuration = 2000; // micros
|
|
unsigned int DCCACK::maxAckPulseDuration = 20000; // micros
|
|
|
|
MotorDriver * DCCACK::progDriver=NULL;
|
|
ackOp const * DCCACK::ackManagerProg;
|
|
ackOp const * DCCACK::ackManagerProgStart;
|
|
byte DCCACK::ackManagerByte;
|
|
byte DCCACK::ackManagerByteVerify;
|
|
byte DCCACK::ackManagerStash;
|
|
int DCCACK::ackManagerWord;
|
|
byte DCCACK::ackManagerRetry;
|
|
byte DCCACK::ackRetry = 2;
|
|
int16_t DCCACK::ackRetrySum;
|
|
int16_t DCCACK::ackRetryPSum;
|
|
int DCCACK::ackManagerCv;
|
|
byte DCCACK::ackManagerBitNum;
|
|
bool DCCACK::ackReceived;
|
|
bool DCCACK::ackManagerRejoin;
|
|
volatile uint8_t DCCACK::numAckGaps=0;
|
|
volatile uint8_t DCCACK::numAckSamples=0;
|
|
uint8_t DCCACK::trailingEdgeCounter=0;
|
|
|
|
|
|
unsigned int DCCACK::ackPulseDuration; // micros
|
|
unsigned long DCCACK::ackPulseStart; // micros
|
|
volatile bool DCCACK::ackDetected;
|
|
unsigned long DCCACK::ackCheckStart; // millis
|
|
volatile bool DCCACK::ackPending;
|
|
bool DCCACK::autoPowerOff;
|
|
int DCCACK::ackThreshold;
|
|
int DCCACK::ackLimitmA = 50;
|
|
int DCCACK::ackMaxCurrent;
|
|
unsigned int DCCACK::ackCheckDuration; // millis
|
|
|
|
|
|
CALLBACK_STATE DCCACK::callbackState=READY;
|
|
|
|
ACK_CALLBACK DCCACK::ackManagerCallback;
|
|
|
|
void DCCACK::Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback) {
|
|
progDriver=TrackManager::getProgDriver();
|
|
if (progDriver==NULL) {
|
|
callback(-3); // we dont have a prog track!
|
|
return;
|
|
}
|
|
if (!progDriver->canMeasureCurrent()) {
|
|
callback(-2); // our prog track cant measure current
|
|
return;
|
|
}
|
|
|
|
ackManagerRejoin=TrackManager::isJoined();
|
|
if (ackManagerRejoin ) {
|
|
// Change from JOIN must zero resets packet.
|
|
TrackManager::setJoin(false);
|
|
DCCWaveform::progTrack.clearResets();
|
|
}
|
|
|
|
autoPowerOff=false;
|
|
if (progDriver->getPower() == POWERMODE::OFF) {
|
|
autoPowerOff=true; // power off afterwards
|
|
if (Diag::ACK) DIAG(F("Auto Prog power on"));
|
|
progDriver->setPower(POWERMODE::ON);
|
|
|
|
/* TODO !!! in MotorDriver surely!
|
|
if (MotorDriver::commonFaultPin)
|
|
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
|
|
DCCWaveform::progTrack.clearResets();
|
|
**/
|
|
}
|
|
|
|
|
|
ackManagerCv = cv;
|
|
ackManagerProg = program;
|
|
ackManagerProgStart = program;
|
|
ackManagerRetry = ackRetry;
|
|
ackManagerByte = byteValueOrBitnum;
|
|
ackManagerByteVerify = byteValueOrBitnum;
|
|
ackManagerBitNum=byteValueOrBitnum;
|
|
ackManagerCallback = callback;
|
|
}
|
|
|
|
void DCCACK::Setup(int wordval, ackOp const program[], ACK_CALLBACK callback) {
|
|
ackManagerWord=wordval;
|
|
Setup(0, 0, program, callback);
|
|
}
|
|
|
|
const byte RESET_MIN=8; // tuning of reset counter before sending message
|
|
|
|
// checkRessets return true if the caller should yield back to loop and try later.
|
|
bool DCCACK::checkResets(uint8_t numResets) {
|
|
return DCCWaveform::progTrack.getResets() < numResets;
|
|
}
|
|
// Operations applicable to PROG track ONLY.
|
|
// (yes I know I could have subclassed the main track but...)
|
|
|
|
void DCCACK::setAckBaseline() {
|
|
int baseline=progDriver->getCurrentRaw();
|
|
ackThreshold= baseline + progDriver->mA2raw(ackLimitmA);
|
|
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %uus and %uus"),
|
|
baseline,progDriver->raw2mA(baseline),
|
|
ackThreshold,progDriver->raw2mA(ackThreshold),
|
|
minAckPulseDuration, maxAckPulseDuration);
|
|
}
|
|
|
|
void DCCACK::setAckPending() {
|
|
ackMaxCurrent=0;
|
|
ackPulseStart=0;
|
|
ackPulseDuration=0;
|
|
ackDetected=false;
|
|
ackCheckStart=millis();
|
|
numAckSamples=0;
|
|
numAckGaps=0;
|
|
ackPending=true; // interrupt routines will now take note
|
|
}
|
|
|
|
byte DCCACK::getAck() {
|
|
if (ackPending) return (2); // still waiting
|
|
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%uuS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
|
|
ackMaxCurrent,progDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
|
|
if (ackDetected) return (1); // Yes we had an ack
|
|
return(0); // pending set off but not detected means no ACK.
|
|
}
|
|
|
|
|
|
void DCCACK::loop() {
|
|
while (ackManagerProg) {
|
|
byte opcode=GETFLASH(ackManagerProg);
|
|
|
|
// breaks from this switch will step to next prog entry
|
|
// returns from this switch will stay on same entry
|
|
// (typically waiting for a reset counter or ACK waiting, or when all finished.)
|
|
switch (opcode) {
|
|
case BASELINE:
|
|
if (progDriver->getPower()==POWERMODE::OVERLOAD) return;
|
|
if (checkResets(autoPowerOff || ackManagerRejoin ? 20 : 3)) return;
|
|
setAckBaseline();
|
|
callbackState=AFTER_READ;
|
|
break;
|
|
case W0: // write 0 bit
|
|
case W1: // write 1 bit
|
|
{
|
|
if (checkResets(RESET_MIN)) return;
|
|
if (Diag::ACK) DIAG(F("W%d cv=%d bit=%d"),opcode==W1, ackManagerCv,ackManagerBitNum);
|
|
byte instruction = WRITE_BIT | (opcode==W1 ? BIT_ON : BIT_OFF) | ackManagerBitNum;
|
|
byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
|
|
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
|
setAckPending();
|
|
callbackState=AFTER_WRITE;
|
|
}
|
|
break;
|
|
|
|
case WB: // write byte
|
|
{
|
|
if (checkResets( RESET_MIN)) return;
|
|
if (Diag::ACK) DIAG(F("WB cv=%d value=%d"),ackManagerCv,ackManagerByte);
|
|
byte message[] = {DCC::cv1(WRITE_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
|
|
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
|
setAckPending();
|
|
callbackState=AFTER_WRITE;
|
|
}
|
|
break;
|
|
|
|
case VB: // Issue validate Byte packet
|
|
{
|
|
if (checkResets( RESET_MIN)) return;
|
|
if (Diag::ACK) DIAG(F("VB cv=%d value=%d"),ackManagerCv,ackManagerByte);
|
|
byte message[] = { DCC::cv1(VERIFY_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
|
|
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
|
setAckPending();
|
|
}
|
|
break;
|
|
|
|
case V0:
|
|
case V1: // Issue validate bit=0 or bit=1 packet
|
|
{
|
|
if (checkResets(RESET_MIN)) return;
|
|
if (Diag::ACK) DIAG(F("V%d cv=%d bit=%d"),opcode==V1, ackManagerCv,ackManagerBitNum);
|
|
byte instruction = VERIFY_BIT | (opcode==V0?BIT_OFF:BIT_ON) | ackManagerBitNum;
|
|
byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
|
|
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
|
|
setAckPending();
|
|
}
|
|
break;
|
|
|
|
case WACK: // wait for ack (or absence of ack)
|
|
{
|
|
byte ackState=2; // keep polling
|
|
|
|
ackState=getAck();
|
|
if (ackState==2) return; // keep polling
|
|
ackReceived=ackState==1;
|
|
break; // we have a genuine ACK result
|
|
}
|
|
case ITC0:
|
|
case ITC1: // If True Callback(0 or 1) (if prevous WACK got an ACK)
|
|
if (ackReceived) {
|
|
callback(opcode==ITC0?0:1);
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case ITCB: // If True callback(byte)
|
|
if (ackReceived) {
|
|
callback(ackManagerByte);
|
|
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);
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case NAKFAIL: // If nack callback(-1)
|
|
if (!ackReceived) {
|
|
callback(-1);
|
|
return;
|
|
}
|
|
break;
|
|
|
|
case CALLFAIL: // callback(-1)
|
|
callback(-1);
|
|
return;
|
|
|
|
case BIV: // ackManagerByte initial value
|
|
ackManagerByte = ackManagerByteVerify;
|
|
break;
|
|
|
|
case STARTMERGE:
|
|
ackManagerBitNum=7;
|
|
ackManagerByte=0;
|
|
break;
|
|
|
|
case MERGE: // Merge previous Validate zero wack response with byte value and update bit number (use for reading CV bytes)
|
|
ackManagerByte <<= 1;
|
|
// ackReceived means bit is zero.
|
|
if (!ackReceived) ackManagerByte |= 1;
|
|
ackManagerBitNum--;
|
|
break;
|
|
|
|
case SETBIT:
|
|
ackManagerProg++;
|
|
ackManagerBitNum=GETFLASH(ackManagerProg);
|
|
break;
|
|
|
|
case SETCV:
|
|
ackManagerProg++;
|
|
ackManagerCv=GETFLASH(ackManagerProg);
|
|
break;
|
|
|
|
case SETBYTE:
|
|
ackManagerProg++;
|
|
ackManagerByte=GETFLASH(ackManagerProg);
|
|
break;
|
|
|
|
case SETBYTEH:
|
|
ackManagerByte=highByte(ackManagerWord);
|
|
break;
|
|
|
|
case SETBYTEL:
|
|
ackManagerByte=lowByte(ackManagerWord);
|
|
break;
|
|
|
|
case STASHLOCOID:
|
|
ackManagerStash=ackManagerByte; // stash value from CV17
|
|
break;
|
|
|
|
case COMBINELOCOID:
|
|
// ackManagerStash is cv17, ackManagerByte is CV 18
|
|
callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
|
|
return;
|
|
|
|
case ITSKIP:
|
|
if (!ackReceived) break;
|
|
// SKIP opcodes until SKIPTARGET found
|
|
while (opcode!=SKIPTARGET) {
|
|
ackManagerProg++;
|
|
opcode=GETFLASH(ackManagerProg);
|
|
}
|
|
break;
|
|
case SKIPTARGET:
|
|
break;
|
|
default:
|
|
DIAG(F("!! ackOp %d FAULT!!"),opcode);
|
|
callback( -1);
|
|
return;
|
|
|
|
} // end of switch
|
|
ackManagerProg++;
|
|
}
|
|
}
|
|
|
|
void DCCACK::callback(int value) {
|
|
// check for automatic retry
|
|
if (value == -1 && ackManagerRetry > 0) {
|
|
ackRetrySum ++;
|
|
LCD(0, F("Retry %d %d Sum=%d"), ackManagerCv, ackManagerRetry, ackRetrySum);
|
|
ackManagerRetry --;
|
|
ackManagerProg = ackManagerProgStart;
|
|
return;
|
|
}
|
|
|
|
static unsigned long callbackStart;
|
|
// We are about to leave programming mode
|
|
// Rule 1: If we have written to a decoder we must maintain power for 100mS
|
|
// Rule 2: If we are re-joining the main track we must power off for 30mS
|
|
|
|
switch (callbackState) {
|
|
case AFTER_READ:
|
|
if (ackManagerRejoin && autoPowerOff) {
|
|
progDriver->setPower(POWERMODE::OFF);
|
|
callbackStart=millis();
|
|
callbackState=WAITING_30;
|
|
if (Diag::ACK) DIAG(F("OFF 30mS"));
|
|
} else {
|
|
callbackState=READY;
|
|
}
|
|
break;
|
|
|
|
case AFTER_WRITE: // first attempt to callback after a write operation
|
|
if (!ackManagerRejoin && !autoPowerOff) {
|
|
callbackState=READY;
|
|
break;
|
|
} // lines 906-910 added. avoid wait after write. use 1 PROG
|
|
callbackStart=millis();
|
|
callbackState=WAITING_100;
|
|
if (Diag::ACK) DIAG(F("Stable 100mS"));
|
|
break;
|
|
|
|
case WAITING_100: // waiting for 100mS
|
|
if (millis()-callbackStart < 100) break;
|
|
// stable after power maintained for 100mS
|
|
|
|
// If we are going to power off anyway, it doesnt matter
|
|
// but if we will keep the power on, we must off it for 30mS
|
|
if (autoPowerOff) callbackState=READY;
|
|
else { // Need to cycle power off and on
|
|
progDriver->setPower(POWERMODE::OFF);
|
|
callbackStart=millis();
|
|
callbackState=WAITING_30;
|
|
if (Diag::ACK) DIAG(F("OFF 30mS"));
|
|
}
|
|
break;
|
|
|
|
case WAITING_30: // waiting for 30mS with power off
|
|
if (millis()-callbackStart < 30) break;
|
|
//power has been off for 30mS
|
|
progDriver->setPower(POWERMODE::ON);
|
|
callbackState=READY;
|
|
break;
|
|
|
|
case READY: // ready after read, or write after power delay and off period.
|
|
// power off if we powered it on
|
|
if (autoPowerOff) {
|
|
if (Diag::ACK) DIAG(F("Auto Prog power off"));
|
|
progDriver->setPower(POWERMODE::OFF);
|
|
/* TODO
|
|
if (MotorDriver::commonFaultPin)
|
|
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
|
|
**/
|
|
}
|
|
// Restore <1 JOIN> to state before BASELINE
|
|
if (ackManagerRejoin) {
|
|
TrackManager::setJoin(true);
|
|
if (Diag::ACK) DIAG(F("Auto JOIN"));
|
|
}
|
|
|
|
ackManagerProg=NULL; // no more steps to execute
|
|
if (Diag::ACK) DIAG(F("Callback(%d)"),value);
|
|
(ackManagerCallback)( value);
|
|
}
|
|
}
|
|
|
|
|
|
void DCCACK::checkAck(byte sentResetsSincePacket) {
|
|
if (!ackPending) return;
|
|
// This function operates in interrupt() time so must be fast and can't DIAG
|
|
if (sentResetsSincePacket > 6) { //ACK timeout
|
|
ackCheckDuration=millis()-ackCheckStart;
|
|
ackPending = false;
|
|
return;
|
|
}
|
|
|
|
int current=progDriver->getCurrentRawInInterrupt();
|
|
numAckSamples++;
|
|
if (current > ackMaxCurrent) ackMaxCurrent=current;
|
|
// An ACK is a pulse lasting between minAckPulseDuration and maxAckPulseDuration uSecs (refer @haba)
|
|
|
|
if (current>ackThreshold) {
|
|
if (trailingEdgeCounter > 0) {
|
|
numAckGaps++;
|
|
trailingEdgeCounter = 0;
|
|
}
|
|
if (ackPulseStart==0) ackPulseStart=micros(); // leading edge of pulse detected
|
|
return;
|
|
}
|
|
|
|
// not in pulse
|
|
if (ackPulseStart==0) return; // keep waiting for leading edge
|
|
|
|
// if we reach to this point, we have
|
|
// detected trailing edge of pulse
|
|
if (trailingEdgeCounter == 0) {
|
|
ackPulseDuration=micros()-ackPulseStart;
|
|
}
|
|
|
|
// but we do not trust it yet and return (which will force another
|
|
// measurement) and first the third time around with low current
|
|
// the ack detection will be finalized.
|
|
if (trailingEdgeCounter < 2) {
|
|
trailingEdgeCounter++;
|
|
return;
|
|
}
|
|
trailingEdgeCounter = 0;
|
|
|
|
if (ackPulseDuration>=minAckPulseDuration && ackPulseDuration<=maxAckPulseDuration) {
|
|
ackCheckDuration=millis()-ackCheckStart;
|
|
ackDetected=true;
|
|
ackPending=false;
|
|
DCCWaveform::progTrack.clearRepeats(); // shortcut remaining repeat packets
|
|
return; // we have a genuine ACK result
|
|
}
|
|
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
|
|
}
|
|
|