595 lines
21 KiB
C#
595 lines
21 KiB
C#
///////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Microsoft Research Singularity
|
|
//
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
//
|
|
// Useful reference URLs:
|
|
// http://developer.intel.com/design/archives/periphrl/index.htm
|
|
// http://developer.intel.com/design/archives/periphrl/docs/7203.htm
|
|
// http://developer.intel.com/design/archives/periphrl/docs/23124406.htm
|
|
//
|
|
// The basic ideas for this driver come from the MMOSA code,
|
|
// though the implementation differs. This is partly because
|
|
// the code needs to run on Virtual PC and it isn't able to do
|
|
// a very accurate emulation of the i8254.
|
|
//
|
|
// There are two source available for timing - the Real-Time
|
|
// Clock (RTC) and the programmable interval timer (PIT). The
|
|
// standard PC RTC is based on derivatives of the Motorola
|
|
// MC146818A. It's able to provide the time with a resolution
|
|
// of 1 second and also has a programmable periodic interrupt.
|
|
//
|
|
// The programmable interrupt timer is based on the i8254. It
|
|
// can be programmed in a variety of modes - we use it to
|
|
// generate an interrupt at a configurable time in the future
|
|
// and then reprogram it each interrupt. The maximum interrupt
|
|
// period is 65535 ticks of a 1.193MHz clock.
|
|
//
|
|
// We use both of the RTC and the programmable interrupt timer to
|
|
// maintain our estimate of the current time. The RTC provides granularity
|
|
// to with 1/64 seconds and the time is used to get an estimate to within
|
|
// 1/1.193 * 10e-6 seconds within each RTC interval.
|
|
//
|
|
// The key variables are:
|
|
//
|
|
// upTime - the time the system has been up. This variable gets
|
|
// updated during the periodic RTC interrupt handling
|
|
// (delta = 1/64Hz).
|
|
//
|
|
// pitLast - the last value programmed into the PIT. The PIT counts down
|
|
// and generates an interrupt at (or shortly after) the instant
|
|
// the current PIT value reaches zero.
|
|
//
|
|
// pitAccum - the accumulated time measured by the PIT since upTime
|
|
// was updated.
|
|
//
|
|
// The current kernel time is always:
|
|
// upTime + pitAccum + (pitLast - pitCurrent)
|
|
//
|
|
// The PIT is always programmed to run, either by the consumer of the timer
|
|
// interface or by the timer itself.
|
|
//
|
|
// Timer::SetNextInterrupt(t)
|
|
// pitAccum += (pitLast - pitCurrent)
|
|
// // program PIT (not shown)
|
|
// pitLast = t
|
|
//
|
|
// Timer::Interrupt()
|
|
// pitAccum += pitLast;
|
|
// // But PIT time may accumulate between interrupt dispatch and crossing
|
|
// // Zero so.
|
|
// if (pitCurrent != 0)
|
|
// pitAccum += (MaxPitValue - pitCurrent)
|
|
// // Inform user of interrupt
|
|
// if (userNotScheduledInterrupt)
|
|
// SetNextInterrupt(MaxInterruptInterval)
|
|
//
|
|
// RTC::Interrupt()
|
|
// pitLast = pitNow
|
|
// pitAccum = 0
|
|
// upTime += RTCInterruptPeriod
|
|
//
|
|
// All of these methods are atomic interrupt-wise.
|
|
//
|
|
// Note: if we want to test the accuracy of the timer over a
|
|
// period we can set RTC::Interrupt to just return without
|
|
// touching any variables. All of the time accumulated will end
|
|
// up in pitAccum.
|
|
//
|
|
// Conditionals
|
|
//
|
|
// TIMER_NO_GO - disables timer and scheduling of timer interrupts.
|
|
//
|
|
// RTC_NO_GO - disable RTC.
|
|
//
|
|
// DEBUG_TIMER - enable timer debug messages and boot-time diagnostics.
|
|
//
|
|
// DEBUG_CLOCK - enable clock debug messages
|
|
//
|
|
// LOG_CLOCK - log adjustments to clock time and dump out later.
|
|
//
|
|
// LOG_SNI - log calls to SetNextInterrupt to see what's being thrown in.
|
|
//
|
|
// Tip: When this code does not behave useful things to check
|
|
// are the interrupt rate and the rate of calls to
|
|
// SetNextInterrupt.
|
|
//
|
|
|
|
// #define VERBOSE
|
|
|
|
using Microsoft.Singularity.Io;
|
|
|
|
using System;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
|
|
using Microsoft.Singularity.Configuration;
|
|
|
|
namespace Microsoft.Singularity.Hal
|
|
{
|
|
// declare resources for the kernel manifest
|
|
[DriverCategory]
|
|
[Signature("/pnp/PNP0100")]
|
|
public sealed class TimerResourcesLegacyPC : DriverCategoryDeclaration
|
|
{
|
|
[IoPortRange(0, Default = 0x40, Length = 0x04)]
|
|
public IoPortRange port1;
|
|
|
|
[IoIrqRange(1, Default = 0x00, Length = 0x01)]
|
|
public IoIrqRange irq1;
|
|
|
|
[IoFixedPortRange(Base = 0x0061, Length = 0x01)]
|
|
public IoPortRange port2;
|
|
}
|
|
|
|
//
|
|
// Note on the i8254 timer:
|
|
//
|
|
// The clock is driven by a 14.318 MHz clock through a divide-by-12
|
|
// counter.
|
|
// This gives a clock frequency of 1.193 MHz.
|
|
// Each tick is therefore 0.838 microseconds long.
|
|
// Thus a 16 bit timer gives us a maximum delay of 0.05 seconds.
|
|
//
|
|
[CLSCompliant(false)]
|
|
public sealed class Timer8254LegacyPC : HalTimer
|
|
{
|
|
// Registers
|
|
const byte i8254_C0 = 0x00; // counter 0
|
|
const byte i8254_C1 = 0x01; // counter 1
|
|
const byte i8254_C2 = 0x02; // counter 2
|
|
const byte i8254_CW = 0x03; // control word
|
|
|
|
// The control word to the 8254 is composed of four fields:
|
|
// bits 6-7: select the counter
|
|
// bits 4-5: select read/write
|
|
// bits 1-3: mode to use
|
|
// bit 0 : BCD mode
|
|
|
|
// bits 6-7 select the counter.
|
|
const byte i8254_CW_SEL0 = 0x00; // select counter 0
|
|
const byte i8254_CW_SEL1 = 0x40; // select counter 1
|
|
const byte i8254_CW_SEL2 = 0x80; // select counter 2
|
|
const byte i8254_CW_RBC = 0xc0; // read-back command
|
|
|
|
// bits 4-5 select transaction type.
|
|
const byte i8254_CW_CLC = 0x00; // counter latch comm.
|
|
const byte i8254_CW_LSB = 0x10; // r/w lsb only
|
|
const byte i8254_CW_MSB = 0x20; // r/w msb only
|
|
const byte i8254_CW_BOTH = 0x30; // r/w lsb, then msb
|
|
|
|
// bits 1-3 select the mode. bit 3 is sometimes a don't care.
|
|
const byte i8254_CW_MODE0 = 0x00; // int. on term. count
|
|
const byte i8254_CW_MODE1 = 0x02; // h/w retrig. one-shot
|
|
const byte i8254_CW_MODE2 = 0x04; // rate generator
|
|
const byte i8254_CW_MODE3 = 0x06; // square wave mode
|
|
const byte i8254_CW_MODE4 = 0x08; // s/w trig. strobe
|
|
const byte i8254_CW_MODE5 = 0x0a; // h/w trig. strobe
|
|
|
|
// bit 0 sets BCD mode, if you really must.
|
|
const byte i8254_CW_BCD = 0x01; // set BCD operation
|
|
|
|
// read-back commands use bits 4 and 5 to return status these
|
|
// are the wrong way round since the bits are inverted (RTFDS).
|
|
//
|
|
const byte i8254_RB_NOSTATUS = 0xd0; // Don't get latched count
|
|
const byte i8254_RB_NOCOUNT = 0xe0; // Don't get status
|
|
const byte i8254_RB_ALL = 0xc0; // Get status and count
|
|
|
|
// read-back commands use bits 3-1 to select counter
|
|
const byte i8254_RB_SEL0 = 0x02; // counter 0
|
|
const byte i8254_RB_SEL1 = 0x04; // counter 1
|
|
const byte i8254_RB_SEL2 = 0x08; // counter 2
|
|
|
|
// status from read-back is returned in bits 6-7
|
|
const byte i8254_RB_OUT = 0x80; // out pin value
|
|
const byte i8254_RB_NULL = 0x40; // 1 = count null
|
|
|
|
private PnpConfig config;
|
|
private Pic pic;
|
|
private byte irq;
|
|
|
|
// IOPorts.
|
|
private IoPort C0Port;
|
|
private IoPort C2Port;
|
|
private IoPort CWPort;
|
|
|
|
internal static readonly int PitInterruptThreshold = 0xffff;
|
|
internal static readonly int PitMaxWrite = 0xffff;
|
|
|
|
// Timer State.
|
|
private volatile bool interruptPending = false;
|
|
|
|
internal volatile uint irqCount = 0;
|
|
internal RtcPitState rps;
|
|
|
|
internal void SetTicksPerSecond(int count)
|
|
{
|
|
#if DYNAMIC_TICKS
|
|
ticksPerSecond = count;
|
|
unchecked {
|
|
Tracing.Log(Tracing.Audit, "i8254 Frequency {0} Hz.",
|
|
(UIntPtr)(uint)ticksPerSecond);
|
|
}
|
|
#endif // DYNAMIC_TICKS
|
|
}
|
|
|
|
// Constructor
|
|
internal Timer8254LegacyPC(PnpConfig config, Pic pic)
|
|
{
|
|
#if VERBOSE
|
|
Tracing.Log(Tracing.Debug, "Timer8254: create");
|
|
#endif
|
|
|
|
// /pnp/08/02/01/PNP0100 0003 cfg dis : ISA 8254 Timer : AT Timer
|
|
// IRQ mask=0001 type=47
|
|
// I/O Port inf=01 min=0040 max=0040 aln=01 len=04 0040..0043
|
|
this.config = config;
|
|
this.pic = pic;
|
|
this.irq = ((IoIrqRange)config.DynamicRanges[1]).Irq;
|
|
IoPortRange ioPorts = (IoPortRange) config.DynamicRanges[0];
|
|
C0Port = ioPorts.PortAtOffset(i8254_C0, 1, Access.ReadWrite);
|
|
C2Port = ioPorts.PortAtOffset(i8254_C2, 1, Access.ReadWrite);
|
|
CWPort = ioPorts.PortAtOffset(i8254_CW, 1, Access.ReadWrite);
|
|
|
|
// Enable Timer2
|
|
// Lower 2 bits of port 61 are described in:
|
|
// The Indispensable PC Hardware Book (3rd Edition) p.724
|
|
ioPorts = (IoPortRange) config.FixedRanges[0];
|
|
IoPort p = ioPorts.PortAtOffset(0, 1, Access.ReadWrite);
|
|
|
|
//
|
|
// Also clear the NMI RAM parity error to enable the PCI card
|
|
// to generate NMI if the button is depressed
|
|
//
|
|
|
|
p.Write8((byte) ((p.Read8() & 0xf8) | 0x01));
|
|
}
|
|
|
|
public byte Initialize()
|
|
{
|
|
#if VERBOSE
|
|
Tracing.Log(Tracing.Debug, "Timer.Initialize()");
|
|
#endif
|
|
|
|
rps = new RtcPitState();
|
|
Stop();
|
|
DoPitPerformanceTests();
|
|
|
|
pic.EnableIrq(irq);
|
|
return pic.IrqToInterrupt(irq);
|
|
}
|
|
|
|
public void ReleaseResources()
|
|
{
|
|
pic.DisableIrq(irq);
|
|
Stop();
|
|
}
|
|
|
|
internal void Start()
|
|
{
|
|
#if !TIMER_NO_GO
|
|
bool iflag = Processor.DisableInterrupts();
|
|
try {
|
|
PitWrite(0xffff);
|
|
Timer2Start();
|
|
SetKeepAliveInterrupt();
|
|
}
|
|
finally {
|
|
Processor.RestoreInterrupts(iflag);
|
|
}
|
|
#endif // TIMER_NO_GO
|
|
}
|
|
|
|
internal void Stop()
|
|
{
|
|
// Half-program counter to stop counter running. Trick copied
|
|
// from MMOSA i386 timer code
|
|
CWPort.Write8(i8254_CW_MODE0 | i8254_CW_BOTH | i8254_CW_SEL0);
|
|
C0Port.Write8((byte)(0)); // LSB
|
|
// C0Port.Write8((byte)(0)); MSB DON'T!
|
|
}
|
|
|
|
internal void Timer2Start()
|
|
{
|
|
CWPort.Write8(i8254_CW_MODE2 | i8254_CW_BOTH | i8254_CW_SEL2);
|
|
C2Port.Write8((byte)0xff);
|
|
C2Port.Write8((byte)0xff);
|
|
}
|
|
|
|
[NoHeapAllocation]
|
|
internal static int TimeSpanTicksToPitTicks(int ts)
|
|
{
|
|
// We should calculate scaling
|
|
// factors based on ticksPerSecond.
|
|
|
|
// Ticks = ts * 1193182 / 10000000
|
|
// = ts * (0x1e/0x100 + 0x8ba/0x100000)
|
|
DebugStub.Assert(ts < 960000); // Limit for 0x8ba factor
|
|
return ((ts * 0x1e) >> 8) + ((ts * 0x8ba) >> 20);
|
|
}
|
|
|
|
[NoHeapAllocation]
|
|
internal static int PitTicksToTimeSpanTicks(int pit)
|
|
{
|
|
// We should calculate scaling
|
|
// factors based on ticksPerSecond.
|
|
|
|
// Ticks = pit * 10000000 / 1193182
|
|
// = pit * (0x86 / 0x10) + (0x186 / 0x10000)
|
|
DebugStub.Assert(pit < 5500000); // Limit for 0x186 factor
|
|
return ((pit * 0x86) >> 4) + ((pit * 0x186) >> 16);
|
|
}
|
|
|
|
private readonly TimeSpan maxInterruptInterval = new TimeSpan(500000); // 50ms
|
|
private readonly TimeSpan minInterruptInterval = new TimeSpan(1000); // 1ms
|
|
private readonly TimeSpan interruptGranularity = new TimeSpan(17); // 1.7us
|
|
|
|
/// <value>
|
|
/// Maximum value accepted by SetNextInterrupt (in units of 100ns).
|
|
/// </value>
|
|
public override TimeSpan MaxInterruptInterval
|
|
{
|
|
[NoHeapAllocation]
|
|
get { return maxInterruptInterval; }
|
|
}
|
|
|
|
/// <value>
|
|
/// Minimum value accepted by SetNextInterrupt (in units of 100ns).
|
|
/// </value>
|
|
public override TimeSpan MinInterruptInterval
|
|
{
|
|
[NoHeapAllocation]
|
|
get { return minInterruptInterval; }
|
|
}
|
|
|
|
[NoHeapAllocation]
|
|
public override void SetNextInterrupt(TimeSpan delta)
|
|
{
|
|
DebugStub.Assert(Processor.InterruptsDisabled());
|
|
DebugStub.Assert(delta >= minInterruptInterval);
|
|
DebugStub.Assert(delta <= maxInterruptInterval);
|
|
|
|
#if TIMER_NO_GO
|
|
return;
|
|
#endif
|
|
ClockLogger.AddEntry(2, rps, this, delta.Ticks);
|
|
|
|
PitWrite(TimeSpanTicksToPitTicks((int)delta.Ticks));
|
|
this.interruptPending = true;
|
|
|
|
ClockLogger.AddEntry(3, rps, this, delta.Ticks);
|
|
}
|
|
|
|
/// <remarks> Set timer interrupt to start or keep timer running.
|
|
/// The user is expected to call SetNextInterrupt regularly during
|
|
/// normal operation. This method is invoked at start-up and if
|
|
/// we find the timer interrupt is not programmed during the clock
|
|
/// interrupt.
|
|
/// </remarks>
|
|
[NoHeapAllocation]
|
|
internal void SetKeepAliveInterrupt()
|
|
{
|
|
ClockLogger.AddEntry(0, rps, this);
|
|
PitWrite(PitMaxWrite);
|
|
ClockLogger.AddEntry(1, rps, this);
|
|
this.interruptPending = true;
|
|
}
|
|
|
|
internal bool InterruptPending
|
|
{
|
|
[NoHeapAllocation]
|
|
get { return interruptPending; }
|
|
}
|
|
|
|
[NoHeapAllocation]
|
|
public override void ClearInterrupt()
|
|
{
|
|
#if VERBOSE
|
|
Tracing.Log(Tracing.Debug, "Timer.ClearInterrupt()");
|
|
#endif
|
|
#if TIMER_NO_GO
|
|
Tracing.Log(Tracing.Notice, "Unwanted timer interrupt!");
|
|
#endif // TIMER_NO_GO
|
|
|
|
ClockLogger.AddEntry(10, rps, this);
|
|
|
|
long currentTime = rps.GetKernelTicks(Timer2Read());
|
|
|
|
this.interruptPending = false;
|
|
|
|
irqCount++;
|
|
ClockLogger.AddEntry(11, rps, this);
|
|
pic.AckIrq(irq);
|
|
}
|
|
|
|
[NoHeapAllocation]
|
|
private void PitAccessError()
|
|
{
|
|
DebugStub.Break();
|
|
}
|
|
|
|
|
|
[NoHeapAllocation]
|
|
internal void PitWrite(int pt)
|
|
{
|
|
#if TIMER_NO_GO
|
|
return;
|
|
#endif // TIMER_NO_GO
|
|
IoResult result;
|
|
|
|
result = CWPort.Write8NoThrow(i8254_CW_MODE0 | i8254_CW_BOTH | i8254_CW_SEL0);
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
|
|
result = C0Port.Write8NoThrow((byte)(pt & 0xff));
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
|
|
result = C0Port.Write8NoThrow((byte)(pt >> 8));
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
byte v;
|
|
do {
|
|
result = CWPort.Write8NoThrow(i8254_RB_NOCOUNT | i8254_RB_SEL0);
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
|
|
result = C0Port.Read8NoThrow(out v);
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
} while ((v & i8254_RB_NULL) == i8254_RB_NULL);
|
|
}
|
|
|
|
volatile int timer2reads = 0;
|
|
|
|
[NoHeapAllocation]
|
|
internal int Timer2Read()
|
|
{
|
|
timer2reads++;
|
|
DebugStub.Assert(timer2reads == 1);
|
|
|
|
IoResult result = CWPort.Write8NoThrow(i8254_RB_NOSTATUS | i8254_RB_SEL2);
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
|
|
byte lo, hi;
|
|
|
|
result = C2Port.Read8NoThrow(out lo);
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
|
|
result = C2Port.Read8NoThrow(out hi);
|
|
DebugStub.Assert(IoResult.Success == result);
|
|
|
|
timer2reads--;
|
|
DebugStub.Assert(timer2reads == 0);
|
|
|
|
return (int)lo | ((int)hi << 8);
|
|
}
|
|
|
|
internal int PitRead()
|
|
{
|
|
CWPort.Write8(i8254_RB_ALL | i8254_RB_SEL0);
|
|
byte status = C0Port.Read8();
|
|
int pit1 = (int)C0Port.Read8() | ((int)C0Port.Read8() << 8);
|
|
|
|
// Extend pit value using i8254 output bit
|
|
if ((status & i8254_RB_OUT) == 0 ||
|
|
(pit1 == 0 && interruptPending == true)) {
|
|
pit1 += PitInterruptThreshold;
|
|
}
|
|
|
|
return pit1;
|
|
}
|
|
|
|
[ Conditional("DEBUG_TIMER") ]
|
|
private void PitDirectReadTest()
|
|
{
|
|
uint[] p = new uint [6];
|
|
|
|
PitWrite(0xffff);
|
|
|
|
for (int i = 0; i < p.Length; i++) {
|
|
CWPort.Write8(i8254_RB_NOSTATUS | i8254_RB_SEL0);
|
|
p[i] = C0Port.Read8();
|
|
p[i] += (uint) (((int)C0Port.Read8()) << 8);
|
|
}
|
|
Stop();
|
|
|
|
DebugStub.Print("Direct back to back reads in pit ticks:" +
|
|
"{0} {1} {2} {3} {4} {5}\n",
|
|
__arglist(
|
|
p[0] - p[1],
|
|
p[1] - p[2],
|
|
p[2] - p[3],
|
|
p[3] - p[4],
|
|
p[4] - p[5]));
|
|
}
|
|
|
|
[ Conditional("DEBUG_TIMER") ]
|
|
private void PitWriteReadTest()
|
|
{
|
|
DebugStub.Print("PitWriteRead() test...\n");
|
|
int pitLast = 0x4fff;
|
|
int pitNow;
|
|
int pitEnd = 0x3fff;
|
|
|
|
PitWrite(pitLast);
|
|
do {
|
|
pitNow = PitRead();
|
|
if (pitNow > pitLast) {
|
|
DebugStub.Print("({0} > {1})\n",
|
|
__arglist(pitNow, pitLast));
|
|
break;
|
|
}
|
|
pitLast = pitNow;
|
|
} while (pitNow > pitEnd);
|
|
|
|
DebugStub.Print((pitNow <= pitEnd) ? "PASSED\n" : "FAILED\n");
|
|
}
|
|
|
|
[Conditional("DEBUG_TIMER")]
|
|
private void DoPitPerformanceTests()
|
|
{
|
|
PitWriteReadTest();
|
|
|
|
ulong tsc0 = 0;
|
|
ulong tsc1 = 0;
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
tsc0 = Processor.CycleCount;
|
|
tsc0 = Processor.CycleCount;
|
|
tsc0 = Processor.CycleCount;
|
|
tsc0 = Processor.CycleCount;
|
|
tsc1 = Processor.CycleCount;
|
|
tsc1 = Processor.CycleCount;
|
|
DebugStub.Print("Kernel.GetCpuCycleCount took {0} cycles\n",
|
|
__arglist(tsc1 - tsc0));
|
|
ulong overhead = (tsc1 - tsc0) / 2;
|
|
// On VPC this figure varies wildly
|
|
// So it's set to zero and the measurement below include
|
|
// the cost of measuring the cycles.
|
|
overhead = 0;
|
|
|
|
DebugStub.Print("PitTicksToTimeSpanTicks() in cycles...\n");
|
|
for (int j = 0; j < 10; j++) {
|
|
tsc0 = Processor.CycleCount;
|
|
PitTicksToTimeSpanTicks(0);
|
|
tsc1 = Processor.CycleCount;
|
|
DebugStub.Print("{0}\n", __arglist(tsc1 - tsc0 - overhead));
|
|
}
|
|
|
|
DebugStub.Print("TimeSpanTicksToPitTicks() in cycles...\n");
|
|
for (int j = 0; j < 10; j++) {
|
|
tsc0 = Processor.CycleCount;
|
|
TimeSpanTicksToPitTicks(0);
|
|
tsc1 = Processor.CycleCount;
|
|
DebugStub.Print("{0}\n", __arglist(tsc1 - tsc0 - overhead));
|
|
}
|
|
|
|
DebugStub.Print("PitWrite() in cycles...\n");
|
|
for (uint n = 0; n < 10; n++) {
|
|
tsc0 = Processor.CycleCount;
|
|
PitWrite(0xffff);
|
|
tsc1 = Processor.CycleCount;
|
|
DebugStub.Print("{0}\n", __arglist(tsc1 - tsc0 - overhead));
|
|
}
|
|
Stop();
|
|
|
|
PitWrite(0xffff);
|
|
DebugStub.Print("PitRead() in cycles...\n");
|
|
for (uint n = 0; n < 10; n++) {
|
|
tsc0 = Processor.CycleCount;
|
|
PitRead();
|
|
tsc1 = Processor.CycleCount;
|
|
DebugStub.Print("{0}\n", __arglist(tsc1 - tsc0));
|
|
}
|
|
Stop();
|
|
|
|
DebugStub.Print("Pit Direct Read Test\n");
|
|
PitDirectReadTest();
|
|
|
|
// Eliminate spurious warnings from SSC
|
|
tsc0 = tsc1;
|
|
tsc1 = tsc0;
|
|
}
|
|
}
|
|
}
|
|
}
|