Saturday, 26 May 2012

Some Code And Lots Of Graphs

Over the past few days I've been experimenting with ways to improve my new home made quadrature encoder. By the end of my previous post I was successfully taking readings and converting them into rpm, which were then printed out to the serial monitor and finally graphed in excel. However my initial results were fairly noisy as you can see from this graph:

Initial spikey encoder results - rpm plotted against time
In theory that's the rpm of the wheel over time, however the wheel is turning at a roughly constant velocity so in a perfect world the graph would be a nice straight line. Now that's not necessarily going to be achievable (especially with my fairly rugged home made solution), however it's worth having a go at improving my results. Be forewarned: this post is gonna be very graphy and probably close to none-understandable, especially if you didn't catch the last one - read on if you dare!

The first thing I do is sit down and think of what might be causing these inaccuracies. I'm not convinced it's a sensor issue, and the Arduino is easily fast enough to keep up. After some pondering, the first few potential culprits I come up with are:
  • If the sensors are not out of phase by exactly 1/4 of a wave length then I'd expect readings to alternate between slightly too fast, then slightly too slow every interrupt. They should however average out, making every 2nd interrupt (half a wave) or 4th interrupt (a whole wave) right on the money. Note: not worked out which one yet, but the key is, it'd be very very regular :).
  • The wheel is a little wobbly, which means it's getting closer to and farther from the sensors each revolution. If the sensors are perfectly aligned this wouldn't be a problem, however if they were at a slight angle it could turn the square wave into a slightly sin-waveish thing. I'd expect to see fairly regular inaccuracies in a pattern that repeats once per revolution (every 32 interrupts) if this was the issue.
  • It could be the time difference between when my interrupt triggers to tick the encoder counter, and when I actually do my RPM calculations (every ms) in the main loop. If this were the issue I'd expect to see fairly random inaccuracies within a threshold of roughly 5ms (the time it takes for a single encoder tick at 360rpm)
To track down the culprit, I print out rpm calculations every 50ms, then every 150ms, then every 250ms, then every 1000ms and plot them against encoder position:

Encoder results at different intervals - still very spikey
Longer gaps will inevitably give flatter lines as they allow more interrupts to pass in between readings, so the data will naturally be smoothed. However the difference in patterns between the graphs should still indicate which of the above problems (if any) are the issue. As you can see, regardless of interval time, we get extremely spikey results with no discernable pattern. Before getting clever it occurs to me that I'm measuring times in milliseconds, but a single encoder tick can take around 5ms - this gives me at best a 20% error margin. I make a very simple change so the code does calculations at a microsecond level of accuracy instead, and look what happens:

Encoder results using microsecond accuracy instead of millsecond
Wow! What a difference. I'm still getting spikey results, but there's clearly a median line for each interval time, and the errors that bounce either side of it are extremely regular.

Now I'm convinced I'm not losing data due to accuracy (as even the errors are predictable!) I switch to running at constant 150ms intervals, printing out the rpm using 4 different calculations:

  • No error compensation
  • Compensation for time difference between interrupt and calculation time
  • Compensation for out of phase sensors
  • Combined both settings above

I make 2 changes to achieve this. First, my interrupt function now records the time at which it took it's last reading:

void updateEncoder()
  encoder_time = micros(); //record time in microseconds of reading
  byte new_encoder = readEncoder();
  int dir = QEM[current_encoder*4+new_encoder];
  encoder_pos += dir;
  current_encoder = new_encoder;

Then I update the loop function as follows:

  //record current time, the current encoder position, and the time the last encoder reading occured
  long new_time         = micros();
  long new_encoder_pos  = encoder_pos;
  long new_encoder_time = encoder_time;
  //calculate rpm with no compensation
  current_rpm_nocomp    = calcrpm(last_time, new_time, last_encoder_pos, new_encoder_pos);
  //calculate rpm using time compensation (i.e. we use the last encoder time rather than current time in calculations)
  current_rpm_timecomp  = calcrpm(last_encoder_time, new_encoder_time, last_encoder_pos, new_encoder_pos);
  //calculate rpm, only updating if it's an even numbered reading
  current_rpm_evencomp  = (new_encoder_pos & 1) ? current_rpm_evencomp : calcrpm(last_time, new_time, last_encoder_pos, new_encoder_pos);
  //calculate rpm if even numbered reading, using time compensation
  current_rpm_allcomp   = (new_encoder_pos & 1) ? current_rpm_allcomp : calcrpm(last_encoder_time, new_encoder_time, last_encoder_pos, new_encoder_pos);
  //record last readings to use in next calculations
  last_time             = new_time;
  last_encoder_pos      = new_encoder_pos;
  last_encoder_time     = new_encoder_time;

Printing out the results and plotting against time, I now get the following graph:

Different approaches to error compensation for the encoder
The purple line contains the fully compensated data and look how flat it is! I decide that given this is a prototype (in MmBot V2 I'll use motors with built in encoders) this is good enough for now. So, as one final step, I attach the encoder to the inside of MmBot as you can see here:

Black/white wheel disk on inside of wheel, and 2 IR reflectivity
sensors facing it to make a quadrature encoder
And with it attached, I plot encoder position against time, with the motor turning on/off at regular intervals:

Encoder position plotted against time (in microseconds)
with motor turning on/off at regular intervals
Pretty slick!

That's it for now - next up I'll properly attach both encoders, connect them to the Arduino Mega inside MmBot, wire up my Sabre Tooth motor controller and get some real navigation going!

No comments:

Post a Comment