Preemptive real-time software debugging

Software engineers creating embedded software,
and especially real-time embedded software, are always looking for
imaginative ways to increase the effectiveness and efficiency of
their code. This article presents some often-overlooked ways to use
your compiler to prevent debugging in real-time systems.

Errors and warnings

In the same vein as “only floss the
teeth you want to keep,” you should only clear compiler
errors and warnings from those files that you want to work
properly. Many engineers can’t help but think that
compiler errors and warnings are criticisms of their coding. In
truth, of course, every message represents the compiler telling you
that it’s either unsure of how to interpret what
you’ve written, or worse, that it’s pretty sure
that what you wrote is not what you meant. It’s much
easier to ensure that the compiler is able to properly interpret
your code, including, heaven forbid, actual coding errors, than it
is to determine coding errors from the real-time performance debug
of your product code.

Although cleaning up compiler errors and
warnings is tedious, finding misbehaving real-time code on your
board is ten times worse! If you write tons of code and try to rush
a clean compile, you’re asking for a boatload of
frustration. Real-time code is hard enough to write without
fighting the compiler. Write small chunks of code and compile them
frequently to make sure the compiler understands them. Also, start
at the very beginning and clean up only the first error or two, and
then re-compile. Even a single missing semicolon can generate so
many error messages that the compiler stops printing them.

Designers know what they should do when coding
— short functions with a single purpose, clean control
flow, good encapsulation of data (limit global variables), and
coherent pointer/array usage. It makes the task of completing your
real-time assignments that much more efficient. If you have a tough
time being disciplined in what you know would make your code work
better, you should seriously consider using a MISRA (Motor Industry
Software Reliability Association) checker. MISRA has a number of
rules that limit the most often abused features of the C
programming language. If you stick with MISRA, you’ll get
more deterministic execution. You may even be surprised to see your
code size shrink after you’ve coded things so
cleanly.

Optimization

More than any other aspect of real-time and
embedded programming, optimization causes the most nervousness.
Here are a few tips about optimization.

1. Choose an algorithm
carefully.

Picking a good, or great, algorithm will beat an
optimizing compiler every time. The compiler’s most
important job is to generate “correct” code,
not efficient code. Many programmers mistakenly assume that an
optimizer is there to change their code, which it
won’t.

2. Avoid inline
assembly.

Engineers with real-time constraints often
assume they have to use inline assembly. The old adage says
“make it work, then make it fast.” These
engineers should consider writing completely in C first. However,
don’t avoid inline assembly because it is inherently bad,
avoid it because it is difficult to write correctly and usually
unnecessary. Try using built-in functions of the compiler. If you
absolutely must use inline assembly, ensure that you understand and
use the “extended” form, so that you can make
your usage clear to the compiler, and use C variables, not the
underlying registers.

3. Remove code that has no
effect — the bane of real-time functionality.

Optimizers won’t change
programmers’ code. So, how do they reduce code size? One
of several answers lies in the former mystery of code that has
“no effect.” When the compiler reduces the size
of your code, it can’t arbitrarily throw things away, so
one of the tricks it uses is to look for code that has
“no effect.” It’s important that you
understand what a compiler means by this. Take the following timing
loop, for example:

for(i = 0; i < 10000;
i++);

// wait 1 us

You know that a microsecond is important, but
the compiler doesn’t have any conception of the passage
of time, so this entire construct will be eliminated by the
optimizer because this code has “no
effect.”

The general principal by which optimizations
remove expressions that have “no effect” is as
follows: if the compiler can deduce that values in the expression
are not used and that no needed side effects are produced, it is
free to remove the code. There are several ways around this, one of
which, volatile, is covered in the
next section.

Concurrent operations, such as interrupt
handlers, are a key part of real-time programming. It is often
necessary to share variables with ISRs. Shared variables are
frequently a source of problems under optimization because the
compiler, unless told otherwise, assumes that it can see everything
that happens to a variable. The modifier volatile is often used to protect shared
variables between concurrent execution events. In essence, volatile tells the compiler that it
doesn’t know everything about a variable, with the side
effect of preventing it from optimizing access to that
variable.

Volatile
doesn’t imply that an access is uninterrupted (atomic,
done in one step). Even a simple assignment in mainline code
like:

volatilelong x = 3;

(that looks like it is atomic) could actually be
jeopardized by a similar assignment in an ISR. An 8-bit compiler
will probably compose a 32-bit “long” write out
of four consecutive 8-bit writes. Any of those writes may be
interrupted at any time. If the assignment to x is interrupted and
the ISR writes to x, when control returns to the mainline, x will
likely be corrupted.

A good way to control the access of a shared
variable is through the use of accessor functions. Accessor
functions provide common code where the programmer can decide the
level of safety required for each variable and ensure that each
access follows the rules. In the previous case, one might decide to
disable interrupts when writing to x. If you need to guarantee
atomicity, try accessor functions instead of using volatile.

5. Watch unfettered
casting of pointers (also known as coercion or type
punning)

Casting is a means for circumventing the type
system using pointers, and is another clever but misguided way to
completely mess up the operation of real-time code. According to
standard syntax, casting pointers is syntactically allowed, but it
is often semantically invalid (that is, it doesn’t work).
At first glance, it may seem efficient (and may even work on a
particular architecture at a given optimization level), to get to
the exponent of an IEEE 754 floating-point number with the
following:

float x =
186000;

int exponent =
((*(int*)(&x)) & 0x7F800000) >> 23;

However, this kind of expression is fragile and
can yield unexpected results with different compilers or
optimization settings. Instead of casting pointers, use a union,
such as:

union {

float f;

struct float_format
s;

} v;

Once the value is assigned to v.f, the exponent,
or any other field, can be accessed easily through the structure.
Under optimization, the compiler will be able to make the access as
safe as it is efficient.

Modern compilers

Modern compilers, such as Microchip’s
MPLAB XC line, are powerful tools capable of helping real-time
systems programmers accomplish a great deal in a short amount of
time. Like any other tool, they must be used skillfully and you
must “listen” to the tool. The compiler makes
that as easy as reading. With proper understanding a lot of the
frustration and mystery associated with compilers and optimization
can be avoided. ■