Let's Make Robots!

Is an Arduino Capable of Reading All Four Rover 5 Quadrature Encoders?

Note: Sorry this post is so long. I used bold text to indicate the main point of the post.

I didn't want to hijack Oddbot's Rover 5 threadso I'm starting this one here in the forum.

In replying to odong's question about the quadrature encoders I stated (among other things):

"From what I've read, I think most Arduinos aren't fast enough to capture all the quadrature encoder interrupts and still have time to perform other tasks. I believe reading all four quadrature encoders requires a fast microcontroller. I used a Propeller in both of my Rover 5 projects."

I'm glad I used "From what I've read" and "I believe" to indicate my uncertainty about the topic because bdk6 soon corrected me with:

"The arduino running at 16 MHz (the normal speed) processes close to 16 million instructions per second.  At 180 RPM on each wheel, using all four encoders, and the motors turning 180 RPM, there would be a total of right at 1000 interrupts per second.  With each interrupt taking about 20 instruction cycles, that would be 20000 out of 16000000 each second, or 00.125% of the total processing cycles available.  That amount is negligible.  Even horrible code that took 100 times longer would still leave over 87% of the processing cycles for other things."

This is very encouraging for anyone wanting to control the individual motors of a 4WD Rover 5 with an Arduino. What I don't understand is where are the four quadrature reading Arduino controlled Rover 5's?

I know of two examples of people stating all the interrupts were causing problems for their Arduinos.

CliffO improved my technique of mounting Vex Mecanum wheels to his Rover 5 and mentions his frustrations of having the interrupts overwhelming the Arduino.

amano001 mentioned in my thread over at the Parallax forums:

"we were looking into using one of the propeller proto boards instead of the arduino mega as we have seen the code response get very sluggish with interrupts going off all the time for the encoders."

I also recall Oddbot mentioning the need of a fast microcontroller (I think he was suggesting one of the faster Arduinos) to read four quadrature encoders. Sorry, I couldn't find a link.

There are a lot of Rover 5 chassis on LMR. I'm pretty sure I've looked at all the robot projects on LMR using the Rover 5 and I haven't found any (other than mine) using four quadrature encoders.

Most of the builders/programers of the Rover 5 projects on LMR state they are new to programming and robotics. Is this why there aren't any Arduino controlled Rover 5's using all four quadrature encoders? (I don't recall seeing any using two quadrature encoders.)

amano001 who I previously mentioned ended up using a Propeller QuickStart board as a slave to the Arduino. The Arduino would send direction and speed commands to the Propeller and the Propeller (using my code) would drive the motors while monitoring the encoders to produce the desired movements.

Is a 16MHz Arduino really capable of monitoring four quadrature encoders (with time left over for other tasks)? I'd really like to know.

Just yesterday I started designing a shield with a Propeller. My intention is to make it easier for others to use the Propeller as a slave to an Arduino. The Arduino would send speed and direction commands to the Propeller. The Propeller would then drive the motors appropriately based on a PID algorithm using feedback from the encoders.

If an Arduino can do this on its own, then the Propeller shield I plan to make wouldn't be needed.

Here are some numbers to help any interested parties to aid me in my decision to continue to develop a Propeller shield.

In my tests the max rotation speed of a Rover 5 wheel is 90 rpm. This produces 500 encoder transitions per second per wheel. All four wheels would generate 2000 transitions per second at 90 rpm. (This was a free spinning wheel. The speed will likely be lower when used in contact with the ground.)

Besides simple odometry (how far has the wheel traveled), I think it's very useful to also compute the speed of each wheel. The speed of each wheel should probably be computed at about 50Hz.

Are these interrupt rates and computation requirements within an Arduino's capability?

I have plenty of other projects I'd like to work on if a quadrature encoder reading Propeller shield isn't needed for an Arduino controlled 4WD Rover 5. I presently do not have enough experience with the Arduino to know if the proposed shield is needed. I'd really apperciate information about the Arduino's ability to monitor four quadrature encoders unassisted.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

I just found out that the Teensy 3.1 (Arduino work-a-like with an ARM M4 in about the same size of an Arduino nano) has hardware that decodes two quad encoders. I haven't had a chance to check it out yet.

Yes, one of my tivas has this (only 1 though not 2, does however support full quadrature + an indexing pulse), the other is actually an older stellaris not a tiva so lacks it, however I think the tiva replacement has 2.

Also yet to play with it. Looks like it would be a damn interesting module to play with though. Best  I had thought up for a pure interrupt based alternative would be to wire encoder channel A to 2 pins to catch both the rising and falling interrupt, same for B. No real point though. Overcomplicates for no real reason.

I think that everything but the propellor and low end PICs have more interrupts than the Uno and company.

Besides everybody seems to use delay() and the interrupts might mess with that. :)

What it needs is a new bootloader that would allow the Arduino's to have some sort of round robin muti-taking based on delay nd a switch at the end of a loop and using multiple loop statements loop, loop1, loop2...

Actually it's probably simple minded because there would need to be separate stacks and frames and I'm not sure the memory could hold it. It was a nice dream for a few seconds.

I didn't think the Arduino Uno had enough interrupt pins for four encoders. The Mega has a lot more encoder pins, as do some ARM based Arduino work-a-likes. I'm a bid fan of lots of interrupt pins becuase it gives you a lot more options.

Msp430 chips also have external interrupts on ports 1 and 2 for a total of 16 interrupts on devices possessing a full size port 1 and 2.

 

Lots of interrupts is certainly a good thing.

 

Did have some code I was working on for the tiva c launchpads from Texas instruments (programmed in arduino from the energia IDE) using 2 seperate interrupts for taking RC receiver input asynchronously rather than using pulseIn.

Of course on Duane's preferred platform (propeller) he could just dedicate a cog to that.

OddBot, were your comments directed at my code?

I am using port manipulation and (myDitigalRead does that, but allows me to compile the same code for multiple Arduino boards).

And, if I didn't allow for the very first interrupt, the code would be smaller (without checking the current PWM), but it would be off by one encoder tick 50% of the time for the first cycle, and when there are normally about 7 or 8 encoder ticks in that first cycle, it can be off by 15% which could mean a large difference in direction angle.

And, as I wrote before when I had the encoders in separate ISR routines I lost encoder ticks fairly regularly. Which is why all 4 interrupts go through the same ISR.

And it does work, very reliably. And it is fast, when I've added variables at the start and end of the function to capture the time difference and accumulate over a long period, the actual amount of time in this ISR is less than the amount of time in separate ISRs for each wheel. At first I thought it was because of overhead - calling the function - but since I was keeping track of the time between the start and end within the function, this was not logical.  I think, now, that it's because by doing port manipulation (myDigitalRead which is plagerised from a couple of different Fast IO ideas I'd read about), and that I have put the encoder pins "next to each other" on the Arduino, and hence on the same port, that there's less done "in the background" to get the values for the encoders - ie. the same port is used for all 4 reads, so the compiler has not had to put in code to get different variables for the 4 "reads".

Now, it might be faster if I had not used constants and myDigitalRead, but rather put the actual b_left_a = PORTD & _BV(3); or whatever it worked out to be for that bit, but my code can, as I've already said, be compiled and used on any of my Arduinos (Mega, Leonardo or Uno).

My initial code was much simpler, because I did not have both wheels in the same ISR, and I did not look at the PWM, I just relied on the "a" reading and the previous "b" reading to get the direction. However, as stated in the other posts by other people, this seems to lose encoder counts. I can only imagine that somehow, when in the ISR the interrupt for the other wheel is ignored, OR possibly A changes, the ISR is started and before it ends B changes, and the ISR is not called again. But I don't understand it well enough to know why, only that this does work.

Yes OddBot I've seen your code and have tried to modify it for 4 encoders and an ATmega1280. But I got a bit lost in the Atmel datasheet. Must give it another try :)

The answer is Yes, the Arduino can easily read all 4 encoders but I doubt it could with your code. Try using port manipulation and a timer interrupt. Your code won't be such a ghastly nightmare, it will be a lot smaller and it will run faster. Have a look at my sample code here: http://letsmakerobots.com/content/smart-motor-driver-reads-encoders-control-speed-and-measure-distance It was written for a 2 motor Rover 5 but by using a full port instead of only half a port and a few other modifications it will read all 4 encoders.

Very interesting Andrew, are you willing to share your ISR code? I like to use that method on my Rover 5 with mecanum wheels, it's controlled by an ATmega1280.

Assuming you were responding to my post, here's my encoder code:

  void f_encdr_interrupt()

  {

    #ifdef INCLUDE_ENCODERS

      static byte b_prev_left_a = 255 ,b_prev_left_b = 255 ,b_prev_right_a = 255 ,b_prev_right_b = 255 ;

      byte b_left_a  = myDigitalRead( cb_pin_encdr_left_a ) ;

      byte b_left_b  = myDigitalRead( cb_pin_encdr_left_b ) ;

      byte b_right_a = myDigitalRead( cb_pin_encdr_right_a ) ;

      byte b_right_b = myDigitalRead( cb_pin_encdr_right_b ) ;

      while( b_left_a != b_prev_left_a || b_left_b != b_prev_left_b || b_right_a != b_prev_right_a || b_right_b != b_prev_right_b )  // any change

      {

        if( b_left_a != b_prev_left_a || b_left_b != b_prev_left_b )  // left change

        {

          if( b_prev_left_b == 255 )  // this is the very first encoder count for this side

          {

            if(      ia_motor_PWM[ cb_motor_left ] > 0 ) ia_encdr_count[ cb_motor_left ] ++ ;  // PWM + assume forward

            else if( ia_motor_PWM[ cb_motor_left ] < 0 ) ia_encdr_count[ cb_motor_left ] -- ;  // PWM - assume backward

          }

          else

          {

            if( b_left_a != b_prev_left_b ) ia_encdr_count[ cb_motor_left ] ++ ;  // when A != prevB its forward

            else                            ia_encdr_count[ cb_motor_left ] -- ;  // when A == prevB its backward

          }

        }

        if( b_right_a != b_prev_right_a || b_right_b != b_prev_right_b )  // right change

        {

          if( b_prev_right_b == 255 )  // this is the very first encoder count for this side

          {

            if(      ia_motor_PWM[ cb_motor_right ] > 0 ) ia_encdr_count[ cb_motor_right ] ++ ;  // PWM + assume forward

            else if( ia_motor_PWM[ cb_motor_right ] < 0 ) ia_encdr_count[ cb_motor_right ] -- ;  // PWM - assume backward

          }

          else

          {

            if( b_right_a != b_prev_right_b ) ia_encdr_count[ cb_motor_right ] ++ ;  // when A != prevB its forward

            else                              ia_encdr_count[ cb_motor_right ] -- ;  // when A == prevB its backward

          }

        }

        b_prev_left_a  = b_left_a ;  // to keep in the loop if it changes

        b_prev_left_b  = b_left_b ;  // to keep in the loop if it changes and determine direction

        b_prev_right_a = b_right_a ; // to keep in the loop if it changes

        b_prev_right_b = b_right_b ; // to keep in the loop if it changes and determine direction

        b_left_a  = myDigitalRead( cb_pin_encdr_left_a ) ;  // new readings in case they changed while we were doing the if statements above

        b_left_b  = myDigitalRead( cb_pin_encdr_left_b ) ;

        b_right_a = myDigitalRead( cb_pin_encdr_right_a ) ;

        b_right_b = myDigitalRead( cb_pin_encdr_right_b ) ;

      }  //   while  we stay in the loop until no changes (when in an interrupt, any other interrupts are "ignored", which is why both sides are in the same interrupt routine)

    #endif  //  #ifdef INCLUDE_ENCODERS

  }  //  f_encdr_interrupt

</pre>
(I can't find a way to insert as "code", so the formatting isn't great, sorry)
Notes:
1. INCLUDE_ENCODERS is defined for "normal" operations, but when I'm debugging I comment out the define so that I can "fake" the encoder counts.
2. myDigitalRead is a function I've written that does "fast IO", but plain old digitalRead will work just as well
3. ia_encdr_count[ x ] is an integer array that holds the total encoder count per "loop" cycle for (in my case) left and right
3. ia_motor_PWM[ x ] is an integer array that holds the value of the current PWM of the motor (left or right)
4. the very first time this is called, it doesn't know the previous B value, so it uses the PWM to determine the direction - this is only one encoder "tick" but I can be a bit pedantic in my programming
5. I use _ instead of capitals in my variable and function names, it's an old habit that I still like, but you should be able to figure out what each variable represents. (b_ is a byte, i_ is an int, cb_ is a const byte, ia_ is an int array
6. All 4 encoders are attached to this interrupt routine in the setup function (this is not EXACTLY my code, I use a function to get the interrupt number)
      myPinMode( cb_pin_encdr_left_a  ,INPUT ) ;
      myPinMode( cb_pin_encdr_left_b  ,INPUT ) ;
      myPinMode( cb_pin_encdr_right_a ,INPUT ) ;
      myPinMode( cb_pin_encdr_right_b ,INPUT ) ;
      attachInterrupt( 0 ,f_encdr_interrupt ,CHANGE ) ;
      attachInterrupt( 1 ,f_encdr_interrupt ,CHANGE ) ;
      attachInterrupt( 2 ,f_encdr_interrupt ,CHANGE ) ;
      attachInterrupt( 3 ,f_encdr_interrupt ,CHANGE ) ;