From : Julian Schmidt - BCD Tutor
Here's a bit of tutorial information for Assembly Language Programmers:
If anyone notices any errors, omissions, or flat-out lies, please
correct me... This text is in the public domain.
Q: What is the BCD number format and why should I care about it?
A: BCD stands for Binary Coded Decimal. It's a number format that
assembly programmers occasionally need to deal with since some BIOS
functions return values as BCD. BCD uses 4-bit binary representation
for the decimal numbers 0 to 9. Since each BCD numeral is encoded with
4 bits, it is possible to store two BCD numbers in a byte with the most
significant digit stored in the high-order 4 bits. This format is known
as packed BCD. Packed BCD is also referred to as 'packed decimal' or
'decimal integer'. A format known as unpacked BCD uses the low-order
four bits of a byte to store a single BCD digit and wastes the
high-order nybble. Unpacked BCD is also called 'unpacked decimal' or
'ASCII integer'.
A comparison of Packed versus Unpacked BCD formats:
Unpacked BCD Format Packed BCD Format
----------------- -------------------
memory memory memory memory
contents address contents address
0000 0101 X+3
0000 0001 X+2
0000 0101 X+1 0101 0001 X+1
0000 0000 X 0101 0000 X
Both formats encode the decimal number 5150d, but the unpacked BCD
requires twice the storage space as does the packed BCD.
Arithmetic on unpacked BCD numbers can be performed by the 80XXX
microprocessors. Unfortunately, the arithmetic instructions available
for unsigned binary integers will often result in errors when used with
BCD numbers. Consider the following calculation:
0000 1001 + 0000 0011 = 0000 1100 9 + 3 = 12
This result is, of course, correct if we interpret it as a binary
integer but 1100 is not a valid BCD digit. Furthermore, consider this
addition:
0000 1001 + 0000 1000 = 0001 0001 9 + 8 = 17
The low-order 4 bits contain a valid BCD digit, but the result is
incorrect (in terms of BCD representation).
Fortunately, the 80XXX microprocessor has several features that enable
us to correct for these errors. Whenever there is a carry out of, or
borrow into, the low-order 4 bits of a byte, the auxiliary carry flag
(AF) is set to 1. If no carry out, or borrow into, occurs, AF is set to
zero. Therefore, the 80XXX utilizes a method of 'post facto'
adjustement to correct the results of BCD arithmetic. The instructions
AAA and DAA are provided to adjust the results of BCD addition. They
are used immediately after the ADD instruction. AAA operates on the
result, in register AL, an addition of two unpacked BCD numbers. DAA
operates on the result, in AL, of an addition of two packed BCD
numbers. In the interest of brevity, I will only explain AAA here.
;Add two BCD numbers, 9 + 3:
Number1 db 00001001b ;A byte data item containing unpacked BCD.
Number2 db 00000011b ;"
mov al,Number1 ;Put the number into register AL.
add al,Number2 ;Add two numbers, result in AL.
;Now, AL = 00001100b which is not a valid BCD number.
aaa ;Issue the Ascii Adjust for Addition instruction.
;Now, AX = 00000001 00000010b which is valid unpacked BCD.
AAA determines if the contents of AL are greater than 9 (which indicates
an invalid unpacked BCD number), or if the AF has been set, and if so,
adds an adjustment factor (00000110b) to the low-order four bits of AL,
clears the high-order four bits of AL, a 1 is added to AH, and the CF
and AF flags are set to 1.
Multiple precision BCD arithmetic can also be performed by a series of
ADDs and AAAs on each digit of the two respective numbers.
Subtraction of packed or unpacked BCD numbers is handled with the AAS
and DAS instructions. I won't go into detail here.
Multiplication and division of packed BCD numbers is not supported by
the 80XXX. If it is necessary to perform these operations, the numbers
must converted to unpacked BCD. I won't discuss multiplication or
division here, since they are considerably more involved than simple
addition and subtraction, except to say that the AAM (ASCII Adjust for
Multiply) and AAD (ASCII Adjust for Divide) are provided by the 80XXX to
adjust MUL and DIV operations for unpacked BCD numbers.
If extensive BCD arithmetic needs to be performed, my recommendation is
to convert the BCD numbers to integers, and then use the 80XXX to
perform the math. The result can then be converted back into BCD.
If an 80X87 is present, the instruction FBLD (Load BCD) will load a BCD
number that is stored in the 'packed decimal format'. The packed
decimal format is an 80-bit ten-word (you never knew that ten-word data
items were useful, did you?). Bit 79 is a sign bit which is set if the
number is negative and clear if the number is positive. 18 digits are
stored as 4-bit BCD numbers with the least-significant digit starting at
bit zero of the ten-word. Bits 72 through 78 are unused:
PACKED DECIMAL s --- D17 D16 .... D2 D1 digits
79 72 0 bit position
The 80X87 can be used to perform math on these packed decimal BCD
numbers. If desired, fractional numbers can be simulated by establishing
a decimal point position and multiplying by a power of ten after the BCD
is loaded into the 80X87. The instruction FBSTP will store the result
from the 80X87 back into memory.
The next part contains a procedure that will convert an ASCII string
into a packed decimal BCD number:
Comment*
ASKY2BCD.ASM - ASCII to Binary Coded Decimal
Written by Julian H. Schmidt, public domain code
This routine will convert an ASCII string into a number stored in
the packed decimal BCD numeric format. Creating packed BCD is more
involved than creating unpacked BCD, so this will by my example.
Acceptable characters in the ASCII string are the numbers (0 to 9).
Any other characters will cause errors. This procedure does NO error
checking! That is, it will not handle negative signs or radix points,
or 'garbage' ASCII. This routine will convert up to 18 digits.
Run this code from within DEBUG, and then (d)ump the Data Segment to
verify that the Sample data (defined below, in the Data Segment) was
converted and written to the Scratch buffer.
On Entry:
DS:DX Pointer to buffer of ASCII data that represents
the 'number' to be converted to packed decimal.
CX Number of characters in the string.
On Exit:
The Scratch data buffer will contain the packed decimal BCD.
End of Comment*
_data segment 'DATA' para
Sample db '135792468098765432' ;18-character ASCII 'number'
Scratch dt ? ;A ten-byte data item.
_data ends
_code segment 'CODE' byte
assume cs:_code, ds:_data
Driver proc
mov ax,_data ;This is the program entry point.
mov ds,ax ;Make the Data Segment accessible.
mov dx,offset Sample ;Point DX to data to convert.
mov cx,18 ;CX is number of chars to convert.
call Asky2bcd ;Do the conversion.
mov ax,4c00h ;Request function 4Ch (DOS).
int 21h ;Dos's Terminate function.
driver endp
Asky2bcd proc
push es ;Save some registers.
;------------------------------------------------
; Initialize the scratch data buffer to zero |
;------------------------------------------------
mov di,offset Scratch ;DI points to the scratch buffer.
mov ax,ds
mov es,ax ;Now, ES = DS.
push cx ;We can't destroy the contents of CX.
mov cx,5 ;Initialize 5 words.
xor ax,ax ;Word to fill buffer with = 00h.
rep stosw ;Store the NULLs into Scratch buffer.
pop cx
;----------------------------
; Scan the ASCII digits |
;----------------------------
;The Scratch buffer was initialized to all zeros.
;18 decimal places fit into 9 bytes in packed BCD.
;CX holds the number of digits left to process.
;DX points to the current position in the ASCII buffer.
;The Scratch buffer is 9 bytes long, access it with BX.
add dx,cx
dec dx
;This addition moves the pointer DX to the end of the ASCII
;string. I will scan from the end to the beginning. I do
;it this way because of the method by which I construct the
;packed BCD number.
push ax ;AL will be used as a flag.
push dx ;DX will also be used as a flag.
mov bx,dx ;BX points to current ASCII character.
xor ax,ax ;AL=0 means low nybble, 1 = high.
mov dx,9 ;DX holds place in Scratch buffer.
;When a high nybble is processed, DX will be decremented
;to point to the 'next' byte.
cmp cx,18 ;Is CX > 18 digits?
jbe _ab0 ;If not, continue normally.
mov cx,18 ;Truncate to 18 digits.
_ab0:
mov ah,byte ptr[bx] ;Copy the ASCII to ah.
;---------------
push cx ;Save the count temporarily.
mov cx,4 ;Shift left by 4 places.
shl ah,cl
pop cx ;Restore the count.
;---------------
;The nybble is now in the high four bits of AH.
cmp al,0 ;Is the nybble flag zero? (low)?
jne _ab1 ;If not, jump.
;The low nybble will now be taken care of.
push bx ;I need a base register.
mov bx,offset Scratch
add bx,dx ;Move to current byte.
;---------------
push cx
mov cx,4
shr ah,cl ;Shift right - creates BCD.
pop cx ;Now, value is in LOW bits.
;---------------
mov [bx],ah ;Move the value into scratch buffer.
pop bx ;All done!
inc al ;Set the nybble flag.
jmp short _ab2 ;Jump to the loop instruction(s).
_ab1: ;High nybble taken care of here.
push bx
mov bx,offset Scratch
add bx,dx
or [bx],ah ;Move value into HIGH bits.
pop bx
xor al,al ;Clear the nybble flag.
dec dx ;Move to previous byte in Scratch.
_ab2:
dec bx ;Go to next character.
loop _ab0 ;Main scan loop.
pop dx ;Registers pushed before the loop.
pop ax
pop es
ret ;Return to the driver program.
Asky2bcd endp
_code Ends
_stack Segment stack 'STACK'
db 100 dup(0)
_stack Ends
END Driver