Subject: Interger to ASCII in Assembly

From: "Matthew" <[email-masked]>
Date: Wed, 18 Nov 2009 23:03:43 -0000

Hey Adam, are you out there?  I was reading your CRPG site (http://www.adamantyr.com/crpg/) again and noticed your code for converting numbers to ASCII for display.  Something jumped out at me in the size of the routine and that you used two DIV instructions (the absolute slowest 9900 instruction we have).

I went digging and found some routines I wrote a long time ago and posted them in my newly resurrected online journal (blog).  You can check it out here if you want:

http://codehackcreate.blogspot.com/2009/11/interger-to-ascii-conversion-in.html

Matthew

---

From: mark wills <[email-masked]>
Date: Wed, 18 Nov 2009 21:12:43 -0800 (PST)

Nice post Matthew!

Want some more code? Here's something I wrote for my Forth system. It will convert numbers (signed or unsigned) to their ascii equivalents, in ANY number base (well, from 2 to 36 anyway). I'm proud of this little baby! It has a few optimisations in there for speed. I'm proud of the speed of it (you can see it running on the video that can be found in the FILES section of this board)

Rock on :-)

Mark.

;[ NUMBER TO STRING ( num -- addr len )
; Takes a number off the stack and converts it to a signed string equivalent,
; with respect to the current number base. Number base may be between
; 2 and 36. The routine checks location DOSIGN, and if 0, the
; number is treated as signed, else its unsigned. The routine also checks 
; location LZI, and, if zero, leading zero's will be supressed. 
; This is quite a bitch of a routine. Since any number base (between 2 and 36)
; can be employed this routine is rather complex. The routine must first
; determine the appropriate powers of the number base so we can divide the 
; target number later. Obviously this is expensive, so the routine remembers
; what the active number base was the last time it was called, and ONLY
; re-computes the exponents if the base has changed since the last time it was
; called. Sneaky.
; So, here we go, this first part computes the column values.
; So, if the base is 10, you end up with 1,10,100,1000,10000
; Oh, and, as with all our routines, we can't touch r3,r4,r5 or r12 :-)
ntsh    data hdoth,3
        text 'NTS '
nts     data $+2
        mov r5,r15            ; save r5 'cos we're using it
        mov *r4,r9            ; get number off stack
        li r7,2               ; exponent counter (base^0 and base^1 are easy to
                              ; compute ;-)
                              ; used as a word offset into workbuffer so counts
                              ; in multiples of 2
        c @base,@lbase        ; check if base has chaged since the last time we
                              ; were called
        jeq dodiv             ; base hasn't changed, no need to compute powers
        mov @base,@lbase      ; base has changed, store it in 'last base'
        li r0,1               ; base^0 is always 1 - easy ;-)
        li r1,wrkbuf          ; place to store the powers of our number base

        ; determine base^x until result > 65535
        mov r0,*r1+           ; store base^0 and move forward in buffer
        mov @base,*r1         ; base^1 is always base ;-) store it
pwr     mov *r1+,r5           ; get previous exponent
        mpy @base,r5          ; multiply it by base - lower 16 bit result in r6
        mov r5,r5             ; see if the result overflowed into upper 16 bits
        jne pwrout            ; there was an overflow, exit loop
        mov r6,*r1            ; otherwise store result
        inct r7               ; and increment exponent counter
        jmp pwr               ; and repeat

        ; Ok we have computed the 'column values' (powers) for our base. Now we
        ; sucessively divide the number down until nothing is left, building
        ; the string equivalent as we compute each digit. Just to make life
        ; harder for ourselves, we will optionally allow leading zero's to be
        ; supressed. If the word at LZI>0 then leading zero's are suppressed.
pwrout  mov r7,@expcnt        ; save exponent count for next time routine is run
dodiv   mov @expcnt,r7        ; entry point when exponents aren't computed.
                              ; restore exponent count
        li r0,strbuf          ; address of string buffer where we build string
        clr r1                ; buffer length counter
        mov @dosign,r8        ; check if we are producing an unsigned number
        jne ninn              ; skip if we are
        mov r9,r8             ; else, check if number is negative and if so, 
                              ; add "-" character
        andi r8,>8000         ; is it negative
        jeq    ninn           ; its not negative, jump
        li r8,'-'*256         ; the number is negative, add a minus sign to the 
                              ; string buffer
        movb r8,*r0+          ; place it in the buffer
        inc r1                ; increment length counter
        neg r9                ; change the number to positive
ninn    clr r8                ; div instruction uses 32 bit dividend, our 16 bit
                              ; argument is in r9
        mov @lzi,r10          ; leading zero indicator 0=suppress leading zero's
nxtdig  div @wrkbuf(r7),r8    ; divide our number by exponent value. result=r8,
                              ; remainder=r9
        mov r8,r8             ; was the result 0?
        jeq testlz            ; if yes then check if we're ignoring leading zeros
        seto r10              ; not zero, so reset leading zero indicator
dodig   movb @tlut(r8),*r0+   ; lookup digit value, move it to string buffer and
                              ; advance buffer address
        clr r8                ; clear result for next interation
        inc r1                ; increment length counter
iglz    dect r7               ; done all our columns/exponents?
        jne nxtdig            ; loop if not
        movb @tlut(r9),*r0+   ; lookup digit value, move it to string buffer and
                              ; advance buffer address

        ; we've done our division, push address & length to the stack and exit
        li r0,strbuf          ; address of string buffer
        mov r0,*r4+           ; move address to stack, increment stack pointer
        inc r1                ; adjust length for remainder
        mov r1,*r4            ; move length to stack
        mov r15,r5            ; restore return stack pointer
        b *r12

        ; we're looking for leading zero's and ignoring them
testlz  mov r10,r10           ; are we ignoring leading zero's?
        jeq iglz              ; 0=ignore leading digit
        jmp dodig             ; else do digit normally
        ; character lookup table for printing numbers between bases 2 to 36
tlut    text '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
;]

---

From: "Matthew" <[email-masked]>
Date: Thu, 19 Nov 2009 19:34:45 -0000

Thanks.

Your routine is pretty extensive!  Very nice.  That could come in handy for language development for sure.  I didn't read it close enough to tell if you support numbers larger than 16-bit?  My code is limited to 16-bit and I was really thinking of displaying game scores, so size and speed were the requirements.

Hmm, now that I think about it though, games have scores that go to 999,999 or even into the millions...  I better go think about that.

Matthew

---

From: tursi <[email-masked]>
Date: Thu, 19 Nov 2009 15:01:50 -0800 (PST)

> Hmm, now that I think about it though, games have scores that go to 
> 999,999 or even into the millions...  I better go think about that.

To keep the math simple in Mario Bros, which does that, and also uses 
16-bit variables, I capped the low word at 9,999 (I didn't bother capping 
the high word in the code, but conceptially, it's capped too). 9,999 is 
the highest "all nines" decimal number that a 16-bit value can hold (since 
99,999 exceeds 16 bits).

That way the same 4-digit output routine could display each half of the 
score and there was no overlap to worry about in the display code. I had a 
wrapper function "AddScore" that would take care of incrementing the high 
word when the low word overflowed at 10,000. It worked pretty well. :)

---

From: "Matthew" <[email-masked]>
Date: Fri, 20 Nov 2009 00:28:23 -0000

> word when the low word overflowed at 10,000. It worked pretty well. :)

That's a good idea!  I was also thinking of just storing the digits in 8 bytes (1 byte per digit) and writing a simple function to take care of adding to the score.  It would be similar to BCD except the digits would be in full bytes instead of nybbles.  It would probably be faster than the int to ASCII routine just by avoiding 1 to 5 possible DIV instructions.

I saw this done in the PAC-MAN coin-op.  They stored the character patterns for the digits 0 - 9 as the first character patterns, so there was no digit to ASCII adjustment required (i.e. having to add 48), and they stored the score directly in the video screen buffer, so there was no memory unnecessarily wasted storing the score.  Of course in the coin-op they also had direct CPU access to the video memory so there was no delay going through the VDP to update the score.  I liked the idea a lot!

Matthew