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
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;
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}
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).