Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Interfacing with C plus plus-programing communication with microcontrolers (K. Bentley, 2006)

.pdf
Скачиваний:
192
Добавлен:
12.08.2013
Размер:
3.18 Mб
Скачать

184 7 DRIVING LEDS

Leds.SetPatternAddress(LightPattern,

sizeof(LightPattern));

Programming with functions can often be made more efficient by defining macros and then calling the functions through the macro. As can be seen in the above statement, the word LightPattern occurs twice in the function call. In order to minimise the possibility of coding an error, a macro can be written that requires the parameter LightPattern be specified only once. The following section describes the process of defining a macro.

7.7 Macros

Macros can be viewed as placeholders that the preprocessor replaces with an expression. For example, consider when the compiler encounters the following statement:

y = x*x*x;

We can define a macro to facilitate programming as follows:

#define CUBE(x) ((x)*(x)*(x))

The preprocessor will replace all occurrences of CUBE(x) with ((x)*(x)*(x)). Thus, CUBE(x) can be used freely in the program. The extra pair of parenthesis is necessary to adhere strictly with the intended precedence of operations. For example, if CUBE(x) was defined as:

#define CUBE(x)

x*x*x;

then,

y

=

CUBE(3);

will be replaced by, y = 3*3*3 evaluating to 27.

On the other hand, the line:

 

y

=

CUBE(2+1);

will be expanded to, y = 2+1*2+1*2+1 incorrectly

evaluating to 7.

 

 

If we now use the definition with every x placed in a pair of parentheses; (x)*(x)*(x), the expression will be expanded to:

y = (2+1)*(2+1)*(2+1) which correctly evaluates to 27.

If the preceding definition for CUBE was used for the following expression, an incorrect result will be generated.

y = 81/CUBE(3);

This will expand to:

y = 81/(3)*(3)*(3);

which evaluates to 243 instead of the intended result 3. Using an additional outer pair of parenthesis ensures a correct result.

7 DRIVING LEDS 185

To improve the program given in Listing 7-8, we can include a macro as follows:

#define SetArray(x)

SetPatternAddress((x), sizeof(x))

The main() function is shown in Listing 7-9.

Listing 7-9 The main() function – user fills in the LED pattern.

#define SetArray(x) SetPatternAddress((x), sizeof((x)))

void main()

{

unsigned char UserPattern, LightPattern[4]; LEDs Leds;

int i;

cout << "Enter “ << sizeof(LightPattern);

cout << “ user patterns in the range 0x00-0xFF "; cout << endl;

for(i = 0; i < sizeof(LightPattern); i++)

{

cin >> UserPattern;

*(LightPattern + i) = UserPattern;

}

Leds.SetArray(LightPattern);

Leds.LightLEDs();

getch();

Leds.LightLEDs();

}

7.8 Dynamic Memory Allocation

It often happens that a C++ program will use dynamic memory allocation to request and have memory allocated at run-time. Dynamic memory allocation is used very widely in C++ programs and can greatly reduce the size of the executable program. It is especially useful when the actual storage requirements of the program are not known at the time of programming. For example, a program written to process the marks of a class of students will operate on various sized classes, their size unknown at the time of programming.

Memory allocation can be static or dynamic. In the static case, the compiler allocates the required memory at compile time. Programs with statically allocated memory tend to be bigger than programs with dynamically allocated memory. Furthermore, the statically allocated memory is obtained from the data area - an area specially set aside to store data. The memory region used to store program

186 7 DRIVING LEDS

instructions is known as the code area and is a different region to the data area. In general, the program instructions or code do not change during the life of the program. However, data will change as the executing program operates on it.

Temporary data is created and destroyed during program execution in another area named the stack. The stack is a last-in-first-out (LIFO) type queue using a specially allocated region of computer memory. Some examples of temporary data are; parameters passed to a function, local variables declared within a function, and values returned by functions.

In the case of dynamic memory allocation, the requested memory is granted from yet another area named the heap (also known as the free store). Note: memory that has been allocated is not automatically returned back to the heap by the system. It is the programmer’s responsibility to include instructions to return the dynamically allocated memory for other use.

The two operators that manage dynamic memory allocation are shown in Table 7-4. The operator new allocates the requested memory and returns a pointer that points to the beginning of the allocated area. The simplest use of the new operator is shown in the following example:

int *IntPtr; IntPtr = new int;

The same effect can be achieved in one statement as follows:

int *IntPtr = new int;

Table 7-4 Operators used with dynamic memory allocation.

Operator

Function

new

To request memory

delete

To free up memory

In this example, we have requested the dynamic allocation of space from the heap for one int type data. We do not have a name for the allocated space, however, the pointer IntPtr knows where it is. Since we know how to manipulate data using pointers, we can use the allocated space as we need. At the end of its use, the memory space must be returned using the delete operator as follows:

delete IntPtr;

This operation does not remove the pointer variable IntPtr. Instead it releases or returns the portion of memory pointed to by IntPtr, thereby making the dynamically allocated integer no longer available. Now the memory previously occupied by that integer is available for any future dynamic memory allocation operations. If the memory was not released using the delete operator, then that memory will not be able to be used during remaining program operation. In this

7 DRIVING LEDS 187

situation we have created what is known as a memory leak and the computer will have a reduced amount of memory it can use.

A slightly more complex example is now given that requests space for an array of ten integers:

int *IntPtr;

IntPtr = new int[10];

When the allocated memory space is no longer needed, it must be relinquished using the delete operator as follows:

delete IntPtr;

The following example requests space for a two-dimensional array of 100 int type data:

int(*RowPtr[10]);

RowPtr = new int[10][10];

RowPtr is a pointer to a row of 10 elements. The new operator asks for memory to store 10 sets of 10 element arrays of type int. The allocated storage can be freed by using:

delete RowPtr;

Space can be dynamically allocated to class objects as well. To dynamically create a DAC type object, we can use the following statements:

DAC *DACPtr;

DACPtr = new DAC;

We do not have a name for the DAC object that has been dynamically created on the heap. However, the pointer DACPtr knows where the object is. The pointer can be used just as efficiently as an object name to manipulate the object.

The allocated space must be deleted by:

delete DACPtr;

In all these memory allocation operations the new operator calls the constructor of the object. Although we did not create a constructor for int type objects, the data type int has its own constructor. For example, to create space for one int type data which is initialised to 0, the following statements can be used:

int *IntPtr;

IntPtr = new int(0);

or they can be combined into one line as follows:

int *IntPtr = new int(0);

188 7 DRIVING LEDS

Similarly, if we want to create a new DAC type object that communicates with the PC using the BASE address 0x3BC instead of 0x378, we use the following statement:

DAC* DACPtr = new DAC(0x3BC);

When the new operator calls the constructor of the DAC class, the BASE address that is specified (in this case 0x3BC) will be passed.

When the object belongs to a class hierarchy, the pointer does not necessarily need to be of the same type – it can be a pointer to one of its base classes. In the case of the DAC class, the following is still valid:

ParallelPort *Ptr;

Ptr = new DAC;

The same pointer can even be made to point to a new, yet different object of the same hierarchy. Suppose we had an object type named DAC16Bit further down in the hierarchy. Then, the following statement is allowed:

Ptr = new DAC16Bit;

In Section 8.6 we will be using this concept together with virtual functions to improve our programming.

It is also possible for the dynamic memory allocation operation to fail. This can occur if there is insufficient memory available to allocate. If memory allocation fails, then the pointer returned by the new operator will be set to NULL, a predefined constant. A simple test such as that shown below can be carried out to check if memory allocation has been unsuccessful:

if(Ptr == NULL)

{

cout << “Memory allocation failed “; exit(1);

}

The function exit() is a library routine that can be called to terminate the program (if you decide insufficient memory on the heap justifies termination). Another approach is to write the program to cause an exception as described in Section 7.9. Any pointer returned by the new operator can be tested this way before proceeding. The usual practice is to pass an actual argument of 1 to the exit() function. This indicates to the system that runs your application that the program has terminated prematurely.

Typecasting

Wherever permitted, typecasting can be used to convert an existing type to match another type. The application of typecasting to fundamental data types is demonstrated by the following example:

int a;

7 DRIVING LEDS 189

float b = 8.73;

a = (int) b; // a will be 8.

The float type variable b is typecast to match that of a (data type int) and hence its value rounds down to 8. Similarly, pointers can also be typecast as shown in the following example:

int *IntPtr;

IntPtr = (*int) new int[10][10];

The pointer returned by the new operator is type casted to match the pointer type of IntPtr by using (*int), which can be interpreted as ‘pointer to int’. Note that the new operator returns a pointer to an array of 10 elements (because the row size of the array being created is 10). However, the pointer IntPtr is a pointer to just one int type data.

7.9 Exception Handling

Exception handling allows a program to take appropriate actions in the event of exceptional conditions ocurring. These situations usually happen when a program cannot continue exececution as expected due to events that occur outside the scope of normal program control. For example, the program may not be able to continue its normal operation if there is insufficient memory to fulfil a memory allocation request.

There are many situations that can cause a program to terminate abnormally, such as not having enough disk space to write to a file, attempting to write to a file that is already opened for reading, etc. Such situations often arise due to the circumstances under which the program is running and not necessarily due to programming errors. To manage such situations, C++ uses exception handling. Exception handling can only manage routine events that arise when executing a program. It is not used to handle events such as user-driven abortion of program execution by pressing ‘Control-C’.

The three keywords associated with exception handling are try, throw and catch. The keyword try is used to form a try block. A try block consists of the keyword try followed by the try block contained within a pair of matching braces:

try

{

. . .

}

All statements that are likely to cause exceptional situations are executed within the try block. Each situation that may lead to an exception must be identified and a throw statement must then be executed. In the example shown in Listing 7-10 we are attempting to allocate memory from the free store; being n unsigned char

190 7 DRIVING LEDS

locations. This attempt may fail; (a) if the value of n is less than 1, (b) there is no space available in the free store.

Listing 7-10 An example try block for dynamic memory allocation.

unsigned char* LightPattern; try

{

if(n < 1) throw(n);

LightPattern = new unsigned char[n]; if(LightPattern == NULL)

throw("Memory error");

}

The most significant observation to be made here is the signature of the throw statements. The first throw, throws just one integer, being n. The second throw throws a string. Immediately after the try block, there must be matching catches. In our case there must be a catch that matches the throw of one integer and another catch that matches the throw of one string. When these two catches are included, Listing 7-10 will become the code shown in Listing 7-11.

Listing 7-11 Try block with the catches.

unsigned char* LightPattern;

try

{

if((n < 1) || (n > 4)) // Note: || is logical OR throw(n);

LightPattern = new unsigned char[n]; if(LightPattern == NULL)

throw("Memory error");

}

catch(int n) // catches the throw of integer

{

cout << "Illegal number of elements requested" << endl; cout << "Array size defaults to 4" << endl;

n = 4;

LightPattern = new unsigned char[n];

}

7 DRIVING LEDS 191

catch(char* memerror)

{

cout << "Memory allocation failed " << endl; cout << "Terminating program " << endl; exit(1);

}

Walking LEDs – User Definable Array Size and Contents

We can improve the program presented in Listing 7-9 so the user has the flexibility to define the size as well as the contents of the LED pattern array. Each number entered into the array by the user will be output to the bank of LEDs in sequence, followed by a short delay. Therefore, the main() function shown in Listing 7-9 can be re-written using dynamic memory allocation and exception handling as shown in Listing 7-12.

Listing 7-12 Dynamic memory allocation and exception handling for the LED walk .

#include <iostream.h> #include <conio.h> #include <stdlib.h> #include <dos.h>

#define SetArray(x) SetPatternAddress((x), sizeof((x)))

class ParallelPort

{

private:

unsigned int BaseAddress; unsigned char InDataPort1;

public:

ParallelPort(); ParallelPort(int baseaddress);

void WritePort0(unsigned char data); void WritePort2(unsigned char data); unsigned char ReadPort1();

};

ParallelPort::ParallelPort()

{

BaseAddress = 0x378; InDataPort1 = 0;

}

192 7 DRIVING LEDS

ParallelPort::ParallelPort(int baseaddress)

{

BaseAddress = baseaddress; InDataPort1 = 0;

}

void ParallelPort::WritePort0(unsigned char data)

{

outportb(BaseAddress,data);

}

void ParallelPort::WritePort2(unsigned char data)

{

outportb(BaseAddress+2,data ^ 0x0B);

}

unsigned char ParallelPort::ReadPort1()

{

InDataPort1 = inportb(BaseAddress+1);

//Invert most significant bit to compensate

//for internal inversion by printer port hardware. InDataPort1 ^= 0x80;

//Filter to clear unused data bits D0, D1 and D2 to zero. InDataPort1 &= 0xF8;

return InDataPort1;

}

class LEDs : public ParallelPort

{

private:

unsigned char* PatternPtr; int PatternIndex;

int MaxIndex;

public:

LEDs();

LEDs(int baseaddress);

void SetPatternAddress(unsigned char* pattern,

int maxidx);

void LightLEDs();

};

LEDs::LEDs()

{

7 DRIVING LEDS 193

MaxIndex = 0; PatternIndex = 0;

}

LEDs::LEDs(int baseaddress) : ParallelPort(baseaddress)

{

MaxIndex = 0; PatternIndex = 0;

}

void LEDs::SetPatternAddress(unsigned char* pattern, int maxidx)

{

PatternPtr

= pattern;

//

pointer Pattern assigned address

MaxIndex =

maxidx;

//

of pattern

 

 

}

void LEDs::LightLEDs()

{

if(MaxIndex == 0)

{

cout << "No Patterns to display " << endl; return;

}

while(!kbhit())

{

WritePort0(*(PatternPtr + PatternIndex++));

// Reset PatternIndex when it gets to MaxIndex. if(PatternIndex == MaxIndex) PatternIndex = 0; delay(500);

}

getch(); // absorb the key that was hit

}

void main()

{

LEDs Leds;

unsigned char* LightPattern; int TempPattern;

int n, i;

cout << "Pass in the desired size of LightPattern => ";