Forum: Mikrocontroller und Digitale Elektronik Drehencoder Atmega328P


von Herman Uh. (Gast)


Lesenswert?

http://www.mikrocontroller.net/articles/Drehgeber

Hallo,

ich möchte den Code für einen wackeligen Drehencoder verwerden (APLS).

Zur Hardware,

im Vergleich zum Orignalcode würde ich gerne ohne Pullups arbeiten. Habe 
das soweit auch angepasst.

Die Timer/ISR musste ich für dem m328p anpassen. ISR wird jede Sekunde 
aufgerufen (getestet).

Als Werte bekomme ich nur -1 oder 0 zurückgeliefert. Inkrementieren 
macht er nicht.


encoder.h
1
#ifndef ENCODER_H_
2
#define ENCODER_H_
3
4
#define ENCODER_PIN      PIND  
5
#define ENCODER_PHASE_A     5
6
#define ENCODER_PHASE_B     6
7
8
#define ENCODER_USE_PULLUP  1
9
10
int8_t encoder_init(void);
11
12
int8_t encoder_read(void);
13
14
#endif /* ENCODER_H_ */

encoder.c
1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3
#include <avr/pgmspace.h>
4
#include "encoder.h"
5
6
volatile int8_t enc_delta;
7
8
// Dekodertabelle
9
const int8_t table[16] PROGMEM =
10
{
11
  0,0,-1,0,0,0,0,1,1,0,0,0,0,-1,0,0
12
};
13
14
ISR(TIMER2_COMPA_vect)
15
{
16
  static int8_t last=0;
17
  last = (last<<2) & 0x0F;
18
  
19
  #if ENCODER_USE_PULLUP
20
  ENCODER_PIN |= ((1<<ENCODER_PHASE_A)|(1<<ENCODER_PHASE_B));
21
  #endif
22
23
  #if ENCODER_USE_PULLUP
24
  if (!(ENCODER_PIN & 1<<ENCODER_PHASE_A)) last |=2;
25
  if (!(ENCODER_PIN & 1<<ENCODER_PHASE_B)) last |=1;
26
  #else
27
  if (ENCODER_PIN & 1<<ENCODER_PHASE_A) last |=2;
28
  if (ENCODER_PIN & 1<<ENCODER_PHASE_B) last |=1;
29
  #endif
30
  
31
  enc_delta += pgm_read_byte(&table[last]);
32
}
33
34
int8_t encoder_init(void)
35
{
36
  TCCR2A = (1 << WGM21);            // CTC Modus
37
  TCCR2B |= (1 << CS21) | (1 << CS20);    // F_CPU / 64
38
  OCR2A = (uint8_t)(F_CPU / 64.0 * 1e-3 - 0.5);
39
  TIMSK2 |= (1 << OCIE2A);          // Compare Interrupt erlauben
40
  
41
  return 0;
42
}
43
44
int8_t encoder_read(void)
45
{
46
  int8_t val;
47
  
48
  // atomarer Variablenzugriff
49
  cli();
50
  val = enc_delta;
51
  enc_delta = 0;
52
  sei();
53
  
54
  return val;
55
}
56
57
main.c
58
[c]
59
#include <avr/io.h>
60
#include "lib/encoder/encoder.h"
61
#include "lib/uart/uart.h"
62
63
#define NEWLINE  uart_puts("\r\n")
64
65
int main(void)
66
{
67
  cli();
68
  
69
  uart_init(57600);
70
  uart_puts("Test \r\n");
71
  encoder_init();
72
73
  char buffer[20];
74
  sei();
75
  DDRD |= (1<<4);
76
  
77
  while(1)
78
  {
79
    static int8_t value;
80
    static int8_t last_value;
81
82
    if(last_value != value)
83
    {
84
      PORTD ^= 1<<4;
85
      itoa(value, buffer, 10);
86
      uart_puts(buffer);
87
      NEWLINE;
88
      
89
      last_value = value;
90
    }
91
    value += encoder_read();
92
  }
93
}

von Jürgen S. (jurs)


Lesenswert?

Herman Uh. schrieb:
> Als Werte bekomme ich nur -1 oder 0 zurückgeliefert. Inkrementieren
> macht er nicht.

Auch nicht, wenn Du den Drehgeber in Super-Zeitlupe drehst?

Dein Timer-Interrupt scheint bei 16 MHz Takt des Controllers nur 62,5 
mal pro Sekunde zu laufen. Und noch langsamer, wenn Du mit weniger als 
16 MHz taktest.

Das liegt für eine vernünftige Drehgeberauswertung für meinen Geschmack 
um den Faktor 10 zu niedrig.

Ich würde als erstes mal den Timer-Interrupt 500 bis 1000 mal pro 
Sekunde laufen lassen.

von Herman Uh. (Gast)


Lesenswert?

Pin getoggelt und mit Logik Analyser kommt ich auf 1kHz. Das ist es 
nicht. Also wird häufiger abgefragt. Habe es auch direkt mit OCR = 249 
versucht. Timer 0 und 2 beide getestet.

von Matthias S. (Firma: matzetronics) (mschoeldgen)


Lesenswert?

Herman Uh. schrieb:
> #if ENCODER_USE_PULLUP
>   ENCODER_PIN |= ((1<<ENCODER_PHASE_A)|(1<<ENCODER_PHASE_B));
>   #endif

Ich habe keine Ahnung, was du da vorhast, aber wenn man ins PIN Register 
was reinschreibt, toggelt man nur das entsprechende Bit im Data 
Register:
Zitat:
'However, writing a logic one to a bit in the PINx Register, will result 
in a toggle in the corresponding bit in the Data Register.'

Vllt. schreibst du nochmal, wie du den Encoder nun wirklich 
angeschlossen hast. Ausserdem sehe ich keine Initialisierung für die 
internen Pullups.

von greg (Gast)


Lesenswert?

Heißer Tipp, Mikrocontroller mit Hardwareunterstützung für 
Quadraturencoder nehmen, z.B. xmega. Alternativ gehen auch spezielle 
ASICs oder wenn du ambitioniert bist ein CPLD/FPGA. Alles andere 
verursacht nur Kopfschmerzen, gerade wenn's genau sein soll und der 
Mikrocontroller noch andere Sachen tun soll außer den Encoder 
abzufragen.

von m.n. (Gast)


Angehängte Dateien:

Lesenswert?

greg schrieb:
> Heißer Tipp, Mikrocontroller mit Hardwareunterstützung für
> Quadraturencoder nehmen, z.B. xmega.

Eiskalter Tipp: nimm den ATmega, den Du hast und programmiere ihn 
richtig! Ich hänge ein Beispiel(-schnipsel) für einen Quadraturdekoder 
per Timer an. Dieses ganze Tabellenzeugs braucht man nicht.

greg schrieb:
> Alles andere
> verursacht nur Kopfschmerzen,

So ist es ;-)

von Dieter F. (Gast)


Lesenswert?

Herman Uh. schrieb:
> #if ENCODER_USE_PULLUP
>   ENCODER_PIN |= ((1<<ENCODER_PHASE_A)|(1<<ENCODER_PHASE_B));
>   #endif

Damit schaltest Du aber keine Pullups ein, bei der Definition:

Herman Uh. schrieb:
> #define ENCODER_PIN      PIND

Über PORT.. setzt Du den Pullup-Widerstand.
http://www.mikrocontroller.net/articles/AVR-Tutorial:_IO-Grundlagen


Und .. darf man erfahren, warum Du nicht mit Pullups arbeiten willst?

von Herman Uh. (Gast)


Angehängte Dateien:

Lesenswert?

@greg: Warum nur für den Encoder ein speziellen Controller? Es handelt 
sich um folgenden Encoder (siehe Bild). Genutzt wird dieser mit einer 
niedrigen Geschwindigkeit. Dieser dient lediglich zur Menüführung.


Externe Pullups möchte ich nicht verwenden, da ich diese nirgends 
anlöten kann derzeit. Sollte aber doch auch ohne gehen wenn ich die 
internen aktiviere. Tut es leider noch nicht.

Der Encoder ist wie folgt angeschlossen:
1 PD5
2 GND
3 PD6


//h-File
1
#ifndef ENCODER_H_
2
#define ENCODER_H_
3
4
#define ENC_DDR    DDRD
5
#define ENC_PORT  PORTD
6
#define ENC_PIN    PIND
7
#define ENC_BIT_A  5
8
#define ENC_BIT_B  6
9
10
#define ENC_USE_PU  // PullUps aktivieren
11
12
int8_t encoder_init(void);
13
14
int8_t encoder_read(void);
15
16
#endif /* ENCODER_H_ */

//c-File
1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3
#include <avr/pgmspace.h>
4
#include "encoder.h"
5
6
volatile int8_t enc_delta;
7
8
// Dekodertabelle
9
const int8_t table[16] PROGMEM =
10
{
11
  0,0,-1,0,0,0,0,1,1,0,0,0,0,-1,0,0
12
};
13
14
ISR(TIMER0_COMPA_vect)
15
{
16
  static int8_t last=0;
17
  last = (last<<2) & 0x0F;
18
  PORTD |= 1<<4;
19
20
  #ifdef ENC_USE_PU
21
  if (!(ENC_PIN & 1<<ENC_BIT_A)) last |=2;
22
  if (!(ENC_PIN & 1<<ENC_BIT_B)) last |=1;
23
  #else
24
  if (ENC_PIN & 1<<ENC_BIT_A) last |=2;
25
  if (ENC_PIN & 1<<ENC_BIT_B) last |=1;
26
  #endif
27
  
28
  enc_delta += pgm_read_byte(&table[last]);
29
}
30
31
int8_t encoder_init(void)
32
{
33
  TCCR0A = (1 << WGM01);            // CTC Modus
34
  TCCR0B |= (1 << CS01) | (1 << CS00);    // F_CPU / 64
35
  OCR0A = 249;                 //(uint8_t)(F_CPU / 64.0 * 1e-3 - 0.5)
36
  TIMSK0 |= (1 << OCIE0A);          // Compare Interrupt erlauben
37
  
38
  // als Eingaenge
39
  ENC_DDR &= ~((1<<ENC_BIT_A) | (1<<ENC_BIT_B));
40
  
41
  // PullUps aktivieren
42
  #ifdef ENC_USE_PU
43
  ENC_PORT |= (1<<ENC_BIT_A) | (1<<ENC_BIT_B);
44
  #endif
45
  
46
  return 0;
47
}
48
49
int8_t encoder_read(void)
50
{
51
  int8_t val;
52
  
53
  // atomarer Variablenzugriff
54
  cli();
55
  val = enc_delta;
56
  enc_delta = 0;
57
  sei();
58
  
59
  return val;
60
}

//Aufruf
1
  while(1)
2
  {
3
    static int8_t value;
4
    static int8_t last_value;
5
  
6
    if(last_value != value)
7
    {
8
      uart_puts(itoa(value, buffer, 10));
9
      NEWLINE;
10
11
      last_value = value;
12
    }
13
    value += encoder_read();
14
  }

von greg (Gast)


Lesenswert?

Herman Uh. schrieb:
> @greg: Warum nur für den Encoder ein speziellen Controller? Es handelt
> sich um folgenden Encoder (siehe Bild). Genutzt wird dieser mit einer
> niedrigen Geschwindigkeit. Dieser dient lediglich zur Menüführung.

Naja, dann ist es ziemlich egal, wie man die Signale dekodiert. Ich 
dachte ich hätte aus deinem Post rausgelesen, dass du hohe Genauigkeit 
benötigst. Hab mich da wohl geirrt.

von Herman Uh. (Gast)


Lesenswert?

@greg,

kein Problem.

Das ganze geht leider noch nicht, spuckt mir nur 2 verschiedene Werte 
aus. -1 oder 0

von m.n. (Gast)


Lesenswert?

Herman Uh. schrieb:
> Das ganze geht leider noch nicht,

Ein Codeschnipsel reicht Dir nicht, das Ganze mal anders anzugehen?

von Jürgen S. (jurs)


Lesenswert?

Herman Uh. schrieb:
> Externe Pullups möchte ich nicht verwenden, da ich diese nirgends
> anlöten kann derzeit. Sollte aber doch auch ohne gehen wenn ich die
> internen aktiviere.

Solange die Leitungen nicht zu lang werden, spricht ja auch gar nichts 
gegen die internen PullUps.

> Tut es leider noch nicht.

Heute mit dem geänderten Timer läuft der Interrupt auch tatsächlich 1000 
mal pro Sekunde.

Ich habe mir Deinen Code in die Arduino IDE gezogen (mit minimalen 
Änderungen, die der Arduino-IDE geschuldet sind), und der Code 
funktioniert wunderprächtig auf einem Arduino-Board mit Atmega328 und 16 
MHz Taktfrequenz!

Ich sehe nicht, warum das bei Dir nicht funktionieren sollte.

von Max M. (jens2001)


Lesenswert?

Code aus verschiedenen  Quellen per  c&p zusammengeklickt u. noch ein 
wenig selbst drann rumgepfuscht.
Und du erwartest das dabei was vernünftiges raus kommt?

von isidor (Gast)


Lesenswert?

Warum fragt hier niemand nach dem konkreten Schaltplan ?

Wenn hier interne Pullups verwendet werden (und sonst nichts)
dann hat der Aufbau keinerlei Entprellung (dazu bräuchte man
einen Längswiderstand und einen Kondensator für jedes einzelne
Signal.

von Jürgen S. (jurs)


Lesenswert?

isidor schrieb:
> Warum fragt hier niemand nach dem konkreten Schaltplan ?
>
> Wenn hier interne Pullups verwendet werden (und sonst nichts)
> dann hat der Aufbau keinerlei Entprellung (dazu bräuchte man
> einen Längswiderstand und einen Kondensator für jedes einzelne
> Signal.

Das Ding ist aus zwei Gründen entprellt:

1. Die Drehgeberstellung wird nur 1x pro Millisekunde abgefragt und das 
Prellen mechanischer Kontakte dauert fast nie länger als 1 Millisekunde.

2. Der Drehgeber zählt mit einwandfreier Auswertelogik vor- und 
rückwärts. Solange von der Drehgeschwindigkeit her nicht "überdreht" 
wird, führt ein Prellen, das ausnahmsweise über 1ms dauert, allenfalls 
dazu, dass im Millisekundentakt vor- und rückwärts gezählt wird.

Der heute gepostete Code, der @16 Mhz Taktfrequenz mit 1000 Interrupts 
pro Sekunde läuft, läuft nach meiner Feststellung einwandfrei, ich kann 
jedenfalls kein Problem damit feststellen.

von Dieter F. (Gast)


Lesenswert?

Hi,

also bei mir funktioniert das - mit PA2 und PA3 - mit denke ich dem 
gleichen oder ähnlichen Encoder wunderbar (Dank P. Danegger).

Die Erklärung, wie das funktioniert habe ich mir aus div. Quellen 
zusammenkopiert, damit ich immer weiß, was ich da mache:



/*********************************************************************** 
*/
/* 
*/
/*            Drehgeber mit wackeligem Rastpunkt dekodieren 
*/
/* 
*/
/*********************************************************************** 
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>

#define XTAL        16e6                  // 16MHz

#define PHASE_A     (PINA & 1<<PA3)              // an Pinbelegung 
anpassen
#define PHASE_B     (PINA & 1<<PA2)              // an Pinbelegung 
anpassen


volatile int8_t enc_delta;                  // Drehgeberbewegung 
zwischen

/*
Darauf basiert die Tabelle ...
dgtab:      ;Tabelle mit Drehgeber-Werten (alt-alt-neu-neu als Index)
;aa nn,     aa nn
.db     0, 0        ;00 00,     00 01
.db     0, 0        ;00 10,     00 11
.db     1, 0        ;01 00,++   01 01
.db     0, 0        ;01 10,     01 11
.db    -1, 0        ;10 00,--   10 01
.db     0, 0        ;10 10,     10 11
.db     0, 0        ;11 00,     11 01
.db     0, 0        ;11 10,     11 11

Mein Alps Drehgeber liefert nur Impulse - hat also generell den Zustand
11 und liefert je nach Drehrichtung Impulsfolgen 01->00 oder 10->00 !!


Hier die generelle Erklärung:

Bei Drehgebern, die ihre Rastung auf 00 und 11 haben, gibt es pro
Rastung auf jeder Spur eine Flanke. Um diese Drehgeber auszuwerten,
prüft man ja eine Spur auf Flanke und die andere Spur auf Zustand. Bei
symmetrischen Drehgebern ist es egal, welche Spur man auf Flanke prüft.
Bei diesen "selten dämlichen" Drehgebern prüft man Spur A (also die
Spur, die im eingerasteten Zustand stabilen Pegel liefert) auf Flanke
und Spur B auf Zustand. Da der Zustand nur relevant ist, wenn eine
Flanke erkannt wurde, spielt der (eingerastet) undefinierte Zustand der
Spur B keine Rolle.

Zum Abtasten des Drehgebers wird das Bitmuster (der Zustand der beiden
Spuren) eingelesen und auf die beteiligten Bits maskiert. Dies wird dann
zu dem "gemerkten" und um 2 Bits verschobenen Bitmuster der letzten
Abtastung geORT. Es entsteht eine 4-Bit-Zahl, die als Index auf die LUT
genutzt wird. Von diesen 16 möglichen Zuständen sind bei diesen
Drehgebern aber nur 4 Zustände relevant.

In die LUT werden also nur dort Incremente eingetragen, wo Spur A den
Pegel wechselt (also eine Flanke hat).

Eine Drehrichtung:
Rastung alt neu
A B A B
-------
-->    0 0 0 1   1
|  >   0 1 1 1   7  Flanke an A
|      1 1 1 0  14
|  >   1 0 0 0   8  Flanke an A
|      0 0 0 1   1
|  >   0 1 1 1   7  Flanke an A
|      1 1 1 0  14
|  >   1 0 0 0   8  Flanke an A
--<    0 0 0 1   1

Andere Drehrichtung:
Rastung alt neu
A B A B
-------
-->    0 1 0 0   4
|  >   0 0 1 0   2  Flanke an A
|      1 0 1 1  11
|  >   1 1 0 1  13  Flanke an A
|      0 1 0 0   4
|  >   0 0 1 0   2  Flanke an A
|      1 0 1 1  11
|  >   1 1 0 1  13  Flanke an A
--<    0 1 0 0   4

Die eine Drehrichtung ergibt also bei Flanken an Spur A die Zahlenwerte
(als Index auf die LUT) 7 und 8, die andere Drehrichtung 2 und 13.
Daraus ergibt sich, dass bei Index 7 und 8 der Wert +1 (als Increment)
in die LUT eingetragen wird und bei Index 2 und 13 der Increment-Wert
-1. Alle anderen Elemente des Arrays (der LUT) werden mit dem Wert 0
aufgefüllt.

Wird Spur A und B vertauscht, so ergeben sich andere Index-Werte. Bei
symmetrischen Drehgebern ist das egal, die "selten dämlichen" spinnen
dann aber.

Somit wird der Zählerstand nur verändert, wenn eine Flanke an Spur A
erkannt wurde. Der Zustand von B ist in diesem Zeitpunkt ja stabil.

Dies alles läuft im Timer-Int oder einem per Timer synchronisierten Job
ab. Die Aufruf-Frequenz ist ein Kompromiss zwischen CPU-Last und maximal
möglicher Drehgeschwindigkeit. Bei Verwendung des Drehgebers als
manuelles Eingabegerät hat sich bei mir eine Abtastfrequenz von 1 kHz
bewährt.

Die Mainloop (bzw. ein Job davon) addiert nun diesen Zählerstand auf den
zur Bearbeitung anstehenden Wert und löscht ihn danach. Somit gehen
keine Drehbewegungen verloren, wenn es in Main mal länger dauert.
*/

                              // zwei Auslesungen im Hauptprogramm
                              // Dekodertabelle für meinen speziellen 
Drehgeber
                              // volle Auflösung
const int8_t table[16] PROGMEM = {0,0,0,0,1,0,0,0,-1,0,0,0,0,0,0,0};


ISR( TIMER0_COMP_vect )                    // 1ms fuer manuelle Eingabe
{
  static int8_t last=0;                  // alten Wert speichern
  static int8_t akt =0;                  // alten Wert speichern

  akt = last & 0x0C;
  if (PHASE_A) akt |=2;
  if (PHASE_B) akt |=1;

  if( akt != last)
  {
    enc_delta += pgm_read_byte(&table[akt]);
    last  = ((akt << 2) & 0x0C) | (akt & 0x03);
  }

    last = (last << 2)  & 0x0F;
    if (PHASE_A) last |=2;
    if (PHASE_B) last |=1;
    enc_delta += pgm_read_byte(&table[last]);

}


void encode_init( void )                  // nur Timer 0 initialisieren
{
  DDRA  &= ~(1<<PA2);                  //Pins als Eingang
  DDRA   &= ~(1<<PA3);
  PORTA   |=  (1<<PA2);                  //Pullups aktivieren
  PORTA   |=  (1<<PA3);

  TCCR0 = (1<<WGM01) | (1<<CS01)  | (1<<CS00);      // CTC, XTAL / 64
  OCR0 = (uint8_t)(XTAL / 64.0 * 1e-2 - 0.5);        // 64 uS
  TIMSK |= 1<<OCIE0;

  sei();
}



int8_t encode_read( void )                  // Encoder auslesen
{
  int8_t val;

                              // atomarer Variablenzugriff
  cli();
  val = enc_delta;
  enc_delta = 0;
  sei();
  return val;
}






Drehgeber.h sieht bei mir so aus:

/*
 * Drehgeber.h
 *
 * Created: 07.12.2013 09:31:02
 *  Author: Dieter
 */

#ifndef DREHGEBER_H_
#define DREHGEBER_H_


#endif                            /* DREHGEBER_H_ */

void encode_init( void );

int8_t encode_read( void );                  // read single step 
encoders
int8_t encode_read1( void );
int8_t encode_read2( void );
int8_t encode_read4( void );



Das sind rein private Kopieen von Peter Daneggers Programmier-Leistung 
und nicht zur Weitergabe bestimmt.  Wer es verwendet möge bitte 
beachten, dass nicht ich der Autor bin !!!!

von Herman Uh. (Gast)


Lesenswert?

würg

Da hätte ich lange an 5 und 6 testen können. PD6 hat scheinbar nen 
Schuss weg. An PD2-5 geht es wunderbar.

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.