Aerospace



Citadel Forums

Home

Company Information

Information Request

Linux How-to Guides

ADSP 21xx
Digital Signal Processing
Tutorials

SW Utilities

On-line Order Form

Server Support


bonk

Have you found this site useful? Did we save you time? Did we cure your head-ache? Is your hair growing back now?

Please make a donation to help with maintenance.


Objective Real-Time Software on the ADSP21XX

Interrupt Service Routines and Reset Initialization

General

Interrupt Service Routines (ISR) are nearly synonimous with real-time systems, as an interrupt is the preferred way for a processor to respond to real world stimuli. The ADSP21XX family architecture provides a few helpful features, to achieve very fast ISR context switching, but one needs to be aware of the limitations thereof. In this chapter, we shall create a couple of useful macros, to alleviate some of these limitations. As the initialization of the processor is intimately tied to the reset interrupt, I included a short discussion of efficient processor initialization.

Reaction Time

Reaction time is an important parameter in most real-time systems and in some systems, such as frequency hopping spread spectrum radios, the amount of interrupt response jitter may also be important. If the processor has only one interrupt enabled, the response time to an interrupt would typically be a maximum of one instruction. A problem arises when there are multiple interrupts enabled. The ADSP 21XX family has some special features that apply to non-nested interrupt mode and if this is the mode being used, then any new interrupt would be delayed by the current interrupt being executed. Therefore, when using non-nested mode, it is important to keep all ISRs as short as possible.

Reaction Time

A good ISR should not exceed about 10 lines of code in length

When writing in a high level language, a good ISR should not exceed about 10 lines of code in length. In assembler, let us just say that the routine should be as short as possible.

In essence, a data receive ISR should only grab the data and plonk it into a buffer. When the buffer nears the halfway full mark, it should set a flag to signal to a foreground process to go and do something with the data. The ISR itself should do as little work as possible. The circular buffering abilities of the DAG registers were specially created for this purpose - use them!

Producer/Consumer

The circular buffering abilities of the DAG registers were specially created for this purpose - use them!

Jitter arises due to the random nature of the interrupts. It means that in non-nested mode, any interrupt can be delayed by any other interrupt and in nested mode when interrupts are prioritized, a low level interrupt can be delayed by any higher level interrupt activity. The result is that the interrupt response time can vary wildly, evident as jitter in the timing of an output control signal. If you need to minimize interrupt response jitter, you need to make all interrupts as short as possible, allow interrupts to be nested and make the sensitive interrupt the highest priority. Thereby the sensitive interrupt will be able to interrupt the other interrupts and the jitter will be a maximum of one instruction, provided that interrupts are not disabled anywhere in the code!

Nesting

The ADSP21XX provides a set of shadow registers for most frequently used registers, which can enable very fast ISR context switching, provided that the non-nested interrupt mode is used. When interrupts are nested, shadowing cannot be used, since a higher priority interrupt can overwrite the shadow registers, which can cause the context of a lower priority interrupt to get lost. Therefore all registers would need to be laboriously saved on the stack. Due to the architectural advantage of non-nested mode, the use of nested interrupts is discouraged. (If you really want to use nested mode, the trick is to remember to save the m7 register also...).

It is important to note that the Data Address Generator (DAG) registers are NOT shadowed.

Shadow Play

Shadowing is only useful in Non-Nested Mode.

The DAG registers are NOT shadowed!

This is good motivation to allocate the DAG registers in a certain way during the design phase and not to change it ever, during program execution. Thereby, one can minimize the number of DAG registers that are required to be saved/restored upon ISR entry/exit.

Here is a set of macros that can be used to shadow the DAG registers:

ISR Macros
/**********************************************************************
*
* Name:        isr_loc.h
*
* Description: ISR Macros and Shadow Variables
*
*
* Copyright (c) Aerospace Software Ltd., 1996.
*
* Revisions:
* --------------------------------------------------------------------
* 1         01/02/96    Herman Oosthuysen
*
**********************************************************************/

/********************* Resolve PUBLIC Definitions *******************/
#undef PUBLIC
#undef PROTOTYP
#ifdef ISR
#define PUBLIC    GLOBAL
#define PROTOTYP  ENTRY
#else
#define PUBLIC    EXTERNAL
#define PROTOTYP  EXTERNAL
#endif

/************************ Macros and Literals ************************/
/* 
 * Macro:         ISR_ENTER
 * Description:   Shadow the index registers in the ISRs
 * Constraints:   Optimise these macros, by deleting i0 till i5
 *                if not used.
 */
#define ISR_ENTER \
   dm(isr_i0) = i0; \
   dm(isr_m0) = m0; \
   dm(isr_l0) = l0; \
   dm(isr_i1) = i1; \
   dm(isr_m1) = m1; \
   dm(isr_l1) = l1; \
   dm(isr_i2) = i2; \
   dm(isr_m2) = m2; \
   dm(isr_l2) = l2; \
   dm(isr_i3) = i3; \
   dm(isr_m3) = m3; \
   dm(isr_l3) = l3; \
   dm(isr_i4) = i4; \
   dm(isr_m4) = m4; \
   dm(isr_l4) = l4; \
   dm(isr_i5) = i5; \
   dm(isr_m5) = m5; \
   dm(isr_l5) = l5; \
   dm(isr_i6) = i6; \
   dm(isr_m6) = m6; \
   dm(isr_l6) = l6; \
   dm(isr_i7) = i7; \
   dm(isr_m7) = m7; \
   dm(isr_l7) = l7; 
   

/*
 * Macro:         ISR_EXIT
 * Description:   Restore the index registers in the ISRs
 *                from the shadow variables.
 * Constraints:   Optimise these macros, by deleting i0 till i5
 *                if not used.
 */
#define ISR_EXIT \
   i0 = dm(isr_i0); \
   m0 = dm(isr_m0); \
   l0 = dm(isr_l0); \
   i1 = dm(isr_i1); \
   m1 = dm(isr_m1); \
   l1 = dm(isr_l1); \
   i2 = dm(isr_i2); \
   m2 = dm(isr_m2); \
   l2 = dm(isr_l2); \
   i3 = dm(isr_i3); \
   m3 = dm(isr_m3); \
   l3 = dm(isr_l3); \
   i4 = dm(isr_i4); \
   m4 = dm(isr_m4); \
   l4 = dm(isr_l4); \
   i5 = dm(isr_i5); \
   m5 = dm(isr_m5); \
   l5 = dm(isr_l5); \
   i6 = dm(isr_i6); \
   m6 = dm(isr_m6); \
   l6 = dm(isr_l6); \
   i7 = dm(isr_i7); \
   m7 = dm(isr_m7); \
   l7 = dm(isr_l7); 
   
/******************************* Variables ***************************/
#ifdef ISR
/*
 * ISR DAG Shadow Variables
 */
.VAR/DM/RAM/SEG=INT_DM
   isr_i0,
   isr_m0,
   isr_l0,
   isr_i1,
   isr_m1,
   isr_l1,
   isr_i2,
   isr_m2,
   isr_l2,
   isr_i3,
   isr_m3,
   isr_l3,
   isr_i4,
   isr_m4,
   isr_l4,
   isr_i5,
   isr_m5,
   isr_l5,
   isr_i6,
   isr_m6,
   isr_l6,
   isr_i7,
   isr_m7,
   isr_l7;
#endif

/******************************** PUBLICs ***************************/
/*
 * ISR DAG Shadow Variables
 */
.PUBLIC
   isr_i0,
   isr_m0,
   isr_l0,
   isr_i1,
   isr_m1,
   isr_l1,
   isr_i2,
   isr_m2,
   isr_l2,
   isr_i3,
   isr_m3,
   isr_l3,
   isr_i4,
   isr_m4,
   isr_l4,
   isr_i5,
   isr_m5,
   isr_l5,
   isr_i6,
   isr_m6,
   isr_l6,
   isr_i7,
   isr_m7,
   isr_l7;


/************************** EOF - isr_loc.h ************************/

These macros can be used the same way as the procedure macros discussed elsewhere in this book, but remember to end the ISR with a rti instruction and not a rts!

This is however not the full story. Firstly, the macros should be optimized by deleting all references to index registers that need not be saved. This is simple, since all DAGs that are not used inside the ISRs, need not be saved, but remember to inspect whatever macros you use inside the ISRs, for index register usage, before you start to prune the lists. Secondly, there is some initialization to be done.

Interrupt Vector Table

Before we look at the interrupt vectors, bear in mind that with this processor family, interrupts are enabled after a hardware reset. This is a bit weird, since with all other processors I know, interrupts are disabled after reset, this being the safest position. In this case, the only thing saving you from a startup disaster, is the interrupt mask register. Therefore, it is a good idea to make a dis ints your very first instruction. This has the added benefit that you can perform a warm boot, by executing a jump to the reset vector and be assured that interrupts will be disabled immediately.

The interrupt vectors differ depending upon the exact processor type used. The following example is based upon the 2185. Please consult a data book on the specific processor you want to use. The one interrupt which is always the same though, is the reset interrupt, which is at address zero. The interrupts are spaced four addresses apart. This allows one to write a few instructions right in the vector table.

Here is an example of the 2185 interrupt vector table. Note the ENA SEC_REG instruction used to switch to the shadow registers upon timer interrupt entry. The processor will automatically switch back to the regular registers upon execution of rti at the end of the ISR.

Interrupt Vector Table
/**********************************************************************
*
* Name:        ini.dsp
*
* Description: Initialization code and LED flasher
*
* Copyright (c) Aerospace Software Ltd. 1998.
*
* Revisions:
* --------------------------------------------------------------------
* 1.0       April 98    Herman Oosthuysen
*
**********************************************************************/
.MODULE/RAM/ABS=0 ini;

/* Resolve public definitions */
#define INI

#include "inc\mac_pub.h"
#include "inc\reg_pub.h"
#include "inc\bug_pub.h"
#include "inc\ser_pub.h"

#include "inc\ini_pub.h"
#include "inc\ini_loc.h"

#undef   INI

/*********************** Interrupt Vector Table **********************/

ini_reset:
   dis ints;
   jump ini_start;
   rti; rti;

ini_irq2:
   rti; rti; rti; rti;

ini_irql1:
   rti; rti; rti; rti;

ini_irql0:
   rti; rti; rti; rti;

ini_sport0tx:
   rti; rti; rti; rti;

ini_sport0rx:
   rti; rti; rti; rti;

ini_irqe:
   rti; rti; rti; rti;

ini_bdma:
   rti; rti; rti; rti;

ini_sport1tx:
   rti; rti; rti; rti;

ini_sport1rx:
   rti; rti; rti; rti;

ini_timer:
   ENA SEC_REG;
   jump isr_timer;
   rti; rti;

ini_pwr_dn:
   rti; rti; rti; rti;


After defining the vector table, we have to initialize the processor registers, create a parameter stack and then we are up and going.

The Reset Interrupt

The processor can vector to the reset interrupt following a power up reset, BDMA boot reset, or because of a warm boot executed by a fatal error handler. Nowadays, people prefer to call error handlers exception handlers - same thing, but four more characters to type, hence my aversion to the name.

Let us continue with file ini.dsp which resides in the work\dbugger directory:

/**********************************************************************
* Initialization Start-up Code
***********************************************************************/

/*********************************************************************
* Name:           ini_start
* Description:    Initialization code
* Constraints:    none
*
*********************************************************************/
ini_start:
   icntl = INI_ICNTL;            /* nesting is disabled, edge sensitive */

   pmovlay = 0;                  /* Internal memory only */
   dmovlay = 0;

   /*
    * All buffers should be linear, unless explicitly set otherwise
    * Zero ALL the buffer length registers
    */
   l0 = 0;
   l1 = 0;
   l2 = 0;
   l3 = 0;
   l4 = 0;
   l5 = 0;
   l6 = 0;
   l7 = 0;

   /*
    * Create a Procedure Call Stack using DAG 7
    * The Frame Pointer is DAG 6
    */
   i7 = ^ini_stack;                 
   m7 = %ini_stack;
   modify(i7, m7);

   /*
    * Register initialization
    * The top 20H DM addresses are memory mapped registers
    * Registers DM 3FE0H..3FFFH
    * Retrieve register information from the INI_REG_TBL object in PM
    */
   i4 = ^INI_REG_TBL;         /* get the table address */
   m4 = 1;
   m5 = 0;
   modify(i4, m5);            
   m4 = 1;
   cntr = INI_REG_MAX;
   do ini_reg_init until ce;
      ar = pm(i4, m4);        /* get the register address */
      i5 = ar;
      modify(i5, m5);
      ar = pm(i4, m4);        /* get the register data */
      dm(i5, m5) = ar;        /* set the register */
ini_reg_init: nop;
      

   /*
    * Initialize Data Memory
    * Zero all of DM
    * (Beware of the memory mapped registers!)
    */
   i0 = INI_INT_DM;                 /* data segment in DM */
   m0 = 1;                          /* linear, increment 1 */
   cntr = INI_TOP_OF_MEM - INI_INT_DM;
   ar = 0;                          /* zero the data space */
   do ini_zero_data until ce;
      dm(i0, m0) = ar;
ini_zero_data: nop;


   /*
    * Copy segment INT_CONST to segment INT_INIT
    */
   i0 = INI_INT_INIT;               /* Initialized data segment in DM */
   i4 = INI_INT_CONST;              /* Constant segment in PM */
   m4 = 1;                          /* Linear, increment 1 */
   cntr = INI_TOP_OF_MEM - INI_INT_CONST;
   do ini_data until ce;
      ar = pm(i4, m4);
      dm(i0, m0) = ar;              /* copy PM to DM */
ini_data:   nop;


   /*
    * Run the dbugger main menu at start-up
    */
   call bug_cmd;

   /*
    * LED Flasher Loop
    * Asymetrical On/Off to show intelligent behaviour
    */
ini_led_reset:
   dis ints;

   ax0 = 0x7F57;              /* out */
   dm(PFTYPE) = ax0;

   ax0 = 0x0010;              /* on */
   dm(PFDATA) = ax0;

   cntr = 0x3FFF;
   do ini_wait1 until ce;
      cntr = 0x0040;
      do ini_wait3 until ce;
         nop;
ini_wait3:  nop;
ini_wait1:  nop;

   ax0 = 0x0000;              /* off */
   dm(PFDATA) = ax0;

   cntr = 0x3FFF;
   do ini_wait2 until ce;
      cntr = 0x0400;
      do ini_wait4 until ce;
         nop;
ini_wait4:  nop;
ini_wait2:  nop;

   jump ini_led_reset;



.ENDMOD;

First of all, we explicitly zero all the buffer length registers. This ensures that all DAG registers will behave linearly, unless otherwise initialized somewhere. This is important, since their startup values are undefined. Then, we define the user stack, object which is simply a reserved block of memory defined as an array.

An Initialization Object

The memory mapped registers are initialized from a table object, using a loop. This ensures that we can easily change the contents of any register in one convenient place. This is an example of a two dimensional table object, handled in a linear fashion. In a high level language, the table would have been defined as a structure, something like this:

Register Initialization Object in C
typedef struct
{
   unsigned int address;
   unsigned int data;
}INI_TABLE_TYP;

INI_TABLE_TYP   INI_TABLE[0x0020] = 
{
   address1, data1,
   address2, data2,
   etc...
};

In the assembly language loop, since we have to alternately read the address, then the value, then the next address and so on, we still need only increment the DAG pointer by one, for each access, which makes handling this two dimensional object very simple indeed.

Note the multiply by 0x0100 in the table definition in file ini_loc.h. Since this table is saved in 24 bit wide PM, all 16 bit data values need to be shifted left by 8 bits.

Register Initialization Object in Assembler
/****************************** Literals *****************************/
/*
 * Memory Map
 * This has to correspond with the architecture file
 */
#define  INI_INT_DM              0x0000
#define  INI_INT_INIT            0x2000
#define  INI_INT_CONST           0x2000
#define  INI_TOP_OF_MEM          0x2AAA

#define  INI_STACK_SIZE          0x0100

#define  INI_ICNTL               0x0006

/*
 * Register Initialization data
 */
#define INI_WAIT_STATE_CNTRL     0x7FFF
#define INI_SP0_CNTRL            0x0000
#define INI_TSCALE               0x00FF
#define INI_TCOUNT               0x00FF
#define INI_TPERIOD              0x007F
#define INI_PFDATA               0x00FF
#define INI_PFTYPE               0x75F7
#define INI_SYSTEM_CNTRL         0x0000
#define INI_REG_MAX              0x0008
#define INI_REG_WIDTH            0x0002

/**************************** Constants  *****************************/
#ifdef INI
/*
 * Register Initialization Table
 * PM is 24 bit, USB 16 bit is used, LSB is zero, therefore * 0x100
 * 2 dimensional table
 */
.VAR/PM/RAM/seg=INT_PM  INI_REG_TBL[(INI_REG_MAX * INI_REG_WIDTH)];

/*
 *       Name           Register                  Data
 */
.INIT    INI_REG_TBL:   WAIT_STATE_CNTRL * 0x100, INI_WAIT_STATE_CNTRL * 0x100,
                        SP0_CNTRL * 0x100,        INI_SP0_CNTRL * 0x100,
                        TSCALE * 0x100,           INI_TSCALE * 0x100,
                        TCOUNT * 0x100,           INI_TCOUNT * 0x100,
                        TPERIOD * 0x100,          INI_TPERIOD * 0x100,
                        PFDATA * 0x100,           INI_PFDATA * 0x100,
                        PFTYPE * 0x100,           INI_PFTYPE * 0x100,
                        SYSTEM_CNTRL * 0x100,     INI_SYSTEM_CNTRL * 0x100;
#endif

.PUBLIC  INI_REG_TBL;

Finally, we zero the data memory, but take care not to overwrite all the meticulously initialized memory mapped registers again and then copy the initialization data to the initialized variables.

Saving initialized variables in PM may seem wasteful, but usually there are very few such variables and it allows us to avoid the added complexity of handling DM with our BDMA loader in addition to the PM. It is a tradeoff, but you would have to have a very large amount of initialization data, before it would be worth while creating a more complex loader. Since loaders are hard to debug, I prefer to keep them as simple as possible.

That's it! Your DSP is ready to go!

Hmm, remember to enable interrupts after initialization...



Copyright © 1996-2008, Aerospace Software Ltd., GPL.