Forum: Compiler & IDEs _delay_us() sehr! ungenau?


von Arthur Dent (Gast)


Lesenswert?

Hallo,

ich habe hier folgenden kurzen code-ausschnitt.

ich möchte damit einfach ein bussignal auswerten.
wenn ich mir den testpin (hier portd pd0) am oszi anschaue muss ich 
feststellen dass dessen frequenz zunimmt, ich habe testweise auch schon 
mal recvcount=20 initialisiert um das phänomen genauer zu untersuchen, 
auch hier scheint es so dass ich je öfter hintereinander ich _delay_us 
aufrufe, umso kürzer wartet es,.... kann doch nicht sein, oder?

wenn diese empfangsroutine dann nochmal von neuem aufgerufen wird ist 
beim ersten bit wieder alles in ordnung, aber um so mehr es gegen das 
achte bit geht um so kürzer wartet das delay,...

ich bin ratlos, aber vll kann mir jemand von euch weiterhelfen.

lg

1
RecvCount = 8;
2
loop_until_bit_is_clear(PIND, PD0);  //Auf Startbit warten
3
_delay_us(MyDelayValueAndAHalf);
4
do {
5
  RecvCount--;
6
  _delay_us(MyDelayValue);
7
  PORTD ^= (1<<PD1);       //***TEST
8
  if( !(PIND & (1<PD0)) ) Recv |= (1<<RecvCount);
9
  else Recv &= ~(1<<RecvCount);
10
} while (RecvCount != 0);

von Peter II (Gast)


Lesenswert?

Arthur Dent schrieb:
> ich bin ratlos, aber vll kann mir jemand von euch weiterhelfen.

ja, doku lese hilft.

_delay_us();

darf nur mit konstanten und mit optimerung verwendet werden.

von Arthur Dent (Gast)


Lesenswert?

Peter II schrieb:
> ja, doku lese hilft.
>
> _delay_us();
>
> darf nur mit konstanten und mit optimerung verwendet werden.

-os hab ich als optimierungsstufe gewählt,

und im code weiter oben (ziehmlich am anfang um genau zu sein ;)) steht 
folgendes:
1
#define MyDelayValue 8
2
#define MyDelayValueAndAHalf 14
jaja ich weis, 8*1,5 = nicht 14 ;)
aber diese werte hab ich experimentell ermittelt ;)

von Peter II (Gast)


Lesenswert?

Arthur Dent schrieb:
> und im code weiter oben (ziehmlich am anfang um genau zu sein ;)) steht
> folgendes:
> #define MyDelayValue 8
> #define MyDelayValueAndAHalf 14

ok das hätte nich nicht erwartet, weil man define im üblichen GROSS 
schreibt. Dann ist das nicht das Problem.

(kommt aber auch nur weil man nicht alles wichtige sieht)

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Arthur Dent schrieb:

> -os hab ich als optimierungsstufe gewählt,

Das ist keine Optimierungsstufe, sondern es setzt die Ausgabedatei auf 
s. Siehe Dokumentation zu -o

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Compilierfähiger Code wäre schon manchmal schön ...  Angabe des
genauen Controllers und der benutzten Taktfrequenz ebenfalls.

OK, ich habe deinen Code mal wie folgt ergänzt:
1
#include <avr/io.h>
2
3
#define F_CPU 8E6
4
#include <util/delay.h>
5
6
#define MyDelayValue 8
7
#define MyDelayValueAndAHalf 14
8
9
uint8_t Rx(void)
10
{
11
    uint8_t Recv, RecvCount;
12
13
    RecvCount = 8;
14
    loop_until_bit_is_clear(PIND, PD0);  //Auf Startbit warten
15
    _delay_us(MyDelayValueAndAHalf);
16
    do {
17
        RecvCount--;
18
        _delay_us(MyDelayValue);
19
        PORTD ^= (1<<PD1);       //***TEST
20
        if( !(PIND & (1<PD0)) ) Recv |= (1<<RecvCount);
21
        else Recv &= ~(1<<RecvCount);
22
    } while (RecvCount != 0);
23
24
    return Recv;
25
}

(Ja, ich weiß, hab' vergessen, Recv zu initialisieren.  Spielt aber
fürs Prinzip jetzt keine Geige.)

Wenn ich das jetzt für einen ATmega8 compiliere (GCC 4.5.1), bekomme
ich:
1
.global Rx
2
        .type   Rx, @function
3
Rx:
4
/* prologue: function */
5
/* frame size = 0 */
6
/* stack size = 0 */
7
.L__stack_usage = 0
8
.L2:
9
        sbic 48-32,0
10
        rjmp .L2
11
         ldi r20,lo8(37)
12
    1:dec r20
13
    brne 1b
14
        nop
15
        ldi r25,lo8(8)
16
        ldi r22,lo8(2)
17
        ldi r18,lo8(1)
18
        ldi r19,hi8(1)
19
.L3:
20
        subi r25,lo8(-(-1))
21
         ldi r20,lo8(21)
22
    1:dec r20
23
    brne 1b
24
        nop
25
        in r20,50-32
26
        eor r20,r22
27
        out 50-32,r20
28
        in r20,48-32
29
        movw r20,r18
30
        mov r0,r25
31
        rjmp 2f
32
1:      lsl r20
33
        rol r21
34
2:      dec r0
35
        brpl 1b
36
        or r24,r20
37
        tst r25
38
        brne .L3
39
/* epilogue start */
40
        ret

Wie du sehen kanst, sind deine Ausdrücke der Form (1 << RecvCount)
die Übeltäter.  Da der AVR keinen Schiebebefehl für mehrere Bits
hat, muss die variable Verschiebung durch eine Schleife von LSL/ROL
implementiert werden.  Je nachdem, wie groß dein RecvCount gerade
ist, nimmt diese Schleife eine unterschiedliche Zeit in Anspruch.

Das gesamte Timing des restlichen "Overheads" geht halt im Vergleich
zu deinen _delay_us() bereits heftig mit ein (mit 1 MHz CPU-Takt
könnte man die Sache bereits komplett in die Tonne drücken).

Wenn du mit -O3 compilieren lässt, entrollt der Compiler die
Schleife, und du bist die Abhängigkeit vom Durchlaufzähler los:
1
.global Rx
2
        .type   Rx, @function
3
Rx:
4
/* prologue: function */
5
/* frame size = 0 */
6
/* stack size = 0 */
7
.L__stack_usage = 0
8
.L2:
9
        sbic 48-32,0
10
        rjmp .L2
11
         ldi r24,lo8(37)
12
    1:dec r24
13
    brne 1b
14
        nop
15
         ldi r25,lo8(21)
16
    1:dec r25
17
    brne 1b
18
        nop
19
        in r25,50-32
20
        ldi r24,lo8(2)
21
        eor r25,r24
22
        out 50-32,r25
23
        in r25,48-32
24
         ldi r25,lo8(21)
25
    1:dec r25
26
    brne 1b
27
        nop
28
        in r25,50-32
29
        eor r25,r24
30
        out 50-32,r25
31
        in r25,48-32
32
         ldi r25,lo8(21)
33
    1:dec r25
34
    brne 1b
35
        nop
36
        in r25,50-32
37
        eor r25,r24
38
        out 50-32,r25
39
        in r25,48-32
40
         ldi r25,lo8(21)
41
    1:dec r25
42
    brne 1b
43
        nop
44
        in r25,50-32
45
        eor r25,r24
46
        out 50-32,r25
47
        in r25,48-32
48
         ldi r25,lo8(21)
49
    1:dec r25
50
    brne 1b
51
        nop
52
        in r25,50-32
53
        eor r25,r24
54
        out 50-32,r25
55
        in r25,48-32
56
         ldi r25,lo8(21)
57
    1:dec r25
58
    brne 1b
59
        nop
60
        in r25,50-32
61
        eor r25,r24
62
        out 50-32,r25
63
        in r25,48-32
64
         ldi r25,lo8(21)
65
    1:dec r25
66
    brne 1b
67
        nop
68
        in r25,50-32
69
        eor r25,r24
70
        out 50-32,r25
71
        in r25,48-32
72
         ldi r25,lo8(21)
73
    1:dec r25
74
    brne 1b
75
        nop
76
        in r25,50-32
77
        eor r25,r24
78
        out 50-32,r25
79
        in r24,48-32
80
        ldi r24,lo8(-1)
81
/* epilogue start */
82
        ret

Du kannst aber auch deinen Quellcode ein wenig umschreiben und
die Bitmaske als Variable mitführen, um dem Compiler zu helfen:
1
#include <avr/io.h>
2
3
#define F_CPU 8E6
4
#include <util/delay.h>
5
6
#define MyDelayValue 8
7
#define MyDelayValueAndAHalf 14
8
9
uint8_t Rx(void)
10
{
11
    uint8_t Recv, RecvCount, RecvMask;
12
13
    RecvCount = 8;
14
    loop_until_bit_is_clear(PIND, PD0);  //Auf Startbit warten
15
    _delay_us(MyDelayValueAndAHalf);
16
    for (Recv = 0, RecvCount = 0, RecvMask = 0x80;
17
         RecvCount < 8;
18
         RecvCount++, RecvMask >>= 1)
19
    {
20
        _delay_us(MyDelayValue);
21
        PORTD ^= (1<<PD1);       //***TEST
22
        if( !(PIND & (1<PD0)) ) Recv |= RecvMask;
23
        else Recv &= ~RecvMask;
24
    }
25
26
    return Recv;
27
}

Der sich ergebende Code (-Os) ist nicht nur invariabel von der Zahl
der Schleifendurchläufe, sondern auch noch kompakter:
1
.global Rx
2
        .type   Rx, @function
3
Rx:
4
/* prologue: function */
5
/* frame size = 0 */
6
/* stack size = 0 */
7
.L__stack_usage = 0
8
.L2:
9
        sbic 48-32,0
10
        rjmp .L2
11
         ldi r19,lo8(37)
12
    1:dec r19
13
    brne 1b
14
        nop
15
        ldi r18,lo8(8)
16
        ldi r25,lo8(-128)
17
        ldi r24,lo8(0)
18
        ldi r20,lo8(2)
19
.L3:
20
         ldi r19,lo8(21)
21
    1:dec r19
22
    brne 1b
23
        nop
24
        in r19,50-32
25
        eor r19,r20
26
        out 50-32,r19
27
        in r19,48-32
28
        or r24,r25
29
        lsr r25
30
        subi r18,lo8(-(-1))
31
        brne .L3
32
/* epilogue start */
33
        ret

Allerdings wird das Timing mit der nächsten Version des Compilers
trotzdem wahrscheinlich geringfügig anders sein.  Eigentlich sollte
man derartige Dinge wohl daher in Assembler schreiben (auch wenn
ich davon sonst nicht der große Protagonist bin ;) und dann die
Tatkzyklen gleich mit der Hand auszählen.

von Peter D. (peda)


Lesenswert?

Delays können garnicht den Anspruch erheben, genau zu sein. Sie können 
ja nicht wissen, welche Zeit der Code davor und danach benötigt.

Z.B. braucht ein einzelner Befehl schon 1µs bei 1MHz und eine C-Zeile 
besteht fast nie aus nur einem Befehl, eher sinds 10 .. 20.

Und Interrupthandler addieren sich auch auf die Delayzeit auf.

Delays sollen nur nicht kürzer sein, als angegeben. Z.B. wenn man ein 
Byte an ein LCD schickt, muß man ~50µs warten. Wenns länger dauert, 
juckt das niemanden.

Je länger ein Delay, umso geringer wird natürlich der Einfluß der 
Codelaufzeit auf die Gesamtdauer.


Solls genau sein, muß man einen Timer nehmen.


Peter

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jörg Wunsch schrieb:

> Eigentlich sollte man derartige Dinge wohl daher in Assembler
> schreiben (auch wenn ich davon sonst nicht der große Protagonist
> bin ;) und dann die Taktzyklen gleich mit der Hand auszählen.

Geht zB mit so einer Instruktionsfolge:
1
// Return   1 << (x & 7)
2
static inline __attribute__((always_inline))
3
unsigned char shift1 (unsigned char x)
4
{
5
    asm ("ldi  %0, 4"  "\n\t"
6
         "sbrc %1, 1"  "\n\t"
7
         "ldi  %0, 1"  "\n\t"
8
         "sbrc %1, 0"  "\n\t"
9
         "lsl  %0"     "\n\t"
10
         "sbrc %1, 2"  "\n\t"
11
         "swap %0"
12
         : "=&d" (x) : "r" (x));
13
         
14
    return x;
15
}

Dafür kann man dann 7-8 Ticks veranschlagen.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Johann L. schrieb:
> Jörg Wunsch schrieb:
>
>> Eigentlich sollte man derartige Dinge wohl daher in Assembler
>> schreiben (auch wenn ich davon sonst nicht der große Protagonist
>> bin ;) und dann die Taktzyklen gleich mit der Hand auszählen.
>
> Geht zB mit so einer Instruktionsfolge:

Nein, ich meinte nicht den Shift, sondern die gesamte Schleife,
die ja offenbar zeitkritisch ist.

von Arthur Dent (Gast)


Lesenswert?

ok, vielen vielen dank,...

so was ähnliches hab ich in assembler schon rumliegen, das soll aber 
eben jetzt c werden (wenn möglich) warum?,... hmmm eigentlich nur weils 
halt noch mehr "state of the art" ist als asm ;) UND weil wir in letzter 
zeit immer öffter änbderungen oder spezielitäten hatten die sich in c 
dann besser anpassen/ändern lassen als in asm. momentan soll wieder eine 
änderung rein, und beim erneuten einlesen ins asm hab ich dann lieber 
angefangen das in c zu machen.

ich glaube mit den ansätzen kann ich heut gut arbeiten!!

vielen dank!

lg

von Frank (Gast)


Lesenswert?

Verzögerungsschleifen programmier ich lieber selbst wenn Sie genau sein 
sollen.

Zum testen am besten auf einen OSI an einem pin ausgeben 
(Rechtecksignal) dann kann man das exakt messen.

Wenn es nicht Programmlastig werden soll die Delay Scheife über 
Timerinteruupt triggern.

von Klaus (Gast)


Lesenswert?

Frank schrieb:
> Verzögerungsschleifen programmier ich lieber selbst wenn Sie genau sein
> sollen.
>
> Zum testen am besten auf einen OSI an einem pin ausgeben
> (Rechtecksignal) dann kann man das exakt messen.

Und das machst du wirklich nach jedem mal kompilieren erneut, und 
stellst die delays neu ein? Kann ich kaum glauben.

von Arthur Dent (Gast)


Lesenswert?

Jörg Wunsch schrieb:
> Du kannst aber auch deinen Quellcode ein wenig umschreiben und
> die Bitmaske als Variable mitführen, um dem Compiler zu helfen:
> #include <avr/io.h>
>
> #define F_CPU 8E6
> #include <util/delay.h>
>
> #define MyDelayValue 8
> #define MyDelayValueAndAHalf 14
>
> uint8_t Rx(void)
> {
>     uint8_t Recv, RecvCount, RecvMask;
>
>     RecvCount = 8;
>     loop_until_bit_is_clear(PIND, PD0);  //Auf Startbit warten
>     _delay_us(MyDelayValueAndAHalf);
>     for (Recv = 0, RecvCount = 0, RecvMask = 0x80;
>          RecvCount < 8;
>          RecvCount++, RecvMask >>= 1)
>     {
>         _delay_us(MyDelayValue);
>         PORTD ^= (1<<PD1);       //***TEST
>         if( !(PIND & (1<PD0)) ) Recv |= RecvMask;
>         else Recv &= ~RecvMask;
>     }
>
>     return Recv;
> }

so scheints zu funktionieren!
vielen dank jörg!

jetzt kann ich mit dem ganzen drum herum loslegen....

schönen tag und lg

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Arthur Dent schrieb:
> so was ähnliches hab ich in assembler schon rumliegen, das soll aber
> eben jetzt c werden (wenn möglich) warum?,... hmmm eigentlich nur weils
> halt noch mehr "state of the art" ist als asm ;) UND weil wir in letzter
> zeit immer öffter änbderungen oder spezielitäten hatten die sich in c
> dann besser anpassen/ändern lassen als in asm.

Du sollst ja deshalb auch nicht das ganze Projekt in Assembler
schreiben, sondern nur die eine, zeitkritische Funktion.  Diese
lässt sich ja dann als separates Objekt zu einem ansonsten in
C geschriebenen Projekt dazu linken.

Frank schrieb:
> Verzögerungsschleifen programmier ich lieber selbst wenn Sie genau sein
> sollen.

Wird nicht mehr und nicht minder genau als _delay_us().  Sieht mir
eher danach aus, als wüsstest du überhaupt nicht, wie diese
Verzögerungsschleifen tatsächlich implmentiert sind.

> Zum testen am besten auf einen OSI an einem pin ausgeben
> (Rechtecksignal) dann kann man das exakt messen.

Genau das hat Arthur ja versucht.  Hat nur nicht funktioniert.  Hast
du dir den Rest seines Problems denn überhaupt angesehen?

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.