singrdk/base/Kernel/Singularity.Hal.LegacyPC/Timer8254.cs

616 lines
21 KiB
C#

///////////////////////////////////////////////////////////////////////////////
//
// Microsoft Research Singularity
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// File: Timer8254.cs
//
// 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 TimerResources : 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 Timer8254
{
/* 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 Timer8254(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);
p.Write8((byte) ((p.Read8() & 0xfc) | 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 Finalize()
{
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);
}
/* Interrupt constants (in units of TimeSpan tick == 100ns) */
public long MaxInterruptInterval {
[NoHeapAllocation]
get {
return 500000; // 50ms
}
}
public long MinInterruptInterval {
[NoHeapAllocation]
get {
return 10000; // 1ms
}
}
public long InterruptIntervalGranularity
{
[NoHeapAllocation]
get {
return 17;
}
}
[NoHeapAllocation]
public bool SetNextInterrupt(long delta)
{
if (delta < MinInterruptInterval || delta > MaxInterruptInterval) {
#if VERBOSE
unchecked {
Tracing.Log(Tracing.Audit, "Next Interrupt {0} false",
(UIntPtr)(uint)delta);
}
#endif
return false;
}
#if VERBOSE
unchecked {
Tracing.Log(Tracing.Audit, "NextInterrupt {0} true",
(UIntPtr)(uint)delta);
}
#endif
#if TIMER_NO_GO
return true;
#endif
bool iflag = Processor.DisableInterrupts();
try {
ClockLogger.AddEntry(2, rps, this, delta);
PitWrite(TimeSpanTicksToPitTicks((int)delta));
this.interruptPending = true;
ClockLogger.AddEntry(3, rps, this, delta);
} finally {
Processor.RestoreInterrupts(iflag);
}
return true;
}
/// <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 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();
// Shut up SSC
tsc0 = tsc1;
tsc1 = tsc0;
}
}
}
}