Other Pitfalls

As you have seen so far, pitfalls can occur in the most unexpected places—for example, algorithms, translations from mathematical equations—and through common typos. What you have not yet seen are pitfalls generated by external influences to source files. This section looks at pitfalls that can occur because of the way in which character arrays are implemented by compilers, and the use of hardware addresses from source files.

Pitfalls with Character Arrays

This section presents pitfalls related to character arrays and the difference of interpretation of character arrays between C and C++. Consider the following statement:

// Does not compile in C++.
char text[3] = "fit";

This statement attempts to reserve three bytes of memory for an array called text, and fill it with the characters f, i, and t. When you run this statement through a C compiler it will compile just fine; however, it cannot be compiled with a C++ compiler. This is because the C++ compiler wants to add a terminating zero (' 0') character at the end of the constant string, and for this it would need an array of four bytes. The proof of this is given by the following example:

// Array of 4 elements.
char text[] = "fit";
int sizetext = sizeof(text);

This time the compiler can decide how large the text array should be and this length is placed in the variable sizetext. It should not surprise you that the value of sizetext is four in this example. What is surprising is that sizetext is four also when a C compiler is used. So when given a chance it appends a terminator also. Sometimes, however, you want to specify exactly how much memory is needed for a character array, and whether or not a terminator is needed. Think of fixed length strings for a user interface or for configuration files. If you know beforehand that a string is always three characters in size, you probably do not need a fourth terminating character. In that case you could try the following:

// Alignment pitfall.
char threeC[3] = { 'f','i','t'} ;
char fourC [4] = { 'f','i','t','s'} ;

This compiles just fine in both C and C++; however, now you have to deal with an alignment pitfall (for more details on alignment refer to Chapter 6). Some compilers will align any array on a four-byte boundary or sometimes even an eight-byte boundary. This means that there will be a stuffing byte between arrays threeC and fourC, or, put differently, &threeC[4] == &fourC[0]. On compilers that align character arrays on a one-byte boundary, there is no stuffing byte, or, put differently, &threeC[3] == &fourC[0]. This may not be a problem until you try to copy data into the arrays with the strcpy() function.

// Possible copy error.
strcpy(threeC,"abc");

This statement copies the constant string "abc" into the array threeC, up to and including its zero terminator! This means that with four-byte alignment, the string fourC is not affected by the string copy (the extra zero terminator of "abc" is placed in the extra stuffing byte following threeC). But with one-byte alignment this copy changes the value of the first character of fourC into a zero. Even more deceptive, the following string copy causes a problem with both alignment strategies.

// definite copy error.
strcpy(fourC,"abcd");

This time there is no stuffing byte to contain the terminator and the memory following array fourC is corrupted. When you do not want to use terminators in (character) arrays, use a loop, memcpy(), or strncpy() instead of strcpy().

memcpy (fourC, "abcd", 4);
strncpy(fourC, "abcd", 4);

Using Hardware Addresses (Memory-Mapped IO)

When you make use of data that becomes available through specific hardware addresses—for instance, when accessing data in memory-mapped IO schemes—it is important to be sure exactly how this data access is treated in the executable code that is generated by your compiler. The use of normal variables for directly accessing specific hardware addresses can cause problems with efficiency of the code, and more importantly, the actual correctness of the code. This section explains how to access hardware addresses correctly and efficiently.

Correct Access to Hardware Addresses

Different compilers generate different executable code with different levels of optimizations. This is why you should use the volatile keyword with memory-mapped objects to indicate to the compiler that no optimization may be performed with those objects. Using volatile guarantees that the executable code will reload the value of the object each time it is used, allowing external changes to the value (system events/interrupts, OS states, other processes, and external devices) to be visible in your code. Often you will access specific hardware addresses through pointers. In these cases, it is important to note that the value on the address that is pointed to is volatile and not the address in the pointer itself. The difference is denoted by the place of the volatile keyword in the pointer declaration.

// Error, here pointer p is volatile (p)
char* volatile p = 0xC000050;

In the preceding example, the value of pointer p is denoted as volatile, meaning that internal as well as external influences may cause it to point to a different hardware address.

// Correct, value on address in p is volatile (*p)
volatile char *p = 0xC000050;

The preceding example specifies that the byte value on address 0xC000050 may be changed due to internal or external influences, and its usage should therefore not be optimized.

Another keyword that you might like to use in combination with volatile is const , to tell the compiler that the value of an object should not change. It stands to reason that you use const oppositely to volatile ; this way you can tell the compiler that pointer p should always point to address 0xC000050 (it is not allowed to change and point to any other address), but that the value in address 0xC000050 can change at any given time.

// Constant pointer to a volatile address value
volatile char const *p = 0xC000050;

Listing 15.12 shows different ways of accessing the same address through pointers.

Code Listing 15.12. Using volatile and const Pointers
char hw1 = 'A';
char hw2 = 'A';

void main(void)

{
    char * p1 =&hw1;
    volatile char * p2 = &hw1;
    volatile char * const p3 = &hw1;

    // Legal actions:
    *p1 = 'b'; p1 = &hw2;
    *p2 = 'c'; p2 = &hw2;
    *p3 = 'd';

    // Illegal action:
    p3 =
								
								
								
								
								
								
								 &hw2;
}

In Listing 15.12, pointer p1 may be subject to compiler optimization, so accessing address hw through p1 may not always work correctly. Accessing hw through pointer p2 is never optimized and is therefore safe, as long as you do not (accidentally) change the value of p2 itself. The safest way to access hw is through pointer p3, as it is never optimized and its address cannot change; the last statement p3 = &hw2; therefore does not even compile. Note that the value of p3 can, of course, be corrupted through non-const pointers to the address of p3 itself and through other memory corruption, such as writing past array bounds and so on.

Efficient Access to Hardware Addresses

Now that you have a correct way of accessing specific hardware addresses, the next step is to make sure that access is done efficiently. Often you will want to read a stream of bytes from a device. When this device has memory-mapped IO, information is retrieved by continuously reading from the mapped addresses. That is, when no DMA is available. Each read action signals the underlying device to place the next value on the address. Reading a number of bytes from such a device can be done as follows:

unsigned char buffer[1024];
for (int i = 0; i < buflen; i++)
    buffer[i] = *HARDWAREMAPPEDREGISTER;

HARDWAREMAPPEDREGISTER is a pointer to a specific hardware address on which a device places bytes of data. The buffer is used to store sequentially read bytes. Although this implementation seems pretty straightforward and looks lean because of the small number of statements it contains, it is far from efficient. This is because data is read from the hardware address and placed in normal memory. As you have seen in Chapter 5, "Measuring Time and Complexity," using memory can be slow because of caching and paging schemes. (Cache needs to be consistent with memory, resulting in cache updates if a write-through scheme is used. You can minimize these updates by using processor registers so that such an update is necessary only once per 4 bytes.) Listing 15.13 shows how this kind of memory access can be speeded up by using a register as a four-byte IO buffer.

Code Listing 15.13. Efficient Hardware Address Access (Big-Endian)
unsigned char buffer[1024];
for (int i = 0; i < buflen; i+= 4)
{
    register long tmp;

    tmp = *HARDWAREMAPPEDREGISTER;
    tmp << 8;
    tmp |= *HARDWAREMAPPEDREGISTER;
    tmp << 8;
    tmp |= *HARDWAREMAPPEDREGISTER;
    tmp << 8;
    tmp |= *HARDWAREMAPPEDREGISTER;

    buffer[i] =
								
								
								 tmp
}

Because of the small number of variables used within the loop in Listing 15.13, it is very likely that the compiler will assign tmp to a register. However, the register keyword is added to urge the compiler in the right direction. Now four consecutive read actions can be performed very fast. Each byte that is read is shifted upwards in the long word to make room for the next byte to be read. When the long word is full, it is finally placed into the buffer. With larger registers, IO can be speeded up further; however, make sure that you choose a type for variable tmp that can easily be mapped on a CPU register. If, for instance, you define a 64-bit tmp when the processor does not have any (or a sufficient number of) 64-bit registers, the compiler will choose a normal memory variable for tmp instead of a register. This means you just lose IO speed because of the extra statements added in Listing 15.13.

Note that the way in which data is placed in tmp and shifted upwards in the direction of the most significant byte, makes it ideal for big-endian implementations. If, however, you would like to receive the bytes of the device in a little-endian order, some changes need to be made as shown in Listing 15.14.

Code Listing 15.14. Efficient Hardware Address Access (Little-Endian)
unsigned char buffer[1024];
for (int i = 0; i < buflen; i+= 4)
{
    register long tmp;

        tmp =  (*HARDWAREMAPPEDREGISTER);
        tmp |= (*HARDWAREMAPPEDREGISTER) << 8;
        tmp |= (*HARDWAREMAPPEDREGISTER) << 16;
        tmp |= (*HARDWAREMAPPEDREGISTER) << 24;

    buffer[i] 
								
								
								
								
								
								 = tmp
}

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset