Home | | Embedded Systems Design | Writing a library

Chapter: Embedded Systems Design : Writing software for embedded systems

Writing a library

For example, given that you have an M68000 compiler running on an IBM PC and that the target is an MC68040 VMEbus system, how do you modify or replace the runtime libraries? There are two generic solutions: the first is to change the hardware design so that it looks like the hardware design that is supported by the run-time libraries.

Writing a library

 

For example, given that you have an M68000 compiler running on an IBM PC and that the target is an MC68040 VMEbus system, how do you modify or replace the runtime libraries? There are two generic solutions: the first is to change the hardware design so that it looks like the hardware design that is supported by the run-time libraries. This can be quite simple and involve configuring the memory map so that memory is located at the same addresses. If I/O is used, then the peripherals must be the same — so too must their address locations to allow the software to be used without modifica-tion. The second technique is to modify the libraries so that they work with the new hardware configuration. This will involve changing the memory map, adding or changing driv-ers for new or different peripherals and in some cases even porting the software to a new processor or variant.

 

The techniques used depend on how the run-time librar-ies have been supplied. In some cases, they are supplied as assembler source modules and these can simply be modified. The module will have three sections: an entry and exit section to allow data to be passed to and from the routine and, sandwiched between them, the actual code that performs the requested command. It is this middle section that is modified.

 

Other compilers supply object libraries where the rou-tines have already been assembled into object files. These can be very difficult to patch or modify, and in such cases the best approach is to create an alternative library.

 

Creating a library

 

The first step is to establish how the compiler passes data to routines and how it expects information to be returned. This information is normally available from the documentation or can be established by generating an assembler listing during the compilation process. In extreme cases, it may be necessary to reverse engineer the procedure using a debugger. A break point is set at the start of the routine and the code examined by hand.

 

The next problem is concerned with how to tell the compiler that the routine is external and needs to be specially handled. If the routine is an addition and not a replacement for a standard function, this is normally done by declaring the routines to be external when they are defined. To complement this, the routines must each have an external declaration to allow the linker to correctly match the references.

 

With replacements for standard library functions, the external declaration from within the program source is not needed, but the one within the replacement library routine is. The alternative library is accessed first to supply the new version by setting the library search list used by the linker.

To illustrate these procedures, consider the following PASCAL example. The first piece of source code is written in PASCAL and controls a semaphore in a typical real-time operating system, which is used to synchronise other tasks. The standard PASCAL did not have any run-time support for operating system calls and therefore a library needed to be created to supply these. The data passing mechanism is typical of most high level languages, including C and FORTRAN, and the trap mechanism, using directive numbers and parameter blocks, is also common to most operating systems.

 

The PASCAL program declares the operating system calls as external procedures by defining them as procedures and marking them as FORWARD. This tells the compiler and linker that they are external references that need to be resolved at the linker stage. As part of the procedure declaration, the data types that are passed to the procedure have also been defined. This is essential to force the compiler to pass the data to the routine — without it, the information will either not be accepted or the routine will misinterpret the information. In the example, four external procedures are declared: delay, wtsem, sgsem and atsem. The procedure delay takes an integer value while the others pass over a four character string

 

— described as a packed array of char. Their operation is as follows:

 

delay delays the task by a number of milliseconds. atsem creates and attaches the task to a semaphore. wtsem causes the task to wait for a semaphore. sgsem signals the semaphore.

program timer(input,output);

 

type

 

datatype = packed array[1..4] of char;

 

var

 

msecs:integer;

name :datatype; i :integer;

 

procedure delay( msecs:integer); FORWARD;

procedure wtsem( var name:datatype); FORWARD;

procedure sgsem( var name:datatype); FORWARD;

procedure atsem( var name:datatype); FORWARD;

 

begin

 

name:= ‘1sec’;

atsem(name);

delay(10000);

sgsem(name);

 

for i := 1 to 10 do begin;

wtsem(name);

delay(10000);

sgsem(name);

end;

end.

 

The program TIMER works in this way. When it starts, it assigns the identity 1sec to the variable name. This is then used to create a semaphore called 1sec using the atsem proce-dure. The task now delays itself for 10000 milliseconds to allow a second task to load itself, attach itself to the semaphore 1sec and wait for its signal. The signal comes from the sgsem procedure on the next line. The other task receives the signal, TIMER goes into a loop where it waits for the 1sec semaphore, delays itself for 10000 milliseconds and then signals with the 1sec semaphore. The other task complements this operation by signalling and then waiting, using the 1sec semaphore.

 

The end result is that the program TIMER effectively controls and synchronises the other task through the use of the semaphore.

 

The run-time library routines for these procedures were written in MC68000 assembler. Two of the routines have been listed to illustrate how integers and arrays are passed across the stack from a high level language — PASCAL in this case — to the routine. In C, the assembler routines would be declared as functions and the assembler modules added at link time. Again, it should be remembered that this technique is common to most compilers.

 

DELAY                             IDNT   1,0

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

 

*                           ++++ ++++

 

*                           ++++ Runtime procedure call for PASCAL ++++

 

*                           ++++ ++++

 

*                           ++++ Version 1.0 ++++

 

*                           ++++ ++++

 

*                           ++++ Steve Heath - Motorola Aylesbury ++++

 

*                           ++++ ++++

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

*                           PASCAL call structure:

 

*

 

*                           procedure delay(msecs:integer);FORWARD

 

*                           This routine calls the delay directive of the OS

 

*                           and delays the task for a number of ms.

 

*                           The number is passed directly on the stack

          XDEF DELAY                       

          SECTION 9                            

DELAY      EQU  *                

          MOVE.L     (A7)+,A4    Save return address

          MOVE.L     (A7)+,A0    Load time delay into A0

          MOVE.L     A3,-(A7)     Save A3      for PASCAL

          MOVE.L     A5,-(A7)     Save A5      for PASCAL

          MOVE.L     A6,-(A7)     Save A6      for PASCAL

EXEC         MOVE.L     #21,D0       Load directive number 21

          TRAP         #1      Execute OS command   

          BNE  ERROR      Error handler if problem

POP  MOVE.L     (A7)+,A6    Restore saved values    

          MOVE.L     (A7)+,A5    Restore saved values    

          MOVE.L     (A7)+,A3    Restore saved values    

          JMP  (A4)  Jump back to PASCAL

ERROR      MOVE.L     #14,D0       Load abort directive no.

          TRAP         #1      Abort task 

 

END

 

 

 

The code is divided into four parts: the first three corre-spond with the entry, execution and exit stages previously mentioned. A fourth part that handles any error conditions has been added.

 

The routine is identified to the linker as the delay proce-dure by the XDEF delay statement. The section 9 command instructs the linker to insert this code in the program part of the file. Note how there are no absolute addresses or address references in the source. The actual values are calculated and inserted by the linker during the linking stage.

 

The next few instructions transfer the data from PAS-CAL to the assembler routine. The return address is taken from the stack followed by the time delay. These values are stored in registers A4 and A0, respectively. Note that the stack pointer A7 is incremented after the last transfer to effectively remove the passed parameters. These are not left on the stack. The next three instructions save the address registers A3, A5 and A6 onto the stack so that they are preserved. This is necessary to successfully return to PASCAL. If they are corrupted, then the return to PASCAL will either not work or will cause the program to crash at a later point. With some compilers, more registers may need saving and it is a good idea to save all registers if it is not clear which ones must be preserved. With this example, only these three are essential.

 

The next part of the code loads the directive number into the right register and executes the system call using the TRAP #1 instruction. The directive needs the delay value in A0 and this is loaded earlier from the stack.

 

If the system call fails, the condition code register is returned with a non-zero setting. This is tested by the BNE ERROR instruction. The error routine simply executes a termi-nation or abort system call to halt the task execution.

 

The final part of the code restores the three address registers and uses the return address in A4 to return to the PASCAL program. If the procedure was expecting a returned value, this would be placed on the stack using the same technique used to place the data on the stack. A common fault is to use the wrong method or fail to clear the stack of old data.

 

The next example routine executes the atsem directive which creates the semaphore. The assembler code is a little more complex because the name is passed across the stack using a pointer rather than the actual value and, secondly, a special parameter block has to be built to support the system call to the operating system.

 

ATSEM                             IDNT    1,0

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

 

*                           ++++ ++++

 

*                           ++++ Runtime procedure call for PASCAL ++++

 

*                           ++++ ++++

 

*                           ++++ Version 1.0 ++++

 

*                           ++++ ++++

 

*                           ++++ Steve Heath - Motorola Aylesbury ++++

 

*                           ++++ ++++

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

 

*                           ++++++++++++++++++++++++++++++++++++++++++++++++++

 

*         

*                                    PASCAL call structure:

 

*

 

*                                    type

 

*                                               datatype = packed array[1..4] of char

 

*

 

*                                    procedure atsem(var name:datatype);FORWARD

 

*         

*                                    This routine calls the OS and creates a

*                                    semaphore. Its name is passed across on the

*        stack using an address pointer. *                        

          XDEF ATSEM   

          SECTION 9        

DELAY      EQU           *

          MOVE.L     (A7)+,A4    Save return address

          MOVE.L     (A7)+,A0    Get pointer to the name

          LEA  PBL(PC),A1         Load the PBL address

          MOVE.L     (A0),(A1)    Move the name into PBL

          MOVE.L     A3,-(A7)     Save A3 for PASCAL

          MOVE.L     A5,-(A7)     Save A5 for PASCAL

          MOVE.L     A6,-(A7)     Save A6 for PASCAL

EXEC         MOVE.L     #21,D0       Load directive number 21

          LEA  PBL(PC),A0         Load the PBL address

          TRAP         #1      Execute OS command

          BNE  ERROR      Error handler if problem

POP  MOVE.L     (A7)+,A6    Restore saved values

          MOVE.L     (A7)+,A5    Restore saved values

          MOVE.L     (A7)+,A3    Restore saved values

          JMP  (A4)  Jump back to PASCAL

ERROR      MOVE.L   #14,D0         Load abort directive no.

          TRAP         #1      Abort task 

          SECTION 15                          

PBL   EQU  *                

          DC.L ‘ ‘      Create space for

name                             

          DC.L 0        Semaphore key   

          DC.B 0        Initial count         

          DC.B 1        Semaphore type  

 

END

Assembler listing for the atsem call

 

The name is passed via a pointer on the stack. The pointer is fetched and then used to point to the packed array that contains the semaphore name. Normally, each byte is taken in turn by using the pointer and moving it on to the next location until it points to a null character, i.e. hexadecimal 00. Instead of writing a loop to perform this task, a short cut was taken by assuming that the name is always 4 bytes and by transferring the four characters as a single 32 bit long word.

 

The address of the parameter block PBL is obtained using the PC relative addressing mode. Again, the reason for this is to allow the linker freedom to locate the parameter block wherever it wants to, without the need to specify an absolute address. The address is calculated and transferred to register A1 using the load effective address instruction, LEA.

 

The parameter block is interesting because it has been put into section 15 as opposed to the code which is located in section 9. Both of these operations are carried out by the appropriate SECTION command. The reason for this is to ensure that the routines work in all target types, irrespective of whether there is a memory management unit present or the code is in ROM. With this compiler and linker, two sections are used for any program: section 9 is used to hold the code while section 15 is used for data. Without the section 15 command, the linker would put the parameter block immediately after the code routine somewhere in section 9. With a target with no memory management, or with it disabled, this would not cause a problem — provided the code was running in RAM. If the memory management declares the program area as read only

 

— standard default for virtually all operating systems — or the code is in ROM, the transfer of the semaphore name would fail as the parameter block was located in read only memory. By forcing it into section 15, the block is located correctly in RAM and will work correctly, whatever the system configuration.

 

These routines are extremely simple and quick to create. By using a template, it is easy to modify them to create new procedure calls. More sophisticated versions could transfer all the data to build the parameter block rather than just the name, as in these examples. The procedure could even return a completion code back to the PASCAL program, if needed. In addition, register usage in these examples is not very efficient and again could be improved. However, the important point is that the amount of sophistication is dependent on what the software engineer requires.

 

Device drivers

 

This technique is not just restricted to creating run-time libraries for operating systems and replacement I/O functions. The same technique can even be used to drive peripherals or access special registers. This method creates a pseudo device driver which allows the high level language access to the lower levels of the hardware, while not going to the extreme of hard coding or in-lining assembler. If the application is moved to a different target, the pseudo device driver is changed and the application relinked with the new version.

 

Debugger supplied I/O routines

 

I/O routines which read and write data to serial ports or even sectors to and from disk can be quite time consuming to write. However, such routines already exist in the onboard debugger which is either shipped with a ready built CPU board or can be obtained for them.

 

*                            Output a character to console

 

*         

*                            The character to be output is passed to

 

*                            this routine on the stack as byte 5 with

 

*                            reference to A7.

 

*

 

*                            A TRAP #14 call to the debugger does the actual work

 

*                            Tabs are handled separately

 

putch

 

move.b 5(A7),D0 Get char from stack cmp #09,D0 Is it tab character? beq _tabput Yes,go to tab routine trap #14 Call debugger I/O dc.w 1 Output char in D0.B rts

 

An example putchar routine for C using debugger I/O

 

Many suppliers provide a list of basic I/O commands which can be accessed by the appropriate trap calls. The mechanism is very similar to that described in the previous examples: parameter block addresses are loaded into registers, the command number loaded into a data register and a trap instruction executed. The same basic technique template can be used to create replacement I/O libraries which use the debugger rather than an operating system.

Run-time libraries

 

The example assembler routines simply use the predefined stack mechanisms to transfer data to and from PASCAL. At no point does the routine actually know that the data is coming from a high level language as opposed to an assembler routine — let alone differentiate between C and PASCAL. If a group of high level languages have common transfer mechanisms, it should be possible to share libraries and modules between them, without having to modify them or know how they were generated. Unfortunately, this utopia has not quite been realised, although some standards have been put forward to implement it.

 

Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail
Embedded Systems Design : Writing software for embedded systems : Writing a library |


Privacy Policy, Terms and Conditions, DMCA Policy and Compliant

Copyright © 2018-2024 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.