Polling vs. Interrupts - An In-Depth Example with a Motor Encoder

(article continued from previous page)

Because I have developed a number of tachometers, I felt that a counting device would be easy. What I didn’t realize was that there is a fundamental difference between a counter and a tachometer.

A tachometer samples a signal for a period of time and then computes and displays the rotational frequency. It doesn’t matter if the tachometer misses pulses before or after the sample period, since it is only measuring the speed for a given moment.

In contrast, a counter can’t miss any pulses, even when it needs to branch off to do other work like service the LCD screen or user buttons. Every missed count accumulates errors in the total count. Therefore, an accurate counter needs to either:

The next two pages illustrate the problems with trying to read the sensors fast enough -- despite multiple optimizations. Ultimately, the count-tracking routines needed to be connected to hardware interrupts to be successful at catching all higher-speed pulses.

Maxon Gearhead Motor with Magnetic Encoder

To test the Counter project, I used a Maxon A-max 22 mm 24 V DC motor (#110164) with a digital magnetic encoder (110778). It is a quadrature encoder with two channels (two outputs) that provide 16 counts each per motor rotation.

A Maxon A-max motor with gearhead and encoder.

A Maxon A-max motor with gearhead and encoder.

At around 8V, the sample Maxon motor spins at 3400 RPM (revolutions per minute). Under those conditions, the encoder emits 907 pulses per second. Here’s the math:

3400 revolutions per minute * 16 quadrature encoder counts per revolution = 54400 encoder counts per minute
54400 encoder counts per minute / 60 seconds per minute = 907 encoder counts per second

That means the wave length (peak-to-peak) per count is 1.1 ms:

1 / 907 counts per second = 0.0011 seconds per count

Oscilloscope trace of the outputs of a Maxon motor with quadrature encoder and the four sensor states that must be read.

Oscilloscope trace of the outputs of a Maxon motor with quadrature encoder and the four sensor states that must be read.

The oscilloscope trace of the sample Maxon encoder shows nice clean digital signals for sensor A and sensor B. No spikes, no ringing, and reasonably fast (vertical) rise and fall times. The output is nearly rail-to-rail -- meaning it goes from almost 0V to almost 5V. That’s excellent.

As expected, the total wave pulse length is about 1.1 ms (milliseconds). Of course, if the motor speed is increased then the encoder output frequency will also increase.

To accurately read a quadrature encoder, there are four encoder states that must be detected per count (low low, low high, high high, high low). Therefore, for the sample Maxon motor, the microcontroller must read at least every 0.275 ms (1.1 ms / 4) to accurately record motor direction, distance, and speed. Four reads is the minimum number of reads -- reading more often will avoid errors due to output glitches or transition edges.

Problems Reading the Encoder

When the Maxon motor encoder was initially connected to the Counter project, the displayed count was erratic. Sometimes the count would increment, sometimes it would decrement, and sometimes it would oscillate around the same value.

The first step is to make sure the encoder outputs are reaching the microcontroller pins with a clean, glitch-free, square-wave signal that goes from nearly ground to nearly 5V (or whatever the microcontroller voltage is). The oscilloscope trace was taken at the microcontroller pins, not at the encoder pins, to rule out bad connections or other errors. It confirms that the source signal is not at fault.

So, the next question is “How often is the microcontroller reading the encoder outputs?”

A quick method of determining when and where a microcontroller read occurs is to toggle / flip a spare microcontroller pin upon each read. This doesn’t take much processor time and it is easy to display on a logic analyzer (digital scope) beside the encoder outputs. Each time the spare pin goes high or low is when the microcontroller read routine is executing.

Most of the Atmel AVR microcontrollers include the ability to toggle an output pin with a single instruction. For example, to toggle output pin 6 on port D in ImageCraft C, simply set the bit on the input register with: PIND |= (1<<6); Which translates in assembly language to SBI 0x09,6. It seems weird to always SET the INPUT pin to toggle an output, but that takes advantage of a low-level address where the write operation wasn’t doing anything otherwise.

My oscilloscope only has two inputs. Since I needed to display three signals (Read routine pin, Encoder sensor A, and Encoder sensor B), I turned to the Itronix Logicport logic analyzer that can display up to 34 digital signals.

Logic analyzer trace showing that the initial code on the Counter project wasn’t reading the encoder outputs of the Maxon motor often enough.

Logic analyzer trace showing that the initial code on the Counter project wasn’t reading the encoder outputs of the Maxon motor often enough.

The first signal (top) in the above image is the spare microcontroller pin that signals a microcontroller read whenever the pin goes from low to high or high to low. The second signal (middle) is the encoder A output. The third signal (bottom) is the encoder B output.

As you can see, there are huge gaps between each microcontroller read. Entire encoder sensor pulses are passing by without being read. This causes the microcontroller to misinterpret the direction and amount of rotation of the motor. This explains why the count is all messed up.

To determine the source of the delay between reads, I decided to eliminate the counter display routine to see if it was taking up most of the processor time.

Logic analyzer trace showing analog reads and encoder outputs, without the microcontroller taking the time to display the current count on an LCD screen.

Logic analyzer trace showing analog reads and encoder outputs, without the microcontroller taking the time to display the current count on an LCD screen.

Indeed, without the counter display routine, the number of reads increases significantly and the time between reads is much steadier. This indicates that the microcontroller goes through a lot of work to convert a long (4 byte) integer into a string and then to display up to 11 characters ('-2147483648') on the LCD screen.

This also tells me that the interrupt routines and other background operations are not consuming significant processor time, since there aren’t any major time gaps between reads. (I looked over a lot more of the trace to be sure.)

Without the display routine, the encoder reads occur about 5 times per encoder wave, which is slightly more than the minimum 4 times required. But, that’s lame.

Since the encoder outputs are digital signals, what speed improvement would occur if the microcontroller were to read the signals digitally, rather than using the analog-to-digital converter?

Logic analyzer trace showing digital reads and encoder outputs, without the microcontroller displaying the current count.

Logic analyzer trace showing digital reads and encoder outputs, without the microcontroller displaying the current count.

Ahh! Much better. The Counter project can digitally read the example Maxon motor encoder outputs over 40 times per count (pulse wave), which is 10 times more than the minimum required. That tells us that reading an analog signal takes significantly more time than reading a digital signal.

Let’s put the display code back in...

Logic analyzer trace showing digital reads and encoder outputs while displaying the current count on an LCD.

Logic analyzer trace showing digital reads and encoder outputs while displaying the current count on an LCD.

Bummer. Even with a digital read routine, not enough time is saved to display the count on the LCD without missing encoder steps. That is, entire high or low portions of an encoder output are occurring before the microcontroller finishes displaying the current count and returns to read the encoder.

What if the count is switched from a long (4 byte) integer to a short (2 byte) integer?

Logic analyzer trace showing digital reads and encoder outputs while displaying a two-byte count on an LCD.

Logic analyzer trace showing digital reads and encoder outputs while displaying a two-byte count on an LCD.

Better, but not good enough. In some cases, only two reads are occurring per encoder wave. That’s half the minimum.

In the display routine, what’s taking so much time? The conversion to a string divides by 10 twice for each digit (once to obtain the modulus and once to shift the number down).

Another time consumer is that the LCD/OLED display routine is based on a 4-bit parallel output (rather than 8 bit) and must wait when the LCD asserts a busy bit signal. To see if the LCD routine is the bottleneck, a test was performed where the code skips the division and simply displays a fake number on the LCD.

Logic analyzer trace showing digital reads and encoder outputs while displaying a fixed string on an LCD.

Logic analyzer trace showing digital reads and encoder outputs while displaying a fixed string on an LCD.

The time between reads is cut in half by displaying a fixed string (fake number) on the LCD. This shows that about half of the display routine is spent converting an actual number and the other half of the time is spent showing the number on the LCD. This means both parts of the display routine need to be optimized to obtain adequate speed improvement.

Several hours were spent optimizing the code, with very little improvement in speed. Unfortunately, there doesn’t seem to be C library for dividing by ten that also provides the remainder in the same operation. Such a routine would almost double the speed of converting a decimal number to a string.

Since a human being can’t read a number that is being updated hundreds of times a second, one of the easiest optimizations is to only update the display a couple of times per second. That allows the processor to spend most of the time concentrating on reading the encoder.

Now, every time the encoder is read and the count is updated, a Boolean flag is set to indicate the display needs updating. A timer goes off several times a second to check the flag and to draw the new counter value to the LCD. This simple change allows the Counter Project to successfully read and update the count, with only a couple of missed counts (during display updates) per second.

Several missed counts per second is amateurish. Isn’t there a way for such a fast microcontroller to read the encoder perfectly without any errors? What about using hardware interrupts whenever a sensor state changes?