Файл: Introduction to Microcontrollers. Architecture, Programming, and Interfacing of the Motorola 68HC12 (G.J. Lipovski, 1999).pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 14.06.2025

Просмотров: 1422

Скачиваний: 0

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

204

Chapter 7 Arithmetic Operations

24 * 1.00 . . . 0

+22 * 1.00 . . . 0

we first unnormalize the number with the smaller exponent and then add as shown,

24 * 1.000 .. . 0

+24 * 0.010 . . . 0

24 * 1.010 .. . 0

(For this example and all those that follow, we give the value of the exponent in decimal and the 24-bit magnitude of the significand in binary.) Sometimes, as in adding,

24

*

1.00

. . . 0

+ 24

*

1.00

. . . 0

24

*10.00

. . . 0

the sum will have to be renormalized before it is used elsewhere. In this example

25 * 1.00 . . . 0

is the renormalization step. Notice that the unnormalization process consists of repeatedly shifting the magnitude of the significand right one bit and incrementing the exponent until the two exponents are equal. The renormalization process after addition or subtraction may also require several steps of shifting the magnitude of the significand left and decrementing the exponent. For example,

24 * 1.0010 .. . 0

-24 * 1.0000 . . .0

24 * 0.0010 .. . 0

requires three left shifts of the significand magnitude and three decrements of the exponent to get the normalized result:

21 * 1.00 . . . 0

With multiplication, the exponents are added and the significands are multiplied to get the product. For normalized numbers, the product of the significands is always less than 4, so that one renormalization step may be required. The step in this case consists of shifting the magnitude of the significand right one bit and incrementing the exponent. With division, the significands are divided and the exponents are subtracted. With normalized numbers, the quotient may require one renormalization step of shifting the magnitude of the significand left one bit and decrementing the exponent. This step is required only when the magnitude of the divisor significand is larger than the magnitude of the dividend significand. With multiplication or division it must be remembered also that the exponents are biased by 127 so that the sum or difference of the exponents must be rebiased to get the proper biased representation of the resulting exponent.

7,5 Floating-Point Arithmetic and Conversion

205

In all of the preceding examples, the calculations were exact in the sense that the operation between two normalized floating-point numbers yielded a normalized floatingpoint number. This will not always be the case, as we can get overflow, underflow, or a result that requires some type of rounding to get a normalized approximation to the result. For example, multiplying

256 * i.oo . . . 0

*210Q * 1.00 . . . 0

2*56 * 1.00 . . . 0

yields a number that is too large to be represented in the 32-bit floating-point format. This is an example of overflow, a condition analogous to that encountered with integer arithmetic. Unlike integer arithmetic, however, underflow can occur, that is, we can get a result that is too small to be represented as a normalized floating-point number. For example,

2-126 * 1.0010 . . . 0

-2-l26 * 1.0000 . . . 0 2-126 * 0.0010 . . . 0

yields a result that is too small to be represented as a normalized floating-point number with the 32-bit format.

The third situation is encountered when we obtain a result that is within the normalized floating-point range but is not exactly equal to one of the numbers (14). Before this result can be used further, it will have to be approximated by a normalized floating-point number. Consider the addition of the following two numbers.

22

*

1.00

. . . 00

+ 20 *

1.00

. . . 01

22

* 1.01

. . . 00(01)

(in parenthesis: least significant bits of the significand)

The exact result is expressed with 25 bits in the fractional part of the significand so that we have to decide which of the possible normalized floating-point numbers will be chosen to approximate the result. Rounding toward plus infinity always takes the approximate result to be the next larger normalized number to the exact result, while rounding toward minus infinity always takes the next smaller normalized number to approximate the exact result. Truncation just throws away all the bits in the exact result beyond those used in the normalized significand. Truncation rounds toward plus infinity for negative results and rounds toward minus infinity for positive results. For this reason, truncation is also called rounding toward zero. For most applications, however, picking the closest normalized floating-point number to the actual result is preferred. This is called rounding to nearest. In the case of a tie, the normalized floating-point number with the least significant bit of 0 is taken to be the approximate result. Rounding to nearest is the default type of rounding for the IEEE floating-point standard. With rounding to nearest, the magnitude of the error in the approximate result is less than or equal to the magnitude of the exact result times 2~24.


206

Chapter 7 Arithmetic Operations

One could also handle underflows in the same way that one handles rounding.For example, the result of the subtraction

2-126 * i . oilO . . . 0

-2-126 * i.oooo . . . Q 2-126 * 0.0110 . . . 0

could be put equal to 0, and the result of the subtraction

2-126 *

i.ioiO

. . . 0

2-126

* i.oooo

. . .

Q

2-126

*

0.1010

. . .

0

could be put equal to 2~126 * 1.0000. More frequently,all underflow results are put equal to 0 regardless of the rounding method used for the other numbers. This is termed flushing to zero. The use of denormalized floating-point numbers appears natural here, as it allows for a gradual underflow as opposed to, say, flushing to zero. To see the advantage of using denormalized floating-point numbers, consider the computation of the expression (Y — X) + X. I f Y - X underflows, X will always be the computed result if flushing to zero is used. On the other hand, the computed result will always be Y if denormalized floating-point numbers are used. The references mentioned at the end of the chapter contain further discussions on the merits of using denormalized floating point numbers. Implementing all of the arithmetic functions with normalized and denormalized floating-point numbers requires additional care, particularly with multiplication and division, to ensure that the computed result is the closest represented number, normalized or denormalized, to the exact result. It should be mentioned that the IEEE standard requires that a warning be given to the user when a denormalized result occurs. The

motivation for this is that one is losing precision with denormalized

floating-point

numbers. For example, if during the calculation of the expression

(Y

X)

*

Z. If Y

-X underflows, the precision of the result may be doubtful even if

(Y

X)

*

Zisa

normalized floating-point number. Flushing to zero would, of course, always produce zero for this expression when (Y — X) underflows.

The process of rounding to nearest, hereafter just called rounding, is straightforward after multiplication. However, it is not so apparent what to do after addition, subtraction, or division. We consider addition/subtraction. Suppose, then, that we add the two numbers

20 * 1.0000 . . . 0

+2-23 * 1.1110 . . . 0

After unnormalizing the second number,we have

20 * 1.0000 .. . 00

+2° * 0.0000 . . . Qlflll) 20 * 1.0000 . . . 01(111)


7.5 Floating-Point Arithmetic and Conversion

207

(The enclosed bits are the bits beyond the 23 fractional bits of the significand.) The result, when rounded, yields 2° * 1.0 . . . 010. Byexamining a number ofcases, one can see that only three bits need to be kept in the unnormalization process,namely,

where g is the guard bit, r is the round bit, and s is the sticky bit. When a bit b is shifted out of the significand in the unnormalizationprocess.

Notice that if s ever becomes equal to 1 in the unnormalization process, it stays equal to 1 thereafter or "sticks" to 1. With these three bits, rounding is accomplished by incrementing the result by 1 if

or

If adding the significands or rounding causes an overflow in the significand bits (only one of these can occur), a renormalization step is required. For example,

2° * 1.1111 . . . 1

+2-23 * Lino . . . 0

becomes, after rounding, 2° * 10.0 . . . 0 Renormalization yields 2l * 1.0

. . . 0, which is thecorrect rounded result, and no further rounding is necessary. Actually, it is just as easy to save one byte for rounding as it is to save three bits,

so that one can use six rounding bits instead of one, as follows.

The appropriate generalization of (15) can be pictured as

while (16) is exactly the same as before with r replaced by 15 ... TO

216

Chapter 7 Arithmetic Operations

Do You Know These Terms?

See the end of chapter 1 for instructions.

long data

floating-point

rounding toward

fuzzy inference

float data

numbers

zero

kernel

state

bias

rounding to

knowledge base

biasing

normalized

nearest

antecedent

rn-digit base-b

number

flushing to zero

fuzzy AND

representation

denormalized

rounding

consequent

Polish notation

number

guard bit

fuzzy OR

parsing tree

unnormalization

round bit

fuzzy negate

fixed-point

renormalized

sticky bit

singleton

representation

overflow

decimal floating-

significand

underflow

point number

exponential part

rounding toward

linguistic variable

floating-point

plus infinity

value

hidden bit

rounding toward

membership

single precision

minus infinity

function

floating-point

truncation

rule



8

Programming in C and C++

This chapter gives background material for Chapter 9, which shows how C or C++ statements are encoded in assembly language. Together, they illustrate what a programmer is doing when he or she writes high-level language programs. However, if you have already covered this material, it can be skipped.

The first section provides terminology and understanding of where to use high-level language compilers and interpreters. We then begin with a description of C, illustrating first operators and statements and then conditional and loop expressions. We give an example of a program that uses many of the features we need in the next chapter, and then discuss C++ and object-oriented programming.

8.1 Compilers and Interpreters

We first discuss the difference between an assembler and a compiler. A compiler is a program that converts a sequence of (ASCII) characters that are written in a high-level language into machine code or into assembly language that can be converted into machine code. A high-level language is different from an assembly language in two ways. First, a line of a high-level language statement will often generate five to a few tens of machine instructions, whereas an assembly-language statement will usually generate (at most) one machine instruction. Second, a high-level language is designed to be oriented to the specification of the problem that is to be solved by the program and to the human thought process, while a program in an assembly language is oriented to the computer instruction set and to the hardware used to execute the program. Consider the dot product subroutine used in the previous chapter, written in C below. Each line of the program generates many machine instructions or lines of assembly-language code. Each high-level language statement is designed to express an idea used in the statement of the problem and is oriented to the user rather than the machine. The compiler could generate the assembly-language program or the machine code produced by this program.

int dotprod(char

v[], char w [ ] ) { int i,

dprd = 0;

for(i = 0;

i

< 2; i++) dprd += v[i]

* w[i];

return dprd;

}

221

8.7 Object-oriented Programming in C++

237

A class's function members are written rather like C procedures with the return type and class name in front of two colons and the function member name.

void Cstack::push{int i){if(Ptr==Top){Error=l; return?} *(++Ptr)=i; }

int Cstack::pull{){if(Ptr==Bottom){ Error=l; return 0;} return *(Ptr—);}

char Cstack::error{){ char i; i = Error; Error = 0; return i; }

Any data member, such as Top, may be accessed inside any function member of class Cstack, such as push() . Inside a function member, when a name appears in an expression, the variable's name is first searched against local variables and function formal parameters. If the name matches, the variable is local or an argument. Then the variable is matched against the object data members and finally against the global variables. In a sense, object data members are global among the function members, because each of them can get to these same variables. However, it is possible that a data member and a local variable or argument have the same name such as Error. The data member can be identified as this->Error, using key word this to point to the object that called the function member, while the local variable or argument is just Error.

C++ uses constructors, allocators, destructors, and deallocators. An allocator allocates data member storage. A constructor initializes these variables; it has the same function name as the class name. Declaring or blessing an object automatically calls the allocator and constructor, as we will see shortly. A destructor terminates the use of an object. A destructor has the same function name as the class name but has a tilde (~) in front of the function member name. A deallocator recovers storage for data members for later allocation. We do not use a deallocator in our experiments; it is easier to reset the 6812 to deallocate storage. Here's Cstack's constructor:

Cstack::Cstack(int i){Top=(Ptr=Bottom=(char*)allocate(i))+i;Qlen= Error=0;}

Throughout this section, a conventional C procedure allocate provides buffer storage for an object's data members and for an object's additional storage such as its stacks. The contents of global variable free are initialized to the address just above the last global; storage between free and the stack pointer is subdivided into buffers for each object by the allocate routine. The stack used for return addresses and local variables builds from one end and the allocator builds from the other end of a common RAM buffer area, allocate's return type void * means a pointer toanything.

char *free=0xb80;

void *allocate(int i) { void *p=free; free += i; return p; }

A global object of a class is declared and then used as shown below:

Cstack S(10);

void main() { int i;

S.push(l); i = S.pull();

}