Forum: Mikrocontroller und Digitale Elektronik Falsche Ergebnisse mit Modulo


von tg (Gast)


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);

von Stefan F. (Gast)


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.

von c-hater (Gast)


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.

von Stefan F. (Gast)


Lesenswert?

c-hater schrieb:
> du hast keine Ahnung von dem, was du da tust. Lerne.

Bitte ...

von Oliver S. (oliverso)


Lesenswert?

Auch wenn es wie üblich grenzwertig deutlich formuliert ist, hat c-hater 
im Prinzip wohl recht.

Der Wurm steckt nicht im Modulo-Operator.

Oliver

von tg (Gast)


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

von Egon D. (Gast)


Lesenswert?

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?

von Thomas Z. (usbman)


Lesenswert?

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.

Thomas

von c-hater (Gast)


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.

von Stefan F. (Gast)


Angehängte Dateien:

Lesenswert?

tg schrieb:
> sollte 11753 raus kommen

Tut es bei mir auch.

von tg (Gast)


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.

von Stefan F. (Gast)


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.

von Egon D. (Gast)


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.

von tg (Gast)


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

von Egon D. (Gast)


Lesenswert?

tg schrieb:

> Ich hatte wie gesagt die Hoffnung, das jemand was
> auffällt, was ich konstant übersehe, [...]

Ist ja auch geschehen -- aber die Leute ignorierst
Du ja.

Nun gut.

von Klaus (Gast)


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

von Oliver S. (oliverso)


Lesenswert?

Müsste man, spielt aber beim TO keine Rolle.

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

Oliver

von tg (Gast)


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

von tg (Gast)


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

von Thomas Z. (usbman)


Lesenswert?

Was passiert bei

ra_val = ra_val % 12000L;

Stimmt dann der Wert?

Thomas

von Oliver S. (oliverso)


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

von tg (Gast)


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

von Peter D. (peda)


Lesenswert?

Dein Fehler ist, daß 65536 (uint16_t) nicht ohne Rest durch 12000 
teilbar ist.
Bilde daher die Differenz zu einer Variable, die Du nachführst, sobald 
der Wert außerhalb 0..11999 ist.
Differenzen stimmen immer, auch über einen Überlauf hinweg.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

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
}

von Stefan F. (Gast)


Lesenswert?

Peter D. schrieb:
> Dein Fehler ist, daß 65536 (uint16_t)

für 16 bit genau um 1 zu groß ist. Aber egal, wir haben trotzdem 
verstanden, was du meintest.

von Peter D. (peda)


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
}

von Andreas M. (amesser)


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
}

von Samuel C. (neoexacun)


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
}

von Andreas M. (amesser)


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
}

von tg (Gast)


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

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.