[Back] [Top] [Next]

3 Declaring Variables And Constants

3.1 Constants

The most basic requirement when writing any program is to know how to allocate storage for program data. Constants are the simplest; these can reside in the code (Eprom) area or as constants held in RAM and initialised at runtime. Obviously, the former really are constants and cannot be changed.

While the latter type are relatively commonplace on big systems (Microsoft C), in 8051 applications the code required to set them up is often best used elsewhere. Also, access is generally faster to ROMmed constants than RAM ones if the RAM is external to the chip, as ROM "MOVC A,@DPTR" instruction cycle is much faster than the RAM "MOVX A,@DPTR".

Examples of Eprommed constant data are:

    code unsigned char coolant_temp = 0x02 ;
    code unsigned char look_up table[5]='1','2','3','4''} ;
    code unsigned int  pressure = 4 ;

Note that "const" does not mean "code". Objects declared as "const" will actually end up in the data memory area determined by the current memory model.

Obviously, any large lookup tables should be located in the CODE area - a declaration might be:

    /* Base FuelMap    */
    
    /* x = Load : y = engine speed : output = Injector PW, 0 - 8.16ms */
    
    /* (x_size,y_size,
        x_breakpoints,
        y_breakpoints,
        map_data)    
    */
    
    code unsigned char default_base_fuel_PW_map[] = { 

                0x08,0x08,
                0x00,.0x00,0x00,0x09,0x41,0x80,0xC0,0xFF,
                0x00,0x00,0x13,0x1A,0x26,0x33,0x80,0xFF,
                0x00,0x00,0x00,0x09,0x41,0x80,0x66,0x66,
                0x00,0x00,0x00,0x09,0x41,0x80,0x66,0x66,
                0x00,0x00,0x00,0x00,0x4D,0x63,0x66,0x66,
                0x00,0x00,0x00,0x02,0x4D,0x63,0x66,0x66,
                0x00,0x00,0x00,0x05,0x4A,0x46,0x40,0x40,
                0x00,0x00,0x00,0x08,0x43,0x43,0x3D,0x3A,
                0x00,0x00,0x00,0x00,0x2D,0x4D,0x56,0x4D,
                0x00,0x00,0x00,0x00,0x21,0x56,0x6C,0x6F
    
            } ;
                

With large objects like the above it is obviously important to state a memory space. When working in the SMALL model in particular, it is very easy to fill up the on-chip RAM with just a single table!

RAM constants would be:

    unsigned char scale_factor = 128    ;
    unsigned int fuel_constant = 0xFD34 ;

These could, however, have their values modified during program execution. As such, they are more properly thought of as initialised variables - see section 3.2.2

3.2 Variables

3.2.1 Uninitialised Variables

Naturally, all variables exist in RAM, the configuration of which is given in section 2.1.1.

The #pragma SMALL line will determine the overall memory model. In this case, all variables are placed within the on-chip RAM. However, specific variables can be forced elsewhere as follows:

    #pragma SMALL
      .
      .
      xdata unsigned char engine_speed ;
      xdata char big_variable_array[192] ;

This will have engine_speed placed in an external RAM chip. Note that no initial value is written to engine_speed, so the programmer must not read this before writing it with a start value! This xdata placement may be done to allow engine_speed to be traced "on the fly", by an in-circuit emulator for example.

In the case of the array, it would not be sensible to place this in the on-chip RAM because it would soon get filled up with only 128 bytes available. This is a very important point - never forget that the 8051 has very limited on-chip RAM.

Another example is:

      .
    #pragma LARGE
      .
      .
      .
      function(data unsigned char para1)
      {
      data unsigned char local_variable ;
      .
      .
      .
      .
      }

Here the passed parameters are forced into fast directly addressed internal locations to reduce the time and code overhead for calling the function, even though the memory model would normally force all data into XDATA.

In this case it would be better to declare the function as SMALL, even though the prevailing memory model is large. This is extremely useful for producing a few fast executing functions within a very big LARGE model program.

On a system using paged external RAM on Port 0, the appropriate directive is "pdata".

See notes in section 2.1.3 for details on how to best locate variables.

3.2.2 Initialised Variables

To force certain variables to a start value in an overall system setup function, for example, it is useful to be able to declare and initialise variables in one operation. This is performed thus:

    unsigned int engine_speed = 0 ;

    function()
      {
      .
      .
      .
      }

Here the value "0" will be written to the variable before any function can access it. To achieve this, the compiler collects together all such initialised variables from around the system into a summary table. A runtime function named "C_INIT" is called by the "startup.obj" program which writes the table values into the appropriate RAM location, thus initialising them.

Immediately afterwards, the first C program "main()" is called. Therefore no read before write can occur, as C_INIT gets there first. The only point to note is that you must modify the "startup.a51" program to tell C_INIT the location and size of the RAM you are using. For the large model, XDATASTART and XDATALEN are the appropriate parameters to change.

3.3 Watchdogs With Large Amounts Of Initialised Data

In large programs the situation may arise that the initialisation takes longer to complete than the watchdog timeout period. The result is that the cpu will reset before reaching main() where presumably a watchdog refresh action would have been taken.

To allow for this the INIT.A51 assembler file, located in the \C51p\LIB directory, should be modified.

;__________________________________________________________;  
This file is part of the C-51 Compiler package Copyright KEIL ELEKTRONIK  GmbH 1990 
;__________________________________________________________;
INIT.A51:  This code is executed if the application program contains initialised variables at file level. 
; _________________________________________________________;
;  User-defined Watch-Dog Refresh. 
; 
;  If the C application containing many initialised variables uses a watchdog it 
;  might be possible that the user has to include a watchdog refresh in the 
;  initialisation process. The watchdog refresh routine can be included in the 
;  following MACRO and can alter all CPU registers except DPTR. 
; 
WATCHDOG    MACRO
            ;Include any Watchdog refresh code here
        P6 ^= watchdog_refresh  ;Special application code
        ENDM 
;____________________________________
        NAME    ?C_INIT

?C_C51STARTUP SEGMENT CODE
?C_INITSEG    SEGMENT CODE  ; Segment with Initialising Data

              EXTRN CODE (MAIN)
              PUBLIC    ?C_START
              RSEG    ?C_C51STARTUP INITEND:    LJMP    MAIN

?C_START:    
              MOV    DPTR,#?C_INITSEG


LOOP:
              WATCHDOG   ;<<_ WATCHDOG REFRESH CODE ADDED HERE!
              CLR    A
              MOV    R6,#1
              MOVC    A,@A+DPTR
              JZ    INITEND
              INC    DPTR
              MOV    R7,A
. 
. 
. 
.  Large initialisation loop code 
. 
. 
.
        XCH    A,R0
        XCH    A,R2
        XCH    A,DPH
        XCH    A,R2
        DJNZ    R7,XLoop
        DJNZ    R6,XLoop
        SJMP    Loop
        LJMP MAIN              ; C51 Program start

        RSEG    ?C_INITSEG
        DB    0
        END

A special empty macro named WATCHDOG is provided which should be altered to contain your normal watchdog refresh procedure. Subsequently, this is automatically inserted into each of the initialisation loops within the body of INIT.A51.

3.4 C51 Variables

3.4.1 Variable Types

Variables within a processor are represented by either bits, bytes, words or long words, corresponding to 1, 8, 16 and 32 bits per variable. C51 variables are similarly based, for example:

bit            =1 bit         0 - 1
char           =8 bits        0 - +/- 127
unsigned char  =8 bits        0 - 255  
int            =16 bits       0 - +/-32768
unsigned int   =16 bits       q0 - 65535
long           =32 bits       0 - +/- 2.147483648x109
unsigned long  =32 bits       0 - 4.29496795x109
float          =32 bits       +/-1.176E-38 
                              to +/-3.4E+38
pointer        =24/16/8 bits  Variable address

Typical declarations would be:
    xdata unsigned char battery_volts ;
    idata int correction_factor       ;
    bit flag_1 ;       
              

(Note: bit variables are always placed in the bit-addressable memory area of the 8051 - see section 2.1.1)

With a processor such as the 8086, int is probably the commonest data type. As this is a 16 bit processor, the handling of 16 bit numbers is generally the most efficient. The distinction between int and unsigned int has no particular impact on the amount of code generated by the compiler, since it will simply use signed opcodes rather than the unsigned variety.

For the 8051, naturally enough, the char should be the most used type. Again, the programmer has to be aware of the thoroughly 8 bit nature of the chip. Extensive use of 16 bit variables will produce slower code, as the compiler has to use library routines to achieve apparently innocuous 16 by 8 divides, for example.

The use of signed numbers has to be regulated, as the 8051 does not have any signed arithmetic instructions. Again, library routines have to do the donkey work.

An interesting development has been the Siemens 80C537, which does have an extended arithmetic instruction set. This has, for instance, 32 by 16 divide and integer instructions. Indeed, this device might be a good upgrade path for those 8051 users who need more number crunching power and who might be considering the 80C196. A suite of runtime libraries is available from Keil to allow the compiler to take advantage of the 80C537 enhancements.

3.4.2 Special Function Bits

A major frustration for assembler programmers coming to C is the inability of ANSI C to handle bits in the bit-addressable BDATA area directly. Commonly bit masks are needed when testing for specific bits with chars and ints. In C51 version 3 however, it is possible to force data into the bit-addressable area (starting at 0x20) where the 8051's bit instructions can be used directly from C.

An example is testing the sign of a char by checking for bit = 1.

Here, the char is declared as "bdata" thus:


    bdata char test ;
    sign_bit is defined as:
    sbit sign ^ 7   ;

To use this:
  
void main(void) {
     test = -1 ;
     if(test & 0x80) { // Conventional bit mask and &
        test = 1 ;     // test was -ve
        }
     if(sign == 1) {   // Use sbit
        test = 1 ;     // test was -ve
        }
      }



Results in the assembler:

    RSEG  ?BA?T2 
test:            DS  1 
sign    EQU    test.7
; 
; bdata char test ; 
; sbit sign = test ^ 7 ; 
; 
; void main(void) {
main: 
;   test = -1 ;
    MOV      test,#0FFH
; 
;   if(test & 0x80) { // Conventional bit mask and &
    MOV      A,test
    JNB      ACC.7,?C0001
; 
;      test = 1 ;        // test was -ve
    MOV      test,#01H
;    } 
?C0001: 
; 
;   if(sign == 1) {      // Use sbit
    JNB      sign,?C0003
; 
;        test = 1 ;      // test was -ve
    MOV      test,#01H
;    } 
;       
;    }
?C0003:
    RET      

Here, using the sbit, the check of the sign bit is a single JNB instruction, which is an awful lot faster than using bit masks and &'s in the first case! The situation with ints is somewhat more complicated. The problem is that the 8051 does not store things as you first expect. The same sign test for an int would still require bit 7 to be tested. This is because the 8051 stores int's high byte at the lower address. Thus bit 7 is the highest bit of the higher byte and 15 is the highest bit of the lower.

Byte Number: test_int(high) 20H Bit Number: 0,1,2,3,4,5,6,7

Byte Number: test_int+1(low) 21H Bit Number: 8,9,10,11,12,13,14,15

Bit locations in an integer

3.4.3 Converting Between Types

One of the easiest mistakes to make in C is to neglect the implications of type within calculations or comparisons

Taking a simple example:

    unsigned char x ;
    unsigned char y ;
    unsigned char z ;
    
    x = 10 ;
    y = 5  ;
    
    z = x * y ;

Results in z = 50 

However:

    x = 10 ;
    y = 50 ;
    
    z = x * y ;

results in z = 244. The true answer of 500 (0x1F4) has been lost as z is unable to accommodate it. The solution is, of course, to make z an unsigned int. However, it is always a good idea to explicitly cast the two unsigned char operands up to int thus:

    unsigned char x ;
    unsigned char y ;
    unsigned int z ;

    z = (unsigned int) x * (unsigned int) y ;

While C51 will automatically promote chars to int, it is best not to rely on it! It could be argued that on any small microcontroller you should always be aware of exactly what size data is.

3.4.4 A Non-ANSI Approach To Checking Data Type

A very common situation is where two bytes are to be added together and the result limited to 255, i.e. the maximum byte value. With the 8051 being byte-orientated, incurring integers must be avoided if maximum speed is to be achieved. Likewise, if the sum of two numbers exceeds the type maximum the use of integers is needed.

In this example the first comparison uses a proper ANSI approach. Here, the two numbers are added byte-wise and any resulting carry used to form the least significant bit of the upper byte of the notional integer result. A normal integer compare then follows. Whilst C51 makes a good job of this, a much faster route is possible, as shown in the second case.

; #include <reg51.h>
;
;
; unsigned char x, y, z ;
;
; /*** Add two bytes together and check if ***/
; /***the result has exceeded 255 ***/ 
; 
; void main(void) {
    RSEG  ?PR?main?T
    USING    0
main:
            ; SOURCE LINE # 8
; 
;    if(((unsigned int)x + (unsigned int)y) > 0xff) {
            ; SOURCE LINE # 10
    MOV      A,x
    ADD      A,y
    MOV      R7,A
    CLR      A
    RLC      A
    MOV      R6,A
    SETB     C
    MOV      A,R7
    SUBB     A,#0FFH
    MOV      A,R6
    SUBB     A,#00H
    JC       ?C0001
; 
;       z = 0xff ;   // ANSI C version
            ; SOURCE LINE # 12

    MOV      z,#0FFH
;       }
            ; SOURCE LINE # 13

In this case the carry flag, "CY", is checked directly, removing the need to perform any integer operations, as any addition resulting in a value over 255 sets the carry. Of course, this is no longer ANSI C as a reference to the 8051 carry flag has been made.


?C0001: 
; 
;    z = x + y ;
            ; SOURCE LINE # 15
    MOV      A,x
    ADD      A,y
    MOV      z,A
; 
;    if(CY) {
            ; SOURCE LINE # 17
    JNB      CY,?C0003
;  
;       z = 0xff ;   // C51 Version using the carry flag
            ; SOURCE LINE # 19
    MOV      z,#0FFH
;       }
            ; SOURCE LINE # 20
; 
; 
;    
; 
;    }
            ; SOURCE LINE # 25
?C0003:
    RET      

The situation of an integer compare for greater than 65535 (0xffff) is even worse as long maths must be used. This is almost a disaster for code speed as the 8051 has very poor 32 bit performance. The trick of checking the carry flag is still valid as the final addition naturally involves the two upper bytes of the two integers.

In any high performance 8051 system this loss of portability is acceptable, as it allows run time targets to be met. Unfortunately, complete portability always compromises performance!


[Back] [Top] [Next]