Falsche Ergebnisse mit Modulo

Gast #5974629
Lesenswert?

Hallo,

ich habe an einem ATMEGA1284 einen Drehgeber per HCTL2016 angeschlossen, 
der die Position mit 16 Bit zählt, den Istwert lese ich in den ersten 
Zeilen in ra_val ein. Da kommt an, was erwartet wird.

Eine volle Umdrehung der Achse liefert 12000 Inkremente, die sollen auf 
0 … 24h umgerechnet werden (Stundenachse am Teleskop). In positiver 
Zählrichtung kein Problem, in negativer muss ich einen Offset verrechen, 
weil 2 hoch 16  / 12000 nicht ohne Rest aufgeht. Das funktioniert auch. 
Dass die Berechnung nur innerhalb von reichlich ±2 Umdrehungen richtig 
funktioniert, ist mir klar, da ich soweit gar nicht komme, stört das 
nicht.

Ich habe die Zeile unten gekennzeichnet, welche falsch rechnet … 
übersehe ich da irgendwas? Ich hab aus der Vermutung heraus, dass eine 
interne Umwandlung stattfindet, zur Kontrolle mit / ohne Vorzeichen 
ausgeben lassen, da kommen die gleichen Werte raus.

Die Ausgaben auf UART0 sind nur zur Fehlersuche drin. Ich habe dort 
jeweils die erwarteten bzw. falschen Werte im Kommentar vermerkt.

encoder_resolution wird mit 12000 aufgerufen, kommt auch richtig an. Ich 
habe die deklaration der Encoderauflösung versuchsweise auf uint32_t 
geändert - ist wirkungslos. cli() am Anfang und sei() am Ende der 
Funktion bringt auch nichts. Hat jemand eine Idee?

Grüße
tg

1
//globale Deklarationen:
2

3
uint16_t resolution_r = 12000;              // Startwert Auflösung Stundenwinkel
4

5

6
//------------------------------------------[]-------------------------------------------
7
//Quadraturencoder RA auslesen und aufbereiten, Übergabe in Sekunden
8
uint32_t get_ra_seconds (uint16_t encoder_resolution)
9
{
10
    uint32_t ra_val = 0;
11

12
    char Buffer[10];                                                // Test
13

14
    HCTL_CONTROL_PORT &= ~((1 << HCTL_SEL) | (1 << HCTL_OE_RA));    //SEL=L; OE=L; set inhibit
15
    asm volatile ("nop");                                           //Reaktionszeit für HCTL erforderlich
16
    ra_val = (HCTL_DATA_PORT << 8);                                 //high-Byte lesen + schieben
17
    HCTL_CONTROL_PORT |= (1 << HCTL_SEL);                           //SEL=H; OE=L; start inhibit reset
18
    asm volatile ("nop");                                           //Reaktionszeit für HCTL erforderlich
19
    ra_val |= HCTL_DATA_PORT;                                       //low-Byte lesen + addieren
20
    HCTL_CONTROL_PORT |= (1 << HCTL_OE_RA);                         //SEL=H; OE=H; complete inhibit reset
21

22
    uart0_puts ("RA: ");
23
    utoa( ra_val, Buffer, 10 );
24
    uart0_puts (Buffer);           //Beispiel: 65291
25
    uart0_puts (" | ");
26

27
    if (ra_val >= 65536/2)         //negative Zählrichtung: Werte umrechnen
28
    {
29
        ra_val = ra_val - (65535 % encoder_resolution);             //Offset bei negativer Zählrichtung
30
    }
31

32
    utoa( ra_val, Buffer, 10 );
33
    uart0_puts (Buffer);           //Beispiel: 59756 … ok
34
    uart0_puts (" | ");
35

36
    ra_val = ra_val % encoder_resolution;        // RECHNET FALSCH auf eine Umdrehung reduzieren
37

38
    utoa( ra_val, Buffer, 10 );
39
    uart0_puts (Buffer);           //Beispiel: 5516 kommt, 11756 erwartet
40
    uart0_puts (" u|i ");
41
    itoa( ra_val, Buffer, 10 );
42
    uart0_puts (Buffer);           //Beispiel: 5516 kommt, 11756 erwartet
43

44
    ra_val = (ra_val * (uint32_t) 86400) / encoder_resolution;  //in Sekunden, umskalieren auf 24h
45

46
//...
47

48
    return ra_val;
49
}

Der Vollständigkeit halber noch der Aufruf dieser Funktion:
1
   ha_seconds = get_ra_seconds(resolution_r);
Gast #5974642
Lesenswert?

Gebe mal ein Beispiel an, mit welchen konkreten zahlen man das Problem 
(auf einem PC) reproduzieren kann:
1
#include <stdio.h>
2
#include <stdint.h>
3

4
int main()
5
{
6
    uint32_t ra_val = 20000;
7
    uint16_t encoder_resolution = 180;
8
    printf("ra_val=%d, encoder_resolution=%d, modulo=%d\n",
9
        ra_val, encoder_resolution, ra_val%encoder_resolution);
10
    return 0;
11
}

Hier bitte die beiden Zahlen ersetzen.
Gast #5974643
Lesenswert?

tg schrieb:

> Eine volle Umdrehung der Achse liefert 12000 Inkremente, die sollen auf
> 0 … 24h umgerechnet werden (Stundenachse am Teleskop). In positiver
> Zählrichtung kein Problem, in negativer muss ich einen Offset verrechen,
> weil 2 hoch 16  / 12000 nicht ohne Rest aufgeht.

Häh? Wenn das in positiver Richtung geht, muss es auch in negativer 
Richtung gehen. Wenn irgendwelche Teilbarkeiten tatsächlich eine Rolle 
spielen würden, täten sie das in beiden Richtungen gleichermaßen. Das 
kann man sogar wissen, ohne Programmieren zu können, dazu braucht man 
einfach nur bis zur vierten Klasse in Mathe mitgehalten zu haben...

Kurzfassung: du hast keine Ahnung von dem, was du da tust. Lerne.
Gast #5974683
Lesenswert?

Stefanus F. schrieb:

Hallo Stefanus

mit den eingetragenen Werten
1
#include <stdio.h>
2
#include <stdint.h>
3

4
int main()
5
{
6
    uint32_t ra_val = 59753;
7
    uint16_t encoder_resolution = 12000;
8
    printf("ra_val=%d, encoder_resolution=%d, modulo=%d\n",
9
        ra_val, encoder_resolution, ra_val%encoder_resolution);
10
    return 0;
11
}

sollte 11753 raus kommen, tatsächlich erscheinen bei mir 5513.

Grüße
tg
Gast #5974695
Lesenswert?

Oliver S. schrieb:

> Auch wenn es wie üblich grenzwertig deutlich formuliert ist, hat c-hater
> im Prinzip wohl recht.
>
> Der Wurm steckt nicht im Modulo-Operator.

So ist es. Aber wollen wir ihn mal nicht völlig dumm sterben lassen...

@tg (Gast):

Der Trick ist einfach: zyklische Sachverhalte (nämlich z.B. die 
Erdumdrehung bzw. unsere 24h-Abbildung selbiger) auch eben als Zyklus zu 
sehen und so im Code umzusetzen.

Du kannst das an zwei Stellen tun. Sinnvoller wird sein, es zu tun, 
bevor du die Schritte in deine Funktion einfüllst. Allerdings musst du 
dann ggf. einen zusätzlichen Parameter mitschleppen, nämlich einen 
Tages-Offset. Nötig wird das, wenn du im Endergebnis nicht nur die 
Uhrzeit sehen willst (die es ja jeden Tag gleichermassen gibt), sondern 
tatsächlich eine konkrete, eindeutige Zeit.
Gast #5974712
Lesenswert?

Egon D. schrieb:
> tg schrieb:
>
>> In positiver Zählrichtung kein Problem, [...]
>
> Tatsächlich?
>
> Das Teleskop läuft genau 5 Tage lang vorwärts; dann
> steht der Zähler auf 5*12000 = 60'000. Was passiert
> im Verlaufe des sechsten Tages?

Das Teleskop soll sich ja nicht tagelang kontinuierlich drehen. Schon um 
die Kabel nicht aufzuwickeln, wird das immer wieder zurückgedreht, so 
dass ich sicher in dem Limit von ±2 richtig verarbeiteten Umdrehungen 
bleibe.
Gast #5974714
Lesenswert?

Thomas Z. schrieb:
> Ich werfe mal %d in den Raum...
> Du verwendest eine 8 Bit CPU ein int ist nur 16 Bit groß. Das gibt
> natürlich Probleme wenn du printf auf einmal eine 32bit Zahl vorwirfst.

Vielleicht musst du beim AVR "%ld" schreiben. Wenn du alle Warnungen vom 
Compiler einschaltest (-Wall) sollte er sich ggf. dazu melden.

Allerdings: 11753 würde in einen 16bit Integer noch rein passen.
Gast #5974719
Lesenswert?

tg schrieb:

> Egon D. schrieb:
>> tg schrieb:
>>
>>> In positiver Zählrichtung kein Problem, [...]
>>
>> Tatsächlich?
>>
>> Das Teleskop läuft genau 5 Tage lang vorwärts; dann
>> steht der Zähler auf 5*12000 = 60'000. Was passiert
>> im Verlaufe des sechsten Tages?
>
> Das Teleskop soll sich ja nicht tagelang kontinuierlich
> drehen.

Herrgott. Beantworte doch einfach meine Frage.
Gast #5974730
Lesenswert?

Stefanus F. schrieb:
> tg schrieb:
>> sollte 11753 raus kommen
>
> Tut es bei mir auch.

Naja, hatte ich auch erwartet ;-)
Ich hab ja die kompletten Berechnungen, die da mal laufen sollen, in 
einer Tabellenkalkulation parallel als "normale" Berechnung und in 
Ganzzahlarithmetik vorbereitet, da funktioniert auch alles.

Ich hatte wie gesagt die Hoffnung, das jemand was auffällt, was ich 
konstant übersehe, z.B. hinsichtlich Deklarationen, implizite Casts 
usw., was den Effekt verursacht. Am Modulo-Operator hab ich eigentlich 
keine Zweifel.

Danke dir!
Grüße
tg
Gast #5974917
Lesenswert?

tg schrieb:
> übersehe ich da irgendwas?

> Die Modulo-Berechnung ergibt in C grundsätzlich nur mit positiven
> Werten sinnvolle Ergebnisse. Wenn der zweite Operand negativ ist,
> so wird dieses Vorzeichen von C schlichtweg ignoriert. Wenn der erste
> Operand negativ ist, so wird die Berechnung ausgeführt, als ob der
> Operand positiv wäre und schlussendlich dem Resultat das Vorzeichen
> wieder angehängt.

http://manderc.com/operators/modoperator/index.php

Man muß also für Fälle, in denen ein negatives Verzeichen vorkommt, eine 
Sonderbehandlung einbauen.

MfG Klaus
Gast #5975939
Lesenswert?

Klaus schrieb:
> Man muß also für Fälle, in denen ein negatives Verzeichen vorkommt, eine
> Sonderbehandlung einbauen.

Die tatsächlich auftretenden Werte schließen aus, dass die nicht 
funktionierende Modulo-Operation eine negative Zahl zu sehen bekommt. 
Ich habe trotzdem mal
1
    if (ra_val < 0)
2
        uart0_puts (" <0 ");

davor gesetzt - es wird keine Meldung ausgegeben, wenn danach falsche 
Werte berechnet werden. Ein impliziter Cast findet also offensichtlich 
nicht statt.

Ich habe inzwischen auch unterschiedliche Optimierungseinstellungen 
getestet .. macht keinen Unterschied beim Ergebnis. Es ist nach wie vor 
diese Zeile
1
   ra_val = ra_val % encoder_resolution;

die nicht mitspielt. Wenn ich die Auflösung "hart" eintrage, stimmt das 
Ergebnis ebenfalls nicht:
1
   ra_val = ra_val % 12000;

Beide Fällen führen z.B. zu 59980 % 12000 = 5740. Wenn ich aber die 
59980 fest eintrage
1
   ra_val = 59980 % encoder_resolution;

kommen wie zu erwarten 11980 raus. Bei vorangegangenen Tests wurden mir 
die Werte von ra_val korrekt angezeigt, irgendwas geht daneben, wenn 
diese Zeile losarbeitet ...

Grüße
tg
Gast #5975957
Lesenswert?

Oliver S. schrieb:
> Ansonsten scheint die Aufgabe zu sein, den Wertebereich 0-12000 auf den
> Bereich 0-24 abzubilden. Sollte machbar sein ;)

Ja, das geht gerade noch so ;-). Ich hab das oben vielleicht etwas 
unklar beschrieben.

Pro kompletter Umdrehung der Achse bekomme ich 12000 Inkremente. Die 
werden von einem 16bit-Zähler-IC erfasst. Der zählt also in die eine 
Richtung 0, 1, ... 12000 (nach einer Umdrehung), 24000 (2. Umdr.) ... 
36000 ... usw.

In die andere Richtung zählt der 0, 65535, .. 53535 (1. Umdr.) ... usw. 
Je nachdem, wie herum ich drehe, bekomme ich also für den gleichen 
Winkel unterschiedliche Zählerstände. Deshalb wird die zuletzt genannte 
drehrichtung per Offset so korrigiert, dass der Wert richtungsunabhängig 
den Winkel eindeutig abbildet (nach der letzten Modulo-12000-Operation).

Mit einem umfangsbegrenzten Ringzähler hätte ich's einfacher, aber da an 
dem Controller noch 2 Stepper, 2 Drehgeber und mehrere I²C-Teile (incl. 
Display) hängen, wollte ich mir keine Timingproblem einhandeln.

Fehler würden bei meiner Berechnung erst entstehen, wenn ich 2^16 / 2 
überschreite, d.h, mehr als ca. 2,7 Umdrehungen in eine Richtung mache. 
Das kann ich aber ausschließen.

Aber wie gesagt, das ist alles nicht das Problem .. das besteht "nur" 
darin, die Ursache für die eine falsch rechnende Zeile zu finden.

Grüße
Thilo
#5975987
Lesenswert?

tg schrieb:
> In die andere Richtung zählt der 0, 65535, .. 53535 (1. Umdr.) ... usw.
> Je nachdem, wie herum ich drehe, bekomme ich also für den gleichen
> Winkel unterschiedliche Zählerstände.

Je nun, eigentlich bekommst du 0, -1, -2, usw. (bei positiver und 
negativer Drehrichtung ist ein signed integer ganz passend), was durch 
Addition von 12000  bei negativen Werten dann dazu führt, daß du doch 
die selben Werte für eine Winkelstellung erhältst, unabhängig von der 
Drehrichtung.

Oliver
Gast #5976008
Lesenswert?

Thomas Z. schrieb:
> Was passiert bei
>
> ra_val = ra_val % 12000L;
>
> Stimmt dann der Wert?

Nein, bleibt falsch. Ich hatte die Deklaration für encoder_resolution 
schon versuchsweise auf uint32_t geändert, ebenfalls ohne Erfolg.

Ich versuchs morgen mal mit dem Vorschlag von Oliver, scheint so, als 
könnte man damit die Modulo-Rechnerei ganz vermeiden ... ich melde mich 
wieder.

tg
#5976521
Lesenswert?

Da war noch ein Fehler drin.
old wird ja abgezogen, d.h. wenn Differenz negativ, muß old kleiner 
werden
So ist es richtig rum:
1
uint16_t modulo12000(uint16_t val)
2
{
3
  static uint16_t old = 0;
4
  while( (val - old) >= 12000)
5
  {
6
    if ((int16_t)(val - old) < 0)
7
      old -= 12000;
8
    else
9
      old += 12000;
10
  }
11
  return val - old;
12
}
#5976575
Lesenswert?

Ich würde gar nicht versuchen direkt auf der 16 Bit Variable des Zählers 
zu arbeiten. Stattdessen würde ich mir in der MCU den letzten 16 Bit 
Zählerstand merken.

Mit der Differenz des aktuellen Stand lässts sich das problemlos auf 
jede beliebige Darstellung, also auch zyklisch mit 12000 Schritten 
abbilden. Und zwar ganz ohne Modulo. (Ist vermutlich sogar schneller)
1
uint16_t last_enc;
2

3
int16_t pos;
4

5
void update_pos()
6
{
7
  uint16t_t enc = get_enc();
8

9
  /* max 3 iterations */
10
  while(enc != last_enc)
11
  {
12
    int16_t delta = (int16_t)(enc - last_enc);
13
    int16_t tmp;
14

15
    if (delta > 11999)
16
      delta = 11999;
17
    else if (delta < -12000)
18
      delta = -12000;
19

20
    last_enc += (uint16_t)delta;
21

22
    tmp = delta + pos;
23

24
    if (tmp >= 12000)
25
      pos = tmp - 12000
26
    else if (tmp < 0)
27
      pos = tmp + 12000;
28
    else
29
      pos = tmp;
30
  }
31
}
#5976588
Lesenswert?

Peter D. schrieb:
>
1
> uint16_t modulo12000(uint16_t val)
2
> {
3
>   static uint16_t old = 0;
4
>   while( (val - old) >= 12000)
5
>   {
6
>     if ((int16_t)(val - old) < 0)
7
>       old += 12000;
8
>     else
9
>       old -= 12000;
10
>   }
11
>   return val - old;
12
> }
13
>

Warum derart aufwendig?
1
uint16_t modulo12000(uint16_t val)
2
{
3
  while( val >= 12000)
4
  {
5
      val -= 12000;
6
  }
7
  return val;
8
}

oder ohne Schleife:
1
uint16_t modulo12000(uint16_t val)
2
{
3
  uint16_t mul = val / 12000;
4
  val -= mul*12000;
5
  return val;
6
}

oder als Einzeiler:
1
uint16_t modulo12000(uint16_t val)
2
{
3
  return val - ((val / 12000) * 12000);
4
}
#5976749
Lesenswert?

Etwas nachdenken hilft manchmal :-).

Andreas M. schrieb:
>
1
> ... // Umständlicher C-Code
2
>
1
uint16_t last_enc;
2

3
int16_t pos;
4

5
/** Update encoder position
6
 *
7
 *  @note Must be called once at least within two full rotations
8
 */
9
void update_pos()
10
{
11
  uint16t_t enc = get_enc();
12
  int16_t delta = 0;
13

14
  /* Modulo full rotations:
15
   * Works for max 2 full rotations in either
16
   * pos or neg direction */
17
  do
18
  {
19
    delta = (int16_t)(enc - last_enc)
20

21
    if (delta > 12000)
22
      delta = 12000;
23
    else if (delta < -12000)
24
      delta = -12000;
25

26
    last_enc += (uint16_t)delta;
27
  } while(enc != last_enc)
28

29

30
  /* Update current position with remaining
31
   * fraction of rotation */
32
  tmp = delta + pos;
33
 
34
  if (tmp >= 12000)
35
    pos = tmp - 12000
36
  else if (tmp < 0)
37
    pos = tmp + 12000;
38
  else
39
    pos = tmp;
40
}
Gast #5977030
Lesenswert?

So, wie gestern Abend angekündigt, habe ich mir nach Olivers Anregung, 
den Encoderwert als int statt uint zu behandeln, die Funktion 
umgestrickt:
1
//Quadraturencoder RA auslesen und aufbereiten, Übergabe in Sekunden
2
uint32_t get_ra_seconds (uint16_t encoder_resolution)
3
{
4
    uint32_t ra_val;
5
    int16_t  ra_enc;
6

7
    HCTL_CONTROL_PORT &= ~((1 << HCTL_SEL) | (1 << HCTL_OE_RA));    //SEL=L; OE=L; set inhibit
8
    asm volatile ("nop");                                           //Reaktionszeit für HCTL erforderlich
9
    ra_enc = (HCTL_DATA_PORT << 8);                                 //high-Byte lesen + schieben
10
    HCTL_CONTROL_PORT |= (1 << HCTL_SEL);                           //SEL=H; OE=L; start inhibit reset
11
    asm volatile ("nop");                                           //Reaktionszeit für HCTL erforderlich
12
    ra_enc |= HCTL_DATA_PORT;                                       //low-Byte lesen + addieren
13
    HCTL_CONTROL_PORT |= (1 << HCTL_OE_RA);                         //SEL=H; OE=H; complete inhibit reset
14

15
    while (ra_enc < 0)
16
        ra_enc += encoder_resolution;
17

18
    while (ra_enc >= encoder_resolution)
19
        ra_enc -= encoder_resolution;
20

21
    ra_val = (ra_enc * (uint32_t) 86400) / encoder_resolution;     //auf 24h umskaliert in Sekunden
22

23
    return ra_val;
24
}

Funktioniert wie geplant. Die Unterscheidung zwischen positiven und 
negativen Zählerständen habe ich mir gespart, da ja die kopfgesteuerten 
Schleifen eigentlich die selbe Funktion erfüllen, d.h., satt if oder 
else wird hier nur die zutreffende while-Schleife abgearbeitet. Ich 
nehem mal an, dass in der Bearbeitungszeit kein relevanter Unterschied 
besteht.

Ich danke euch nochmal für eure Mühe und Vorschläge!

Grüße
Thilo

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren