;********************************************************************** ;* 8052.com Single Board Computer -- SBCMON Demonstration Program * ;* ---> SOFTWARE-BASED CLOCK DEMONSTRATION PROGRAM <--- * ;* For 8052.com SBC, hardware rev 1.3 * ;*--------------------------------------------------------------------* ;* This program is offered as-is with no warranty or guarantee of any * ;* kind. Its purpose is to serve as an educational aid in mastering * ;* the topics and concepts covered by the program. You are free to * ;* use this code for any purpose you see fit--including commercial-- * ;* but the exclusive responsibility for its use is the user of this * ;* code. This code is offered completely free of charge and VIS' and * ;* 8052.com's liability will be limited to the amount paid: zero. * ;*--------------------------------------------------------------------* ;* This program was coded to be compatible with the Pinnacle 52 IDE. * ;* Pinnacle 52 is available at http://www.vaultbbs.com/pinnacle. The * ;* program may require minor modifications in order to assemble with * ;* other assemblers. * ;*--------------------------------------------------------------------* ;* NOTE: This program was designed to be loaded into SBCMON's XRAM * ;* and executed as an SBCMON external program. It depends on one or * ;* more SBCMON library routines and, as such, will not work without * ;* SBCMON. This program is not intended to be loaded via in-system * ;* programming, only loaded within SBCMON. Should you wish to load * ;* this program via ISP as a stand-alone program, the SBCMON library * ;* entry point equates should be removed and the respective routines * ;* should be copied from SBCMON into this program. The source code * ;* for SBCMON may be found at http://www.8052.com/sbc/sbcmon. * ;*====================================================================* ;* Filename: SFTCLOCK.ASM * ;* Description: This is a simple digital clock that operates entirely * ;* in software. It uses a timer interrupt to measure time--it does * ;* *NOT* use the DS1307 RTC that is part of the SBC. This * ;* demonstrates the use of interrupts to measure time accurately. * ;********************************************************************** ;Constants used by the program CRYSTAL EQU 11059200 ;The crystal speed TMRCYCLE EQU 12 ;The number of crystal cycles per timer increment TMR_SEC EQU CRYSTAL/TMRCYCLE ;The # of timer increments per second F20TH_OF_SECOND EQU TMR_SEC / 20 ;Number of instruction cycles in 1/20th of a second PRE_RESET_VALUE EQU 65536-F20TH_OF_SECOND ;Time for 1/20th of a second RESET_VALUE EQU PRE_RESET_VALUE + 13 ;See comment below (was 14) ;The reset value is the calculated reset value +14 to account for all the instructions that ;are executed by the ISR before the clock is actually reset. Without this adjustment, the ;clock would lose 14 cycles per interrupt * 20 interrupts per second * 3600 seconds per hour = ;1,008,000 cycles per hour--or about 1.09 secons per hour, or about 26 seconds per day. The ;addition of 14 to the reset cycle compensates for this and should keep the time fairly ;accurate. ;Variable storage in Internal RAM. Register bank 1 is used since SBCMON ;doesn't use it and neither does this program. RTC_TICKS EQU 0Fh ;RTC 1/20th of a second countdown timer RTC_HOUR EQU 0Eh ;Holds the current RTC hour RTC_MINUTE EQU 0Dh ;Holds the current RTC minute RTC_SECOND EQU 0Ch ;Holds the current RTC second NEW_HOUR EQU 0Bh ;Temporarily holds the new hour entered by the user NEW_MINUTE EQU 0Ah ; ;Temporarily holds the new minute entered by the user ;========================================================== ; SBCMON LIBRARY ROUTINE ENTRY POINT EQUATES ; ;If you wish to make this a stand-alone program, remove ;these equates and copy the source code for the named ;routines from the SBCMON source code. SBCMON source code ;may be found at http://www.8052.com/sbc/sbcmon. ;=========================================================== SendLCDCommand EQU 0053h SendLCDText EQU 0050h GetKeyDebounced EQU 00B0h WaitKeyReleased EQU 00B3h SendSerialHexByte EQU 0044h ;========================================================== ; PROGRAM LOCATION ; ;The program is located at 8000h since this is where the ;8052.com SBC's XRAM/code memory is in the memory map. ;Programs loaded into this RAM area can be accessed as both ;data and executed as a program. If you are going to make ;this a stand-alone program the following ORG should be ;modified to 0000h rather than 8000h. ;=========================================================== ORG 8000h SJMP Main ;Jump to the main program ;========================================================== ; INTERRUPT CODE ; ;When using SBCMON, the interrupt vector table (0003h-0023h) ;is contained in firmware that cannot be modified by a ;program that is loaded into XRAM. In this case, SBCMON ;has interrupt vectors that simply jump to the interrupt ;vector +8000h. So timer 0 interrupt, which is normally at ;000Bh, will jump automatically to 800Bh. Since we want ;to use timer 0 interrupt in this program, we place our ;timer 0 interrupt vector at 800Bh. SBCMON will automatically ;jump to this address whenever a timer 0 interrupt occurs. ;=========================================================== ORG 800Bh ;Timer 0 interrupt vector for SBCMON LJMP TIMER0_INTERRUPT ;Jump to the timer 0 interrupt routine ;========================================================== ; PROGRAM CODE ; ;This is the actual guts of the demonstration code. ;=========================================================== Main: ;Push the original interrupt configuration on to the stack ;to restore when we exit the clock PUSH IE ;Protect interrupt enable register ;Initialize RTC variables MOV RTC_HOUR,#00 ;Initialize to 0 hours MOV RTC_MINUTE,#00 ;Initialize to 0 minutes MOV RTC_SECOND,#00 ;Initialize to 0 seconds MOV RTC_TICKS,#20 ;Initialize countdown tick counter to 20 (i.e. 20 per second) ;Set timer 0. We do this by reading TMOD, modifying it, and writing it back so as to not mess up timer 1 configuration MOV A,TMOD ;Get current value of TMOD ANL A,#0F0h ;Zero out timer 0 configuration ORL A,#01h ;Set timer 0 to 16-bit mode MOV TMOD,A ;Update TMOD MOV TH0,#HIGH RESET_VALUE ;Initialize timer high-byte MOV TL0,#LOW RESET_VALUE ;Initialize timer low-byte SETB TR0 ;Start timer 0 running ;Configurae interrupts SETB ET0 ;Enable timer 0 interrupt for RTC SETB EA ;Make sure interrupts are enabled Start: LCALL LCD_ClearScreen ;Clear the LCD initially MOV DPTR,#MSG_ClockLn1 ;Point to line 1 message LCALL LCD_DisplayString ;Write it MOV A,#40h ;Point to line 2 LCALL LCD_PositionCursor ;Position cursor for second line MOV DPTR,#MSG_ClockLn2 ;Point to line 2 message LCALL LCD_DisplayString ;Write it DisplayTime: MOV A,#44 ;Position of cursor on line 2 for time LCALL LCD_PositionCursor ;Position cursor for time display MOV A,RTC_HOUR ;Get current hour LCALL Display2DecimalDigits ;Display hour MOV A,#':' LCALL SendLCDText ;Send out colon after hour MOV A,RTC_MINUTE ;Get current minute LCALL Display2DecimalDigits ;Display minute MOV A,#':' LCALL SendLCDText ;Send out colon after minute MOV A,RTC_SECOND ;Get current second LCALL Display2DecimalDigits ;Display second MOV B,RTC_SECOND ;Hold the currently displayed second in B ClockLoop: LCALL GetKeyDebounced ;Get a key, if any JNZ ProcessKey ;If a key was pressed, process it ResumeLoop: MOV A,RTC_SECOND ;Get current RTC second CJNE A,B,DisplayTime ;If not same as the displayed time,display it again SJMP ClockLoop ;Otherwise keep waiting for a key CheckExit: CJNE A,#'A',WaitDebounce ;See if it's 'A'--which means exit. ;We got an 'A' key which means exit, so pop the IE register off ;the stack and return to SBCMON. POP IE ;Restore interrupt enable register RET WaitDebounce: ; LCALL WaitKeyReleased ;Wait for the key to be released SJMP ResumeLoop ;Resume the loop ;========================================================== ; PROCESS KEY ; ;The following code is executed when a key is pressed. ;The code then determines which key is pressed--valid ;keys are "*" to set the clock or "A" to exit the program. ;=========================================================== ProcessKey: CJNE A,#'*',CheckExit ;If it's not the '*' key then ignore the key and wait for it to be released ;If we get here it means the user pressed '*', so we display the ;"setting clock" message and prompt for the hour, minute, and second LCALL DisplaySetClock ;Display the "Setting the clock" prompt LCALL GetKeyDebounced ;Get a key, if any ;========================================================== ; PROMPT FOR NEW TIME ; ;The following code is executed when the "*" key is pressed. ;It will prompt for the hour, then the minute, then the ;second, validating each value after it is entered. When ;all three values have been entered it will set the current ;time to the time entered by the user. ;=========================================================== PromptHour: MOV A,#40h ;Cursor position is line 2, character 0 LCALL LCD_PositionCursor ;Position cursor at line 2, character 0 MOV DPTR,#MSG_SetHour ;Point to the "Set hour" message LCALL LCD_DisplayString ;Display the message MOV A,RTC_HOUR ;Default to the current hour LCALL Get2Digits ;Read 2-digit value from keypad and echo CJNE A,#24,$+3 ;Is the value entered less than 24? JC HourOk ;If it is then it's a valid hour LCALL InvalidPrompt ;Display the invalid prompt message SJMP PromptHour ;Prompt for the hour again HourOk: MOV NEW_HOUR,A ;Store the new hour temporarily in NEW_HOUR LCALL DisplaySetClock ;Re-display the "Setting the clock" prompt PromptMinute: MOV A,#40h ;Cursor position is line 2, character 0 LCALL LCD_PositionCursor ;Position cursor at line 2, character 0 MOV DPTR,#MSG_SetMinute ;Point to the "Set minute" message LCALL LCD_DisplayString ;Display the message MOV A,RTC_MINUTE ;Default to the current hour LCALL Get2Digits ;Read 2-digit value from keypad and echo CJNE A,#60,$+3 ;Is the value entered less than 60? JC MinuteOk ;If it is then it's a valid minute LCALL InvalidPrompt ;Display the invalid prompt message SJMP PromptMinute ;Prompt for the hour again MinuteOk: MOV NEW_MINUTE,A ;Store the new minute temporarily in NEW_MINUTE LCALL DisplaySetClock ;Re-display the "Setting the clock" prompt PromptSecond: MOV A,#40h ;Cursor position is line 2, character 0 LCALL LCD_PositionCursor ;Position cursor at line 2, character 0 MOV DPTR,#MSG_SetSecond ;Point to the "Set second" message LCALL LCD_DisplayString ;Display the message MOV A,RTC_SECOND ;Default to the current second LCALL Get2Digits ;Read 2-digit value from keypad and echo CJNE A,#60,$+3 ;Is the value entered less than 60? JC SecondOk ;If it is then it's a valid second LCALL InvalidPrompt ;Display the invalid prompt message SJMP PromptSecond ;Prompt for the hour again SecondOk: MOV RTC_TICKS,#20 ;Reset tick to 20/20ths of a second MOV RTC_SECOND,A ;Update the RTC with the new second MOV RTC_MINUTE,NEW_MINUTE ;Update the RTC with the new minute MOV RTC_HOUR,NEW_HOUR ;Update the RTC with the new hour LJMP Start ;Done with input, so go back to clock loop ;========================================================== ; DISPLAY SET CLOCK ; ;This routine simply displays the "Set Clock" message on ;the first line of the screen after clearing the screen. ;=========================================================== DisplaySetClock: LCALL LCD_ClearScreen ;Clear the LCD screen MOV DPTR,#MSG_SetClock ;Point to the "Set clock" message LCALL LCD_DisplayString ;Display the message RET ;========================================================== ; INVALID PROMPT ; ;This routine simply displays the "Invalid Value" message ;on the first line of the screen. It is displayed if an ;invalid hour, minute, or second is entered. ;=========================================================== InvalidPrompt: MOV A,#00h ;Cursor position is line 1, character 0 LCALL LCD_PositionCursor ;Move the cursor MOV DPTR,#MSG_Invalid ;Point to the "Invalid value" message LCALL LCD_DisplayString ;Display the string RET ;========================================================== ; MESSAGE STRINGS ; ;The following are the strings that are displayed on the ;LCD in various circumstances. ;=========================================================== MSG_ClockLn1: DB "Current time is:",0 MSG_ClockLn2: DB " ",0 MSG_SetClock: DB "Set clock",0 MSG_SetHour: DB "Enter hour:",0 MSG_SetMinute: DB "Enter minute:",0 MSG_SetSecond: DB "Enter second:",0 MSG_Invalid: DB "Invalid value!",0 ;========================================================== ; GET TWO DIGITS ; ;The following routine displays a current value (from the ;accumulator) at the last positions of line 2. It then ;allows the user to enter a new 2-digit value. The user ;may continue entering digits as long as they like and the ;new digits will replace the old ones until the user presses ;the "*" key to indicate they are done entering the values. ;=========================================================== Get2Digits: LCALL ConvertToBCD ;Convert value in accumulator to BCD in accumulator MOV B,A ;Store the initial BCD value in B MOV R1,#00h ;R1 is either 0 or 1 for first or second character G2D_Loop: MOV A,#4Eh ;Address for the data input LCALL LCD_PositionCursor ;Set the cursor position MOV A,B ;Get the current value LCALL Display2BCDDigits ;Display the current 2-digit value MOV A,#4Eh ;Default to first character entry CJNE R1,#01h,G2D_Position ;If character position is 0, use 4Dh offset INC A ;If character position is 1, use 4Eh offset G2D_Position: LCALL LCD_PositionCursor ;Position the cursor in the right place G2D_WaitKey: LCALL GetKeyDebounced ;Check to see if there's a key JNZ G2D_ProcKey SJMP G2D_WaitKey ;Otherwise keep waiting for a key G2D_WaitReleaseKey: LCALL WaitKeyReleased SJMP G2D_Loop G2D_ProcKey: ;This means a key was pressed. First we check to see if it was the ;E key which would mean we're done CJNE A,#'*',G2D_NotAsterisk ;If not 'E', goto G2D_NotAsterisk ;If we get here then it means the 'E' key was pressed. The current ;value is in 'B' so we transfer it to the accumulator and return it MOV A,B ;Move current BCD value to accumulator LCALL ConvertFromBCD ;Convert from BCD back to a real value LCALL WaitKeyReleased ;Wait for * key to be released RET ;Exit routine G2D_NotAsterisk: CJNE A,#'*',G2D_NotPound ;If not '#', goto G2D_NotPound SJMP G2D_WaitReleaseKey ;It was a #, invalid, so wait for next key G2D_NotPound: ;If we get here then they didn't press the 'E' key. That means the ;only thing that is valid are digits from 0 to 9. So we check to ;see if the value is less than or equal to 9 (less than ASCII ":"). CJNE A,#':',$+3 ;Compare keypress to the ascii value of the colon JNC G2D_WaitReleaseKey ;If key was not a digit, ignore it and wait for next keypress ;If we get here then the user pressed a key between 0 and 9. The ;current value is in ASCII code so we subtract the ASCII value of ;'0' to get a true number between 0 and 9. CLR C ;Make sure carry is initially clear for subtraction SUBB A,#'0' ;Subtract ASCII value for 0 ;We now have a value between 0 and 9. This either needs to be stored in the high ;nibble or the low nibble of B. If we're prompting for character 0 then this value ;needs to be shifted left 4 bits so that it'll be stored in the high nibble. If we're ;prompting for character 1 then the value is already in the right place CJNE R1,#00h,G2D_SkipShift ;If cursor position is not 0 (i.e. it is '1'), then skip the shift SWAP A ;This swaps the high and low nibble of accumulator G2D_SkipShift: ;Accumulator now holds the new digit of either the high or low nibble of the value. ;To update it we most zero out either high or low nibble of the current value (currently ;held in 'B') and then put the new value (in accumulator) in that nibble of B. XCH A,B ;Swap A and B, A now holds previous value, B holds key value CJNE R1,#00h,G2D_BlankLow ;If cursor position not 00h (i.e. it is 1) then blank out low nibble ;If we get here then we are working with the first character of the input so blank out the ;high nibble of the accumulator which holds the first digit. ANL A,#0Fh ;Zero out the high nibble SJMP G2D_OrIt ;Jump to the "OR" process G2D_BlankLow: ;If we get here then we are working the second character of the input so blank out low ;nibble of the accumulator which holds the second digit. ANL A,#0F0h ;Zero out the low nibble G2D_OrIt: ;We now have the original value in the accumulator with the current nibble blanked out. ;We OR it with the keypress that is currently stored in B to get a new value ORL A,B ;Combine the two for new value of current value MOV B,A ;B holds the current value, so update it INC R1 ;Increment character position CJNE R1,#02h,G2D_WaitReleaseKey ;If new character position is not 2 (i.e. third character), wait for input ;If we get here then they were entering the second character and are now in character ;position 3. Since that't impossible, reset character position to zero and wait for next ;character. MOV R1,#00h ;Point to first character SJMP G2D_WaitReleaseKey ;Wait for next character to be input ;========================================================== ; DISPLAY LCD STRING ; ;The following routine will send the message pointed to by ;DPTR to the LCD. The message is considered complete when ;a null value (0) is encountered. ;=========================================================== LCD_DisplayString: CLR A ;Offset is always zero MOVC A,@A+DPTR ;Get the next byte of the string JZ LCD_DisplayStringExit;If null character (0), then end of string LCALL SendLCDText ;Otherwise display the character on the LCD INC DPTR ;Increment DPTR to point to next character SJMP LCD_DisplayString ;Repeat for each character of string LCD_DisplayStringExit: RET ;========================================================== ; POSITION LCD CURSOR ; ;The following routine positions the LCD cursor at the ;address indicated by the accumulator. If A=0, that's the ;first character of the first line. If A=40h, that's the ;first character of the second line. ;=========================================================== LCD_PositionCursor: ADD A,#80h ;The address is added to 80h to get the cursor command LCALL SendLCDCommand ;Send the cursor positioning command RET ;========================================================== ; CLEAR LCD SCREEN ; ;The following routine clears the LCD screen. ;=========================================================== LCD_ClearScreen: MOV A,#01h ;Clear screen command is 01h LJMP SendLCDCommand ;Send the command to the LCD RET ;========================================================== ; DISPLAY 2 DECIMAL DIGITS ; ;Takes the value in the accumulator and sends it to the ;LCD as two decimal digits. The value of 0 will be displayed ;as 00, the value 10 will be displayed as 10, etc. This is ;used by the main clock to display the hour, minute, and ;second values which are stored internally as a value between ;0 and 59 decimal. ;=========================================================== Display2DecimalDigits: PUSH ACC ;Protect accumulator PUSH B ;Protect B MOV B,#10 ;Going to divde by 10 DIV AB ;Divide it ORL B,#30h ;Add 30h to it to make it an ASCII digit between '0' and '9' ORL A,#30h ;Add 30h to it to make it an ASCII digit between '0' and '9' LCALL SendLCDText ;Display high byte MOV A,B ;Get low byte in accumulator LCALL SendLCDText ;Display the low byte POP B ;Restore B POP ACC ;Restore accumulator RET ;========================================================== ; DISPLAY 2 BCD DIGITS ; ;Takes the value in the accumulator and sends it to the ;LCD as two BCD digits. The value of 0 will be displayed ;as 00, the value 10h will be displayed as 10, etc. This ;is used by the "Get2Digits" routine which handles the ;values as BCDs as opposed to decimal values. ;=========================================================== Display2BCDDigits: PUSH ACC ;Protect accumulator PUSH B ;Protect B MOV B,A ;Protect value in B ANL A,#0F0h ;Only interested in high nibble for first byte SWAP A ;Put it in the low nibble ADD A,#'0' ;Add ASCII value of '0' to make it printable LCALL SendLCDText ;Send it to LCD display MOV A,B ;Get original value again ANL A,#0Fh ;Only interested in low nibble for secomd byte ADD A,#'0' ;Add ASCII value of '0' to make it printable LCALL SendLCDText ;Send it to LCD display POP B ;Restore B POP ACC ;Restore accumulator RET ;========================================================== ; CONVERT TO BCD ; ;Takes the value in the accumulator and converts it to a ;BCD in the accumulator. Thus the value 10 (decimal) will ;be converted to 10h (16 decimal), the value 25 (decimal) ;will be converted to 25h (37 decimal), etc. ;=========================================================== ConvertToBCD: PUSH B ;Protect B MOV B,#10 ;Need to divide by 10 DIV AB ;Divide value by 10 SWAP A ;50 is now 5, so we swap accumulator to make it 50h ADD A,B ;Add the remainder from the division POP B ;Restore B RET ;========================================================== ; CONVERT FROM BCD ; ;Takes the value in the accumulator and converts it from a ;BCD to a decimal value. Thus the value 10h will be converted ;to 10 (decimal), the value 25h will be converted to 25 (decimal), ;etc. ;=========================================================== ConvertFromBCD: PUSH B ;Protect B PUSH ACC ;Protect original value ANL A,#0F0h ;Only interested in high nibble (so 54h becomes 50h) SWAP A ;Put it in low nibble so 50h becomes 5h MOV B,#10 ;Multiply it by 10 MUL AB ;Execute multiplication, so it now becomes 50 MOV B,A ;Store the ten's value in B POP ACC ;Pop the original value ANL A,#0Fh ;Only interested in the low nibble ADD A,B ;Combine the multiplied value with low nibble POP B ;Restore B RET ;========================================================== ; TIMER 0 INTERRUPT ; ;This is the actual timer 0 interrupt. It is taken basically ;from the tutorial at http://www.8052.com/tutrtc.phtml. It ;is executed 20 times per second and, each time, decrements a ;1/20th of a second counter. When this counter reachers 0 it ;means one second has passed--at which time the interrupt ;increments the seconds, minutes, and hours as required. ;=========================================================== TIMER0_INTERRUPT: ;LJMP from 000Bh to 800bh (2) ;LJMP from 800Bh to here (2) PUSH PSW ;Protect PSW register (2) PUSH ACC ;We'll use the accumulator, so we need to protect it (2) CLR TR0 ;Turn off timer 1 as we reset the value (1) MOV TH0,#HIGH RESET_VALUE ;Set the high byte of the reset value (2) MOV TL0,#LOW RESET_VALUE ;Set the low byte of the reset value (2) SETB TR0 ;Restart timer 1 now that it has been initialized (1) DJNZ RTC_TICKS,EXIT_RTC ;Decrement TICKS, if not yet zero we exit immediately MOV RTC_TICKS,#20 ;Reset the ticks variable INC RTC_SECOND ;Increment the second varaiable MOV A,RTC_SECOND ;Move the seconds variable into the accumulator CJNE A,#60,EXIT_RTC ;If we haven't counted 60 seconds, we're done. MOV RTC_SECOND,#0 ;Reset the seconds varaible INC RTC_MINUTE ;Increment the number of minutes MOV A,RTC_MINUTE ;Move the minutes variable into the accumulator CJNE A,#60,EXIT_RTC ;If we haven't counted 60 minutes, we're done MOV RTC_MINUTE,#0 ;Reset the minutes variable INC RTC_HOUR ;Increment the hour variable MOV A,RTC_HOUR ;Move the hour variable into the accumulator CJNE A,#24,EXIT_RTC ;If we haven't reached the 24th hour of the day, we're done MOV RTC_HOUR,#00h ;Reset, it's a new day EXIT_RTC: POP ACC ;Restore the accumulator POP PSW ;Restore PSW RETI ;Exit the interrupt routine