Say if you wanted to read a sensor every 100 ms, and then send the data over serial. How would you do it? That's easy, isn't it? Just a few lines of code like this should do the trick:
const int sensorPin = A0;
void setup()
{
pinMode(sensorPin, INPUT); // Initialize pin as input
Serial.begin(9600); // Start serial com
}
void loop()
{
int reading = analogRead(sensorPin); // Get reading
Serial.println(reading); // Print it
delay(100); // Wait for 100 ms
}
Lets take a closer look at this code:
- Firstly the sensor value is read into the 'reading' variable. This takes around 100 microseconds.
- Next, the 'reading' variable is then sent via Serial comms to the computer. This is quite fast to execute, as we do not have to wait for the data to be sent.
- Finally, we delay for 100 ms. This in itself isn't even 100% accurate. If we take a good look at the function, we will see that in only guarantees a delay of at least 100 ms.
So by the time we get back to reading the sensor again, its been over 100 ms. Not really what we want.
Another issue it that while we are in the delay() function, we are essentially wasting time, and nothing else can be done in the meantime. (except for an interrupt of course)
Now we could adjust the delay to be a little less, but but that's still not very accurate, and what if you wanted to add more code? Do more than just Serial.println()? Or what if your code takes an unknown amount of time to execute? Like if there's a lot of 'if' and 'switch' statements. This is not how we want to write our code, as things could get very messy timing wise.
Lets look at some better, and more efficient alternatives.
This first way is the simplest, and easiest way to improve the timing:
const int sensorPin = A0;
unsigned long time = 0;
unsigned long timeOld = 0;
void setup()
{
pinMode(sensorPin, INPUT); // Initialize pin as input
Serial.begin(9600); // Start serial com
}
void loop()
{
time = millis(); // Get current millis()
if(time - timeOld >= 100) // If its been 100 ms or more...
{
int reading = analogRead(sensorPin); // Get reading
Serial.print(reading); // Print it
timeOld = time; // Reset timeOld
// Can put more code here,
// as long as it takes
// less time than 100 ms
}
// Can also put more code here,
// provided it it takes significantly
// less time than 100 ms
// (Putting code here does however affect
// the timing accuracy significantly)
}
This is very similar to the BlinkWithoutDelay example in the Arduino IDE. Here we are using the millis() function to control our timing, rather than the delay() function. This has a few distinct advantages:
- Our timing is much better and is taken care of by the millis() function.
- We can execute much more code, as we aren't wasting any time with delay() functions.
- Code which takes an unknown amount of time won't cause any problems, provided it takes less than 100 ms.
Although better than the first code example, this example it has its own problems:
- Putting code outside of the 'if' statement will not affect the average rate at which we read the sensor, but it may cause small variations in the timing between reads. This is because we can't check the millis() function as often, so there may be a small delay between the exact moment we hit 100 ms and when we can actually check the time.
- If our code locks up, or takes an unexpectedly long time to complete, we wont be able to read the sensor and the timing goes to hell.
This next way is much better, but somewhat more advanced than using the millis() function. It allows you to achieve near perfect timing and is done by using an ISR (Interrupt Service Routine).
You may have heard of an interrupt before as a block of code which can be executed by a pin change, called an external interrupt. Well that block if code can be triggered by much more than just an external pin change. We can use one of the Arduino's (
specifically the Atmega328p, but this applies to most, if not all Arduino boards) hardware timers to trigger the ISR at a regular interval, regardless of what the main loop is doing. This means we can poll the sensor at exactly 10 Hz (every 100 ms) for example, and not have to worry about what happens in the main loop. And because we're using an interrupt, the main loop could be doing almost anything, and the sensor will still be read at the same time, every time.
Here's an example of using an ISR triggered by timer1:
const int sensorPin = A0;
volatile int reading = 0; // Variables used in the ISR need to be volatile
int readingOld = 0;
void setup()
{
pinMode(sensorPin, INPUT);
Serial.begin(9600);
// This configurres timer1 to CTC every 100 ms
// It also attaches an interrupt to the CTC
noInterrupts();
TCCR1A = 0x00; // Clear timer1's control registers
TCCR1B = 0x00;
TCCR1C = 0x00;
TCNT1 = 0; // Pre-load the timer to 0
OCR1A = 6250; // Set output compare register to 6250
TCCR1B |= _BV(WGM12); // Turn on CTC mode
TCCR1B |= _BV(CS12); // Set prescaler to 256
TIMSK1 |= _BV(OCIE1A); // Enable timer compare interrupt
interrupts();
}
void loop()
{
// As a rule of thumb, we try to minimize the amount of code
// in the ISR itself, so the Serial stuff (and whatever else)
// goes in the main loop.
if(reading != readingOld)
{
Serial.println(reading);
readingOld = reading;
}
}
// This is the interrupt service routine
ISR(TIMER1_COMPA_vect)
{
// Code in here gets executed every time the counter
// reaches, in this case, 6250 (every 100 ms)
reading = analogRead(sensorPin);
}
In the block of code between noInterrupts() and interrupts() we are configuring timer1. You may not recognise some of the names here like 'TCCR1A'. These are the registers that relate to the timer, and we are accessing them directly, to set up the timer.
In the ISR macro, the '
TIMER1_COMPA_vect' connects the interrupt routine to the timer, causing the block of code below it to execute every time timer1 reaches 6250, in this case.
To keep things simple, I used analogRead (like I did in the previous examples) in the ISR. This isn't the best way to go about it, because the analogRead function has a small period where it waits for the conversion to complete. Ideally, we would start the conversion in the first interrupt, then take in the reading in the seccond interrupt, and start a new conversion again, etc.
The nice thing about this technique, it that the code in the interrupt routine (or ISR for short) will always be executed at the right time. As the name suggests, an interrupt causes the processor to drop whatever its doing, and tend to the interrupt itself. This means that we will always be reading the sensor on time.
This is how I poll the inputs in my
IR decoder,
rotary encoder, and
debouncer libraries, and this is probably the best way to execute your code at a regular interval, if timing is very important. It is however not without some small disadvantages. Timer1 is used for pwm control on digital pins 11 and 12, so these pins cannot be used for pwm when the timer is configured like this. Also, the servo library relies on timer1, so there may be a conflict if you're trying to use this library and reconfigure the timer.