Using Zumo 32U4 Encoders library on A-Star 32U4 SV controller?

Hello, as posted here, i am progressing on my first robot project and am now ready to implement the wheel encoders.

I wonder if I could use the encoders functions from the Zumo 32U4 Arduino library with some modifications to the pin assignments?

The Zumo 32U4 uses the following pins:

  • Left encoder XOR (A) => pin 8 (interrupt)
  • Left encoder B => IO_E2 (PE2)
  • Right encoder XOR (A) => pin 7 (interrupt)
  • Right encoder B => pin 23

I would use the following pins on the A-Star 32U4:

  • Left encoder XOR (A) => pin 8 (interrupt) - same as Zumo
  • Left encoder B => pin 4 (A6) - cannot use PE2 since it is used for motor 2 direction on the A-Star
  • Right encoder XOR (A) => pin 7 (interrupt) - same as Zumo
  • Right encoder B => pin 5 - cannot use pin 23 (A5) since I am already using it for my sonar.

Any thoughts?

Hello.

The Zumo 32U4 main board uses XOR gates to combine both channels of each encoder so that one interrupt is used instead of two. (For more information, see the Zumo 32U4 schematic diagram and the “Quadrature encoder” section of the Zumo 32U4 Robot User’s Guide. Both resources can be found under the “Resources” tab on any of the Zumo 32U4 product pages.) You could use the Zumo 32U4 encoder library (Zumo32U4Encoders) to read the encoder signals from the A-Star 32U4 SV Robot Controller, but you will need to add an XOR gate between pins 8 and 4 and pins 7 and 5.

- Amanda

Hello Amanda, thanks for the prompt reply.

In light of your response, the Zumo user guide is indeed very clear. However, as a complete beginner in electronics adding XOR gates looks a bit out of reach (probably need a special chip to do so???)

Instead of adding an XOR gate, I will look into using digital pins 15 and 16 (PCINT1 & PCINT2) as interrupts for pin B of the two encoders. I read a few very useful references on external interrupts and pin change interrupts (http://playground.arduino.cc/Main/PinChangeInterrupt & http://gammon.com.au/interrupts for instance). These indicate which PCINT vector corresponds to different ranges of pins as well as the corresponding PCMSK / PCIF / PCIE values as per table below. The issue is that these lists do not include digital pins 15 and 16 so I don’t know which PCINT_vect and PCMSK / PCIF / PCIE values to use. Is there somewhere I could look to find about those for digital pins 15 & 16?

D0	  PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1	  PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2	  PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3	  PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4	  PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5	  PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6	  PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7	  PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8	  PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9	  PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10	  PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11	  PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12	  PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13	  PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0	  PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1	  PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2	  PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3	  PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4	  PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5	  PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Another issue I am facing is that while testing my encoders and code for handling interrupts, I noticed that pin B of one of my two encoders stays high permanently. Inverting the cables between the two motors/encoders allowed me to confirm that the problem is in the encoder itself. I checked the welds of the headers and they are clean so I could not locate any source of possible short to VCC. It seems I will have to replace this encoder with a new one.

Without a functional pin B, I can still count all changes on pin A. At 6 counts per motor rotations, one count equals to 0.5 degree rotation of the Romi wheels which is still quite accurate. Since without pin B I cannot determine the direction of rotation, I use the motor direction commands to infer the direction of rotation and change the sign of the delta counts between polls. This is not ideal but it will do the job until I get a new encoder.

Still a lot to do but considering where I start just a few days ago, I consider I made good progress!

Yes, you would need to add an XOR chip like this one from Digi-Key to your setup, but it should not be too difficult to integrate that part. You can look at that chip’s datasheet (which is linked on that page) for details on how to connect the two output channels of each encoder to the chip and the respective XOR gate outputs to the interrupt pins (pins 7 and 8) on the A-Star 32U4 SV Robot Controller.

The pin table you copied from the “Interrupts” subforum on Nick Gammon’s Forum is for the Arduino Uno, which uses the ATmega328 microcontroller. The A-Star 32U4 Robot Controller uses the ATmega32U4 microcontroller, so you should look at the ATmega32U4 datasheet. That datasheet can be found under the “Resources” tab on the A-Star 32U4 Robot Controller SV’s product page.

Since you are planning to use two interrupts for each encoder, it would be easier to use a different encoder library (like this encoder library) instead of the Zumo 32U4 encoder library, which would require you to make modifications to the code.

- Amanda

Thank you Amanda, again very useful answer!

I found what I was looking for in the ATmega31U4 datasheet. I finally went ahead making my own simple encoders library. As with the Zumo encoder library, I am using the Pololu FastGPIO library for performance. Everything works fine and I am now working on a python Odometer class to run on the Raspberry Pi using the encoder data received from the A Star microcontroller.

1 Like

I finally got my encoders with both pins A & B working properly. I ended up using pins 8 & 16 (PCINT4 & PCINT2) for the left encoders A & B pins and pins 7 and 0 (INT6 & INT2) for the right encoder A & B pins. Setup code below (may be useful to someone else.)

#include <FastGPIO.h>

// Encoders variables
const byte encoderLeftPinA = 8;    // PCINT4
const byte encoderLeftPinB = 16;   // PCINT2
const byte encoderRightPinA = 7;   // INT6
const byte encoderRightPinB = 0;   // INT2

static volatile bool lastLeftA;
static volatile bool lastLeftB;
static volatile bool lastRightA;
static volatile bool lastRightB;

volatile int16_t encoderLeftCount;
volatile int16_t encoderRightCount;

// ISRs
// ISR for left encoder pins A & B (PCINT4, 2)
ISR(PCINT0_vect)
{
  bool newLeftA = FastGPIO::Pin<encoderLeftPinA>::isInputHigh();
  bool newLeftB = FastGPIO::Pin<encoderLeftPinB>::isInputHigh();

  encoderLeftCount += (newLeftA ^ lastLeftB) - (lastLeftA ^ newLeftB);

  lastLeftA = newLeftA;
  lastLeftB = newLeftB;
}

// ISR type function for right encoder pins A & B (INT6 & INT2)
static void rightISR()
{
  bool newRightA = FastGPIO::Pin<encoderRightPinA>::isInputHigh();
  bool newRightB = FastGPIO::Pin<encoderRightPinB>::isInputHigh();

  encoderRightCount += (newRightA ^ lastRightB) - (lastRightA ^ newRightB);

  lastRightA = newRightA;
  lastRightB = newRightB;
}

void setup()
{  
  // Setup encoders pins and attach ISRs
  FastGPIO::Pin<encoderLeftPinA>::setInputPulledUp();
  FastGPIO::Pin<encoderLeftPinB>::setInputPulledUp();
  FastGPIO::Pin<encoderRightPinA>::setInputPulledUp();
  FastGPIO::Pin<encoderRightPinB>::setInputPulledUp();
  PCMSK0 |= bit(PCINT4);
  PCMSK0 |= bit(PCINT2);
  PCIFR = (1 << PCIF0);
  PCICR = (1 << PCIE0);
  attachInterrupt(digitalPinToInterrupt(encoderRightPinA), rightISR,     CHANGE);
  attachInterrupt(digitalPinToInterrupt(encoderRightPinB), rightISR, CHANGE);

  // Initialize encoders variables
  lastLeftA = FastGPIO::Pin<encoderLeftPinA>::isInputHigh();
  lastLeftB = FastGPIO::Pin<encoderLeftPinB>::isInputHigh();
  encoderLeftCount = 0;
  lastRightA = FastGPIO::Pin<encoderRightPinA>::isInputHigh();
  lastRightB = FastGPIO::Pin<encoderRightPinB>::isInputHigh();
  encoderRightCount = 0;
}

The encoderLeftCount and encoderRightCount variables are transferred to the Raspberry Pi via the slave buffer in the main loop. I also have a boolean in the buffer that the Pi can write True to reset the encoder count variables.

1 Like

I am glad you got both encoders to work in your setup; thank you for letting us know and for sharing your code! I’m sure other will find your code quite useful for their own projects.

It sounds like you are making considerable progress with your RPB-202 robot. Keep up the good work!

- Amanda

Thanks Amanda!

I’m quite satisfied with the progress so far and I will post an update in the “share your project” section shortly.

I still have a small issue with the encoders where I noticed that when I increase the frequency at which I read the I2C struct data from the Raspberry Pi (say from 20/s to 50/s), I seem to be loosing some counts, i.e. the robot moves faster for the same speed command (the motor speeds are regulated with PID controllers using feedback from the encoders) and farther for the same number of counts.

I believe it has to do with the fact that, to avoid overflow of the encoders count variables on the A-Star, I reset these values every time the python code on the Raspberry Pi reads the encoder counts from the I2C struct data.

I plan on trying a few things to identify the cause of the issue:
1 - Remove the encoder reset (not a solution but can help to see if this is the cause)
2 - Disable interrupts around the portion of the A-Star code were I write the encoder count variables to the slave buffer and where I reset the encoder counts.

Any other idea? Thanks!

Below is the method of my Encoders class that reads the encoders (python, Raspberry Pi end):

def readCounts(self):
    countLeft, countRight = self.aStar.read_encoders()
    self.aStar.reset_encoders()
    self.countLeft += self.countSignLeft * countLeft
    self.countRight += self.countSignRight * countRight
    return self.countLeft, self.countRight

and the corresponding function in the AStar class (python, Raspberry Pi end):

def read_encoders(self):
return self.read_unpack(25, 4, "hh")

def reset_encoders(self):
  self.write_pack(24, 'B', 1)

Below is the portion of the A-Star code where the encoder values are written to the struct and then reset:

// Read encoders
slave.buffer.encoderLCount = encoderLeftCount;
slave.buffer.encoderRCount = encoderRightCount;
if (slave.buffer.resetEncoders)
{
  slave.buffer.resetEncoders = 0;
  encoderLeftCount = 0;
  encoderRightCount = 0;
}

Full code is available at https://github.com/DrGFreeman/RasPiBot202

Hello.

I agree that the inaccuracy you are seeing is probably caused by this resetting of the encoder counts you are doing. Any counts that happen between the call to read_encoders and the call to reset_encoders will be lost. When you tried to read data from the A-Star more frequently, the percentage of time that your program spends between those two calls went up, so you ended up discarding more encoder counts.

The way I like to do it is to not reset the encoder counts on the A-Star and instead just let them overflow. If you want your Python program to have encoder count variables that do not overflow, then you could record the last count you read from each encoder. When you read a new count, you would compare it to the last count to see whether the counts increased or decreased, accounting for overflow. Then you would add or subtract the appropriate number of counts from a variable in your Python program (which would be a normal Python integer that does not overflow). The code for one encoder might look something like:

diffLeft = (countLeft - self.lastCountLeft) % 0x10000
if diffLeft >= 0x8000:
  diffLeft -= 0x10000
self.totalCountLeft += diffLeft
self.lastCountLeft = countLeft

If you implement this, it would be a good idea to test it by driving the robot far enough so that the A-Star’s encoder counts actually do overflow (which should happen after about 10 meters).

–David

Great! Thanks David!

I did not think I could just let the encoder counts on the A-Star overflow.

I am not very fluent with hexadecimal numbers so it took me a moment to figure out your code example but it is very clear now. I’ll try it shortly.

Your response is very helpful and instructive at the same time. And I am starting to learn the power and simplicity of the modulo operator. I see it could simplify some of my odometer functions dealing with angles that loop from 359 to 0 degrees.

Would you also recommend disabling interrupts with cli() and sei() around the A-Start code lines reading the count variables? I noticed the Zumo encoders library does this.

Finally, as a curiosity, what are typical loop frequencies used for robots with encoders like the Zumo? Faster is better for responsiveness to inputs and movement accuracy but on the other hand if the number of counts per time step becomes too small, this can create scatter in speed and derivative calculations used by the PID controlling the motors. So far I was running my vision based line following at around 20-30 fps to match the rate at which I could capture and process frames from the camera but am considering accelerating it for other usages.

Thanks again.

I just implemented and tested the changes and it works to perfection.

I ran forward and backward until the encoder counts exceeded 2**16 without any issues.

I ran forward tests of 2500 mm target distance with poll frequencies from 20 to 100 Hz and got repeatable distances within about 12 mm or ~1/2% accuracy which I consider very good.

I kept the method to reset the encoders on the A-Star which I call in the Encoder class constructor to reset the values between different runs of the python code on the Raspberry Pi.

Again, many thanks @AmandaS and @DavidEGrayson for all the help! You not only help us resolve issues, you help us learn a lot along the way!

1 Like

I am glad that your robot is working better now.

Yes. The encoder count variables used by the ISRs have two bytes (they are 16-bit), but the AVR can only read one byte at a time from RAM. If you don’t disable interrupts, it is possible that you would get an invalid value for one of those variables because the interrupt could modify it after you read the first byte and before you read the second byte.

I do not have any specific numbers to give you. You might consider reading about the robots we had at our last dead reckoning competition to see how they worked.

–David

1 Like