Scientific & Numeric Applications


Encapsulating Math Coprocessor BCD Arithmetic

Mark R. Parker


Mark Parker, who holds an MS in Mathematics, teaches C, C++ and Fortran programming (as well as mathematics) at Shoreline Community College in Seattle, WA. He is co-author (with Leo Scanlon) of Dos Subroutines for C and Assembler (TAB/McGraw-Hill). His interests include optimizing mathematical computations and OOP in C++. He can be reached on CompuServe at 76223,3637 and on the Internet at mparker@ctc.edu.

Introduction

The benefits of encapsulation, as provided by C++ classes, are generally well known. Encapsulation hides messy implementation details from users, enabling designers to protect class data, and giving users relief from excessive complexity. Encapsulation also enables designers to change a class's internal implementation without requiring a change in client code. This last benefit can be especially significant when that implementation interacts with hardware. Hardware is platform-dependent, and encapsulation can hide that platform dependence from the user.

In this article I present a class that partially encapsulates a hardware device — the math coprocessor — to perform binary-coded decimal arithmetic. (For more information about BCD arithmetic, please refer to the sidebar.) Applications built from the BCD class can perform input/output and add or subtract numbers of up to 18 decimal digits (in this implementation) using exact arithmetic. By itself, this may not seem terribly astounding; after all, you could duplicate all of BCD's functionality using the C/C++ long double type, and much more simply as well. But one purpose of this article is to show how to control a peripheral device — not directly supported by a high-level language — in a completely transparent manner.

The BCD class is designed primarily for ease of use, followed closely by portability. To achieve these goals, the class design consists of three layers. The outermost layer is the layer seen by client programs. In this layer, binary-coded decimal numbers exist as an abstraction, not much different from an int or a char or other primitive type. The design defines arithmetic and input/output operators for this new type. The middle layer implements many of the operations defined in the outer layer. The middle layer must make some assumptions about the system it will be running on, so that it can map blocks of memory into and out of the math coprocessor. At the core is the innermost layer, which is responsible for communicating directly with the math coprocessor. This layer is completely system-dependent, and would have to be completely rewritten to use a different math coprocessor. The assembly code included here is for the Intel 80x87 family of math coprocessors.

BCD Class Services

The functions declared in the header file BCD.H (Listing 1) form the public interface to the BCD class. The interface provides for a constructor, a destructor, overloaded operators for assignment, addition, subtraction, and negation, and overloaded stream insertion (output) and extraction (input) operators. The member functions BCDPut and BCDGet provide alternatives to the stream operators for class users who prefer a more C-like notation.

The class's only data member is a pointer to an unsigned char. Since no details about the data are available, class users should not be tempted to modify an object's data through means other than those provided by the class.

The BCD class users need little more information than that already presented. The following code fragment shows how the class can be used:

BCD Val1("123456789012345"), Val2("-1122334455667788"), Val3, Val4;
Val3 = Val1 + Val2;
cout << Val3;
cout << "Enter a new value... ";
cin >> Val3;
Val4 = Val1 + Val2 - (-Val3),
cout << Val4;

BCD Class Implementation

The file BCD.CPP (Listing 2) contains the member function definitions. Users need never see this file. In most circumstances they could use this class without knowledge of its internal workings.

Data

Each BCD object contains a pointer to a dynamically allocated array of unsigned chars. In this implementation, the array has ten elements, or 80 bits — just the right size for the math coprocessor registers. (See Figure 1 for a depiction of one of the Intel 80x87 math coprocessor's registers.)

Constructor

The class constructor, BCD(char*), is invoked whenever a BCD object comes into scope. Because the constructor is declared with a default argument, the new object will be initialized either with 0 (the default) or with the string of decimal digits. In either case, the constructor dynamically allocates a block of memory, and immediately sets each byte to zero. If invoked with a string of digits — i.e., a number — the constructor will then store a representation of that number in the array. This process is a little more complicated than it may seem at first. The constructor first does some rudimentary parsing to determine whether the string is negative or positive, and how many pairs of digits it contains. The constructor then copies the digits, in pairs, to their proper locations in memory. The digit pairs must be stored in reverse order. For example, a BCD object initialized with the string "123456789" will store these digits in memory as 89 67 45 23 01 00 00 00 00 00. The least significant pair is stored lowest in memory. This is the format required by the math coprocessor prior to its loading a BCD value from memory.

Destructor

The destructor behaves as you would expect, returning the dynamically allocated data array to memory when a BCD object goes out of scope.

Overloaded Operators

The BCD class contains member function operators for assignment, addition, subtraction, and negation. Clients of the class can assign one object to another, add or subtract two objects, or change the sign of an object, using intuitive C/C++ notation.

Input/Output

The stream operators >> and << are overloaded to input and ouput BCD objects on C++ iostreams. Typical usage is as follows:

cin >> Num;
cout << Num;
These stream operators mediate between the BCD class and either of the two iostreams classes, istream or ostream. As mediators, I've made these stream operators friends, not member functions, of the BCD class. Earlier in the class's development I created the functions BCDGet and BCDPut to perform input and output. Now stream output (operator<<) is implemented as a call to BCDPut and input (operator>>) as a call to BCDGet.

The Nitty-Gritty

The lowest level of class design consists of the assembly-coded functions BCDAdd, BCDSub, and BCDNeg. Listing 3 shows BCDAdd; BCBSub and BCDNeg are on this month's code disk. Both BCDAdd and BCDSub take three arguments: two BCD-type arguments, and a pointer to a BCD object that will hold the result. Since a BCD object contains only a pointer (assumed NEAR, or 16-bit, in my small memory model program), each object requires only a word of memory on the stack. The address of the destination object takes up a third word on the stack. BCDNeg works similarly, but requires only two stack words instead of three — the first holds the BCD object to be negated; the second holds the address of the destination object.

The assembly-coded functions introduce two instructions that may be unfamiliar to non-hardware types: FBLD and FBSTP. These are coprocessor instructions (all begin with F) that respectively load and store a packed BCD number. FBLD will load a ten-byte quantity onto the top of the coprocessor stack. FBSTP will store the contents of the top-of-stack register into memory and then pop the stack. These instructions carry out all the data transfer operations.

Possible Extensions

As it stands, the BCD class provides some useful functionality, but could use a few extensions. Perhaps its most glaring deficiency is this class's cavalier assumption that a numeric coprocessor is present. A reasonably cautious program would interrogate the hardware at run time to be sure. The class could be extended with a static data member whose purpose in life is recording the presence or absence of a coprocessor. The member functions that depend on a coprocessor could then check the status of this flag before they attempt to produce a result.

The BCD class also does not guard against overflows. If the sum of two 18-digit numbers won't fit into 18 digits, operator+ adds them anyway, returning no indication of an error. One way to correct this would be to extend the class to check for an invalid exception after execution of an FBSTP instruction. Since the BCD class does not check for overflows (or underflows), I did not think it wise to implement multiplication or division — two operations very likely to produce just such errors. Were overflow/underflow checking implemented, these two operations would make worthwhile extensions to the class.

Conclusion

C++ classes can make useful wrappers around hardware devices that only "speak" assembly language. C++-style encapsulation allows programmers having little knowledge of a device to still create useful programs. This layering also isolates the most system-dependent details in a small region, simplifying the task of porting the software to a different environment.

Bibliography

Intel Corporation, Microprocessors, Volume I. Intel, 1992.

Liu, Yu-cheng. The M68000 Microprocessor Family, Fundamentals of Assembly Language Programming and Interface Design. Prentice Hall, 1991.

Sidebar: Binary-coded Decimal Numbers