' ' Nixie Clock using Model 01 rev. B CPU/IO card and Nixie daughterboard. ' Bud Box series, serial numbers 4, 5, 6, 7. ' ' FRONT PANEL: ' 4-digit display. ' TIME/DATE switch, displays time or date; HH:MM or DY:MO. ' SEC/YEAR switch, momentary, displays 00:SS or YYYY, depending ' on TIME/DATE switch. ' ' REAR PANEL: ' SET switch. ' CHANGE rotary encoder. ' ' The RTC chip keeps year as 00 - 99; we display as ' 1952 - 2051; since 2000 is a leap year it works ' just fine. 1952 is the year Nixies were introduced ' by Burroughs Corp. ' ' SET MODES: ' If SET is pressed on power up, display mode is selected: ' 12 hour, blinking colon ' 24 hour, blinking colon ' 12 hour, non-blinking colon ' 24 hour, non-blinking colon ' When SET is pressed again, clock returns to normal mode. ' ' ' When SET is pressed, CPU goes into the set loop. ' The RTC runs, but the display is frozen. Digits ' blink in SET mode. ' ' First press: ' Displays time or date, as indicated by F.P. switches. ' Turning CHANGE changes whatever is displayed. ' Pressing SEC/YEAR when time displayed simply ' displays 00:00, since seconds is always ' zeroed on set. ' ' Second press: ' Returns to normal operation. ' ' The in-memory values are changed; the RTC chip ' is written to on the second press of SET. If no ' value changes, the clock is not set; this way, if ' SET is pressed accidentally but the knob not turned, ' nothing changes. ' ' ' Tom Jennings ' ' 26 Feb 2000 ' ' Fixed erratic velocity in readrot(). ' ' NIGHTMARE problems. The sequence in readrot(): ' comf portB, W ' andlw 0c0h ' btfss status, 2 ' ... ' ' caused undiagnosable problems (display problems when SW_SET, SW_DATE on and rotary encoder ' turned -- OK if SW_DATE in TIME mode). Changed to two btfss's and it works fine. Also tried ' movf portB, W : xorlw 255 : ... to no avail. Reading portB does this? ' ' Second, and more mysterious, the colon-blinking code at m3: is seriously fucked: the sequence ' ' colon= 1 ' if timer < TIMEON then xxx ' colon= 0 ' xxx: ' ' does not work! colon always equals 1. timer is a word, TIMEON is 400. Is < 8-bit only? What ' the hell is going on? Currently, colon state is exactly backwards. Ship it. ' ' ' ' 24 Feb 2000 ' Decreased PW measurement (pause) in readrot, to minimize ' excessive "high velocity" returns. (From 25 to 20.) ' ' 5 Nov 1999 ' Cleaned up comments, final code. ' ' 26 Oct 1999 ' Fixed to/from BCD array address error; clock seems solid. ' ' 22 Oct 1999 ' POST in assembly, rearranged display-mode-set logic. ' ' 20 Oct 1999 ' More in assembly. Debugging work of the 17th. ' ' mru 17 Oct 1999 ' Recoded getclock(), setclock() in assembly, to cram more code in. ' Moved ephemeral flags into B1, from B0. Added 'doset' to avoid ' changing clock if SET is pressed, then pressed again without ' changing anything. Managed to fit POST back in. ' ' mru 10 Oct 1999 ' new 6 Oct 1999 ' ' Adapted from Model 11b, serial number 2. See that code ' for original revisions. ' ' Copyright Tom Jennings, 1999. ' ' e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e ' ' Bit flags are all in B0, which is stored in EEPROM, and reloaded ' on power up. Some values are global, eg. 12/24 hour display mode. Some ' are local, eg. TIME/DATE switch pressed. The colon and AM/PM indicators ' states are stored here as well. Most flags are bit-position-dependent, ' used for programming convenience. ' symbol flags= b0 ' reserved for bit flags ' ' c ' s d d t b ' e d m m l ' c a o o i ' y t d d n ' r e e e k ' ---------------- ' 0 0 - 0 0 hh mm 12 hr time, blinking colon ' 0 0 - 0 1 hh mm 12 hr, no blink ' 0 0 - 1 0 hh mm 24 hr time, blink ' 0 0 - 1 1 hh mm 24 hr, no blink ' 1 0 - 0 0 00 ss ' 1 0 - 0 1 00 ss ' 1 0 - 1 0 00 ss ' 1 0 - 1 1 00 ss ' 0 1 0 - - mm dd ' 0 1 1 - - dd mm ' 1 1 0 - - yyyy ' 1 1 1 - - yyyy ' symbol TIMEBITS= 3 ' isolate TIME mode bits symbol TIMEBITM= 252 ' isolate all but TIME mode bits ' Stored in EEPROM. symbol cblink=bit0 ' (0..1) 1 == disable colon blink symbol tmode= bit1 ' (0..1) 1 == 24 hour mode symbol dmode= bit2 ' (0..1) 1 == MM DD or DD MM mode ' ' e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e ' ' Not stored in EEPROM. ' symbol flags2=b1 symbol doset= bit8 ' (0..1) 1 == set the clock symbol secyr= bit9 ' (0..1) 1 == display sec/year (switch) 'symbol = bit10 ' (0..1) 1 == flash display during SET ' ' ' ' ' ' ' ' ' ' ' ' ' colon, pm must be MS, 2nd MS bits; they're ORed into SRA output directly. symbol pm= bit14 ' (0..1) 1 == PM else AM symbol colon= bit15 ' (0..1) 1 == colons lit symbol HWBITS= 192 ' mask these two hardware bits ' ' ' ' ' ' ' ' ' ' ' symbol i= b2 symbol n= b3 symbol k= b4 symbol j= b5 symbol l= b6 'symbol b7= symbol TIMEMAX= 900 ' maximum timer period symbol TIMEON= 400 ' amt of above displays are ON in SET mode. symbol timer= w4 ' timer for flashing the display during SET. ' w4= b8:b9 symbol lastA= b10 ' most recent written to SR A symbol lastB= b11 ' most recent written to SR B ' ' Time & date data as used by the RTC routines. See the getclock() and setclock() ' code for data handling details (eg. packing/unpacking of BCD for the compiler ' RTC routines). These always contain the last-read RTC values. ' symbol GSARRAY= B15 ' address of first array element. symbol year= b15 ' (data referenced by these) symbol month= b16 symbol day= b17 symbol daywk= b18 symbol hour= b19 symbol minute= b20 symbol second= b21 ' ' Pin assignments (eg. 3 = pin3). Very annoying that the compiler doesn't ' syntactically allow 'pin 3' or 'pin SD'. Inputs are pulled up, and are ' logically ON when pulled to ground. ' symbol SW_DATE= pin7 ' (0..1) 0 == TIME/DATE in DATE position symbol ROTA= 6 ' Rotary encoder bit A, pin # symbol ROTB= 5 ' Rotary encoder bit B, pin # symbol SW_SECYR= pin4 ' (0..1) 0 == display time or date (else sec or year) 'symbol SW_SET= PA4 ' (0..1) 0 == RUN/SET in SET position ' ' Data is written to the shift registers by asserting data on the SD pin, and clocking ' the appropriate SR clock pin. 8 bits per shift register, MS bit output first. ' symbol INVAL= 15 ' invalid 7441 code; blanks display symbol SDpin= pin0 ' pin number, Serial Data to shift reg symbol SD= 0 ' Serial Data pin number ' SRC not used on 4-digit machine. 'symbol SRC= 3 ' SR C clock (SS, YY) symbol SRB= 2 ' SR B clock (MM, DD) symbol SRA= 1 ' SR A clock (HH, MO) ' ' Startup code starts here. - - - - - - - - - - - - - - - - - - - - - - - - - ' main: dirs= %00000111 ' MS bits inputs, rest outputs. pins= 0 ' deassert device selects ' peek 133, n : n= n | 24 : poke 5, n ' make PA4 and PA3 inputs call POST ' test display, returns N, read 0, flags ' read global settings from EEPROM. if n = 0 then run ' if SET was pressed during 24:00, m3: colon= 1 ' turn colon on, if timer > TIMEON then m3a ' when display blinks off, colon= cblink ' blink or not, m3a: gosub display ' display in current mode, call setsw : if n = 1 then m5 ' exit if SET pressed again. call readrot ' read CHANGE encoder, if SW_DATE = 0 then m4 ' if TIME display, i= flags + n & TIMEBITS ' incr/decr TIME mode bits flags= flags & TIMEBITM | i ' add 'em back in goto m3 m4: dmode= dmode + n ' easy when there's only 1 bit! goto m3 m5: write 0, flags ' write out new mode. ' End of startup, beginning of runtime. - - - - - - - - - - - - - - - - - - - - - - - - - - - - ' ' The main loop here is the basic clock function. TIME/DATE determines which three ' bytes are displayed. RUN/SWITCH must be in the SET switch for a long time before ' we enter SET mode; this not only debounces the switch, it prevents spurious writes ' on power down where the switch pullups go down before CPU power. ' run: flags2= 0 ' clear timer, doset call getclock ' read RTC data, unpack and copy, colon= second + 1 | cblink ' colon blink control, colon= colon & SW_DATE ' always off for date display, timer= 0 ' prevent blinking, gosub display ' display current values, call setsw : if n = 0 then run ' loop 'til SET pressed. ' ' Set time, date until SET is pressed again. ' set: call setsw ' read the SET switch, n= n + n + doset ' bit1=sw, bit0=doset branch n, (set2, set2, run, set1) set1: call setclock goto run set2: colon= SW_DATE ' on for time display, gosub display ' update display, and blink, call readrot ' else read CHANGE encoder (sets N, I) if n = 0 then set ' if CHANGE turned, n= n & 4 ' bit2=1 iff readrot() returns 255 n= n + SW_DATE + SW_DATE ' bit1=1 iff TIME (not DATE), n= n + SW_SECYR ' bit0=1 iff normal (not SEC/YR) branch n, (syi, sdi, stz, sti, syd, sdd, stz, std) sti: minute= minute + i ' increment minute, 0..59, if minute < 60 then stz ' if out of bounds, minute= 0 : hour= hour + 1 ' increment hour, 0..23, if hour < 24 then stz : hour= 0 ' (cheaper than modulo!) stz: second= 0 ' always clear seconds, doset= 1 : goto set ' flag data-changed, std: minute= minute - i : if minute < 60 then stz ' unsigned int minute= 59 ' (eg. 0 - 1 = 255) hour= hour - 1 : if hour < 24 then stz ' (unsigned int) hour= 23 : goto stz ' wrap past midnight, sdi: day= day + i : gosub luday ' increment day, lookup days in the month, if day <= n then stz ' if > days in the month, day= 1 : month= month + 1 ' first day, next month, if month < 13 then stz ' wrap dec to jan month= 1 : goto stz luday: lookup month, (0,31,28,31,30,31,30,31,31,30,31,30,31), n ' find max day, k= year & 3 + month : if k <> 2 then ret ' if (year % 4) && (month == 2), n= 29 ' 29 days in Feb. ret: return sdd: day= day - i ' decrement day, if day = 0 then sd1 ' (eg. if day=1 - 1) if day < 32 then stz ' (eg. if day=1 - 10) sd1: month= month - 1 ' previous month, if month = 0 then sd1a ' 1 <= N <= 12 if month < 13 then sd2 sd1a: month= 12 ' wrap jan to dec, sd2: gosub luday : day= n ' last day of month, goto stz syi: year= year + i : if year < 100 then stz year= 0 : goto stz syd: year= year - i : if year < 100 then stz ' 99..2,1,0,255 year= 99 : goto stz ' then wrap to 99. ' ' Write time or date to the four Nixie displays. ' Handles display mode processing, and blinking for SET ' mode. ' Uses N, I, J, K, modifies flags. ' display: timer= timer + 1 ' run the blinker, if timer < TIMEON then disp3 ' if times out, blank display, k= lastA | INVAL : call outA ' blank LS hour, k= 255 : call outB ' blank MS, LS minutes. if timer < TIMEMAX then ret ' when reached max. off time, timer= 0 ' reset timer return ' ' Dispatch if DATE pressed, else set up for time display. ' disp3: pm= 0 ' default off. if SW_DATE = 0 then dispd ' date, else time, ' ' Time display. ' dispt: j= SW_SECYR + SW_SECYR + tmode ' bit1=SECYR, bit0=time mode, k= hour ' (preset for 24hr time), branch j, (ds, ds, d0, d1) ' (switch is 0=pressed), ' 12hr. time, AM, PM. d0: if hour < 12 then d0a ' if afternoon, pm= 1 : k= hour - 12 ' set PM, fix hour, d0a: if k <> 0 then d1 : k= 12 ' noon, midnight display as "12" ' 12 or 24 hr. time (AM/PM pre-set, above). d1: call outABCD ' HH (pre-set) k= minute : call outBBCD ' MM return ' Time, seconds. ds: k= 0 : call outABCD ' 00 in upper, k= second : call outBBCD ' SS in lower. return ' ' Display date. ' dispd: j= SW_SECYR + SW_SECYR + dmode ' bit1=SECYR, bit0=dmode branch j, (dy, dy, d2, d3) ' Date, MM DD. d2: k= month : call outABCD ' MM k= day : call outBBCD ' DD return ' Date, DD MM. d3: k= day : call outABCD ' DD k= month : call outBBCD ' MM return ' Date, YYYY. The RTC chip assumes that it's two-digit year, if divisible ' by 4, is a leap year. The Model 11 supports 4-digit years from 1952 through ' 2051 (1952 is when Nixies were introduced... it just seems approriate). ' So the year must be offset by 52, and wrapped when it exceeds 99. ' ' RTC year 40 displays as "1992"; RTC year 47 displays as "1999", and ' RTC year 48 displays as "2000". Full-scale is 99, which displays as ' "2051". ' dy: k= 19 ' assume century is "19xx", if year < 48 then dy1 ' if clock year > 48 ("1999"), k= 20 ' it's "20xx", dy1: call outABCD ' display year MSDs; k= year + 52 : if k < 100 then dy2 ' if overflow, k= k - 100 ' do modulo 100, dy2: call outBBCD ' output year LSDs. dz: return END 'BASIC asm ; ; Convert canonical time & date to packed BCD, write to the RTC; ; convert back to canonical. (Leaves the data consistently usable ; to other routines at little processing cost). ; _setclock call _setgs ; set up, _sc1 call _tBCD ; [i]= toBCD([i]) incf FSR ; ++i, decfsz _L goto _sc1 ; loop... call _CLOCKSET ; set the clock, goto _gc0 ; return data to canonical. ; ; Set up for getclock()/setclock(). ; _setgs movlw _GSARRAY ; first address, movwf FSR ; FSR= movlw 7 movwf _L ; L= loop byte count, bcf RP0 return ; ; Read the clock chip (packed BCD) and convert to canonical, in-place. ; Also has an entry point for converting back to canonical, jump to from ; setclock(). ; Modifies K, L. ; _getclock call _CLOCKGET ; read BCD time, ; Entry point from setclock(). _gc0 call _setgs ; set up first, _gc1 call _fBCD ; [i]= fBCD([i]); incf FSR ; ++i; decfsz _L goto _gc1 ; loop... goto done ; ; Convert packed BCD to canonical. Address of operand in W. Modifies J. ; Entry point fBCD is for loops that explicitly set FSR. ; ; l= k / 16 * 10 : k= k & $0f : k= k + l ; ; K / 16 * 10 is done with three right-shifts and an intermediate ; save; eg. N * 10 is (N * 8) + (N * 2), and we already have ; N * 16; so one shift-right makes N * 8, two more make N * 2. ; _fromBCD movwf FSR ; point to var, _fBCD movf INDF, w ; get BCD value, movwf _J ; save in J, andlw 0f0h ; upper digit only, movwf INDF ; in reg, rrf INDF ; make div 8 movf INDF, w ; save in W, rrf INDF rrf INDF ; div 2 addwf INDF ; the sum in reg, movf _J, w ; get lower digit, andlw 0fh ; mask off lower digit, addwf INDF ; add together. return ; ; Convert decimal value in K to packed BCD in K. Address of ; operand in W. Modifies J. ; Entry point tBCD is for loops that explicitly set FSR. ; _toBCD movwf FSR ; point to var, _tBCD clrf _J ; clear quotient, movlw 10 _toB1 incf _J ; incr quotient, subwf INDF ; k= k + (-10) NOTE 2's COMPL ADD! btfsc status, 0 ; loop until underflow, goto _toB1 _toB2 addwf INDF ; undo last subtract, decf _J ; uncount last iteration, swapf _J, w ; put (K / 10) in upper nybble, ; WHY DOES THIS NEED TO BE THERE!? ; Without it, BCD x0 (10 20 30 40 50 00...) produces an invalid code ; (outside 0..9). It means there is junk in the MS nybble of ; J, which is disturbing because J is simply a counter 0..9, ; assuming the input is maximum BCD 99, equal to $99. ; Worse yet, it happens only for SECONDS. ; andlw 0f0h ; mysterious iorwf INDF ; add in remainder. return ; ; Output decimal value K to the MS two Nixies (hour/month), converting to ; packed BCD first (outABCD). The colon and PM bit are ORed in before ; output. Modifies J, K. ; _outABCD movlw _K ; point to K, call _toBCD ; convert K to BCD, _outA movf _FLAGS2, w ; get colon, AM/PM bits, andlw _HWBITS ; strip clean, iorwf _K ; OR in BCD bits, movf _K, w ; subwf _lastA, w ; lastA - K --> W btfsc status, 2 ; if zero (same), goto done ; no change, don't write, movf _K, w ; else movwf _lastA ; remember last bits output movlw 8 movwf _J ; set bit loop counter, _oa1 bcf portB, _SD ; set data bit= 0 rlf _K ; move MSB to carry, btfsc status, 0 ; if carry set (MSB == 1) bsf portB, _SD ; assert a 1, bsf portB, _SRA ; clock data bit out, bcf portB, _SRA decfsz _J ; repeat. goto _oa1 goto done ; ; Same as above, but outputs to the two LS Nixies. ; _outBBCD movlw _K ; point to K, call _toBCD ; convert K to BCD, _outB movlw 8 movwf _J ; set bit counter, movf _K, w ; subwf _lastB, w ; lastB - K --> W btfsc status, 2 ; if zero (same), goto done ; no change, don't write, movf _K, w ; else movwf _lastB ; remember last bits output _ob1 bcf portB, _SD ; set data bit= 0 rlf _K ; move MSB to carry, btfsc status, 0 ; if carry set (MSB == 1) bsf portB, _SD ; assert a 1, bsf portB, _SRB ; clock data bit out, bcf portB, _SRB decfsz _J ; repeat. goto _ob1 goto done ; ; Performs Power On Self-Test of the display. Runs through all usable ; display combinations. It also returns N indicating if the SET switch ; is pressed during the display of 24:00, to allow display mode set. ; ; 00:00, 11:11, 22:22, ; 23:33, 23:44, 23:55, ; 23:56, 23:57, 23:58, 23:59. ; _post movlw 11 movwf _N ; N= initial minute increment, movwf _I ; I= initial hour increment, movwf _lastA movwf _lastB movlw 2 ; W= interations, call _postdr ; display 00:00, 11:11, (now 22:22) movlw 1 movwf _I ; I= 1, call _postdr ; display 22:22, (now 23:33) clrf _I ; I= 0, movlw 2 call _postdr ; display 23:33 .. 23:44 (now 23:55) movlw 1 movwf _N ; N= 1, movlw 5 ; display 23:55 .. 23:59, call _postdr ; (now 23:60) incf _hour clrf _minute ; display 24:00 movlw 1 ; 1 iteration, fall through. call _postdr ; ; Set up for display mode set. ; decf _hour movlw 59 movwf _minute ; leave time as 23:59 movlw 12 movwf _month movlw 31 movwf _day ; and date as 12/31, goto _setsw ; check SET switch. ; ; Display HH:MM, pause, increment hour and minute by N and I, respectively. ; Enter with W=iteration count. ; _postdr movwf _L ; set loop count, _pd1 movf _hour, w movwf _K call _outABCD ; display HH, movf _minute, w movwf _K call _outBBCD ; display MM, movlw 1 movwf R0+1 ; pause for 400 mS movlw 90h ; (01 90 = 400) call pause@XW movf _I, w ; increment, addwf _hour movf _N, w addwf _minute decfsz _L ; loop. goto _pd1 return ; ; Return N=1 if the SET switch (on PA4) is pressed. ; _setsw clrf _N ; assume not, btfsc portA, 4 ; if not pressed, goto done ; return N= 0. incf _N ; N= 1, _ssw1 call done ; don't let WDT kill us! btfss portA, 4 ; wait til it is released. goto _ssw1 goto done ; ; Read the rotary encoder. Returns in N; 0 if no motion ; detected, 1 if clockwise, 255 (-1) if counterclockwise. ; Also generates an ad hoc velocity, in I; 1 if "slow", or ; 10 if "fast"; which corresponds to the width of the A ; pulse... Ideally, we'd measure the period between the start ; of each ABCD sequence, but without interrupts it's ; too irritating. ; ;-------+ +------------ ROTA ; | | ; +--+ ;--------+ +----------- ROTB ; | | ; +--+ ; A B C D ; ; This is the simplest possible interpretation of a rotary ; encoder; ROTA is used as a data-presence bit. When it goes ; true (low) ROTB is read to determine direction. The kludgey ; part is that we have to wait until ROTA goes idle again ; before returning; this is acceptable because the entire ; ABCD sequence takes under 40 mS (15mS typical). ; _readrot clrf _N ; assume idle, btfsc portB, _ROTA ; if ROTA false (high), goto _rr1 ; exit. movlw 1 ; assume clockwise, btfss portB, _ROTB ; test, movlw 255 ; else CCW, movwf _N clrf _I _rr0 incf _I ; measure on time of ROTA+ROTB movlw 1 call pause@0W ; pause, btfss portB, _ROTA goto _rr0 btfss portB, _ROTB goto _rr0 movlw 10 ; on time threshold subwf _I ; movlw 10 ; velocity=high btfsc status, 0 ; if carry set, _rr1 movlw 1 ; velocity=low movwf _I ; set velocity goto done endasm