Timekeeping is the product of a continuously-running "timepiece", which may be a wall clock, wristwatch, a counter in computer memory, etc. The timepiece keeps time without regard to whether or not someone or something is "watching" it. The process of referring to a timepiece or setting it to the correct time is not part of the timekeeping process. As will be seen, this distinction is important in the realm of computers, as the software used to keep time is not the same as that used to read or set the time, or to convert time between machine-readable and human-readable formats.
In this post, I will focus on the mechanics of timekeeping itself, and will discuss the process of reading or setting the time in a subsequent post. It should be noted that "time" in this sense refers to both the time-of-day (wall clock time or real time) and the calendar date, since the methods described in this topic manipulate a single variable that encapsulates both date and time-of-day.
As previously described, keeping time in UNIX and Linux is a process of incrementing an integer counter at one second intervals, which is an uncomplicated task. Code in an interrupt service routine (ISR) is executed at regular intervals in response to a periodic hardware interrupt request that is referred to as a "jiffy" IRQ. The source of the jiffy IRQ is the "time base," whose stability ultimately determines the degree of long-term timekeeping accuracy that will be attained.
Choosing a jiffy IRQ rate involves a tradeoff: a slower rate will generally produce coarser timekeeping resolution. However, the relatively low IRQ rate will improve perceived foreground process performance. On the other hand, a faster rate will produce finer timekeeping resolution, but at the expense of performance, since the microprocessor (MPU) will be processing more frequent interrupts.
It is recommended that the jiffy IRQ rate be a round number that can produce an even number of milliseconds between each increment of the time counter, such as 50 (20ms resolution), 100 (10ms resolution) or 250 (4ms resolution)—50 is the minimum rate that I recommend. The rate should be such that the corresponding number can fit into a single byte, an important consideration with the eight bit 65xx MPUs. In my POC unit, I use a 100 Hz jiffy IRQ, which is the most commonly used rate in UNIX systems (note that current Linux kernels use 250 Hz).
In the following discussion, UNIX time is maintained in a field defined as
uxtime, which is a 48-bit integer.
uxtime will be incremented at exactly one second intervals. A separate counter byte, referred to as
jiffct—the "jiffy counter"—is used to keep track of how many jiffy IRQs have been serviced since the last update to
uxtime.
jiffct is initially set to value
hz at boot time,
hz being the jiffy IRQ rate that has been defined for your system. Subsequently,
jiffct is decremented with each jiffy IRQ. When
jiffct reaches zero, it is reset to
hz and
uxtime is incremented. Of course, the ISR has to determine if the jiffy IRQ timer was responsible for the IRQ—if it wasn't, neither
jiffct or
uxtime would be touched.
The following flowchart illustrates a procedure that may be employed to keep time:
Attachment:
File comment: Timekeeping Update ISR Flowchart
timekeeping_isr_flowchart.gif [ 65.82 KiB | Viewed 3385 times ]
As can be seen, it isn't complicated—in fact, the process is designed to be as simple and succinct as possible. Here's the code that would accomplish the above on a 65C02 or on a 65C816 running in emulation mode:
Code:
;process system time with 65C02...
;
dec jiffct ;time to update?
bne l0000020 ;no
;
ldx #hz ;yes (hz = jiffy IRQ rate)
stx jiffct ;reset jiffy IRQ counter
ldx #0 ;time field index
ldy #s_time_t ;time field size in bytes
;
l0000010 inc uxtime,x ;bump system time
bne l0000020 ;done
;
inx ;bump index
dey ;decrement count
bne l0000010 ;not done
;
l0000020 ...program continues...
The code for a 65C816 is somewhat different, reflecting that device's ability to handle 16-bit data:
Code:
;process system time with 65C816 in native mode...
;
sep #%00110000 ;8 bit registers
dec jiffct ;time to update?
bne l0000010 ;no
;
ldx #hz ;yes
stx jiffct ;reset jiffy counter
rep #%00100000 ;16-bit accumulator
inc uxtime ;bump time least significant word
bne l0000010 ;done with time
;
inc uxtime+s_word ;bump time middle word
bne l0000010
;
inc uxtime+s_dword ;bump time most significant word
;
l0000010 ...program continues...
In the 65C816 code,
s_word is
2, the number of bytes in a (16 bit) word, and
s_dword is
4, the number of bytes in a double (32 bit) word. Each increment operation processes 16 bits at a time, and looping and indexed addressing are completely avoided, improving execution speed.
Speaking of execution speed, it is worth noting that despite the jiffy IRQ rate, the actual amount of real time expended in processing
uxtime is very small. Assuming a 100 Hz jiffy IRQ, 99 of 100 IRQs will only decrement
jiffct. When
jiffct does reach zero the least significant byte of
uxtime (i.e., byte offset $00 in the field) will be incremented, which will occur once per second. At 256 second intervals, byte offset $01 of
uxtime will have to be incremented. At 65,536 second intervals, byte offset $02 of
uxtime will have to be incremented, and so forth. It should be patent that keeping time in this fashion demands very little in the way of processor resources.
That said, for best performance,
jiffct and
uxtime should be on page zero (direct page with the 65C816) to take advantage of the performance gain. With the 65C816, direct page can be made to appear anywhere in the first 64 kilobytes of address space (i.e., bank $00), a feature that gives the '816 programmer a lot of flexibility. However, note that a direct page access will incur a one clock cycle penalty if direct page isn't aligned to an even page boundary.
Before moving on, a comment about programming style is in order. "Magic numbers" such as the jiffy IRQ rate (
hz) and the size of the time variable (
s_time_t) should always be declared in your source code prior to first use, and never embedded in instructions as hard-coded numbers. Burying magic numbers in code tends to decrease understandability and may open the door to obdurate bugs caused by mistyping a number. Also, should something be changed it's much less work to change one declaration in an INCLUDE file than to hunt down multiple magic numbers that might be scattered about in several source files.
If
jiffct and
uxtime are to be on page zero as recommended they should be defined prior to any code references to them. Many assemblers will assume that if a location has not been defined prior to first reference said location is an absolute address, even though a subsequent definition says otherwise. The result is that instructions that act on the location will be assembled using absolute addressing modes, not zero page modes.
Continuing with programming, an interesting problem arises for foreground processes that need to get or set system time.
uxtime never stops incrementing, even when a reference must be made to it. What this means is the possibility exists that an access to
uxtime may produce erroneous results because the access was interrupted and
uxtime was updated during the interrupt processing, resulting in a "carry" error. Hence there should be a way to prevent a carry error by deferring updates to
uxtime when an access is required.
One method is to temporarily halt IRQ processing (
SEI) and then immediately resume it after the copy has completed (
CLI). Halting IRQ processing is simple and will minimally affect overall system performance. However, doing so may have an adverse effect on any interrupt-driven I/O processing, especially time-critical data reception from a UART that lacks a receiver FIFO (e.g., the MOS6551 or MC6850). Also, good operating system design generally frowns on the suppression of IRQ processing as part of a routine foreground API call, due to the potential for deadlock if something goes awry during the API call.
Another method is to using a semaphore to tell the ISR to defer the update of
uxtime for one jiffy period—presumably, the foreground process accessing
uxtime can finish in less than one jiffy period (if not, the operating system design needs rethinking, the hardware is in
desperate need of an update—or both). Use of a semaphore doesn't disrupt any IRQ-driven activities and hence won't create any conditions that might provoke deadlock. However, some extra code is required in the ISR to manage the semaphore, which means there will be a small but inexorable amount of performance degradation.
Here are examples of how a semaphore, defined as
semaphor, could be used to avoid
uxtime read/write carry contretemps:
Code:
;process system time with 65C02 using semaphore...
;
; semaphor: xx000000
; ||
; |+————————> 0: previous update not deferred
; | 1: previous update was deferred
; +—————————> 0: process time update
; 1: defer time update
;
; Bits 6 & 7 of semaphor must never be simultaneously set!
;
bit semaphor ;okay to update?
bpl l0000010 ;yes
;
lsr semaphor ;no, tell next jiffy IRQ...
bra l0000040 ;this update was deferred
;
l0000010 dec jiffct ;update time field?
bne l0000030 ;no
;
ldx #hz ;yes
stx jiffct ;reset jiffy IRQ counter
ldx #0 ;time field index
ldy #s_time_t ;time field size in bytes
;
l0000020 inc uxtime,x ;bump system time
bne l0000030 ;done
;
inx ;bump index
dey ;decrement count
bne l0000020 ;not done
;
l0000030 lda #%01000000 ;deferred update flag bit
trb semaphor ;deferred update pending?
bne l0000010 ;yes, process it
;
l0000040 ...program continues...
The following is for the 65C816 running in native mode:
Code:
;process system time with 65C816 in native mode using semaphore...
;
; semaphor: xx000000
; ||
; |+————————> 0: previous update not deferred
; | 1: previous update was deferred
; +—————————> 0: process time update
; 1: defer time update
;
; Bits 6 & 7 of semaphor must never be simultaneously set!
;
sep #%00110000 ;8 bit registers
bit semaphor ;okay to update?
bpl l0000010 ;yes
;
lsr semaphor ;no, tell next jiffy IRQ...
bra l0000040 ;this update was deferred
;
l0000010 dec jiffct ;update time field?
bne l0000030 ;no
;
ldx #hz ;yes
stx jiffct ;reset jiffy counter
rep #%00100000 ;16-bit accumulator
inc uxtime ;bump time least significant word
bne l0000020 ;done with time
;
inc uxtime+s_word ;bump time middle word
bne l0000020
;
inc uxtime+s_dword ;bump time most significant word
;
l0000020 sep #%00100000 ;8 bit accumulator
;
l0000030 lda #%01000000 ;deferred update flag bit
trb semaphor ;deferred update pending?
bne l0000010 ;yes, process it
;
l0000040 ...program continues...
semaphor is a dual purpose flag (ideally, on page zero) that indicates if an update should be deferred and also if the previous update was deferred. In order to tell the ISR to defer the update of
time, the foreground process must set bit 7 (
and only bit 7) of
semaphor prior to accessing
uxtime. Before the ISR does anything to the time it checks the state of
semaphor. If bit 7 is clear a normal update occurs. If bit 7 is set, the ISR "knows" that it is to skip the update and "reminds" itself that it did so by shifting bit 7 to bit 6.
On the next pass through the ISR, an update will occur and then
semaphor will be tested to determine if the previous update had been deferred. The
TRB instruction is used to both determine if the previous update was deferred and to clear bit 6 of
semaphor. If bit 6 was set in
semaphor a second pass will occur through the time update code, thus making up for the deferred update.
Typically, reading or writing the system time is an operating system kernel function (in UNIX and Linux, writing is restricted to the
root user). The access procedure in either case would be to set bit 7 of
semaphor to tell the ISR to defer updating and then immediately read or write
uxtime. This must be accomplished in the time space between successive jiffy IRQs.