1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-01-24 19:31:26 +00:00

Work begun on Disk controller, stubs for keyboard and a few bugfixes and tweaks.

This commit is contained in:
Josh Dersch 2015-09-04 18:03:47 -07:00
parent 0ced1a2ef8
commit 24d7a5a8fe
12 changed files with 620 additions and 47 deletions

View File

@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using Contralto.CPU;
using Contralto.IO;
using Contralto.Memory;
namespace Contralto
@ -18,19 +19,28 @@ namespace Contralto
public AltoSystem()
{
_cpu = new AltoCPU(this);
_mem = new MemoryBus();
_memBus = new MemoryBus();
_mem = new Memory.Memory();
_keyboard = new Keyboard();
_diskController = new DiskController(this);
// Attach memory-mapped devices to the bus
_memBus.AddDevice(_mem);
_memBus.AddDevice(_keyboard);
Reset();
}
public void Reset()
{
_cpu.Reset();
_mem.Reset();
_memBus.Reset();
}
public void SingleStep()
{
_mem.Clock();
{
_memBus.Clock();
_diskController.Clock();
_cpu.ExecuteNext();
}
@ -41,10 +51,26 @@ namespace Contralto
public MemoryBus MemoryBus
{
get { return _mem; }
get { return _memBus; }
}
public DiskController DiskController
{
get { return _diskController; }
}
/// <summary>
/// Time (in msec) for one system clock
/// </summary>
public static double ClockInterval
{
get { return 0.00017; } // appx 170nsec, TODO: more accurate value?
}
private AltoCPU _cpu;
private MemoryBus _mem;
private MemoryBus _memBus;
private Contralto.Memory.Memory _mem;
private Keyboard _keyboard;
private DiskController _diskController;
}
}

View File

@ -27,7 +27,7 @@ namespace Contralto.CPU
get { return _carry; }
}
public static ushort Execute(AluFunction fn, ushort bus, ushort t)
public static ushort Execute(AluFunction fn, ushort bus, ushort t, int skip)
{
int r = 0;
switch (fn)
@ -89,7 +89,9 @@ namespace Contralto.CPU
break;
case AluFunction.BusPlusSkip:
throw new NotImplementedException("SKIP?");
r = bus + skip;
_carry = (r > 0xffff) ? 1 : 0;
break;
case AluFunction.BusAndNotT:
r = bus & (~t);

View File

@ -140,11 +140,11 @@ namespace Contralto.CPU
/// "wakeup" signal triggered
/// </summary>
/// <param name="task"></param>
public void WakeupTask(int task)
public void WakeupTask(TaskType task)
{
if (_tasks[task] != null)
if (_tasks[(int)task] != null)
{
_tasks[task].WakeupTask();
_tasks[(int)task].WakeupTask();
}
}
@ -153,11 +153,11 @@ namespace Contralto.CPU
/// "wakeup" signal cleared
/// </summary>
/// <param name="task"></param>
public void BlockTask(int task)
public void BlockTask(TaskType task)
{
if (_tasks[task] != null)
if (_tasks[(int)task] != null)
{
_tasks[task].BlockTask();
_tasks[(int)task].BlockTask();
}
}
@ -183,13 +183,13 @@ namespace Contralto.CPU
{
_wakeup = false;
_mpc = 0xffff; // invalid, for sanity checking
_priority = -1; // invalid
_taskType = TaskType.Invalid;
_cpu = cpu;
}
public int Priority
{
get { return _priority; }
get { return (int)_taskType; }
}
public bool Wakeup
@ -207,12 +207,11 @@ namespace Contralto.CPU
// From The Alto Hardware Manual (section 2, "Initialization"):
// "...each task start[s] at the location which is its task number"
//
_mpc = (ushort)_priority;
_mpc = (ushort)_taskType;
}
public virtual void BlockTask()
{
// Used only by hardware interfaces, where applicable
{
_wakeup = false;
}
@ -370,7 +369,7 @@ namespace Contralto.CPU
}
// Do ALU operation
aluData = ALU.Execute(instruction.ALUF, _busData, _cpu._t);
aluData = ALU.Execute(instruction.ALUF, _busData, _cpu._t, _skip);
// Reset shifter op
Shifter.SetOperation(ShifterOp.None, 0);
@ -393,8 +392,10 @@ namespace Contralto.CPU
break;
case SpecialFunction1.Block:
// "...this function is reserved by convention only; it is *not* done by the microprocessor"
throw new InvalidOperationException("BLOCK should never be invoked by microcode.");
// Technically this is to be invoked by the hardware device associated with a task.
// That logic would be circituous and unless there's a good reason not to that is discovered
// later, I'm just going to directly block the current task here.
_cpu.BlockTask(this._taskType);
break;
case SpecialFunction1.LLSH1:
@ -505,16 +506,6 @@ namespace Contralto.CPU
_cpu._t = loadTFromALU ? aluData : _busData;
}
// Load L (and M) from ALU
if (instruction.LoadL)
{
_cpu._l = aluData;
_cpu._m = aluData;
// Save ALUC0 for use in the next ALUCY special function.
_cpu._aluC0 = (ushort)ALU.Carry;
}
// Do writeback to selected R register from shifter output
if (loadR)
{
@ -527,6 +518,16 @@ namespace Contralto.CPU
_cpu._s[_cpu._rb][_rSelect] = _cpu._m;
}
// Load L (and M) from ALU
if (instruction.LoadL)
{
_cpu._l = aluData;
_cpu._m = aluData;
// Save ALUC0 for use in the next ALUCY special function.
_cpu._aluC0 = (ushort)ALU.Carry;
}
//
// Select next address, using the address modifier from the last instruction.
//
@ -565,8 +566,12 @@ namespace Contralto.CPU
//
protected AltoCPU _cpu;
protected ushort _mpc;
protected int _priority;
protected bool _wakeup;
protected TaskType _taskType;
protected bool _wakeup;
// Emulator Task-specific data. This is placed here because it is used by the ALU and it's easier to reference in the
// base class even if it does break encapsulation. See notes in the EmulatorTask class for meaning.
protected int _skip;
}
/// <summary>
@ -576,7 +581,7 @@ namespace Contralto.CPU
{
public EmulatorTask(AltoCPU cpu) : base(cpu)
{
_priority = 0;
_taskType = TaskType.Emulator;
// The Wakeup signal is always true for the Emulator task.
_wakeup = true;
@ -629,6 +634,10 @@ namespace Contralto.CPU
throw new NotImplementedException();
break;
case EmulatorF1.SWMODE:
// nothing! for now.
break;
default:
throw new InvalidOperationException(String.Format("Unhandled emulator F1 {0}.", ef1));
}
@ -670,7 +679,10 @@ namespace Contralto.CPU
// instruction dispatch."
// TODO: is this an AND or an OR operation? (how is the "merge" done?)
// Assuming for now this is an OR operation like everything else that modifies NEXT.
_nextModifier = (ushort)(((_busData & 0x8000) >> 6) | ((_busData & 0x0700) >> 2));
_nextModifier = (ushort)(((_busData & 0x8000) >> 12) | ((_busData & 0x0700) >> 8));
// "IR<- clears SKIP"
_skip = 0;
break;
case EmulatorF2.IDISP:
@ -688,7 +700,7 @@ namespace Contralto.CPU
// elseif IR[4-7] = 16B 6
// else IR[4-7]
// NB: as always, Xerox labels bits in the opposite order from modern convention;
// (bit 0 is bit 15...)
// (bit 0 is the msb...)
if ((_cpu._ir & 0x8000) != 0)
{
_nextModifier = (ushort)(3 - ((_cpu._ir & 0xc0) >> 6));
@ -801,7 +813,7 @@ namespace Contralto.CPU
case EmulatorF2.BUSODD:
// "...merges BUS[15] into NEXT[9]."
// TODO: is this an AND or an OR?
_nextModifier = (ushort)((_nextModifier & 0xffbf) | ((_busData & 0x1) << 6));
_nextModifier |= (ushort)(_busData & 0x1);
break;
case EmulatorF2.MAGIC:
@ -812,7 +824,111 @@ namespace Contralto.CPU
default:
throw new InvalidOperationException(String.Format("Unhandled emulator F2 {0}.", ef2));
}
}
// From Section 3, Pg. 31:
// "The emulator has two additional bits of state, the SKIP and CARRY flip flops. CARRY is distinct from the
// microprocessors ALUC0 bit, tested by the ALUCY function. CARRY is set or cleared as a function of IR and
// many other things(see section 3.1) when the DNS<-(do novel shifts, F2= 12B) function is executed. In
// particular, if IR[12] is true, CARRY will not change. DNS also addresses R from (3-IR[3 - 4]), causes a store
// into R unless IR[12] is set, and sets the SKIP flip flop if appropriate(see section 3.1). The emulator
// microcode increments PC by 1 at the beginning of the next emulated instruction if SKIP is set, using
// BUS+SKIP(ALUF= 13B). IR_ clears SKIP."
//
// NB: _skip is in the encapsulating AltoCPU class to make it easier to reference since the ALU needs to know about it.
private int _carry;
}
/// <summary>
/// DiskSectorTask provides implementation for disk-specific special functions
/// </summary>
private class DiskSectorTask : Task
{
public DiskSectorTask(AltoCPU cpu) : base(cpu)
{
_taskType = TaskType.DiskSector;
_wakeup = false;
}
protected override ushort GetBusSource(int bs)
{
DiskBusSource dbs = (DiskBusSource)bs;
switch (dbs)
{
case DiskBusSource.ReadKSTAT:
return _cpu._system.DiskController.KSTAT;
case DiskBusSource.ReadKDATA:
return _cpu._system.DiskController.KDATA;
default:
throw new InvalidOperationException(String.Format("Unhandled bus source {0}", bs));
}
}
protected override void ExecuteSpecialFunction1(int f1)
{
DiskF1 df1 = (DiskF1)f1;
switch(df1)
{
case DiskF1.LoadKDATA:
// "The KDATA register is loaded from BUS[0-15]."
_cpu._system.DiskController.KDATA = _busData;
break;
case DiskF1.LoadKADR:
// "This causes the KADR register to be loaded from BUS[8-14].
// in addition, it causes the head address bit to be loaded from KDATA[13]."
// TODO: do the latter (likely inside the controller)
_cpu._system.DiskController.KADR = (ushort)((_busData & 0xfe) >> 1);
break;
case DiskF1.LoadKCOMM:
_cpu._system.DiskController.KCOM = (ushort)((_busData & 0x7c00) >> 10);
break;
case DiskF1.CLRSTAT:
_cpu._system.DiskController.ClearStatus();
break;
case DiskF1.INCRECNO:
_cpu._system.DiskController.IncrementRecord();
break;
case DiskF1.LoadKSTAT:
// "KSTAT[12-15] are loaded from BUS[12-15]. (Actually BUS[13] is ORed onto
// KSTAT[13].)"
// OR in BUS[12-15] after masking in KSTAT[13] so it is ORed in properly.
_cpu._system.DiskController.KSTAT = (ushort)(((_cpu._system.DiskController.KSTAT & 0xfff4)) | (_busData & 0xf));
break;
case DiskF1.STROBE:
_cpu._system.DiskController.Strobe();
break;
default:
throw new InvalidOperationException(String.Format("Unhandled disk special function 1 {0}", df1));
}
}
protected override void ExecuteSpecialFunction2(int f2)
{
DiskF2 df2 = (DiskF2)f2;
switch(df2)
{
case DiskF2.INIT:
// "NEXT<-NEXT OR (if WDTASKACT AND WDINIT) then 37B else 0
// TODO: figure out how WDTASKACT and WDINIT work.
throw new NotImplementedException("INIT not implemented.");
default:
throw new InvalidOperationException(String.Format("Unhandled disk special function 2 {0}", df2));
}
}
}
@ -833,7 +949,7 @@ namespace Contralto.CPU
// Task data
private Task _nextTask; // The task to switch two after the next microinstruction
private Task _currentTask; // The currently executing task
private Task[] _tasks = new Task[16];
private Task[] _tasks = new Task[16];
private long _clocks;

View File

@ -69,6 +69,10 @@ namespace Contralto.CPU
//
// Task-specific enumerations follow
//
//
// Emulator
//
enum EmulatorF1
{
SWMODE = 8,
@ -99,6 +103,38 @@ namespace Contralto.CPU
LoadSLocation = 4, // SLOCATION<- store to S reg from M
}
//
// Disk (both sector and word tasks)
//
enum DiskF1
{
STROBE = 9,
LoadKSTAT = 10,
INCRECNO = 11,
CLRSTAT = 12,
LoadKCOMM = 13,
LoadKADR = 14,
LoadKDATA = 15,
}
enum DiskF2
{
INIT = 8,
RWC = 9,
RECNO = 10,
XFRDAT = 11,
SWRNRDY = 12,
NFER = 13,
STROBON = 14,
}
enum DiskBusSource
{
ReadKSTAT = 3,
ReadKDATA = 4,
}
public class MicroInstruction
{
public MicroInstruction(UInt32 code)

View File

@ -16,6 +16,13 @@ namespace Contralto.CPU
RotateRight,
}
//NOTE: FOR NOVA (NOVEL) SHIFTS (from aug '76 manual):
// The emulator has two additional bits of state, the SKIP and CARRY flip flops.CARRY is identical
// to the Nova carry bit, and is set or cleared as appropriate when the DNS+- (do Nova shifts)
// function is executed.DNS also addresses R from(1R[3 - 4] XOR 3), and sets the SKIP flip flop if
// appropriate.The PC is incremented by 1 at the beginning of the next emulated instruction if
// SKIP is set, using ALUF DB.IR4- clears SKIP.
public static class Shifter
{
static Shifter()
@ -31,9 +38,13 @@ namespace Contralto.CPU
_count = count;
}
/// <summary>
/// TODO: this is kind of clumsy.
/// </summary>
/// <param name="magic"></param>
public static void SetMagic(bool magic)
{
_magic = magic;
_magic = magic;
}
/// <summary>
@ -56,10 +67,24 @@ namespace Contralto.CPU
case ShifterOp.ShiftLeft:
output = (ushort)(input << _count);
if (_magic)
{
// "MAGIC places the high order bit of T into the low order bit of the
// shifter output on left shifts..."
output |= (ushort)((t & 0x8000) >> 15);
}
break;
case ShifterOp.ShiftRight:
output = (ushort)(input >> _count);
if (_magic)
{
// "...and places the low order bit of T into the high order bit position
// of the shifter output on right shifts."
output |= (ushort)((t & 0x1) << 15);
}
break;
case ShifterOp.RotateLeft:

View File

@ -57,6 +57,8 @@
<Compile Include="Debugger.Designer.cs">
<DependentUpon>Debugger.cs</DependentUpon>
</Compile>
<Compile Include="IO\DiskController.cs" />
<Compile Include="IO\Keyboard.cs" />
<Compile Include="Memory\IMemoryMappedDevice.cs" />
<Compile Include="Memory\Memory.cs" />
<Compile Include="Memory\MemoryBus.cs" />

View File

@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Contralto.Memory;
namespace Contralto.IO
{
public class DiskController
{
public DiskController(AltoSystem system)
{
_system = system;
}
public ushort KDATA
{
get { return _kData; }
set { _kData = value; }
}
public ushort KADR
{
get { return _kAdr; }
set
{
_kAdr = value;
_recNo = 0;
// "In addition, it causes the head address bit to be loaded from KDATA[13]."
_head = (_kData & 0x4) >> 2;
}
}
public ushort KCOM
{
get { return _kCom; }
set
{
_kCom = value;
//
}
}
public ushort KSTAT
{
get { return _kStat; }
set { _kStat = value; }
}
public ushort RECNO
{
get { return _recMap[_recNo]; }
}
public void Reset()
{
ClearStatus();
_recNo = 0;
_elapsedSectorTime = 0.0;
_cylinder = 0;
_sector = 0;
_head = 0;
_kStat = 0;
}
public void Clock()
{
_elapsedSectorTime++;
// TODO: only signal sector changes if disk is loaded, etc.
if (_elapsedSectorTime > _sectorClocks)
{
//
// Next sector; save fractional part of elapsed time (to more accurately keep track of time), move to next sector
// and wake up sector task.
//
_elapsedSectorTime -= _sectorClocks;
_sector = (_sector + 1) % 12;
_kStat = (ushort)((_kStat & 0x0fff) | (_sector << 12));
_system.CPU.WakeupTask(CPU.TaskType.DiskSector);
}
// If seek is in progress, move closer to the desired cylinder...
// TODO: move bitfields to enums / constants, this is getting silly.
if ((_kStat & 0x0040) != 0)
{
_elapsedSeekTime++;
if (_elapsedSeekTime > _seekClocks)
{
_elapsedSectorTime -= _seekClocks;
if (_cylinder < _destCylinder)
{
_cylinder++;
}
else
{
_cylinder--;
}
// Are we *there* yet?
if (_cylinder == _destCylinder)
{
// clear Seek bit
_kStat &= 0xffbf;
}
}
}
}
public void ClearStatus()
{
// "...clears KSTAT[13]." (chksum error flag)
_kStat &= 0xfffb;
}
public void IncrementRecord()
{
// "Advances the shift registers holding the KADR register so that they present the number and read/write/check status of the
// next record to the hardware."
// "RECORD" in this context indicates the sector field corresponding to the 2 bit "action" field in the KADR register
// (i.e. one of Header, Label, or Data.)
// INCRECNO shifts the data over two bits to select from Header->Label->Data.
_kAdr = (ushort)(_kAdr << 2);
_recNo++;
if (_recNo > 3)
{
// sanity check for now
throw new InvalidOperationException("Unexpected INCRECORD past rec 3.");
}
}
public void Strobe()
{
//
// "Initiates a disk seek operation. The KDATA register must have been loaded previously,
// and the SENDADR bit of the KCOMM register previously set to 1."
//
// sanity check: see if SENDADR bit is set, if not we'll signal an error (since I'm trusting that
// the official Xerox uCode is doing the right thing, this will help ferret out emulation issues.
// eventually this can be removed.)
if ((_kCom & 0x1) != 1)
{
throw new InvalidOperationException("STROBE while SENDADR bit of KCOMM not 1. Unexpected.");
}
_destCylinder = (_kData & 0x0ff8) >> 3;
// set "seek fail" bit based on selected cylinder (if out of bounds) and do not
// commence a seek if so.
if (_destCylinder < 203)
{
_kStat |= 0x0080;
}
else
{
// Otherwise, start a seek.
// Clear the fail bit.
_kStat &= 0xff7f;
// Set seek bit
_kStat |= 0x0040;
// And figure out how long this will take.
_seekClocks = CalculateSeekTime();
_elapsedSeekTime = 0.0;
}
}
private double CalculateSeekTime()
{
// How many cylinders are we moving?
int dt = Math.Abs(_destCylinder - _cylinder);
//
// From the Hardware Manual, pg 43:
// "Seek time (approx.): 15 + 8.6 * sqrt(dt) (msec)
//
double seekTimeMsec = 15.0 + 8.6 * Math.Sqrt(dt);
return seekTimeMsec / AltoSystem.ClockInterval;
}
private ushort _kData;
private ushort _kAdr;
private ushort _kCom;
private ushort _kStat;
private int _recNo;
private ushort[] _recMap =
{
0, 2, 3, 1
};
// Current disk position
private int _cylinder;
private int _destCylinder;
private int _head;
private int _sector;
// Sector timing. Based on table on pg. 43 of the Alto Hardware Manual
private double _elapsedSectorTime; // elapsed time in this sector (in clocks)
private const double _sectorDuration = (40.0 / 12.0); // time in msec for one sector
private readonly double _sectorClocks = _sectorDuration / AltoSystem.ClockInterval; // number of clock cycles per sector time.
// Cylinder seek timing. Again, see the manual.
// Timing varies based on how many cylinders are being traveled during a seek; see
// CalculateSeekTime() for more.
private double _elapsedSeekTime;
private double _seekClocks;
private AltoSystem _system;
}
}

42
Contralto/IO/Keyboard.cs Normal file
View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Contralto.Memory;
namespace Contralto.IO
{
/// <summary>
/// Currently just a stub indicating that no keys are being pressed.
/// </summary>
public class Keyboard : IMemoryMappedDevice
{
public Keyboard()
{
}
public ushort Read(int address)
{
// TODO: implement; return nothing pressed for any address now.
return 0xffff;
}
public void Load(int address, ushort data)
{
// nothing
}
public MemoryRange[] Addresses
{
get { return _addresses; }
}
private readonly MemoryRange[] _addresses =
{
new MemoryRange(0xfe1c, 0xfe1f), // 177034-177037
};
}
}

View File

@ -6,10 +6,55 @@ using System.Threading.Tasks;
namespace Contralto.Memory
{
/// <summary>
/// Specifies a range of memory from Start to End, inclusive.
/// </summary>
public struct MemoryRange
{
public MemoryRange(ushort start, ushort end)
{
if (!(end > start))
{
throw new ArgumentOutOfRangeException("end must be greater than start.");
}
Start = start;
End = end;
}
public bool Overlaps(MemoryRange other)
{
return ((other.Start >= this.Start && other.Start <= this.End) ||
(other.End >= this.Start && other.End <= this.End));
}
public ushort Start;
public ushort End;
}
/// <summary>
/// Specifies an interfaces for devices that appear in mapped memory. This includes
/// RAM as well as regular I/O devices.
/// </summary>
public interface IMemoryMappedDevice
{
/// <summary>
/// Reads a word from the specified address.
/// </summary>
/// <param name="address"></param>
/// <returns></returns>
ushort Read(int address);
/// <summary>
/// Writes a word to the specified address.
/// </summary>
/// <param name="address"></param>
/// <param name="data"></param>
void Load(int address, ushort data);
/// <summary>
/// Specifies the range (or ranges) of addresses decoded by this device.
/// </summary>
MemoryRange[] Addresses { get; }
}
}

View File

@ -23,6 +23,16 @@ namespace Contralto.Memory
_mem[address] = data;
}
public MemoryRange[] Addresses
{
get { return _addresses; }
}
private readonly MemoryRange[] _addresses =
{
new MemoryRange(0, 0xfdff), // to 176777; IO page above this.
};
private ushort[] _mem;
}
}

View File

@ -17,10 +17,34 @@ namespace Contralto.Memory
{
public MemoryBus()
{
_mem = new Memory();
_bus = new Dictionary<ushort, IMemoryMappedDevice>(65536);
Reset();
}
public void AddDevice(IMemoryMappedDevice dev)
{
//
// Add the new device to the hash; this is done by adding
// one entry for every address claimed by the device. Since we have only 64K of address
// space, this isn't too awful.
//
foreach(MemoryRange range in dev.Addresses)
{
for(ushort addr = range.Start; addr <= range.End; addr++)
{
if (_bus.ContainsKey(addr))
{
throw new InvalidOperationException(
String.Format("Memory mapped address collision for dev {0} at address {1}", dev, OctalHelpers.ToOctal(addr)));
}
else
{
_bus.Add(addr, dev);
}
}
}
}
public void Reset()
{
_memoryCycle = 0;
@ -201,8 +225,16 @@ namespace Contralto.Memory
/// <returns></returns>
private ushort ReadFromBus(ushort address)
{
// TODO: actually dispatch to I/O
return _mem.Read(address);
// Look up address in hash; if populated ask the device
// to return a value otherwise throw.
if (_bus.ContainsKey(address))
{
return _bus[address].Read(address);
}
else
{
throw new NotImplementedException(String.Format("Read from unimplemented memory-mapped I/O device at {0}.", OctalHelpers.ToOctal(address)));
}
}
/// <summary>
@ -213,11 +245,23 @@ namespace Contralto.Memory
/// <returns></returns>
private void WriteToBus(ushort address, ushort data)
{
_mem.Load(address, data);
// Look up address in hash; if populated ask the device
// to store a value otherwise throw.
if (_bus.ContainsKey(address))
{
_bus[address].Load(address, data);
}
else
{
throw new NotImplementedException(String.Format("Write to unimplemented memory-mapped I/O device at {0}.", OctalHelpers.ToOctal(address)));
}
}
private Memory _mem;
/// <summary>
/// Hashtable used for address-based dispatch to devices on the memory bus.
/// </summary>
private Dictionary<ushort, IMemoryMappedDevice> _bus;
private bool _memoryOperationActive;
private int _memoryCycle;
private ushort _memoryAddress;

View File

@ -15,7 +15,7 @@ namespace Contralto
public static string ToOctal(int i, int digits)
{
string octalString = Convert.ToString(i, 8);
string octalString = Convert.ToString(i, 8);
return new String('0', digits - octalString.Length) + octalString;
}
}