/////////////////////////////////////////////////////////////////////////////// // // 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 /// /// Maximum value accepted by SetNextInterrupt (in units of 100ns). /// public override TimeSpan MaxInterruptInterval { [NoHeapAllocation] get { return maxInterruptInterval; } } /// /// Minimum value accepted by SetNextInterrupt (in units of 100ns). /// 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); } /// 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. /// [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; } } } }