;:ts=8 ; ; EXAMPLE PROGRAM #3 ; ; This is most of the time-of-day clock code which appears in a couple of ; my projects. The interrupt handler maintains the clock. "setdate" and ; "getdate" set and read the clock, respectively. The mainline has code ; to parse an ASCII date/time into the internal form, and to translate the ; internal form back to readable ASCII. The whole thing is tied together ; in a simple clock program, allowing the clock to be set from the terminal ; and then displayed on the LCD. ; ; The code makes no allowance for an inaccurate CPU clock. A software ; "tweak" would probably be necessary, adding or subtracting a 1/100 tick ; every so often, to make the clock accurate enough for real use. ; ; Internal memory allocation ; 00-07: Register bank 0 -- used for non-interrupt code ; 08-0F: Register bank 1 -- used for interrupt code ; 10-17: Register bank 2 \ ; 18-1F: Register bank 3 > misc. variables go here ; 20-2F: Bit addressable space / ; 30-7F: Generic memory -- used for stack .equ ireg0, 8 .org 0x10 hours: .skip 1 minutes: .skip 1 seconds: .skip 1 day: .skip 1 month: .skip 1 year: .skip 1 hours_copy: .skip 1 minutes_copy: .skip 1 seconds_copy: .skip 1 day_copy: .skip 1 month_copy: .skip 1 year_copy: .skip 1 tickcnt1: .skip 1 tickcnt2: .skip 1 datetimer: .skip 1 .flag cdflag,0x20.0 .flag secflag,0x20.1 .flag outputflag,0x20.2 ; 0: LCD 1: serial port .flag badflag,0x20.3 .org 0x28 bufptr: .skip 1 ; External memory allocation .equ bufsize,80 .org 0x2700 inbuf: .skip bufsize+1 .equ bufpage,0x27 .org 0x2100 ljmp tick_isr .org 0x2800 ; CPU initialization ; ------------------ ; Initialize CPU registers. start: mov SP,#0x2f ; Stack at 30H mov PSW,#0 ; Registers at 0H mov TCON,#0 ; Disable timers mov IE,#0 ; Disable all interrupts mov IP,#0 mov PCON,#0 ; Initialize other registers. mov P1,#0xff mov P3,#0xff ; Data area initialization ; ------------------------ ; Initialize the date to 12:00 AM, Jan. 1, 1980 clr A mov hours,A mov minutes,A mov seconds,A inc A mov day,A mov month,A mov year,#80 ; Interrupt and serial port initialization ; ---------------------------------------- ; Set both timers to free running, 8 bit auto reload. mov TMOD,#0x22 ; Set up timer 0 to generate tick interrupts at the ; slowest possible rate (3600/sec). mov TH0,#0 setb IP.1 setb IE.1 setb IE.7 setb TCON.4 mov tickcnt1,#1 mov tickcnt2,#1 ; Start baud rate generator and initialize serial port. mov TH1,#0xfd setb TCON.6 mov SCON,#0x52 setb secflag mov datetimer,#0 lcall linit ;----------------------------------------------------------------------- dateprompt: setb outputflag ; Output to serial port mov dptr,#promptstr lcall puts lcall getline ; Now the date and time are in the buffer in ASCII ; string form. Parse them. Re-request input if any ; error occurs. mov r2,#'/' lcall getnum jb badflag,dateprompt mov a,r1 clr C subb a,#80 jnc not_2000 mov a,#100 add a,r1 mov r1,a not_2000: mov year_copy,r1 ; Get the month mov r2,#'/' lcall getnum jb badflag,dateprompt mov a,r1 jz dateprompt mov month_copy,a clr c subb a,#13 jnc dateprompt ; Get the day, making sure it fits with the month. mov r2,#' ' lcall getnum jb badflag,dateprompt mov a,r1 jz dateprompt mov a,month_copy mov R0,year_copy lcall daysthismonth clr c subb a,r1 jc dateprompt mov day_copy,r1 lcall skipspc ; Get the hour mov r2,#':' lcall getnum jb badflag,dateprompt mov a,r1 mov hours_copy,a clr c subb a,#24 jnc dateprompt ; Get the minute mov r2,#':' lcall getnum mov r2,a jb badflag,dateprompt mov a,r1 mov minutes_copy,a clr c subb a,#60 jnc dateprompt ; Set the second default to zero, then check if the second ; was supplied. mov seconds_copy,#0 cjne r2,#':',gd_ok ; Get the second mov r2,#' ' lcall getnum jb badflag,gd_bad mov a,r1 mov seconds_copy,a clr c subb a,#60 jc gd_ok gd_bad: ljmp dateprompt gd_ok: lcall setdate ; Main loop. If the secflag is set, update the LCD. ; If a character comes in on the serial port, set the ; datetimer and display the date, keep displaying it ; until the datetimer runs down. mainloop: jnb SCON.0,notdatereq clr SCON.0 mov datetimer,#200 sjmp showtd notdatereq: jnb secflag,mainloop ; Clear the secflag and put time or date on the LCD as ; needed. First, clear the LCD. showtd: clr secflag clr cdflag ; Command mode mov a,#0x01 ; Clear command lcall writelcd clr outputflag ; Direct output to the LCD. lcall getdate mov a,datetimer jnz showdate lcall printtime sjmp mainloop showdate: lcall printdate sjmp mainloop promptstr: .byte 13,10,"Enter time and date in format shown:",13,10 .byte "YY/MM/DD HH:MM:SS",13,10,0 helpstr: .byte 13,10,"Press any key to display date for 2 seconds." .byte 13,10,0 ;----------------------------------------------------------- ; Interrupt service routine ; tick_isr: djnz tickcnt1,isr_exit ; Reload the counter and save context. mov tickcnt1,#36 push PSW push ACC push DPL push DPH mov dptr,#tick_entry push DPL push DPH isr_exit: reti ; This is executed 100 times per second. Select register ; bank 1 (bank 0 will be restored by the "pop psw" at exit). tick_entry: mov PSW,#08h ; 100 times/sec interrupt code goes here. ; If the datetimer is nonzero, decrement it. When it goes ; to zero, set the secflag to allert the mainline that ; something has happened. mov a,datetimer jz skipdt dec a mov datetimer,a jnz skipdt setb secflag skipdt: djnz tickcnt2,int_exit sjmp do1sec ; Restore context and exit. int_exit: pop DPH pop DPL pop ACC pop PSW ret ; The following is executed once per second. First, reload ; the counter. do1sec: mov tickcnt2,#100 setb secflag ; Update clock ; ------------ ; Add 1 second to the current time. wait_idle: mov A,#60 inc seconds cjne A,seconds,date_done mov seconds,#0 inc minutes cjne A,minutes,date_done mov minutes,#0 mov A,#24 inc hours cjne A,hours,date_done mov hours,#0 ; Add 1 day to the date. mov A,month mov R0,year lcall daysthismonth cjne A,day,notlastday mov day,#0 mov A,#13 inc month cjne A,month,notlastday mov month,#1 inc year notlastday: inc day ; End of interrupt processing. date_done: ljmp int_exit ;--------------------------------------------------------------------------- ; Function: daysthismonth ; ; Description: ; Given a month number in A and the year number currently in R0, ; returns the number of days in that month. Handles February 29th for ; leap years. ; ; Inputs: A - month number ; R0 - year number ; ; Outputs: A - number of days ; daysthismonth: cjne A,#2,notfeb mov A,R0 anl A,#0xFD jnz not29feb mov A,#29 ret not29feb: mov A,#28 ret notfeb: mov DPTR,#dayspermonth-1 movc A,@A+DPTR gotdays: ret dayspermonth: .byte 31,28,31,30,31,30,31,31,30,31,30,31 ;----------------------------------------------------------- ; Routines to set/get the time ; getdate: clr IE.7 mov hours_copy,hours mov minutes_copy,minutes mov seconds_copy,seconds mov day_copy,day mov month_copy,month mov year_copy,year mov tickcnt1,#36 mov tickcnt2,#100 setb IE.7 ret setdate: clr IE.7 mov hours,hours_copy mov minutes,minutes_copy mov seconds,seconds_copy mov day,day_copy mov month,month_copy mov year,year_copy setb IE.7 ret ;--------------------------------------------------------------------------- ; Function: printnum ; ; Description: ; This function prints the number contained in A, in decimal. It right ; justifies the result in a field of given width, using a given pad ; character. ; ; Inputs: A - the number to be printed. ; R1 - pad character. ; R2 - field width. ; ; Outputs: None ; Since the digits are generated in reverse order, store ; them on the stack for use later. This is OK, since only ; 3 digits, plus a terminating character, will ever be ; pushed. ; Push a -1 on the stack to terminate the character list. printnum: mov B,#0ffh push B ; Divide A by 10, and push the remainder on the stack. The ; result stays in A, and the operation is repeated until ; nothing is left. Subtract 1 from the field width for ; each digit pushed. printloop: mov B,#10 div AB push B dec R2 jnz printloop ; Check if the field width is still positive. mov A,R2 jz nopad jb ACC.7,nopad ; It is, so put out however many bytes of padding required. padloop: mov A,R1 lcall putchar djnz R2,padloop ; Now pop the digits off the stack and print them. The -1 ; will generate a carry when '0' is added to it, and ; terminate the loop. nopad: pop ACC add A,#'0' jc printdone lcall putchar sjmp nopad printdone: ret ;----------------------------------------------------------- ; Print the date. printdate: mov a,month_copy mov B,#3 mul AB mov R3,#3 mov DPTR,#monthtab-3 ploop: push ACC movc A,@A+DPTR lcall putchar pop ACC inc A djnz R3,ploop mov a,#' ' lcall putchar mov a,day_copy lcall prt2dig mov a,#' ' lcall putchar mov a,year_copy mov B,#100 div AB add a,#19 push B lcall prt2dig pop ACC ljmp prt2dig monthtab: .byte "JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC" ;----------------------------------------------------------- ; Print the time. printtime: mov a,hours_copy lcall prt2dig ; Print ":", and the minute. mov a,#':' lcall putchar mov a,minutes_copy lcall prt2dig ; Print ":", the second, and exit. mov a,#':' lcall putchar mov a,seconds_copy ljmp prt2dig ; Subroutine to print a number as 2 digits. prt2dig: mov r1,#'0' mov r2,#2 ljmp printnum ; Subroutine to print a null-terminated string. puts: jnb outputflag,lputs puts1: movx a,@dptr jz pdone lcall putchar inc dptr sjmp puts pdone: ret ; Subroutine to print a single character. putchar: jnb outputflag,lputchar putchar1: jnb SCON.1,putchar1 mov SBUF,A clr SCON.1 ret lputchar: setb cdflag sjmp writelcd ; Subroutine to initalize the LCD. This simply prints a ; string to the LCD consisting of control register escapes ; and values to write into the control register. linit: mov dptr,#linitstr ljmp lputs linitstr: .byte 31,0x30 ; 8 bit interface, 1 line, 5x7 chars .byte 31,0x0c ; display on, cursor on, no blink .byte 31,0x06 ; autoincrement address, no scroll .byte 31,0x01 ; clear display .byte 31,0x02 ; home display and cursor .byte 0 ; done ; Subroutine to write a string to the LCD. DPTR points to ; the string. Bytes are written to the LCD data register ; until a zero byte is found. Byte value "31" causes the ; next byte to be written to the command register instead. lputs: movx a,@dptr ; Get character jz lpdone ; Exit if terminating zero setb cdflag ; Data register cjne a,#31,notcmd clr cdflag ; Command register inc dptr movx a,@dptr ; Get command byte to write notcmd: lcall writelcd inc dptr sjmp lputs lpdone: ret ; Subroutine to write a command or data byte to the LCD. ; This waits for the LCD to become ready if it is busy. ; ; Inputs: A - character to write. ; cdflag - command (0) or data (1) register writelcd: push DPL push DPH push ACC mov dptr,#0x8000 ; LCD base address clr P1.1 ; Command register setb P1.0 ; Read waitlcd: movx @dptr,a ; Start E pulse movx a,@dptr ; Finish E pulse and get data jb ACC.7,waitlcd ; Loop until LCD is ready mov C,cdflag mov P1.1,C ; Select command/data register clr P1.0 ; Write pop ACC ; Data to write movx @dptr,a ; Start E pulse and write data movx a,@dptr ; Finish E pulse pop DPH pop DPL ret ;--------------------------------------------------------------------------- ; Function: getline ; ; Description: ; "getline" gets a line of text from the terminal into the ; line input buffer. ; ; Inputs: None. ; getline: mov P2,#bufpage mov r1,#0 ; Buffer pointer setb outputflag ; Output to serial port ; Character loop. gloop: jnb SCON.0,gloop mov a,SBUF clr SCON.0 anl a,#0x7f ; Check for backspace or rubout. cjne a,#8,notbs sjmp delchar notbs: cjne a,#127,notdel ; Delete a character, but only if the buffer is not empty. delchar: mov a,r1 jz gloop lcall do1del sjmp gloop ; Check for carriage return. If it is one, zero-terminate ; the buffer, echo a CRLF, initialize the input buffer ; pointer, and return. notdel: cjne a,#13,notcr clr a movx @r1,a mov bufptr,a ljmp crlf ; Check for CTRL-X. If it is one, delete characters until ; nothing is left of the line. notcr: cjne a,#24,notctlx cloop: mov a,r1 jz gloop lcall do1del sjmp cloop ; That's it for the control characters. If the character ; isn't printable and hasn't been handled by now, get rid ; of it. notctlx: movx @r1,a anl a,#0e0h jz gloop ; Check if the input line is at maximum length already. ; If it is, don't advance the buffer pointer and don't ; echo the character. mov a,r1 clr c subb a,#bufsize jnc gloop ; Advance the buffer pointer and echo the character if echo ; is enabled. Otherwise, echo a space. movx a,@r1 inc r1 lcall putchar sjmp gloop ; Delete one character by decrementing the buffer pointer and ; printing . do1del: mov a,#8 lcall putchar mov a,#32 lcall putchar mov a,#8 lcall putchar dec r1 ret crlf: mov a,#13 lcall putchar mov a,#10 ljmp putchar ;--------------------------------------------------------------------------- ; Function: skipspc ; ; Description: ; Advances the command line input buffer pointer until a non-space ; character or the terminating zero is found. Does not advance the ; pointer past the termintating zero even if called more than once. ; Returns the first nonspace character found. ; ; Inputs: None ; ; Outputs: A - first nonspace character ; ; Use P2 and R0 to access the buffer, rather than ; corrupting the DPTR. skipspc: mov P2,#bufpage mov R0,bufptr skiploop: movx A,@R0 jz skipdone cjne A,#' ',skipdone inc R0 mov bufptr,R0 sjmp skiploop skipdone: ret ;--------------------------------------------------------------------------- ; Function: bgetchar ; ; Description: ; Gets one character from the command line input buffer, and advances ; the buffer pointer. Does not advance the pointer if the terminating ; zero was found. ; ; Inputs: None ; ; Outputs: A - character from the buffer ; ; Use P2 and R0 to access the buffer, rather than ; corrupting the DPTR. bgetchar: mov P2,#bufpage mov r0,bufptr movx a,@r0 jz gotzero inc bufptr gotzero: ret ;--------------------------------------------------------------------------- ; Function: getnum ; ; Description: ; Given the current position in the command line buffer, which must be ; on a non-space character, reads a decimal number from the buffer. The ; number must be terminated by a null or the character given in R2. If ; an error is encountered, "badflag" is set. ; ; Inputs: R2 - character (other than 0) which may terminate the number. ; Usually set to a space. ; ; Outputs: R1 - the number which was read. ; badflag - set if an error occurred and R1 is not valid. ; ; Initially clear the error flag, and set the number ; accumulator to zero. Then get the first digit. getnum: clr badflag mov r1,#0 lcall bgetchar ; Digit loop. Convert the current character to a binary ; value, and exit with an error if it is not valid. nextdigit: clr C subb a,#'0' jc badnum cjne a,#10,dig3 dig3: jnc badnum ; Multiply the number accumulator by 10 and add the current ; digit. Exit with an error if the result would exceed 255. xch a,r1 mov B,#10 mul AB jb OV,badnum add a,r1 jc badnum mov r1,a ; Get the next digit, and return if it is either zero or the ; value in R2. This check is done here since there must be ; at least one digit. lcall bgetchar jz numdone cjne a,#' ',checkr2 sjmp numdone checkr2: cjne a,2,nextdigit numdone: ret ; Error exit. badnum: setb badflag ret