Foreword
In the previous blog we started to implement tape emulation for our emulator. In this blog we will continue.Implementing the timer
Let us start with implementing the timer.I think probably a good place to start, is just to get an overview of how timers work in the CIA chip
Overview
We scratched the surface on timers in a previous blog post where implemented the flashing cursor.It might not have been that obvious from that blog post, but the CIA actually have 2 timers: Timer A and Timer B.
Each of these timers have it own set of registers which are almost identical.
To avoid a cluttered discussion, I will exclusively focus on the registers of Timer A. Please note, however, that you can apply exactly the same reasoning to timer B.
Also, in this section, when I am talking about The Timer, I am implying Timer A.
Let us start with summarising what we already know about timers on the CIA:
- Timers are actually 16-bit count down counters. When a counter reaches 0, an underflow condition occurs and an interrupt triggers.
- These counters can be driven by a number of clock sources. The most notable clock source is 02 which clocks at 1Mhz
It is interesting to note that when you write a value to register 4 or 5, the timer will not immediately update the counter with given value. Instead, all writes to registers are stored in a special register, called a latch.
So, how do you set the counter to count down from a specific number? There are two ways.
The first way is the brute force way. Writing a 1 to bit 4 of register 14 will force the timer to load the counter with the value stored in the latch.
The second way is when the counter reaches zero, the timer will automatically reload the counter with the value stored in the latch.
Next, question: How do you start the timer? You write a 1 to bit 0 of register 14. Conversely, if you write a 0 to bit zero, the timer will halt counting down. If you resume the count down process by writing a one again to bit 0, the counter will just continue down from the value it previously stopped at.
Lets conclude this section by discussing the two modes in which CIA timers can operate: One-Shot and Continuous.
Both One-Shot and Continuous mode will count down till underflow, raise an interrupt and reload the counter with the value stored in the latch. The subtle difference between the two modes will actually happen after executing previous mentioned two actions.
In One-Shot mode countdown will halt after an underflow condition. This halting process will also set the start/stop bit (e.g. bit 0 of location 14) to zero.
In Continuous mode, the counter will automatically continue to countdown after the counter was reloaded during an underflow. Continuous mode is a kind of an unattended mode.
Coding
We will start off with a new file called Timer.js. I will again start showing the majority of the code, and then explaining it:
function timer(alarmManager) { var myAlarmManager = alarmManager; var isEnabled = false; var ticksBeforeExpiry = 0; myAlarmManager.addAlarm(this); var timerHigh = 255; var timerLow = 255; var continious = false; this.getIsEnabled = function() { return isEnabled; } this.getTicksBeforeExpiry = function() { return ticksBeforeExpiry; } this.setTicksBeforeExpiry = function(ticks) { ticksBeforeExpiry = ticks; } this.trigger = function() { //cuase interrupt ticksBeforeExpiry = (timerHigh << 8) | timerLow; if (!continious) isEnabled = false; } this.setTimerHigh = function(high) { timerHigh = high; } this.setTimerLow = function(low) { timerHigh = low; } this.getTimerHigh = function() { return (ticksBeforeExpiry >> 8); } this.getTimerLow = function() { return (ticksBeforeExpiry & 0xff); } this.setControlRegister = function (byteValue) { if ((byteValue & (1 << 4)) != 0) ticksBeforeExpiry = (timerHigh << 8) | timerLow; continious = ((byteValue & (1 << 3)) == 0) ? true : false; isEnabled = ((byteValue & 1) == 1) ? true : false; } this.getControlRegister = function() { var tempValue = 0; if (continious) tempValue = tempValue | 1 << 3; if (isEnabled) tempValue = tempValue | 1; return tempValue; } }
Needless to say, this class will also add itself to the alarm manager, so I have implemented all the methods required by AlarmManager, which is the following:
- getIsEnabled
- getTicksBeforeExpiry
- setTicksBeforeExpiry
- trigger
Now, lets look at the functionality which is unique to the timer itself. The first is the two private variables timerHigh and timerLow. As you can see only a setter is provided for access to these private variables variables and no getter. This nicely models the fact that the timer latches are write only registers. You can also see that assign a default value of 255 to each of these private variables. This is also the default when a real CIA powers up.
There is indeed a getTimerHigh and getTimerLow method, but as previously mentioned, it not does not return the contents of the timerHigh/timerLow private variables. It is using the timerTicksBeforeExpiry private variable.
Another timer related private variable is continuous. It is mainly utilised within the trigger method. Within the method the Alarm is disabled when continuous is false.
Finally, let us look at the methods setControlRegister and getControlRegister. In effect, these two methods deals with the contents of location 14 of the CIA chip.
Implementing Interrupts
In this section we are going to discuss and implement CIA interrupt functionality into our emulator. Hopefully after this section we will have more clarity in what we need to implement for our timer and tape class during the event of an interrupt.Overview
Up to this point in time we know that the CIA can generate interrupts for a number of different events.The beauty of the CIA chip is that it collate all these interrupt into a single interrupt going to the CPU.
There are a couple of scenarios where you would like to block some of these interrupts (e.g. to mask an interrupt) so that it doesn't cause an interrupt on the CPU. You can exercise this option by writing to location 13.
When you write to location 13 for masking an interrupt or two, it is important that the Most significant bit of the byte you write is set to zero. This will inform the CIA to mask every interrupt which applicable bit is set to a one. For example, if you write 03h to location 13, both the timer A and timer B interrupts will be disabled.
If the byte you write to location 13 has the MSB set to a one, the opposite will happen. Each bit set to a one will enable the applicable interrupt.For example, writing 83h to location 13 will enable the Timer A and Timer B interrupts.
When you read from location you will see which interrupts you have masked, but which interrupts actually have occurred. This will enable you to figure out which interrupt actually interrupted the CPU.
It is important to note that reading location 13 has one side effect. Reading this location will reset all occurred interrupts to zero. So it is important that which ever piece of code read this location, should store it somewhere so that all applicable events can be served, if necessary.
Coding
Lets code the interrupt functionality.We will wrap all this functionality in a file InterruptController.js Our first take on the class will look as follows:
function interruptController() { var mycpu; var interruptMask = 0; var interruptsOccured = 0; this.setCpu = function(cpu) { mycpu = cpu; } this.setInterruptMask = function(mask) { if (mask > 127) { interruptMask = interruptMask | mask; } else { interruptMask = interruptMask & (~mask & 0xff); } } this.getInterrupts = function() { var temp = interruptsOccured; interruptsOccured = 0; return temp; } }
Here we closely model our theoretical discussion. Writing to the class we first check if bit 7 is, then we OR the input to the private variable interruptMask. If bit 7 is a zero, we mask off relevant bits.
Nothing interesting happening in the read. We return the interrupts that have occurred and reset the private variable storing this information.
Next, let us service some public methods simulating inerrupts for Time A, Timer B and FLAG:
this.interruptFlag1 = function() { interruptsOccured = interruptsOccured | 16 | 128; if ((interruptMask & 16) == 0) return; mycpu.setInterrupt(); } this.interruptTimerA = function() { interruptsOccured = interruptsOccured | 1 | 128; if ((interruptMask & 1) == 0) return; mycpu.setInterrupt(); } this.interruptTimerB = function() { interruptsOccured = interruptsOccured | 2 | 128; if ((interruptMask & 2) == 0) return; mycpu.setInterrupt(); }
So, basically or Timer classes and Tape class should call one of these methods in the event of an interrupt. Each method then first checks whether applicable interrupt is enable and then call setInterrupt on the CPU class if it is enabled.
Glueing everything together
Time to glue everything together.First, lets finish off the interrupt functionality within the Tape and Timer classes.
We start with Tape.js:
function tape(alarmManager, interruptManager) { var myAlarmManager = alarmManager; var myInterruptManager = interruptManager; var tapeData; var posInTape; var isEnabled = false; var ticksBeforeExpiry = 0; myAlarmManager.addAlarm(this); this.attachTape = function(file) { var reader = new FileReader(); reader.onload = function(e) { var arrayBuffer = reader.result; tapeData = new Uint8Array(arrayBuffer); posInTape = 0x14; scheduleNextTrigger(); alert("Tape attached"); } reader.readAsArrayBuffer(file); } this.setMotorOn = function(bit) { isEnabled = (bit == 0) ? true : false; } function scheduleNextTrigger() { ticksBeforeExpiry = tapeData[posInTape] << 3; posInTape++; } this.getIsEnabled = function() { return isEnabled; } this.getTicksBeforeExpiry = function() { return ticksBeforeExpiry; } this.setTicksBeforeExpiry = function(ticks) { ticksBeforeExpiry = ticks; } this.trigger = function() { myInterruptManager.interruptFlag1(); scheduleNextTrigger(); } }
That was relatively painless.
Next up, the timers. Here we are faced with a bit of a problem. It make sense to create one separate timer instance for timer A and another instance for timer B. Currently, however there is know way for a timer instance to know whether it is timer A or B. This knowledge is necessary so that the timer instance can call the correct method on the InteruuptController class during an interrupt.
We sort of need to inject this into the timer when we create a timer instance. This will enable the following changes within our Timer Class:
function timer(alarmManager, InterruptController, timerName) { ... var myname = timerName; var myInterruptController = InterruptController; ... this.trigger = function() { if (myname == "A") { myInterruptController.interruptTimerA(); } else { myInterruptController.interruptTimerB(); } ticksBeforeExpiry = (timerHigh << 8) | timerLow; if (!continious) isEnabled = false; } ... }
From this code we see that when creating a timer instance, either an A or a B should be passed as timerName.
Time to wire everything into the memory class:
function memory(allDownloadedCallback, keyboard, timerA, timerB, interruptController,tape) { ... var mytimerA = timerA; var mytimerB = timerB; var myinterruptController = interruptController; var mytape = tape;... function ciaRead(address) { if (address == 0xdc01) { return keyboardInstance.getColumnByte(mainMem[0xdc00]); } else if (address == 0xdc04) { return mytimerA.getTimerLow(); } else if (address == 0xdc05) { return mytimerA.getTimerHigh(); } else if (address == 0xdc06) { return mytimerB.getTimerLow(); } else if (address == 0xdc07) { return mytimerB.getTimerHigh(); } else if (address == 0xdc0d) { return myinterruptController.getInterrupts(); } else if (address == 0xdc0e) { return mytimerA.getControlRegister(); } else if (address == 0xdc0f) { return mytimerB.getControlRegister(); } else { return mainMem[address]; } } function ciaWrite(address, byteValue) { if (address == 0xdc04) { return mytimerA.setTimerLow(byteValue); } else if (address == 0xdc05) { return mytimerA.setTimerHigh(byteValue); } else if (address == 0xdc06) { return mytimerB.setTimerLow(byteValue); } else if (address == 0xdc07) { return mytimerB.setTimerHigh(byteValue); } else if (address == 0xdc0d) { return myinterruptController.setInterruptMask(byteValue); } else if (address == 0xdc0e) { return mytimerA.setControlRegister(byteValue); } else if (address == 0xdc0f) { return mytimerB.setControlRegister(byteValue); } else { mainMem[address] = byteValue; } } this.readMem = function (address) { if ((address >= 0xa000) & (address <=0xbfff)) return basicRom[address & 0x1fff]; else if ((address >= 0xe000) & (address <=0xffff)) return kernalRom[address & 0x1fff]; else if ((address >= 0xdc00) & (address <= 0xdcff)) { return ciaRead(address); } return mainMem[address]; } this.writeMem = function (address, byteval) { if ((address >= 0xdc00) & (address <= 0xdcff)) { ciaWrite(address, byteval); return; } else if (address == 1) { var temp = byteval & 32; temp = temp >> 5; tape.setMotorOn(temp); } mainMem[address] = byteval; } }
All CIA location accesses has moved into two separate methods.
One thing also worth mentioning is the check if we write to memory location 1. In such a scenario we isolate the motor bit and do the appropriate action on the tape class.
We need to do some wiring in our index page as well. So we start with the initialisation code:
<script language="JavaScript"> var mykeyboard = new keyboard(); var myAlarmManager = new alarmManager(); var myInterruptController = new interruptController(); var myTimerA = new timer(myAlarmManager, myInterruptController,"A"); var myTimerB = new timer(myAlarmManager, myInterruptController, "B"); var myTape = new tape(myAlarmManager, myInterruptController); var mymem = new memory(postInit,mykeyboard,myTimerA, myTimerB, myInterruptController, myTape); var mycpu = new cpu(mymem); myAlarmManager.setCpu(mycpu); myInterruptController.setCpu(mycpu); var myvideo = new video(document.getElementById("screen"), mymem); var running = false; var breakpoint = 0; ...
We also have some unfinished business in the runBatch method:
function runBatch() { if (!running) return; myvideo.updateCanvas(); var targetCycleCount = mycpu.getCycleCount() + 20000; while (mycpu.getCycleCount() < targetCycleCount) { mycpu.step(); myAlarmManager.processAlarms(); //if (mycpu.getCycleCount() > 6000000) // mymem.setSimulateKeypress(); var blankingPeriodLow = targetCycleCount - 100; if ((mycpu.getCycleCount() >= blankingPeriodLow) & (mycpu.getCycleCount() <= targetCycleCount)) { mymem.writeMem(0xD012, 0); } else { mymem.writeMem(0xD012, 1); } if (mycpu.getPc() == breakpoint) { stopEm(); return; } } //mycpu.setInterrupt(); }
I removed the call to setInterrupt and added a call to processAlarms of the alarmmanager.
We still need something to simulate pressing play on tape with the effect of bringing the sense line low.
We will delegate this responsibility to the Memory class:
function memory(allDownloadedCallback, keyboard, timerA, timerB, interruptController,tape) { var playPressed = false; ... this.togglePlayPresed = function() { playPressed = !playPressed; } ... this.readMem = function (address) { if ((address >= 0xa000) & (address <=0xbfff)) return basicRom[address & 0x1fff]; else if ((address >= 0xe000) & (address <=0xffff)) return kernalRom[address & 0x1fff]; else if ((address >= 0xdc00) & (address <= 0xdcff)) { return ciaRead(address); } else if (address == 1) { var temp = mainMem[address] & 239; if (!playPressed) temp = temp | 16; return temp; } return mainMem[address]; } }
We have exposed a method togglePlaypressed that you can call outside the memory class which will toggle the state of the sense line.
On our index page we can just add a button which will call this method on click:
... <br/> <input type="file" id="files" name="myfile"/> <button onclick="attachTape()">Attach</button> </br> <button onclick="mymem.togglePlayPresed()">Play</button> </br> <textarea id="registers" name="reg" rows="1" cols="60"></textarea> <br/> ...
Some Final touches
Some testing yielded that it is not so a good idea to set an interrupt flag in the CPU class when an interrupt occurs.You end off with times that there is still a pending interrpt in the CPU class, but the interrupt might already have been cleared by reading the flags register in the interrupt class,A better approach would therefore be to rather let the CPU class ask the Interrupt class whether an Interrupt has occured
this.step = function () { if ((myInterruptController.getCpuInterruptOcurred()) & (interruptflag == 0)) { interruptOcurred = 0; Push(pc >> 8); Push(pc & 0xff); breakflag = 0; Push(getStatusFlagsAsByte()); breakflag = 1; interruptflag = 1; tempVal = localMem.readMem(0xffff) * 256; tempVal = tempVal + localMem.readMem(0xfffe); pc = tempVal; }
This will yield an interrupt class as follows:
function interruptController() { var mycpu; var interruptMask = 0; var interruptsOccured = 0; var interruptCpu = false; this.setCpu = function(cpu) { mycpu = cpu; } this.setInterruptMask = function(mask) { if (mask > 127) { interruptMask = interruptMask | mask; } else { interruptMask = interruptMask & (~mask & 0xff); } } this.getCpuInterruptOcurred = function() { return interruptCpu; } this.getInterrupts = function() { var temp = interruptsOccured; interruptsOccured = 0; interruptCpu = false; return temp; } this.interruptFlag1 = function() { interruptsOccured = interruptsOccured | 16 | 128; if ((interruptMask & 16) == 0) return; interruptCpu = true; //mycpu.setInterrupt(); } this.interruptTimerA = function() { interruptsOccured = interruptsOccured | 1 | 128; if ((interruptMask & 1) == 0) return; interruptCpu = true; //mycpu.setInterrupt(); } this.interruptTimerB = function() { interruptsOccured = interruptsOccured | 2 | 128; if ((interruptMask & 2) == 0) return; interruptCpu = true; //mycpu.setInterrupt(); } }
With all this we managed to get our emulator to find the header on the tape:
This concludes this blog.
In Summary
In this blog we managed to emulate tape emulation up to the point where our emulator could identify the header on tape.
In the next blog we will continue with our mission to emulate the game Dan Dare.
Dan Dare is also a game where its loader shows flashing colourful borders. So it would make sense as a next to try and emulate these borders.
This goal will force us to jack up the capabilities of our Video class.
Till next time!
No comments:
Post a Comment