PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : PID Regler für den AVR auf C implementiert



cumi
28.08.2006, 19:14
Hallo Zusammen

Ich möcht die Geschwindigkeit eines Modellautos mit einem AVR (konkret sehe ich den ATmega8 vor) regeln. Ich habe dazu einmal einen PID-Regler versucht zu implementieren.

Zuerst ein paar Bemerkungen zu meinen konzeptionellen Überlegungen:
Da auf dem AVR auch noch ein paar andere Prozesse am laufen sind, sollte der Regler möglichst schnell sein. Ich habe daher keine Gleitkommazahlen sondern nur signed und unsigned Integer verwendet (meine Typendefinition: IntX ist ein signed Typ, welcher X Bit lang ist, byte ist ein 8Bit langer, word 16bit und dword32 bit langer unsigned Typ). Daher auch die Gewichtungsangabe als zp/zn (zp: Zähler vom Proportionalteil). Der Eingang habe ich noch "skaliert". Ich messe ja eine Frequenz (Wegimpulsgeber). w ist nun die Sollfrequenz (nicht Geschwindigkeit!). Wenn diese grösser als wh (wird extern berechnet, zb. mit 1.2*w) dann gebe ich einfach Vollausschlag auf den Eingang. Ist die Frequenz dazwischen wird diese linear abgebildet.

Nun zu meinen Fragen:
1. Was hält ihr von den oben geschilderten Bemerkungen? Sind die schlau? Oder ist man mit Gleitkommazahlen ebensoschnell? Schneller?
2. Ist der PID-Regler so richtig implementiert?
3. An einigen Stellen habe ich das "überlaufen" der Integer noch nicht verhindert. Wie macht man das schön? Und was kommt eigentlich raus, wenn man einer Variable vom Type Byte den Wert 5*100 (also ein Produkt welches Grösser als 2^8 -1 ist) zuweist? (ich habe auf dieser Maschiene leider gleich keinen Compiler um es auszuprobieren). Wie verhindert man das überlaufen am schönsten?
4. sonstige Bemerkungen? Was würdet ihr noch ändern?


// includes --------------------------------------------------------------------
#include <normlib.h>
#include "global.h"
#include "freq.h"
#include "tim.h"
#include "svo.h"

// local definitions -----------------------------------------------------------
#define BYTE_MIN 0
#define BYTE_MAX 255
#define WORD_MIN 0
#define WORD_MAX 65535
#define DWORD_MIN 0
#define DWROD_MAX 4294967295
#define INT8_MIN -127
#define INT8_MAX 127
#define INT16_MIN -32767
#define INT16_MAX 32767

// global variables ------------------------------------------------------------

// local variables -------------------------------------------------------------
word w,wl,wh; // w:Sollwort, wl: unterer Skalierwert, w2: oberer Skalierwert
byte zp,np,zi,ni,zd,nd; //Gewichtung des p-,i-,d-Anteil
word iSat; //Max/Min Val of ie; must be < INT16_MAX

dword t0; //timestamp of last execution
int16 e0; //e of last execution
int16 ie; //integrated e

// local function declarations -------------------------------------------------
inline static int16 calcE(); //positiv: too fast, negativ: too slow
inline static byte pid(int16 e, dword dt); //0: full power, 255: no power

// global function implementation ----------------------------------------------
void regelInit(){
e0=timGet();
ie=0;
}
void regel(){
svoSet(pid(calcE(),timGetDif(t0)));
t0=timGet();
}

// local function implementation -----------------------------------------------
inline static int16 calcE(){
int16 fDif=freqGet()-w;
if(fDif>0){
if(fDif>=wh)
return INT16_MAX;
else
return fDiff*(INT16_MAX+1)/wh;
}else{
if(er<=-wl)
return INT16_MIN;
else
return fDiff*(INT16_MIN-1)/wl;
}
}
inline static byte pid(int16 e, dword dt){
int16 p,i,d;

//proportional part
p=(e+INT16_MAX+1)*zp/np;

//intergral part
{
int32 tmp=e*dt/F_CPU; //dt must be <1s
if(tmp>0){
if((INT16_MAX-ie)<=tmp)
ie=INT16_MAX;
else
ie+=tmp;
}else{
if((INT16_MIN-ie)<=tmp)
ie=INT16_MIN;
else
ie+=tmp;
}
}
i=ie*zi/ni;

//differential part
d=(e-e0)*dt/F_CPU*zd/nd;
e0=e;
{
int32 tmp=((p+i+d)/(BYTE_MAX+1))+127;
if(tmp>=BYTE_MAX)
return BYTE_MAX;
if(tmp<0)
return 0;
return tmp;
}
}

// EOF -------------------------------------------------------------------------


Vielen Dank für eure Hilfe!
Und ich hoffe, dass all diejenigen, welche in Zukunft einen PID Regler für einen AVR in C suchen hier fündig werden :)

Grüsse
cumi

cumi
30.08.2006, 17:42
hmm, ist niemand hier, der C programmieren kann?

SprinterSB
30.08.2006, 18:27
ad 1) Schau mal in <inttypes.h> bzw. <limits.h>. Für AVR ist Gleitkomma langsamer (falls man nicht bei der Imlementierung von Fixpunkt nen riesigen Bock geschossen hat ;-))

ad 3) Man kann die Werte saturieren, also bei unsigned char zB: 200+200=255. Ist natürlich teurer, weil AVR das nicht mitbringt. Wenn der Wertebereich zu klein ist wird abgeschnitten, dh der Highteil landet in der Tonne. (char) 500 = -12 oder so.

ad 4) Standardisiere: byte --> uint8_t, INT8_MIN --> SCHAR_MIN, etc (siehe ad 1). inlining kann zu langsamerem Code führen! Werd dir klar darüber welche Auswirkungen inline hat!

cumi
30.08.2006, 18:40
Danke für deine Ausführungen!


ad 1) Schau mal in <inttypes.h> bzw. <limits.h>.
Die sprichst von der AVR C Lib oder wie die heisst, oder? Werd sie einmal herunterladen und reinschauen.


ad 3) Man kann die Werte saturieren, also bei unsigned char zB: 200+200=255.
Hmm, das ist genau das, was ich brauche. Dabei handelt es sich um eine eigenschaft von GNU AVR-C-Compiler, oder? Weist du wo sowas dokumentiert ist?


ad 4) Standardisiere [...]
Hmm, wäre eigentlich keine schlechte Idee, nur an welchen Standart? Ich habe gemerkt, dass die AVR-C-Lib (oder wie die genau heisst) einen Standart definiert hat, welcher ziemlich verbreitet ist. Vielleicht werde ich mich da einmal anpassen. Auf der C-Programmierung für PCs benutz man jedoch wieder andere Standarts...
Als ich das letzte mal die AVR-C-Lib genauer angeschaut habe, hat mich einiges gestört. Also ich finde sie hat kein schlaues Konzept (oder mindestens keines, welches für mich nachvollziehbar wäre). Das merkt man am meisten bei der Verzeichnisstruktur. Ich weiss nicht mehr so genau aber ein Verzeichnis, welches "avr/avr/bin/avr" oder sowas heisst ist einfach mühsam und blöd. Vielleicht ist das ja unterdessen anders. Zudem brauche ich persönlich keine Library, welche bis in alle Ewigkeit versucht rückwärtskompatibel zu sein. Irgendwann kann man alte Standart umstosse und neue einführen und diese verwenden. Das andere macht das ganze nur langsam, gross und unübersichtlich.
Daher schreibe ich unterdessen meine eigene Library für die AVRs.

Grüsse cumi

SprinterSB
30.08.2006, 19:18
ad 1) Schau mal in <inttypes.h> bzw. <limits.h>.
Du sprichst von der AVR C Lib oder wie die heisst, oder? Werd sie einmal herunterladen und reinschauen.

inttypes.h und limits.h sind Standard-Header. ANSI-C oder ISO-C-xx. Wenn du keine linits.h anbei hast, besorg dir eine, pass sie an für AVR und leg sie in einen Include-Pfand, den du avr-gcc mit -I mitgibst. (Du wirst limits.h auch in anderen Projekten verwenden wollen).

[Saturierung]. Dabei handelt es sich um eine Eigenschaft von GNU AVR-C-Compiler, oder?
Nein. Saturierung wird von C nicht unterstützt (in gewissem Maße zwar durch die Maschinenbeschreibung, aber das nützt dir nix). Saturierung musst du von Hand machen, sorry.


ad 4) Standardisiere [...]
Hmm, wäre eigentlich keine schlechte Idee, nur an welchen Standart?ANSI-C, ISO-C99, ...

Als ich das letzte mal die AVR-C-Lib genauer angeschaut habe, hat mich einiges gestört. Also ich finde sie hat kein schlaues Konzept (oder mindestens keines, welches für mich nachvollziehbar wäre). Das merkt man am meisten bei der Verzeichnisstruktur...
Die Verzeichnisstruktur ist so bei gcc und den binutils und libc. Nicht nur für AVR, sondern auch bei den anderen 50 Controller/Prozessortypen, auf anderen Plattformen, ... Für nen Anwender ist's ja reichlich egal, wie die Verzeichnisstruktur ist. Target-Zeug steht eben im Target-Verzeichnis, das ist "avr". Darunter stehen die Standard-Includes in "include", darunter die AVR-abhängigen Includes wieder in "avr". That's it. AVR-abhängige Libs/Linkerscripts in avr/lib...

Bibliotheken
Eigene Bibliotheken sind eine gute Idee, wenn du in mehreren Projekten auf die gleichen, Standardisierten Features zurückgreifen willst. Wie etwa Fixpunkt-Rechnung.

cumi
30.08.2006, 20:06
Vielen Dank für deine netten Ausführungen!!

Nun, also, wenn ich eine saturierung selbst implementieren will, wie gehe ich dann vor?
Nehem wir mal an ich möchte zwei bytes multiplizieren. Also im schlimmesten (grössten) Fall wäre dass dann (2^8-1)*(2^8-1). Das gibt einbisschen (500 oder so) weniger als 2^16. Das würde also in ein word passen. Jetzt kann ich schauen, ob es grösser als 255 ist und gegebenenfalls abschneiden, oder geht das anders?


// c soll das Produkt von a und b sein
byte a,b,c;
word tmp=a*b;
if(tmp>=255)
c=255;
else
c=tmp;

Und für die Multiplikation von zwei Word dann einfach gleich mit einem dword (int32_t).

Äh und noch zum inlining. Du hast erwähnt, dass dies den Code verlangsam kann. Also zugegebenerweise habe ich mich nie genau damit beschäftigt. Ich habe jetzt einmal in meinem C-Buch (Programmieren in C von Kernighan und Ritchie) nachgeschaut und nichts dazu gefunden.
Also ich dachte, bei einer inline-funktion, werde der code einfach reinkopiert und dabei noch von den parameter-variabeln eine neue instanz angelegt. Wie das mit dem return genau funktionieren könnte habe ich mir nie überlegt. Irgendwie muss da ja noch ans ende gesprungen werden. Das basiert jedoch auf keinen Unterlagen, das habe ich einfach mal so angenommen.
Ich dachte, mit inline könne man die Ausführgeschwindigkeit des Codes steigern auf kosten der Compilatgrösse. Stimmt das nicht so? Oder in welchen Fällen stimmt das so?

Und noch zu den Verzeichnissen vom GCC. Ok, das mag ja seinen Sinn haben. Ich bin da nur geich einwenig erschrocken und habe mich dann entschlossen eine komplett eigene Bibliothek anzufertigen, damit ich verstehe, mit was ich programmiere. Ich habe auch beim programmieren furchtbar Angst vor dem Unbekannten :), welches im Hintergrund für mich nicht nachvollziehbare Dinge tut. Die IO-Definitionen habe ich zu einem grossen Teil aus der AVR-Lib-C rauskopiert.

Grüsse cumi

Babbage
30.08.2006, 23:46
//differential part
d=(e-e0)*dt/F_CPU*zd/nd;

Ich nehme an Du fährst den Atmega mit ein paar Magahertz.
Falls Du dann : (e-e0)*dt/F_CPU ausrechnest kommt wohl meist irgendwas unter 1 raus, bei nem Integer ergibt das also 0.
(e-e0)*dt muß also um einiges höher also F_CPU sein.

So wie ich das sehe läuft der Regler nicht mit äquidistanten Aufrufen, wird also aufgerufen wenn Zeit ist.
Das ist nach meinen Kenntnissen schlecht für die Reglerqualität. Ist es nicht möglich das in nem Timer zu erledigen?

Babbage

P.S. Ich habe nicht so richtig durch die Implementierung deines Reglers geblickt, ist das nicht normalerweise ein Einzeiler in der Form:
https://www.roboternetz.de/wissen/index.php/Regelungstechnik#PID-Regler

SprinterSB
31.08.2006, 12:49
Nun, also, wenn ich eine saturierung selbst implementieren will, wie gehe ich dann vor?
Genau wie du es geschreiben hast.

Wenn es optimal sein soll nimmst du Assembler, das spart pro Multiplikation ein paar Takte/Instruktionen; bei signed saturation wohl noch mehr.

#define UCHAR_MAX 255
#define CR "\n"
#define CR_TAB CR"\t"

#define UMUL8_SAT(a,b) \
({ \
unsigned char __tmp; \
asm volatile ( \
"mul %1, %2" CR_TAB \
"mov %0, r0" CR_TAB \
"tst r1" CR_TAB \
"breq 0f" CR_TAB \
"ldi %0, %3" CR_TAB \
"clr r1" CR \
"0:" \
: "=d" (__tmp) \
: "r" (a), "r" (b), "i" (UCHAR_MAX)); \
__tmp; \
})


Je nachdem, welche Werte du hast, kann es günstiger sein, statt immer wieder zu saturieren eine Fixpunktarithmetik auf normierten Werten zu verwenden, z.B. eine 8.8 Aritmetik (8 Bit vorm Komma, 8 dahinter). Wenn die Werte zB immer im Intervall
inge auch eine 0.16 Arithmetik...

Kommt drauf an, welche Genauigkeit du brauchst und welchen Aufwand du für welche Effizienz betreiben willst.

[quote="cumi"]Äh und noch zum inlining. [...]
Ich dachte, mit inline könne man die Ausführgeschwindigkeit des Codes steigern auf kosten der Compilatgrösse. Stimmt das nicht so? Oder in welchen Fällen stimmt das so?
Falls eine Funktion geinlint wird, dann wird anstatt Parameteraufbereitung - Paramerübergabe - Aufruf - Returnübergabe - Parameternachbereitung das Stückchen Syntaxbaum, welches die inline-Funktion darstellt, an der Stelle ihres Aufrufs in den Syntaxbaum des Aufrufers reinkopiert. Lax gesprochen.

Da der Code der Funktion bekannt ist und die Funktion nicht mehr als Blackbox anzusehen ist, ermöglich das Optimierungen, insbesondere bei der Registerverwendung. Natürlich ist vom Inlining nicht nur der Code der Funktion selbst betrofen, sondern auch der Code des Aufrufers. Inlining ist ja nich lokal auf die Aufrufstelle beschränkt und beeinfluus auch die Registerallokierung des Aufrufers.

Ist nun die i-Funktion komplex und braucht viele Register, dann kann das dazu führen, daß für den Aufrufer ein Frame angelegt werden muß und lokale Variablen nicht mehr in Register leben, sondern im Frame der Funktion. Das ist ungünstig, denn ein RAM-Zugriff ist deutlich ineffizienter als Werte in GPRs zu halten. Die Slot-Zugriffe machen den Code langsamer und breiter.

Die Intention von "inline" ist, in eine bestimmt Richtung zu optimieren. Wenn einem die Effizienz von optimiertem gcc-Code nicht genügt (und die ist schon beachtlich gut), dann muss man sehr genau wissen, was man tut, um weiter in Richtung Optimum zu kommen. Ansonsten geht das ruck-zuck nach hinten los. Je näher man schon am Optimum ist, desto mehr Wege führen eben wieder vom Optimum weg...


Mühsam ernährt sich das Eichhörnchen

gcc hat intern Heuristiken, anhand derer bestimmt wird, ob eine Funktion es würdig ist, geinlinet zu werden. Dabei können auch Funktionen geinlint werden, die nicht mit "inlinie" gekennzeichnet sein, oder solche mit "inline" werden davon ausgenommen, AFAIK. Das statisch zu analysieren wäre viel zu aufwändig und spekulatives Compilieren wird nicht gemacht, da das ebenfalls sehr (zeit)aufwändig wäre.


Und noch zu den Verzeichnissen vom GCC. Ok, das mag ja seinen Sinn haben. Ich bin da nur geich einwenig erschrocken und habe mich dann entschlossen eine komplett eigene Bibliothek anzufertigen, damit ich verstehe, mit was ich programmiere. Ich habe auch beim programmieren furchtbar Angst vor dem Unbekannten :), welches im Hintergrund für mich nicht nachvollziehbare Dinge tut. Die IO-Definitionen habe ich zu einem grossen Teil aus der AVR-Lib-C rauskopiert.

Verstehen zu wollen, was man tut, ist heutzutage eine aussterbende Tugend...

GCC ist ein Moloch. Rund 4.000.000 Zeilen Quellcode verteilen sich auf über 15.000 Dateien. Da sind weder die libc noch die binutils (Assembler, Linker, Debugger, ...) mitgezählt.

Zweifellos schleppt gcc viele Altlasten mit sich herum. Seine Designpattern gehen auf die 80-er Jahre zurück. Solch ein Schlachtschiff komplett umzubauen und zig Mannjahre drauf zu verwenden, ohne was an Funktionalität zu gewinnen, wurde bislang nicht angegangen. Ohnehin gibt es nur eine handvoll Leute auf diesem Planeten, die *wirklich* durchblicken, was in den Gedärmen von gcc wirklich geschieht. Neuerungen finden eher im Bereich "Effizienzsteigerung" statt, im Bereich "Redesign" nur sehr zurückhaltend.

IMHO ist C auch keine angemessene Sprache, um einen Compiler wie gcc zu programmieren. Man müsste erst mal eine adäquate Sprache entwickeln...