0% found this document useful (0 votes)
8 views14 pages

Unti 3

The document discusses essential components for embedded program design, focusing on state machines, circular buffers, and queues. It explains the importance of data flow graphs and the assembly and linking processes in compiling programs. Additionally, it covers techniques for optimizing execution time, power, energy, and program size, as well as the significance of program validation and testing in complex systems.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views14 pages

Unti 3

The document discusses essential components for embedded program design, focusing on state machines, circular buffers, and queues. It explains the importance of data flow graphs and the assembly and linking processes in compiling programs. Additionally, it covers techniques for optimizing execution time, power, energy, and program size, as well as the significance of program validation and testing in complex systems.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 14

Program Design

PROGRAM DESIGN:

Components for Embedded Programs:

In this section, we consider code for three structures or components that are commonly used
in embedded software: the state machine, the circular buffer, and the queue. State machines
are well suited to reactive systems such as user interfaces; circular buffers and queues are
useful in digital signal processing.

State Machines

When inputs appear intermittently rather than as periodic samples, it is often convenient to
think of the system as reacting to those inputs.
The reaction of most systems can be characterized in terms of the input received and the
current state of the system. This leads naturally to a finite-state machine style of describing
the reactive system’s behavior.
Moreover, if the behavior is specified in that way, it is natural to write the program
implementing that behavior in a state machine style.
The state machine style of programming is also an efficient implementation of such
computations. Finite-state machines are usually first encountered in the context of hardware
design.

Stream-Oriented Programming and Circular Buffers

The data stream style makes sense for data that comes in regularly and must be processed on
the fly. For each sample, the filter must emit one output that depends on the values of the
last n inputs. In a typical workstation application, we would process the samples over a given
interval by reading them all in from a file and then computing the results all at once in a batch
process. In an embedded system we must not only emit outputs in real time, but we must also
do so using a minimum amount of memory.
The circular buffer is a data structure that lets us handle streaming data in an efficient way.
Figure 2.13 illustrates how a circular buffer stores a subset of the data stream. At each point
in time, the algorithm needs a subset of the data stream that forms a window into the stream.
The window slides with time as we throw out old values no longer needed and add new
values. Since the size of the window does not change, we can use a fixed-size buffer to hold
the current data.

To avoid constantly copying data within the buffer, we will move the head of the buffer in
time. The buffer points to the location at which the next sample will be placed; every time we
add a sample, we automatically overwrite the oldest sample, which is the one that needs to be
thrown out. When the pointer gets to the end of the buffer, it wraps around to the top.
MODELS OF PROGRAMS:

Data Flow Graphs:

A data flow graph is a model of a program with no conditionals. In a high-level


programming language, a code segment with no conditionals—more precisely, with only one
entry and exit point is known as a basic block. Figure 2.14 shows a simple basic block. As the
C code is executed, we would enter this basic block at the beginning and execute all the
statements.

w = a+b;
x = a-c;
y = x+d;
x = a+c;
z = y+e;

Fig 2.14 A basic block in C.

w = a+b;
x = a-c;
y = x1+d;
x2= a+c;
z = y+e;

Fig 2.14 The basic block in single-assignment form.


Before we are able to draw the data flow graph for this code we need to modify it slightly.
There are two assignments to the variable x—it appears twice on the left side of an
assignment. We need to rewrite the code in single-assignment form, in which a variable
appears only once on the left side.

Since our specification is C code, we assume that the statements are executed sequentially, so
that any use of a variable refers to its latest assigned value. In this case, x is not reused in this
block (presumably it is used elsewhere), so we just have to eliminate the multiple assignment
to x. The result is shown in Figure 2.14, where we have used the names x1 and x2 to
distinguish the separate uses of x.
The single-assignment form is important because it allows us to identify a unique location in
the code where each named location is computed. As an introduction to the data flow graph,
we use two types of nodes in the graph round nodes denote operators and square nodes
represent values.
The value nodes may be either inputs to the basic block, such as a and b, or variables
assigned to within the block, such as w and x1.

The data flow graph for our single-assignment code is shown in Figure 2.15.The single-
assignment form means that the data flow graph is acyclic—if we assigned to x multiple
times, then the second assignment would form a cycle in the graph including x and the
operators used to compute x.

The data flow graph for our single-assignment code is shown in Figure 2.15.The single-
assignment form means that the data flow graph is acyclic—if we assigned to x multiple
times, then the second assignment would form a cycle in the graph including x and the
operators used to compute x.
The data flow graph for our single-assignment code is shown in Figure 2.15.The single-
assignment form means that the data flow graph is acyclic—if we assigned to x multiple
times, then the second assignment would form a cycle in the graph including x and the
operators used to compute x.
ASSEMBLY AND LINKING:

Assembly and linking are the last steps in the compilation process they turn a list of
instructions into an image of the program’s bits in memory. Loading actually puts the
program in memory so that it can be executed. In this section, we survey the basic techniques
required for assembly linking to help us understand the complete compilation and loading
process.

Figure 2.16 highlights the role of assemblers and linkers in the compilation process. This
process is often hidden from us by compilation commands that do everything required to
generate an executable program. As the figure shows, most compilers do not directly generate
machine code, but instead create the instruction-level program in the form of human-readable
assembly language. Generating assembly language rather than binary instructions frees the
compiler writer from details extraneous to the compilation process, which includes the
instruction format as well as the exact addresses of instructions and data.

The assembler’s job is to translate symbolic assembly language statements into bit-level
representations of instructions known as object code. The assembler takes care of instruction
formats and does part of the job of translating labels into addresses. However, since the
program may be built from many files, the final steps in determining the addresses of
instructions and data are performed by the linker, which produces an executable binary file.
That file may not necessarily be located in the CPU’s memory, however, unless the linker
happens to create the executable directly in RAM. The program that brings the program into
memory for execution is called a loader.
The simplest form of the assembler assumes that the starting address of the assembly
language program has been specified by the programmer. The addresses in such a program
are known as absolute addresses.

Assemblers

When translating assembly code into object code, the assembler must translate opcodes and
format the bits in each instruction, and translate labels into addresses. In this section, we
review the translation of assembly language into binary. Labels make the assembly process
more complex, but they are the most important abstraction provided by the assembler. Labels
let the programmer (a human programmer or a compiler generating assembly code) avoid
worrying about the locations of instructions and data. Label processing requires making two
passes through the assembly source code as follows:
1. The first pass scans the code to determine the address of each label.

2. The second pass assembles the instructions using the label values computed in the first
pass.

As shown in Figure 2.17, the name of each symbol and its address is stored in a symbol
table that is built during the first pass. The symbol table is built by scanning from the first
instruction to the last.
During scanning, the current location in memory is kept in a program location counter
(PLC). Despite the similarity in name to a program counter, the PLC is not used to execute
the program, only to assign memory locations to labels. For example, the PLC always makes
exactly one pass through the program, whereas the program counter makes many passes over
code in a loop. Thus, at the start of the first pass, the PLC is set to the program’s starting
address and the assembler looks at the first line. After examining the line, the assembler
updates the PLC to the next location (since ARM instructions are four bytes long, the PLC
would be incremented by four) and looks at the next instruction. If the instruction begins with
a label, a new entry is made in the symbol table, which includes the label name and its value.
The value of the label is equal to the current value of the PLC. At the end of the first pass, the
assembler rewinds to the beginning of the assembly language file to make the second pass.
During the second pass, when a label name is found, the label is looked up in the symbol
table and its value substituted into the appropriate place in the instruction.
But how do we know the starting value of the PLC? The simplest case is absolute addressing.
In this case, one of the first statements in the assembly language program is a pseudo-op that
specifies the origin of the program, that is, the location of the first address in the program. A
common name for this pseudo-op (e.g., the one used for the ARM) is the ORG statement.

ORG 2000

Which puts the start of the program at location 2000. This pseudo-op accomplishes this by
setting the PLC’s value to its argument’s value, 2000 in this case. Assemblers generally allow
a program to have many ORG statements in case instructions or data must be spread around
various spots in memory.

Linking:

Many assembly language programs are written as several smaller pieces rather than as a
single large file. Breaking a large program into smaller files helps delineate program
modularity. If the program uses library routines, those will already be preassembled, and
assembly language source code for the libraries may not be available for purchase.

A linker allows a program to be stitched together out of several smaller pieces. The linker
operates on the object files created by the assembler and modifies the assembled code to
make the necessary links between files.

Some labels will be both defined and used in the same file. Other labels will be defined in a
single file but used elsewhere as illustrated in Figure 2.18. The place in the file where a label
is defined is known as an entry point. The place in the file where the label is used is called
an external reference.
The main job of the loader is to resolve external references based on available entry points.
As a result of the need to know how definitions and references connect, the assembler passes
to the linker not only the object file but also the symbol table.

Even if the entire symbol table is not kept for later debugging purposes, it must at least pass
the entry points. External references are identified in the object code by their relative symbol
identifiers.

The linker proceeds in two phases.

1. First, it determines the address of the start of each object file. The order in which object
files are to be loaded is given by the user, either by specifying parameters when the loader is
run or by creating a load map file that gives the order in which files are to be placed in
memory. Given the order in which files are to be placed in memory and the length of each
object file, it is easy to compute the starting address of each file.

2. At the start of the second phase, the loader merges all symbol tables from the object files
into a single, large table. It then edits the object files to change relative addresses into
addresses. This is typically performed by having the assembler write extra bits into the object
file to identify the instructions and fields that refer to labels. If a label cannot be found in the
merged symbol table, it is undefined and an error message is sent to the user.
BASIC COMPILATION TECHNIQUES:

It is useful to understand how a high-level language program is translated into instructions.


Since implementing an embedded computing system often requires controlling the instruction
sequences used to handle interrupts, placement of data and instructions in memory, and so
forth, understanding how the compiler works can help you know when you cannot rely on the
compiler.

Next, because many applications are also performance sensitive, understanding how code is
generated can help you meet your performance goals, either by writing high-level code that
gets compiled into the instructions you want or by recognizing when you must write your
own assembly code.
The compilation process is summarized in Figure 2.19. Compilation begins with high-level
language code such as C and generally produces assembly code. (Directly producing object
code simply duplicates the functions of an assembler which is a very desirable stand-alone
program to have.)

The high-level language program is parsed to break it into statements and expressions. In
addition, a symbol table is generated, which includes all the named objects in the program.
Some compilers may then perform higher-level optimizations that can be viewed as
modifying the high-level language program input without reference to instructions.
A simple code generator would generate the address for x[i] twice, once for each appearance
in the statement. The later optimization phases can recognize this as an example of common
expressions that need not be duplicated. While in this simple case it would be possible to
create a code generator that never generated the redundant expression, taking into account
every such optimization at code generation time is very difficult. We get better code and more
reliable compilers by generating simple code first and then optimizing it.
ANALYSIS AND OPTIMIZATION OF EXCUTION TIME, POWER,
NRGY , PROGRAM SIZE:

The memory footprint of a program is determined by the size of its data and instructions.
Both must be considered to minimize program size.

Data provide an excellent opportunity for minimizing size because the data are most highly
dependent on programming style. Because inefficient programs often keep several copies of
data, identifying and eliminating duplications can lead to significant memory savings usually
with little performance penalty.

Buffers should be sized carefully rather than defining a data array to a large size that the
program will never attain, determine the actual maximum amount of data held in the buffer
and allocate the array accordingly. Data can sometimes be packed, such as by storing several
flags in a single word and extracting them by using bit-level operations.
A very low-level technique for minimizing data is to reuse values. For instance, if several
constants happen to have the same value, they can be mapped to the same location. Data
buffers can often be reused at several different points in the program. This technique must be
used with extreme caution, however, since subsequent versions of the program may not use
the same values for the constants.

A more generally applicable technique is to generate data on the fly rather than store it. Of
course, the code required to generate the data takes up space in the program, but when
complex data structures are involved there may be some net space savings from using code to
generate data.

Minimizing the size of the instruction text of a program requires a mix of high-level program
transformations and careful instruction selection.

Encapsulating functions in subroutines can reduce program size when done carefully.
Because subroutines have overhead for parameter passing that is not obvious from the high-
level language code, there is a minimum-size function body for which a subroutine makes
sense.

Architectures that have variable-size instruction lengths are particularly good candidates for
careful coding to minimize program size, which may require assembly language coding of
key program segments. There may also be cases in which one or a sequence of instructions is
much smaller than alternative implementations for example, a multiply-accumulate
instruction may be both smaller and faster than separate arithmetic operations.
A very low-level technique for minimizing data is to reuse values. For instance, if several
constants happen to have the same value, they can be mapped to the same location. Data
buffers can often be reused at several different points in the program. This technique must be
used with extreme caution, however, since subsequent versions of the program may not use
the same values for the constants.

A more generally applicable technique is to generate data on the fly rather than store it. Of
course, the code required to generate the data takes up space in the program, but when
complex data structures are involved there may be some net space savings from using code to
generate data.
Minimizing the size of the instruction text of a program requires a mix of high-level program
transformations and careful instruction selection.

Encapsulating functions in subroutines can reduce program size when done carefully.
Because subroutines have overhead for parameter passing that is not obvious from the high-
level language code, there is a minimum-size function body for which a subroutine makes
sense.

Architectures that have variable-size instruction lengths are particularly good candidates for
careful coding to minimize program size, which may require assembly language coding of
key program segments. There may also be cases in which one or a sequence of instructions is
much smaller than alternative implementations for example, a multiply-accumulate
instruction may be both smaller and faster than separate arithmetic operations.
PROGRAM VALIDATION AND TESTING:

Complex systems need testing to ensure that they work as they are intended. But bugs can be
subtle, particularly in embedded systems, where specialized hardware and real-time
responsiveness make programming more challenging. Fortunately, there are many available
techniques for software testing that can help us generate a comprehensive set of tests to
ensure that our system works properly.

The first question we must ask ourselves is how much testing is enough. Clearly, we cannot
test the program for every possible combination of inputs. Because we cannot implement an
infinite number of tests, we naturally ask ourselves what a reasonable standard of
thoroughness is. One of the major contributions of software testing is to provide us with
standards of thoroughness that make sense. Following these standards does not guarantee that
we will find all bugs.
The two major types of testing strategies:

Black-box methods generate tests without looking at the internal structure of the program.

Clear-box (also known as white-box) methods generate tests based on the program structure.

Clear-Box Testing:
The control/data flow graph extracted from a program’s source code is an important tool in
developing clear-box tests for the program. To adequately test the program, we must exercise
both its control and data operations.

In order to execute and evaluate these tests, we must be able to control variables in the
program and observe the results of computations, much as in manufacturing testing. In
general, we may need to modify the program to make it more testable. By adding new inputs
and outputs, we can usually substantially reduce the effort required to find and execute the
test.
No matter what we are testing, we must accomplish the following three things in a test:

· Provide the program with inputs that exercise the test we are interested in.

· Execute the program to perform the test.

· Examine the outputs to determine whether the test was successful.

Black-Box Testing:
Black-box tests are generated without knowledge of the code being tested. When used alone,
black-box tests have a low probability of finding all the bugs in a program. But when used in
conjunction with clear-box tests they help provide a well-rounded test set, since black-box
tests are likely to uncover errors that are unlikely to be found by tests extracted from the code
structure.

Black-box tests can really work. For instance, when asked to test an instrument whose front
panel was run by a microcontroller, one acquaintance of the author used his hand to depress
all the buttons simultaneously. The front panel immediately locked up. This situation could
occur in practice if the instrument were placed face-down on a table, but discovery of this
bug would be very unlikely via clear-box tests.

One important technique is to take tests directly from the specification for the code under
design. The specification should state which outputs are expected for certain inputs. Tests
should be created that provide specified outputs and evaluate whether the results also satisfy
the inputs.
We can’t test every possible input combination, but some rules of thumb help us select
reasonable sets of inputs. When an input can range across a set of values, it is a very good
idea to test at the ends of the range. For example, if an input must be between 1 and 10, 0, 1,
10, and 11 are all important values to test. We should be sure to consider tests both within
and outside the range, such as, testing values within the range and outside the range. We may
want to consider tests well outside the valid range as well as boundary-condition tests.

Random tests form one category of black-box test. Random values are generated with a
given distribution. The expected values are computed independently of the system, and then
the test inputs are applied. A large number of tests must be applied for the results to be
statistically significant, but the tests are easy to generate. Another scenario is to test certain
types of data values. For example, integer valued inputs can be generated at interesting values
such as 0, 1, and values near the maximum end of the data range. Illegal values can be tested
as well.

Regression tests form an extremely important category of tests. When tests are created
during earlier stages in the system design or for previous versions of the system, those tests
should be saved to apply to the later versions of the system. Clearly, unless the system
specification changed, the new system should be able to pass old tests. In some cases old
bugs can creep back into systems, such as when an old version of a software module is
inadvertently installed. In other cases regression tests simply exercise the code in different
ways than would be done for the current version of the code and therefore possibly exercise
different bugs.

You might also like