Tuesday, 6 March 2012

In Which We Play For Time


Before moving on to VIC initialisation, I though it might be a good idea to do something a little more useful with the IRQ service routine than simply changing the colour of the screen, especially as when we start doing stuff with the VIC itself it'll probably be quite distracting if the colours change every second. So the ISR will change to administer the progression of a Time-of-Day (TOD) clock which can be interrogated to give us a simple 24-hour readout.

I've already got a working IRQ handler which can reliably count down each 1/60th of a second (a Jiffy) so extending that as a subroutine to count 60 seconds, 60 minutes, and 24 hours isn't going to be difficult. However, we want IRQ activities to be as quick as possible - don't forget that we only have a maximum of 18473 cycles to do all IRQ operations (assuming we use my VIC++ PAL VIA settings), because that's how long a jiffy is. If we run longer than that, IRQs will start to drift because we'd be taking longer to do IRQ stuff than the VIA countdown takes, which means we'd miss the subsequent IRQ signal and therefore only respond to every alternate signal. In fact, we don't want to be taking anywhere near the cycle limit, because that would imply that we were using almost 100% of our CPU capacity doing 'background' stuff, and never doing anything relating to the users' software. We want to keep the ISR duration as tiny as possible, so that as many of those 18473 cycles between IRQ signals are available to the rest of the system as possible. Obviously we can turn IRQ processing off at any point with SEI, but as in the original Commodore design that would inherently suspend a bunch of stuff that we really want to be happening all the time.

Now let's think about the logic required to maintain a TOD clock; we know the ISR gets called every jiffy, so we can easily measure the passage of a second by tallying 60 jiffies. Equally, if we then keep track of seconds, then after 60 seconds we'll have measured a minute. Tally 60 minutes for an hour, and 24 hours for a day, and there's the clock. This is not complicated, although the thresholds of 24 and 60 are a bit cumbersome - you can thank the Ancient Egyptians for this - and it would be much nicer if days were divided in some easier-to-process way. Decimalisation is one option, but I'd prefer to divide days into 256 hours of 256 minutes, each comprised of 256 seconds made up of 256 jiffies - imagine how simple that would be to represent in binary!

Alas, we're stuck with the 24/60/60 model, so we need to figure-out a good way to track these thresholds as quickly as possible. We could just count jiffies, then increment a 'second' variable, and then test it to see if it had passed 60, and if so reset it and increment 'minutes', and repeat for minutes-to-hours, ending by looking to see if 'hours' had passed 24, but that means quite a lot of code to do those increments, tests, and resets:

.updtcdc
LDY #$00 ; [2]

INC .CCLOCKJ ; [5] ZP increment clock jiffies
LDX .CCLOCKJ ; [3] ZP get clock jiffies
CPX #$3C ; [2] 60 jiffies?
BNE .cdcexit ; [3/2] no tickover
STY .CCLOCKJ ; [3] reset jiffy count

INC .CCLOCKS ; [5] ZP increment clock seconds
LDX .CCLOCKS ; [3] ZP get clock seconds
CPX #$3C ; [2] 60 seconds?
BNE .cdcexit ; [3/2] no tickover
STY .CCLOCKS ; [3] reset second count

INC .CCLOCKM ; [5] ZP increment clock minutes
LDX .CCLOCKM ; [3] ZP get clock minutes
CPX #$3C ; [2] 60 minutes?
BNE .cdcexit ; [3/2] no tickover
STY .CCLOCKM ; [3] reset minute count

INC .CCLOCKH ; [5] ZP increment clock hours
LDX .CCLOCKH ; [3] ZP get clock hours
CPX #$18 ; [2] 24 hours?
BNE .cdcexit ; [3/2] no tickover
STY .CCLOCKH ; [3] reset hour count
.cdcexit
RTS ; [6]
; 43 bytes 21 cycles (no tickover) or 68 (full tickover)

This works perfectly well, but I'm reminded of that line in The Fly (1986) when Veronica Quaife (Geena Davis) asks Seth Brundle (Jeff Goldblum) what he's thinking, just after the disastrous 'Baboon' teleport experiment...
  • VQ: The world will want to know what you're thinking.
  • SB: Yuck! is what I'm thinking.
OK, it might not have been 'Yuck!', but you get the idea - this is perfectly functional, entirely correct, and spectacularly horrible, clunky code. We can do better; one option might be just to store the time as an accumulated number of jiffies, and then deconstruct that into hours, minutes and seconds whenever we want to read it - but although incrementing it is easier, the logic to divide it into meaningful units for display can be a bit cryptic, and converting a standard time format into jiffies when we want to reset it would be a pain as well.

But how about if we just store the TOD clock as a standard-unit countdown sequence, making each time unit update a decrement operation and avoiding the need to do specific comparisons or conversions - we can just branch if the unit crosses the 'zero' threshold, decrementing the next unit along and resetting to the start value again:

.updtcdc
DEC .CCLOCKJ ; [5] ZP decrement countdown clock jiffies
BPL .cdcexit ; [3/2] skip the rest if no tickover
LDA #$3B ; [2] counter reset value (#59)
STA .CCLOCKJ ; [3] ZP reset countdown clock jiffies

DEC .CCLOCKS ; [5] ZP decrement countdown clock seconds
BPL .cdcexit ; [3/2] skip the rest if no tickover
STA .CCLOCKS ; [3] ZP reset countdown clock seconds

DEC .CCLOCKM ; [5] ZP decrement countdown clock minutes
BPL .cdcexit ; [3/2] skip the rest if no tickover
STA .CCLOCKM ; [3] ZP reset countdown clock minutes

DEC .CCLOCKH ; [5] ZP decrement countdown clock hours
BPL .cdcexit ; [3/2] skip the rest if no tickover
LDA #$17 ; [2] counter reset value (#23)
STA .CCLOCKH ; [3] ZP reset countdown clock hours
.cdcexit
RTS ; [6]
; 29 bytes 14 cycles (no tickover) or 50 (full tickover)

This is nice and fast - decrements which do not cross a threshold are quick, and we only have to propagate the decrement to the next unit if it drops 'below' zero to $FF, which is indicated by the 'N' flag and simultaneously checked and branched using BPL (instead of having a specific comparison instruction). I wonder if this can be optimised, as there's obvious duplication on three of the units...

.updtcdc
LDA #$3B ; [2] unit reset value (#59 -> jiffy / second / minute)
LDX #$03 ; [2] ZP index
.dcdc
DEC .CCLOCK,X ; [6] ZP decrement countdown clock unit (j/s/m/h)
BPL .cdcexit ; [3/2] skip the rest if no tickover
STA .CCLOCK,X ; [4] ZP reset countdown clock
DEX ; [2] decrement index
BMI .cdcexit ; [2/3] all done, exit
BNE .dcdc ; [3/2] loop back to decrement cdc
LDA #$17 ; [2] unit reset value (#23 -> hour)
BPL .dcdc ; [3/3] loop back to decrement cdc
.cdcexit
RTS ; [6]
; 20 bytes 19 cycles (no tickover) or 88 (full tickover)

Well, it can be optimised for space, as we can save 9 bytes by combining the three duplicate sections into a loop and folding the 'hours' update in with some judicious flag-testing, but look at the timing - up by 5 cycles when we just do a jiffy update, and then progressively more as we tick other units over until we're 38 cycles up on the full update. This is a tough call, because it's a toss-up between consuming fewer precious ROM bytes, or fewer precious IRQ cycles. I think I'm going to leave it as the smaller code version for now and take the IRQ cycle hit, because it's more likely that ROM space will be at a premium later on than CPU cycles - but if we get ever get to the point where 30-odd or more cycles will make a critical difference to something, I can always trade in those 9 saved bytes.

The obvious drawback to a reverse-counted TOD clock is that it counts backwards from 23:59:59 down to 00:00:00 which is not especially useful when you want to know what the time actually is. But for that, and indeed to set the current time, we simply provide a pair of interface subroutines which do the unit inversion at the point the time is set or read; these subroutines may be fractionally slower than they would be if the clock counted forwards, but they don't get called every IRQ - the TOD update itself executes every jiffy, but when we need to read out the time, or set it, we can afford the fractional delay incurred by having to invert the values first.

We'll take care of those subroutines later, when we need them. Before that, I want to be able to see something on the screen, so it's time to start playing with the VIC.

No comments:

Post a Comment