Avoiding
the pitfalls of embedded application programming |
Although writing software for embedded computers may sound simple enough, particularly to the seasoned PC programmer, there are several pitfalls that programmers of embedded systems should avoid:
- Overuse of the embedded computer's internal flash memory,
- Memory leak that results when unused dynamic memory is not released, and
- Creating applications that do not anticipate every possible error.
Since embedded systems must often run unattended for months at a time, and embedded computers are often housed in remote out of the way places, one of your main goals as an embedded systems programmer is to protect the stability of your system.
Embedded
vs. personal—it's all about memory
The most significant problem with writing code for embedded computers is a misunderstanding of the role of an embedded computer. This is particularly true for software developers who are experienced PC programmers. Whereas modern PCs are extremely fast and PC applications can freely utilize the massive amount of memory available on a typical PC, embedded systems provide limited memory resources. In fact, embedded systems programmers are often mystified when their application becomes unstable, without realizing that the problem is related to memory issues.
Here's a simple example. When writing software for PC applications it is not uncommon to write 100 MB of data to a 1 GB hard disk. Compare this with writing 10 MB of data to a 15 MB flash disk for an embedded application. Although using only 2/3 of the available space may seem conservative enough to avoid problems, repeatedly writing this much data at a time will result in "dead blocks" in the flash drive. This is because flash memory devices have a limited life cycle of write operations, and since the various sectors of the flash memory will generally not fail all at the same time, the flash memory device could remain active until the amount of good memory is degraded below the amount needed to run the application. When that happens, your application will become unstable and then fail.
The correct way to use an embedded computer's flash disk is to store program files on the disk and store data produced by the programs on an external CF card or SD card. The external card provides a high capacity of storage space and at the same time avoids the life cycle problem. If for some reason you must use the flash for data storage, we strongly recommend that you use at most half of the free flash space available.
Memory
leak
Another pitfall to avoid is memory leak. Embedded computers are extremely sensitive to memory usage, and memory leak degrades system performance due to the increased paging that results when different programs fight for the available memory. Eventually, there won't be enough memory available for all of the programs to run, and consequently some programs will run out of memory and crash. What's worse, though, is that the operating system itself will eventually crash.
Memory leak is often the result of a reckless use of allocated memory. Keep in mind that C/C++ programmers can control the allocation of dynamic memory. A memory leak occurs when the allocated memory is not released after each use, or when the pointer to an allocated memory location is overwritten.
Memory leak problems can also occur when unused file handlers or descriptors are not closed. In communication applications, each TCP or serial connection must be associated with a file descriptor that occupies some amount of memory. When the file descriptor is not closed properly and the connection is re-established repeatedly, memory consumption will eventually lead to memory leak.
To prevent memory leak from occurring, software developers must pay close attention to how their programs handle and use memory. In addition, applications must be well structured.
Programming entails a top-down design and a bottom-up implementation. The application is broken down into layered modules that are isolated from each other as much as possible, and then each module is further broken down into a number of functions. During the implementation stage, you should build your functions first before combining them to create a complete module.
Ideally, functions in a module are accounted for at least in the birth, input/output, and death of the module. The following code segment illustrates such an example.
typedef struct _YYYCONN
{
int fd;
char *packet_data;
int packet_size;
} YYYCONN;
/* the death of a module */
void yyy_close(YYYCONN *con)
{
if (con)
{
if (con->fd > 0) close(con->fd); /* close the file descriptor */
if (con->packet_data) free(con->packet_data); /* free the data buffer */
free(con); /* be sure to structure body */
}
}
/* the birth of a module */
YYYCONN* yyy_open(char *host, int port)
{
int fd;
YYYCONN *con;
con = (YYYCONN *) malloc(sizeof(YYYCONN)); /* memory allocation */
if (con==NULL)
return NULL:
con->fd = make_tcp_client(host, port); /* API call to connect to a TCP server */
If (con->fd < 0)
{
yyy_close(con) ;
return NULL;
}
con->packet_data = (char*) malloc(1024); /* memory allocation */
if (con->packet_data==NULL)
{
yyy_close(con) ;
return NULL;
}
con->packet_size=0;
return con;
}
The function yyy_open creates a TCP connection module along with an open file descriptor. The module body and its member packet_data are memory allocated. When it comes time to destroy the module, we call function yyy_close, which is designed to close the file descriptor and then free the allocated memory properly. To manage multiple connection modules we need a global area, which can either be an array or a linked list to trace these modules. Some of the following management functions may also be required:
- yyy_connection_add: add a connection into the global list.
- yyy_connection_remove: remove a connection from the global list.
- yyy_connection_lookup: lookup a specified connection.
Exception
handling
The final topic of this paper is robustness, which is something we should strive for with all of our applications. Every possible exception must be carefully handled. A carelessly coded program may implicitly assume that the program logic will perform smoothly and as expected. The following code segment is an example:
while(1)
{
if (select(fd+1, read_fds, NULL, NULL, NULL) > 0)
{
…;
}
}
This example assumes that the connection behind the file descriptor, fd, stays alive forever. However, the connection might be intentionally disconnected by the peer party, in which case the function "select" will immediately return -1. The program will then consume CPU power and will not return to the normal state.
Conclusion
Programmers who do not avoid the three pitfalls discussed in this paper will eventually run into instability problems that prevent applications from running over a long period of time. These problems can be very hard to resolve, particularly when a project is under pressure to be ready to market "as soon as possible." For this reason, most developers implement a watchdog timer to run parallel with their applications. If the amount of available memory gets too low, or the program logic falls into an unexpected state, the watchdog timer will not be refreshed and a system rest will be triggered.
Learn more
Rcore Ready-to-Run Embedded Software Platform
Application Partner Community (APC)
» Back to index
|