Home
JAQForum Ver 20.06
Log In or Join  
Active Topics
Local Time 10:42 20 May 2024 Privacy Policy
Jump to

Notice. New forum software under development. It's going to miss a few functions and look a bit ugly for a while, but I'm working on it full time now as the old forum was too unstable. Couple days, all good. If you notice any issues, please contact me.

Forum Index : Microcontroller and PC projects : CFunctions - learning by example (2)

Author Message
matherp
Guru

Joined: 11/12/2012
Location: United Kingdom
Posts: 8608
Posted: 07:55am 18 Sep 2015
Copy link to clipboard 
Print this post

This will be the second of three threads looking at the development of CFunctions.
Having understood how to program a CFunction in the first thread we can build on this by looking at how we incorporate floating point arithmetic into a CFunction. Eventually in this thread we will code a PID control loop but we have a way to go first.

To start we need to go back to the old chestnut of position independence and some of the other limitations on CFunctions.

You may have noticed in the previous thread that when I wanted to output CR/LF I inserted the characters one at a time into the array. Normal C for this would have been char crlf[3]="\r\n";
"\r\n" is just the C version for chr$(13)+chr$(10). However, I couldn't do that because when the compiler/linker sees a string like "Hello World" it allocates it in flash and that will be outside of the CFunction.

Similarly, I can't do something like:

const char mychar[]={10,20,30,40,50};

to initialise an array, because again the compiler will try and put the array in flash.

I can't even go:

char mychar[]={10,20,30,40,50};

because this time the compiler will put the array in a separate part of RAM from the code.

With floating point numbers the situation gets even worse. The PIC32MX does not have a hardware floating point unit. This means that all floating point operations are executed as calls to library routines which are not part of the Cfunction code and therefore will fail.

What this means is that you cannot use any operators on floating point numbers including both arithmetic and logical operators
+ - / * < > != == ^
are all banned

To get round this in 4.7 Geoff has included a set of basic functions that can replicate these so:
a=FAdd(b,c); adds b and c and puts the answer into a
FMul, FSub, and FDiv operate in similar fashion

To deal with the logical operators there is a single function:

a=FCmp(b,c); this returns 1 if b>c, 0 if b=c, and -1 if b<c

Together, these allow us to do pretty much any floating point arithmetic

EXCEPT!!!!!

a=FAdd(b,2.0) doesn't work because even converting a literal to its floating point representation does something that breaks the CFunction

Luckily, there is a way round this too:

a=FAdd(b, LoadFloat(0x40000000));

Obvious really, or perhaps not.

0x40000000 is the HEX representation of the number 2.0 expressed as a 32-bit floating point number. I won't go into floating point representations but Wikipedia will explain all. LoadFloat is a special function that puts 0x40000000 into a floating point variable, in this case the LoadFloat function return.

So we need a mechanism to translate any floating point literals we need in our program to their hex representations - I feel a CFunction coming on


long long floatconv(float *a){
char b[11],crlf[3];
union ftype{
unsigned long a;
float b;
}f;
f.b=*a;
crlf[0]=13;
crlf[1]=10;
crlf[2]=0;
b[0]=48; //0
b[1]=120; //x
IntToStr(b+2,f.a,16);
MMPrintString(b);
MMPrintString(crlf);
return f.a;
}


This code mostly uses things we have seen before except the single input is a floating point number. The interesting bit is:


union ftype{
unsigned long a;
float b;
}f;
f.b=*a;


The union says that the "unsigned long a" and the "float b" are actually using the same bit of memory which we have called f. Then I can copy the incoming floating point number *a into f.a, but I can also treat it as an integer by using f.b

I can then use the function we have seen before IntToStr to convert the integer representation into a number-string in the b array in base 16. Note that I am telling the conversion to start at position 2 in b as I have put the characters "0x" into positions 0 and 1. "0x" is the C equivalent of "&H" in Basic.

MMPrintString is then used to output the results of the conversion on the terminal.


dim a!=pi
dim i%=floatconv(a!)
end
CFunction floatconv
00000000
27BDFFC8 AFBF0034 AFB10030 AFB0002C 8C910000 2402000D A3A20024 2402000A
A3A20025 A3A00026 24020030 A3A20018 24020078 A3A20019 3C109D00 24020010
AFA20010 8E020030 27A4001A 02203021 0040F809 00003821 8E02002C 0040F809
27A40018 8E02002C 0040F809 27A40024 02201021 00001821 8FBF0034 8FB10030
8FB0002C 03E00008 27BD0038
End CFunction


My example program converts PI to get the answer 0x40490FDA. Note that the program does not go i%=floatconv(PI) as this would give the wrong answer. The Micromite firmware converts pi to 3 if used this way - a feature.

Of course I could have done the same thing in Basic but that just makes it too easy and we are supposed to be learning about CFunctions

dim a!=pi
i%=peek(varaddr a!)
print "0x"+hex$(peek(word i%),8)
end


This all makes it seem difficult to use floating point numbers in CFunctions and it is indeed a bit clunky.

I'm not going to go into the following code sample but want to show that even complex arithmetic is possible. The following code calculates and displays a two pole bezier curve.


long long fbezier(float *x0, float *y0, float *x1, float *y1,float *x2, float *y2, float *x3, float *y3, long long *colour){//Cfunction
float tmp,tmp1,tmp2,tmp3,tmp4,tmp5,tmp6,tmp7,tmp8,t,xt,yt;
int xti,yti,xtlast=-1, ytlast=-1;
int i;
t = 0x0;
for (i=0;i<500;i++)
{
tmp =FSub(LoadFloat(0x3f800000),t);
tmp3=FMul(t,t);
tmp4=FMul(tmp,tmp);
tmp1=FMul(tmp3,t);
tmp2=FMul(tmp4,tmp);
tmp5=FMul(LoadFloat(0x40400000), t);
tmp6=FMul(LoadFloat(0x40400000), tmp3);
tmp7=FMul(tmp5,tmp4);
tmp8=FMul(tmp6,tmp);
xt = FAdd(FAdd(FAdd(FMul(tmp2 , *x0) ,
FMul(tmp7 , *x1)) ,
FMul(tmp8 , *x2)) ,
FMul(tmp1 , *x3));

yt = FAdd(FAdd(FAdd(FMul(tmp2 ,*y0) ,
FMul(tmp7 , *y1)) ,
FMul(tmp8 , *y2)) ,
FMul(tmp1 , *y3));
xti=FloatToInt(xt);
yti=FloatToInt(yt);
if((xti!=xtlast) || (yti!=ytlast)) {
DrawPixel(xti, yti, *colour);
xtlast=xti;
ytlast=yti;
}
t=FAdd(t,LoadFloat(0x3B03126F));
}
return 0;
}





Edited by matherp 2015-09-19
 
Zonker

Guru

Joined: 18/08/2012
Location: United States
Posts: 761
Posted: 04:12am 19 Sep 2015
Copy link to clipboard 
Print this post

Wow..! A PID controller in a C Function... Awesome....
This would be quite useful in external circuits....

You are amazing fine Sir..!
 
matherp
Guru

Joined: 11/12/2012
Location: United Kingdom
Posts: 8608
Posted: 01:08am 20 Sep 2015
Copy link to clipboard 
Print this post

This part of the tutorial will introduce some more advanced mechanisms available to CFunctions. Some of this may seem difficult and obscure but stick with it. Much of the code can be used as boiler plate and the two key techniques I will introduce in this post open up the possibilities for CFunctions to an almost unlimited extent.

To build our PID controller, first we need to set up some infrastructure. We want the PID loop to be run on a regular basis. We could probably do this with settick but we are CFunction programmers and settick is for wimps

4.7 includes a facility to call a CFunction from within the 1 msec timer interrupt inside the Micromite firmware. To use this we just need to load the address of the CFunction into "CFuncmSec" and then C uses the mechanism of pointers to functions to execute it.

To calculate the address of our CFunction we need to pass the CFunction address of an initialisation Cfunction to itself and then this initialisation routine can calculate and set up the interrupt to use the actual runtime CFunction.

We probably also need to make parameters available to the runtime CFunction (in a PIDs case Kp, Kd, Ki, and dt) and to do this we need some memory to be allocated which is preserved between calls to the CFunction and we need the runtime CFunction to know where that memory is.

The following example does all these things:


#include "../cfunctions.h"

void msec(void){
unsigned int * p;
p=(void *)(unsigned int)StartOfCFuncRam;
p[1]++; //increment p[1]
if(p[1]==p[2]){ //check whether it is time to execute
PinSetBit(p[0], LATINV);
p[1]=0;
}
}


long long main(long long *func, long long *MyAddress, long long *digoutpin, long long *rate){
int Offset;
int * p; //Set up a pointer which will be used to access the allocated memory
//If already Open just use the memory
if (StartOfCFuncRam){
p=(void *)(unsigned int)StartOfCFuncRam;
}else{
//Get some persistent memory
p = GetMemory(256);
//Save the address for other functions
StartOfCFuncRam=(unsigned int)p;
}
//Close Request ?
if (*func==1){
CFuncmSec=0;
FreeMemory(p);
StartOfCFuncRam=0;
return 0;
}

if ((unsigned int)&msec < (unsigned int)&main){ //calculate the address of the msec routine
Offset=*MyAddress - ((unsigned int)&main - (unsigned int)&msec);
}else{
Offset=*MyAddress + ((unsigned int)&msec - (unsigned int)&main);
}

p[0]=*digoutpin;
p[1]=0;
p[2]=*rate;

ExtCfg(p[0],EXT_DIG_OUT,0);

//Set the CFuncmSec vector to point to our msec function
CFuncmSec=Offset;

msec(); //run this once to avoid the compiler "optimising" it away completely

return Offset;

}


Let us start by looking at the runtime function "msec". This starts by getting the address of the permanently allocated memory and putting it into a pointer p

" p=(void *)(unsigned int)StartOfCFuncRam;"

"StartOfCFuncRam" is a memory location defined in CFunctions.h specially for this purpose.

In this example I define p as being a pointer to an integer

"int * p;"

This allows me to use it to access an array of integer values: p[0], p[1], etc.

in the next statement I increment the value in p[1] "p[1]++;" which I am using to count msec between executing my real code:

" if(p[1]==p[2]){ //check whether it is time to execute
PinSetBit(p[0], LATINV);
p[1]=0;
}"

This says invert the state of the output pin whose number is contained in p[0] and then zero my counter. When we write the PID controller the real logic will be inside this conditional.

So that is all easy enough apart from setting up "p", but that is boilerplate.

Now we can look at the initialisation function "main". Note this must always be called "main" and note that we will be converting the binary to our Basic code using the MERGE function in CFGEN. This is essential to package all the code together and allow one CFunction to call another.

"if (StartOfCFuncRam){" checks if the permanent memory is already set up and
"p=(void *)(unsigned int)StartOfCFuncRam;" is the same code we already saw in "msec" to use it if it is.

Assuming the memory isn't already set up we call:

"p = GetMemory(256);"

This gets 256 bytes of memory and sets the pointer p to point to it. As in msec, we can now access this memory as an array "p[n]". Memory is allocated in 256 byte chunks so there is no advantage to asking for less than this.

We then save the address of this memory in the special variable we used in msec:

"StartOfCFuncRam=(unsigned int)p;"

The next section of code looks at the first parameter to "main" *func and if it is set to 1 this triggers a cleanup:
"CFuncmSec=0;" disconnects the interrupt routine
"FreeMemory(p);" returns the permanent memory to the pool
"StartOfCFuncRam=0;" "sets the memory marker to say no memory is allocated

The next section is the most important, here we calculate the address of "msec"

We use "peek(CFUNADDR fname)" in Basic to get the address of "main", the initialisation routine. We then calculate the address of msec from that. We don't know if "msec" is going to be before or after "main" in memory, the compiler seems to vary depending on how it optimises so we check which it is and calculate accordingly.

Next we set up the reference information in the permanent memory that will be accessed by "msec"

"p[0]=*digoutpin;" set the pin which will be toggled
"p[1]=0;" initialise the msec counter
"p[2]=*rate;" set the number of milliseconds between executing the main code

We set up the pin as an output:

"ExtCfg(p[0],EXT_DIG_OUT,0);"

And very finally, we load the address of msec into the memory location that the Micromite firmware uses to check if it needs to run a CFunction:

"CFuncmSec=Offset;"

To avoid the compiler optimising "msec" away we then call it once.

The basic code is then:


dim myaddr%=peek(cfunaddr test)
dim i%=test(0,myaddr%,23,250)
pause 4000
i%=test(1,myaddr%,23)
end
CFunction test
00000016
'msec
27BDFFE8 AFBF0014 AFB00010 3C029D00 8C42008C 8C500000 8E020004 24420001
AE020004 8E030008 14430008 8FBF0014 3C029D00 8C42001C 8E040000 0040F809
24050007 AE000004 8FBF0014 8FB00010 03E00008 27BD0018
'main
27BDFFD8 AFBF0024 AFB40020 AFB3001C AFB20018 AFB10014 AFB00010 00808021
00A09021 00C09821 3C029D00 8C42008C 8C420000 14400007 00E08821 3C149D00
8E82003C 0040F809 24040100 8E83008C AC620000 8E040000 24030001 1483000F
3C039D00 8E030004 1460000C 3C039D00 3C109D00 8E030084 AC600000 8E030044
0060F809 00402021 8E02008C AC400000 00002021 1000001C 00002821 24630058
3C049D00 24840000 0083282B 10A00004 00831823 8E450000 10000003 00659021
8E450000 00659021 8E640000 AC440000 AC400004 8E230000 AC430008 3C109D00
8E020010 24050008 0040F809 00003021 8E020084 AC520000 0411FFAB 00000000
02402021 00122FC3 00801021 00A01821 8FBF0024 8FB40020 8FB3001C 8FB20018
8FB10014 8FB00010 03E00008 27BD0028
End CFunction


As explained above, this gets the address of the Cfunction and calls the initialisation routine specifying to use pin 23 and 250 msec update rate.

This will cause an LED connected to pin 23 to flash twice /second for 4 seconds

Then we call the intialisation routine again with mode=1 to turn off the routine and return everything to normal.

As I said at the beginning, this is the most difficult structure we will be using in any of the tutorials but it can be treated as boilerplate.

We have seen how we can set up a CFunction to run on a regular basis without interacting with Basic.
We have seen how to pass parameters to it and how it can remember things between calls.
And we have seen how to tidy up after ourselves.

Many of the concepts here will also be relevant in the third series of tutorials which will look at display drivers.

Please let me know if this makes sense, PM if you wish



















 
twofingers
Guru

Joined: 02/06/2014
Location: Germany
Posts: 1141
Posted: 01:46am 20 Sep 2015
Copy link to clipboard 
Print this post

  Quote  To build our PID controller, first we need to set up some infrastructure.

I'm also heavily interested in PID constructing!
Thanks for your efforts!

Michael
 
matherp
Guru

Joined: 11/12/2012
Location: United Kingdom
Posts: 8608
Posted: 01:25am 22 Sep 2015
Copy link to clipboard 
Print this post

  Quote  I'm also heavily interested in PID constructing!


That was harder than I expected

Getting the basic PID processing to work was OK but making it useful I had to learn about and program things like "integral windup protection"

First job was to create a testing environment



So I bonded a 100K thermistor to a 20ohm resistor using some thermal glue. One end of the resistor was connected to +5V, the other to the collector of a 2N2222 transistor. The emitter of the transistor was connected to ground and the base to a MM output pin via a 180 ohm resistor. This gave me a simple "heater"

One end of the thermistor was connected to ground and the other to 3.3V via a 4K7 ohm resistor making a potential divider. The mid point of the divider was connected to a MM input pin via a simple RC filter.

This gave me the measuring input for the controller.

The test system has very little thermal mass and the temperature changes very rapidly with just a slight air movement so should provide a good challenge for the controller.

The Basic program uses the thermistor code I have published in a separate thread and is fairly straightforward.


OPTION EXPLICIT
OPTION DEFAULT NONE
const display = 1
const PWMpin = 32
CONST ThermistorPin=24
'
if display then
CLS
line 0,120,319,120,,rgb(green)
endif
DIM i%,j%,k%=0
DIM setpoint as float
dim myaddr%=peek(cfunaddr PID)
dim float setpointtemp=70
dim float setpointresistance=Temp_to_resistance(setpointtemp,0.6985229e-3,2.200879883e-4,0.7970586598e-7)
print "Resistance at ",setpointtemp," = ",setpointresistance
setpoint= Resistance_to_voltage(1024,4700,setpointresistance)'get the target temp in ADC counts
Print "ADC counts ",setpointtemp," should be ",setpoint
'
DIM FLOAT Kp=-2.5,Kd=-10,Ki=-0.5
dim integer dt = 300 'set evaluation rate at 300msec
DIM FLOAT bias=dt*0.6 ' approximate output for stable running
'
dim PIDdata%=PID(0,myaddr%,PWMpin,ThermistorPin,dt,setpoint,Kp,Kd,Ki,bias) 'Start the controller
'
do
i%=peek(word PIDdata%+12) 'get the current tick value
if i%<j% then
logvalues(PIDdata%)
j%=0
else
j%=i%
endif
loop while k%<640
'
i%=PID(1,myaddr%)
end
'
sub logvalues(address%)
local i%,measured_value%,fvaladdress%
LOCAL fval!
fvaladdress%=peek(VARADDR fval!)
i%=peek(word address%+16) 'get the measured value
poke word fvaladdress%,i%
print "measured_value:",cint(fval!)," ";
i%=peek(word address%+36)
poke word fvaladdress%,i%
print "error:",cint(fval!)," ";
i%=peek(word address%+68)
poke word fvaladdress%,i%
print "direct:",cint(fval!)," ";
i%=peek(word address%+76)
poke word fvaladdress%,i%
print "derivative:",cint(fval!)," ";
i%=peek(word address%+72)
poke word fvaladdress%,i%
print "integral:",cint(fval!) ," ";
i%=peek(word address%+52)
poke word fvaladdress%,i%
print "output:",cint(fval! )
i%=peek(word address%+56)
poke word fvaladdress%,i%
if fval!>-120 and display then
pixel k%\2,fval!+120,rgb(white)
k%=k%+1
endif
end sub
'
FUNCTION Temp_to_resistance(T as float,A as float,B as float,C as float) as float
LoCAL x As float = (a - (1 / (T+273.16))) / c
LoCAL y As FLOAT = b / c
LoCAL R As FLOAT = 0
LoCAL a1 As FLOAT = (-x / 2)
LoCAL a2 As FLOAT = (x ^ 2) / 4
LoCAL a3 As FLOAT = (y ^ 3) / 27
LoCAL b1 As FLOAT = (a2 + a3) ^ (1 / 2)
LoCAL Alpha As FLOAT = (a1 + b1) ^ (1 / 3)
LoCAL Beta As FLOAT = Abs(a1 - b1) ^ (1 / 3)
R = (Alpha - Beta)
Temp_to_resistance = exp(R)
end function

Function Resistance_to_voltage(VIN As FLOAT,RTOP as float,RBOTTOM AS FLOAT) AS FLOAT
Resistance_to_voltage=RBOTTOM/(RTOP+RBOTTOM)*VIN
end function

CFunction PID
0000011C
'msec
27BDFF70 AFBF008C AFBE0088 AFB70084 AFB60080 AFB5007C AFB40078 AFB30074
AFB20070 AFB1006C AFB00068 3C029D00 8C43008C 8C720000 8E5E0018 8E43001C
AFA30048 8E470020 AFA70050 8E570014 8E560008 8E43003C AFA30054 8E50000C
26100001 AE50000C 8C42007C 0040F809 8E440034 5C60000B 8EC40000 14600003
0202102B 54400007 8EC40000 3C029D00 8C42001C 8E440000 0040F809 24050005
8EC40000 8E42000C 0044102B 144000E5 8FBF008C 3C109D00 8E130064 8E020080
0040F809 00002821 00408821 8E02009C 0040F809 3C04447A 02202021 0260F809
00402821 AFA2004C 8E470030 AFA70058 8E420028 AFA2005C 8E020068 8E440050
0040F809 8EE50000 54400001 AE400040 8E420028 AE420038 AE40000C 27B30010
24140001 00008821 3C159D00 10000004 2410FFFF 26310001 26730004 26940001
8EA20018 0040F809 8E440004 1220FFF9 AE620000 8E65FFFC 00A2182B 10600014
2626FFFF 2624FFFE 00111880 27A70010 10000009 00E31821 8C65FFF8 8C62FFFC
2487FFFF 00A2302B 10C00009 2463FFFC 00803021 00E02021 00063080 27A70010
00E63021 ACC20000 1490FFF3 AC650000 2E82000E 5440FFE0 26310001 27A20018
27A50040 00002021 8C430000 24420004 1445FFFD 00832021 3C109D00 8E130064
8E020080 0040F809 00002821 00408821 8E02009C 0040F809 3C044120 02202021
0260F809 00402821 AE420010 8E030060 8EE40000 0060F809 00402821 0040A021
AE420024 00409821 8E110068 8E02009C 0040F809 00002021 02802021 0220F809
00402821 2403FFFF 1443000A 0280A821 3C029D00 8C500060 8C42009C 0040F809
00002021 00402021 0200F809 02802821 0040A821 3C029D00 8C430068 AFA30060
8C510064 8EF00000 8C42009C 0040F809 3C044248 02002021 0220F809 00402821
02A02021 8FA70060 00E0F809 00402821 2403FFFF 14430004 3C029D00 24020001
AE420040 3C029D00 8C50005C 8C420058 02602021 0040F809 8FA5004C 8FA40058
0200F809 00402821 8E430040 50600004 AE400030 0040A821 10000002 AE420030
0000A821 3C119D00 8E300064 8E220060 02602021 0040F809 8FA5005C 00402021
0200F809 8FA5004C 00408021 AE42002C 8E220058 8FC40000 0040F809 02602821
00409821 AFA2004C 8E220058 8FA30048 8C640000 0040F809 02A02821 0040A821
8E220058 8FA70050 8CE40000 0040F809 02002821 0040B821 8FA20054 8C420000
AFA20048 8E30005C 02A02021 0200F809 02E02821 8FA4004C 0200F809 00402821
8FA40048 0200F809 00402821 00408021 AE530044 AE550048 AE57004C 8E330068
8E220080 8EC40000 0040F809 00002821 02002021 0260F809 00402821 04400006
3C029D00 8C420080 8EC40000 0040F809 00002821 00408021 3C029D00 8C510068
8C42009C 0040F809 00002021 00402021 0220F809 02002821 04430008 AE400034
12000006 AE500034 3C029D00 8C42001C 8E440000 0040F809 24050006 AE540028
8FBF008C 8FBE0088 8FB70084 8FB60080 8FB5007C 8FB40078 8FB30074 8FB20070
8FB1006C 8FB00068 03E00008 27BD0090
'main
27BDFFD0 AFBF002C AFB60028 AFB50024 AFB40020 AFB3001C AFB20018 AFB10014
AFB00010 00808821 00A0A021 00C09021 00E09821 3C029D00 8C42008C 8C500000
16000008 8FB50044 3C169D00 8EC2003C 0040F809 24040100 00408021 8EC2008C
AC500000 8E230000 24020001 14620019 3C029D00 8E220004 14400016 3C029D00
3C119D00 8E220084 AC400000 8E220044 0040F809 02002021 8E22008C AC400000
8E220010 8E040000 00002821 0040F809 00003021 8E220010 8E040004 00002821
0040F809 00003021 00002021 10000033 00002821 24420470 3C039D00 24630000
0062202B 10800004 00621023 8E840000 10000003 0044A021 8E840000 0044A021
8E420000 AE020000 8E620000 AE020004 3C119D00 8E220010 8E040000 24050008
0040F809 00003021 8E220010 8E040004 24050001 0040F809 00003021 AE000028
AE000030 AE00000C AE000034 AE000040 AE150014 8FA20048 AE020018 8FA20050
AE02001C 8FA2004C AE020020 8FA20040 AE020008 8FA20054 AE02003C 8EA20000
AE020050 8E220084 AC540000 0411FE80 00000000 02002021 00002821 00801021
00A01821 8FBF002C 8FB60028 8FB50024 8FB40020 8FB3001C 8FB20018 8FB10014
8FB00010 03E00008 27BD0030
End CFunction





The code takes a target temperature "setpointtemp=70" and converts it to an ADC reading (0-1023) that the code expects to see if the target is met.

The PID coefficients are then set up "Kp=-2.5,Kd=-10,Ki=-0.5". These have been very roughly tuned. For PID tuning in general consult google.

Then the loop duration for the control is set up "dt = 300" in msec. Finally, as the output isn't centred on zero we set the bias position for the output to be 60% "bias=dt*0.6"

The way the control works is that the PID loop turns on the heater for a part of the PID loop based on the calculations in order to try and achieve and maintain the setpoint.

The PID initialisation routine is then called:

"PIDdata%=PID(0,myaddr%,PWMpin,ThermistorPin,dt,setpoint,Kp,Kd,Ki,bias)"

This sets up the controller working autonomously in the background. No further involvement from Basic is required and it will run until an abort command is issued "i%=PID(1,myaddr%)"

Note that any of the parameters "dt,setpoint,Kp,Kd,Ki,bias" can be changed at any time in Basic and the PID loop will be updated immediately.

For debugging it was important to see what is going on inside the loop. The PID initialisation had returned the memory address (PIDdata%) of a block of data that the PID control loop maintains in real-time.

"PIDdata%+12" is the location of the counter that it uses to count the 300 msec, so by testing when this wraps back to zero we can do some monitoring each time round the loop.

the subroutine logvalues does this and outputs diagnostics about the operation of the loop. It also plots the error on a display if one is connected (use "CONST display=0" at the top of the program if you haven't got a suitable display connected)





The C code is an enhanced version of the code developed in the previous post in this thread. The intialisation routine sets up the main PID control loop to be executed automatically every millisecond and sets up a permanent memory area that allows the interrupt routine to "remember" things and also to allow Basic to communicate with the control loop.

I am using #define to "name" locations in the permanent memory. This just does a text substitution before the compiler runs.


#define DigOutPin p[0]
#define AnaInPin p[1]
#define dtAddress p[2]
#define ticks p[3]
#define measured_value p[4]
#define setpointAddress p[5]
#define KpAddress p[6]
#define KiAddress p[7]
#define KdAddress p[8]
#define error p[9]
#define previous_error p[10]
#define derivative p[11]
#define integral p[12]
#define output p[13]
#define display_previous_error p[14]
#define biasAddress p[15]
#define useintegral p[16]
#define directcomponent p[17]
#define integralcomponent p[18]
#define derivativecomponent p[19]
#define lastsetpoint p[20]


The initialisation is basically identical to that we used earlier except that we are storing more information in the permanent memory and initialising an analogue input channel. In particular we are storing the memory addresses of the Basic parameters to the PID control rather than the values. By doing this, they can be changed in real-time in Basic and the PID loop will respond to those changes.


setpointAddress=(unsigned int)setpoint; //store the addresses of the input variables
getsetpoint=(void *)(unsigned int)setpointAddress; //Get a pointer to the Basic variable setpoint
KpAddress = (unsigned int)Kp; //etc.
KiAddress = (unsigned int)Ki;
KdAddress = (unsigned int)Kd;
dtAddress=(unsigned int)rate;
biasAddress=(unsigned int)bias;


The permanent memory is defined in the code as an integer array. However, we are going to store integers, floats, and addresses in various of the locations. To store floats we use the "union" mechanism we saw earlier. To store addresses we need to tell the compiler to interpret the adresses as integers on the way in:
[code]KiAddress = (unsigned int)Ki;[/code]
but then tell it to use them as pointers on the way out:
[code] float * ki;
ki=(float *)(unsigned int)KiAddress; //Get a pointer to the Basic variable Ki
[/code]

In the main processing in the loop we first test if the heating period has expired and we need to turn it off

f_output.a=output; //restore the saved value of the output
if(ticks>=FloatToInt(f_output.b)){ //has the required output time expired?
PinSetBit(DigOutPin, LATCLR); //Turn off the output
}

Then we test if the main loop timer has reached the required time to execute the main PID code:
"if(ticks>=*dt){ //check whether it is time to execute main PID loop"

We then restore some of the save variables we will need for calculating

dtsecs=FDiv(IntToFloat(*dt),LoadFloat(0x447A0000)); //divide by 1000 to get dt in seconds
f_integral.a=integral;
f_previous_error.a=previous_error;
f_lastsetpoint.a=lastsetpoint;



The next statement deals with the issue of integral windup.
"if(FCmp(f_lastsetpoint.b,*setpoint)!=0)useintegral=0; //disable integral after change of setpoint"
If the setpoint has just changed or we are first time in we may be way off the target temperature. If we do nothing then as the heating takes place the integral component will increase and increase which will cause the loop to misbehave as we near the target temperature. By setting useintegral to zero we can override the integral component until the temperature nears the target. Later on the function we have some code that test whether we are within 2% of the target and if so enables the integer component

abserror=f_error.b;
if(FCmp(f_error.b,LoadFloat(0))==-1){ //error is -ve
abserror=FSub(LoadFloat(0),f_error.b);
}
if(FCmp(abserror,FDiv(*setpoint,LoadFloat(0x42480000)))==-1)useintegral=1; //within 2% of target
/


The next section of the code is shamelessly copied out of Geoff's Micromite firmware. We take 14 ADC readings using an insertion sort into an array. We then average the middle 10


for(l = 0; l < 14; l++) { //read in the ADC, take 14 samples and use middle 10, code copied from MM firmware
b = ExtInp(AnaInPin); // get the value
for(j = l; j > 0; j--) { // and sort into position
if(b[j - 1] < b[j]) {
t = b[j - 1];
b[j - 1] = b[j];
b[j] = t;
}
else
break;
}
}
// we then discard the top 2 samples and the bottom 2 samples and add up the remainder
for(j = 0, l = 2; l < 12; l++) j += b;
f_measured_value.b=FDiv(IntToFloat(j),LoadFloat(0x41200000)); //Get the new input value by dividing by 10
measured_value=f_measured_value.a; //save it for inspection in Basic


Next we use our floating point maths to do the main PID calculation and store the various values so we can inspect them in Basic


// error = setpoint - measured_value
f_error.b=FSub(*setpoint,f_measured_value.b); //calculate the error from the setpoint
error=f_error.a; //save it for inspection in Basic
abserror=f_error.b;
if(FCmp(f_error.b,LoadFloat(0))==-1){ //error is -ve
abserror=FSub(LoadFloat(0),f_error.b);
}
if(FCmp(abserror,FDiv(*setpoint,LoadFloat(0x42480000)))==-1)useintegral=1; //within 2% of target
// integral = integral + error*dt
f_integral.b=FAdd(f_integral.b,FMul(f_error.b,dtsecs)); //Calculate the integral of the error
if(useintegral)integral=f_integral.a; //save it for inspection in Basic
else { //not yet in controlled region so zero
integral=0;
f_integral.a=0;
}
// derivative = (error - previous_error)/dt
f_derivative.b=FDiv(FSub(f_error.b,f_previous_error.b),dtsecs); //Calculate the derivative of the error
derivative=f_derivative.a;
// output = Kp*error + Ki*integral + Kd*derivative
f_directcomponent.b=FMul(*kp,f_error.b);
f_integralcomponent.b=FMul(*ki,f_integral.b);
f_derivativecomponent.b=FMul(*kd,f_derivative.b);
f_output.b=FAdd(*bias,FAdd(f_directcomponent.b, FAdd(f_integralcomponent.b, f_derivativecomponent.b)));
directcomponent = f_directcomponent.a;
integralcomponent = f_integralcomponent.a;
derivativecomponent = f_derivativecomponent.a;


Finally, we make sure the output is in range

// Scale output 0-dt
if(FCmp(f_output.b,IntToFloat(*dt)) >= 0) {
f_output.b=IntToFloat(*dt);
}
if(FCmp(LoadFloat(0),f_output.b)>=0){
f_output.a=0;
}
output=f_output.a;
if(output)PinSetBit(DigOutPin, LATSET);


and store the error value for use in calculating the derivative next time. That concludes this tutorial. Hopefully, you now understand a bit about using floating point numbers in CFunctions, passing parameters to and from Basic, running time based code without Basic involvement, and perhaps even a bit about PID control mechanisms.

The next (and last) tutorial will look at developing a loadable display driver as
a CFunction
The complete code is :


#include "../cfunctions.h"
#define DigOutPin p[0]
#define AnaInPin p[1]
#define dtAddress p[2]
#define ticks p[3]
#define measured_value p[4]
#define setpointAddress p[5]
#define KpAddress p[6]
#define KiAddress p[7]
#define KdAddress p[8]
#define error p[9]
#define previous_error p[10]
#define derivative p[11]
#define integral p[12]
#define output p[13]
#define display_previous_error p[14]
#define biasAddress p[15]
#define useintegral p[16]
#define directcomponent p[17]
#define integralcomponent p[18]
#define derivativecomponent p[19]
#define lastsetpoint p[20]




void msec(void){
union ftype{
unsigned long a;
float b;
}f_error, f_previous_error, f_derivative, f_integral, f_measured_value, f_output, f_directcomponent, f_integralcomponent, f_derivativecomponent, f_lastsetpoint;
unsigned int * p;
float * kp;
float * ki;
float * kd;
float * bias;
float * setpoint;
float dtsecs,abserror;
unsigned int l,j,t,b[14];
unsigned int * dt;
p=(void *)(unsigned int)StartOfCFuncRam; //get a pointer to the permanent memory area
kp=(float *)(unsigned int)KpAddress; //Get a pointer to the Basic variable Kp
ki=(float *)(unsigned int)KiAddress; //Get a pointer to the Basic variable Ki
kd=(float *)(unsigned int)KdAddress; //Get a pointer to the Basic variable Kd
setpoint=(void *)(unsigned int)setpointAddress; //Get a pointer to the Basic variable setpoint
dt=(void *)(unsigned int)dtAddress; //Get a pointer to the Basic variable dt
bias=(float *)(unsigned int)biasAddress; //Get a pointer to the Basic variable bias
ticks++; //increment p[1]
f_output.a=output; //restore the saved value of the output
if(ticks>=FloatToInt(f_output.b)){ //has the required output time expired?
PinSetBit(DigOutPin, LATCLR); //Turn off the output
}
if(ticks>=*dt){ //check whether it is time to execute main PID loop
dtsecs=FDiv(IntToFloat(*dt),LoadFloat(0x447A0000)); //divide by 1000 to get dt in seconds
f_integral.a=integral;
f_previous_error.a=previous_error;
f_lastsetpoint.a=lastsetpoint;
if(FCmp(f_lastsetpoint.b,*setpoint)!=0)useintegral=0; //disable integral after change of setpoint
display_previous_error=previous_error; // just used to see error and previous error in Basic
ticks=0;
for(l = 0; l < 14; l++) { //read in the ADC, take 14 samples and use middle 10, code copied from MM firmware
b = ExtInp(AnaInPin); // get the value
for(j = l; j > 0; j--) { // and sort into position
if(b[j - 1] < b[j]) {
t = b[j - 1];
b[j - 1] = b[j];
b[j] = t;
}
else
break;
}
}
// we then discard the top 2 samples and the bottom 2 samples and add up the remainder
for(j = 0, l = 2; l < 12; l++) j += b;
f_measured_value.b=FDiv(IntToFloat(j),LoadFloat(0x41200000)); //Get the new input value by dividing by 10
measured_value=f_measured_value.a; //save it for inspection in Basic
// error = setpoint - measured_value
f_error.b=FSub(*setpoint,f_measured_value.b); //calculate the error from the setpoint
error=f_error.a; //save it for inspection in Basic
abserror=f_error.b;
if(FCmp(f_error.b,LoadFloat(0))==-1){ //error is -ve
abserror=FSub(LoadFloat(0),f_error.b);
}
if(FCmp(abserror,FDiv(*setpoint,LoadFloat(0x42480000)))==-1)useintegral=1; //within 2% of target
// integral = integral + error*dt
f_integral.b=FAdd(f_integral.b,FMul(f_error.b,dtsecs)); //Calculate the integral of the error
if(useintegral)integral=f_integral.a; //save it for inspection in Basic
else { //not yet in controlled region so zero
integral=0;
f_integral.a=0;
}
// derivative = (error - previous_error)/dt
f_derivative.b=FDiv(FSub(f_error.b,f_previous_error.b),dtsecs); //Calculate the derivative of the error
derivative=f_derivative.a;
// output = Kp*error + Ki*integral + Kd*derivative
f_directcomponent.b=FMul(*kp,f_error.b);
f_integralcomponent.b=FMul(*ki,f_integral.b);
f_derivativecomponent.b=FMul(*kd,f_derivative.b);
f_output.b=FAdd(*bias,FAdd(f_directcomponent.b, FAdd(f_integralcomponent.b, f_derivativecomponent.b)));
directcomponent = f_directcomponent.a;
integralcomponent = f_integralcomponent.a;
derivativecomponent = f_derivativecomponent.a;
// Scale output 0-dt
if(FCmp(f_output.b,IntToFloat(*dt)) >= 0) {
f_output.b=IntToFloat(*dt);
}
if(FCmp(LoadFloat(0),f_output.b)>=0){
f_output.a=0;
}
output=f_output.a;
if(output)PinSetBit(DigOutPin, LATSET);
// previous_error = error
previous_error=f_error.a;
}
}


long long main(long long *func, long long *MyAddress, long long *digoutpin, long long *anainpin,long long *rate, float *setpoint, float *Kp, float *Kd, float *Ki, float *bias ){
int Offset;
unsigned int * p; //Set up a pointer which will be used to access the allocated memory
unsigned int * getsetpoint;
//If already Open just use the memory
if (StartOfCFuncRam){
p=(void *)(unsigned int)StartOfCFuncRam;
}else{
//Get some persistent memory
p = GetMemory(256);
//Save the address for other functions
StartOfCFuncRam=(unsigned int)p;
}
//Clean up if Close Request
if (*func==1){
CFuncmSec=0;
FreeMemory(p);
StartOfCFuncRam=0;
ExtCfg(DigOutPin,EXT_NOT_CONFIG,0);
ExtCfg(AnaInPin,EXT_NOT_CONFIG,0);
return 0;
}

if ((unsigned int)&msec < (unsigned int)&main){ //calculate the address of the msec routine
Offset=*MyAddress - ((unsigned int)&main - (unsigned int)&msec);
}else{
Offset=*MyAddress + ((unsigned int)&msec - (unsigned int)&main);
}

DigOutPin=*digoutpin; //Set up the I/O pins
AnaInPin=*anainpin;
ExtCfg(DigOutPin,EXT_DIG_OUT,0);
ExtCfg(AnaInPin,EXT_ANA_IN,0);

previous_error=0; //Initialise the PID variables
integral=0;
ticks=0;
output=0;
useintegral=0;

setpointAddress=(unsigned int)setpoint; //store the addresses of the input variables
getsetpoint=(void *)(unsigned int)setpointAddress; //Get a pointer to the Basic variable setpoint
KpAddress = (unsigned int)Kp; //etc.
KiAddress = (unsigned int)Ki;
KdAddress = (unsigned int)Kd;
dtAddress=(unsigned int)rate;
biasAddress=(unsigned int)bias;
lastsetpoint = *getsetpoint;

//Set the CFuncmSec vector to point to our msec function
CFuncmSec=Offset;

msec(); //run this once to avoid the compiler "optimising" it away completely

return (unsigned int)p;

}
 
Print this page


To reply to this topic, you need to log in.

© JAQ Software 2024