## 16Bit Binary to Ascii

17 posts / 0 new
Author
Message

Hello everyone.

My problem is simple. I need to get the value from my 16bit counter1 to my LCD.

I am programming in AVR Assembler!

Lets say my counter has reached a count of 5000 that means that:
TCNT1H = 0001 0011
TCNT1L = 1000 1000

The way I was trying to achieve this was by loading each nibble (after conversion) in its own byte like this:
High_Byte_High_Nibble = 5
High_Byte_Low_Nibble = 0
Low_Byte_High_Nibble = 0
Low_Byte_Low_Nibble = 0
This will make it easy to print on to the LCD.

The biggest problem I am having is that I can't wrap my mind around how to use a 16 Bit number to keep my count.

But If anyone can point me in the direction of some example code, I would really appreciate it.

Total votes: 0

In the old days, there used to be books published for 6502, 6800, Z80 and other popular chips. If you can find an old book for \$1, it is worth buying.

They would show you how to convert numbers to strings, multiply, divide, ... and other common tasks.

Nothing has changed. The algorithms are the same. The ASM mnemonics vary slightly.

A useful algorithm is to test and subtract 10000, 1000, 100, ... and output the relevant ascii digit. This involves no multiply or divide (and is what I used to do in 6502 ASM).

Personally, I think you would be better advised to learn C or some other HLL. Writing whole apps in ASM is not worth the effort.

David.

Total votes: 0

Try this...

```
; .device ATmega88
_16bitToAscii:

.def tenThousPlace = r19
.def thousPlace = r20
.def hundPlace = r21
.def tensPlace = r22
.def onesPlace = r23
.def numL = r24
.def numH = r25
; save registers

_10000:
cpi numH, high(10000)
brsh sub10000
cpi numL, low(10000)
brlo _1000
sub10000:
subi numH, high(10000)
sbci numL, low(10000)
inc tenThousPlace
rjmp _10000

_1000:
cpi numH, high(1000)
brsh sub1000
cpi numL, low(1000)
brlo _100
sub1000:
subi numH, high(1000)
sbci numL, low(1000)
inc thousPlace
rjmp _1000

_100:
cpi numL, 100
brlo _10
subi numL, 100
inc hundPlace
rjmp _100

_10:
cpi numL, 10
brlo _1
subi numL, 10
inc tensPlace
rjmp _10

_1:
add onesPlace, numL

SolveOnes:
cpi onesPlace, 10
brlo SolveTens
subi onesPlace, 10
inc tensPlace
rjmp SolveOnes

SolveTens:
cpi tensPlace, 10
brlo SolveHundreds
subi tensPlace, 10
inc hundPlace
rjmp SolveTens

SolveHundreds:
cpi hundPlace, 10
brlo SolveThousands
subi hundPlace, 10
inc thousPlace
rjmp SolveHundreds

SolveThousands:
cpi thousPlace, 10
brlo Add48
subi thousPlace, 10
inc tenThousPlace
rjmp SolveThousands

Add48:
ori onesPlace, 0b00110000
ori tensPlace, 0b00110000
ori hundPlace, 0b00110000
ori thousPlace, 0b00110000
ori tenThousPlace, 0b00110000
```

Or this, it's slightly different but has lots of comments...

```;Micro:	atmega16
;This program gets a value from the adc, converts it to ascii,
;then spits it out the usart
;make sure the internal 1 Mhz oscilator fuse bit isn't set or this
;program won't work, it needs to be run off the stk500's 3.69Mhz clock

;books for avr noobies and greenies
;Programing and Customizing the Avr
;Smiley's book C Programming for Microcontrollers www.smileymicros.com
;AVR An Introductory Course by John Morton
;Print out the datasheets and any appnote that applies to what you want
;to do

;define registers
.def c10000	=	r16
.def c1000	=	r17
.def c100	=	r18
.def c10	=	r19
.def c1		=	r20
.def temp	=	r21
.def upper	=	r22
.def lower	=	r23
.def treg	=	r24

;the line below tells the assembler to include the file m16def.inc
;this included a bunch of definitions so we can type stuff like
;"spl" (stack pointer low) instead of a bunch of numbers
.include "m16def.inc"

;the next two lines tell the assembler the code segment is begining
;and we're starting at address 0x00 (the first one available)
.cseg
.org 0x00

;this is your interupt vector table, if you have interupts enabled
;and one is triggered it tells the microcontroller what to do next
jmp reset			;reset
jmp reset			;exturnal interrupt request0
jmp reset			;exturnal interrupt request1
jmp reset			;timer counter2 compare match
jmp reset			;timer counter2 overflow
jmp reset			;timer counter1 capture event
jmp reset			;timer counter1 compare matchA
jmp reset			;timer counter1 compare matchB
jmp timer1_over		;timer counter1 overflow
jmp timer1_over		;timer counter0 overflow
jmp reset			;spi complete
jmp reset			;usart rx complete
jmp reset			;usart data register empty
jmp reset			;usart tx complete
jmp adc_complete	;adc converstion complete
jmp reset			;eeprom ready
jmp reset			;analog comparator
jmp reset			;two wire serial interface
jmp reset			;external interupt request2
jmp reset			;timer counter0 compare match
jmp reset			;store program memory ready

;these are the things we want to do after a reset
reset:
;STACK, PORTB, PORTA, TIMER1, ADC, GLOBAL INTERRUPTS

;some memory locations can not be accessed dirrectly, you have to
;use one of the general purpose registers to get to them. The ldi (load
;immediate) instructions is used for this.
;STACK
ldi r16, low(ramend)	;initialize stack, sets lower and upper
out spl, r16			;boundries
ldi r16, high(ramend)
out sph, r16

;set port b up for out put by loading all 8 bits of ddrd (data
;direction register b) with "1"
;PORTB
ldi r16, 0xff				;portb all output
out ddrb, r16				;0xff = 0b11111111 = 255

;to make a pin an input make the the bit/bits 0
;PORTA
ldi r16, 0x00
out ddra, r16				;porta all input

;the goal here is to get the timer to overflow about once a second, the
;values below will make the timer overflow every 1.14 seconds if the
;clock speed is 3.69MHz, like it is on the stk500
;if you don't understand the "(1<<cs11)|(1<<cs10)" read the datasheets
;for the meaning of cs11 and cs10, there in the timer/counter1 register
;discription. As for the bitwise OR (|) google "bitwise or"

;TIMER1
ldi r16, (1<<cs11)|(1<<cs10)	;prescale = 64
out tccr1b, r16					;sets the cs11 and cs10 bits

ldi r16, (1<<toie1)				;overflow interrupt enabled
out timsk, r16

;ADC
ldi r16, 0b10001101			;adc enable,
out adcsr, r16				;adc interrupt prescale = 32

;GLABAL INTERRUPTS
sei							;enable global interupts

;USART
ldi r16, 0x02				;portd pin1 output
out ddrd, r16

ldi r16, (1<<txen)			;tx enabled
out ucsrb, r16

ldi r16, 23					;baudrate = 9600
out ubrrl, r16				;the value 23 will only give you
;a baudrate of 9600 if your
;clock speed is 3.69MHz
;there is a table and formula
;to find this vaule in the datasheets
main:
rjmp main					;this loops is only interrupted by an
;interrupt from the adc finishing a
;conversion or timercounter1 overflow
;(see the interupt vector table above)

;This is what we want to do when timercounte1 overflows
timer1_over:
sbi adcsr, adsc				;tell the adc to start a conversion
reti

;This is what we want to do when the adc completes a conversion
adc_complete:
in lower, adcl				;get low byte
in upper, adch				;get high byte

;Here is the part that converts the 16bit value stored in "lower" and
;"upper" to ascii and send it out the usart

;I'm not sure how other people do it but here's I did it. I use a
;register for each place holder (one's, ten's, hundred's, etc). For the
;upperbyte I look at each bit starting with the least significant bit.
;bits if the first bit (bit zero) is set then I add 2 to the hundred's
;place, 5 to the ten's place, and 6 to the one's place (256). If the second
;bit (bit 1) is set I add a 5 to the hundreds place, 1 to the tens place,
;and 2 to the one's place (512) keep inspecting the bits until they're
;all inspected and you've added the apropriate values:
;bit 0 256
;bit 1 512
;bit 3 1024
;bit 4 2048
;bit 5 4096
;bit 6 8192
;bit 7 16384
;

SixteenBitToAsciiOutUSART:

ldi c10000,	0						;set all place holders to 0
ldi	c1000, 	0
ldi c100,	0
ldi c10,	0
ldi c1,		0

UpperByte:
sbrc upper, 0						;check each bit, if bit is set add the
rcall two56							;appropriate numbers to C1-C10000
sbrc upper,	1						;sbrc = skip if bit in register is cleard
rcall five12
sbrc upper,	2
rcall one024
sbrc upper,	3
rcall two048
sbrc upper,	4
rcall four096
sbrc upper,	5
rcall eight192
sbrc upper,	6
rcall one6384
sbrc upper,	7
rcall three2768
rjmp LowerByte

two56:
ldi temp, 6
add c1,	temp
ldi temp, 5
add c10, temp
ldi temp, 2
add c100, temp
ret

five12:
ldi temp, 2
add c1,	temp
ldi temp, 1
add c10, temp
ldi temp, 5
add c100, temp
ret

one024:
ldi temp, 4
add	c1, temp
ldi temp, 2
add c10, temp
ldi temp, 1
add c1000, temp
ret

two048:
ldi temp, 8
add c1, temp
ldi temp, 4
add c10, temp
ldi temp, 2
add c1000, temp
ret

four096:
ldi temp, 6
add c1, temp
ldi temp, 9
add c10, temp
ldi temp, 4
add c1000, temp
ret

eight192:
ldi temp, 2
add c1, temp
ldi temp, 9
add c10, temp
ldi temp, 1
add c100, temp
ldi temp, 8
add c1000, temp
ret

one6384:
ldi temp, 4
add c1, temp
ldi temp, 8
add c10, temp
ldi temp, 3
add c100, temp
ldi temp, 6
add c1000, temp
ldi temp, 1
add c10000, temp
ret

three2768:
ldi temp, 8
add c1, temp
ldi temp, 6
add c10, temp
ldi temp, 7
add c100, temp
ldi temp, 2
add c1000, temp
ldi temp, 3
add c10000, temp
ret

;Here's where we work on the lower byte. The above method works on the
;lower byte but this part of the code was the first thing I programmed
;for the avr that did not involve making an led blink and I wanted
;to try out different branching instructions
;
;start with the hundreds place
;compare the byte by 100, if lower work on the ten's place, if not
;subtract 100 and repeat.
;
;repeat but use 10 and 1
;
;
;
;

LowerByte:
LowerHundredsPlace:
cpi lower, 100
brlo LowerTensPlace
subi lower, 100
inc c100
rjmp LowerHundredsPlace

LowerTensPlace:
cpi lower, 10
brlo LowerOnesPlace
subi lower, 10
inc c10
rjmp LowerTensPlace

LowerOnesPlace:
add c1, lower

;Now the all the place holders have vaules in them but some of them
;will have values above 10, so we have to get the place holder value
;below 10, we use a process simular to the one to convert the lowerbyte
;
;example
;123, 1 in the hundred's place, 2 in the ten's place, 3 in the one's place
;before we go to the next part of the program it may look like this:
;1 in the hundred's place, 1 in the ten's place, 13 in the one's place
SolveOnes:
cpi c1, 10
brlo SolveTens
subi c1, 10
inc c10
rjmp SolveOnes

SolveTens:
cpi c10, 10
brlo SolveHundreds
subi c10, 10
inc c100
rjmp SolveTens

SolveHundreds:
cpi c100, 10
brlo SolveThousands
subi c100, 10
inc c1000
rjmp SolveHundreds

SolveThousands:
cpi c1000, 10
brlo Add48
subi c1000, 10
inc c10000
rjmp SolveThousands

;convert the value to ascii by adding 48 or ORI 0b00110000
;I picked the ori part up at an avr tutorial website, google
;"avr tutorial" it will be one of the first few sites, it's in
;German and English
Add48:
ori c1, 0b00110000
ori c10, 0b00110000
ori c100, 0b00110000
ori c1000, 0b00110000
ori c10000, 0b00110000

;this part sends the values out the usart and adds a ";" as a delimiter
Tx10000:
sbis UCSRA, UDRE
rjmp Tx10000
out UDR, c10000
Tx1000:
sbis UCSRA, UDRE
rjmp Tx1000
out UDR, c1000
Tx100:
sbis UCSRA, UDRE
rjmp Tx100
out UDR, c100

Tx10:
sbis UCSRA, UDRE
rjmp Tx10
out UDR, c10

Tx1:
sbis UCSRA, UDRE
rjmp TX1
out UDR, c1

Tx59:
sbis ucsra, udre	;delimiter
rjmp Tx59
ldi temp, 59
out udr, temp
reti
```

[/code]

Total votes: 0

Buttercup1101 wrote:

The way I was trying to achieve this was by loading each nibble (after conversion) in its own byte like this:
High_Byte_High_Nibble = 5
High_Byte_Low_Nibble = 0
Low_Byte_High_Nibble = 0
Low_Byte_Low_Nibble = 0
This will make it easy to print on to the LCD.
Wrong idea. You have to convert the integer to ASCIIZ string instead and then just send this string to LCD.

Warning: Grumpy Old Chuff. Reading this post may severely damage your mental health.

Total votes: 0

david.prentice wrote:
A useful algorithm is to test and subtract 10000, 1000, 100, ... and output the relevant ascii digit. This involves no multiply or divide (and is what I used to do in 6502 ASM).

I agree this a good approach for devices without hardware mul and div (and AVR has no hardware div)

Total votes: 0

david.prentice wrote:
Nothing has changed. The algorithms are the same. The ASM mnemonics vary slightly.
Oh really? How about this then?

```;---------------------------------------------------------------------------------------------------
; Converts an unsigned integer N in W0 to a 5-byte ASCIIZ numerical string [W4]
; using a reciprocal multiplication and fractional conversion techniques
;
; 25 clocks including RETURN (0.625 uS @40 MIPS dsPIC), 16 program words
; Registers trashed: NONE
;
; (C) 2006 MBedder
;
uitoa:
magic10 = 429497                        ; =~ 2^32/10000
push.s                          ; Save W0..W3, SRL to shadow "stack"

mov     #magic10 >> 16,w1
mul.uu  w1,w0,w2                ; W3:W2 = partial product MSWs
mov     #magic10 & 0xFFFF,w1
mul.uu  w1,w0,w0                ; W1:W0 = partial product LSWs

add     w1,w2,w0                ; W0 = fract(N%10000)
mov     #'0',w2                 ; W2 = ASCII bit mask
addc.b  w3,w2,[w4++]            ; W3 = N/10000, store an ASCII MS character

inc     w0,w0                   ; Correct a remainder to use 16-bit ops

do      #3,1f                   ; Repeat the block below 4 times:

mul.uu  w0,#10,w0       ; W1 = next ASCII digit (0..9 range), w0 = fractional remainder
1:      ior.b   w1,w2,[w4++]    ; Store a next ASCII character

clr.b   [w4]                    ; Place a zero string terminator

sub     w4,#5,w4                ; Restore W4
pop.s                           ; Restore W0..W3, SRL

return
;---------------------------------------------------------------------------------------------------```

The (X)Mega equivalent would be ~5..8 times slower and ~3..4 times longer due to its poor architecture, lousy instruction set and primitive addressing modes, but anyway this algorithm is at least twice as fast comparing to the degrees of ten subtraction, or at least 5x faster than a popular Gorner algorithm (shift-correction).

Warning: Grumpy Old Chuff. Reading this post may severely damage your mental health.

Last Edited: Thu. May 3, 2012 - 06:22 PM
Total votes: 0

The basic algorithm is:

```uint16_t decades[] = { 1, 10, 100, 1000, 10000 };

void printnum(uint16_t value, uint8_t width)
{
uint8_t c;
while (width--) {
decade = decades[width];
c = '0';
while (value >= decade) {
value -= decade;
c++;
}
output(c);
}
}
```

You should be able to code that in ASM very easily.
The output() subroutine either prints or appends a character to a string. (you need a NUL to terminate)

You can access the decades[] array with the X, Y or Z register.
The comparison can be a 'test subtraction' and inspect borrow flag.

Note that this algorithm is no more than 20 lines of ASM. Heaven knows how anyone can bloat it to pages is a mystery!

David.

Total votes: 0

~15 lines, but slow enough (~300 clocks):

```;-------------------------------------------------------------
; Converts unsigned integer value of r17:r16 to ASCII string x[5]
itoa_short:
ldi	zl,low(dectab*2)
ldi	zh,high(dectab*2)

itoa_lext:
ldi	r18,'0'-1

lpm	r2,z+
lpm	r3,z+

itoa_lint:
inc	r18

sub	r16,r2
sbc	r17,r3
brsh	itoa_lint

add	r16,r2
adc	r17,r3

st	x+,r18
cpi	zl,low(dectab*2)+1
brne	itoa_lext

ret

dectab:	.dw	10000,1000,100,10,1
;-------------------------------------------------------------```

Warning: Grumpy Old Chuff. Reading this post may severely damage your mental health.

Total votes: 0

@MBedder,

I like your MIPS algorithm.

The AVR algorithm may not be as fast, but it is certainly neat and tidy.
Almost as elegant as a 6502 or 68000 solution!

Which just goes to show that AVR assembly is not too unpleasant.

David.

Total votes: 0

This is dsPIC, not MIPS. I just mentioned MIPS there as Million Instructions Per Second :lol:

Warning: Grumpy Old Chuff. Reading this post may severely damage your mental health.

Total votes: 0

Dave was just being diplomatic and didn't want to mention The Other Microcontroller company's cpu.

Imagecraft compiler user

Total votes: 0

WoW! Thanx for all the replies... Really helped me to get my code right!!

Much appreciated!!

Total votes: 0

I'd like to see the Russkie's reciprocal algorithm in c is you please.

Imagecraft compiler user

Total votes: 0

It's almost impossible due to many register oriented tricks and handling the partial multiplication results. You can try reconstructing the algorithm in C using unions and explicit register variables, but I doubt this would be very efficient.

Warning: Grumpy Old Chuff. Reading this post may severely damage your mental health.

Total votes: 0

If you need speed I have posted a fast (but ugly) ASM program that allways solve it in less than 70 clk.
and it does it with div by mul with 1/x , in different ways
find first digit
div with 100 (result = 2 digit reminder the other two).
div result with 10 (result = 2. digit reminder 3.)
div reminder with 10 (result=4 and reminder 5.)

Total votes: 0

Quote:

Nothing has changed.

Quote:

If you need speed I have posted a fast (but ugly) ASM program that allways solve it in less than 70 clk.

I thought that we explored every possible :twisted: AVR approach years ago.
https://www.avrfreaks.net/index.p...

Besides "fastest" and "smallest", consider also "most useful". As I opined in the linked thread and elsewhere, right-justification and leading-zero suppression and insertion of implied decimal point is used with display work.

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

Total votes: 0

MBedder wrote:

```;-------------------------------------------------------------
; Converts unsigned integer value of r17:r16 to ASCII string x[5]
itoa_short:
ldi	zl,low(dectab*2)
ldi	zh,high(dectab*2)

itoa_lext:
ldi	r18,'0'-1

lpm	r2,z+
lpm	r3,z+

itoa_lint:
inc	r18

sub	r16,r2
sbc	r17,r3
brsh	itoa_lint

add	r16,r2
adc	r17,r3

st	x+,r18

cpi	zl,low(dectab*2)+1        ;this points to a location only( value not loaded yet!)

brne	itoa_lext                 ;loop forever ?

ret

dectab:	.dw	10000,1000,100,10,1
;-------------------------------------------------------------```

; Converts unsigned integer value of r17:r16 to ASCII string x[5]

itoa_short:

ldi    XL,10                    ;DECODED OUTPUT BUFFER R10-----(R10+x)

ldi    zl,low(dectab*2)        ; x0^x power compare pointer ONLY
ldi    zh,high(dectab*2)

itoa_lext:
ldi    r18,'0'-1                ;(ascii 0) -1

lpm    r2,z+                    ;load 10^x word,point to next
lpm    r3,z+

itoa_lint:
inc    r18                        ;start with '0' ascii

sub    r16,r2                    ; (## - 10^x)
sbc    r17,r3
brsh    itoa_lint

add    r16,r2                    ; if neg, reconstruct
adc    r17,r3

st    x+,r18                    ; save 1/10^x count,point to next location 2 save into

lpm                                  ;read last ZX pointed at from 10^x table in (r0)
tst        r0                        ; LAST WORD YET?
=0x00

;BUGGG  ?????????    ;cpi    zl,low(dectab*2)+1        ;THIS IS A POINTER ONLY!!

brne    itoa_lext        ;Z SET?

ret

dectab:    .dw    10,1,0            ; SHORT FOR TESTING

;.dw    10000,1000,100,10,1,0
;.dw          1000,100,10,1,0
;.dw               100,10,1,0
;.dw                   10,1,0
;.dw    0x2710,0x03e8,0x0064,0x000a,0x0001,0x0000
;-------------------------------------------------------------

Just browsing around ,I found this old useful routine in my notes.

I do not know if this was pointed at before.

tested with this old AVR Studio        4.18.692  and avr-asm2

Was this a test bug or a compiler dependency issue?