Software Design

Because software for embedded systems often goes into some kind of ROM chip rather than on a disk, the software is commonly called firmware.

Reentrancy

Reentrancy addresses the question: will anything 'bad' happen if a function (or subroutine or procedure) is re-entered.

Functions can be re-entered if some event causes execution of the function to stop, and causes another thread of execution to call the same function from the beginning.

Functions can be re-entered via

Reentrancy is best explained by an example:

For CPUs without an FPU, floating-point operations must be handled by a floating-point library. Typically, this library is not reentrant: if you use floating-point math in both the foreground routine and an interrupt-service routine (ISR), the system will either crash or give garbage results.

 
These evil effects occur because the floating-point library uses static data. Each time the library code is called, a single region of memory is used, either as temporary scratchpad memory, or to simulate the registers of the missing FPU. The foreground routine starts a floating-point operation, which puts intermediate results into the global memory. Then an interrupt occurs, and one or more floating-point operations in the ISR overwrite the intermediate results calcuated for the foreground routine. When the interrupt routine returns, the floating-point calcuations made for the foreground routine have been corrupted.

If memory is needed by the floating-point library on a temporary basis only, it could be allocated from the stack instead. Every time the library is called, a new region of temporary memory would be created. The floating-point operation could be interrupted, and a new floating-point operation begun and run to completion, without harmful effects.

To avoid problems with non-reentrant floating-point math:
  • Ask your compiler vendor about the availability of a reentrant floating-point package. You may have better luck with compilers for CPU families (such as most 8-bit microcontrollers) which have no FPU whatever.
  • Use floating-point math only in the foreground routine or in the ISR -- not both.
  • Have the ISR save the global data before using floating-point math, and restore it afterwards. (This is CPU- and compiler-dependent.)
  • Disable interrupts before using floating-point math in the foreground routine, enable interrupts afterward.
  • Use fixed-point math. A clever C++ class, with operator overloading, can make fixed-point very easy and intuitive to use.

In general, any routine that modifies global data (or, in C, static local data) is not reentrant. Many functions in the standard C library are not reentrant.

In software for desktop systems, where reentrancy is a concern because of multitasking, the term thread-safe is used instead.

Doing without an OS

Can your DOS program run, unmodified, on a single-board computer? Unless that computer was designed with DOS compatability in mind, this is unlikely. Even a seemingly simple function like printf() may fail -- how can it use the video BIOS to put text on the screen when there is no BIOS (and no screen)?

The C functions that require an OS or BIOS can be difficult to spot. Most functions in io.h that access files fall into this category, as well as most of the stream I/O functions in stdio.h (though sprintf() is usually a happy exception). If you have access to source code for your C library, you can try looking for the macros, functions, or software interrupt instructions that are used to access OS syscalls or BIOS services.

Functions in the Watcom C/C++ standard library that require OS services. Note that many of these functions are also in the list of non-reentrant functions above.

Volatile data

The volatile keyword in C tells the compiler that some external agent can change the value of a variable, and that the compiler should suppress all optimizations for that variable.

Almost all compilers perform optimization, to make the machine-language code they generate smaller and/or faster. A common optimization is to store variables in CPU registers, rather than in memory. In embedded systems, this can be disasterous. Consider a function that writes one byte to parallel EEPROM memory:

int writeEEPROMByte(unsigned char Data, unsigned Offset)
{       unsigned char *Dst;
        unsigned Await;

	Dst=EEPROM_SEG + Offset;
/* if the EEPROM already has this value in it, skip this byte (saves wear) */
	if(*Dst != Data)
	{	*Dst=Data;
/* ... else write it and read it repeatedly until it verifies (D7 polling) */
                for(Await=EEPROM_TIMEOUT; Await != 0; Await--)
                {       if(*Dst == Data) break; }
                if(Await == 0) return(-1); }    /* timeout (failure) */
        return(0); }                            /* success */
When compiled with DJGPP and heavy (-O2) optimization, the inner for loop gets converted to code like this:
00001568 <L6>:
    1568:       38 ca           cmpb   %cl,%dl
    156a:       74 04           je     1570 <L4>
    156c:       66 48           decw   %ax
    156e:       75 f8           jne    1568 <L6>
The compiler has cached the byte at Dst in one of the registers (dl). Nothing in this loop changes either the value of dl or cl, therefore, the first comparison will always fail. The write will ALWAYS timeout.

The compiler created this dumb code because it doesn't know any better. Tell it that the byte at Dst is volatile; that an external agent (hardware in this case: the EEPROM chip) can change the value there:

{       volatile unsigned char *Dst;
With this fix, the compiled inner loop code becomes
0000156c <L6>:
    156c:       8a 01           movb   (%ecx),%al
    156e:       38 d8           cmpb   %bl,%al
    1570:       74 04           je     1576 <L4>
    1572:       66 4a           decw   %dx
    1574:       75 f6           jne    156c <L6>
Now the byte at Dst is no longer being cached in a register. Rather, it is being reloaded from memory each time it's needed. When the EEPROM write cycle ends, and the byte read back from the chip verifies, the loop will exit successfully.

Other uses for volatile

Aside: there are many good reasons to look at the output of your compiler and this is one of them. Others are to see how efficient the compiler is, or to see if it's generating bad code.