Pages: [1] 2   Go Down
Author Topic: How to read TTL sensor every exact time interval?  (Read 1750 times)
0 Members and 1 Guest are viewing this topic.
Offline Offline
Newbie
*
Karma: 0
Posts: 20
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Hey everybody. I am working on a robot that has a TTL gyroscope. I have used the code here as a a guide for mine, which is very similar. however, I am running into a problem when I add more code to the project. Since the gyro values have to be integrated over time, there needs to be the time constant (see line 34, gyroRate /=100;) which is to say how often the analog input is read.

The issue I am having is that with the addition of other sensors that I am checking (particularly 3 ultrasonic sensors, of which the time to read varies), I find that along with the 10ms delay, there is also the time that it takes for the other sensors to be checked, causing gyro drift since the time constant is not perfect.

Is there a way that I could force the gyro to be checked at a certain time, all the time, regardless of what else is being executed? I was thinking of possibly using interrupts, but I am not familiar with them, and with this type of sensor, it doesn't really have values that fall into the range of rising, falling, etc.

Thanks
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

You could make a timer generate an interrupt. See here about timers:

http://www.gammon.com.au/forum/?id=11504

And here about interrupts:

http://www.gammon.com.au/interrupts

At the desired interval the interrupt can fire and your interrupt routine can commence an analog conversion. That conversion itself can generate an interrupt when finished (eg. to save the result).  The page above has a section about doing that ("Read the Analog-to-Digital converter asynchronously").
Logged

Offline Offline
Sr. Member
****
Karma: 1
Posts: 314
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I agree with Nick.  One thing you will need to be careful of is some libraries think they are the only ones in the system and will take over timers or disable interrupts.  So you could find yourself carefully setting up you interrupt only to find it disabled by another routine.

On the flip side, remember that while you are in your interrupt handler, no one process can handle theirs.  It can also through off other timing.  That is why it is so important to process and get out of your Interrupt Service Routine ASAP.  Collect your raw data, save it to a global variable, and get out.  Set a  flag that says there is data to process.  Process the data in a less time critical section of code and reset the flag.

I hope this has help and I have not just stating the obvious.

BTW.  PWM outputs are tied to timers.  If you take over a timer, you disable 2 PWM outputs.  So if you are using analogWrite(), be aware.
« Last Edit: May 30, 2012, 10:23:27 am by RandallR » Logged

Offline Offline
Newbie
*
Karma: 0
Posts: 20
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Thanks to both of you. I have timer2 set up with an overflow interrupt, but I originally had it running my whole gyro function within the routine. Now I only have this: ISRvalue is  my global var, gyroPin is a constant for the pin, and canCheck is my global boolean. The global var and boolean should be declared as volatile, right?

Code:
ISR(TIMER2_OVF_vect) { //Timer2 overflow interrupt vector, called every 1 ms
  count++;
  if(count > 99) { //how many ms to wait before doing this(0 based)
    // readHeading();  - don't use this, too much going on...
    ISRvalue = analogRead(gyroPin);
    count = 0;
   canCheck = true;
  }
  TCNT2 = 130; //set to 130 out of 255 (125 cycles)
  TIFR2 = 0x00; //restart timer
};
Logged

Offline Offline
Newbie
*
Karma: 0
Posts: 20
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

So I am getting some readings, but I am also getting some very odd behavior when it comes to the gyroscope readings. Normally, if the gyro isn't calibrated correctly, it drifts, with the heading slowly increasing or decreasing when stationary. Now, it will drift for a few seconds, then jump a large amount of degrees, only to drift slowly again. It didn't do this before using the interrupt, but now it does. Any idea why?
The spaces in the video serial output are caused each time the angle is computed, and the value shown is printed at the end of the loop function, regardless of whether or not the angle was computed.
Since there is over 500 lines, I will only post the relavant bits. This video shows the behavior I am seeing: http://youtu.be/cNpZ62TFNcQ


Code:
#include <avr/interrupt.h>  //include interrupts to use timer 2 overflow as internal interrrupt
#include <avr/io.h>
//#####################################
//Gyro vars
int gyroPin = A4;               //Gyro is connected to analog pin 0
float gyroVoltage = 5;         //Gyro is running at 5V
float gyroZeroVoltage = 2.425;   //Gyro is zeroed at 2.423V
float gyroSensitivity = .007;  //Our example gyro is 7mV/deg/sec
float rotationThreshold = 1;   //Minimum deg/sec to keep track of - helps with gyro drifting
volatile float currentAngle = 0;          //Keep track of our current angle
volatile int roundedAngle; //rounded angle for whole number use
volatile float gyroRate;
unsigned int count = 0;
volatile float ISRvalue;
volatile boolean canCheck = false;


//#############################################################################################
//Setup
//#############################################################################################
void setup()
{

  Serial.begin(9600); //Setup serial for debugging
  pinMode(5, OUTPUT); //Setup all digital pins that are permanant I/O
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
  pinMode(8, OUTPUT);
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(13, OUTPUT);
  digitalWrite(10, HIGH); //Turn fan off. it defaults to on when LOW
 
    //Setup Timer2 to fire every 1ms
  TCCR2B = 0x00;        //Disbale Timer2 while we set it up
  TCNT2  = 130;         //Reset Timer Count to 130 out of 255
  TIFR2  = 0x00;        //Timer2 INT Flag Reg: Clear Timer Overflow Flag
  TIMSK2 = 0x01;        //Timer2 INT Reg: Timer2 Overflow Interrupt Enable
  TCCR2A = 0x00;        //Timer2 Control Reg A: Normal port operation, Wave Gen Mode normal
  TCCR2B = 0x05;        //Timer2 Control Reg B: Timer Prescaler set to 128
}


//#############################################################################################
//Interrupt Service Routine
//#############################################################################################
ISR(TIMER2_OVF_vect) { //Timer2 overflow interrupt vector, called every 1 ms
  count++;
  if(count > 99) { //how many ms to wait before doing this(0 based)
    // readHeading();  - don't use this, too much going on...
    ISRvalue = analogRead(gyroPin);
    count = 0;
   canCheck = true;
  }
  TCNT2 = 130; //set to 130 out of 255 (125 cycles)
  TIFR2 = 0x00; //restart timer
};

//#############################################################################################
//Gyro Code
//#############################################################################################

void readHeading()
{
  if(canCheck)
  {
  gyroRate = (ISRvalue * gyroVoltage) / 1023; //This line converts the 0-1023 signal to 0-5V
  gyroRate -= gyroZeroVoltage;  //This line finds the voltage offset from sitting still
  gyroRate /= gyroSensitivity;   //This line divides the voltage we found by the gyro's sensitivity
  if (gyroRate >= rotationThreshold || gyroRate <= -rotationThreshold) { //Ignore the gyro if our angular velocity does not meet our threshold
    gyroRate /= 10; //This line divides the value by 100 since we are running in a 10ms loop (1000ms/10ms)
    currentAngle += gyroRate;
  }
  if (currentAngle < 0) {  //Keep our angle between 0-359 degrees
    currentAngle += 360;
  }
  else if (currentAngle > 359) {
    currentAngle -= 360;
  }
  roundedAngle = currentAngle;
  roundedAngle = 360 - roundedAngle;
  //Serial.println(" ");
  canCheck = false;
  }
  return;
}



//#############################################################################################
//Check all sensors
//#############################################################################################
void checkSensors()
{

//I left out the functions for most of these sensors in this post. basically checkSensors() will run readHeading()
  leftEncoder(); //get encoder ticks
  rightEncoder();
  flameFLval = senseFlame(flameFLpin); //check all flame sensors
  flameFCval = senseFlame(flameFCpin);
  flameFRval = senseFlame(flameFRpin);
  flameBLval = senseFlame(flameBLpin);
  flameBCval = senseFlame(flameBCpin);
  flameBRval = senseFlame(flameBRpin);
  readHeading(); // check gyro
  frontUSdist = measureDistance(frontUSpin); // check all US sensors
  leftUSdist = measureDistance(leftUSpin);
  rightUSdist = measureDistance(rightUSpin);
  reflectVal = reflectSensor(); //check reflectivity sensor
  return;
}


//#############################################################################################
//Loop Code
//#############################################################################################

void loop()
{
  checkSensors();
  Serial.println(roundedAngle);
}
Logged

New Hampshire
Offline Offline
God Member
*****
Karma: 13
Posts: 779
There are 10 kinds of people, those who know binary, and those who don't.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Gyros are going to drift no matter how well they are calibration.  Calibration can only minimize drift, not eliminate it.
You are also going to always have integration errors that will accumulate over time.  There's no way to eliminate that either.

If you want reasonable heading tracking over significant periods of time, you're going to have to incorporate a magnetometer (electronic compass basically) into your calculations with some form of filter to combine the gyro and magnetometer inputs into a heading output.  A complementary filter is fairly easy to implement.  A kahlman filter is more complex (not just to implement, but to understand as well), but provides better accuracy as well.

On the other hand, in a robotic application you typically only need to integrate rotation when the robot is actually rotating, and since I assume you also have control of the robot, you can start and stop your integration to coincide with the appropriate commands.  There's no reason to integrate your heading when you know your robot is not moving.  This will still not eliminate your drift and integration errors entirely, but it will significantly reduce them, and depending on what kind of accuracy you are trying to attain, it may be enough.
Logged


Offline Offline
Newbie
*
Karma: 0
Posts: 20
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I understand they will drift. Drift can be minimized. There is more than drift going on here...
If you look at the video, from 0:09 - 0:16, that is drift... 359, 358,357, 356...
Then continue watching. in 0:16-0:17 it jumps, in 1 line, from 356 to 22, a change of + 24. That is not drift...I'm not sure what that is... Which is why I am here.
Logged

Offline Offline
Sr. Member
****
Karma: 1
Posts: 314
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I don't know about Gyros and their calculations.  I will leave that for someone else to comment on.

Your code looks good.  A couple little things I might change are:
1) Rather than comparing to 99 which it must do every ms I would check for zero.  This is a little faster.
2) I could have an error check.  If the "canCheck" flag is still set when you get new data you lost a sample.
Code:
 if (count-- == 0) { //how many ms to wait before doing this(0 based)
    // readHeading();  - don't use this, too much going on...
    ISRvalue = analogRead(gyroPin);
    bMissedReading = canCheck;
    canCheck = true;
    count = 99;
  }

You could save yourself a little time, though not much if you change:
   readHeading(); // check gyro
to
   if (canCheck) readHeading(); // check gyro
and remove the check from "readHeading()".  There's not much overhead to a call but it's something.

Other things that might same time is avoiding type conversions and variable access.
Code:
// change this
float gyroVoltage = 5;         //Gyro is running at 5V
float gyroZeroVoltage = 2.425;   //Gyro is zeroed at 2.423V
float gyroSensitivity = .007;  //Our example gyro is 7mV/deg/sec
float rotationThreshold = 1;   //Minimum deg/sec to keep track of - helps with gyro drifting
// to this
#define gyroVoltage           5.0      //Gyro is running at 5V
#define gyroZeroVoltage    2.425   //Gyro is zeroed at 2.423V
#define gyroSensitivity      0.007  //Our example gyro is 7mV/deg/sec
#define rotationThreshold  1.0     //Minimum deg/sec to keep track of - helps with gyro drifting

For
     gyroRate = (ISRvalue * gyroVoltage) / 1023; //This line converts the 0-1023 signal to 0-5V
it is good that you multiply before you divide.  Although with a float it probably doesn't matter, it a good habit.
Go ahead and cast you "int" and divide by a float.
     gyroRate = ((float)ISRvalue * gyroVoltage) / 1023.0; //This line converts the 0-1023 signal to 0-5V

Avoid data conversions like the following:
  if (currentAngle < 0) {  //Keep our angle between 0-359 degrees
    currentAngle += 360;
  }
should be
  if (currentAngle < 0.0) {  //Keep our angle between 0-359 degrees
    currentAngle += 360.0;
  }

Hope this helps.
Logged

New Hampshire
Offline Offline
God Member
*****
Karma: 13
Posts: 779
There are 10 kinds of people, those who know binary, and those who don't.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

You're likely running into a race condition with your ISR.  canCheck is an inadequate guard as there is no synchronization.  It's still entirely possible for your ISR to execute and update ISRValue while you are in the middle of calculating your rotation using that value.  If that happens, that calculation is completely invalid.

Here's an old post of mine demonstrating the need for atomic access of isr modified variables:
http://arduino.cc/forum/index.php/topic,73838.0.html

Create a local float in your readHeading function.  Assign your ISRValue to that local float in an ATOMIC_BLOCK, then perform your calculations using that local value, and see if that eliminates your reading jumps.
Logged


Offline Offline
Sr. Member
****
Karma: 1
Posts: 314
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

canCheck is an inadequate guard as there is no synchronization.  It's still entirely possible for your ISR to execute and update ISRValue while you are in the middle of calculating your rotation using that value.
You are correct in that there is no synchronization.  However, is should not be needed.  If the timing is that close, he is going to be missing reading.  That's why I suggested a Missed-Reading-Flag.  If he is missing reading, that should tell him.  Since the canCheck is reset at the end of his calculations and his calculations will totally stop if interrupted, if he can not get his reading processed between samples, it will show up as a missed reading.

At least that's how it looks to me.
« Last Edit: May 30, 2012, 03:12:46 pm by RandallR » Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
Code:
ISR(TIMER2_OVF_vect) { //Timer2 overflow interrupt vector, called every 1 ms
  count++;
  if(count > 99) { //how many ms to wait before doing this(0 based)
    // readHeading();  - don't use this, too much going on...
    ISRvalue = analogRead(gyroPin);
    count = 0;
   canCheck = true;
  }

You didn't follow my second suggestion. Start an analog read here, don't do one. The analogRead you are doing takes 104 uS or more, far too long for an ISR.

http://www.gammon.com.au/interrupts

That shows how to start an analog read. Then have something like this (as in that post):

Code:
// ADC complete ISR
ISR (ADC_vect)
  {
  byte low, high;
 
  // we have to read ADCL first; doing so locks both ADCL
  // and ADCH until ADCH is read.  reading ADCL second would
  // cause the results of each conversion to be discarded,
  // as ADCL and ADCH would be locked when it completed.
  low = ADCL;
  high = ADCH;

  adcReading = (high << 8) | low;
  adcDone = true; 
  }  // end of ADC_vect

Now your main loop can take that value and so something with it.

I also don't see why you are doing a count inside a timer ISR. That's what timers are for ... counting. Use Timer 1 (the 16 bit timer), that gives you plenty of resolution. Set it up so that when the timer goes off you just start the ADC conversion. No more counting or anything.
Logged

New Hampshire
Offline Offline
God Member
*****
Karma: 13
Posts: 779
There are 10 kinds of people, those who know binary, and those who don't.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

and his calculations will totally stop if interrupted

If his calculations get interrupted in the middle of accessing ISRValue (which is entirely possible since it is a 16 bit variable), then the value that gets used in his calculations will be completely invalid, and thus result in an invalid calculation.  The canCheck variable does nothing to prevent this.
Logged


Offline Offline
Sr. Member
****
Karma: 1
Posts: 314
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

If his calculations get interrupted in the middle of accessing ISRValue (which is entirely possible since it is a 16 bit variable), then the value that gets used in his calculations will be completely invalid, and thus result in an invalid calculation.  The canCheck variable does nothing to prevent this.

If the interrupt occurs as you say, "... in the middle of accessing ISRValue ..." then he will not have cleareds the "canCheck" flag, therefore the interrupt will set the "MissedReading" flag and he will know that data was missed.  Also the "MissedReading" flag is not some sort of synchronization feature.  It is simply an indicator that there is a problem.  It doesn't prevent the problem or fix the problem.  It just let you know that there is a problem.  The solution is TBD.

I can not see a case where bad data is read, because of being interrupted mid read, where the "MissedReading" flag is not set.  He could choose to discard any reading where the "MissedReading" flag is set.  The bigger problem, if he is missing readings, is that he is not processing the readings in a timely manner.  If this problem is resolved then there is no need to worry about corrupted reads.
Logged

Offline Offline
Edison Member
*
Karma: 17
Posts: 1041
Arduino rocks
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I'm not so sure you need to use interrupts at all. As long as you read the sensor in 10ms intervals, it doesn't matter exactly when its read (ie you can have a margin of error around the 10ms "tick" but the tick has to stay steady). For that, you can just check to see if it's time yet:

Code:

void loop() {
  static unsigned long prevgyrotime = 0;
  unsigned long currenttime = millis();
  if (currenttime - prevgyrotime > 10) {
    prevgyrotime = currenttime; // Don't use millis() here or you might get a (tiny) bit of drift
    // Read gyro
  }

  // Do other stuff
}

This will make sure that after 1 second, exactly 100 have taken place, and after 100 seconds exactly 10000 readings have taken place.
Logged

New Hampshire
Offline Offline
God Member
*****
Karma: 13
Posts: 779
There are 10 kinds of people, those who know binary, and those who don't.
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

I don't know if there was some editing that has occurred, but the code eovnu posted in reply #4 contains no trace of a MissedReading variable.
Logged


Pages: [1] 2   Go Up
Jump to: