0

(EDIT: Added TMR0 & TMR1 set-up code)

I have a small robot vehicle controlled by a PIC18f4585. It has two wheels (ball bearing on the front) as well as two rotary encoders. The following function is supposed to travel a specified distance in inches perfectly straight. It travels straight enough, the problem is the distance. the travelled distance has an error of up to 2cm. I was just wondering whether anyone else could see an error I have missed.

Additional info: The PIC is clocked at 40MHz. I am counting the encoder ticks using TMR0 and TMR1 in 8-bit mode.

Any help/suggestions would be greatly appreciated, thanks.

TMR CONFIG:

TRISAbits.TRISA4 = IO_INPUT; 
    OpenTimer0( TIMER_INT_OFF &
            T0_8BIT &
            T0_SOURCE_EXT &
            T0_EDGE_RISE &
            T0_PS_1_1   );

TRISCbits.TRISC0 = IO_INPUT;
OpenTimer1( TIMER_INT_OFF &
        T1_8BIT_RW &
        T1_SOURCE_EXT &
        T1_PS_1_1 & 
        T1_OSC1EN_OFF &
        T1_SYNC_EXT_ON  );

MOVE STRAIGHT FUNCTION:

void moveForwardDistance(double distance, int power)
{
int tickGoal = distance * 5.13348;      //This seems to get ~the correct distance

int masterPower = power;
int slavePower = power;
int error = 0;
int kp = 50;            //error factor
int totalTicks = 0;     //used to keep track of total ticks

resetEncoder();

while(totalTicks < tickGoal)
{
    SetRightMotor(masterPower);
    SetLeftMotor(slavePower);

    error = TMR1L - TMR0L;
    slavePower = power + (error / kp);

    resetEncoder();
    DelayMs(100);
    totalTicks += TMR1L;
}
SetRightMotor(0);
SetLeftMotor(0);

DelayS(1);
}
chib
  • 131
  • 3
  • 1
    What's the problem? Clearly, one "tick" represents 1/5.13348 of whatever unit you're using to measure "distance". – Dave Tweed Mar 17 '13 at 03:35
  • How far are you trying to move? What percent of the total distance is the 2cm? When driving straight how much does it 'wiggle' back and forth? Veering left and right would cause it to read the correct number of ticks but not all that movement would be forward. – Gorloth Mar 17 '13 at 03:44
  • The distance is measured in inches. I played with the value (5.13348) until I got it travelling the ~correct distance. The confusing part is that the actual circumference of the wheel is 2.6" which at 64 ticks rotation should work out to be a different value. The distances I tested were 5 and 10 inches (10 times each to get the avg). When it travels straight there is no visible "wiggle". – chib Mar 17 '13 at 04:23
  • Take data over multiple trials of distances, both short, and long. Is the error random? Constant? Proportional to distance? The answer to that will point towards the type of cause. Also, can your system miss a tick if it comes in as your are resetting? How many ticks on average per cycle of the loop? Are you properly decoding a quadrature encoder, or just a single channel which cannot determine direction of rotation? – Chris Stratton Mar 17 '13 at 04:30
  • The encoders are working in a mode where they output ticks through one channel and direction through the other, I am only using the ticks channel. They can work in quadrature mode but I was unsure how to count using that mode with the timers available. I'm not sure whether or not I could be missing a tick after resetting, how would I go about testing this? As for the error, it seems to be fairly sporadic, sometimes 3 or more runs in a row will be the same then the next will be off my a few cm (higher or lower). – chib Mar 17 '13 at 04:54
  • Shouldn't tickgoal be a double?? Probably technically better to avoid floats altogether,and multiply up to use large integers. – Scott Seidman Mar 17 '13 at 12:11
  • I may be missing something here, but are you resetting TMR0L and TMR1L each time you read them? If not, you're accumulating residual counts from the previous reading on each iteration of your loop. Also, those registers are only 8 bits; how are you accounting for when they roll over? – Dave Tweed Mar 17 '13 at 13:28
  • The function resetEncoder() resets both TMR0L and TMR1L. I didn't think it was necessary to use 16-bit counters as they should never reach 256 ticks as they are cleared every 100ms. I will play with the data types to see whether that has any effect Scott. Maybe I should move the resetEncoder() call to directly before salvePower is calculated, I'll give it a try. – chib Mar 17 '13 at 14:09
  • 1
    Seriously?!? You reset the timers *and then* read TMR1L (100 ms later) in order to accumulate the distance? How's that supposed to work? That'll give you a speed estimate, but will totally ruin any sort of odometry. – Dave Tweed Mar 17 '13 at 15:38
  • I was following this example : http://goo.gl/6KMst What order would make more sense? – chib Mar 17 '13 at 16:24
  • Dave makes a good point - although most of the loop time is probably in between the reset and the measurement, it is an opportunity to loose counts. And ignoring the direction is an opportunity to accumulate false counts when oscillating across the encoder edge at start/stop or when not moving. – Chris Stratton Mar 17 '13 at 21:33

2 Answers2

1

Sounds like you are missing encoder ticks when your CPU is busy with other tasks.

Your Robot is executing code in a loop and the loop times are approximately constant so you are missing the same number of ticks (roughly) in each loop. You've calibrated this error out of your system.

However, occasionally, you get unlucky and the loop runs longer and you miss more ticks than average.

It is impossible to decode/debug/double-check your firmware with the snippet provided since the register-level configuration of the hardware input capture unit is omitted. I'd look there.

DrFriedParts
  • 12,482
  • 36
  • 54
  • I added the config code for the timers to the post. What would be causing the loop to run longer, the calculation for slavePower? Thanks for all the help guys, I'm determined to at least reduce the error a bit. – chib Mar 17 '13 at 14:42
  • What encoders are you using? Where is the rest of the timer management code? Where is the resetEncoder code? You totalTicks accumulator seems to have a logic flaw in that the resetEncoder should reset TMR1L no? – DrFriedParts Mar 17 '13 at 19:20
  • I just have that TMR config code. I am using Nubotics WW-11 encoders (http://goo.gl/2kUWJ). I'm using the them in decoded mode, and have the clock line on one going into TMR0 and the other to TMR1. The resetEncoder() function literally just sets each timers registers (high and low) to 0. Would it be better / more accurate to use external interrupts and just have the encoders wired to PORTB? It's important that they be pretty accurate. – chib Mar 17 '13 at 20:38
  • Couple of possible problems: (1) encoder is actually missing ticks, (2) your floating point math is causing variation in your distance target, (3) you need a critical region (disable interrupts) around your timer reads to guarantee atomicity – DrFriedParts Mar 17 '13 at 23:58
0

The example you cited in your comments is just wrong. Sorry, but that happens sometimes.

Conceptually, what you are trying to do is transfer the value from the 8-bit hardware counter to a 16-bit (or more) software counter. The problem with this is that the hardware counter can advance by one tick at any time, including between when you read it and when you reset it.

The example code has many problems with implementing this transfer, and clearing the hardware counter before adding it to the software counter is just the most egregious of them. Reading the hardware counter multiple times is another problem, and allowing an arbitrary amount of time to elapse between reading and clearing is another.

Ideally, you'd like to have zero time between the reading and the clearing, but this is not going to be possible. The next best thing would be to write a function (possibly in assembly language) that reads both counters, resets them in the fewest instructions absolutely possible, and then saves the values read in the first step into variables that the rest of the program can use.

In C, this function would look something like this:

unsigned char tmr0_value;
unsigned char tmr1_value;

void read_and_reset_timers (void)
{
   register unsigned char tmr0_temp;
   register unsigned char tmr1_temp;

   /* Read the two timers into CPU registers and reset them
    * as quickly as absolutely possible.
    */
   tmr0_temp = TMR0L;
   TMR0L = 0;
   tmr1_temp = TMR1L;
   TMR1L = 0;

   /* Now, write the values to global variables.
    * It is less critical how long this takes.
    */
   tmr0_value = tmr0_temp;
   tmr1_value = tmr1_temp;
}

You would then use this in your main loop like this:

void moveForwardDistance (double distance, int power)
{
  int tickGoal = distance * 5.13348;      //This seems to get ~the correct distance

  int masterPower = power;
  int slavePower = power;
  int error = 0;
  int kp = 50;            //error factor
  int totalTicks = 0;     //used to keep track of total ticks

  read_and_reset_timers();

  while (totalTicks < tickGoal) {
    SetRightMotor (masterPower);
    SetLeftMotor (slavePower);

    read_and_reset_timers();
    error = tmr1_value - tmr0_value;
    totalTicks += tmr1_value;
    slavePower = power + (error / kp);

    DelayMs(100);
  }

  SetRightMotor(0);
  SetLeftMotor(0);

  DelayS(1);
}
Dave Tweed
  • 168,369
  • 17
  • 228
  • 393
  • That makes a lot more sense, cheers. I made the changes, but there still seems to be a large degree of unpredictability. At first I thought the problem was completely solved as the robot travelled the exact same distance 10 times in a row, but then it started to either fall short or overshoot again, sometimes as much as an inch. Do you think writing the same read/reset function in assembler would fix it, or do you think there could be another factor at work too? – chib Mar 17 '13 at 22:34
  • How far is the robot moving in 0.1 second? It could be that you simply need better time resolution than that; in other words, reduce the loop delay to something less than 100 ms. If that isn't it, then I would suspect that there's some other factor involved here. – Dave Tweed Mar 18 '13 at 00:01