Friday, October 10, 2014

More About Odometry

A few weeks after I had the odometry working, I found that the robot started veering off course, and it wouldn't follow waypoints as accurately. I was adding sonar sensors at the time and was worried I was getting noise from all the additional wiring. So I researched a little more and experimented with a pull-up resistor instead of a pull-down.

The old wiring:


With the pull-down resistor, the analog reading on pin A0 ranged from about 30 (black stripe) to about 150 (white stripe), enough to distinguish, but not high enough to use a digital input.


The new wiring:

With this setup the voltage range is a lot wider, about 700–800 at the high end (black stripe) to about 50–150 at the low end (white stripe). I played around with resistor values rather than finding an optimum value analytically. Note that the sense is now reversed: a high voltage indicates a black stripe, because the phototransistor has a low current and low voltage drop.

I also wrote a program to capture voltage values over a test period to see if I could see noise in the data. Because the Arduino has so little RAM, I captured data values as bytes, the actual value divided by 4 so that leftmost 8 bits of the 10-bit analog value could be stored. I used a 1500-byte circular buffer and my AnalogScanner library (https://github.com/merose/AnalogScanner) to capture values using the ADC interrupt, rather than delaying and using analogRead(). I scanned all 6 analog pins to slow down the rate at which A0 was read. This gave a scan interval for A0 of 723μs, or 1383 reads/second, so the entire buffer holds about 1.08 seconds worth of data.

The encoders for the left and right wheels were wired to A0 and A1, respectively. Turning the wheels as fast as possible, I got these results, which are surprisingly noise-free. I've shown both the raw values and a smoothed value using a digital decay filter: new_value = 0.75*old_value + 0.25*new_reading




It's interesting how distinguishable the black stripes are. See, for example, the moderately higher value in the 2rd peak from the left on the right encoder and the peak just before 750ms. Those are exactly 15 peaks apart, so they are the same black stripe. Originally I had printed the encoder disks on plain paper (recycled 20lb) in draft mode. The variance in the detected signal caused me to switch to higher quality paper and photo-quality print output.

The results were good enough to see the problem: a few stripes on the left wheel at around 750ms stand out as having too high a value, especially for the white stripes. Looking at the wheels more carefully I saw that the striped disk was puckered up a little in one place. Problem found! I was also using the 40% and 60% points of the total smoothed range to detect when we crossed a stripe boundary. Based on this data it looks like using the mean value is safer for both white-to-black and black-to-white crossings. The signal may even be clean enough to use a digital input and an interrupt on value change, but I'm worried about slower speeds causing spurious transitions because of fluctuating readings on a stripe boundary. I may test that later, but for now I'll stick with analog input and use software to find the boundaries.

After switching to higher quality paper (matte 44lb presentation paper and best-quality printing), I got these results:



Here you'll notice that one white stripe (valley) on each disk is narrower than the rest, and one black stripe (peak) is wider, the 2nd valley in the left plot and the valley about 500ms in the right plot. I used a marker to widen one of the stripes so if there were variations in the readings I could count forward to look at the stripe in more detail. The lower peak value for the wide black stripe on the right wheel might be because the marker ink may be a little more reflective than the inkjet printing. The presentation paper is not as white as plain paper, and may be more reflective when black, which might explain the narrower input value range. I could probably modify the pull-up value to compensate. Incidentally, I tested a photograph with a glossy finish, too. It was so reflective that dark and light colors were indistinguishable.

Based on this data it looks like the smoothing is working well and the stripes are uniform enough to ensure better odometry. Sure enough, the robot again follows waypoints well. I'm curious to see if I can increase the number of stripes and keep a good signal. Each stripe right now represents about 7mm of forward travel. I don't yet know enough about the wheel slippage to know if narrower stripes would make any difference in the overall navigation. An experiment for later.

The Code

The code, for those who are interested, is below. It uses my AnalogScanner library (https://github.com/merose/AnalogScanner) and the Servo library. The smoothing is not done in the code below, it capture raw analog values only. The smoothing was added after pasting the output into a Google Spreadsheet. Notice that the two columns of output are separated by a tab (\t). That causes Google Spreadsheet (and maybe Excel?) to put the values in adjacent cells when pasted in as text. Spaces don't work that way. Good to know if you want to take Arduino serial output and paste into a spreadsheet.

The scan rate is very uniform, by the way. I got exactly 2767 scans on the desired pin each time I ran this code. I'm spinning both motors in the code, probably not necessary. I wanted to maximize any possible motor noise affecting the analog read levels.

#include <Servo.h>
#include <AnalogScanner.h>

// Creates an instance of the analog pin scanner.
AnalogScanner scanner;

Servo leftMotor;
Servo rightMotor;

// Change this to capture data from a different pin.
const int PLOT_PIN = A0;

const int PLOT_POINTS = 1500;

// These are volatile since they will be read and written in
// an interrupt service routine as well as the main code.
volatile unsigned char plotData[PLOT_POINTS];
volatile int curPoint = 0;
volatile unsigned long totalScans = 0;
volatile int savingPlotData = 0;

// The order in which we should scan the analog pins.
int scanOrder[] = {A0, A1, A2, A3, A4, A5};
const int SCAN_COUNT = sizeof(scanOrder) / sizeof(scanOrder[0]);

void setup() {
  Serial.begin(38400);

  // Sets the scan order to A0 through A5, in turn, and begins reading
  // analog values.
  scanner.setScanOrder(SCAN_COUNT, scanOrder);
  scanner.setCallback(PLOT_PIN, myCallback);
  scanner.beginScanning();
  
  // Spin up the motors and wait a little bit to allow them
  // to settle in to a steady speed.
  leftMotor.attach(A4);
  rightMotor.attach(A5);
  leftMotor.write(180);
  rightMotor.write(0);
  delay(3000);
  
  // Start saving analog input data for plotting. Only the
  // last 1500 values are saved, since a circular buffer is
  // used, so waiting 2 seconds is more than enough time.
  savingPlotData = 1;
  delay(2000);
  
  // Stop saving data and turn off the motors.
  savingPlotData = 0;
  delay(100);
  leftMotor.detach();
  rightMotor.detach();
  
  Serial.print("Total scans: ");
  Serial.println(totalScans);
  
  for (int i=0; i < PLOT_POINTS; ++i) {
    Serial.print(i);
    Serial.print("\t");
    Serial.print(plotData[(curPoint + i) % PLOT_POINTS] << 2);
    Serial.println();
  }
}

void myCallback(int index, int pin, int value) {
  if (savingPlotData) {
    plotData[curPoint] = value >> 2;
    curPoint = (curPoint + 1) % PLOT_POINTS;
    ++totalScans;
  }
}

void loop() {
  // do nothing
}

No comments:

Post a Comment