16Bit Binary to Ascii

Go To Last Post
17 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

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.

  • 1
  • 2
  • 3
  • 4
  • 5
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.

  • 1
  • 2
  • 3
  • 4
  • 5
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]

  • 1
  • 2
  • 3
  • 4
  • 5
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.

  • 1
  • 2
  • 3
  • 4
  • 5
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)

  • 1
  • 2
  • 3
  • 4
  • 5
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
  • 1
  • 2
  • 3
  • 4
  • 5
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.

  • 1
  • 2
  • 3
  • 4
  • 5
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.

  • 1
  • 2
  • 3
  • 4
  • 5
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.

  • 1
  • 2
  • 3
  • 4
  • 5
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.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

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

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

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

Much appreciated!!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

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

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
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.

  • 1
  • 2
  • 3
  • 4
  • 5
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.)

  • 1
  • 2
  • 3
  • 4
  • 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.

  • 1
  • 2
  • 3
  • 4
  • 5
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?