PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : sehr Kurze Warteschleifen für das korrekte Timing



Siro
23.06.2016, 10:58
Ich hab mal eine Frage in die Runde:

Wenn ich z.B. ein LCD Display ansteuere und im Datenblatt steht:
Data Setup Time mindestens 500ns,

muss ich damit rechnen, dass meine CPU schneller arbeitet und demnach
ist ein Wartezyklus erforderlich.

Im Einfachsten Falle geht das natürlich mit delay_ms(1);
Einen Millisekundentimer hat man ja meist immer am laufen.

Ist natürlich "reine Zeitverschwendung" :-)

Nicht lachen.....

Nun könnt ich mit NOP's die Zeit überbrücken.
ein NOP bei mir sind 13,88ns (72MHz Takt)
das wären dann 500ns / 18,88 = 36,023 NOPs für 500ns, also 37
Ist natürlich recht speziell und nicht sonderlich kompatibel

Oft sehe ich "for i" Schleifen, die je nach Optimierung und Compiler Einstellungen
aber völlig undefinierte Zeiten erzeugen.

Leide ich am Zeitverschwendungssyndrom ? oder wie macht Ihr das...

Siro

HaWe
23.06.2016, 11:50
ich verwende für solche "Probleme" grundsätzlich Multitasking, und zwar
- für Arduino: Scheduler.h (kooperativ), bei delay() wird die Timeslice trotzdem auf den nächsten Task weitergestellt, es geht also keine cpu-Zeit "verloren" ("non-blocking"),
- und für den Raspberry Pi: POSIX pthread (preemptiv), da kann sowieso jeder Task tun was nötig ist, sogar mit verschiedener Priorität, und delay() klappt hier ebenso "zeitsparend" ("non-blocking") über die wiringPi lib.

(ein Display ist ja nicht zeitkritisch, man kann sowieso nur alle 50 Milli- (!) Sekunden ein neues Bild wahrnehmen - es darf nur der übrige Programmablauf dadurch nicht blockiert werden.)

Siro
23.06.2016, 16:00
Hallo HaWe, Danke für deine Rückmeldung.
Mit Multitasking ist das sicher eine gute Lösung.
Habe bisher nur mit "einem" Task und tonnenweise Interrupts gearbeitet. ;)
Da möcht ich mein System natürlich nicht ausbremsen.

Ich hab grad ein Paradebeispiel für "unsicheren" Code.

Ohne meine Compileroptimierung läuft das Display einwandfrei.
Schalte ich die "C" Optimierung ein, egal welche Stufe, funktioniert das Display nicht mehr.

Das Problem ist hier eindeutig das "Timing" was ich grad versuche "sinnvoll" zu ordnen.



void LCD_DataOut8(U8 data)
{
LCD_RS_LOW;
LCD_RW_LOW;
LCD_EN_HIGH;
Delay_ms(2); /* ist hier nicht erforderlich */

/* write the data byte, THREAD SAFE ??? RMW problem auf den PORTs */
GPIO_CLR1 = LCD_DATA_BITS;
GPIO_SET1 = ((U32)(data) << 20);
Delay_ms(2); /* OHNE Delay gibt es ein TIMING PROBLEM sofern die C-OPTIMIERUNG eingeschaltet ist */

LCD_EN_LOW;
Delay_ms(2); /* normalerweise nicht erforderlich */
}

Mxt
23.06.2016, 16:01
Warten mit NOPs geht natürlich bei "richtigen" Mikrocontrollern. Alles was z.B. die Taktfrequenz regelt oder mehrere Kerne hat, ist da weniger geeignet.

Für hardwarenahe Programmierung fehlt mir meist die Zeit, meist greife ich für schnelle Entwicklungen auf Dinge wie mbed zurück, da gäbe es mehrere Ansätze:
https://developer.mbed.org/handbook/Timeout
https://developer.mbed.org/handbook/RTOS#rtos-timer
https://docs.mbed.com/docs/getting-started-mbed-os/en/latest/Full_Guide/MINAR/

Ceos
23.06.2016, 16:07
meiner Erfahrung nach, haben alle Controller < 100Mhz irgendwo ein Register oder ein Makro zur Compilezeit, welches über die Taktrate informiert und in der regel innerhalb der <delay.h> ausgewertet wird. An diese Methode kann man sich ja anhängen! Controller > 100Mhz (in der Regel auch Mehrkerner) haben fast immer ein Cycleregister, weches nebenher in einem festen Takt (i.d.R. definiert aber auch manchmal programmierbar) mitzählt. Der Zugriff darauf ist dann immer die Kunst :D

Siro
23.06.2016, 16:10
@Picture.
Bei den PICs hab ich auch die ASM Zyklen gezählt, das ging wunderbar.

Bei "C" weia man aber leider nicht was der Compiler draus macht. Ich hab grad das kalte Grausen bekommen als ich mir den "optimierten" ASM Code angesehen habe.
Kann man kaum noch nachvollziehen.
Ich möcht halt "möglichst sicher" programmieren und da lohnt es ab und an mal etwas genauer "nachzuhaken" wie man das am besten macht.
Oder sogar mal etwas "experimentieren"

@Mxt:
Ich programmiere immer "alles neu" ganz unten. Benutze keine Bibliotheken.
Damit mache ich mir zwar das Leben schwer, aber ......

@Ceos:
Ja, da bin ich auch grad dran. Ich lasse einen Timer frei laufen, im Mikrosekundentakt.
Den Zählerstand lese ich und warte bis er xxx Mikrosekunden überschritten hat.

count = TimerCount+20; /* warte 20 Mikrosekunden */
while (Timercount < count) ;

Problem: Der Timer läuft bei 32 Bit nach rund 1,19 Stunden über
Das muss ich noch berücksichtigen....

Sisor
23.06.2016, 16:36
Überlaufproblem: Sofern 'TimerCount' und 'old' unsigend sind, geht folgendes:


old = TimerCount;
while (Timercount-old < 20) ; // warte 20µs

Klebwax
23.06.2016, 16:56
@Picture.
Bei den PICs hab ich auch die ASM Zyklen gezählt, das ging wunderbar.

In C muß man das natürlich nicht. Dafür gibts fertige Funktionen bzw Macros (XC8 Users Guide)

__DELAY_MS, __DELAY_US, __DELAYWDT_US, __DELAYWDT_MS
Synopsis
__delay_ms(x) //request a delay in milliseconds
__delay_us(x) //request a delay in in microseconds

Description
It is often more convenient to request a delay in time-based terms, rather than in cycle
counts. The macros __delay_ms(x) and __delay_us(x) are provided to meet this
need. These macros convert the time-based request into instruction cycles that can be
used with _delay(n). In order to achieve this, these macros require the prior defini-
tion of preprocessor symbol _XTAL_FREQ, which indicates the system frequency. This
symbol should equate to the oscillator frequency (in hertz) used by the system.
On PIC18 devices only, you can use the alternate WDT-form of these functions, which
uses the CLRWDT instruction as part of the delay code. See the _delaywdt function.
The macro argument must be a constant expression. An error will result if these macros
are used without defining the oscillator frequency symbol, the delay period requested
is too large, or the delay period is not a constant.

Diese Funktionen/Macros werden natürlich nicht "wegoptimiert". Also am Anfang des Codes
#define _XTAL_FREQ xxxx

und dann einfach

_delay_us(1)

Bei Zeiten im Submikrosekundenbereich lohnt die Verwendung eines Timers nicht wirklich.

MfG Klebwax

PS: warum du eigentlich ein Delay von 2 Millisekunden programmierst, obwohl du eigenlich 500 Nanosekunden brauchst, erschließt sich mir nicht ganz.

Peter(TOO)
23.06.2016, 17:46
Hallo, Siro,

Nicht lachen.....

Nun könnt ich mit NOP's die Zeit überbrücken.
ein NOP bei mir sind 13,88ns (72MHz Takt)
das wären dann 500ns / 18,88 = 36,023 NOPs für 500ns, also 37
Ist natürlich recht speziell und nicht sonderlich kompatibel
NOPs verwendet man eigentlich nur, wenn man nur ein paar braucht, sonst bremst man die CPU aus.
Etwas längere Zeiten erreicht man mit einem Aufruf einer leeren Unterroutine, besonders das Retten und Laden vom Stack braucht doch einige Zyklen mehr als ein NOP.


Oft sehe ich "for i" Schleifen, die je nach Optimierung und Compiler Einstellungen
aber völlig undefinierte Zeiten erzeugen.

Leide ich am Zeitverschwendungssyndrom ? oder wie macht Ihr das...
Auf Hochsprachen-Ebene muss man sich halt mit dem Compiler auseinandersetzen und die Optimierung für so eine Schleife ausschalten. Sonst optimiert der Compiler diese Zeitverschwendung einfach weg :-( Schon ein Compiler- oder Interpreter-Update kann da alles verändern.
Die for-Schleife kann man noch etwas optimieren, indem man die CPU-Frequenz berücksichtigt oder das Timing der Schleife misst.

BTW: In alten Zeiten hat der Norton-Benchmark eine Multiplikation als Zeitelement verwendet. Da dieser Befehl im 8086 als Mikrocode implementiert war, brauchte er eine Unmenge am CPU-Takten. Ab dem 80186 war dieser Befehl dann aber als Hardware implementiert. Die Benchmarks schossen dann auf diesen CPUs durch den Bildschirm!

Wenn man einen passenden Timer-Interrupt hat, kann man es über diesen lösen. Das Problem dabei ist, die Grenze zwischen NOPs und Interrupt zu finden. Das Aufrufen eines Interrupts kostet ja zusätzliche Zeit.

Je nachdem kann die DMA auch helfen, diese braucht nur zum Aufsetzen CPU-Zeit.

Eine weitere Möglichkeit sind noch Wait States, falls vorhanden. und entsprechend programmierbar. Allerdings wird dabei auch nur CPU-Zeit verbraten, wie bei NOPs.

MfG Peter(TOO)

witkatz
23.06.2016, 17:47
Hallo Siro,

count = TimerCount+20; /* warte 20 Mikrosekunden */
while (Timercount < count) ;
mit while blockierst du wieder den µC. Wenn der PIC in der Wartezeit noch andere Sachen tun soll, geht das nur über kooperatives Multitasking. Für Routinen, die "nebenbei" laufen sollen, könnte man eine Schrittkette nach folgendem Muster verwenden:
void DoJob1(void){
static unsigned short Evt = 0;
static char step = 0;

// getaktete Schrittkette
if((TMR0 - Evt) < 0x7fff ) {
Evt = TMR0 + 1000; // nächter Schritt in mind. 1ms
switch (step){
case 0:
// Do Something
step ++;
break;
case 1:
// Do Something
step ++;
break;
case 2:
// Do Something
Evt = TMR0 + 5000; // nächter Schritt in 5ms
step = 0;
break;
}
}
}mit Timer0 konfiguriert mit 1MHz und 16Bit Breite auf PIC18F. Die Funktion muss mindestens alle 30ms gepollt werden. Wenn die Zeit "reif" ist, wird ein Schritt ausgeführt und der Zeitwert für den nächsten Schritt gesetzt.

Klebwax
23.06.2016, 18:22
Hallo Siro,
mit while blockierst du wieder den µC. Wenn der PIC in der Wartezeit noch andere Sachen tun soll ....

Was soll in 500ns (in Worten Nanosekunden) wohl sinnvolles passieren? Da kann man kaum einen Call, ein Return und das Retten von einem Register unterbringen.


NOPs verwendet man eigentlich nur, wenn man nur ein paar braucht, sonst bremst man die CPU aus.

Also genau hierfür, wo man um eine lahme HW herumprogrammieren muß

MfG Klebwax

witkatz
23.06.2016, 21:33
Was soll in 500ns (in Worten Nanosekunden) wohl sinnvolles passieren?
Dafür natürlich nicht. Ein Zeichen ausgeben mit einem (oder zwei bei 4bit Anschluss) Strobe auf Enable des LCD kann natürlich in einem Schritt untergebracht werden.
Zwei Postings weiter hat Siro ein __delay_ms(2) - Wartezeit für die Ausführung des LCD Kommandos. Auch wenn hier 100µs ausgereicht hätten - dafür lohnt sich das Multitasking schon mal - wenn der µC parallel noch andere Aufgaben rechnet.

- - - Aktualisiert - - -

meine Schrittkette sieht z.B. so aus:
if(Tmr8bit250usLcd == 0){
Tmr8bit250usLcd = 4; // Standardtaktung 1ms, kann in der Statemachine überschrieben werden
static int8_t step = 0;

switch(step){
case 0: // Set DDRAM Adresse = 0x00
LCD_TRIS_DATA = 0; // Data Pins to Outputs
LCD_RS = 0; // command
LCD_DATA = 0b10000000;
strobeE();
Tmr8bit250usLcd = 20; // next call 5ms
Idx = 0; // Initilaisiere Zeichenausgabe
step++;
break;

case 1: // 16 Zeichen der Line 1 ausgeben
LCD_RS = 1; // data
LCD_DATA = LCD_line1[Idx];
if (LCD_DATA == 0)
LCD_DATA = ' ';
strobeE();
Idx++; // Nächstes Zeichen der Zeile
if (Idx >= 16) {
Idx = 0;
step++;
}
break;
//usw

mit
void strobeE(){
LCD_E = 1;
__delay_us(1); // delay 1us
LCD_E = 0;
}
Die Timervariable Tmr8bit250usLcd wird in einem 250µs Timerinterrupt dekrementiert.
if (Tmr8bit250usLcd != 0)
Tmr8bit250usLcd--;

Ist natürlich nur ein Vorschlag, vielleicht habe ich Siro's Frage auch nur falsch verstanden ...

Peter(TOO)
23.06.2016, 23:48
Hallo,

Was soll in 500ns (in Worten Nanosekunden) wohl sinnvolles passieren? Da kann man kaum einen Call, ein Return und das Retten von einem Register unterbringen.
Er bekommt in die 500ns immerhin 37 NOPs rein @72MHz.

Je nach CPU kann man in dieser Zeit schon einiges rechnen.

- - - Aktualisiert - - -


ich verwende für solche "Probleme" grundsätzlich Multitasking, und zwar
Auch bei 72MHz reichen die 500ns nicht einmal für einen Taskswitch des Schedulers aus. Hier ist dann meist die Anzahl CPU-Register das Problem.
Bei 32 Registern und einem PUSH/POP ALL-Befehl kommt man für den Wechsel auf mindestens ein Äquivalent von etwa 100 NOPs. Dann nochmals zurück, sind dann 200 NOPs, wo 37 reichen würden. Wenn die Busbreite nur die Hälfte der Registerbreite beträgt ist es gleich mal das Doppelte.

500ns sind einfach eine doofe Zeit für dieses Problem :-(

Eine Möglichkeit habe ich noch vergessen, meist hat man so etwas:


put_LCD(char c)
{
Daten ausgeben
Delay(500ns)
Strobe setzen
Strobe rücksetzen
}

LCD_Out(char *s)
{
while (*s)
{
put_LCD(*s);
s++;
// evtl. noch weiteres.
}
}

Wenn man jetzt eben nicht streng strukturiert programmiert, kann man "s++:" und weiteres in das delay(500ns); verlegen.


put_LCD(char c)
{
c ausgeben
Delay(500ns)
Strobe setzen
Strobe rücksetzen
}

LCD_Out(char *s)
{
while (*s)
{
c ausgeben
s++;
// evtl. noch weiteres.
Delay(Restzeit)
Strobe setzen
Strobe rücksetzen
}
}

Ist dann halt nicht so übersichtlich, aber sogar schneller, weil der Overhead für den Aufruf von put_LCD() wegfällt.
Allerdings muss man sehen, dass man die Optimierung des Compiler ausschaltet, sonst weiss man nicht was ab geht.
Sinnvoll kann auch der Einsatz von Inline Assembler an dieser Stelle sein, das kann der Compiler dann nicht optimieren und man hat immer den selben Code.
Allerdings trägt man dann selber die Verantwortung, wenn sich die CPU-Frequenz ändert. Das lässt sich aber teilweise mit Macros automatisieren.
Besonders bei CISC-CPUs muss man dann aber noch aufpassen, dass kompatible CPU-Versionen nicht unbedingt die selbe Zyklenzahl für die Befehle benötigen.
Bei RISC-CPUs ändert die Zyklenzahl meistens nicht, da sie meistens der Anzahl Speicherzyklen entspricht.
In beiden Fällen spielt dann aber noch das Pipelining und vorhandener Cache ins Timing mit rein.
Hier kann es auch hilfreich sein, JMPs an Stelle der NOPs zu verwenden.

Ach, früher war das alles einfacher :-(

MfG Peter(TOO)

Klebwax
23.06.2016, 23:51
Hallo,

Er bekommt in die 500ns immerhin 37 NOPs rein @72MHz.

Je nach CPU kann man in dieser Zeit schon einiges rechnen.

Nicht wirklich. Erst mal muß man einige Register retten, sie mit neuen Werten laden, dann ist die halbe Zeit rum und man speichert die Werte zurück und lädt die Register zurück und hat noch nichts gerechnet. Und je schneller die CPU, desto mehr Register sind betroffen und desto langsamer ist der Speicher im Vergleich zu Registern. Zeitscheiben im einstelligen Mikrosekundenbreich sind wegen des Taskswitchoverhead keine brauchbare Idee, mehr eine akademische Luftnummer.

Aber es läßt sich sicher ein exotisches Beispiel finden, wo das sinnvoll ist.

MfG Klebwax

Peter(TOO)
24.06.2016, 00:43
Hallo Klebwachs,

Nicht wirklich. Erst mal muß man einige Register retten, sie mit neuen Werten laden, dann ist die halbe Zeit rum und man speichert die Werte zurück und lädt die Register zurück und hat noch nichts gerechnet. Und je schneller die CPU, desto mehr Register sind betroffen und desto langsamer ist der Speicher im Vergleich zu Registern. Zeitscheiben im einstelligen Mikrosekundenbreich sind wegen des Taskswitchoverhead keine brauchbare Idee, mehr eine akademische Luftnummer.
Das habe ich alles in der Antwort an HaWe ausführlich erläutert.

MfG Peter(TOO)

Siro
24.06.2016, 09:52
Guten Morgen, Ihr wart ja fleissig , erstmal vielen Dank
für Eure Informationen und Ideen.

@ Sisor:
Ja, Du hast recht, so kann ich den Timerüberlauf ignorieren. Gefällt mir sehr gut.


@Klebwax:

Ich hab die "2ms" nur zum Testen/Bestätigen das dort das Problem liegt eingebaut.
Das ist natürlich völlig "überdosiert"


Die Idee, dass der Compiler an bestimmten Stellen NICHT optimiert, wäre in meinem Falle
sogar auch eine Lösung, find ich aber etwas unschön.


@Witkatz:
Ich habe so etwas in der Art auch schon implementiert.
Sieht fast aus wie dein Code :



typedef struct
{
void (*function)(); /* do this function */
U32 microSec; /* do next function after xxx micro second */
} t_TimerFunc;

/* shift out time for one byte is 17,92 microsec. we calc with 25 for every byte */
/* the latch signal are set to 5 microsec. active phase and 5 microsec. recovery time */

const t_TimerFunc functable[] = {

{fn_shift_out_leds,100}, /* shift out 32 Bit LED code, after 100 microsec. */
{fn_DISP_ENABLE_LEDS_HIGH,5}, /* generate the latch signal for 5 microsec. high*/
{fn_DISP_ENABLE_LEDS_LOW,5}, /* reset the latch signal to low */

{fn_shift_out_pressure,50}, /* shift out 16 Bit data for pressure display, after 50us*/
{fn_DISP_ENABLE_P_HIGH,5}, /* generate the latch signal for 5 microsec. high */
{fn_DISP_ENABLE_P_LOW,5}, /* reset the latch signal to low */

{fn_DISP_ENABLE_Q_LOW,5}, /* enable data for the flow display */
{shift_out_flow,100}, /* shift out, 5 Bytes, data for flow display */
{fn_DISP_ENABLE_Q_HIGH,5}, /* disable data for the flow display */

{fn_DISP_ENABLE_V_LOW,5}, /* enable data for the volume display */
{fn_shift_out_volume,125}, /* shift out data, 5 Bytes, for volume display */
{fn_DISP_ENABLE_V_HIGH,5}, /* disable data for the volume display */

. . . .

Wie dem obigen Codeausschnitt zu entnehmen ist, wurde eine spezielle Struktur angelegt um die Vorgehensweise
möglichst übersichtlich zu gestalten. Der Timer Interrupt ruft jeweils die entsprechende Funktion auf und
startet dann eine neue Zeitzähleinheit. Nach Ablauf der Zeit wird die nächste Funktion aufgerufen.
Wurden alle Funktionen durchlaufen, wird wieder mit der ersten Funktion in der Tabelle begonnen.
Die entsprechende Timer Interrupt Funktion konnte so kurz und übersichtlich implementiert werden.
Für dieses Vorgehen wurde der Prozessorinterne Timer 1 verwendet.
fn_xxxxxx sind die entsprechenden Funktionen die aufgerufen werden.

void TMR1_IRQHandler (void)
{
functable[timer1FuncNo].function(); /* do the selected function */
LPC_T1TCR.bits.CR = 1; /* set reset STOP and HOLD Timer */

/* set the new time for the next interrupt */
LPC_T1MR0 = functable[timer1FuncNo].microSec;

/* selects the next function which want to call from this interrupt */
if (++timer1FuncNo >= ((sizeof(functable) / sizeof(t_TimerFunc))))
timer1FuncNo = 0;

LPC_T1IR.bits.MR0INT = 1; /*clear the timer match interrupt */
LPC_T1TCR.bits.CR = 0; /* release reset START Timer */
}



Um Sicherzustellen, dass alle Funktionen weniger Zeit benötigen, als angegeben, wurden die Laufzeiten mit einem
Oszilloskop gemessen. Dazu wurde am Anfang der Funktion ein Portbit gesetzt und am Ende der Funktion wieder gelöscht.
Dieses Portbit konnte so ohne Probleme mit einem Oszilloskop abgegriffen werden und die Laufzeiten anhand der
High Phase des Signal gemessen werden. Die Laufzeiten wurden mit verschiedenen C-Compiler Optionen gemessen
um sicherzustellen, daß unabhängig von den Compiler Einstellungen, die Zeiten eingehalten werden.
Es wurde auf Speed, Size und Balanced mit hoher, niedriger und völlig ohne Optimierung gemessen.
Die langsamste Variante war die völlig ohne Optimierung. Diese maximalen Zeiten wurden in der Software,
in den entsprechenden Funktionen, als Kommentar mit aufgenommen.

Es wurde jedoch aus Sicherheitsgründen noch ein Rekursionstest eingeführt. Sollte sich die
Software innerhalb einer Funktion der Tabelle befinden und es erfolgt, aus welchen Grund auch immer, ein erneuter Eintritt
in diese Timerfunktion, liegt ein Fehler vor und das Gerät wird sofort angehalten. Dadurch könnte sonst ein Stacküberlauf
generiert werden und dies soll natürlich abgeblockt werden. Normalerweise ist dieses Vorgehen nicht nötig, da der Interrupt
erst am Ende des Interrupt Handlers bestätigt wird und damit das momentan anstehende Interruptbit gelöscht wird.
Ich habe mich aber aus Sicherheitsgründen trotzdem für diese Vorgehensweise entschieden.



Ich werde mich für die Timer Variante entscheiden, ein paar Mikrosekunden warten, spielt in dieser, meiner Anwendung keine Rolle.
Habe ein 16 Bit Timer übrig, der dafür ausreicht.

Meine While Schleife für den Timerwert blockiert ja nur kurzzeitig das Hauptprogramm,alle Interrupts laufen ja weiter. ADU, RS232 usw.

Siro

- - - Aktualisiert - - -

Ich habe noch einen wichtigen Grund, warum ich der Delay Funktion niemals eine "1" übergebe:
Es könnte passieren, dass garnicht gewartet wird.
Wenn nach dem Setzten der globalen Variablen DelayCount der Timerinterrupt schon auftritt,
wird der Wert gleich um eins runtergezählt und die While Schleife sofort wieder verlassen.
Wir haben also generell eine Abweichung von ca. -1 Millisekunde.


/* EXTREM wichtig: volatile, sonst optimiert der Compiler Code weg */
volatile static U32 DelayCount; /* used for Delay_ms function */

/*----------------------------------------------------------------------------*/
/* DelayCount wird im Interrupt kontinuierlich um 1 runtergezählt */
/* sofern er nicht schon 0 ist.

void Delay_ms(U32 ms)
{
DelayCount = ms;

while (DelayCount) ;
}
/*----------------------------------------------------------------------------*/
/* this Interrupt handler is called every Millisecound */

void SysTick_Handler(void)
{
if (DelayCount) DelayCount--;
}
/*----------------------------------------------------------------------------*/

witkatz
24.06.2016, 10:27
@Witkatz:
Ich habe so etwas in der Art auch schon implementiert.
Coole Idee mit der Funktionstabelle. Hab ich mir gemerkt


Ich habe noch einen wichtigen Grund, warum ich der Delay Funktion niemals eine "1" übergebe:
Es könnte passieren, dass garnicht gewartet wird.
Wenn nach dem Setzten der globalen Variablen DelayCount der Timerinterrupt schon auftritt,
wird der Wert gleich um eins runtergezählt und die While Schleife sofort wieder verlassen.
Ich nehme für solche ms-Timings lieber einen 250µs Basistakt. Damit können mit einem char-Zähler (8bit PIC -> char wo es nur geht) Wartezeiten von 1ms bis 1s ziemlich gut realisiert werden, wenn eine Abweichung von 250µs keine Rolle spielt.

Klebwax
24.06.2016, 11:15
Die Idee, dass der Compiler an bestimmten Stellen NICHT optimiert, wäre in meinem Falle
sogar auch eine Lösung, find ich aber etwas unschön.

C ist in seiner ursprünglichen Form für die embeded Programmierung eigentlich nicht geeignet. Ohne einen Mechanismus, der die Optimierung steuert, geht nichts. Der Compiler würde sonst jede Abfrage von Funktionsregistern bis zur Unbrauchbarkeit optimieren. Stichworte sind volatile und INTRINSIC FUNCTIONS. Es mag für dich zwar unschön sein, ist aber unumgänglich.


void strobeE(){
LCD_E = 1;
__delay_us(1); // delay 1us
LCD_E = 0;
}
Wenn hier nicht ein solcher Mechanismus greifen würde, würde der Compiler das Setzen von LCD_E auf 1 rausschmeißen. Da niemand LCD_E liest, während es 1 ist, kann es auch gleich auf 0 gesetzt werden. Toter Code. LCD_E muß also volatile sein, damit das funktioniert.

@Peter

Ich habe deine Antwort nur bis --aktualisiert-- lesen können, als ich schrieb.

MfG Klebwax

Siro
24.06.2016, 12:33
@Klebwax:
Das Problem Optimierung ist mir schon recht oft zum Verhängnis geworden.
Genau in solchen Situationen wie Du eben beschrieben hast.
Habe immer öfter bemerkt, dass ein "volatile" unumgänglich ist, damit der "schlaue" Compiler es auch "richtig" versteht.
Am Anfang hab ich immer gemerkert :p warum der Compiler meinen Code klaut,
Er hat ja recht würd ich aus heutiger Sicht sagen, woher soll er wissen dass......


Ich hab grad noch etwas rumprobiert und da stellt sich gleich noch eine Frage:

Zuvor der Code, der schien zu laufen.....
Beim Timerüberlauf stimmt aber die Berechnung nicht mehr. Doch dazu später....


/*--------------------------------------------------------------------------*/
/* static volatile U16 t_old; */

inline void wait_us(U16 us)
{ U16 t_old;

t_old = TIMER1_TC;
while ((TIMER1_TC - t_old) < us) ;
}
/*--------------------------------------------------------------------------*/

Nun zur Frage:

Ich habe das Schlüsselwort "inline" benutzt, damit darf der Compiler den Code ja direkt einsetzen ohne einen Call.
Ob er das macht bleibt glaube ich ihm überlassen.
Damit ich meine Funktion evtl. sogar aus verschiedenen threads bzw. Interrupts aufrufen könnte, müsste mein Variable t_old ja Dynmsich auf dem Stack liegen, also Lokal zum Funktionsaufruf.
Aber wie funktioniert das mit dem Inline, ist das überhaupt erlaubt ? Inline und lokale Variable ?

Der Compiler könnte den Call nun sparen aber was macht er mit der Variable t_old ? die müste er ja dynamsich auf dem Stack verwalten.
Wenn es um Geschwindigkeit geht und ich nur aus einem Thread die Funktion aufrufe, kann ich die Variable ja Global anlegen,
ich vermute hier wird er den besten Code dafür erzeugen können.

Siro
24.06.2016, 15:02
Warum meine Berechnung nach Timerüberlauf nicht mehr funktioniert, habe ich unn auch ermittelt.


while (TIMER1_TC - t_old < us) ; /* geht nicht */

/* Also "komplette" explizite Typwandlung auf allen Werten und Ergebnissen angewendet: */

while ((U16)((U16)(TIMER1_TC) - (U16)(t_old)) < us) ; /* siehe da, es funktioniert */

/* nun Typwandlungen gekürzt */

while ((U16)(TIMER1_TC - t_old) < us) ; /* auch OKAY */


Durch die Integer Promotion des Standard Typs int bei 32 Bit muss hier anscheinend das Ergebnis der Subtraktion
explizit wieder in einen unsigned 16 Bit gewandelt werden, sonst geht es schief, jedoch nur manchmal, je nach Timerstand.

Ich bin begeistert und kann nun das Wochenende geniessen...
Habt auch ein schönes, warmes WE.

Siro

Peter(TOO)
24.06.2016, 22:07
Hallo Siro,

Ich habe das Schlüsselwort "inline" benutzt, damit darf der Compiler den Code ja direkt einsetzen ohne einen Call.
Ob er das macht bleibt glaube ich ihm überlassen.
Genau so ist es.


Damit ich meine Funktion evtl. sogar aus verschiedenen threads bzw. Interrupts aufrufen könnte, müsste mein Variable t_old ja Dynmsich auf dem Stack liegen, also Lokal zum Funktionsaufruf.
Aber wie funktioniert das mit dem Inline, ist das überhaupt erlaubt ? Inline und lokale Variable ?

Man kann in jedem Block heute eine lokale Variable festlegen, OK, bei K&R war das noch nicht erlaubt.



void function(void)
{
int i;
// mach was mit i
{
int i2;
// mach was mit i und i2
}
// ab hier gibt es i2 nicht mehr
// mach noch was mit i
}



Der Compiler könnte den Call nun sparen aber was macht er mit der Variable t_old ? die müste er ja dynamsich auf dem Stack verwalten.
Wenn es um Geschwindigkeit geht und ich nur aus einem Thread die Funktion aufrufe, kann ich die Variable ja Global anlegen,
ich vermute hier wird er den besten Code dafür erzeugen können.
Vermutlich wird der Compiler die einzelne Variable gar nicht auf dem Stack ablegen, sondern in einem Register behalten.
Kommt aber auf die CPU und den restlichen Code an.

Es gibt noch das Keyword register, damit konnte man dem Compiler nahelegen, welche Variablen, wenn möglich, in einem Register abgelegt werden sollen. Das war so eine "per Hand" Optimierung. Heute hat register keinen praktischen Wert mehr, der Compiler optimiert dies automatisch.

MfG Peter(TOO)

Siro
25.06.2016, 07:46
Guten Morgen und Danke Peter.

Nanu, da bin ich jetzt echt erstaunt, mit den "Block Lokalen" Variablen.
Das ist mir völlig neu... Ist das Ansi/C99 konform ?
Muss ich mal probieren, der Compiler müsste ja dann bei einem Verstoß meckern.

Ich denke auch, bei solchen Ein/Zweizeilern wird der Compiler das automatisch in den Registern halten.

Siro

HaWe
25.06.2016, 13:52
ja, das mit den Blockvariablen ist 100% ANSI C99.

Der C-Compiler wird die Variablen aber nur in den exe-Code übernehmen, wenn du mit ihren Werten auch irgendetwas machst (z.B. printf() Ausgabe). Wenn du nur rechnest, ohne mit den Ergebnissen was zu machen, fliegen sie komplett raus beim Kompilieren.
Nur das Zauberwörtchen
volatile
vor der Variablendefinition kann dich vor dieser Optimierung (egal welches Optimierungs-Level) schützen, also wundere dich nicht, wenn du sie eventuell nicht im exe-Code wiederfindest! 8)