Establish Exception Handlers

The next step is to initialize the CPU vector table. The vector table for the CPU is some block of memory (either at a fixed location or pointed to by a system register) that contains code or pointers to code. When an exception occurs, the running code branches to an entry in the vector table as the result of some system event. The event might be an interrupt, execution of an invalid instruction, a breakpoint trap, or an address alignment error (to name a few). The list of exceptions as well as the implementation of the vector table is different for each CPU.

Usually, the exception table takes one of two forms: a table of function pointers or a table of small blocks of code, where the intent of each small block is to perform some very basic task, then jump to some common exception handler. From the monitors point of view, there are two important things to do: first you must create the table, and second, the code must be prepared to handle the exception. MicroMonitors exception handlers will log the type of exception that occurred and then store the state of the registers at the time of the exception.

Listing 4.4: A Polled-Mode putchar () .
image from book
 int putchar(int c) {     int timeout;     for(timeout=0;timeout<MAX_WAIT;timeout++) {         if (XMIT_HOLD_EMPTY())             break;     }     if (timeout == MAX_WAIT) {         ERROR();     }     STORE_XMIT_HOLD_REG((char)c);     return((int)char); } 
image from book
 

In general, I am trying to avoid tying the discussion to any one processor; however, here I need to make some assumptions. For this discussion, I will use the PowerPC as an example processor. The PPC architecture defines the exception table as blocks of memory, with each block typically (not always) being 256 bytes. For many implementations , the exception table can be placed at alternate locations in memory. The content of a register tells the CPU where in memory to find it. I will assume the exception table is located at physical address 0x0 , with the first entry (or block) undefined and each entry after that in address increments of 0x100 (256 decimal) bytes.

Exception Handlers in ROM

The default exception table is typically found in the boot flash memory because the memory space that the boot flash memory covers must be the same space to whichthe exception table is mapped at reset. Reset is an exception itself; hence, there must be a reset exception handler in the boot flash memory. Immediately out of powerup at least a portion of the exception table is assumed to be in place. The only difference between the code in the exception table and other code in the boot flash memory is that the exception table entries are at fixed locations, not arbitrary locations that the linker resolves. Listing 4.5 is an extension of the pseudo-code shown in Listing 4.1, but now the code includes a portion of the exception table. The code includes some real PPC assembly language mnemonics plus some new directives.

Listing 4.5: Initializing Exception Vectors.
image from book
 .file       "reset.s"     .extern start, moncom     .global warmstart, moncomptr     .text     .balign 0x100 coldstart:     Initialize "something" to store away a state variable.     StateOfTarget = INITIIALIZE     JumpTo continuestart moncomptr:     .long moncom     .balign 0x100 vector_type1:     li  regX, V_TYPE1     ba  saveregs     .balign 0x100 vector_type2:     li  regX, V_TYPE2     ba  saveregs     .balign 0x100 vector_type3:     li  regX, V_TYPE3     ba  saveregs     /* More exception handlers would be here */ warmstart:     /* Load into StateOfTarget the parameter passed to warmstart      * as if it was the C function:       * warmstart(unsigned long state).      */ continuestart:     Disable interrupts     Flush/invalidate/disable cache     Adjust boot device access     Adjust system ram access     Establish the stack pointer     JumpTo start() 
image from book
 

The strategy in Listing 4.5 is quite simple. Each offset of 0x100 hexadecimal (established with the .balign directive) contains the code that makes the exception handler somewhat unique. The uniqueness is simply that the regX [3] register contains a tag that tells a later function what exception occurred. Each exception handler logs the exception (in reg X ) and then branches to saveregs() to store the context (or register set) of the CPU at the time of the exception. After servicing the exception, the completed monitor either restarts the application or simply returns to the monitors command line interface (CLI). To keep the register save somewhat generic across multiple target platforms, my implementation constructs an initialized table of register name strings in C, as shown in Listing 4.6.

Listing 4.6: Naming Registers.
image from book
 char  *regnames[] = {     "RA", "RB", "RC", "RD", "RE", "RF", "RG", "RH",     "RI", "RJ", "RK", "RL", "RM", "RN", "RO", "RP", }; #define REGTOT  (sizeof regnames/sizeof(char *)) ulong regtbl[REGTOT]; 
image from book
 

The list of names varies from one CPU to the next. Even within one CPU family, the register sets vary. (I am using a dummy set of register names to stay somewhat general here.) Along with this array of strings, I allocate a table of unsigned long values whose size equals the number of pointers in the regnames[] array just above it, which creates a table of strings and a table of potential values. All that remains is to have the exception handler properly put the content of each register into the appropriate location in the table. For example, register RA would be stored at offset , RB would be at offset 1 , and so forth. (Note that this code assumes that a register is the size of an unsigned long variable .) Listing 4.7 is the PowerPC assembly language code that uses these tables to save the PowerPC registers.

Listing 4.7: Saving Registers.
image from book
 /* saveregs:  *  Save register set into regtbl[] array.  */ saveregs:     lis    regY,(regtbl)@ha     addi   regY,regY,(regtbl)@l     stw    rA,0(regY)     stw    rB,4(regY)     stw    rC,8(regY)     stw    rD,12(regY)     stw    rE,16(regY)     stw    rF,20(regY)     stw    rG,24(regY)     stw    rH,28(regY)     stw    rI,32(regY)     stw    rJ,36(regY)     stw    rK,40(regY)     stw    rL,44(regY)     stw    rM,48(regY)     stw    rN,52(regY)     stw    rO,56(regY)     stw    rP,60(regY)          mr     rC,regX     /* parameter to exception (type) */     lis    rP,(exception)@ha     addi   rP,rP,(exception)@l     mtctr  rP     bctr 
image from book
 

This code uses register regY as a pointer to the regtbl array. As each register is stored, the offset relative to regY is increased by four (the byte size of a 32-bit value). After the registers have been stored, the value of regX is moved to the register that is used by the compiler as the first function parameter register. (For this example, I assume that the first function parameter register is rC .) The next function called ( exception ) receives the exception type as its argument and, based on that type information, can perform exception-specific things.

Listing 4.8: Exception-Specific Processing Prior to Reset.
image from book
 void exception(int type) {     switch (type) {         case V_TYPE1             break;          case V_TYPE2             break;          case V_TYPE3             break;          default:             break;     }     ExceptionType = type;     ExceptionAddr = getreg("SRR");     warmstart(EXCEPTION); } 
image from book
 

The final stage of the exception handler is to call warmstart() with the EXCEPTION argument. Recall that the warmstart tag is almost the same as a reset (or coldstart ). The warmstart call causes the target to go back through the startup sequence but does not cause the BSS space to be reinitialized. The start() function is now called with the StateOfMonitor set to EXCEPTION . (Note that because the state is not INITIALIZE , the code in start() does not re-initialize BSS space). The function main() is then called, and the monitor can switch on the exception type to decide what to do next.

Listing 4.9: MicroMonitors main() .
image from book
 void main(int argc,char *argv[]) {     if (StateOfMonitor == INITIALIZE) {         /* Do some higher level initialization here */     }     switch(StateOfMonitor) {         case INITIALIZE:             break;         case APP_EXIT:             reinit();             printf("\nApplication Exit Status: %d (0x%x)\n",             AppExitStatus,AppExitStatus);             break;         case EXCEPTION:             reinit();             printf("\nEXCEPTION (offset 0x%x) occurred near 0x%lx\n\n",             ExceptionType,ExceptionAddr);             showregs();             exceptionAutoRestart(INITIALIZE);             break;         default:             printf("Unexpected monitor state: 0x%x\n",StateOfMonitor);             break;     }       /* Enter the endless loop of command processing: */     CommandLoop(); } 
image from book
 

In Listing 4.9, the StateOfMonitor variable contains EXCEPTION , so main() can execute based on that. Typically, if the state is anything other than INITIALIZE , the system is only partially restarted; some state is left uninitialized with the assumption that it was initialized already. For example, as you will see later, part of the monitor is a flash file system called tiny file system (TFS). One of the startup procedures when StateOfMonitor is INITIALIZE is to scan through all files in TFS and execute auto-bootable files. These files would not be executed for the EXCEPTION case. The EXCEPTION case does some reinitialization, prints the exception type and the register set, and then automatically restarts the application. The function exceptionAutoRestart() looks at some other environment variables to decide whether it should optionally run a script out of TFS and also optionally restart the application (or just return to the monitor CLI).

One final note regarding this exception handler mechanism. After the registers are stored in the regtbl[] array, the content of that array is coordinated with the content of the regnames[] array. You can, therefore, easily build a function that can retrieve the content of any of the registers based on the register name. The getreg() function (see Listing 4.10) illustrates this mechanism.

Listing 4.10: getreg() .
image from book
 int getreg(char *name, ulong *value) {     int     i;     char    *p;     p = name;     while(*p) {         *p = toupper(*p);         p++;     }     if (!strcmp(name,"SP")) {         name="R1";     }     for(i=0;i<REGTOT;i++) {         if (!strcmp(name,regnames[i])) {             *value = regtbl[i];             return(0);         }     }     printf("getreg(%s) failed\n",name);     return(-1); } 
image from book
 

The getreg() function, which takes as input a name of a register and an unsigned long pointer, simply steps through the regnames[] table until it finds the requested name. The matching index is used as an offset into the regtbl[] array. The indexed value is returned through the long pointer argument.

Exception Handlers in RAM

This implementation puts all of the exception handlers in ROM, or non-volatile space. This policy is convenient for the reset handler because at reset you need that exception handler to be in place. However, the other handlers are only needed if their corresponding exception occurs. There are many situations where it would be advantageous for these other vectors to be in writable memory space. After the monitor is up and an application is placed on top of it, that application may have other ideas about handling exceptions, so it would be nice if the exception table could be modified. Also, the memory space for the exception handling table can exceed 8K on some CPUs, which might be excessive in flash-limited systems.

If the vector table is to eventually reside in RAM, it must be written as 100% relocatable code, because the vector table is copied from flash memory to RAM.) Converting the vector table to relocatable code is actually quite simple with a minor modification to the previous exception handler (see Listing 4.11).

Notice that now, instead of using a PC-relative branch instruction, this version uses an indirect branch (the equivalent of calling through a pointer to a function). This change makes the branch operation relocatable. So now all I have to do is copy this branch code into the RAM space that will ultimately be the exception table (see Listing 4.12).

Listing 4.11: Installing the Vector Table.
image from book
 /* This function is copied into the vector table DRAM by vinit().  * Multiple copies of general_vector are made, with the value   * loaded into regX being modified for each vector.  */ general_vector:     li     regX,0x1234     /* '0x1234' is modified by copyGeneralVector(). */     lis    regZ,(saveregs)@ha     addi   regZ,regZ,(saveregs)@l     mtctr  regZ     bctr 
image from book
 
Listing 4.12: Copying to RAM.
image from book
 void copyGeneralVector(ushort *to, ushort vid) {     extern  ulong general_vector;     memcpy((char *)to,(char *)&general_vector,20);     (to+1) = vid;           /* Modify the vector ID value */ } void vinit() {     char    *base;     int offset;     base = RAM_VTABLE;     for(offset = 0x00; offset < 0x1000; offset += 0x100) {         copyGeneralVector((ushort *)(base + offset),offset);     }     asm("   sync");         /* Wait for writes to complete */     putevpr(RAM_VTABLE);    /* Set vector table pointer to RAM */ } 
image from book
 

The size of the small assemblly language copy function is 20 bytes (five instructions). It simply copies the branch code (as if it were data space) to the new RAM-based vector table. Note that I must be aware of cache configuration at this point. Both instruction and data cache should be disabled. This approach uses only 20 bytes of flash memory to create a vector table that could exceed 8K.

Registers Beware!

Finally, it is very important to be aware of what registers are being used in exception handlers. Each implementation is a bit different but, generally, at least one register is available for exception processing only. You can assume that register is not used by any other portion of the application. In this case, for MicroMonitor, the exception handlers job is simply to log the exception type and store away all registers. The code does assume that a few registers are available for use in the exception handling process. You must investigate this assumption; details vary from one CPU/RTOS/compiler combination to another. Generally , instead of caching registers to a global table, a real exception handler (i.e., one associated with a device driver) pushes all the affected registers onto the stack of the interrupted task.

[3] Although I am using a PPC as a basic example, I am not using PPC register names.



Embedded Systems Firmware Demystified
Embedded Systems Firmware Demystified (With CD-ROM)
ISBN: 1578200997
EAN: 2147483647
Year: 2002
Pages: 118
Authors: Ed Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net