mirror of
https://github.com/DCC-EX/CommandStation-EX.git
synced 2024-11-22 23:56:13 +01:00
355 lines
12 KiB
C++
355 lines
12 KiB
C++
/*
|
|
* © 2021 Neil McKechnie
|
|
* © 2021 Mike S
|
|
* © 2021 Fred Decker
|
|
* © 2020-2022 Harald Barth
|
|
* © 2020-2021 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/>.
|
|
*/
|
|
#ifndef ARDUINO_ARCH_ESP32
|
|
// This code is replaced entirely on an ESP32
|
|
#include <Arduino.h>
|
|
#include "DCCWaveform.h"
|
|
#include "TrackManager.h"
|
|
#include "DCCTimer.h"
|
|
#include "DCCACK.h"
|
|
#include "DIAG.h"
|
|
|
|
bool DCCWaveform::cutoutNextTime=false;
|
|
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
|
|
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
|
|
|
|
|
|
// This bitmask has 9 entries as each byte is trasmitted as a zero + 8 bits.
|
|
const byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
|
|
|
|
const byte idlePacket[] = {0xFF, 0x00, 0xFF};
|
|
const byte resetPacket[] = {0x00, 0x00, 0x00};
|
|
|
|
|
|
// For each state of the wave nextState=stateTransform[currentState]
|
|
const WAVE_STATE stateTransform[]={
|
|
/* WAVE_START -> */ WAVE_PENDING,
|
|
/* WAVE_MID_1 -> */ WAVE_START,
|
|
/* WAVE_HIGH_0 -> */ WAVE_MID_0,
|
|
/* WAVE_MID_0 -> */ WAVE_LOW_0,
|
|
/* WAVE_LOW_0 -> */ WAVE_START,
|
|
/* WAVE_PENDING (should not happen) -> */ WAVE_PENDING};
|
|
|
|
// For each state of the wave, signal pin is HIGH or LOW
|
|
const bool signalTransform[]={
|
|
/* WAVE_START -> */ HIGH,
|
|
/* WAVE_MID_1 -> */ LOW,
|
|
/* WAVE_HIGH_0 -> */ HIGH,
|
|
/* WAVE_MID_0 -> */ LOW,
|
|
/* WAVE_LOW_0 -> */ LOW,
|
|
/* WAVE_PENDING (should not happen) -> */ LOW};
|
|
|
|
void DCCWaveform::begin() {
|
|
DCCTimer::begin(DCCWaveform::interruptHandler);
|
|
}
|
|
|
|
void DCCWaveform::loop() {
|
|
// empty placemarker in case ESP32 needs something here
|
|
}
|
|
|
|
#pragma GCC push_options
|
|
#pragma GCC optimize ("-O3")
|
|
|
|
void DCCWaveform::interruptHandler() {
|
|
// call the timer edge sensitive actions for progtrack and maintrack
|
|
// member functions would be cleaner but have more overhead
|
|
if (cutoutNextTime) {
|
|
cutoutNextTime=false;
|
|
railcomSampleWindow=false; // about to cutout, stop reading railcom data.
|
|
DCCTimer::startRailcomTimer(9);
|
|
}
|
|
byte sigMain=signalTransform[mainTrack.state];
|
|
byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];
|
|
|
|
// Set the signal state for both tracks
|
|
TrackManager::setDCCSignal(sigMain);
|
|
TrackManager::setPROGSignal(sigProg);
|
|
|
|
// Refresh the values in the ADCee object buffering the values of the ADC HW
|
|
ADCee::scan();
|
|
|
|
// Move on in the state engine
|
|
mainTrack.state=stateTransform[mainTrack.state];
|
|
progTrack.state=stateTransform[progTrack.state];
|
|
|
|
// WAVE_PENDING means we dont yet know what the next bit is
|
|
if (mainTrack.state==WAVE_PENDING) mainTrack.interrupt2();
|
|
if (progTrack.state==WAVE_PENDING) progTrack.interrupt2();
|
|
else DCCACK::checkAck(progTrack.getResets());
|
|
|
|
}
|
|
#pragma GCC pop_options
|
|
|
|
// An instance of this class handles the DCC transmissions for one track. (main or prog)
|
|
// Interrupts are marshalled via the statics.
|
|
// A track has a current transmit buffer, and a pending buffer.
|
|
// When the current buffer is exhausted, either the pending buffer (if there is one waiting) or an idle buffer.
|
|
|
|
|
|
|
|
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
|
|
isMainTrack = isMain;
|
|
packetPending = false;
|
|
reminderWindowOpen = false;
|
|
memcpy(transmitPacket, idlePacket, sizeof(idlePacket));
|
|
state = WAVE_START;
|
|
// The +1 below is to allow the preamble generator to create the stop bit
|
|
// for the previous packet.
|
|
requiredPreambles = preambleBits+1;
|
|
bytes_sent = 0;
|
|
bits_sent = 0;
|
|
}
|
|
|
|
bool DCCWaveform::railcomPossible=false; // High accuracy only
|
|
volatile bool DCCWaveform::railcomActive=false; // switched on by user
|
|
volatile bool DCCWaveform::railcomDebug=false; // switched on by user
|
|
volatile bool DCCWaveform::railcomSampleWindow=false; // true during packet transmit
|
|
|
|
bool DCCWaveform::setRailcom(bool on, bool debug) {
|
|
if (on && railcomPossible) {
|
|
// TODO check possible
|
|
railcomActive=true;
|
|
railcomDebug=debug;
|
|
}
|
|
else {
|
|
railcomActive=false;
|
|
railcomDebug=false;
|
|
railcomSampleWindow=false;
|
|
}
|
|
return railcomActive;
|
|
}
|
|
|
|
#pragma GCC push_options
|
|
#pragma GCC optimize ("-O3")
|
|
void DCCWaveform::interrupt2() {
|
|
// calculate the next bit to be sent:
|
|
// set state WAVE_MID_1 for a 1=bit
|
|
// or WAVE_HIGH_0 for a 0 bit.
|
|
if (remainingPreambles > 0 ) {
|
|
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
|
|
|
|
remainingPreambles--;
|
|
|
|
// As we get to the end of the preambles, open the reminder window.
|
|
// This delays any reminder insertion until the last moment so
|
|
// that the reminder doesn't block a more urgent packet.
|
|
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<10 && remainingPreambles>1;
|
|
if (remainingPreambles==1)
|
|
promotePendingPacket();
|
|
else if (isMainTrack && railcomActive) {
|
|
if (remainingPreambles==(requiredPreambles-1)) {
|
|
// First look if we need to start a railcom cutout on next interrupt
|
|
cutoutNextTime= true;
|
|
} else if (remainingPreambles==(requiredPreambles-12)) {
|
|
// cutout has ended so its now possible to poll the railcom detectors
|
|
// requiredPreambles is one higher that preamble length so
|
|
// if preamble length is 16 then this evaluates to 5
|
|
railcomSampleWindow=true;
|
|
} else if (remainingPreambles==(requiredPreambles-3)) {
|
|
// cutout can be ended when read
|
|
// see above for requiredPreambles
|
|
DCCTimer::ackRailcomTimer();
|
|
}
|
|
}
|
|
// Update free memory diagnostic as we don't have anything else to do this time.
|
|
// Allow for checkAck and its called functions using 22 bytes more.
|
|
else DCCTimer::updateMinimumFreeMemoryISR(22);
|
|
return;
|
|
}
|
|
|
|
// Wave has gone HIGH but what happens next depends on the bit to be transmitted
|
|
// beware OF 9-BIT MASK generating a zero to start each byte
|
|
state=(transmitPacket[bytes_sent] & bitMask[bits_sent])? WAVE_MID_1 : WAVE_HIGH_0;
|
|
bits_sent++;
|
|
|
|
// If this is the last bit of a byte, prepare for the next byte
|
|
|
|
if (bits_sent == 9) { // zero followed by 8 bits of a byte
|
|
//end of Byte
|
|
bits_sent = 0;
|
|
bytes_sent++;
|
|
// if this is the last byte, prepere for next packet
|
|
if (bytes_sent >= transmitLength) {
|
|
// end of transmission buffer... repeat or switch to next message
|
|
bytes_sent = 0;
|
|
// preamble for next packet will start...
|
|
remainingPreambles = requiredPreambles;
|
|
}
|
|
}
|
|
}
|
|
#pragma GCC pop_options
|
|
|
|
// Wait until there is no packet pending, then make this pending
|
|
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
|
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
|
|
while (packetPending);
|
|
|
|
byte checksum = 0;
|
|
for (byte b = 0; b < byteCount; b++) {
|
|
checksum ^= buffer[b];
|
|
pendingPacket[b] = buffer[b];
|
|
}
|
|
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
|
|
pendingPacket[byteCount] = checksum;
|
|
pendingLength = byteCount + 1;
|
|
pendingRepeats = repeats;
|
|
packetPending = true;
|
|
clearResets();
|
|
}
|
|
|
|
bool DCCWaveform::isReminderWindowOpen() {
|
|
return reminderWindowOpen && ! packetPending;
|
|
}
|
|
|
|
void DCCWaveform::promotePendingPacket() {
|
|
// fill the transmission packet from the pending packet
|
|
|
|
// Just keep going if repeating
|
|
if (transmitRepeats > 0) {
|
|
transmitRepeats--;
|
|
return;
|
|
}
|
|
|
|
if (packetPending) {
|
|
// Copy pending packet to transmit packet
|
|
// a fixed length memcpy is faster than a variable length loop for these small lengths
|
|
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
|
|
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
|
|
|
|
transmitLength = pendingLength;
|
|
transmitRepeats = pendingRepeats;
|
|
packetPending = false;
|
|
clearResets();
|
|
return;
|
|
}
|
|
|
|
// nothing to do, just send idles or resets
|
|
// Fortunately reset and idle packets are the same length
|
|
// Note: If railcomDebug is on, then we send resets to the main
|
|
// track instead of idles. This means that all data will be zeros
|
|
// and only the presets will be ones, making it much
|
|
// easier to read on a logic analyser.
|
|
memcpy( transmitPacket, (isMainTrack && (!railcomDebug)) ? idlePacket : resetPacket, sizeof(idlePacket));
|
|
transmitLength = sizeof(idlePacket);
|
|
transmitRepeats = 0;
|
|
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
|
|
}
|
|
#endif //not ARDUINO_ARCH_ESP32
|
|
|
|
#ifdef ARDUINO_ARCH_ESP32
|
|
#include "DCCWaveform.h"
|
|
#include "TrackManager.h"
|
|
#include "DCCACK.h"
|
|
#include "Pinpair.h"
|
|
|
|
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
|
|
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
|
|
RMTChannel *DCCWaveform::rmtMainChannel = NULL;
|
|
RMTChannel *DCCWaveform::rmtProgChannel = NULL;
|
|
volatile bool DCCWaveform::railcomSampleWindow=false; // true during packet transmit
|
|
|
|
|
|
DCCWaveform::DCCWaveform(byte preambleBits, bool isMain) {
|
|
isMainTrack = isMain;
|
|
requiredPreambles = preambleBits;
|
|
}
|
|
void DCCWaveform::begin() {
|
|
for(const auto& md: TrackManager::getMainDrivers()) {
|
|
Pinpair p = md->getSignalPin();
|
|
if(rmtMainChannel) {
|
|
//DIAG(F("added pins %d %d to MAIN channel"), p.pin, p.invpin);
|
|
rmtMainChannel->addPin(p); // add pin to existing main channel
|
|
} else {
|
|
//DIAG(F("new MAIN channel with pins %d %d"), p.pin, p.invpin);
|
|
rmtMainChannel = new RMTChannel(p, true); /* create new main channel */
|
|
}
|
|
}
|
|
MotorDriver *md = TrackManager::getProgDriver();
|
|
if (md) {
|
|
Pinpair p = md->getSignalPin();
|
|
if (rmtProgChannel) {
|
|
//DIAG(F("added pins %d %d to PROG channel"), p.pin, p.invpin);
|
|
rmtProgChannel->addPin(p); // add pin to existing prog channel
|
|
} else {
|
|
//DIAG(F("new PROGchannel with pins %d %d"), p.pin, p.invpin);
|
|
rmtProgChannel = new RMTChannel(p, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
|
|
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
|
|
|
|
byte checksum = 0;
|
|
for (byte b = 0; b < byteCount; b++) {
|
|
checksum ^= buffer[b];
|
|
pendingPacket[b] = buffer[b];
|
|
}
|
|
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
|
|
pendingPacket[byteCount] = checksum;
|
|
pendingLength = byteCount + 1;
|
|
pendingRepeats = repeats;
|
|
// DIAG repeated commands (accesories)
|
|
// if (pendingRepeats > 0)
|
|
// DIAG(F("Repeats=%d on %s track"), pendingRepeats, isMainTrack ? "MAIN" : "PROG");
|
|
// The resets will be zero not only now but as well repeats packets into the future
|
|
clearResets(repeats+1);
|
|
{
|
|
int ret = 0;
|
|
do {
|
|
if(isMainTrack) {
|
|
if (rmtMainChannel != NULL)
|
|
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
|
} else {
|
|
if (rmtProgChannel != NULL)
|
|
ret = rmtProgChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
|
|
}
|
|
} while(ret > 0);
|
|
}
|
|
}
|
|
|
|
bool DCCWaveform::isReminderWindowOpen() {
|
|
if(isMainTrack) {
|
|
if (rmtMainChannel == NULL)
|
|
return false;
|
|
return !rmtMainChannel->busy();
|
|
} else {
|
|
if (rmtProgChannel == NULL)
|
|
return false;
|
|
return !rmtProgChannel->busy();
|
|
}
|
|
}
|
|
void IRAM_ATTR DCCWaveform::loop() {
|
|
DCCACK::checkAck(progTrack.getResets());
|
|
}
|
|
|
|
bool DCCWaveform::setRailcom(bool on, bool debug) {
|
|
// todo
|
|
(void)on;
|
|
(void)debug;
|
|
return false;
|
|
}
|
|
|
|
#endif
|