[Back] [Top] [Next]

5 C Language Extensions For 8051

Programming

8051 programming is mainly concerned with accessing real devices at specific locations, plus coping with interrupt servicing. C51 has made many extensions to the C language to allow near-assembler code efficiency. The main points are now covered.

5.1 Accessing 8051 On-Chip Peripherals

In the typical embedded control application, reading and writing port data, setting timer registers and reading input captures etc. are commonplace. To cope with this without recourse to assembler, C51 has the special data types sfr and sbit.

Typical declarations are:

    sfr P0 0x80
    sfr P1 0x81
    sfr  ADCON; 0xDE
    sbit EA  0x9F

and so on.

These declarations reside in header files such as reg51.h for the basic 8051 or reg552.h for the 80C552 and so on. It is the definition of sfrs in these header files that customises the compiler to the target processor. Accessing the sfr data is then a simple matter:


   {
   ADCON = 0x08 ;   /* Write data to register */
   P1 = 0xFF    ;   /* Write data to Port */

   io_status = P0 ; /* Read data from Port */
   EA = 1       ;   /* Set a bit (enable all interrupts) */

   }

It is worth noting that control bits in registers which are not part of Intel's original 8051 design generally cannot be bit-addressed.

The rule is usually that addresses that are divisible by 8 are bit addressable. Thus for example, the serial Port 1 control bits in an 80C537 must be addressed via byte instructions and masking.

Always check the processor's user manual to verify which sfr register bits can be bit addressed.

5.2 Interrupts

Interrupts play an important part in most 8051 applications. There are several factors to be taken into account when servicing an interrupt:

The correct vector must be generated so that the routine may be called. C51 does this automatically.

The local variables in the service routine must not be shared with locals in the background loop code: the L51 linker will try to re-use locations so that the same byte of RAM will have different significance depending on which function is currently being executed. This is essential to make best use of the limited internal memory. Obviously this relies on functions being executed only sequentially. Unexpected interrupts cannot therefore use the same RAM.

5.2.1 The Interrupt Function Type

To allow C coding of interrupts a special function type is used thus;

    timer0_int() interrupt 1 using 2
    {
    unsigned char temp1 ;
    unsigned char temp2 ;
    executable C statements ;
    }

Firstly, the argument of the "interrupt" statement, "1" causes a vector to be generated at (8*n+3), where n is the argument of the "interrupt" declaration. Here a "LJMP timer0_int" will be placed at location 0BH in the code memory. Any local variables declared in the routine are not overlaid by the linker to prevent the overwriting of background variables.

Logically, with an interrupt routine, parameters cannot be passed to it or returned. When the interrupt occurs, compiler-inserted code is run which pushes the accumulator, B,DPTR and the PSW (program status word) onto the stack. Finally, on exiting the interrupt routine, the items previously stored on the stack are restored and the closing "}" causes a RETI to be used rather than a normal RET.

5.2.2 Using C51 With Target Monitor Debuggers

Many simple 8032 target debuggers place the monitor's EPROM code at 0, with a RAM mapped into both CODE and XDATA spaces at 0x8000. The user's program is then loaded into the RAM at 0x8000 and, as the PSEN is ANDed with the RD pin, the program is executed. This poses something of a problem as regards interrupt vectors. C51/L51 assume that the vectors can be placed at 0. Most monitors for the 8032 foresee this problem by redirecting all the interrupt vectors up to 0x8000 and above, i.e. they add a fixed offset of 0x8000. Thus the timer 0 overflow interrupt is redirected by a vector at C:0x000B to C:0x800B.

Before C51 v3.40 the interrupt vector generation had to be disabled and assembler jumps had to be inserted. However now the INTVECTOR control has been introduced to allow the interrupt vector area to be based at any address.

In most cases the vector area will start at 0x8000 so that the familar "8 * n + 3" formula outlined in section 5.2.1 effectively becomes:

8 * n + 3 + INTVECTOR

To use this:


#pragma INTVECTOR(0x8000)   /* Set vector area start to 0x8000 */

void timer0_int(void) interrupt 1 {

   /* CODE...*/

   }

This produces an LJMP timer0_int at address C:0x800B. The redirection by the monitor from C:0x000B will now work correctly.

5.2.3 Coping Interrupt Spacings Other Than 8

Some 8051's do not follow the normal interrupt spacing of 8 bytes - the '8' in the 8 * n + 3 formula. Fortunately the "INTERVAL #pragma" copes with this.

The interrupt formula is, in reality:

INTERVAL * n + INTVECTOR and so:

#pragma INTERVAL(6)   /* Change spacing */

will allow a 6 byte spacing.

Please note that for convenience INTERVAL defaults to 8 and INTVECTOR to 0x80000!

5.2.4 The Using Control

The "using" control tells the compiler to switch register banks. This is an area where the 8051 architecture works for the compiler rather than against it; the registers R0 to R7 are used extensively for the temporary storage of library routines and for locals. Ordinarily Bank 1 is used. However, to be able to use this standard code in an interrupt the register bank must be switched to 2 in the above example. Thus the variables of the interrupted routines are preserved.

As a rule interrupts of the same priority can share a register bank, since there is no risk that they will interrupt each other.

If interrupt runtime is not important the USING can be omitted, in which case C51 examines the registers which are actually used within the routine and pushes only these onto the stack. This obviously increases the effective interrupt latency.

5.3 Interrupts, USING, Registerbanks, NOAREGS In C51

Everything You Need To Know

Interrupts play an important part in most 8051 applications and fortunately, C51 allows interrupt service routines to be written entirely in C. Whilst you can write perfectly workable (and safe) programs by using just straight ANSI C, you can significantly improve the efficiency of your code by gaining an understanding of the following special C51 controls:

5.3.1 The Basic Interrupt Service Function Attribute

The correct vector must be generated so that the routine may be called. C51 does this based on the argument to the interrupt keyword. The linker thereafter does not allow local data from interrupt routines to be overlaid with that from the background by creating special sections in RAM. C51 special "interrupt" function attribute example:

/*Timer 0 Overflow Interrupt Service Routine */

timer0_int() interrupt1 
{
unsigned char temp1 ;		
unsigned char temp2 ;		

/* executable C statements ; */	
}

timer0_int Entry Code

void timer0_int(void) interrupt1 
{
RSEG ?PR?timer0_int?TIMER0		
USING 0	
timer0_int:				
; SOURCE LINE # 2

If a function, here called "sys_interp" is now called from the timer0 service function, this is how the entry code to the interrupt changes.

timer0_int Entry Code Now With Called Function

; void timer0_int(void) interrupt 1 
{
RSEG ?PR?timer0_int?TIMER0	
USING 0
timer0_int:	
PUSH ACC	
PUSH B	
PUSH DPH	
PUSH DPL	
PUSH PSW	
PUSH AR0	
PUSH AR1	
PUSH AR2	
PUSH AR3	
PUSH AR4	
PUSH AR5	
PUSH AR6	
PUSH AR7

Note that the entire current registerbank is pushed onto the stack when entering timer0_int() as C51 assumes that all will be used by sys_interp. Sys_interp receives parameters in registers; if the entry to sys_interp is examined, an important compiler trick is revealed:

sys_interp() Entry Code

; unsigned char sys_interp(unsigned char x_value,	
RSEG ?PR?_sys_interp?INTERP	
 USING 0
_sys_interp:	
MOV y_value?10,R5	
MOV map_base?10,R2	
MOV map_base?10+01H,R3
;--Variable 'x_value?10' assigned to Register 'R1' --	
MOV R1,AR7

The efficient MOV of R7 to R1 by using AR7 allows a MOV direct, direct on entry to sys_interp(). This is absolute register addressing and is a useful dodge for speeding up code.

5.3.2 The absolute register addressing trick in detail

The situation often arises that the contents of one Ri register needs to be moved directly into another general purpose register. This usually occurs during a function's entry code when a pointer is passed. Unfortunately, Intel did not provide a MOV Reg,Reg instruction and so Keil use the trick of treating a register as an absolute D: segment address:

Simulating A MOV Reg,Reg Instruction:

In registerbank 0 - MOV R0,AR7, is identical to - MOV R0,07H.

Implementing a "MOV Reg,Reg" instruction the long way:

XCH A,R1
MOV A,R1

The use of this trick means however, that you must make sure that the compiler knows which is the current registerbank in use so that it can get the absolute addresses right. If you use the USING control, problems can arise! See the next few sections...

5.3.3 The USING Control

"using" tells the compiler to switch register banks on entry to an interrupt routine. This "context" switch is the fastest way of providing a fresh registerbank for an interrupt routine's local data and is to be preferred to stacking registers for very time-critical routines. Note that interrupts of the same priority can share a register bank, since there is no risk that they will interrupt each other.

8051 Register Bank Base Addresses

R0 AR0 Absolute Addr.0x00 REGISTERBANK 0
R1 AR1
R2 AR2
R3 AR3
R4 AR4
R5 AR5
R6 AR6
R7 AR7

R0 Absolute Addr. 0x08 REGISTERBANK 1, "USING 1"
R1
R2
R3
R4
R5
R6
R7

R0 Absolute Addr. 0x10 REGISTERBANK 2, "USING 2"
R1
R2
R3
R4
R5
R6
R7

R0 Absolute Addr. 0x18 REGISTERBANK 3, "USING 3"
R1
R2
R3
R4
R5
R6
R7

If a USING 1 is added to the timer1 interrupt function prototype, the pushing of registers is replaced by a simple MOV to PSW to switch registerbanks. Unfortunately, while the interrupt entry is speeded up, the direct register addressing used on entry to sys_interp fails. This is because C51 has not yet been told that the registerbank has been changed. If no working registers are used and no other function is called, the optimizer eliminiates teh code to switch register banks.

timer0_int Entry Code With USING

With USING 1

; void timer0_int(void) interrupt 1 using 1 {	
RSEG ?PR?timer0_int?TIMER0	
USING 1 <--- New register bank now
timer0_int:	
PUSH ACC	
PUSH B	
PUSH DPH	
PUSH DPL	
PUSH PSW	
MOV PSW,#08H

sys_interp() Entry Code

Still using registerbank 0

; unsigned char sys_interp(unsigned char x_value,	
RSEG ?PR?_sys_interp?INTERP	
USING 0
_sys_interp:	
MOV y_value?10,R5	
MOV map_base?10,R2	
MOV map_base?10+01H,R3;
--Variable 'x_value?10' assigned to Register 'R1' --	
MOV R1,AR7      <----- FAILS!!!!

Absolute register addressing used assuming registerbank 0 is still current and so program fails! (Solutions in 5.3.6-8).

5.3.4 Notes on C51's "Stack Frame"

C51 uses a degree of intelligence when entering interrupt functions. Besides the obvious step of substituting RETI for RET at the end of the function, it automatically stacks only those registers that are actually used in the function.

There are however, some points to be aware of:

5.3.5 When To Use USING

5.3.6 The NOAREGS pragma

Dealing With C51's Absolute Register Addressing.

As has been pointed out, the 8051 has no MOV Register, Register instruction so the compiler uses MOV R1,AR7 where AR7 is the absolute address of the current R7. To do this though, the current registerbank number must be known. If a function is called from an interrupt where a using is in force, when compiling a called function the compiler must be told:

(i) not to use absolute register addressing with #pragma NOAREGS control before the function, and #pragma RESTORE or #pragmas AREGS control enter the function.

Or:

(ii) the current registerbank number with #pragma REGISTERBANK(n).

For (i), applying NOAREGS to the sys_interp function removes the MOV R7,AR7, replacing it with an awkward move of R7 to R1 using XCH A,Ri!

timer0_int Entry Code

; void timer0_int(void) interrupt 1 using 1 {	
RSEG ?PR?timer0_int?TIMER0	
USING 1
timer0_int:	
PUSH ACC	
PUSH B	
PUSH DPH	
PUSH DPL	
PUSH PSW	
MOV PSW,#08H

sys_interp() Entry Code With NOAREGS

; unsigned char sys_interp(unsigned char x_value,	
RSEG ?PR?_sys_interp?INTERP	
USING 0
_sys_interp:	
MOV y_value?10,R5	
MOV map_base?10,R2	
MOV map_base?10+01H,R3;
--Variable 'x_value?10' assigned to Register 'R1' --	
XCH A,R1 ;	
MOV A,R7 ; Slow Reg to Reg move

5.3.7 The REGISTERBANK Control Alternative To NOAREGS

#pragma REGISTERBANK(n) tells C51 the absolute address of the current "using" registerbank base so that direct register addressing will work.

EXAMPLE:
/* Timer 0 Overflow Interrupt Service Routine */	
timer0_int() interrupt 1 USING 1 {	
unsigned char temp1 ;	
unsigned char temp2 ;	
/* executable C statements */
 }

Called function:

#pragma SAVE // Rember current registerbank
#pragma REGISTERBANK(1)	// Tel C51 base address of current registerbank.
void func(char x) {	// Called from interrupt routine			
// with "using1"	
/* Code */	
 }
#pragma RESTORE // Put back to original registerbank

Applying #pragma REGISTERBANK(1) to sys_interp() restores absolute register addressing as C51 now knows the base address of the current register bank.

Note: Always try to use the REGISTERBANK(n) control for any functions called from an interrupt with a USING!

sys_interp() Entry Code With REGISTERBANK(n)

; unsigned char sys_interp(unsigned char x_value,	
RSEG ?PR?_sys_interp?INTERP	
USING 1
_sys_interp:	
MOV y_value?10,R5	
MOV map_base?10,R2	
MOV map_base?10+01H,R3;--
Variable 'x_value?10' assigned to Register 'R1' --	
MOV R1,AR7

5.3.8 Summary Of USING And REGISTERBANK

Expressed in psuedo-code!

if(interrupt routine = USING 1){	
subsequently called function uses #pragma REGISTERBANK(1)	
}

Note: subsequently called function must now only be called from functions using register bank 1.

5.3.9 Reentrancy In C51 - The Final Solution

In addition to calling a function from interrupt, it is also sometimes necessary to call the same function from the background as well. This leaves the possibility open that the function may be called from two places simultaneously with disasterous results!

The attribute required to permit a function to be safely called both from background and interrupt routines simultaneously is "reentrant". This can also help in the previous situation of a function being called from an interrupt. The linker's "MULTIPLE CALL TO SEGMENT" warning is the first sign that you may be trying to use a function reentrantly.

Due to the way that C51 allocates storage for local variables and parameters, it is not possible to call a function from both an interrupt and the background loop. To allow only those functions to be used reentrantly that really need to be, it is possible to specify the reentrant attribute when declaring a function.

The ?C_IBP value set up in startup.a51 tells C51 where to locate the artificial stacks used for reentrant functions. Each time a reentrant function is called, its incoming parameters are moved from the registers in which they were passed into an area of RAM, starting at the address indicated by ?C_IBP. Likewise, any local variables used by the reentrant function are also allocated a place on this special stack.

When startup.a51 is executed before main(), the line:

IF IBPSTACK <> 0
EXTRN DATA (?C_IBP)
MOV ?C_IBP,#LOW IBPSTACKTOP
ENDIF

initialises ?C_IBP to the value of IBPSTACKTOP that you set up earlier. As each local is "pushed" on to the reentrant stack, ?C_IBP is decremented. Thus if an interrupt occurs which calls the function again, the new call will start its reentrant stack from the current ?C_IBP value. Thereafter, any local data or parameter is accessed by the code sequence:

Get a local variable at offset 2 from the current base of the re-entrant stack:

MOV R0,?C_IBP ;	Get stack base
MOV A,@R0 ;	Add offset of local
ADD A,#002 ;
MOV A,@R0 ;	Get local via indirect addressing.
MOV R7,A ;	Store value whilst other local is ;		
accessed.

On leaving the function, ?C_IBP is restored to entry value by adding the total number of locals and parameters that were used. This represents a very large overhead and shows why reentrancy should only be used where absolutely necessary.

EXAMPLE:

The Reentrant Stack When Located In The IDATA Area

0xff	sys_interp parameter 0
0xfe	sys_interp parameter 1
0xfd	sys_interp parameter 2L
0xfc	sys_interp parameter 2H - call from background:					
 ?C_IBP= 0xfc
0xfb	sys_interp parameter 0
0xfa	sys_interp parameter 0
0xf9	sys_interp parameter 1
0xf8	sys_interp parameter 2L
0xf7	sys_interp parameter 2H - call from timer0			
interrupt: ?C_IBP = 0xf7
0xf6	sys_interp parameter 0
0xf5	sys_interp parameter 0
0xf4	sys_interp parameter1
0xf3	sys_interp parameter 2L
0xf2	sys_interp parameter 2H - call from background					
?C_IBP = 0xf2
0xf1
0xf0
0xef
0xee

?C_IBP acts as a base pointer to the reentrant stack and is used to access all locals in a reentrant function.

Adding the reentrant attribute to sys_interp() still requires the NOAREGS control as the registerbank has been changed by USING 1. As a matter of policy, any reentrant function should also have the NOAREGS control so that it becomes totally registerbank-independent.

sys_interp() Entry Code

; unsigned char interp_sub(unsigned char x,	
RSEG ?PR?_?interp_sub?INTERP	
USING 0
_?interp_sub:	
DEC ?C_IBP	
DEC ?C_IBP	
MOV R0,?C_IBP	
XCH A,@R0	
MOV A,R2	
XCH A,@R0	
INC R0	
XCH A,@R0	
MOV A,R3	
XCH A,@R0	
DEC ?C_IBP	
MOV R0,?C_IBP	
XCH A,@R0	
MOV A,R5	
XCH A,@R0	
DEC ?C_IBP	
MOV R0,?C_IBP	
XCH A,@R0	
MOV A,R7	
XCH A,@R0	
DEC ?C_IBP ;		
 SOURCE LINE # 22

sys_interp() Exit Code

?C0009:	
MOV A,?C_IBP	
 ADD A,#010H   <-- Restore ?C_IBP to original
position	
MOV ?C_IBP,A	
RET ;
END OF _?sys_interp	
END

5.3.10 Summary Of Controls For Interrupt Functions

Provided the following combinations of controls are used, you will avoid linker warnings and potentially dangerous code.

Interrupt Function Attribute      |    Called Function Attribute: 
----------------------------------|-----------------------------------
                                  |    "non-reentrant"
No USING                          |    no special attribute required
USING n                           |    USING n
                                  |    or
                                  |    #pragma REGISTERBANK(n)
                                  |    or
                                  |    #pragma NOAREGS
Interrupt Function Attribute      |    Called Function Attribute
----------------------------------------------------------------------
                                  |    "reentrant"
         no USING                 |    no register attribute
         USING n                  |    #pragma NOAREGS

5.3.11 Reentrancy And Library Functions

The majority of C51 library functions are reentrant and can be freely used from interrupts and background. However, some larger library functions such as printf(), scanf() etc. are not reentrant. If you have used a non-reentrant library function reentrantly, you will get a "MULTIPLE CALL TO SEGMENT" warning, as would be expected.

"Hidden" library functions used to perform integer divides and multiplies etc. are all reentrant so you can perform a 16/16 divide in an interrupt without fear of upsetting the background.

To Summarise:

You can generally use library functions reentrantly but always check the C51 manual section 9 to check whether a function is reentrant or not.


[Back] [Top] [Next]