4

I have a problem with my Arduino Uno and my AS5311 position sensor

I am trying to get incremental output as described in Section 7 but I don't really know how to do it. It says that there are three outputs: A, B, and index. A and B will change states 512 times per 2mm of magnetic band (256 pulses) and index will produce a pulse every 2mm. It says that A "leads" B when the magnet is moving from right to left and B "leads" A when the magnet is moving from left to right.

What is the right way to get this information? I suppose it's using interrupts on A, B, and index but I'm not sure how.

Here is what I have so far (absolute position works fine):

const int A_Pin = 2;
const int B_Pin = 3;
const int Index_Pin = 8;
const int CSn_Pin = 5;
const int CLK_Pin = 6;
const int DO_Pin = 7;
int Bit;
int Position;
int Pow2[12] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048};
int OCF, COF, LIN, MagINC, MagDEC, EvenPAR;
volatile int ACount = 0;
volatile int BCount = 0;
volatile int IndexCount = 0;

void fA(){
  ACount++;
  //What should I do here?
}

void fB(){
  BCount++;
  //What should I do here?
}

ISR(PCINT0_vect){
  if(digitalRead(Index_Pin) == 1) IndexCount++;
}

void setup(){
  //For incremental output
  pinMode(A_Pin, INPUT_PULLUP);
  pinMode(B_Pin, INPUT_PULLUP);
  pinMode(Index_Pin, INPUT);
  attachInterrupt(digitalPinToInterrupt(A_Pin), fA, RISING);
  attachInterrupt(digitalPinToInterrupt(B_Pin), fB, RISING);
  digitalWrite(Index_Pin, HIGH);

  //For absolute position
  pinMode(CSn_Pin, OUTPUT);
  pinMode(CLK_Pin, OUTPUT);
  pinMode(DO_Pin, INPUT);
  digitalWrite(CSn_Pin, HIGH);
  digitalWrite(CLK_Pin, HIGH);

  Serial.begin(9600);

  cli();
  PCICR = 1;
  PCMSK0 = Pow2[Index_Pin - 8];
  sei();
}

void loop(){
  //Absolute position
  //delay(1);
  digitalWrite(CSn_Pin, LOW);
  //delay(1);

  Position = 0;
  for(int z0 = 0; z0 < 12; z0++){
    digitalWrite(CLK_Pin, LOW);
    //delay(1);
    digitalWrite(CLK_Pin, HIGH);
    //delay(1);
    Bit = digitalRead(DO_Pin);
    Position += Bit * Pow2[11 - z0];
  }

  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  OCF = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  COF = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  LIN = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  MagINC = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  MagDEC = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  EvenPAR = digitalRead(DO_Pin);

  digitalWrite(CSn_Pin, HIGH);

  Serial.print(Position);
  Serial.print(", ");
  Serial.print(OCF);
  Serial.print(", ");
  Serial.print(COF);
  Serial.print(", ");
  Serial.print(LIN);
  Serial.print(", ");
  Serial.print(MagINC);
  Serial.print(", ");
  Serial.print(MagDEC);
  Serial.print(", ");
  Serial.print(EvenPAR);
  Serial.print(", ");

  //Incremental output
  Serial.print(ACount);
  Serial.print(", ");
  Serial.print(BCount);
  Serial.print(", ");
  Serial.println(IndexCount);
}

The first problem is that after a while ACount and BCount drift apart so sometimes interrupts appear to be overlooked. The second problem is that I don't know how to tell which output "leads" the other one. How can I get this information (direction of movement) consistently?

Update: I tried what was suggested in the comments and it works most of the time. The problem is that sometimes I seem to get a false reading. When A goes up I check the state of B to see if I have left or right movement. I save the movement into a boolean variable (Left) which I then use in the function that is called when index is up. If Left is true I decrease the index counter, otherwise I increase it. But after some time the index counter differs by two from its true value which means that I had a false direction of movement when index is up. Here is my new code:

const int A_Pin = 2;
const int B_Pin = 3;
const int Index_Pin = 8;
const int CSn_Pin = 5;
const int CLK_Pin = 6;
const int DO_Pin = 7;
int Bit;
int Position;
int Pow2[12] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048};
int OCF, COF, LIN, MagINC, MagDEC, EvenPAR;
bool Error = false;
volatile int ACount = 0;
//volatile int BCount = 0;
volatile bool Left = true;
volatile int IndexCount = 0;

void fA(){
  if(digitalRead(B_Pin) == HIGH){
    Left = true;
    ACount--;
  }
  else{
    Left = false;
    ACount++;
  }
}

//void fB(){
//  BCount++;
//}

ISR(PCINT0_vect){
  if(digitalRead(Index_Pin) == 1){
    if(Left) IndexCount--;
    else IndexCount++;
  }
}

void setup(){
  pinMode(A_Pin, INPUT_PULLUP);
  pinMode(B_Pin, INPUT_PULLUP);
  pinMode(Index_Pin, INPUT);
  attachInterrupt(digitalPinToInterrupt(A_Pin), fA, RISING);
  //attachInterrupt(digitalPinToInterrupt(B_Pin), fB, RISING);
  digitalWrite(Index_Pin, HIGH);
  pinMode(CSn_Pin, OUTPUT);
  pinMode(CLK_Pin, OUTPUT);
  pinMode(DO_Pin, INPUT);
  digitalWrite(CSn_Pin, HIGH);
  digitalWrite(CLK_Pin, HIGH);

  Serial.begin(4800);

  cli();
  PCICR = 1;
  PCMSK0 = Pow2[Index_Pin - 8];
  sei();
}

void loop(){
  //delay(1);
  digitalWrite(CSn_Pin, LOW);
  //delay(1);

  Position = 0;
  for(int z0 = 0; z0 < 12; z0++){
    digitalWrite(CLK_Pin, LOW);
    //delay(1);
    digitalWrite(CLK_Pin, HIGH);
    //delay(1);
    Bit = digitalRead(DO_Pin);
    Position += Bit * Pow2[11 - z0];
  }

  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  OCF = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  COF = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  LIN = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  MagINC = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  MagDEC = digitalRead(DO_Pin);
  digitalWrite(CLK_Pin, LOW); //delay(1);
  digitalWrite(CLK_Pin, HIGH); //delay(1);
  EvenPAR = digitalRead(DO_Pin);

  digitalWrite(CSn_Pin, HIGH);

  if(LIN == HIGH || MagINC == HIGH || MagDEC == HIGH) Error = true;

  Serial.print(Position);
  Serial.print(", ");
  Serial.print(OCF);
  Serial.print(", ");
  Serial.print(COF);
  Serial.print(", ");
  Serial.print(LIN);
  Serial.print(", ");
  Serial.print(MagINC);
  Serial.print(", ");
  Serial.print(MagDEC);
  Serial.print(", ");
  Serial.print(EvenPAR);
  Serial.print(", ");

  Serial.print(ACount);
  Serial.print(", ");
  Serial.print(Left);
  Serial.print(", ");
  //Serial.print(BCount);
  //Serial.print(", ");
  Serial.print(IndexCount);
  Serial.print(", ");
  Serial.println(Error); 
}

Update 2: Maybe the problem comes from the hysteresis described in section 7.2? If the change of direction is directly at the index pulse, wouldn't I get a false direction of movement?

  • 4
    Search this site for “rotary encoder”. Even though yours may not be rotary, it's exactly the same problem. – Edgar Bonet Apr 6 '17 at 15:04
  • 1
    On a rising edge of A check the state of B, if it's high B is leading A, if it's low A is leading B. – Andrew Apr 6 '17 at 15:42
  • Thanks for your helpful comments. I updated my question accordingly. – Mario Apr 6 '17 at 19:03
4

Using ad hoc methods to handle quadrature inputs (ie, the kind of signals you have) can lead to missed pulses and accumulated error. However, it's possible to handle fairly fast quadrature input pulse trains using pin-change-interrupts on an Arduino.

In my answer to the question How to read RPS for fast rotation with status change method in Arduino?, you will find links to several previous answers that include code to process quadrature inputs at reasonable data rates; for example, Reading from a KY-040 rotary encoder with Digispark.


Edit 1, part 1: Ad hoc methods (such as just checking the state of input B whenever input A has a rising edge), while often simple to implement, can cause extra counts or missing counts. Extra counts may occur if A bounces and has several rising edges in a short interval. Missing counts may occur if rising edges of A occur faster than they are handled, particularly if the ad hoc code includes delays to deal with contact bounce.

Typical finite state machine methods for handling quadrature inputs, by contrast, automatically debounce A and B inputs, either by ignoring cases where a set of inputs repeats, or by counting an equal number down and up transitions as bouncing occurs. See for example the diagrams in reply #17 in “easy rotary encoder implementation” at eevblog.com – from which the first diagram below is taken – and in Omapl137 linux eqep driver at ti.com, where the second diagram is from. Both those sources provide substantial detail about processing quadrature inputs.

eevblog state machine with debounce ti state machine


Edit 1, part 2: Besides using a correct model of the quadrature process, encoder software should be fast, delay free, and asynchronous to other processing.

Slow handling of events (that is, of changes on encoder inputs) is caused by lengthy code or indirect IO, inter alia. Slow event handling cannot keep up with as many events per second as fast event handling; if code is really slow, users will notice that they have to turn or move an encoder at a snail's pace to avoid missing menu items or location steps.

Trying to handle events synchronously, eg via polling, also limits the rate at which software processes events. Unless a processor is quite fast or is dedicated to quadrature input, it is likely to miss occasional transitions on inputs. Using interrupts usually is a better choice when dealing with asynchronous transitions like encoder inputs.

By “indirect IO” I mean use of functions like digitalRead() and attachInterrupt() which result in massive overhead for IO processing.

digitalRead() takes between 3 and 4 μs to read one bit of input. Directly reading a port takes one instruction cycle, 62.5 ns (1/16 μs) on a typical Uno or Nano. If two encoder inputs are wired to the same port, one port read is enough, vs two digitalRead() calls. Note, this comparison assumes encoder pin numbers are known at compile time, that the target MCU is known, etc. With direct port IO, the programmer is responsible for making the software conform to the MCU, instead of having digitalRead() taking care of details like which port bit corresponds to which Arduino digital input.

attachInterrupt() adds about 5 μs of overhead to interrupt processing. [See the section “How long does it take to execute an ISR?” in Nick Gammon's interrupts page.] This is on top of about 2.6 μs of ordinary software overhead (for interrupts on Arduino Uno, Nano, etc.).

Note, some of the example code in previous answers uses attachInterrupt() and digitalRead(), but is concise enough to still get reasonable input rates. Using those functions leads to better code readability and portability. However, for faster input rates, you might use code as shown below. This code probably could be improved (shortened, made faster, made more clear) but anyhow functions ok for up to a few hundred counts per second. To adapt this code for other input pins, replace abNew = PINC & 3; by code to set the low two bits of abNew equal to the input bits; and adapt setup_rotocoder() accordingly. See (eg) Arduino Pin Change Interrupts and Simple Pin Change Interrupt on all pins for further information about setting up pin change interrupts.

// The following three vars should be visible to loop(), which
// can find out if an encoder changed by testing rotoInt.
static   byte roABold;          // A,B states for rotor
volatile uint16_t roCount;      // current rotary count
volatile byte rotoInt;          // Rotary-interrupt flag (change flag)

// jiw (c) 2015  Offered without warranty under GPL v3 terms as at http://www.gnu.org/licenses/gpl.html

ISR(PCINT1_vect) {  // This plus Arduino overhead of 2.6 us probably takes ca 5 us total per interrupt
  enum { upMask = 0x66, downMask = 0x99 };// up & down are complements
  byte roABnew = PINC & 3;  // Read pins  14, 15, which are A0, A1
  byte criterion = roABnew ^ roABold;
  if (criterion==1 || criterion==2) {
    if (upMask & (1 << (2*roABold + roABnew/2))) // Embeds state transition rules
      ++roCount;
    else --roCount;             // upMask = ~downMask
  }
  roABold = roABnew;            // Save new state
  rotoInt = 1;                  // Say something happened
}

// For rotary encoder initialization, in setup(), say:   setup_rotocoder();
void setup_rotocoder() {
  rotoInt = roABold = 0;
  roCount = 32768;              // Start with a middle count
  pinMode(A0, INPUT_PULLUP);    // pin 14, A0, PC0, for pin-change interrupt
  pinMode(A1, INPUT_PULLUP);    // pin 15, A1, PC1, for pin-change interrupt
  PCMSK1 |= 0x03;
  PCIFR  |= 0x02;             // clear pin-change interrupts if any
  PCICR  |= 0x02;             // enable pin-change interrupts
}
  • Could you please elaborate on why rotary encoders tend to lose count and how the solutions you suggested solve the problem? I don't only want it to work, I would also like to understand. I should mention that this is the first time I've ever used a microcontroller so I am a total beginner (know a thing or two about C++ though). – Mario Apr 7 '17 at 6:30
  • @Mario, see edit – James Waldby - jwpat7 Apr 7 '17 at 21:01
  • In addition to this excellent answer I have a page about rotary encoders. – Nick Gammon Apr 7 '17 at 21:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.