[Back] [Top] [Next]

7 Accessing External Memory Mapped

Peripherals

Commonly, extra IO ports are added to 8051s to compensate for the loss of Ports 0 and 2. This is normally done by making the additional device(s) appear to be just external RAM bytes. Thus they are addressed by the MOVX A,@DPTR instruction. Typically UARTS, additional ports and real time clock devices are added to 8031s as xdata-mapped devices.

The simplest approach to adding external devices is to attach the /RD and or /WR lines to the device. Provided that only one device is present and that it only has one register, no address decoding is necessary. To access this device from C simply prefix an appropriately named variable with "xdata". This will cause the compiler to use MOVX A,@DTPR instructions when getting data in or out. In actual fact the linker will try to allocate a real address to this but, as no decoding is present, the device will simply be enabled by /WR or /RD.

In practice life is rarely this simple. Usually a mixture of RAM, UARTS, ports, EEPROM and other devices may all be attached to the 8031 by being mapped into the xdata space. Some sort of decoding is provided by discrete logic or (more usually) a PAL.

Here the various registers of the different devices will appear at fixed locations in the xdata space. With normal on-chip resources the simple "data book" name can be used to access them, so ideally these external devices should be the same.

There are three basic approaches to this:

  1. Use normal variables, char, ints etc, located by the linker
  2. Use pointers and offsets, either via the XBYTE macros or directly with user-defined pointers.
  3. Use the _At_ and _ORDER directives.

In detail, these may be implemented as shown in the following sections.

7.1 The XBYTE And XWORD Macros

To allow memory-mapped devices to be accessed from C, a method is required to effectively force pointers to point to fixed addresses. C51 provides many methods of achieving this, the simplest of which are the XBYTE[addr16] and XWORD[addr16] macros

For instance:

The byte wide PORT8_DDI register of a memory mapped IO device is at 8000H. To access it from C it must be declared thus:

    #include "absacc.h";   /*Contains macro definitions */
    #define port8_ddi   XBYTE[0x8000]
    #define port8_data  XBYTE[0x8001]

To use it then,

    port8_ddi = 0xFF       ;
    input_val = port8_data ;

To access a word at an even external address:

    #define word_reg XWORD[0x4000] 
    /* gives a word variable at 8000H */

Ignoring the pre-defined XWORD macro, the equivalent C line is:

    #define word_reg_ptr ((unsigned int *) 0x24000L)
    /*creates a pointer to a word (int) at address 8000H*/

To use this address then,

    *word_reg_ptr = 0xFFFF ;

Note that the address 8000H corresponds to 4000H words, hence the " 0x24000L ".

Here are some examples with the code produced:

#define XBYTE ((unsigned char volatile *) 0x20000L) 
#define XWORD ((unsigned int volatile *) 0x20000L)

main() {

char x ;
 int y ;

x = XBYTE[0x8000]       ;

0000 908000        MOV     DPTR,#08000H
0003 E0            MOVX    A,@DPTR
0004 FF            MOV     R7,A
0005 8F00    R     MOV     x,R7

y = XWORD[0x8000/sizeof(int)] ;
} 
0007 908000        MOV     DPTR,#08000H
000A E0            MOVX    A,@DPTR
000B FE            MOV     R6,A
000C A3            INC     DPTR
000D E0            MOVX    A,@DPTR
000E FF            MOV     R7,A
000F 8E00    R     MOV     y,R6
0011 8F00    R     MOV     y+01H,R7
}
0013         ?C0001: 
0013 22            RET     

However the address indicated by "word_reg" is fixed and can only be defined at compile time, as the contents of the square brackets may only be a constant. Any alteration to the indicated address is not possible with these macro-based methods. This approach is therefore best suited to addressing locations that are fixed in hardware and unlikely to change at run time.

Note the use of the volatile storage class modifier. This is essential to prevent the optimiser removing data reads from external ports.

See section 7.4 for more details.

Note: the header file "absacc.h" must be included at the top of the source file as shown above. This contains the prototype for the XBYTE macro. (see page 9-15 in the C51 manual)

7.2 Initialised XDATA Pointers

In many cases the external address to be pointed at is known at compile time but may need to be altered at some point during execution. Thus some method of making a pointer point at an intial specific external address is required.

Probably the simplest way of setting up such a pointer is to let the C_INIT program set the pointer to a location. However the initial address must be known at compile time. If the pointer is to be altered at run time, just equate it (without the "*" at run time) to the new address.

Note: this automatic initialisation was not supported on earlier versions of C51.

Simply do:


/* Spaced pointer */

  xdata char xdata *a_ptr = 0x8000 ;

/* Generic Pointer */

  xdata char *a_ptr = 0x028000L ;

Here the pointer is setup to point at xdata address 0x8000. Note that the spaced *a_ptr can only point at xdata locations as a result of the second xdata used in its declaration. In the generic *a_ptr case, the "02" tells C51 that an xdata address is intended.

An example might be:

   6             xdata char xdata *ptr = 0x8000 ;
   7          
   8          
   9             main() {
  11   1         char x ;
  13   1         ptr += 0xf0 ;

0000 900000  R     MOV     DPTR,#ptr+01H
0003 E0            MOVX    A,@DPTR
0004 24F0          ADD     A,#0F0H
0006 F0            MOVX    @DPTR,A
0007 900000  R     MOV     DPTR,#ptr
000A E0            MOVX    A,@DPTR
000B 3400          ADDC    A,#00H
000D F0            MOVX    @DPTR,A

  15   1         x = *ptr ;
  16   1      
  17   1         }

000E E0            MOVX    A,@DPTR
000F FE            MOV     R6,A
0010 A3            INC     DPTR
0011 E0            MOVX    A,@DPTR
0012 F582          MOV     DPL,A
0014 8E83          MOV     DPH,R6
0016 E0            MOVX    A,@DPTR
0017 F500    R     MOV     x,A

  17   1         }

0019 22            RET     

7.3 Run Time xdata Pointers

The situation often occurs that you need to point at addresses in the xdata space which are only known at run time. Here the xdata pointer is setup in the executable code.

The best way to achieve this is to declare an "uncommitted" pointer at compile time and to then equate it to an address when running:

char xdata *xdata_ptr ;   /* Uncommitted pointer */
                    /* to xdata memory */
main() {
     
xdata_ptr=(char xdata*) 0x8000 ; /*Point at 0x8000 in */
                                   /*xdata */
}

An alternative is to declare a pointer to the xdata space and simply equate it to a variable.

Here is an example:

   char xdata *ptr ; /* This is a spaced pointer!!! */

   main(){

   start_address = 0x8000 ;  /*Variable containing address*/
                    /*to be pointed to */

0000 750080  R     MOV     start_address,#080H
0003 750000  R     MOV     start_address+01H,#00H

   ptr = start_address ; 

000C AE00    R     MOV     R6,start_address
000E AF00    R     MOV     R7,start_address+01H
0010 8E00    R     MOV     ptr,R6
0012 8F00    R     MOV     ptr+01H,R7
0014         ?C0001:

   while(1) {

   x = *ptr++ ;

0014 0500    R     INC     ptr+01H
0016 E500    R     MOV     A,ptr+01H
0018 AE00    R     MOV     R6,ptr
001A 7002          JNZ     ?C0004
001C 0500    R     INC     ptr
001E         ?C0004:
001E 14            DEC     A
001F FF            MOV     R7,A

0020 8F82          MOV     DPL,R7
0022 8E83          MOV     DPH,R6
0024 E0            MOVX    A,@DPTR
0025 FF            MOV     R7,A
0026 8F00    R     MOV     x,R7
   }
0028 80EA          SJMP    ?C0001
002A         ?C0002:
  }
002A         ?C0003: 
002A 22            RET     
 
A variation of this is to declare a pointer to zero and use a variable as an offset thus:

char xdata *ptr ;

main() {

unsigned int i ; 
unsigned char x ;

ptr = (char*) 0x0000 ;

for(i = 0 ; 
i < 0x40 ; 
i++) {
   x = ptr[i] ;
   }
}

This results in rather more code, as an addition to the pointer must be performed within each loop.

7.4 The volatile Storage Class

A common situation with external devices is that values present in their registers change without the cpu taking any action. A good example is a real time clock chip - the time changes continuously without the cpu writing anything.

Consider the following:


unsigned int xdata *milliseconds = 0x8000 ;  // Pointer to 
                               // RTC chip

time = *milliseconds ;  -> (1)  // Get RTC register value

x = array[time] ;     

time = *milliseconds ;  -> (2)  // Second register access 
                        // optimised out!

y = array[time] ;

Here the value retrieved from the array is related to the value of *milliseconds, a register in an external RTC.

If this is compiled it will not work. Why? Well the compiler's optimiser shoots itself in the foot by assuming that, because no WRITE occurred between (1) and (2), *millisec cannot have changed. Hence all the code generated to make the second access to the register is optimised out and so y == x!

The solution is declare *milliseconds as "volatile" thus:

unsigned int volatile xdata *milliseconds = 0x8000 ;

Now the optimiser will not try to remove subsequent accesses to the register.

7.5 Placing Variables At Specific Locations -

The Linker Method

A final method of establishing external variables at fixed addresses, especially arrays, is by using the linker rather than the compiler. For example, to produce a 10 character array in external memory, starting at 8000H, the following steps are necessary:


/*** Module 1 ***/

/* This module contains only data declarations! */

xdata unsigned char array[30] ;

/* End Module 1 */

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

/*** Module 2 ***/

/* This module contains the executable statements */

extern xdata unsigned char array[10] ;

   main()

   {
   unsigned char i ;

   i = array[i] ;

   }

Now by linking with the invocation:

L51 module1.obj, module2.obj XDATA (?XD?module1 (8000H))

the linker will make the XDATA segment in Module 1 (indicated by ?XD?module1) start at 8000H, regardless of other xdata declarations elsewhere. Thus the array starts at 8000H and is 10 bytes (+ null terminator) long.

This approach lacks the flexibility of the above methods but has the advantage of making the linker reserve space in the XDATA space.

Similar control can be exercised over the address of segments in other memory spaces. C51 uses the following convention for segment names:


CODE   ?PR?functionname?module_name  (executable code) 
CODE   ?CO?functionname?module_name  (lookup tables etc.) 
BIT    ?BI?functionname?module_name 
DATA   ?DT?functionname?module_name 
XDATA  ?XD?functionname?module_name 
PDATA  ?PD?functionname?module_name

Thus the parameter receiving area of a LARGE model function 'test()' in module MOD1.C would be:


?XD?TEST?MOD1,

The code is:

?PR?TEST?MOD1

And so on.

A knowledge of this is useful for assembler interfacing to C51 programs. See section 14.

7.6 Excluding External Data Ranges From Specific

Areas

This very much follows on from the previous section. Occasionally a memory-mapped device, such as real time clock chip, is used as both a source of time values and RAM. Typically the first 8 bytes in the RTC's address range are the time counts, seconds, minutes etc. whilst the remaining 248 bytes are RAM.

Left to its own devices, the L51 linker will automatically place any xdata variables starting at zero. If the RTC has been mapped at this address a problem occurs, as the RTC time registers are overwritten. In addition, it would be convenient to allow the registers to be individually named.

One approach is to define a special module containing just a structure which describes the RTC registers. In the main program the RTC registers are accessed as elements in the structure. The trick is that, when linking, the XDATA segment belonging to the special module is forced to a specific address, here zero. This results in the RTC structure being at zero, with any other XDATA variables following on. The basic method could also be used to stop L51 locating any variables in a specific area.

Example Of Excluding Specific Areas From L51


/* Structure located at base of RTC Chip */

MAIN.C Module

extern xdata struct {   unsigned char seconds ;
                unsigned char minutes ;
                unsigned char hours   ;
                unsigned char days    ; } RTC_chip ;

/* Other XDATA Objects */

xdata unsigned char time_secs, time_mins ;

void main(void) {

time_secs = RTC_chip.seconds ; 
time_mins = RTC_chip.minutes ;

}

RTCBYTES.C Module


xdata struct { unsigned char seconds ;
               unsigned char minutes ;
               unsigned char hours   ;
               unsigned char days    ; } RTC_chip ;

Linker Input File To Locate RTC_chip structure over real RTC Registers is:

l51 main.obj,rtcbytes.obj XDATA(?XD?RTCBYTES(0h))

7.7 -missing ORDER and AT now in C51

Perhaps the most curious omission from C51 was the inability to fix the address of a data object at an absolute address from the source file. Whilst there have always been methods of achieving the same effect, users have long requested an extension to allow the address of an object to be included in the original declaration. In C51 v4.xx, the new _AT_control now exists.

7.8 Using The_at_and_ORDER_Controls

Here, the order of the variables must not change as it must match the physical location of the real time clock’s registers. The #pragma ORDER tells c51 to place the data objects at ascending addresses, with the first item at the lowest address. The linker must then be used to fix the address of the whole block in memory.

Source File MAIN.C


#pragma ORDER
unsigned char xdata RTC_secs ;
unsigned char xdata RTC_mins ;
unsigned char xdata RTC_hours ;

main() {   RTC_mins = 1 ; }

Linker Input File MAIN.LIN

main.obj & to main & XDATA(?XD?MAIN(0fa00h))

The alternative_at_control forces C51 to put data objects at an address given in the source file:

/** Fix Real Time Clock Registers Over Memory-Mapped Device **/
/** Fix each item individually **/
unsigned char xdata RTC_secs _at_ 0xfa00 ;
unsigned char xdata RTC_mins _at_ 0xfa01 ;
unsigned char xdata RTC_hours _at_ 0xfa02 ;

main()   {    RTC_mins = 1 ;
      }

...which hopefully is self-explanatory!


[Back] [Top] [Next]