4

I am working on an Assembly program to get system time and date, convert it to ASCII, and display it on the monitor. I am having trouble getting it to display properly and cannot find where I've gone wrong. This is for an assignment, and I'd rather have explanations than just solutions, if possible. Here is my code:

TITLE GETDTTM
PAGE 60, 132

;   This program retrieve the system date and time,
;   converts it to ASCII, and displays it to the screen

;   Define constants
;
CR      EQU 0DH ;define carriage return
LF      EQU 0AH ;define line feed
EOM     EQU '$' ;define end of message marker
NULL    EQU 00H ;define NULL byte
;
;   Define variables
;
JMP START
PROMPT  DB  CR, LF, "The current time is: ",EOM
PROMPT2 DB  CR, LF, "The date is: ",EOM
TIME    DB  "00:00:00", CR, LF, EOM
DATE    DB  "00/00/0000", CR, LF, EOM
;
;   Program code
;
START:  
CALL GET_TIME   ;call function to get system time
CALL GET_DATE   ;call function to get system date

LEA DX, PROMPT  ;print time prompt to screen
MOV AH, 09H
INT 21H

LEA DX, TIME    ;print time
MOV AH, 09H
INT 21H

LEA DX, PROMPT2 ;print date prompt to screen
MOV AH, 09H
INT 21H

LEA DX, DATE    ;print date
MOV AH, 09H
INT 21H


CVT_TIME:   ;converts the time to ASCII
CALL CVT_HR
CALL CVT_MIN
CALL CVT_SEC
RET

CVT_HR:
MOV BH, CH  ;copy contents of hours to BH
SHR CH,4    ;convert high char to low order bits
ADD CH, 30H ;add 30H to convert to ASCII
MOV [TIME], CH  ;save it
AND BH, 0FH ;isolate lower 4 bits
ADD BH, 30H ;convert to ASCII
MOV [TIME+1], BH    ;save it
RET

CVT_MIN:
MOV BH, CL  ;copy contents of minutes to BH
SHR CL, 4   ;convert high char to low order bits
ADD CL, 30H ;add 30H to convert to ASCII
MOV [TIME+3], CL    ;save it
AND BH, 0FH ;isolate lower 4 bits
ADD BH, 30H ; convert to ASCII
MOV[TIME+4], BH ;save it

CVT_SEC:
MOV BH, DH  ;copy contents of seconds to BH
SHR DH, 4   ;convert high char to low order bits
ADD DH, 30H ;add 30H to convert to ASCII
MOV [TIME+6], DH    ;save it
AND BH, 0FH ;isolate lower 4 bits
ADD BH, 30H ;convert to ASCII
MOV[TIME+7], BH ;save it

GET_DATE:   ;get date from the system
    MOV AH, 04H    ;BIOS function to read date
    INT 1AH        ;call to BIOS, run 04H
    CALL CVT_DATE
    RET
;CH = Century
;CL = Year
;DH = Month
;DL = Day
;CF = 0 if clock is running, otherwise 1

CVT_DATE:
    CALL CVT_MO
    CALL CVT_DAY
    CALL CVT_YR
    CALL CVT_CT
    RET

CVT_MO:     ;convert the month to ASCII
MOV BH, DH  ;copy month to BH
SHR BH, 4   ;convert high char to low order bits
ADD BH, 30H ;add 30H to convert to ASCII
MOV [DATE], BH  ;save in DATE string
MOV BH, DH  ;copy month to BH
AND BH, 0FH ;isolate lower 4 bits
ADD BH, 30H ;convert lower bits to ASCII
MOV [DATE+1], BH;save in DATE string
RET

CVT_DAY:    ;convert the day to ASCII
MOV BH, DL  ;copy days to BH
SHR BH, 4   ;convert high char to low order bits
ADD BH, 30H ;add 30H to convert to ASCII
MOV [DATE+3], BH    ;save in DATE string
MOV BH, DL  ;copy days to BH
AND BH, 0FH ;isolate lower 4 bits
ADD BH, 30H ;convert lower bits to ASCII
MOV [DATE+4], BH;save in DATE string
RET

CVT_YR:     ;convert the year to ASCII
MOV BH, CL      ;copy year to BH
SHR BH, 4       ;convert high char to low order bits
ADD BH, 30H     ;convert to ASCII
MOV [DATE+8], BH    ;save it
MOV BH, CL      ;copy year to BH
AND BH, 0FH     ;isolate low order bits
ADD BH, 30H     ;convert to ASCII
MOV [DATE+9], BH    ;save in DATE string
RET

CVT_CT:     ;convert the century to ASCII
MOV BH, CH      ;copy century to BH
SHR BH, 4       ;convert high char to low order bits
ADD BH, 30H     ;convert to ASCII
MOV [DATE+6], BH    ;save it
MOV BH, CH      ;copy century to BH
AND BH, 0FH     ;isolate low order bits
ADD BH, 30H     ;convert to ASCII
MOV [DATE+7], BH    ;save it
RET
;
;Program End
;

End

And here's what I get when I run it at 9:11AM on 2/19/2015:

The current time is: 09:00:00
The date is: 02/09/0005

I've tried to add lots of comments of my intentions, so that you can get an idea of what I'm trying to do and easier see if there's some kind of logic error. I think it's pretty clear from the output that I'm missing getting my minutes and seconds into TIME and have some ideas on how to fix that, but after noon, I get some weird times, and I'm confused as to what's happening to my date. Any help is much appreciated.

Edit: Got time to work by splitting it up and actually dealing with minutes and seconds... whoops. Now my output is as follows:

Run at 9:23AM on 02/19/2015

The current time is: 09:23:02
The date is: 02/09/0005

EDIT2: Getting closer! Thanks for the [DATE] catch - I fixed that and am getting correct month and day values, and closer on year values. Figured out I wasn't shifting far enough since year is 4 characters long - 16 bits, not 8! - so I couldn't get the whole thing by only SHR 4 bits! My output now looks like:

The current time is: 09:43:02
The date is: 02/19/0015

EDIT 3: Added CVT_CT to convert the century to ASCII and add it to the [DATE] string, but am still getting the same output...

The current time is: 10:06:02
The date is: 02/19/0015

EDIT 4: I forgot to add a call to my new function... Wow. Working now!!! Thank you all for your help!

The current time is: 10:09:02
The date is: 02/19/2015

Side question: Any idea why the seconds would always be 02?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
aadria
  • 43
  • 1
  • 1
  • 4
  • 2
    Kudos for asking a question properly. If you want to learn, you should single step it in a debugger and see where it doesn't do what you want. In the current form, you of course don't do anything with `CL` and `DH` so obviously you don't get any output for minutes or seconds. The same logic you used for the hours should do the trick. You might want to pass in the location where you want the output to the `CVT_TIME` function so you can reuse it for all 3 parts. – Jester Feb 19 '15 at 15:22
  • @Jester Thanks, I split it up and it's working now; somehow completely overlooked the fact that I wasn't doing anything with them until I posted this, haha. Unfortunately, I don't have a debugger. I'm using A86 emulated on DosBox on Mac OSX - happen to know of any debuggers that would work for me? – aadria Feb 19 '15 at 15:27
  • You are using `DH` in your `CVT_MO` function and so you loose the *month* data (use `push` and `pop` to preserve registers). You also write more than once to `[date]` -- without index -- where you probably want to insert characters further on in the string. – Jongware Feb 19 '15 at 15:35
  • @Jongware Thank you for catching that I'm writing to [date] twice. Fixed that and am getting what appears to be correct days and months. Year is still misbehaving despite now accounting for all of its 16 bits. Sorry, but I don't understand the DH comment - can you tell me what line of the CVT_MO function you're seeing it on? – aadria Feb 19 '15 at 15:51
  • Could it be losing the top two characters of date because I'm moving it to BH - an 8-bit register? Nevermind - I'm ignoring century! Testing now!! – aadria Feb 19 '15 at 15:58
  • (.. on 2nd thought: maybe the reverse notation of source and target got me confused. Nevertheless -- saving and restoring the used registers never harmed nobody.) – Jongware Feb 19 '15 at 16:20
  • dosbox itself has a built-in debugger, but you need a version with that enabled. Alternatively you can run any dos debugger inside dosbox, such as turbo debugger. – Jester Feb 19 '15 at 16:26
  • Could you please paste your entire code, and we'll have a look at why the seconds always come out as 02? I was looking for the GET_TIME routine but I can't see it. – Sam Holloway Feb 20 '15 at 17:12

1 Answers1

3

All those individual (but very similar) functions for converting BCD into characters are somewhat messy and near guarantee you'll mess up some minor thing, such as forgetting to preserve registers when you may not the values in them later on.

If you're interested in avoiding this, look into the DRY (don't repeat yourself) principle (as opposed to WET (write everything twice). The Wikipedia page for DRY is a good start.


If you spend some time thinking about what can be moved to common code (i.e., refactoring), you'll end up with far less code to worry about, and therefore far less opportunity for bugs to sneak in.

The prime example in your case is the code that takes each BCD value and creates two characters from it. This consumed about forty lines of actual code (and that's just for the date bit, I assume there would have been another thirty-odd lines for the time, had you shown that).

If you look at the code below, you'll see I've refactored this out into put_bcd2 for a total of thirteen lines of code - even if you bump that up to twenty-seven because of the extra lines needed to call it, that's still a massive reduction. This greatly simplifyies both the code that does the conversion and the code that uses it.

; Main program.

    call    get_date            ; get date/time into string.
    call    get_time

    lea     dx, output          ; then output the string.
    mov     ah, 09h
    int     21h

    mov     ax, 4c00h           ; exit program.
    int     21h

; Variables.

output:
    db      "The current date is: "
date:
    db      "00/00/0000", 0dh, 0ah
    db      "The current time is: "
time:
    db      "00:00:00", 0dh, 0ah, '$'

; Subroutines.

; Gets the date and inlines it into the output.
get_date:
    mov     ah, 04h             ; get date from bios.
    int     1ah

    mov     bx, offset date     ; do day.
    mov     al, dl
    call    put_bcd2

    inc     bx                  ; do month.
    mov     al, dh
    call    put_bcd2

    inc     bx                  ; do year.
    mov     al, ch
    call    put_bcd2
    mov     al, cl
    call    put_bcd2

    ret

; Gets the time and inlines it into the output.
get_time:
    mov     ah, 02h             ; get time from bios.
    int     1ah

    mov     bx, offset time     ; do hour.
    mov     al, ch
    call    put_bcd2

    inc     bx                  ; do minute.
    mov     al, cl
    call    put_bcd2

    inc     bx                  ; do second.
    mov     al, dh
    call    put_bcd2

    ret

; Places two-digit BCD value (in al) as two characters to [bx].
;   bx is advanced by two, ax is destroyed.
put_bcd2:
    push    ax                  ; temporary save for low nybble.
    shr     ax, 4               ; get high nybble as digit.
    and     ax, 0fh
    add     ax, '0'
    mov     [bx], al            ; store that to string.
    inc     bx
    pop     ax                  ; recover low nybble.

    and     ax, 0fh             ; make it digit and store.
    add     ax, '0'
    mov     [bx], al

    inc     bx                  ; leave bx pointing at next char.

    ret
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • If you `mov ah, al` / `shr ah, 4` you can unpack both nibbles in parallel. `and ax, 0f0fh` / `add ax, '00'` / `mov [bx], ax` / `add bx, 2`. I guess you were writing this for human-readability, not for efficiency, though? Or were you optimizing for code-size by using `inc bx` twice, instead of 3-byte `add bx, 2` and a `[bx+1]` addressing mode (+1 byte)? Probably not, or you would have used `al` instead of `ax` for the 2-byte `add/and al, imm8` encodings. – Peter Cordes Mar 22 '18 at 16:45
  • Anyway, yes, DRY is good, but in assembler sometimes macros instead of functions are the right way to avoid it, if the reason for using assembly is performance. – Peter Cordes Mar 22 '18 at 16:47