Файл: Embedded Systems Design - An Introduction to Processes Tools and Techniques (A. Berger, 2002).pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 13.06.2025
Просмотров: 2114
Скачиваний: 0
Chapter 4: The Development
Environment
Overview
Modern desktop development environments use remarkably complex translation techniques. Source code is seldom translated directly into loadable binary images. Sophisticated suites of tools translate the source into relocatable modules, sometimes with and sometimes without debug and symbolic information. Complex, highly optimized linkers and loaders dynamically combine these modules and map them to specific memory locations when the application is executed.
It’s amazing that the process can seem so simple. Despite all this behindthe- scenes complexity, desktop application developers just select whether they want a free-standing executable or a DLL (Dynamic Link Library) and then click Compile. Desktop application developers seldom need to give their development tools any information about the hardware. Because the translation tools always generate code for the same, highly standardized hardware environment, the tools can be preconfigured with all they need to know about the hardware.
Embedded systems developers don’t enjoy this luxury. An embedded system runs on unique hardware, hardware that probably didn’t exist when the development tools were created. Despite processor advances, the eventual machine language is never machine independent. Thus, as part of the development effort, the embedded systems developer must direct the tools concerning how to translate the source for the specific hardware. This means embedded systems developers must know much more about their development tools and how they work than do their application-oriented counterparts.
Assumptions about the hardware are only part of what makes the application development environment easier to master. The application developer also can safely assume a consistent run-time package. Typically, the only decision an application developer makes about the run-time environment is whether to create a freestanding EXE, a DLL, or an MFC application. The embedded systems developer, by comparison, must define the entire runtime environment. At a minimum, the embedded systems developer must decide where the various components will reside (in RAM, ROM, or flash memory) and how they will be packaged and scheduled (as an ISR, part of the main thread, or a task launched by an RTOS). In smaller environments, the developer must decide which, if any, of the standard run-time features to include and whether to invent or acquire the associated code.
Thus, the embedded systems developer must understand more about the execution environment, more about the development tools, and more about the run-time package.
The Execution Environment
Although you might not need to master all of the intricacies of a given instruction set architecture to write embedded systems code, you will need to know the following:
How the system uses memory, including how the processor manages its stack
What happens at system startup
How interrupts and exceptions are handled
In the following sections, you’ll learn what you need to know about these issues to work on a typical embedded system built with a processor from the Motorola 68000 (68K) family. Although the details vary, the basic concepts are similar on all systems.
Memory Organization
The first step in coming to terms with the execution environment for a new system is to become familiar with how the system uses memory. Figure 4.1 outlines a memory map of a generic microprocessor, the Motorola 68K (Even though the original 68K design is over 20 years old, it is a good architecture to use to explain general principles).
Figure 4.1: Memory map of processor.
Memory model for a 68K family processor.
Everything to the left of I/O space could be implemented as ROM. Everything to the right of I/O space can only be implemented in RAM.
System Space
The Motorola 68K family reserves the first 1,024 memory locations (256 long words) for the exception vector tables. Exception vectors are “hardwired” addresses that the processor uses to identify which code should run when it encounters an interrupt or other exception (such as divide by zero or overflow error). Because each vector consumes four bytes (one long word) on the 68K, this system can support up to 256 different exception vectors.
Code Space
Above the system space, the code space stores the instructions. It makes sense to make the system space and the code space contiguous because you would normally place them in the same physical ROM device.
Data Space
Above the code space, the ROM data space stores constant values, such as error messages or other string literals.
Above the data space, the memory organization becomes less regular and more dependent on the hardware design constraints. Thus, the memory model of Figure 4.1 is only an example and is not meant to imply that it should be done that way. Three basic areas of read/write storage (RAM) need to be identified: stack, free memory, and heap.
The Stack
The stack is used to keep track of the current and all suspended execution contexts. Thus, the stack contains all “live” local or automatic variables and all function and interrupt “return addresses.” When a program calls a function, the address of the instruction following the call (the return address) is placed on the stack. When the called function has completed, the processor retrieves the return address from the stack and resumes execution there. A program cannot service an interrupt or make a function call unless stack space is available.
The stack is generally placed at the upper end of memory (see Figure 4.1) because the 68K family places new stack entries in decreasing memory addresses; that is, the stack grows downwards towards the heap. Placing the stack at the “right” end of RAM means that the logical bottom of the stack is at the highest possible RAM address, giving it the maximum amount of room to grow downwards.
Free Memory
All statically allocated read/write variables are assigned locations in free memory. Globals are the most common form of statically allocated variable, but C “statics” are also placed here. Any modifiable variable with global life is stored in free memory.
The Heap
All dynamically allocated (created by new or malloc()) objects and variables reside in the heap. Usually, whatever memory is "left over" after allocating stack and free memory space is assigned to the heap. The heap is usually a (sometimes complex) linked data structure managed by routines in the compiler’s run-time package.
Many embedded systems do not use a heap.
Unpopulated Memory Space
The “break” in the center of Figure 4.1 represents available address space that isn’t attached to any memory. A typical embedded system might have a few megabytes of ROM-based instruction and data and perhaps another megabyte of RAM. Because the 68K in this example can address a total of 16MB of memory, there’s a lot of empty space in the memory map.
I/O Space
The last memory component is the memory-mapped peripheral device. In Figure 4.1, these devices reside in the I/O space area. Unlike some processors, the 68K family doesn’t support a separate address space for I/O devices. Instead, they are assumed to live at various addresses in the otherwise empty memory regions between RAM and ROM. Although I’ve drawn this as a single section, you should not expect to find all memory-mapped devices at contiguous addresses. More likely, they will be scattered across various easy-to-decode addresses.
Detecting Stack Overflow
Notice that in Figure 4.1 on page 71, the arrow to the left of the stack space points into the heap space. It is common for the stack to grow down, gobbling free memory in the heap as it goes. As you know, when the stack goes too far and begins to chew up other read/write variables, or even worse, passes out of RAM
into empty space, the system crashes. Crashes in embedded systems that are not deterministic (such as a bug in the code) are extremely difficult to find. In fact, it might be years before this particular defect causes a failure.
In The Art of Embedded Systems, Jack Ganssle[1] suggests that during system development and debug, you fill the stack space with a known pattern, such as 0x5555 or 0xAA. Run the program for a while and see how much of this pattern has been overwritten by stack operations. Then, add a safety factor (2X, perhaps) to allow for unintended stack growth. The fact that available RAM memory could be an issue might have an impact on the type of programming methods you use or an influence on the hardware design.
System Startup
Understanding the layout of memory makes it easier to understand the startup sequence. This section assumes the device’s program has been loaded into the proper memory space — perhaps by “burning” it into erasable, programmable, read-only memory (EPROM) and then plugging that EPROM into the system board. Other mechanisms for getting the code into the target are discussed later.
The startup sequence has two phases: a hardware phase and a software phase. When the RESET line is activated, the processor executes the hardware phase. The primary responsibility of this part is to force the CPU to begin executing the program or some code that will transfer control to the program. The first few
instructions in the program define the software phase of the startup. The software |
|||
Y |
|||
phase is responsible for initializing core elements of the hardware and key |
|||
structures in memory. |
L |
||
F |
|||
For example, when a 68K microprocessor first comes out of RESET, it does two |
|||
M |
|||
things before executing any instructions. First, it fetches the address stored in the |
|||
4 bytes beginning at location 000000Aand copies this address into the stack pointer |
|||
(SP) register, thus establishing the bottom of the stack. It is common for this value |
|||
E |
|||
to be initialized to the top of RAM (e.g., 0XFFFFFFFE) because the stack grows |
|||
down toward memory locationT000000. Next, it fetches the address stored in the |
|||
four bytes at memory location 000004–000007 and places this 32-bit value in its program counter register. This register always points to the memory location of the next instruction to be executed. Finally, the processor fetches the instruction located at the memory address contained in the program counter register and begins executing the program.
At this point, the CPU has begun the software startup phase. The CPU is under control of the software but is probably not ready to execute the application proper. Instead, it executes a block of code that initializes various hardware resources and the data structures necessary to create a complete run-time environment. This “startup code” is described in more detail later.
Interrupt Response Cycle
Conceptually, interrupts are relatively simple: When an interrupt signal is received, the CPU “sets aside” what it is doing, executes the instructions necessary to take care of the interrupt, and then resumes its previous task. The critical element is that the CPU hardware must take care of transferring control from one task to the other and back. The developer can’t code this transfer into the normal instruction stream because there is no way to predict when the interrupt signal will be received. Although this transfer mechanism is almost the same on all architectures,
Team-Fly®
small significant differences exist among how different CPUs handle the details. The key issues to understand are:
How does the CPU know where to find the interrupt handling code?
What does it take to save and restore the “context” of the main thread?
When should interrupts be enabled?
As mentioned previously, a 68K CPU expects the first 1024 bytes of memory to hold a table of exception vectors, that is, addresses. The first of these is the address to load into SP during system RESET. The second is the address to load into the program counter register during RESET. The rest of the 254 long addresses in the exception vector table contain pointers to the starting address of exception routines, one for each kind of exception that the 68K is capable of generating or recognizing. Some of these are connected to the interrupts discussed in this section, while others are associated with other anomalies (such as an attempt to divide by zero) which may occur during normal code execution.
When a device[1] asserts an interrupt signal to the CPU (if the CPU is able to accept the interrupt), the 68K will:
Push the address of the next instruction (the return address) onto the stack.
Load the ISR address (vector) from the exception table into the program counter.
Disable interrupts.
Resume executing normal fetch–execute cycles. At this point, however, it is fetching instructions that belong to the ISR.
This response is deliberately similar to what happens when the processor executes a call or jump to subroutine (JSR) instruction. (In fact, on some CPUs, it is identical.) You can think of the interrupt response as a hardwareinvoked function call in which the address of the target function is pulled from the exception vector. To resume the main program, the programmer must terminate the ISR with a return from subroutine (RTS) instruction, just as one would return from a function. (Some machines require you to use a special return from interrupt [RTE, return from exception on the 68k] instruction.)
ISRs are discussed in more detail in the next chapter. For now, it’s enough to think of them as hardware-invoked functions. Function calls, hardware or software, are more complex to implement than indicated here.
[1]In the case of a microcontroller, an external device could be internal to the chip but exter nal to the CPU core.
Function Calls and Stack Frames
When you write a C function and assemble it, the compiler converts it to an assembly language subroutine. The name of the assembly language subroutine is just the function name preceded by an underscore character. For example, main()
becomes _main. Just as the C function main() is terminated by a return statement, the assembly language version is terminated by the assembly language equivalent: RTS.
Figure 4.2 shows two subroutines, FOO and BAR, one nested inside of the other. The main program calls subroutine FOO which then calls subroutine BAR. The compiler translates the call to BAR using the same mechanism as for the call to FOO. The automatic placing and retrieval of addresses from the stack is possible because the stack is set up as a last- in/first-out data structure. You PUSH return addresses onto the stack and then POP them from the stack to return from the function call.
Figure 4.2: Subroutines.
Schematic representation of the structure of an assembly-language subroutine.
The assembly-language subroutine is “called” with a JSR assembly language instruction. The argument of the instruction is the memory address of the start of the subroutine. When the processor executes the JSR instruction, it automatically places the address of the next instruction — that is, the address of the instruction immediately following the JSR instruction — on the processor stack. (Compare this to the interrupt response cycle discussed previously.) First the CPU decrements the SP to point to the next available stack location. (Remember that on the 68K the SP register grows downward in memory.) Then the processor writes the return address to the stack (to the address now in SP).
Hint A very instructive experiment that you should be able to perform with any embedded C compiler is to write a simple C program and compile it with a “compile only” option. This should cause the compiler to generate an assembly language listing file. If you open this assembly file in an editor, you’ll see the various C statements along with the assembly language statements that are generated. The C statements appear as comments in the assembly language source file.
Some argue that generating assembly is obsolete. Many modern compilers skip the assembly language step entirely and go from compiler directly to object code. If you want to see the assembly language output of the compiler, you set a compiler option switch that causes a disassembly of the object file to create an assembly language source file. Thus, assembly language is not part of the process.
The next instruction begins execution at the starting address of the subroutine (function). Program execution continues from this new location until the RTS instruction is encountered. The RTS instruction causes the address stored on the stack to be automatically retrieved from the stack and placed in the program counter register, where program execution now resumes from the instruction following the JSR instruction.
The stack is also used to store all of a function’s local variables and arguments. Although return addresses are managed implicitly by the hardware each time a JSR or RTS is executed, the compiler must generate explicit assembly language to manage local variable storage. Here, different compilers can choose different options. Generally, the compiler must generate code to
Push all arguments onto the stack
Call the function
Allocate storage (on the stack) for all local variables
Perform the work of the function
Deallocate the local variable storage
Return from the function
Deallocate the space used by the arguments
The collection of all space allocated for a single function call (arguments, return addresses, and local variables) is called a stack frame. To simplify access to the arguments and local variables, at each function entry, the compiler generates code that loads a pointer to the current function’s stack frame into a processor register
— typically called Frame Pointer (FP). Thus, within the assembly language subroutine, a stack frame is nothing more than a local block of RAM that must be addressed via one of the CPU’s internal address registers (FP).
A complete description of a stack frame includes more than locals, parameters, and return addresses. To simplify call nesting, the old FP is pushed onto the stack each time a function is called. Also, the "working values" in certain registers might need to be saved (also in the stack) to keep them from being overwritten by the called function. Thus, every time the compiler encounters a function call, it must potentially generate quite a bit of code (called "prolog" and "epilogue") to support creating and destroying a local stack frame. Many CPUs include special instructions designed to improve the efficiency of this process. The 68K processor, for example, includes two instructions, link and unlink (LNK and UNLNK) that were created especially to support the creation of C stack frames.
Run-Time Environment
Just as the execution environment comprises all the hardware facilities that support program execution, the run-time environment consists of all the software structures (not explicitly created by the programmer) that support program execution. Although I’ve already discussed the stack and stack frames as part of the execution environment, the structure linking stack frames also can be considered a significant part of the run-time environment. For C programmers, two other major components comprise the runtime environment: the startup code and the run-time library.
Startup Code
Startup code is the software that bridges the connection between the hardware startup phase and the program’s main(). This bridging software should be executed at each RESET and, at a minimum, should transfer control to main(). Thus, a trivial implementation might consist of an assembly language file containing the single instruction:JMP _main
To make this code execute at startup, you also need to find a way to store the address of this JMP into memory locations 000004–000007 (the exception vector for the first instruction to be executed by the processor.) I’ll explain how to accomplish that later in the section on linkers.
Typically, however, you wouldn’t want the program to jump immediately to main(). A real system, when it first starts up, will probably do some system integrity checks, such as run a ROM checksum test, run a RAM test, relocate code stored in ROM to RAM for faster access, initialize hardware registers, and set up the rest of the C environment before jumping to _main. Whereas in a desktop environment, the startup code never needs to be changed, in an embedded environment, the startup code needs to be customized for every different board. To make it easy to modify the startup behavior, most embedded market C compilers automatically generate code to include a separate assembly language file that contains the startup code. Typically, this file is named crt0 or crt1 (where crt is short for C Run Time). This convention allows the embedded developer to modify the startup code separately (usually as part of building the board support package).
Figure 4.3 shows the flowchart for the crt0 function for the HewlettPackard B3640 68K Family C Cross Compiler.
Figure 4.3: crt0 function
The crt0 program setup flowchart.[2]
Why JMP_main Was Used