KC9UDX wrote:
Interesting! I have thought about using NMI for RTC where accuracy would be critical, but since I had never tried it, I thought I would wait to hear if anyone else ever did something like that.
As I earlier opined, this is not a good use of NMI, as I will explain.
In my POC units, the only thing attached to NMI is a push button header and a Maxim DS1813, the latter which dampens the push button. The NMI handler (to simplify the explanation) ultimately jumps to the BRK entry point of the machine language monitor. What this means is if I have a program running and it gets stuck in a loop I can non-destructively recover control by pulsing NMI. As the push button is the only possible source of an NMI I don't have to poll for multiple causes, nor do I have to be careful to clear all NMI sources. The jump into the BRK entry of the M/L monitor automatically dumps the MPU's registers, so I can see what was going on when the MPU got stuck.
Timekeeping on POC V1.1 and POC V2 is driven by a 100 Hz jiffy interrupt generated by a free-running timer in the UART, the latter which is attached to the IRQ line, not NMI. The timer IRQ drives two jiffy counters on page zero. One jiffy counter counts upward from $00 to $64 (100) and is for timekeeping. The other jiffy counter counts down from $64 to $00 and is for implementing a time delay feature. All timekeeping data are stored on page zero.
When the timekeeping jiffy counter hits $64 it is reloaded with $00, and a 32 bit uptime counter and a 40 bit time-of-day counter that keeps track of the number of seconds that have elapsed since the epoch are incremented. Uptime can be read through a BIOS API call. The time-of-day can be read or written through BIOS API calls.
If the time delay counter, which is 16 bits, is non-zero it will be decremented each time its associated jiffy counter reaches $00. A BIOS API call is used to set the delay and then stall the system until the time delay counter reaches
$00:0000, where
$00: is the jiffy counter itself.
External programs are used to convert the raw time-of-day value to human-readable form, as well as to set the time-of-day from human-readable form (with time zone and Daylight Saving Time correction). Hence the work that has to be done by the interrupt handler is minimal—only when human interaction is needed do things get complicated. A 40 bit time-of-day counter is sufficient in range to produce accurate timekeeping well beyond the year 10,000. For simplification, the conversion code I've written handles the range 00:00:00 UTC on October 1, 1752 (which is the epoch) to 23:59:59 UTC on December 31, 9999. If I and the POC are still around during the year 9999 I will update the software.
The accuracy of this mess is as good as the accuracy of the 3.6864 MHz clock generator that drives the UART, provided IRQ processing is not disabled for more than about 10 milliseconds, e.g., with the SEI instruction. Testing has demonstrated that the average time-of-day drift amounts to less than one second per month in the environment in my office, which is around 23° C.
Incidentally, the choice of epoch has to do with avoiding negative time values, as well as not having to deal with the complications that would arise in converting dates prior to September 14, 1752, which is when the British Empire abandoned the Julian calendar in favor of the modern Gregorian calendar.
Below is an excerpt from POC V1.1's interrupt handler, in which timekeeping is maintained. As you can see, it's not complicated. Since this is running on the 65C816, I am taking advantage of 16 bit processing in incrementing or decrementing counters. However, the additional code that would be required to do the same with the 65C02 is trivial.
Code:
;UART C/T IRQ PROCESSING
;
lda io_acia+nx_isra ;get block A ISR
bne .iirq010 ;UART is interrupting
;
jmp iirq02 ;UART is not interrupting
;
.iirq010 bit #nxpctirq ;C/T interrupting?
beq iirq01 ;no, must be I/O
;
bit io_acia+nx_rcta ;yes, clear interrupt
longa ;16 bit accumulator operations
;
;
; process time delay counter...
;
lda tdsecct ;seconds counter
beq .iirq030 ;no delay in progress
;
ldx tdjiffct ;time delay jiffy counter
dex ;decrement
bne .iirq020 ;not zero, just decrement
;
dec tdsecct ;secs=secs-1
ldx #hz ;reset jiffy counter
;
.iirq020 stx tdjiffct ;new counter value
;
;
; process free-running clocks...
;
.iirq030 ldx jiffct ;clock jiffy counter
inx ;increment
cpx #hz
bcc .iirq050 ;not time to update
;
ldx #0 ;reset jiffy count
inc uptime ;bump uptime LSW
bne .iirq040 ;done with uptime
;
inc uptime+s_word ;bump uptime MSW
;
.iirq040 inc uxtime ;bump UNIX time LSW
bne .iirq050 ;done with UNIX time
;
inc uxtime+s_word ;bump UNIX time MID
bne .iirq050
;
inc uxtime+s_dword ;bump UNIX time MSW
;
.iirq050 stx jiffct ;set new jiffy count
;
...program continues...
In the above,
C/T means "counter/timer" and the symbol
hz is set to 100.
LSW and
MSW mean, respectively, least significant word and most significant word. "UNIX" time refers to the time-of-day counter, which is not actually UNIX time, since the latter uses a signed integer.