692 lines
22 KiB
C#
692 lines
22 KiB
C#
using BizHawk.Emulation.Cores.Components.Z80A;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
|
|
{
|
|
/*
|
|
* Much of the TAPE implementation has been taken from: https://github.com/Dotneteer/spectnetide
|
|
*
|
|
* MIT License
|
|
|
|
Copyright (c) 2017 Istvan Novak
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
*/
|
|
|
|
/// <summary>
|
|
/// Represents the tape device (or DATACORDER as AMSTRAD liked to call it)
|
|
/// </summary>
|
|
public class Tape
|
|
{
|
|
private SpectrumBase _machine { get; set; }
|
|
private Z80A _cpu { get; set; }
|
|
private Buzzer _buzzer { get; set; }
|
|
|
|
private TapeOperationMode _currentMode;
|
|
private TapeFilePlayer _tapePlayer;
|
|
private bool _micBitState;
|
|
private long _lastMicBitActivityCycle;
|
|
private SavePhase _savePhase;
|
|
private int _pilotPulseCount;
|
|
private int _bitOffset;
|
|
private byte _dataByte;
|
|
private int _dataLength;
|
|
private byte[] _dataBuffer;
|
|
private int _dataBlockCount;
|
|
private MicPulseType _prevDataPulse;
|
|
|
|
/// <summary>
|
|
/// Number of tacts after save mod can be exited automatically
|
|
/// </summary>
|
|
public const int SAVE_STOP_SILENCE = 17500000;
|
|
|
|
/// <summary>
|
|
/// The address of the ERROR routine in the Spectrum ROM
|
|
/// </summary>
|
|
public const ushort ERROR_ROM_ADDRESS = 0x0008;
|
|
|
|
/// <summary>
|
|
/// The maximum distance between two scans of the EAR bit
|
|
/// </summary>
|
|
public const int MAX_TACT_JUMP = 10000;
|
|
|
|
/// <summary>
|
|
/// The width tolerance of save pulses
|
|
/// </summary>
|
|
public const int SAVE_PULSE_TOLERANCE = 24;
|
|
|
|
/// <summary>
|
|
/// Minimum number of pilot pulses before SYNC1
|
|
/// </summary>
|
|
public const int MIN_PILOT_PULSE_COUNT = 3000;
|
|
|
|
/// <summary>
|
|
/// Lenght of the data buffer to allocate for the SAVE operation
|
|
/// </summary>
|
|
public const int DATA_BUFFER_LENGTH = 0x10000;
|
|
|
|
/// <summary>
|
|
/// Gets the TZX tape content provider
|
|
/// </summary>
|
|
public ITapeContentProvider ContentProvider { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the tape Save provider
|
|
/// </summary>
|
|
public ISaveToTapeProvider SaveToTapeProvider { get; }
|
|
|
|
/// <summary>
|
|
/// The TapeFilePlayer that can playback tape content
|
|
/// </summary>
|
|
public TapeFilePlayer TapeFilePlayer => _tapePlayer;
|
|
|
|
/// <summary>
|
|
/// The current operation mode of the tape
|
|
/// </summary>
|
|
public TapeOperationMode CurrentMode => _currentMode;
|
|
|
|
|
|
public virtual void Init(SpectrumBase machine)
|
|
{
|
|
_machine = machine;
|
|
_cpu = _machine.CPU;
|
|
_buzzer = machine.BuzzerDevice;
|
|
Reset();
|
|
}
|
|
|
|
public Tape(ITapeContentProvider contentProvider, ISaveToTapeProvider saveToTapeProvider)
|
|
{
|
|
ContentProvider = contentProvider;
|
|
SaveToTapeProvider = saveToTapeProvider;
|
|
}
|
|
|
|
public virtual void Reset()
|
|
{
|
|
ContentProvider?.Reset();
|
|
_tapePlayer = null;
|
|
_currentMode = TapeOperationMode.Passive;
|
|
_savePhase = SavePhase.None;
|
|
_micBitState = true;
|
|
}
|
|
|
|
public void CPUFrameCompleted()
|
|
{
|
|
SetTapeMode();
|
|
if (CurrentMode == TapeOperationMode.Load
|
|
//&& HostVm.ExecuteCycleOptions.FastTapeMode
|
|
&& TapeFilePlayer != null
|
|
&& TapeFilePlayer.PlayPhase != PlayPhase.Completed
|
|
&& _machine.Spectrum.Get16BitPC() == _machine.RomData.LoadBytesRoutineAddress)
|
|
{
|
|
/*
|
|
if (FastLoadFromTzx())
|
|
{
|
|
FastLoadCompleted?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the current tape mode according to the current PC register
|
|
/// and the MIC bit state
|
|
/// </summary>
|
|
public void SetTapeMode()
|
|
{
|
|
switch (_currentMode)
|
|
{
|
|
case TapeOperationMode.Passive:
|
|
if (_machine.Spectrum.Get16BitPC() == _machine.RomData.LoadBytesRoutineAddress)
|
|
{
|
|
EnterLoadMode();
|
|
}
|
|
else if (_machine.Spectrum.Get16BitPC() == _machine.RomData.SaveBytesRoutineAddress)
|
|
{
|
|
EnterSaveMode();
|
|
}
|
|
|
|
var res = _machine.Spectrum.Get16BitPC();
|
|
|
|
return;
|
|
case TapeOperationMode.Save:
|
|
if (_machine.Spectrum.Get16BitPC() == ERROR_ROM_ADDRESS
|
|
|| (int)(_cpu.TotalExecutedCycles - _lastMicBitActivityCycle) > SAVE_STOP_SILENCE)
|
|
{
|
|
LeaveSaveMode();
|
|
}
|
|
return;
|
|
case TapeOperationMode.Load:
|
|
if ((_tapePlayer?.Eof ?? false) || _machine.Spectrum.Get16BitPC() == ERROR_ROM_ADDRESS)
|
|
{
|
|
LeaveLoadMode();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Puts the device in save mode. From now on, every MIC pulse is recorded
|
|
/// </summary>
|
|
private void EnterSaveMode()
|
|
{
|
|
_currentMode = TapeOperationMode.Save;
|
|
_savePhase = SavePhase.None;
|
|
_micBitState = true;
|
|
_lastMicBitActivityCycle = _cpu.TotalExecutedCycles;
|
|
_pilotPulseCount = 0;
|
|
_prevDataPulse = MicPulseType.None;
|
|
_dataBlockCount = 0;
|
|
SaveToTapeProvider?.CreateTapeFile();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Leaves the save mode. Stops recording MIC pulses
|
|
/// </summary>
|
|
private void LeaveSaveMode()
|
|
{
|
|
_currentMode = TapeOperationMode.Passive;
|
|
SaveToTapeProvider?.FinalizeTapeFile();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Puts the device in load mode. From now on, EAR pulses are played by a device
|
|
/// </summary>
|
|
private void EnterLoadMode()
|
|
{
|
|
_currentMode = TapeOperationMode.Load;
|
|
|
|
var contentReader = ContentProvider?.GetTapeContent();
|
|
if (contentReader == null) return;
|
|
|
|
// --- Play the content
|
|
_tapePlayer = new TapeFilePlayer(contentReader);
|
|
_tapePlayer.ReadContent();
|
|
_tapePlayer.InitPlay(_cpu.TotalExecutedCycles);
|
|
_buzzer.SetTapeMode(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Leaves the load mode. Stops the device that playes EAR pulses
|
|
/// </summary>
|
|
private void LeaveLoadMode()
|
|
{
|
|
_currentMode = TapeOperationMode.Passive;
|
|
_tapePlayer = null;
|
|
ContentProvider?.Reset();
|
|
_buzzer.SetTapeMode(false);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// the EAR bit read from tape
|
|
/// </summary>
|
|
/// <param name="cpuCycles"></param>
|
|
/// <returns></returns>
|
|
public virtual bool GetEarBit(int cpuCycles)
|
|
{
|
|
if (_currentMode != TapeOperationMode.Load)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var earBit = _tapePlayer?.GetEarBit(cpuCycles) ?? true;
|
|
_buzzer.ProcessPulseValue(true, earBit);
|
|
return earBit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes the mic bit change
|
|
/// </summary>
|
|
/// <param name="micBit"></param>
|
|
public virtual void ProcessMicBit(bool micBit)
|
|
{
|
|
if (_currentMode != TapeOperationMode.Save
|
|
|| _micBitState == micBit)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var length = _cpu.TotalExecutedCycles - _lastMicBitActivityCycle;
|
|
|
|
// --- Classify the pulse by its width
|
|
var pulse = MicPulseType.None;
|
|
if (length >= TapeDataBlockPlayer.BIT_0_PL - SAVE_PULSE_TOLERANCE
|
|
&& length <= TapeDataBlockPlayer.BIT_0_PL + SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.Bit0;
|
|
}
|
|
else if (length >= TapeDataBlockPlayer.BIT_1_PL - SAVE_PULSE_TOLERANCE
|
|
&& length <= TapeDataBlockPlayer.BIT_1_PL + SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.Bit1;
|
|
}
|
|
if (length >= TapeDataBlockPlayer.PILOT_PL - SAVE_PULSE_TOLERANCE
|
|
&& length <= TapeDataBlockPlayer.PILOT_PL + SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.Pilot;
|
|
}
|
|
else if (length >= TapeDataBlockPlayer.SYNC_1_PL - SAVE_PULSE_TOLERANCE
|
|
&& length <= TapeDataBlockPlayer.SYNC_1_PL + SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.Sync1;
|
|
}
|
|
else if (length >= TapeDataBlockPlayer.SYNC_2_PL - SAVE_PULSE_TOLERANCE
|
|
&& length <= TapeDataBlockPlayer.SYNC_2_PL + SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.Sync2;
|
|
}
|
|
else if (length >= TapeDataBlockPlayer.TERM_SYNC - SAVE_PULSE_TOLERANCE
|
|
&& length <= TapeDataBlockPlayer.TERM_SYNC + SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.TermSync;
|
|
}
|
|
else if (length < TapeDataBlockPlayer.SYNC_1_PL - SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.TooShort;
|
|
}
|
|
else if (length > TapeDataBlockPlayer.PILOT_PL + 2 * SAVE_PULSE_TOLERANCE)
|
|
{
|
|
pulse = MicPulseType.TooLong;
|
|
}
|
|
|
|
_micBitState = micBit;
|
|
_lastMicBitActivityCycle = _cpu.TotalExecutedCycles;
|
|
|
|
// --- Lets process the pulse according to the current SAVE phase and pulse width
|
|
var nextPhase = SavePhase.Error;
|
|
switch (_savePhase)
|
|
{
|
|
case SavePhase.None:
|
|
if (pulse == MicPulseType.TooShort || pulse == MicPulseType.TooLong)
|
|
{
|
|
nextPhase = SavePhase.None;
|
|
}
|
|
else if (pulse == MicPulseType.Pilot)
|
|
{
|
|
_pilotPulseCount = 1;
|
|
nextPhase = SavePhase.Pilot;
|
|
}
|
|
break;
|
|
case SavePhase.Pilot:
|
|
if (pulse == MicPulseType.Pilot)
|
|
{
|
|
_pilotPulseCount++;
|
|
nextPhase = SavePhase.Pilot;
|
|
}
|
|
else if (pulse == MicPulseType.Sync1 && _pilotPulseCount >= MIN_PILOT_PULSE_COUNT)
|
|
{
|
|
nextPhase = SavePhase.Sync1;
|
|
}
|
|
break;
|
|
case SavePhase.Sync1:
|
|
if (pulse == MicPulseType.Sync2)
|
|
{
|
|
nextPhase = SavePhase.Sync2;
|
|
}
|
|
break;
|
|
case SavePhase.Sync2:
|
|
if (pulse == MicPulseType.Bit0 || pulse == MicPulseType.Bit1)
|
|
{
|
|
// --- Next pulse starts data, prepare for receiving it
|
|
_prevDataPulse = pulse;
|
|
nextPhase = SavePhase.Data;
|
|
_bitOffset = 0;
|
|
_dataByte = 0;
|
|
_dataLength = 0;
|
|
_dataBuffer = new byte[DATA_BUFFER_LENGTH];
|
|
}
|
|
break;
|
|
case SavePhase.Data:
|
|
if (pulse == MicPulseType.Bit0 || pulse == MicPulseType.Bit1)
|
|
{
|
|
if (_prevDataPulse == MicPulseType.None)
|
|
{
|
|
// --- We are waiting for the second half of the bit pulse
|
|
_prevDataPulse = pulse;
|
|
nextPhase = SavePhase.Data;
|
|
}
|
|
else if (_prevDataPulse == pulse)
|
|
{
|
|
// --- We received a full valid bit pulse
|
|
nextPhase = SavePhase.Data;
|
|
_prevDataPulse = MicPulseType.None;
|
|
|
|
// --- Add this bit to the received data
|
|
_bitOffset++;
|
|
_dataByte = (byte)(_dataByte * 2 + (pulse == MicPulseType.Bit0 ? 0 : 1));
|
|
if (_bitOffset == 8)
|
|
{
|
|
// --- We received a full byte
|
|
_dataBuffer[_dataLength++] = _dataByte;
|
|
_dataByte = 0;
|
|
_bitOffset = 0;
|
|
}
|
|
}
|
|
}
|
|
else if (pulse == MicPulseType.TermSync)
|
|
{
|
|
// --- We received the terminating pulse, the datablock has been completed
|
|
nextPhase = SavePhase.None;
|
|
_dataBlockCount++;
|
|
|
|
// --- Create and save the data block
|
|
var dataBlock = new TzxStandardSpeedDataBlock
|
|
{
|
|
Data = _dataBuffer,
|
|
DataLength = (ushort)_dataLength
|
|
};
|
|
|
|
// --- If this is the first data block, extract the name from the header
|
|
if (_dataBlockCount == 1 && _dataLength == 0x13)
|
|
{
|
|
// --- It's a header!
|
|
var sb = new StringBuilder(16);
|
|
for (var i = 2; i <= 11; i++)
|
|
{
|
|
sb.Append((char)_dataBuffer[i]);
|
|
}
|
|
var name = sb.ToString().TrimEnd();
|
|
SaveToTapeProvider?.SetName(name);
|
|
}
|
|
SaveToTapeProvider?.SaveTapeBlock(dataBlock);
|
|
}
|
|
break;
|
|
}
|
|
_savePhase = nextPhase;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This enum represents the operation mode of the tape
|
|
/// </summary>
|
|
public enum TapeOperationMode : byte
|
|
{
|
|
/// <summary>
|
|
/// The tape device is passive
|
|
/// </summary>
|
|
Passive = 0,
|
|
|
|
/// <summary>
|
|
/// The tape device is saving information (MIC pulses)
|
|
/// </summary>
|
|
Save,
|
|
|
|
/// <summary>
|
|
/// The tape device generates EAR pulses from a player
|
|
/// </summary>
|
|
Load
|
|
}
|
|
|
|
/// <summary>
|
|
/// This class represents a spectrum tape header
|
|
/// </summary>
|
|
public class SpectrumTapeHeader
|
|
{
|
|
private const int HEADER_LEN = 19;
|
|
private const int TYPE_OFFS = 1;
|
|
private const int NAME_OFFS = 2;
|
|
private const int NAME_LEN = 10;
|
|
private const int DATA_LEN_OFFS = 12;
|
|
private const int PAR1_OFFS = 14;
|
|
private const int PAR2_OFFS = 16;
|
|
private const int CHK_OFFS = 18;
|
|
|
|
/// <summary>
|
|
/// The bytes of the header
|
|
/// </summary>
|
|
public byte[] HeaderBytes { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="T:System.Object" /> class.
|
|
/// </summary>
|
|
public SpectrumTapeHeader()
|
|
{
|
|
HeaderBytes = new byte[HEADER_LEN];
|
|
for (var i = 0; i < HEADER_LEN; i++) HeaderBytes[i] = 0x00;
|
|
CalcChecksum();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance with the specified header data.
|
|
/// </summary>
|
|
/// <param name="header">Header data</param>
|
|
public SpectrumTapeHeader(byte[] header)
|
|
{
|
|
if (header == null) throw new ArgumentNullException(nameof(header));
|
|
if (header.Length != HEADER_LEN)
|
|
{
|
|
throw new ArgumentException($"Header must be exactly {HEADER_LEN} bytes long");
|
|
}
|
|
HeaderBytes = new byte[HEADER_LEN];
|
|
header.CopyTo(HeaderBytes, 0);
|
|
CalcChecksum();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the type of the header
|
|
/// </summary>
|
|
public byte Type
|
|
{
|
|
get { return HeaderBytes[TYPE_OFFS]; }
|
|
set
|
|
{
|
|
HeaderBytes[TYPE_OFFS] = (byte)(value & 0x03);
|
|
CalcChecksum();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the program name
|
|
/// </summary>
|
|
public string Name
|
|
{
|
|
get
|
|
{
|
|
var name = new StringBuilder(NAME_LEN + 4);
|
|
for (var i = NAME_OFFS; i < NAME_OFFS + NAME_LEN; i++)
|
|
{
|
|
name.Append((char)HeaderBytes[i]);
|
|
}
|
|
return name.ToString().TrimEnd();
|
|
}
|
|
set
|
|
{
|
|
if (value == null) throw new ArgumentNullException(nameof(value));
|
|
if (value.Length > NAME_LEN) value = value.Substring(0, NAME_LEN);
|
|
else if (value.Length < NAME_LEN) value = value.PadRight(NAME_LEN, ' ');
|
|
for (var i = NAME_OFFS; i < NAME_OFFS + NAME_LEN; i++)
|
|
{
|
|
HeaderBytes[i] = (byte)value[i - NAME_OFFS];
|
|
}
|
|
CalcChecksum();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Data Length
|
|
/// </summary>
|
|
public ushort DataLength
|
|
{
|
|
get { return GetWord(DATA_LEN_OFFS); }
|
|
set { SetWord(DATA_LEN_OFFS, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets Parameter1
|
|
/// </summary>
|
|
public ushort Parameter1
|
|
{
|
|
get { return GetWord(PAR1_OFFS); }
|
|
set { SetWord(PAR1_OFFS, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets Parameter2
|
|
/// </summary>
|
|
public ushort Parameter2
|
|
{
|
|
get { return GetWord(PAR2_OFFS); }
|
|
set { SetWord(PAR2_OFFS, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of checksum
|
|
/// </summary>
|
|
public byte Checksum => HeaderBytes[CHK_OFFS];
|
|
|
|
/// <summary>
|
|
/// Calculate the checksum
|
|
/// </summary>
|
|
private void CalcChecksum()
|
|
{
|
|
var chk = 0x00;
|
|
for (var i = 0; i < HEADER_LEN - 1; i++) chk ^= HeaderBytes[i];
|
|
HeaderBytes[CHK_OFFS] = (byte)chk;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the word value from the specified offset
|
|
/// </summary>
|
|
private ushort GetWord(int offset) =>
|
|
(ushort)(HeaderBytes[offset] + 256 * HeaderBytes[offset + 1]);
|
|
|
|
/// <summary>
|
|
/// Sets the word value at the specified offset
|
|
/// </summary>
|
|
private void SetWord(int offset, ushort value)
|
|
{
|
|
HeaderBytes[offset] = (byte)(value & 0xff);
|
|
HeaderBytes[offset + 1] = (byte)(value >> 8);
|
|
CalcChecksum();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This enum defines the MIC pulse types according to their widths
|
|
/// </summary>
|
|
public enum MicPulseType : byte
|
|
{
|
|
/// <summary>
|
|
/// No pulse information
|
|
/// </summary>
|
|
None = 0,
|
|
|
|
/// <summary>
|
|
/// Too short to be a valid pulse
|
|
/// </summary>
|
|
TooShort,
|
|
|
|
/// <summary>
|
|
/// Too long to be a valid pulse
|
|
/// </summary>
|
|
TooLong,
|
|
|
|
/// <summary>
|
|
/// PILOT pulse (Length: 2168 cycles)
|
|
/// </summary>
|
|
Pilot,
|
|
|
|
/// <summary>
|
|
/// SYNC1 pulse (Length: 667 cycles)
|
|
/// </summary>
|
|
Sync1,
|
|
|
|
/// <summary>
|
|
/// SYNC2 pulse (Length: 735 cycles)
|
|
/// </summary>
|
|
Sync2,
|
|
|
|
/// <summary>
|
|
/// BIT0 pulse (Length: 855 cycles)
|
|
/// </summary>
|
|
Bit0,
|
|
|
|
/// <summary>
|
|
/// BIT1 pulse (Length: 1710 cycles)
|
|
/// </summary>
|
|
Bit1,
|
|
|
|
/// <summary>
|
|
/// TERM_SYNC pulse (Length: 947 cycles)
|
|
/// </summary>
|
|
TermSync
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the playing phase of the current block
|
|
/// </summary>
|
|
public enum PlayPhase
|
|
{
|
|
/// <summary>
|
|
/// The player is passive
|
|
/// </summary>
|
|
None = 0,
|
|
|
|
/// <summary>
|
|
/// Pilot signals
|
|
/// </summary>
|
|
Pilot,
|
|
|
|
/// <summary>
|
|
/// Sync signals at the end of the pilot
|
|
/// </summary>
|
|
Sync,
|
|
|
|
/// <summary>
|
|
/// Bits in the data block
|
|
/// </summary>
|
|
Data,
|
|
|
|
/// <summary>
|
|
/// Short terminating sync signal before pause
|
|
/// </summary>
|
|
TermSync,
|
|
|
|
/// <summary>
|
|
/// Pause after the data block
|
|
/// </summary>
|
|
Pause,
|
|
|
|
/// <summary>
|
|
/// The entire block has been played back
|
|
/// </summary>
|
|
Completed
|
|
}
|
|
|
|
/// <summary>
|
|
/// This enumeration defines the phases of the SAVE operation
|
|
/// </summary>
|
|
public enum SavePhase : byte
|
|
{
|
|
/// <summary>No SAVE operation is in progress</summary>
|
|
None = 0,
|
|
|
|
/// <summary>Emitting PILOT impulses</summary>
|
|
Pilot,
|
|
|
|
/// <summary>Emitting SYNC1 impulse</summary>
|
|
Sync1,
|
|
|
|
/// <summary>Emitting SYNC2 impulse</summary>
|
|
Sync2,
|
|
|
|
/// <summary>Emitting BIT0/BIT1 impulses</summary>
|
|
Data,
|
|
|
|
/// <summary>Unexpected pulse detected</summary>
|
|
Error
|
|
}
|
|
}
|