This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

Hex to Decimal

Hex to Decimal
by on (#129882)
Here is a routine I wrote to convert a 16 bit number (0-65535) to decimal, each digit separated. It only takes 150 to 162 cycles as shown below.

Edit - This is now revision 2 of the code...

Code:
;--------------------------------
;0-9999 conversion stats
;--------------------------------
  ;cycles          occurances
    ;150 - $0860 -->  2,144
    ;151 - 0
    ;152 - 0
    ;153 - $16C0 -->  5,824
    ;154 - 0
    ;155 - 0
    ;156 - $07F0 -->  2,032

  ;average execution is 152.97 cycles

;--------------------------------
;0-65535 conversion stats
;--------------------------------
  ;cycles          occurances
    ;150 - $1738 -->  5,944
    ;151 - 0
    ;152 - 0
    ;153 - $9528 --> 38,184
    ;154 - 0
    ;155 - 0
    ;156 - $2A30 --> 10,800
    ;157 - 0
    ;158 - 0
    ;159 - $1F18 -->  7,960
    ;160 - 0
    ;161 - 0
    ;162 - $0A58 -->  2,648

  ;average execution is 154.31 cycles


Here is the routine. It takes 258 bytes which is pretty good considering how fast it goes. There are other routines out there that are much shorter, but take 100's of cycles. My goal was speed while keeping the byte cost as low as possible.

If you want ASCII, then you can compile it in at a cost of just 8 more bytes, and 8 more cycles.

Code:
;----------------------------------------------------------
;Convert 16 bit Hex to Decimal (0-65535) Rev 2
;By Omegamatrix
;Further optimizations by tepples
;
; Takes 150-162 cycles to execute.
;
; Starts with 16 bit number split into hexHigh and hexLow.
; Uses A,X,Y, and three bytes of zeropage ram:
;
;    Ten Thousands digit - zp ram
;    Thousands digit - returned in A register
;    Hundreds digit - zp ram
;    Tens digit - returned in X register
;    Ones digit - zp ram
;----------------------------------------------------------

;temp register and decHundreds are doubled up to save ram...
temp = decHundreds


ASCII_OFFSET = $30


Times256_Low:
    .byte $00,$38,$0C,$44,$18,$50,$24,$5C
    .byte $30,$04,$3C,$10,$48,$1C,$54,$28
Times256_Med:
    .byte $00,$02,$05,$07,$0A,$0C,$0F,$11
    .byte $14,$17,$19,$1C,$1E,$21,$23,$26


Times16_Low:
    .byte $00
Times4096_Low:
    .byte $00
Times4096_Med:
    .byte $00
Times4096_High:
    .byte $00 + ASCII_OFFSET

    .byte $10,$60,$28,$00 + ASCII_OFFSET   ; interlaced tables, this allows less shifts to be made...
    .byte $20,$5C,$51,$00 + ASCII_OFFSET
    .byte $30,$58,$16,$01 + ASCII_OFFSET
    .byte $40,$54,$3F,$01 + ASCII_OFFSET
    .byte $50,$50,$04,$02 + ASCII_OFFSET
    .byte $60,$4C,$2D,$02 + ASCII_OFFSET
    .byte $0C,$48,$56,$02 + ASCII_OFFSET
    .byte $1C,$44,$1B,$03 + ASCII_OFFSET
    .byte $2C,$40,$44,$03 + ASCII_OFFSET
    .byte $3C,$3C,$09,$04 + ASCII_OFFSET
    .byte $4C,$38,$32,$04 + ASCII_OFFSET
    .byte $5C,$34,$5B,$04 + ASCII_OFFSET
    .byte $08,$30,$20,$05 + ASCII_OFFSET
    .byte $18,$2C,$49,$05 + ASCII_OFFSET
    .byte $28,$28,$0E,$06 + ASCII_OFFSET

ShiftedBcdTab
    .byte $00,$01,$02,$03,$04,$08,$09,$0A,$0B,$0C
    .byte $10,$11,$12,$13,$14,$18,$19,$1A,$1B,$1C
    .byte $20,$21,$22,$23,$24,$28,$29,$2A,$2B,$2C
    .byte $30,$31,$32,$33,$34,$38,$39,$3A,$3B,$3C
    .byte $40,$41,$42,$43,$44,$48,$49,$4A,$4B,$4C


StartHexToDec:
    lda    hexHigh               ;3  @3
    and    #$0F                  ;2  @5
    tax                          ;2  @7
    eor    hexHigh               ;3  @10
    lsr                          ;2  @12   carry is clear, shifting just 2 times instead of 4,
    lsr                          ;2  @14   since interlaced tables are used.
    tay                          ;2  @16
    lda    Times4096_High,Y      ;4  @20
    sta    decTenThousands       ;3  @23
    lda    Times4096_Low,Y       ;4  @27
    adc    Times256_Low,X        ;4  @31
    sta    temp                  ;3  @34
    lda    Times4096_Med,Y       ;4  @38
    adc    Times256_Med,X        ;4  @42
    tay                          ;2  @44

    lda    hexLow                ;3  @47
    and    #$F0                  ;2  @49
    lsr                          ;2  @51
    lsr                          ;2  @53
    tax                          ;2  @55
    tya                          ;2  @57
    cpx    #13*4                 ;2  @59   times 4 due to interlaced table
    adc    #0                    ;2  @61
    cpx    #7*4                  ;2  @63
    adc    #0                    ;2  @65
    tay                          ;2  @67

    lda    hexLow                ;3  @70
    and    #$0F                  ;2  @72
    adc    Times16_Low,X         ;4  @76
    adc    temp                  ;3  @79
    bcs    .sub100               ;2³ @81/82
    cmp    #100                  ;2  @83
    bcc    .skip1                ;2³ @85/86

.sub100:
    sbc    #100                  ;2  @87
    iny                          ;2  @89
.skip1:
    cmp    #100                  ;2  @91
    bcc    .skip2                ;2³ @93/94
    sbc    #100                  ;2  @95
    iny                          ;2  @97
.skip2:
    lsr                          ;2  @99
    tax                          ;2  @101
    lda    ShiftedBcdTab,X       ;4  @105
    tax                          ;2  @107
    rol                          ;2  @109
    and    #$0F                  ;2  @111
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET         ;2
  ENDIF
    sta    decOnes               ;3  @114

    txa                          ;2  @116
    lsr                          ;2  @118
    lsr                          ;2  @120
    lsr                          ;2  @122
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET         ;2
  ENDIF
    tax                          ;2  @124   or STA decTens
    tya                          ;2  @126
    cmp    #100                  ;2  @128
    bcc    .skip3                ;2³ @130/131
    sbc    #100                  ;2  @132
    inc    decTenThousands       ;5  @137
.skip3:
    lsr                          ;2  @139
    tay                          ;2  @141
    lda    ShiftedBcdTab,Y       ;4  @145
    tay                          ;2  @147
    rol                          ;2  @149
    and    #$0F                  ;2  @151

  IF ASCII_OFFSET
    ora    #ASCII_OFFSET         ;2
  ENDIF
    sta    decHundreds           ;3  @154

    tya                          ;2  @156
    lsr                          ;2  @158
    lsr                          ;2  @160
    lsr                          ;2  @162
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET         ;2
  ENDIF
                                 ;   A = decThousands
Re: Hex to Decimal
by on (#129883)
Very fast. Such efficiency. Wow.

To save 2 cycles and 2 bytes, can this:
Code:
    lda    hexHigh               ;3  @3
    and    #$F0                  ;2  @5
    lsr                          ;2  @7
    lsr                          ;2  @9    carry is clear, shifting just 2 times instead of 4,
    tay                          ;2  @11   since interlaced tables are used.
    lda    hexHigh               ;3  @14
    and    #$0F                  ;2  @16
    tax                          ;2  @18

be replaced with this?
Code:
    lda    hexHigh               ;3  @3    fedcba98
    and    #$0F                  ;2  @5    ....ba98
    tax                          ;2  @7
    eor    hexHigh               ;3  @10   fedc....
    lsr                          ;2  @12   .fedc...
    lsr                          ;2  @14   ..fedc..   carry is clear, shifting twice
    tay                          ;2  @16   instead of 4, since interlaced tables are used.

To save one byte of RAM, can decThousands be the same address as temp?

Source-wise, you don't need the constant CONVERT_TO_ASCII. Instead, the ORs can be wrapped with IF ASCII_OFFSET.

Currently, the ora #ASCII_OFFSET statements require ASCII_OFFSET to be a multiple of $10. Allowing it to be not a multiple, such as storing the digits at $06-$0F, would take additional bytes to ensure that the carry is clear. I'm looking into how many additional cycles those would take. The one for decOnes and decHundreds would take no additional cycles by turning rol A and #$0F into and #$07 rol A because the carry out of the rotation isn't used. But the ones for decTens and decThousands lack a convenient and instruction before it. Cost: 4 cycles, 2 bytes.
Re: Hex to Decimal
by on (#129884)
Cool. Coincidentally only a few days ago I was looking for a 16-bit decimal conversion routine, so I'll probably give this one a shot.
Re: Hex to Decimal
by on (#129886)
tepples wrote:
Very fast. Such efficiency. Wow.

To save 2 cycles and 2 bytes, can this:
Code:
    lda    hexHigh               ;3  @3
    and    #$F0                  ;2  @5
    lsr                          ;2  @7
    lsr                          ;2  @9    carry is clear, shifting just 2 times instead of 4,
    tay                          ;2  @11   since interlaced tables are used.
    lda    hexHigh               ;3  @14
    and    #$0F                  ;2  @16
    tax                          ;2  @18

be replaced with this?
Code:
    lda    hexHigh               ;3  @3    fedcba98
    and    #$0F                  ;2  @5    ....ba98
    tax                          ;2  @7
    eor    hexHigh               ;3  @10   fedc....
    lsr                          ;2  @12   .fedc...
    lsr                          ;2  @14   ..fedc..   carry is clear, shifting twice
    tay                          ;2  @16   instead of 4, since interlaced tables are used.



Ha! That is a good eye. :) I haven't tried it yet, but looking at it I would say yes you can. The routine also executes that part of the code every time, so every execution case has been made better. Very cool, and I'll have to remember that trick (never thought of using it before). :D

tepples wrote:
To save one byte of RAM, can decThousands be the same address as temp?

Yep. I was going to actually going to equate it like that at the top, but I figured it was something people would do anyhow.


tepples wrote:
Source-wise, you don't need the constant CONVERT_TO_ASCII. Instead, the ORs can be wrapped with IF ASCII_OFFSET.

Currently, the ora #ASCII_OFFSET statements require ASCII_OFFSET to be a multiple of $10. Allowing it to be not a multiple, such as storing the digits at $06-$0F, would take additional bytes to ensure that the carry is clear. I'm looking into how many additional cycles those would take. The one for decOnes and decHundreds would take no additional cycles by turning rol A and #$0F into and #$07 rol A because the carry out of the rotation isn't used. But the ones for decTens and decThousands lack a convenient and instruction before it. Cost: 4 cycles, 2 bytes.

I don't 100% follow what you are trying to do here so I'll wait and see what you come up with. Please continue to optimize the routine if you can. :)
Re: Hex to Decimal
by on (#129889)
Omegamatrix wrote:
I don't 100% follow what you are trying to do here

Raw digits: $00 means zero and $09 means 9.
ASCII: $30 means zero and $39 means 9.
Possible layout of tiles in an actual game: $06 means zero and $0F means 9, or $F6 means zero and $FF means 9.
Re: Hex to Decimal
by on (#129891)
tepples wrote:
Omegamatrix wrote:
I don't 100% follow what you are trying to do here

Raw digits: $00 means zero and $09 means 9.
ASCII: $30 means zero and $39 means 9.
Possible layout of tiles in an actual game: $06 means zero and $0F means 9, or $F6 means zero and $FF means 9.


Gotcha. This is my in-experience with the NES coming into play ha ha.


I tested your optimization and it worked as expected. I then also changed the code so that Y ends with decThousands, and X ends with decTens. I thought about this before, but thought it might be too messy. I changed my mind because X and Y get trashed anyhow, and it's easy enough for someone to change it back if they like.


Doing so saved an additional 2 cycles and 2 bytes, and with your savings the code runs quicker and shorter then ever before. :) I've updated the first post with the new routine, and I also put in the equate to specify the temp ram.
Re: Hex to Decimal
by on (#129894)
I have some other Hex to Decimal routines. Here is one I wrote that takes converts 0-63 hex to 0-99 BCD. It doesn't split the digits apart but keeps them in the same bytes. The routine is doing a divide by 10 and then times it by 6 before adding it back to the original sum. The good thing about this routine is that it is constant cycles.
Code:
;Hex to BCD (good 0-99)
;24 bytes, 39 cycles
  sta  temp
  lsr
  adc  temp
  ror
  lsr
  lsr
  adc  temp
  ror
  adc  temp
  ror
  lsr
  and  #$3C
  sta  temp2
  lsr
  adc  temp2
  adc  temp


Next (and this won't work on the NES, but hey maybe some other programmers might be reading this) I wrote one that used BCD mode to help the conversion:
Code:
;Hex to BCD (good 0-99)
;24 bytes, 28 cycles
    tay              ;2  @2
    lsr              ;2  @4
    lsr              ;2  @6
    lsr              ;2  @8
    lsr              ;2  @10
    tax              ;2  @12
    tya              ;2  @14
    and  #$0F        ;2  @16
    sed              ;2  @18
    clc              ;2  @20
    adc  #0          ;2  @22
    adc  BcdTab,X    ;4  @26
    cld              ;2  @28



BcdTab:
  .byte $00,$16,$32,$48,$64,$80,$96


It was at this point where I became interested in the NES, and splitting the individual digits apart. I have some more routines that I wrote which I will post at another time that does this. There is also a nice compact routine that Bregalad wrote and Movax12 helped improved. At the end of the thread I reduced it a few more bytes as well.
Re: Hex to Decimal
by on (#129895)
Thanks.

Got anything to do 0-255 to three digits? My current routine is 53 bytes and takes 80 cycles including the RTS.
Code:
.macro bcd8bit_iter value
  .local skip
  cmp value
  bcc skip
  sbc value
skip:
  rol highDigits
.endmacro

;;
; Converts a decimal number to two or three BCD digits
; in no more than 80 cycles.
; @param a the number to change
; @return a: low digit; 0: upper digits as nibbles
; No other memory or register is touched.
.proc bcd8bit
highDigits = 0

  ; First clear out two bits of highDigits.  (The conversion will
  ; fill in the other six.)
  asl highDigits
  asl highDigits

  ; Each iteration takes 11 if subtraction occurs or 10 if not.
  ; But if 80 is subtracted, 40 and 20 aren't, and if 200 is
  ; subtracted, 80 is not, and at least one of 40 and 20 is not.
  ; So this part takes up to 6*11-2 cycles.
  bcd8bit_iter #200
  bcd8bit_iter #100
  bcd8bit_iter #80
  bcd8bit_iter #40
  bcd8bit_iter #20
  bcd8bit_iter #10
  rts
.endproc
Re: Hex to Decimal
by on (#129896)
tepples wrote:
Thanks.

Got anything to do 0-255 to three digits? My current routine is 53 bytes and takes 80 cycles including the RTS.



Yes I do have some three digits. I am just preparing supper so I'll take a look in a bit. I also just realized I'm using a TAY at the end of the 16 bit routine for no reason. I could just return the value in A, and save 2 cycles and a byte. I'll update that later.
Re: Hex to Decimal
by on (#129900)
Okay here is one that I just tweaked to handle both (0-99) and (0-255) separately. It takes 43 bytes and cycles are listed below including the 12 cycles for jumping and returning from the subroutines.

Edit - This is now rev 3... uses less bytes, slightly faster average execution time, and the BIT instruction has changed. ASCII support is now optional for 2 more cycles and 2 more bytes. :)

Code:
;--------------------------------
;0-99 conversion stats
;--------------------------------
  ;cycles  occurances
    ;28  -  10
    ;31  -  10
    ;34  -  10
    ;37  -  10
    ;39  -  10
    ;42  -  10
    ;45  -  10
    ;48  -  10
    ;50  -  10
    ;56  -  10

  ;average execution is 41 cycles

;--------------------------------
;0-255 conversion stats
;--------------------------------
  ;cycles  occurances
    ;35  -  10
    ;38  -  10
    ;41  -  10
    ;43  -  10
    ;44  -  10
    ;46  -  30
    ;49  -  30
    ;52  -  26
    ;54  -  10
    ;55  -  10
    ;57  -  30
    ;60  -  20
    ;63  -  20
    ;65  -  10
    ;68  -  10
    ;71  -  10

  ;average execution is 52.78 cycles


Here are the routines. You start with the value in A, and at the end of the routine A = Ones digit, X = Tens digit, and Y = Hundreds digit (when using 0-255) conversion. Y is not used for the HexToDec99 subroutine.
Code:
;---------------------------------------
;  0-255 Hex to Decimal conversion
;  By Omegamatrix (rev 3)
;  35-71 cycles, average 52.78 cycles
;---------------------------------------

;change to $30 for ASCII support. Adds 2 cycles and 2 bytes.
ASCII_OFFSET = $00

HexToDec255; SUBROUTINE
    ldy    #0 + ASCII_OFFSET
    cmp    #100                  ; A = 0-255
    bcc    .done100
    iny
    sbc    #200
    bcc    .use100
    iny                          ; If a mapper conflicts with SBC #-101 (becomes BIT $9BE9), then you can try:
    .byte $2C                    ; - 'BNE .done100' instead of .byte $2C, as Y=2 or Y=$32 at this point...
.use100
    sbc    #-101                 ; - substitute with SBC #-101 with ADC #100 (which would become BIT $6469)
.done100:
                                 ; Y = decimal hundreds

;---------------------------------------
;  0-99 Hex to Decimal conversion
;  By Omegamatrix (rev 3)
;  28-56 cycles, average 41 cycles
;---------------------------------------

HexToDec99; SUBROUTINE
    ldx    #0 + ASCII_OFFSET
    cmp    #50                   ; A = 0-99
    bcc    .try20
    sbc    #50
    ldx    #5 + ASCII_OFFSET
    bne    .try20                ; always branch

.div20:
    inx
    inx
    sbc    #20
.try20:
    cmp    #20
    bcs    .div20
.try10:
    cmp    #10
    bcc    .finished
    sbc    #10
    inx
.finished:
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
                                 ; X = decimal tens
                                 ; A = decimal ones
    rts
Re: Hex to Decimal
by on (#129934)
I updated the Hex to Decimal routine (0-255) to rev 2 in the above post. I've chopped 3 bytes out of the old routine and slightly speed it up.

I'm doing BIT $6469 to skip over the ADC #100. From what I've seen of the NES memory map $6469 is just SRAM so reading there should be no problem...
Re: Hex to Decimal
by on (#129940)
Omegamatrix wrote:
I'm doing BIT $6469 to skip over the ADC #100. From what I've seen of the NES memory map $6469 is just SRAM so reading there should be no problem...

The cartridge is free to map whatever it wants at $6469. In fact, all of $4020..$FFFF. It could be RAM, ROM, mapper registers or nothing (= open bus). Most of the time it's (S)RAM or nothing, though.
Re: Hex to Decimal
by on (#129942)
But so long as the cart doesn't put something with read side effects into $6xxx, BIT $6469 should work. (The addresses with read side effects on the NES are $2002, $2007, $4015-$4017, and $4020-$5FFF for the Vs. System's credit acknowledge.) The only mapper that I can think of that has read side effects there is Bandai boards with an I²C EEPROM.
Re: Hex to Decimal
by on (#129959)
Omegamatrix, nice code, but what I am more interested in at the moment is how you determine the following:

Code:
;--------------------------------
;0-99 conversion stats
;--------------------------------
  ;cycles  occurances
    ;28  -  10
    ;31  -  10
    ;34  -  10
    ;37  -  10
    ;39  -  10
    ;42  -  10
    ;45  -  10
    ;48  -  10
    ;50  -  10
    ;56  -  10

  ;average execution is 41 cycles


What method/tools do you use to create that information?
Re: Hex to Decimal
by on (#129960)
I can't speak for the OP's methodology, but I wrote a 6502 simulator in Python so that I could run automated unit tests on 6502 code. With peek(), poke(), and jsr() functions, I can determine whether a subroutine returns a correct result and how long it takes. Combining such a simulator with collections.Counter makes it easy to test all combinations of parameters and produce a cycle count histogram.

Want to see my simulator?
Re: Hex to Decimal
by on (#129965)
Sure, tepples. Right now I am (slowly) working on something in C that will combine 6502 assembly and C in the same source file and call ca65 to build the assembly. I think my idea will work, but I would be interested to see how others solved this problem.
Re: Hex to Decimal
by on (#129967)
Movax12 wrote:
Omegamatrix, nice code, but what I am more interested in at the moment is how you determine the following:

Code:
;--------------------------------
;0-99 conversion stats
;--------------------------------
  ;cycles  occurances
    ;28  -  10
    ;31  -  10
    ;34  -  10
    ;37  -  10
    ;39  -  10
    ;42  -  10
    ;45  -  10
    ;48  -  10
    ;50  -  10
    ;56  -  10

  ;average execution is 41 cycles


What method/tools do you use to create that information?


I'm running a 2600 program I wrote, because I don't know much about the NES yet. The 2600 has a 1 cycle timer which I'm loading before the subroutine and checking afterward. From there I run some code to check for the best times and worst times. The difference between best and worse times is small enough that I can use some zero page ram to record how many times each cycle scenario occurs. I subtract the best time to offset it to zero, and also 4 cycles for loading the 2600 timer, and an additional 3 cycles for saving the value left in A at the end of the routine (I do that before I load the timer in A).


The program I'm using scrolls through all possible values, and checks if they are all correct. If it's good I get a green passed screen, or a red fail screen. Here's some of the code:

Code:
.innerLoop:
    dec    hexValue
    lda    hexValue              ; A = 0-255

  IF DETERMINE_BEST_WORSE_TIME
    ldy    #255
    sty    TIM1T
  ENDIF
    jsr    HexToDec255

;... routine runs here

    sta    decOnes              ; save routine value
    lda    INTIM                ; get timer value
    sty    decHundreds
    stx    decTens

    eor    #$FF
    sec
    sbc    #4+3                 ; take away 3 cycles for STA decOnes, and 4 cycles for LDA INTIM
    cmp    worstTime            ; save best/worse time
    bcc    .lessThen
    sta    worstTime            ; worseTime begins the routine loaded with 0, and bestTime begins with $FF
.lessThen:
    cmp    bestTime
    bcs    .moreThen
    sta    bestTime
.moreThen:

    sec
    sbc    #35                   ; You have run the routine once to get the best time (35 in this case),
    tax                          ; and run the routine again afterward with that value.
    clc
    lda   #1
    adc   occurances,X           ; update count for each routine time occurance...
    sta   occurances,X



    ldy    hexValue              ; verify routine values against tables stored in rom
    lda    decOnes
    cmp    OnesTab,Y
    bne    .setBadFlag
    lda    decTens
    cmp    TensTab,Y
    bne    .setBadFlag
    lda    decHundreds
    cmp    HundredsTab,Y
    beq    .skipMarkBadResult

.setBadFlag:
    lda    #$FF
    sta    badValueFlag
    lda    firstBadValue
    bne    .skipMarkBadResult
    sty    firstBadValue

.skipMarkBadResult:
    dey
    cpy    #$FF
    bne    .innerLoop


Here in Stella's debugger you can see the cycles cases begin at $B0 and continues from there. This snapshot is for the results of 0-255.
Attachment:
DebuggerValues.jpg
DebuggerValues.jpg [ 72.54 KiB | Viewed 3116 times ]
Re: Hex to Decimal
by on (#129968)
Movax12 wrote:
tepples wrote:
I wrote a 6502 simulator in Python so that I could run automated unit tests on 6502 code. [...] Want to see my simulator?

Sure, tepples.

Download my work-in-progress Cookie Clicker clone and look in the "float" folder.

Movax12 wrote:
Right now I am (slowly) working on something in C that will combine 6502 assembly and C in the same source file and call ca65 to build the assembly. I think my idea will work, but I would be interested to see how others solved this problem.

Anything like Leushenko's x86-to-C static recompiler?

Omegamatrix wrote:
I'm running a 2600 program I wrote [describes unit test harness]

Neat. It'd be harder to do that on an NES without mapper support.
Re: Hex to Decimal
by on (#129969)
tepples wrote:
Anything like Leushenko's x86-to-C static recompiler?.


I considered that idea, but i want to be able to use actual ca65 compatible source, so I am going to load the binary output of ca65 into an open-source 6502 emulator's RAM and hook back to C code when needed.

Omegamatrix, interesting solution. I am going to have to look at that more than once to really get what you are doing.
Re: Hex to Decimal
by on (#129971)
tepples wrote:
But so long as the cart doesn't put something with read side effects into $6xxx, BIT $6469 should work. (The addresses with read side effects on the NES are $2002, $2007, $4015-$4017, and $4020-$5FFF for the Vs. System's credit acknowledge.) The only mapper that I can think of that has read side effects there is Bandai boards with an I²C EEPROM.



I have another option I could try. If I substitute:
Code:
    .byte $2C                    ; BIT $6469
.use100
    adc    #100
.done


With this:
Code:
    .byte $2C                    ; BIT $9BE9
.use100
    sbc    #-101
.done


The function is the same. It's a little more convoluted and certainly harder to read, but if it doesn't conflict with anything I would certainly switch over to it.

If that still potentially breaks something I'm going to stick a branch in there with some comments that a byte can be saved with BIT, but the user must check for these odd cases.
Re: Hex to Decimal
by on (#129972)
I don't think any NES mapper has read side effects in $8000-$FFFF because that's where the program itself is, except perhaps for really freaking complicated copy protection schemes.
Re: Hex to Decimal
by on (#129973)
tepples wrote:
read side effects in $8000-$FFFF
Mapper 234 triggers bankswitches on reads from $FF80-$FFF7. (This doesn't pose a problem with using BIT here)
Re: Hex to Decimal
by on (#129974)
thefox wrote:
In fact, all of $4020..$FFFF. It could be RAM, ROM, mapper registers or nothing (= open bus).
Actually, I think it could even map things to the rest of the address space too if it wanted to (I have discussed deliberately interfering with mirrors of RAM and PPU registers, in order to allow writes to write two things at once; for example, a mapper may be designed to mirrors its write-only registers at $1xxx, $3xxx, $5xxx, $7xxx, $9xxx, $Bxxx, $Dxxx, $Fxxx).

Even if such a thing existed it still won't interfere with using BIT here (which only reads), although changing it to SBC #-101 like you have would in fact fix it with whatever mapper you might want to use (probably; I don't know if there are other cases).

Like they said there are some mappers that have side-effects on read from some registers, but $9BE9 is not in that range, and I am not sure why anyone would make one where there are side-effects on read at that address.

Maybe you can put a comment explaining it; if needed, it can then be changed between ADD #100 or SBC #-101 or a branch instead of BIT, depending on the mapper, if it is necessary to do so.
Re: Hex to Decimal
by on (#129976)
zzo38 wrote:
Maybe you can put a comment explaining it; if needed, it can then be changed between ADD #100 or SBC #-101 or a branch instead of BIT, depending on the mapper, if it is necessary to do so.


Good idea. I added in the comment with some choices.

I also added some ASCII support. Cost was just 2 bytes and 2 cycles. :)
Re: Hex to Decimal
by on (#130004)
I took a look and realized there is a easy way to chop another couple of bytes out of the routine by eliminating a branch. This will bring the total size down to 41 bytes, which is not too bad. The downside is that doing so will add an extra 3 cycles to some cases, including the worst case. To me that seemed like no gain at all. In the end I decided to to leave the routine as is and just post the changes here for user to make their own decision.


Old:
Code:
HexToDec99; SUBROUTINE
    ldx    #0 + ASCII_OFFSET
    cmp    #50                   ; A = 0-99
    bcc    .try20
    sbc    #50
    ldx    #5 + ASCII_OFFSET
    bne    .try20                ;always branch

.div20:
    inx
    inx
    sbc    #20
.try20:


New (save 2 bytes by eliminating branch, but add 3 more cycles):
Code:
HexToDec99; SUBROUTINE
    ldx    #0 + ASCII_OFFSET
    cmp    #50                   ; A = 0-99
    bcc    .try20
    sbc    #30
    ldx    #3 + ASCII_OFFSET
.div20:
    inx
    inx
    sbc    #20
.try20:
Re: Hex to Decimal
by on (#130363)
I've wrote a couple more 16 bit number (0-65535) to decimal routines. My old, original routine took 258 bytes, and 150-162 cycles. That routine however was not set up for JSR. To do so would requires 3 more bytes, and 15 cycles. That brings it true cost to 261 bytes, and 165-177 cycles.

The first new routine I made takes only 234 bytes, and 157-162 cycles (including the JSR, RTS). Its worse case time is 3 cycles faster then the old routines best case, and it saves 27 bytes. The second new routine saves even more taking only 174 bytes, but takes longer requiring 178-186 cycles to execute. Still, those cycles are not too bad, and I'm pretty happy about the overall performance of both routines. :D

The basic approach of both new routines is different to the original. Now I'm going after the 10,000's and 1,000's digit by shifting the high byte to the 1024 value bit. This gives a pretty close approximation to an integer divide by 1000. I start with that and correct the result. The fast routine uses a multiply by 24 table to correct the result. I doubled that table up storing the high byte value (0-6) in the bits 0-2. Since the low byte value only ever uses bits 3-7 this was easy to do.

Support has also been added for entries into the routine to do 0-999, 0-255, and 0-99 conversions. Adding support for HexToDec255 and HexToDec999 requires 9 more bytes. Ascii support is also available at a cost of 2 bytes and 2 cycles for each digit.


Stats:
Code:
;slow routine - 174 bytes, 183 bytes with HexToDec255 and HexToDec999
;HexToDec99     ; 37 cycles
;HexToDec255    ; 52-57 cycles
;HexToDec999    ; 72-77 cycles
;HexToDec65535  ; 178-186 cycles


;Fast routine - 234 bytes, 243 bytes with HexToDec255 and HexToDec999
;HexToDec99     ; 37 cycles
;HexToDec255    ; 52-57 cycles
;HexToDec999    ; 72-77 cycles
;HexToDec65535  ; 157-162 cycles

;-------------------------------------------------------------------------------

;HexToDec99
; start in A
; end with A = 10's, decOnes

;HexToDec255
; start in A
; end with Y = 100's, A = 10's, decOnes

;HexToDec999
; start with A = high byte, X = low byte
; end with Y = 100's, A = 10's, decOnes
; requires 1 extra temp register on top of decOnes, could combine
; these two if HexToDec65535 was eliminiated...

;HexToDec65535
; start with A = high byte, X = low byte
; end with decTenThousand, decThousand, Y = 100's, A = 10's, decOnes
; requires 2 extra temp registers on top of decTenThousand, decThousand, decOnes



Here's the fast routine:
Code:
;Hex to Decimal (0-65535) conversion
;by Omegamatrix
;
;HexToDec99     ; 37 cycles
;HexToDec255    ; 52-57 cycles
;HexToDec999    ; 72-77 cycles
;HexToDec65535  ; 157-162 cycles

ASCII_OFFSET = $00
temp         = decOnes
hexHigh      = temp2
hexLow       = temp3


Mult24Tab:
     .byte $00,$18,$30,$48,$60,$78,$90,$A8,$C0,$D8
     .byte $F0,$09,$21,$39,$51,$69,$81,$99,$B1,$C9
     .byte $E1,$F9,$12,$2A,$42,$5A,$72,$8A,$A2,$BA
     .byte $D2,$EA,$03,$1B,$33,$4B,$63,$7B,$93,$AB
     .byte $C3,$DB,$F3,$0C,$24,$3C,$54,$6C,$84,$9C
     .byte $B4,$CC,$E4,$FC,$15,$2D,$45,$5D,$75,$8D
     .byte $A5,$BD,$D5,$ED,$06,$1E

Mod100Tab:
    .byte 0,56,12,56+12

ShiftedBcdTab
    .byte $00,$01,$02,$03,$04,$08,$09,$0A,$0B,$0C
    .byte $10,$11,$12,$13,$14,$18,$19,$1A,$1B,$1C
    .byte $20,$21,$22,$23,$24,$28,$29,$2A,$2B,$2C
    .byte $30,$31,$32,$33,$34,$38,$39,$3A,$3B,$3C
    .byte $40,$41,$42,$43,$44,$48,$49,$4A,$4B,$4C

HexToDec65535; SUBROUTINE
    sta    hexHigh               ;3  @9
    stx    hexLow                ;3  @12
    tax                          ;2  @14
    lsr                          ;2  @16
    lsr                          ;2  @18   integer divide 1024 (result 0-63)

    cpx    #$A7                  ;2  @20   account for overflow of multiplying 24 from 43,000 ($A7F8) onward,
    adc    #0                    ;2  @22   we can just round it to $A700, and the divide by 1024 is fine...
    tay                          ;2  @24
    lda    Mult24Tab+1,Y         ;4  @28   could use LAX...
    tax                          ;2  @30
    and    #$F8                  ;2  @32
    adc    hexLow                ;3  @35
    txa                          ;2  @37
    and    #$07                  ;2  @39
    adc    hexHigh               ;3  @42
    ror                          ;2  @44
    lsr                          ;2  @46
    tay                          ;2  @48   integer divide 1,000 (result 0-65)

    lsr                          ;2  @50   split the 1,000 and 10,000 digit
    tax                          ;2  @52
    lda    ShiftedBcdTab,X       ;4  @56
    tax                          ;2  @58
    rol                          ;2  @60
    and    #$0F                  ;2  @62
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    sta    decThousands          ;3  @65
    txa                          ;2  @67
    lsr                          ;2  @69
    lsr                          ;2  @71
    lsr                          ;2  @73
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    sta    decTenThousands       ;3  @76


    ;at this point we have a number 0-65 that we have to times by 24,
    ;add to original sum, and Mod 1024 to get the remainder 0-999

    lda    Mult24Tab,Y           ;4  @80   could use LAX...
    tax                          ;2  @82
    and    #$F8                  ;2  @84
    clc                          ;2  @86
    adc    hexLow                ;3  @89
    sta    temp                  ;3  @92
    txa                          ;2  @94
    adc    hexHigh               ;3  @97
Start100s:
    and    #$03                  ;2  @99
    tax                          ;2  @101   0,1,2,3
    cmp    #2                    ;2  @103
    rol                          ;2  @105   0,2,5,7
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    tay                          ;2  @107   Y = Hundreds digit

    lda    temp                  ;3  @110
    adc    Mod100Tab,X           ;4  @114  adding remainder of 256, 512, and 256+512 (all mod 100)
    bcs    .doSub200             ;2³ @116/117

.try200:
    cmp    #200                  ;2  @118
    bcc    .try100               ;2³ @120/121
.doSub200:
    iny                          ;2  @122
    iny                          ;2  @124
    sbc    #200                  ;2  @126
.try100:
    cmp    #100                  ;2  @128
    bcc    HexToDec99            ;2³ @130/131
    iny                          ;2  @132
    sbc    #100                  ;2  @134
HexToDec99; SUBROUTINE
    lsr                          ;2  @136
    tax                          ;2  @138
    lda    ShiftedBcdTab,X       ;4  @142
    tax                          ;2  @144
    rol                          ;2  @146
    and    #$0F                  ;2  @148
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    sta    decOnes               ;3  @151
    txa                          ;2  @153
    lsr                          ;2  @155
    lsr                          ;2  @157
    lsr                          ;2  @159
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    rts                          ;6  @165   A = tens digit



HexToDec255; SUBROUTINE
  IF ASCII_OFFSET
    ldy    #ASCII_OFFSET         ;2  @8
    bne    .try200               ;3  @11    always branch
  ELSE
    ldy    #0                    ;2  @8
    beq    .try200               ;3  @11    always branch
  ENDIF

HexToDec999; SUBROUTINE
    stx    temp                  ;3  @9
    jmp    Start100s             ;3  @12



Here's the slower routine, which saves a lot of bytes:
Code:
;Hex to Decimal (0-65535) conversion
;by Omegamatrix
;
;HexToDec99     ; 37 cycles
;HexToDec255    ; 52-57 cycles
;HexToDec999    ; 72-77 cycles
;HexToDec65535  ; 178-186 cycles

ASCII_OFFSET = $00
temp         = decOnes
hexHigh      = temp2
hexLow       = temp3


Mod100Tab:
    .byte 0,56,12,56+12

ShiftedBcdTab
    .byte $00,$01,$02,$03,$04,$08,$09,$0A,$0B,$0C
    .byte $10,$11,$12,$13,$14,$18,$19,$1A,$1B,$1C
    .byte $20,$21,$22,$23,$24,$28,$29,$2A,$2B,$2C
    .byte $30,$31,$32,$33,$34,$38,$39,$3A,$3B,$3C
    .byte $40,$41,$42,$43,$44,$48,$49,$4A,$4B,$4C

HexToDec65535; SUBROUTINE
    sta    hexHigh               ;3  @9
    stx    hexLow                ;3  @12
    tax                          ;2  @14
    lsr                          ;2  @16
    lsr                          ;2  @18   integer divide 1024 (result 0-63)

    cpx    #$A7                  ;2  @20   account for overflow of multiplying 24 from 43,000 ($A7F8) onward,
    adc    #1                    ;2  @22   we can just round it to $A700, and the divide by 1024 is fine...

    ;at this point we have a number 1-65 that we have to times by 24,
    ;add to original sum, and Mod 1024 to get a remainder 0-999


    sta    temp                  ;3  @25
    asl                          ;2  @27
    adc    temp                  ;3  @30  x3
    tay                          ;2  @32
    lsr                          ;2  @34
    lsr                          ;2  @36
    lsr                          ;2  @38
    lsr                          ;2  @40
    lsr                          ;2  @42
    tax                          ;2  @44
    tya                          ;2  @46
    asl                          ;2  @48
    asl                          ;2  @50
    asl                          ;2  @52
    clc                          ;2  @54
    adc    hexLow                ;3  @57
    sta    hexLow                ;3  @60
    txa                          ;2  @62
    adc    hexHigh               ;3  @65
    sta    hexHigh               ;3  @68
    ror                          ;2  @70
    lsr                          ;2  @72
    tay                          ;2  @74    integer divide 1,000 (result 0-65)

    lsr                          ;2  @76    split the 1,000 and 10,000 digit
    tax                          ;2  @78
    lda    ShiftedBcdTab,X       ;4  @82
    tax                          ;2  @84
    rol                          ;2  @86
    and    #$0F                  ;2  @88
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    sta    decThousands          ;3  @91
    txa                          ;2  @93
    lsr                          ;2  @95
    lsr                          ;2  @97
    lsr                          ;2  @99
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    sta    decTenThousands       ;3  @102

    lda    hexLow                ;3  @105
    cpy    temp                  ;3  @108
    bmi    .doSubtract           ;2³ @110/111
    beq    useZero               ;2³ @112/113
    adc    #23 + 24              ;2  @114
.doSubtract:
    sbc    #23                   ;2  @116
    sta    hexLow                ;3  @119
useZero:
    lda    hexHigh               ;3  @122
    sbc    #0                    ;2  @124

Start100s:
    and    #$03                  ;2  @126
    tax                          ;2  @128   0,1,2,3
    cmp    #2                    ;2  @130
    rol                          ;2  @132   0,2,5,7
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    tay                          ;2  @134   Y = Hundreds digit

    lda    hexLow                ;3  @137
    adc    Mod100Tab,X           ;4  @141    adding remainder of 256, 512, and 256+512 (all mod 100)
    bcs    .doSub200             ;2³ @143/144

.try200:
    cmp    #200                  ;2  @145
    bcc    .try100               ;2³ @147/148
.doSub200:
    iny                          ;2  @149
    iny                          ;2  @151
    sbc    #200                  ;2  @153
.try100:
    cmp    #100                  ;2  @155
    bcc    HexToDec99            ;2³ @157/158
    iny                          ;2  @159
    sbc    #100                  ;2  @161

HexToDec99; SUBROUTINE
    lsr                          ;2  @163
    tax                          ;2  @165
    lda    ShiftedBcdTab,X       ;4  @169
    tax                          ;2  @171
    rol                          ;2  @173
    and    #$0F                  ;2  @175
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    sta    decOnes               ;3  @178
    txa                          ;2  @180
    lsr                          ;2  @182
    lsr                          ;2  @184
    lsr                          ;2  @186
  IF ASCII_OFFSET
    ora    #ASCII_OFFSET
  ENDIF
    rts                          ;6  @192   A = tens digit


HexToDec255; SUBROUTINE
  IF ASCII_OFFSET
    ldy    #ASCII_OFFSET         ;2  @8
    bne    .try200               ;3  @11    always branch
  ELSE
    ldy    #0                    ;2  @8
    beq    .try200               ;3  @11    always branch
  ENDIF

HexToDec999; SUBROUTINE
    stx    hexLow                ;3  @9
    jmp    Start100s             ;3  @12