mP PRO for PIC tips and tricks


I have tried to write a few words on every of the following subjects:

Other tips that do not require much explanation:

Internal representation of variables

Multi-byte variables are stored in RAM as little endian, i.e. least significant byte is first, followed by more significant ones. Mostly, one does not need to concern oneself with it as there are operators allowing to access single bytes of a muli-byte variable (Lo, Hi, Higher, and Highest).

Arrays are stored by elements, with multi-dimensional arrays most inner dimension elements stored first (most inner are the ones indexed by the latest index, i.e. in arr[j][i], i is the index pointing to most inner elements). For example, elements of an array declared as

arr: array[2] of array[3] of byte;

are stored in following order

[0][0]  [0][1]  [0][2]  [1][0]  [1][1]   [1][2]

Strings and arrays of characters are stored in the order of their elements (characters) with added termination char equal zero. This means that both string[10] and array[10] of char need 11 bytes of RAM. If a string assigned to string variable (array of char) is shorter than the declared variable size, then terminating char will follow last assigned character. For example

   st:='abc';

results in

st[0]='a', at[1]='b', st[3]='c', st[4]=0

If, instead of using library routines, one manipulates string elements directly, one should always ascertain that terminating char is correctly placed.

Floating-point numbers (type real) are stored in four bytes (32 bits - meaning single precision). mP PRO for PIC uses Microchip format (see application note AN575), not IEEE 754.
Floating-point representation comes to

     x = +/-m * 2^exp

where m is a rational mantissa (significand) and exp - an integer exponent. In Microchip format there are 8 bits of exponent, one sign bit and 23 bits of mantissa (actually, mantissa is 24 bits wide with assumption that first bit is always 1 and so it may be skipped - this only means that mantissa is in the range from 1 to 2, and not 0 to 1 - unlike in IEEE 754, numbers with most significant bit equal 0 are not used in Microchip format):

bit #    31         23 22                  0
         | Exponent |S|       Mantissa      |
                     1.d1        ...      d23 (binary rational number)

Exponent ranges from -126 to 128. It's offset by 127, so 1.0 is represented with 7F 00 00 00.
In order of location in memory, the consequtive four bytes of a real variable, x, are

  Lo(x) -> least significant byte of mantissa (bits d16..d23)
  Hi(x) -> less significant byte of mantissa (bits d8..d15)
  Higher(x) or $80 -> most significant byte of mantissa (1.d1d2d3d4d5d6d7)
   Higher(x).7 -> sign bit (0 - positive, 1 - negative)
  Highest(x) -> exponent, offset by 127

For example, number 12.345 is coded in Micochip format as hex 82 45 85 1F (exponent is hex 82, or decimal 130, sign bit is 0, rest is mantissa without first bit) or, in binary, as 10000010 01000101 10000101 00011111 (naturally, in RAM the order of bytes will be reversed). This represents a binary rational number

                  1.10001011000010100011111 x 2^3
(as 130-127=3, and first mantissa bit is always 1) or
                  1100.01011000010100011111


Storing/accessing variables

When a need arises to store or access multi-byte variables byte by byte, like when writing to EEPROM or sending through serial interface, there are several methods to do it. One may use pointers, byte-access operators, or memory mapping.

Let's consider storing and reading of a variable of type real, x, in EEPROM. To access it's four bytes one may use the byte-access operators, like

  EEPROM_Write(Address,Lo(x));
  EEPROM_Write(Address+1,Hi(x));
  EEPROM_Write(Address+2,Higher(x));
  EEPROM_Write(Address+3,Highest(x));

  Lo(x):=EEPROM_Read(Address);
  Hi(x):=EEPROM_Read(Address+1);
  Higher(x):=EEPROM_Read(Address+2);
  Highest(x):=EEPROM_Read(Address+3);

One may also map an array of bytes on the variable x

 var x: real;
     ax: array[4] of byte at x;

Any operation on elements of ax will be effectively performed on x, like

  for i:=0 to 3 do EEPROM_Write(Address+i,ax[i]);

  for i:=0 to 3 do ax[i]:=EEPROM_Read(Address+i);

And finally, one may declare a pointer to byte initialised to point at x:

 var x: real;
     px: ^byte;

  px:=^byte(@x);
  for i:=1 to 4 do
   begin
    EEPROM_Write(Address,px^);
    inc(px);
    inc(Address);
   end;


Speeding up sequential memory access

When dealing with a string or array, one may significantly speed up code execution using indirect addressing mechanism of PIC18 processors. File Select Registers (FSRs) may be used as pointers and manipulated with Indirect File Operands, like INDFx (does not affect FSRx), POSTINCx (increments FSRx after use), POSTDECx (decrements FSRx after use), etc. Here's an example - let's assume that st is a string of 20 chars - all of which we want to exchange with spaces. Instead of

  i:=0;
  while st[i]<>0 do
   begin
    st[i]:=' ';
    inc(i);
   end;

one may use

  FSR0ptr:=@st;
  while INDF0<>0 do POSTINC0:=' ';

Though compiler also uses indirect addressing, the latter code is twice smaller and over three times faster. PIC18 processors have three FSR registers. In mikroPascal, FSR0, FSR1 and FSR2 are declared in processor definition files as word variables, while FSR0ptr, FSR1ptr, and FSR2ptr are declared as pointers to byte, but they represent the same registers so following assignments are equivalent:

  FSR0ptr:=@st;
  FSR0:=word(@st);

Indirect File Operands may be used on both sides of an assignment, like, for example

  POSTINC0:=POSTDEC1;

After the assignment is executed, besides copying a byte pointed to by FSR1 to memory pointed to by FSR0, FSR0 will be incremented, while FSR1 will be decremented.
As the compiler also uses indirect addressing, one has to be cautious and use presented approach only when sure that it will not interfere with the compiler. Compiler uses indirect addressing not only when accessing elements of strings, arrays or records, but also to access parameters of a routine passed by reference, or when assigning a literal string to string variable (or passing it as a parameter), or while copying strings, etc.

There's one other potential problem - compiler does not save nor restore FSRs in Interrupt Service Routine if they're not used in assembly. When using FSRs in Pascal only, both in main code and in ISR, one has to either take care of adding FSRs to context saving by oneself, or force the compiler to do it. Naturally, FSRs used by compiler will be saved automatically, but one may make sure all FSRs will be saved by calling a simple procedure (present in System replacement library):

procedure FSRcontext;
 begin
  asm
    movf FSR0L,F
    movf FSR1L,F
    movf FSR2L,F
  end;
 End;{FSRcontext}


Reentrancy

mP allows for reentrancy (i.e. calling the same routine from both ISR and main program) only if the routine has no parameters or local variables (placing local variables in Rx space has little sense as they become global and taken Rx space cannot be reused). In all other cases compillation will end with error. While real reentrancy is in general forbidden, it's still possible to call any routine from both ISR and main program body, but first, let me warn you - do not use the following trick with time consuming routines, as these should never be used in ISR.

Now, the trick allows to use the same routine both in ISR and in main program part. It's not real reentrancy, as the routine called from main program cannot be interrupted and re-invoked in ISR, but it can formally be used in both program parts without the compiler complaining about it.

Evident need for such a trick is a situation where one wants to initialise system, before even enabling interrupts (no real reentracy there with interrupts disabled).

Presented method may also be used in general ('reentrant' routine called from anywhere in main program body), whenever blocking interrupts for the time it takes to execute 'reentrant' routine in main code part does not ruin user algorithm.

The trick is made possible thanks to the SetFuncCall procedure introduced in mP PRO which allows to cheat linker into believing that another routine is called than the one which in fact is. The original idea was posted some time ago by ronnym in mC forum.

Here's an example how it works with PWM1_Set_Duty library routine (it's not a working code, just demonstration of the method).

program Reentrancy;

type Tproc = procedure(dr:byte); // procedure type identical to PWMx_Set_Duty

var bb1,bb2: byte;

procedure PWM_dummy(dr:byte); forward; // intermediate procedures
procedure PWM_main(dr:byte);  forward; // allowing to call PWM1_Set_Duty
procedure PWM_inter(dr:byte); forward; // both from main and ISR

procedure interrupt;
 begin
  PWM_inter(127);  // set PWM1 to 127
 End;{interrupt}

procedure PWM_dummy(dr:byte);
 begin
  dr:=dr;  // to avoid compiler hint
 End;{PWM_dummy}

procedure PWM_main(dr:byte);  // to be called from main program
 var ProcPtr: ^Tproc;
     S_INTCON: byte;
 begin
  SetFuncCall(PWM1_Set_Duty); // not really necessary
  ProcPtr:=@PWM1_Set_Duty;
  S_INTCON:=INTCON.GIE;       // main code may call PWM1_Set_Duty only with
  INTCON.GIE:=0;              // interrupts blocked
  ProcPtr^(dr);
  INTCON.GIE:=S_INTCON;
 End;{PWM_main}

procedure PWM_inter(dr:byte); // to be called from ISR
 var ProcPtr: ^Tproc;
 begin
  SetFuncCall(PWM_dummy);     // make linker believe PWM_dummy will be called
  ProcPtr:=@PWM1_Set_Duty;    // while calling PWM1_Set_Duty from ISR
  ProcPtr^(dr);
 End;{PWM_inter}

BEGIN
 PWM1_Init(5000); // PWM initialisation
 PWM1_Start;
 PWM_main(0);     // set PWM1 to 0

 while True do
  begin
                  // main program loop
  end;
END.

As you see, there are additional three procedures declared - a dummy one used to cheat the linker, and two procedures that call PWM1_Set_Duty via pointers. One of the latter may be called from ISR, the other from main part of code.

As mentioned earlier, during the call made from main interrupts are blocked (note, that any call via pointer also blocks interrupts for few assembly instructions - unless one uses the System library replacement, that is). It's another reason not to use the trick with routines that are time consuming in execution as some interrupts may be missed.

If all one wants to do is to use the same routine for initialisation (with interrupts disabled) and later in ISR, the PWM_main procedure is not needed - one may directly call PWM1_Set_Duty (or any other routine used as in the above example).







To be continued..

TopTop        MainHome        Copyright © 2011-2018 by janni