Файл: Embedded Systems Design - An Introduction to Processes Tools and Techniques (A. Berger, 2002).pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 13.06.2025
Просмотров: 2125
Скачиваний: 0
You might be wondering why I used the instruction JMP_main and not the instruction JSR _main. First of all, JSR_main implies that after it’s done running main(), it returns to the calling routine. Where is the calling routine? In this case, main() is the starting and ending point. Once it is running, it runs forever. Thus, function main() might look like this pseudocode representation:
main()
{
Initialize variables and get ready to run;
While(1)
{
Rest of the program here;
}
return 0;
}
After you enter the while loop, you stay there forever. Thus, a JMP _main is as good as a JSR _main.
However, not all programs run in isolation. Just like a desktop application runs under Windows or UNIX, an embedded application can run under an embedded operating system, for example, a RTOS such as VxWorks. With an RTOS in control of your environment, a C program or task might terminate and control would have to be returned to the operating system. In this case, it is appropriate to enter the function main() with a JSR _main.
This is just one example of how the startup code might need to be adjusted for a given project.
The Run-Time Library
In the most restrictive definition, the run-time library is a set of otherwise invisible support functions that simplify code generation. For example, on a machine that doesn’t have hardware support for floating-point operations, the compiler generates a call to an arithmetic routine in the run-time library for each floatingpoint operation. On machines with awkward register structures, sometimes the compiler generates a call to a context-saving routine instead of trying to generate code that explicitly saves each register.
For this discussion, consider the routines in the C standard library to be part of the run-time library. (In fact, the compiler run-time support might be packaged in the same library module with the core standard library functions.)
The run-time library becomes an issue in embedded systems development primarily because of resource constraints. By eliminating unneeded or seldom used functions from the run-time library, you can reduce the load size of the program.
You can get similar reductions by replacing complex implementations with simple ones.
These kinds of optimizations usually affect three facilities that application programmers tend to take for granted: floating-point support, formatted output (printf()), and dynamic allocation support (malloc() and C++’s new). Typically, if one of these features has been omitted, the embedded development environment supplies some simpler, less code-intensive alternative. For example, if no floatingpoint support exists, the compiler vendor might supply a fixed-point library that you can call explicitly. Instead of full printf() support, the vendor might supply functions to format specific types (for example, printIntAsHex(), printStr(), and so on).
Dynamic allocation, however, is a little different. How, or even if, you implement dynamic allocation depends on many factors other than available code space and hardware support. If the system is running under an RTOS, the allocation system will likely be controlled by the RTOS. The developer will usually need to customize the lower level functions (such as the getmem() function discussed in the following) to adapt the RTOS to the particular memory configuration of the target system. If the system is safety critical, the allocation system must be very robust. Because allocation routines can impose significant execution overhead, processor-bound systems might need to employ special, fast algorithms.
Many systems won’t have enough RAM to support dynamic allocation. Even those that do might be better off without it. Dynamic memory allocation is not commonly used in embedded systems because of the dangers inherent in unexpectedly running out of memory due to using it up or to fragmentation issues. Moreover, algorithms based on dynamically allocated structures tend to be more difficult to test and debug than algorithms based on static structures.
Most RTOSs supply memory-management functions. However, unless your target system is a standard platform, you should plan on rewriting some of the malloc() function to customize it for your environment. At a minimum, the cross-compiler that might be used with an embedded system needs to know about the system’s memory model.
For example, the HP compiler discussed earlier isolates the system-specific information in an assembly language function called _getmem(). In the HP implementation, _getmem() returns the address of a block of memory and the size of that block. If the size of the returned block cannot meet the requested size, the biggest available block is returned. The user is responsible for modifying this getmem() according to the requirements of the particular target system. Although HP supplies a generic implementation for getmem(), you are expected to rewrite it to fit the needs and capabilities of your system.
Note You can find more information about dynamic allocation in embedded system projects in these articles:
Dailey, Aaron. “Effective C++ Memory Allocation.” Embedded Systems Programming, January 1999, 44.
Hogaboom, Richard. “Flexible Dynamic Array Allocation.” Embedded Systems Programming, December 2000, 152.
Ivanovic, Vladimir G. “Java and C++: A Language Comparison.” Real Time Computing, March 1998, 75.
Lafreniere, David. “An Efficient Dynamic Storage Allocator.” Embedded Systems Programming, September 1998, 72.
Murphy, Niall. “Safe Memory Utilization.” Embedded Systems Programming, April 2000, 110.
Shaw, Kent. “Run-Time Error Checking,” Embedded Developers Journal, May 2000, 8.
Stewart, David B. “More Pitfalls for Real-Time Software Developers.”
Embedded Systems Programming, November 1999, 74.
Object Placement
It should be clear by now that an embedded systems programmer needs to be able to control the physical position of code and data in memory. To create a table of exception vectors, for example, you must be able to create an array of ISR addresses and force it to reside at location zero. Similarly, embedded systems programmers must be able to force program instructions to reside at an address corresponding to EPROM and to force global data to reside at addresses corresponding to RAM. Startup code and ISRs pose similar challenges.
The linker is the primary tool for controlling code placement. Generally, the assembler creates relocatable modules that the linker “fixes” at specific physical addresses. The following sections explain relocatable modules and how the embedded systems programmer can exercise control over the physical placement of objects.
Relocatable Objects
Figure 4.4 represents the classical development model. The C or C++ source file and include files are compiled into an assembly language source file and then the assembler creates a relocatable object file.
Figure 4.4: Embedded software development process.
A road map for the creation and design of embedded software.
As the assembler translates the source modules, it maintains an internal counter — the location counter — to keep track of the instruction boundaries, relative to the starting address of the block.
Figure 4.5 is a snippet of 68K assembly language code. The byte count corresponding to the current value of the location counter is highlighted in Figure 4.5. The counter shows the address of the instructions in this block of code, although it could just as easily show the relative byte counts (offsets) for data blocks. In the simplest development environments, the developer uses special assembly language pseudo-instructions to place objects at particular locations (such as ORG 200H to start a module at address 512.) When working in a higherlevel language, you need some other mechanism for controlling placement.
Figure 4.5: Assembly lafnguage snippet.
In this snippet of 68K assembly-language code, the location counter is highlighted.
The solution is to have the assembler generate relocatable modules. Each relocatable module is translated as if it will reside at location zero. When the assembler prepares the module, it also prepares a symbol table showing which values in the module will need to change if the module is moved to some location other than zero. Before loading these modules for execution, the linker relocates them, that is, it adjusts all the position-sensitive values to be appropriate for where the module actually will reside. Modern instruction sets often include instructions specifically designed to simplify the linker’s job (for example, “jumprelative” instructions, which do not need adjusting). Often, the compilers and linkers for such machines can be instructed to generate position-independent code (PIC), which requires no adjustments, regardless of where the code will ultimately reside in memory.
The relocatable modules (or files) typically reference functions in other modules, so, at first glance, you have a Pandora’s box of interconnected function calls and memory references. In addition to adjusting internal references for actual location, the linker is also responsible for resolving these inter-module references and creating a block of code that can be loaded into a specific location in the system.
Advantages of Relocatable Modules
Relocatable modules are important for many reasons. For the embedded systems programmer, relocatable modules simplify the physical placement of code generated from a high-level language and allow individual modules to be independently updated and recompiled.
In general-purpose systems, relocatable modules have the added benefits of simplifying memory management (by allowing individual programs to be loaded into any available section of memory without recompilation) and facilitating the use of shared, precompiled libraries.
Using the Linker
The inputs to the linker are the relocatable object modules and the linker command file. The linker command file gives the software engineer complete control of how the code modules are linked together to create the final image. The linker command file is a key element in this process and is an important differentiator between writing code for an embedded system and a desktop PC. The linker command file is a user-created text file that tells the linker how the
relocatable object modules are to be linked together. Linkers use program sections. A program section is a block of code or data that is logically distinct from other sections and can be described by its own location counter.
Sections have various attributes that tell the linker how they are to be used. For example, a section might be:
Program code
Program data
Mixed code and data
ROMable data
Listing 4.1 shows a typical Motorola 68K family linker command file. The meanings of the linker commands are explained in Table 4.1.
Table 4.1: Linker commands.
CHIP
specifies the target microprocessor. It also determines how sections are aligned on memory address boundaries and, depending upon the microprocessor specified, how much memory space is available. Finally, it determines the behavior of certain processor-specific addressing modes.
LISTMAP
generates a symbol table listing both local and external definition symbols. It also causes these symbols to be placed in the output object module so that a debugger can associate symbols with memory addresses later on. The symbol table displays the symbols along with their final absolute address locations. You can look at the link map output and determine whether all the modules were linked properly and will reside in memory where you think they should be. If the linker was successful, all addresses are adjusted to their proper final values and all links between modules are resolved.
COMMON; named COMSEC
is placed at hexadecimal starting address 1000 ($ means hexadecimal). The linker places all COMMON sections from different modules with the same name in the same place in memory. COMMON sections are generally used for program variables that will reside in RAM, such as global variables.
ORDER
specifies the order that the sections are linked together into the executable image.
PUBLIC
specifies an absolute address, hexadecimal 2000, for the variable EXTRANEOUS. This is an interesting command, and I’ll return to it in Chapter 5, when I discuss
using “casting” to assign absolute addresses to variables, as you might do for memory-mapped hardware peripheral devices.
NAME TESTCASE
specifies the filename of the final output module.
PAGE
specifies that the next section begins on a page (256-byte) boundary. After the PAGE command is read, each subsection, or module, of the specified section is aligned on page boundaries. In this example, SECT2 will be started on the next available page boundary.
FORMAT
specifies the output file format. In this case, it is IEEE-695, an industry-standard file format. Another file format could be Motorola S-Record files. S-Records are ASCII-based files and human readable. S-Records are typically used for loading the code into a ROM programming device.
LOAD
loads the next three specified object files.
END |
Y |
|||
signifies the end of the file. |
||||
L |
||||
F |
||||
Listing 4.1: Example of a linker command file. (from Microtec Research, |
||||
[2] |
M |
|||
Inc.). |
||||
A |
||||
CHIP 68000 |
E |
|||
T |
||||
LISTMAP INTERNALS,PUBLICS,CROSSREF
COMMON COMSEC=$1000
ORDER SECT2,SECT3,COMSEC
PUBLIC EXTRANEOUS=$2000
NAME TESTCASE
PAGE SECT2
FORMAT IEEE
*Load first two modules
LOAD Lnk68ka.obj, lnk68kb.obj *Load last module
LOAD lnk68kc.obj
END
Team-Fly®
ROM Code Space as a Placeholder
Another reasonable design practice is to use the ROM code space simply as a placeholder. When the system starts up the first set of instructions, it actually moves the rest of the operational code out of ROM and relocates it into RAM. This is usually done for performance reasons because RAM is generally faster than ROM. Thus, the system might contain a single 8-bit wide ROM, which it relocates into a 32bit wide RAM space on bootup. Thus, aside from the boot-loader code, the remainder of the code is designed and linked to execute out of the RAM space at another address range in memory.
[2]Microtec Research, Inc., is now part of Mentor Graphics, Inc.
Additional Reading
Ganssle, Jack G. “Wandering Pointers, Wandering Code.” Embedded Systems Programming, November 1999, 21.
Jones, Nigel. “A ‘C’ Test: The 0x10 Best Questions for Would-be Embedded Programmers." Embedded Systems Programming, May 2000, 119.
Kernighan, Brian W. and Dennis M. Ritchie. The C Programming Language, 2nd ed. Englewood Clifs, NJ: Prentice-Hall, 1988.
Madau, Dinu. “Rules for Defensive Programming.” Embedded Systems Programming, December 1999, 24.
Murphy, Niall. “Watchdog Timers.” Embedded Systems Programming, November 2000, 112.
Saks, Dan. “Volatile Objects.” Embedded Systems Programming, September 1998, 101.
Saks, Dan. “Using const and volatile in Parameter Types." Embedded Systems Programming, September 1999, 77.
Silberschatz, Abraham, and Peter Baer Galvin. Operating System Concepts, 5th ed. Reading, MA: Addison Wesley Longman, 1998.
Simon, David E. An Embedded Software Primer. Reading, MA: AddisonWesley, 1999, 149.
Stewart, Dave. “The Twenty-Five Most Common Mistakes with RealTime Software Development.” A paper presented at the Embedded Systems Conference, San Jose, 26 September 2000.
Sumner, Scott A. “From PROM to Flash.” Embedded Systems Programming, July 2000, 75.
Summary
Because embedded systems developers must explicitly control the physical placement of code and data, they must have a more detailed understanding of the execution environment and their development tools. Developers whose prior work
has been limited to the desktop-application domain need to pay special attention to the capabilities of their linker.
Embedded systems developers also need a more detailed understanding of many system-level issues. The typical application developer can usually ignore the internal mechanisms of malloc() and free(), for example. Because embedded systems developers might need to write replacements for these services, they should become familiar with several allocation algorithms and their relative tradeoffs. Similarly, the embedded systems developer might need to understand the implications of using fixed-point arithmetic instead of floating-point arithmetic.
Finally, embedded systems developers should expect to become intimately familiar with the details of their system’s hardware and run-time initialization needs. It is impossible to write reliable startup code without understanding the proper initialization of the relevant hardware.
Although I can’t hope to explain how every kind of hardware works, I can show some of the tricks used to manipulate the hardware from C instead of assembly. The next chapter addresses this and other special techniques commonly used by embedded systems programmers.
Works Cited
1.Ganssle, Jack. The Art of Designing Embedded Systems. Boston, MA: Newnes, 2000, 61.
2.Microtec Research. Assembler/Linker/Librarian User’s Guide, from the Software Development Tools Documentation Set for the 68000 Family, Document #100113-011. Santa Clara, CA: Microtec Research, Inc., 1995, 4-1.
Chapter 5: Special Software Techniques
Chapter 4 looked at how the embedded systems software-development process differs from typical application development. This chapter introduces several programming techniques that belong in every embedded systems programmer’s toolset. The chapter begins with a discussion of how to manipulate hardware directly from C, then discusses some algorithms that aren’t seen outside the embedded domain, and closes with a pointer toward a portion of the Unified Modeling Language (UML) that has special significance for embedded systems programmers.
Manipulating the Hardware
Embedded systems programmers often need to write code that directly manipulates some peripheral device. Depending on your architecture, the device might be either port mapped or memory mapped. If your architecture supports a separate I/O address space and the device is port mapped, you have no choice but to “drop down” to assembly to perform the actual manipulation; this is because C has no intrinsic notion of “ports.” Some C compilers provide special CPU-specific intrinsic functions, which are replaced at translation time by CPU-specific assembly language operations. While still machine-specific, intrinsic functions do allow the programmer to avoid in-line assembly. Things are much simpler if the device is memory mapped.
In-line Assembly
If you only need to read or write from a particular port, in-line assembly is probably the easiest solution. In-line assembly is always extremely compiler dependent. Some vendors use a #pragma directive to escape the assembly instructions, some use special symbols such as _asm/_endasm, and some wrap the assembly in what looks like a function call.
asm( "assembly language statements go here" );
The only way to know what a particular compiler expects (or if it even allows inline assembly) is to check the compiler documentation.
Because in-line assembly is so compiler dependent, it’s a good idea to wrap all your assembly operations in separate functions and place them in a separate support file. Then, if you need to change compilers, you only need to change the assembly in one place. For example, if you needed to read and write from a device
register located at port address 0x42, you would create access functions like these: int read_reg( )
{
asm( "in acc,0x42");
}
void write_reg(int newval)
{
asm( "
mov acc,newval