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:
In detail, these may be implemented as shown in the following sections.
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)
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
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.
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.
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.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))
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.
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!