;=============================================================================== ; TIMER - Display system timer value ; Version 1.31 24-July-2024 ;=============================================================================== ; ; Author: Wayne Warthen (wwarthen@gmail.com) ; Updated: MartinR (July 2024) - A user of uppercase mnemonics ;_______________________________________________________________________________ ; ; Usage: ; TIMER [/C] [/?] ; ex: TIMER (display current timer value) ; TIMER /? (display version and usage) ; TIMER /C (display timer value continuously) ; ; Operation: ; Reads and displays system timer value. ; ; This code will only execute on a Z80 CPU (or derivitive) ; ; This source code assembles with TASM V3.2 under Windows-11 using the ; following command line: ; tasm -80 -g3 -l TIMER.ASM TIMER.COM ; ie: Z80 CPU; output format 'binary' named .COM (rather than .OBJ) ; and includes a symbol table as part of the listing file. ;_______________________________________________________________________________ ; ; Change Log: ; 2018-01-14 [WBW] Initial release ; 2018-01-17 [WBW] Add HBIOS check ; 2019-11-08 [WBW] Add seconds support ; 2024-06-30 [MR ] Display values in decimal rather than hexadecimal ; 2024-07-24 [MR ] Also display value in Hours-Mins-Secs format ;_______________________________________________________________________________ ; ; Includes binary-to-decimal subroutine by Alwin Henseler ; Located at: https://www.msx.org/forum/development/msx-development/32-bit-long-ascii ;_______________________________________________________________________________ ; ; Includes division subroutines from: https://wikiti.brandonw.net/ ;;_______________________________________________________________________________ ; #include "../ver.inc" ; Used for building RomWBW ;#include "ver.inc" ; Used for testing purposes during code development ; ;=============================================================================== ; Definitions ;=============================================================================== ; STKSIZ .EQU $40 ; Working stack size ; RESTART .EQU $0000 ; CP/M restart vector BDOS .EQU $0005 ; BDOS invocation vector ; IDENT .EQU $FFFE ; loc of RomWBW HBIOS ident ptr ; BF_SYSVER .EQU $F1 ; BIOS: VER function BF_SYSGET .EQU $F8 ; HBIOS: SYSGET function BF_SYSSET .EQU $F9 ; HBIOS: SYSSET function BF_SYSGETTIMER .EQU $D0 ; TIMER subfunction BF_SYSSETTIMER .EQU $D0 ; TIMER subfunction BF_SYSGETSECS .EQU $D1 ; SECONDS subfunction BF_SYSSETSECS .EQU $D1 ; SECONDS subfunction ; ;ASCII Control Characters LF .EQU 00AH ; Line Feed CR .EQU 00DH ; Carriage Return ; ;=============================================================================== ; Code Section ;=============================================================================== ; .ORG $100 ; ; setup stack (save old value) LD (STKSAV),SP ; save stack LD SP,STACK ; set new stack ; ; initialization CALL INIT ; initialize JR NZ,EXIT ; abort if init fails ; ; process CALL PROCESS ; do main processing JR NZ,EXIT ; abort on error ; EXIT: ; clean up and return to command processor CALL CRLF ; formatting LD SP,(STKSAV) ; restore stack ;JP RESTART ; return to CP/M via restart RET ; return to CP/M w/o restart ; ; Initialization ; INIT: CALL CRLF ; formatting LD DE,MSGBAN ; point to version message part 1 CALL PRTSTR ; print it ; CALL IDBIO ; identify active BIOS CP 1 ; check for HBIOS JP NZ,ERRBIO ; handle BIOS error ; LD A,RMJ << 4 | RMN ; expected HBIOS ver CP D ; compare with result above JP NZ,ERRBIO ; handle BIOS error ; INITX: ; initialization complete XOR A ; signal success RET ; return ; ; Process ; PROCESS: ; look for start of parms LD HL,$81 ; point to start of parm area (past len byte) ; PROCESS00: CALL NONBLANK ; skip to next non-blank char JP Z,PROCESS0 ; no more parms, go to display ; ; check for option, introduced by a "/" CP '/' ; start of options? JP NZ,USAGE ; yes, handle option CALL OPTION ; do option processing RET NZ ; done if non-zero return JR PROCESS00 ; continue looking for options ; PROCESS0: ; ; Test of API function to set seconds value ;LD B,BF_SYSSET ; HBIOS SYSGET function ;LD C,BF_SYSSETSECS ; SECONDS subfunction ;LD DE,0 ; set seconds value ;LD HL,1000 ; ... to 1000 ;RST 08 ; call HBIOS, DE:HL := seconds value ; ; get and print seconds value CALL CRLF2 ; formatting ; PROCESS1: LD B,BF_SYSGET ; HBIOS SYSGET function LD C,BF_SYSGETTIMER ; TIMER subfunction RST 08 ; call HBIOS, DE:HL := timer value LD A,(FIRST) OR A LD A,0 LD (FIRST),A JR NZ,PROCESS1A ; TEST FOR NEW VALUE LD A,(LAST) ; last LSB value to A CP L ; compare to current LSB JP Z,PROCESS2 ; if equal, bypass display ;******************************************************************************* ;******************************************************************************* ; Formatting code added/amended to print values in decimal and Hours-Mins-Secs ; MartinR June & July-2024 PROCESS1A: ; Save and print new value LD A,L ; new LSB value to A LD (LAST),A ; save as last value CALL PRTCR ; back to start of line CALL B2D32 ; Convert DE:HL into ASCII; Start of ASCII buffer returned in HL EX DE,HL CALL PRTSTR ; Display the value LD DE,STRTICK ; "Ticks" message CALL PRTSTR ; Display it ; Get and print seconds value in decimal LD B,BF_SYSGET ; HBIOS SYSGET function LD C,BF_SYSGETSECS ; SECONDS subfunction RST 08 ; Call HBIOS; DE:HL := seconds value; C := fractional part LD (SEC_MS),DE ; Store the most significant part of the 'seconds' value LD (SEC_LS),HL ; And the less significant...... LD A,C ; And also the fractional part SLA A ; But double the 50Hz 'ticks' value to give 1/100ths of a second LD (SEC_FR),A CALL B2D32 ; Convert DE:HL into ASCII; Start of ASCII buffer returned in HL EX DE,HL CALL PRTSTR ; Display the value CALL PRTDOT ; Print a '.' seperator LD A,(SEC_FR) ; Retrieve fractional part (1/100ths second) CALL B2D8 ; Convert into ASCII - up to 3 digits. Umber of digits returned C CALL PRT_LEAD0 ; Print a leading zero if there is exactly 1 character in the string EX DE,HL ; Start of ASCII buffer returned in HL CALL PRTSTR ; Display fractional part of the value LD DE,STRSEC ; "Seconds" message CALL PRTSTR ; Display it ; Now print the seconds value as HMS LD BC,(SEC_MS) ; Retrive the 'seconds' value into AC:IX LD A,B LD IX,(SEC_LS) LD DE,3600 ; 3600 seconds in an hour CALL DIV32BY16 ; AC:IX divided by DE; Answer in AC:IX; Remainder in HL PUSH HL ; Preserve the remainder on the stack LD D,A ; Shuffle registers around ready for conversion to ASCII LD A,C ; AC:IX into DE:HL LD E,A PUSH IX POP HL CALL B2D32 ; Convert DE:HL into ASCII; Start of ASCII buffer returned in HL EX DE,HL CALL PRTSTR ; Display the hours value CALL PRTCOLON ; Print a ':' seperator POP HL ; Retrive the remainder (seconds) LD C,60 ; 60 seconds in a minute CALL DIV_HL_C ; HL divided by C; Answer in HL; Remainder in A PUSH AF ; Preserve the remainder (seconds) on the stack CALL B2D16 ; Convert HL into ASCII; Start of ASCII buffer returned in HL; Count in C CALL PRT_LEAD0 ; Print a leading zero if there is exactly 1 character in the string EX DE,HL CALL PRTSTR ; Display the minutes value CALL PRTCOLON ; Print a ':' seperator POP AF ; Retrive the remainder (seconds) CALL B2D8 ; Convert A into ASCII; Start of ASCII buffer returned in HL; Count in C CALL PRT_LEAD0 ; Print a leading zero if there is exactly 1 character in the string EX DE,HL CALL PRTSTR ; Display the seconds value CALL PRTDOT ; Print a '.' seperator LD A,(SEC_FR) ; Retrieve the fractional part (1/100ths) of the seconds CALL B2D8 ; Convert A into ASCII; Start of ASCII buffer returned in HL CALL PRT_LEAD0 ; Print a leading zero if there is exactly 1 character in the string EX DE,HL CALL PRTSTR ; Display the value LD DE,STRHMS ; Print "HH:MM:SS" message CALL PRTSTR ;******************************************************************************* ;******************************************************************************* PROCESS2: LD A,(CONT) ; continuous display? OR A ; test for true/false JR Z,PROCESS3 ; if false, get out ; LD C,6 ; BDOS: direct console I/O LD E,$FF ; input char CALL BDOS ; call BDOS, A := char OR A ; test for zero JP Z,PROCESS1 ; loop until char pressed ; PROCESS3: XOR A ; signal success RET ; ; Handle special options ; OPTION: INC HL ; next char LD A,(HL) ; get it OR A ; zero terminator? RET Z ; done if so CP ' ' ; blank? RET Z ; done if so CP '?' ; is it a '?'? JP Z,USAGE ; yes, display usage CP 'C' ; is it a 'C', continuous? JP Z,SETCONT ; yes, set continuous display JP ERRPRM ; anything else is an error ; USAGE: JP ERRUSE ; display usage and get out ; SETCONT: ; OR $FF ; SET A TO TRUE LD (CONT),A ; AND SET CONTINUOUS FLAG JR OPTION ; CHECK FOR MORE OPTION LETTERS ; ; Identify active BIOS. RomWBW HBIOS=1, UNA UBIOS=2, else 0 ; IDBIO: ; ; CHECK FOR UNA (UBIOS) LD A,($FFFD) ; fixed location of UNA API vector CP $C3 ; jp instruction? JR NZ,IDBIO1 ; if not, not UNA LD HL,($FFFE) ; get jp address LD A,(HL) ; get byte at target address CP $FD ; first byte of UNA push ix instruction JR NZ,IDBIO1 ; if not, not UNA INC HL ; point to next byte LD A,(HL) ; get next byte CP $E5 ; second byte of UNA push ix instruction JR NZ,IDBIO1 ; if not, not UNA, check others ; LD BC,$04FA ; UNA: get BIOS date and version RST 08 ; DE := ver, HL := date ; LD A,2 ; UNA BIOS id = 2 RET ; and done ; IDBIO1: ; Check for RomWBW (HBIOS) LD HL,($FFFE) ; HL := HBIOS ident location LD A,'W' ; First byte of ident CP (HL) ; Compare JR NZ,IDBIO2 ; Not HBIOS INC HL ; Next byte of ident LD A,~'W' ; Second byte of ident CP (HL) ; Compare JR NZ,IDBIO2 ; Not HBIOS ; LD B,BF_SYSVER ; HBIOS: VER function LD C,0 ; required reserved value RST 08 ; DE := version, L := platform id ; LD A,1 ; HBIOS BIOS id = 1 RET ; and done ; IDBIO2: ; No idea what this is XOR A ; Setup return value of 0 RET ; and done ; ; Print character in A without destroying any registers ; PRTCHR: PUSH BC ; save registers PUSH DE PUSH HL LD E,A ; character to print in E LD C,$02 ; BDOS function to output a character CALL BDOS ; do it POP HL ; restore registers POP DE POP BC RET ; ; Print a 0 if C=1 ; PRT_LEAD0: DEC C ; Decrement C, and a value of 1 becomee zero RET NZ ; If C wasn't 1, then no leading space required LD A,'0' ; Print the leading zero JR Z,PRTCHR ; PRTDOT: ; ; shortcut to print a dot preserving all regs PUSH AF ; save af LD A,'.' ; load dot char CALL PRTCHR ; print it POP AF ; restore af RET ; done ; PRTCOLON: ; ; shortcut to print a colon preserving all regs PUSH AF ; save af LD A,':' ; load colon char CALL PRTCHR ; print it POP AF ; restore af RET ; done ; PRTCR: ; ; shortcut to print a CR preserving all regs PUSH AF ; save af LD A,13 ; load CR value CALL PRTCHR ; print it POP AF ; restore af RET ; done ; ; Print a zero terminated string at (DE) without destroying any registers ; PRTSTR: PUSH DE ; PRTSTR1: LD A,(DE) ; get next char OR A JR Z,PRTSTR2 CALL PRTCHR INC DE JR PRTSTR1 ; PRTSTR2: POP DE ; restore registers RET ; ; Print the value in A in hex without destroying any registers ; PRTHEX: PUSH AF ; save AF PUSH DE ; save DE CALL HEXASCII ; convert value in A to hex chars in DE LD A,D ; get the high order hex char CALL PRTCHR ; print it LD A,E ; get the low order hex char CALL PRTCHR ; print it POP DE ; restore DE POP AF ; restore AF RET ; done ; ; print the hex word value in bc ; PRTHEXWORD: PUSH AF LD A,B CALL PRTHEX LD A,C CALL PRTHEX POP AF RET ; ; print the hex dword value in de:hl ; PRTHEX32: PUSH BC PUSH DE POP BC CALL PRTHEXWORD PUSH HL POP BC CALL PRTHEXWORD POP BC RET ; ; Convert binary value in A to ascii hex characters in DE ; HEXASCII: LD D,A ; save A in D CALL HEXCONV ; convert low nibble of A to hex LD E,A ; save it in E LD A,D ; get original value back RLCA ; rotate high order nibble to low bits RLCA RLCA RLCA CALL HEXCONV ; convert nibble LD D,A ; save it in D RET ; done ; ; Convert low nibble of A to ascii hex ; HEXCONV: AND $0F ; low nibble only ADD A,$90 DAA ADC A,$40 DAA RET ; ; Print value of A or HL in decimal with leading zero suppression ; Use prtdecb for A or prtdecw for HL ; PRTDECB: PUSH HL LD H,0 LD L,A CALL PRTDECW ; print it POP HL RET ; PRTDECW: PUSH AF PUSH BC PUSH DE PUSH HL CALL PRTDEC0 POP HL POP DE POP BC POP AF RET ; PRTDEC0: LD E,'0' LD BC,-10000 CALL PRTDEC1 LD BC,-1000 CALL PRTDEC1 LD BC,-100 CALL PRTDEC1 LD C,-10 CALL PRTDEC1 LD E,0 LD C,-1 PRTDEC1: LD A,'0' - 1 PRTDEC2: INC A ADD HL,BC JR C,PRTDEC2 SBC HL,BC CP E RET Z LD E,0 CALL PRTCHR RET ; ; Start a new line ; CRLF2: CALL CRLF ; two of them CRLF: PUSH AF ; preserve AF LD A,CR CALL PRTCHR ; print CR LD A,LF CALL PRTCHR ; print LF POP AF ; restore AF RET ; ; Get the next non-blank character from (HL). ; NONBLANK: LD A,(HL) ; load next character OR A ; string ends with a null RET Z ; if null, return pointing to null CP ' ' ; check for blank RET NZ ; return if not blank INC HL ; if blank, increment character pointer JR NONBLANK ; and loop ; ; Convert character in A to uppercase ; UCASE: CP 'a' ; if below 'a' RET C ; ... do nothing and return CP 'z' + 1 ; if above 'z' RET NC ; ... do nothing and return RES 5,A ; clear bit 5 to make lower case -> upper case RET ; and return ; ; Add the value in A to HL (HL := HL + A) ; ADDHL: ADD A,L ; A := A + L LD L,A ; Put result back in L RET NC ; if no carry, we are done INC H ; if carry, increment H RET ; and return ; ; Jump indirect to address in HL ; JPHL: JP (HL) ; ; Errors ; ERRUSE: ; command usage error (syntax) LD DE,MSGUSE JR ERR ; ERRPRM: ; command parameter error (syntax) LD DE,MSGPRM JR ERR ; ERRBIO: ; invalid BIOS or version LD DE,MSGBIO JR ERR ; ERR: ; print error string and return error signal CALL CRLF2 ; print newline ; ERR1: ; without the leading crlf CALL PRTSTR ; print error string ; ERR2: ; without the string ; CALL CRLF ; print newline OR $FF ; signal error RET ; done ; ; ;=============================================================================== ; Subroutine to print decimal numbers ;=============================================================================== ; ; Combined routine for conversion of different sized binary numbers into ; directly printable ASCII(Z)-string ; Input value in registers, number size and -related to that- registers to fill ; is selected by calling the correct entry: ; ; entry inputregister(s) decimal value 0 to: ; B2D8 A 255 (3 digits) ; B2D16 HL 65535 5 " ; B2D24 E:HL 16777215 8 " ; B2D32 DE:HL 4294967295 10 " ; B2D48 BC:DE:HL 281474976710655 15 " ; B2D64 IX:BC:DE:HL 18446744073709551615 20 " ; ; The resulting string is placed into a small buffer attached to this routine, ; this buffer needs no initialization and can be modified as desired. ; The number is aligned to the right, and leading 0's are replaced with spaces. ; On exit HL points to the first digit, (B)C = number of decimals ; This way any re-alignment / postprocessing is made easy. ; Changes: AF,BC,DE,HL,IX ; ; by Alwin Henseler ; https://msx.org/forum/topic/who-who/dutch-hardware-guy-pops-back-sort ; ; Found at: ; https://www.msx.org/forum/development/msx-development/32-bit-long-ascii ; ; Tweaked to assemble using TASM 3.2 by MartinR 23June2024 ; B2D8: LD H,0 LD L,A B2D16: LD E,0 B2D24: LD D,0 B2D32: LD BC,0 B2D48: LD IX,0 ; zero all non-used bits B2D64: LD (B2DINV),HL LD (B2DINV+2),DE LD (B2DINV+4),BC LD (B2DINV+6),IX ; place full 64-bit input value in buffer LD HL,B2DBUF LD DE,B2DBUF+1 LD (HL),' ' B2DFILC .EQU $-1 ; address of fill-character LD BC,18 LDIR ; fill 1st 19 bytes of buffer with spaces LD (B2DEND-1),BC ; set BCD value to "0" & place terminating 0 LD E,1 ; no. of bytes in BCD value LD HL,B2DINV+8 ; (address MSB input)+1 LD BC,00909H XOR A B2DSKP0:DEC B JR Z,B2DSIZ ; all 0: continue with postprocessing DEC HL OR (HL) ; find first byte <>0 JR Z,B2DSKP0 B2DFND1:DEC C RLA JR NC,B2DFND1 ; determine no. of most significant 1-bit RRA LD D,A ; byte from binary input value B2DLUS2:PUSH HL PUSH BC B2DLUS1:LD HL,B2DEND-1 ; address LSB of BCD value LD B,E ; current length of BCD value in bytes RL D ; highest bit from input value -> carry B2DLUS0:LD A,(HL) ADC A,A DAA LD (HL),A ; double 1 BCD byte from intermediate result DEC HL DJNZ B2DLUS0 ; and go on to double entire BCD value (+carry!) JR NC,B2DNXT INC E ; carry at MSB -> BCD value grew 1 byte larger LD (HL),1 ; initialize new MSB of BCD value B2DNXT: DEC C JR NZ,B2DLUS1 ; repeat for remaining bits from 1 input byte POP BC ; no. of remaining bytes in input value LD C,8 ; reset bit-counter POP HL ; pointer to byte from input value DEC HL LD D,(HL) ; get next group of 8 bits DJNZ B2DLUS2 ; and repeat until last byte from input value B2DSIZ: LD HL,B2DEND ; address of terminating 0 LD C,E ; size of BCD value in bytes OR A SBC HL,BC ; calculate address of MSB BCD LD D,H LD E,L SBC HL,BC EX DE,HL ; HL=address BCD value, DE=start of decimal value LD B,C ; no. of bytes BCD SLA C ; no. of bytes decimal (possibly 1 too high) LD A,'0' RLD ; shift bits 4-7 of (HL) into bit 0-3 of A CP '0' ; (HL) was > 9h? JR NZ,B2DEXPH ; if yes, start with recording high digit DEC C ; correct number of decimals INC DE ; correct start address JR B2DEXPL ; continue with converting low digit B2DEXP: RLD ; shift high digit (HL) into low digit of A B2DEXPH:LD (DE),A ; record resulting ASCII-code INC DE B2DEXPL:RLD LD (DE),A INC DE INC HL ; next BCD-byte DJNZ B2DEXP ; and go on to convert each BCD-byte into 2 ASCII SBC HL,BC ; return with HL pointing to 1st decimal RET B2DINV: .FILL 8 ; space for 64-bit input value (LSB first) B2DBUF: .FILL 20 ; space for 20 decimal digits B2DEND: .DB 000H ; space for terminating character ;******************************************************************************* ; The following routine divides AC:IX by DE and places the quotient ; in AC:IX and the remainder in HL ; https://wikiti.brandonw.net/ ; IN: ACIX=dividend, DE=divisor ; OUT: ACIX=quotient, DE=divisor, HL=remainder, B=0 DIV32BY16: LD HL,0 LD B,32 DIV32BY16_LOOP: ADD IX,IX RL C RLA ADC HL,HL JR C,DIV32BY16_OVERFLOW SBC HL,DE JR NC,DIV32BY16_SETBIT ADD HL,DE DJNZ DIV32BY16_LOOP RET DIV32BY16_OVERFLOW: OR A SBC HL,DE DIV32BY16_SETBIT: INC IX DJNZ DIV32BY16_LOOP RET ;******************************************************************************* ; The following routine divides HL by C and places the quotient in HL ; and the remainder in A ; https://wikiti.brandonw.net/ DIV_HL_C: XOR A LD B, 16 LOOPDIV1: ADD HL, HL RLA JR C, $+5 CP C JR C, $+4 SUB C INC L DJNZ LOOPDIV1 RET ;=============================================================================== ; Messages Section ;=============================================================================== MSGBAN .DB "TIMER v1.31, 24-Jul-2024",CR,LF .DB "Copyright (C) 2019, Wayne Warthen, GNU GPL v3",CR,LF .DB "Updated by MartinR 2024",0 MSGUSE .DB "Usage: TIMER [/C] [/?]",CR,LF .DB " ex. TIMER (display current timer value)",CR,LF .DB " TIMER /? (display version and usage)",CR,LF .DB " TIMER /C (display timer value continuously)",0 MSGPRM .DB "Parameter error (TIMER /? for usage)",0 MSGBIO .DB "Incompatible BIOS or version, " .DB "HBIOS v", '0' + rmj, ".", '0' + rmn, " required",0 STRTICK .DB " Ticks ",0 STRSEC .DB " Seconds ",0 STRHMS .DB " HH:MM:SS",0 ;=============================================================================== ; Storage Section ;=============================================================================== SEC_MS .DW 0 ; Storage space to preserve the seconds value as SEC_LS .DW 0 ; most and less significant parts SEC_FR .DB 0 ; And the fractional part (1/100s of a second) LAST .DB 0 ; last LSB of timer value CONT .DB 0 ; non-zero indicates continuous display FIRST .DB $FF ; first pass flag (true at start) STKSAV .DW 0 ; stack pointer saved at start .FILL STKSIZ,0 ; stack STACK .EQU $ ; new stack top .END