Design a Robust Quadrature Encoder ISR based program for Arduino Motor Control

What is a quadrature encoder?

A quadrature encoder is an electromechanical sensor used to measure physical rotation such as the motor shaft featured in this engineering brief. The associated microcontroller or Programmable Logic Controller (PLC) can then interpret the sensor’s data to calculate rotational velocity, direction, and the total angular distance traveled.

Quadrature encoders are preferred in many applications as they have low cost yet provide fine measurement resolution. The Pololu #4754 DC motor shown in Figure 1 is a representative example. It features no-contact Hall effect transducers to provide a responsive output that can be directly coupled to the pictured Arduino Nano Every.

In future installments we will show how to design a responsive servo system including a Proportional Integral Derivative (PID) controller. A robust quadrature encoder interface is essential for the servomotor performance.

Who is this article written for?

This article is written for transitioning hobbyist and students. It assumes you are familiar with the Arduino microcontroller and have successfully programmed several of your own designs. The text will guide you to the next level with an applied real-time example featuring moderate timing requirements associated with the quadrature encoder.

Figure 1: This article explores an application of the pictured Pololu #4754 DC motor and the Arduino Nano Every microcontroller. The Digilent Analog Discovery is seen in the background.

How does a quadrature encoder work?

The term “quadrature” refers to signals that are 90 degrees out of phase. For example, the AC current for Tesla’s famous induction motor was driven by a sine and cosine waveform. The quadrature encoder operates on quadrature waveforms. Actually, it uses a digital representation as shown in Figure 2. Here we see the quadrature encoder’s twin digital output signals and their relationship to the SIN and COS waveforms. Observe that each output is active when the corresponding sinusoid is above zero. A representative quadrature encoder is pictured in Figure 1. Mounted on the end of the motor we can see the black ring with a series of magnetics around the periphery. We can also see the Hall effects sensor on the Arduino side of the magnet ring.

For additional information about the quadrature encoder, please review this introductory article. It provides fundamental information about the nature of the twin digital waveforms. It describes the critical relationships used to determine the direction of rotation. It also introduces the state machine as shown in Figure 3 which will be important later in our discussion.

Figure 2: Quadrature signals in a forward (CW) direction with the blue signal leading the green. The Q1 and Q2 sensors activate when corresponding signal is positive.

Why doesn’t my quadrature encoder work especially at high speeds?

A motor driven quadrature encoder is not a trivial application. As we will see shortly, it’s an application that pushes the limits of a microcontroller. Therefore, we must pay close attention to the timing and the microcontroller’s responsiveness. In many cases programming is not enough. Instead, we must use the hardware-based solutions such as the Interrupt Service Routine (ISR). As will be shown, the ISR is a combination of microcontroller hardware and software techniques designed to enhance the microcontroller’s response to real world events.

The most likely reason for failure of a quadrature encoder program is a lost count due to incorrect programming techniques or a microcontroller that is slow to respond. Recall that a quadrature encoder-based system keeps track of position by observing and then responding to the changes in the encoder’s twin digital outputs. If the system (microcontroller and associated program) isn’t fast enough, it will miss a count. The result is a mechanical system that is floundering out of control. For example, if the quadrature encoder is monitoring the position of a servomotor, the system will lose the ability to correctly position the corresponding motor axis.

Later sections of this article present a technique to determine if your system is fast enough. Using an oscilloscope, you will learn how to compute the percentage of time dedicated to the handling the quadrature encoder tasks.

Importance of the encoder’s Counts Per Revolution (CPR) when driven by a motor

There are many ways to construct a quadrature encoder. However, every encoder is designed with a specific Counts Per Revolution (CPR). We can visualize this using a playing card and a spoked bicycle wheel. As the wheel rotates, we can count the number of spokes as ticks in the card. Conversely, by knowing the number of spokes we can determine the angular position of the wheel.

The resolution at which we can determine the shaft’s angular position is determined by the number of spokes. For example, if the wheel has only 4 spokes, our angular measurement is limited to 90 degrees. If the wheel has 64 spokes our resolution is now 5.625 degrees.

The CPR is the quadrature encoders analogy to the number of spokes. It describes the physical relationship between the shaft’s “spokes” and the sensing elements. For our Pololu #4754 motor, the “spokes” are composed of alternating North and South magnet assembly.

The chosen Pololu motor has a CPR of 64. This does not imply 64 N/S magnet assemblies. Our wheel and spoke analogy is too simple. Instead, we expect to see only 16 slots. From Figure 2, we observe that the twin outputs effectively divide each cycle in 4 unique states. Consequently, 16 slots (16 N/S magnet pairs) would be sufficient to obtain a CPR of 64.

Quadrature encoder resolution is a function of CPR

Note that the CPR of 64 describe the sensor as mounted on the motor shaft. Also, observe that the Pololu #4754 is a motor is a gear motor with a 70 to 1 ratio. To determine the CPR as measured at the output shaft, multiply the motor’s CPR by the gear ratio to obtain 4480 counts per rotation.

CPR and rotational speed determine the count per second

The last and perhaps most critical metric for the Pololu motor is the no-load shaft speed given as 150 PRM. This implies a motor speed of 10,500 RPM or 175 revolutions per second. With a CPR of 64, we calculate that the microcontroller must respond to a system operating at 11,200 cycles per second.

Responding to a 11,200 count per second motor is no trivial operation. Careful programming is required so that we do not miss even a single transition. It may be possible to perform this activity from with the main Arduino loop. However, doing anything else within the main loop would be problematic. For example, it would be very difficult to perform that next natural evolution of this project and implement a PID controller. The non-blocking code techniques become complex. Also, sending signals to the serial monitor or a LCD display would present a real challenge as these operations can sometimes take longer than the 11,200 count per second motor would allow.

How to integrate a quadrature encoder with an Interrupt Service Routine (ISR)

The traditional solution is to use an Interrupt Service Routine (ISR). The interrupt is one of the defining attributes of a microcontroller that allow it to respond in a timely manner to asynchronous real-world events.

What is an Interrupt Service Routine?

The ISR is combination of dedicated hardware and software techniques that allow the microcontroller to respond in near real-time. To better understand the ISR, let’s focus on a concept know as the microcontroller’s state. Without getting too technical, recognize that a microcontroller is a highly specialized state machine running combinational (sequential) logic. Recall that sequential logic requires memory as any future action depends on the state (past memory) held in the machine. There are several registers (memory locations) closely associated with the microcontroller’s heart that hold the state. These include the status register, program counter, and local working registers.

When an interrupt occurs, the microcontroller will quickly save its current state and then jump to an event specific piece of software. It’s important to realize that this state change is built into the hardware resulting in a fast transition from the main code to the interrupt code. It’s like a telephone ringing when you are reading a book. You place a bookmark on the current page and then jump up to answer the phone. After you are done with the phone (interrupt) you return to where you left off.

Tech Tip: It’s helpful to think of the ISR and main loop in terms of foreground and background respectively. The ISR is in the foreground with high priority as it must handle time sensitive tasks. In this particular example, the ISR must respond without fail to the 11,200 count per second quadrature encoder of the position will be lost. Other tasks such as sending data to the serial monitor or updating an LCD display are handled in the background. They lower priority and can be interrupted provided the microcontroller’s context is saved.

It is important to keep the ISR short so that the microcontroller spends most of its time on the background tasks. For example, when the motor is operating at maximum speed, the microcontroller spends less than 5% of its time in the ISR. If the time was longer the background tasks would be starved for CPU cycles resulting in sluggish operation. In an extreme situation, the ISR may not finish servicing a change in the quadrature encoder before the next change comes along. In this case the system would lose position and there would be no cycles remaining for the background tasks.

Quadrature encoder ISR

Our implementation of the Arduino quadrature encoder begins with these two lines of code:

attachInterrupt(digitalPinToInterrupt(QUAD1_A_PIN), ISR_QUAD1, CHANGE);
attachInterrupt(digitalPinToInterrupt(QUAD1_B_PIN), ISR_QUAD1, CHANGE);

Notice that the interrupt occurs on any change of the QUAD1_A_PIN or the QUAD_B_PIN. Also notice that both events call the same ISR_QUAD1 code.

State based quadrature encoder ISR with error detection

If you have not already done so, please review the introductory quadrature encoder article. It describes the Finite State Machine (FSM) as shown in Figure 3. As you read the article, note how the quadrature encoder’s outputs have a fixed pattern of 00 → 01 → 11 → 10 → 00 for forward (clockwise) rotation and a related 00 → 10 → 11 → 01 → 00 pattern for reverse (counterclockwise) operation. This is reflected in the upper clockwise state transition of Figure 3 and the inner counterclockwise transitions.

Note that certain transitions are considered faults. For example, if the system is in state 00 and the quadrature outputs suddenly transition to 11, something has gone wrong. The count is considered corrupt, and the system enters a fault condition. For clarity, the return to self loops are not shown in Figure 3.

A portion of this code is shown here below. This code directly follows from Figure 3 and shows the possible state transitions from the 00 (9-oclock) state. The system can mode CW to state 01. It can move CCW to state 10. It can stay in the present state (loop to self). Finally, the system enters a fault state if the quadrature inputs jump from 00 to 11. Note that the 4-byte quad_1_cnt is incremented or decremented based on a valid state transition.

Click here for the Arduino code:
QUAD.ino (7.3 KB)

switch (quad_1_state) {

case 0b00:

    if (BA == 0b00) {
        ;
    } else if (BA == 0b01) {
        quad_1_state = 0b01;
        quad_1_cnt++;
    } else if (BA == 0b10) {
        quad_1_state = 0b10;
        quad_1_cnt--;
    } else{
        quad_1_state = 0xFF;
    }
    break;

Figure 3: State diagram representation of the quadrature encoder signals. The bubbles represent the states while the numbers on the lines represent the sensor values. For clarity the remain-in-state loops have been eliminated.

Atomic transfer of data from the ISR to the main loop

At this point, the ISR maintains a faithful copy of the motor’s shaft position as measured by the quadrature encoder. We could declare victory and conclude this article. However, we would missing a fundamental coordination step between the ISR and the main loop.

Recall that the Arduino Nano Every is an 8-bit machine. At the atomic level it operates on byte-width variables. Normally, this indivisible fact is hidden by the C compiler with keywords such as int32_t (type long). However, we must recognize that a simple operation such as quad_1_cnt++; requires at least four discrete ATMega4809 instruction. The first operation adds the literal 1 to the lowest byte. This is followed by three add with carry instructions to ripple the addition through all bytes.

Tech Tip: The term atomic refers to things that are indivisible. In a microcontroller atomicity is related to the native bit width of the microcontroller. For example, an 8-bit machine operates on byte width variables whereas a 32-bit machine operates on 4-byte atomic elements. Complication arise when transferring non-atomic variables between asynchronous portions of your program. The prime example contained in this engineering brief involved transferring a 32-bit value between the ISR and the main loop code using an 8-bit machine. Careful synchronization is required to preserve data integrity during the transfer process. We could use the word atomic to say that the entire 32-bit variable must be carefully transferred as a single atomic unit.

Again, normally we don’t care about such things. However, we must recognize that the main loop and the ISR run asynchronously; there is no natural coordination between the instructions. Recall that our purpose is to transfer the quadrature encoder count to the main loop. Without proper coordination, there will be an unfortunate alignment of the stars and the main loop will be interrupted by the ISR halfway through a copy process. The resultant variable will be corrupt.

This atomic-safe stipulation is like reading a clock. Suppose we start with the actual time as second 29 as maintained by the ISR. Let the mainline code copy the 10s digit. Then let the ISR interrupt the process. Suppose the actual time is now second 30. When the ISR returns to the main code the second digit is captured as 0. Consequently, the main code thinks the actual time is 20 when in reality, the actual time is second 30. Note that this error it highly intermittent.

Use flag and mailbox synchronization for real-time systems

To prevent the error we could disable the global interrupts. However, this is highly undesirable in a real-time system as we run the risk of missing an event. Instead, we will focus on a solution involving a flag (rudimentary semaphore) and a mailbox. The flag is a request for information. The mailbox is the protected memory location used for atomic transfer.

The result is very simple, the main code will request data from the ISR via a flag. For our 8-bit machine the flag is a single volatile global variable. The atomic (indivisible) flag is set by the main loop. The ISR recognizes that the flag is set. It responds by placing the data into the appropriate volatile global memory location. The ISR then clears the flag.

Meanwhile, the main code enters a while loop waiting for the flag to clear. We see this while 1 delay in this following function as it waits for the quad_1_flag to clear. Observe that we have added a timeout feature to prevent blocking code.

int32_t get_quad_1(uint32_t timeoutms) {
    int32_t last_known_position;
    int32_t startMillis = millis();

    if (!quad_1_flag) { // Retrieve the latest data if no request is pending
        last_known_position = quad_1_mailbox;
    }

    quad_1_flag = 1; // Request new data. This line is redundant if the flag is already set from a previous iteration.
    while (quad_1_flag) {
        if (millis() - startMillis > timeoutms) {
            return last_known_position;
        }
    }

    return quad_1_mailbox;
}

Careful analysis of the get_quad_1 function reveals a workaround for the asynchronous nature of the ISR and main line code. Note that there is no guarantee that the ISR will be called when the main line code wants an update. Rather than waiting for the ISR, the function will timeout and return the last know position. This prevents blocking code when the motor is turning very slowly.

Tech Tip: A good compiler will examine your code and take shortcuts. Depending on the settings, the resulting machine code may be shorter (less ROM used) or faster by unwinding loops inserting small function directly into the code. For microcontrollers such as the ATMega4809, the compiler can also optimize code by using local working registers instead of accessing RAM. Either way, the machine code running on the microcontroller is a shadow of the original program.

The compiler is not specifically aware of variables that are shared between the ISR and main loop. It may take optimizations shortcuts with the variable that break the association between the two parts of you program. We must declare shared variables as volatile to prevent the compiler optimization.

Cooperative time scheduling between the ISR and main loop

In the previous section we explored method a method for reliable data transfer from the ISR to the main loop code. Before concluding, we should consider timing aspects of the ISR.

Recall that the ISR is an interruption. It is a hardware method to save context and switch to a specific section of code such as the ISR_QUAD1 routine featured in this article. No discussion about an ISR would be complete without considering the percentage of time the microcontroller spends in the ISR. Previously, we described the ISR as a foreground process and the main loop as a background process. The ISR is a high priority foreground process while the routine, lower priority, events are relegated to the background.

We must recognize that the microcontroller’s operation is dominated by the architecture and clock speed. The architecture (bit width and instruction types) determines the number of machine cycles required for a particular operation while the clock speed determines the to speed at which those operations are complete.

At any rate, the microcontroller is a constrained resource. There are only so many operations that can be performed. This becomes especially important when we consider that this resource is shared between the ISR and the main loop. The ISR must cooperate with the main loop.

How to measure the ISR time and percentage

As stated earlier, the ISR must be short, or it will cycle starve the main line code. Worse, a long ISR can consume all of the clock cycles preventing the main code from operating or in the case of a quadrature encoder, result in missed transitions with a floundering count.

An oscilloscope or logic analyzer may be used to analyze the timing of the ISR. The example included as Figure 4 shows the waveforms for the Pololu #4754 operating no-load at 13 VDC. There are three waveforms in this image including the A and B outputs of the quadrature encoder along with a pulse corresponding to the time spent in the ISR. Notice that the ISR is entered immediately following a transition of any A or B quadrature signals.

The Time_ISR pulse is easy to program. Simply set an I/O pin high when entering the ISR and then clear it immediately before exiting. The result provides a reasonable estimation of the ISR. In this example, the ISR is completed in approximately 3.8 us. Given an 85 us time between ISR calls (nearly 12 k impulses per second), the ISR consumes about 5 % of the total CPU cycles. This should qualify as a short ISR providing a good utilization of the microcontroller resources. Observe that this is a worst-case situation with the motor operating 1 VDC above rated voltage.

Figure 4: Waveform showing the quadrature encoder A and B signals along with a pulse showing the time spent in the ISR. Note that the ISR is entered on every change of A or B.

Tech Tip: There is a tradeoff between architecture and clock speed. The ARM Cortex is a Reduced Instruction Set Computer (RISC) architecture is a good example. RISC is in the ARM name, as it stands for Advanced RISC Machine (ARM). These microcontrollers were built for speed with simple instruction sets resulting in streamlined silicon. Whan compared to the rich commands in their distant cousins, the RISC machines must complete many more instructions for a given operation – but they do so with high clock speeds. Stated another way, there is more to a microcontroller than clock speed.

Importance of direct port manipulation in the ISR

The Arduino team has done a beautiful job abstracting the complex microcontroller registers, The result is the easy-to-use device proving an excellent introduction to microcontrollers. Unfortunately, Arduino is not fast as any given function such as digitalWrite() must work its way through layers of abstraction before the specific machine code may be generated. This time dependent abstraction becomes important when we are operating within the timing constraints of an ISR.

We can reduce the ISR time by using bare-metal programming. We lose all portability and Arduino based simplicity, but we gain time. For example, consider this line of code written specifically for the ATmega 4809:

  BA = VPORTD.IN & 0x03;

It assumes that the quadrature encoder inputs are connected directly to the lower two pins of the 4809’s port D. This single statement reads the ports (1 clock cycle) and then masks off all but the lower two bits (1 clock cycle). The result is very fast – near optimal solution for the 4809.

A review of the example code shown that several direct ATmega4809 operations were performed. This was important step to reduce the ISR time to 5 % of total cycles. Without this microcontroller specific operation, the ISR consumed nearly 20 % of the total time.

Tech Tip: Bare metal programming with low level interaction with a microcontroller’s Special Function Register (SFR) is an essential skill for embedded programmers. It requires deep exploration of a microcontroller’s datasheet and an understanding of the microcontroller architecture, memory structures, and hardware peripherals. Recall that Arduino abstracts these details providing an easy-to-use programming environment with consistency across all Arduino family members. At the same time, the Arduino IDE does allow direct access to the SFR s demonstrated in this note. This provides a good way for the beginner to explore the microcontroller without fully committing to a dedicated IDE. However, at some point in your education you should shift to a full IDE such as MPLAB for the ATMega4809. It is a necessary step to expand your talent stack. You will find that deep study of a specific microcontroller will allow you to better understand all microcontrollers and the implications for responsive real-time programming.

Conclusion

Interfacing a high-speed motor driven quadrature encoder to a microcontroller is not a trivial application. As we have seen, a motor operating at high speed demands the microcontroller’s attention once every 85 us (approximately 12 kHz). Without the use of the microcontroller’s dedicated interrupt vector and context saving this would be an extraordinary difficult task. It would complicate background tasks such as communications, human machine interface, and even process control difficult.

We have also seen that careful coordination is required between an ISR and main. This takes two forms including careful atomic transfer of variables as well as timing analysis. While there are sever different ways to safely transfer data, we limited our discussion to a rudimentary flag and mailbox synchronization. As for timing, we explored a simple I/O pin set while the ISR is active. We briefly explored methods to shorten the ISR code using direct port access as opposed to the Arduino abstraction.

While this engineering brief does provide a complete solution for a robust high-speed quadrature encoder, it only scratches the surface. For additional information you are encouraged to answer the questions that appear at the end of this note. You are encouraged to download the file and experiment with your own quadrature encoder. Also, stay tuned as I intend to post a related PID control article in the near future.

Finally, please post your questions and comments in the space below. What techniques do you recommend making the task easier and more readable.

Best Wishes,

APDahlen

Please follow this link for related Arduino education content.

Questions

  1. Define the term blocking and why it is so detrimental to reading fast signals such as the quadrature encoder.

  2. What is meant by “Arduino abstraction” when contrasted with the term “bare metal”?

  3. Why is the attached code “not portable?”

  4. Define the term atomic as applied in this article.

  5. Describe the operation of the flag and mailbox synchronization.

  6. Describe the concept of state as it relates to a microcontroller.

  7. Describe the concept of foreground and background.

  8. Identify techniques to minimize the number of ISR clock cycles.

  9. How can an ISR starve the main loop code?

Critical thinking questions

  1. Describe the importance of keeping an ISR short.

  2. Describe techniques to minimize the number of ISR cycles.

  3. Define Direct Memory Access (DMA) as it relates to a microcontroller. Contrast and compare the DMA to the traditional ISR. Could a DMA be used for this quadrature encoder application?

  4. Increasingly, modern microcontrollers are sold with integral FPGA fabric. How could VHDL and Verilog techniques be used in this quadrature encoder application?

  5. The 8051 microcontroller is an old but widely used architecture. Research and describe the unique 8051 hardware-based context switching mechanism.

  6. The Arduino Nano Every is an 8-bit microcontroller. How does your understanding of atomicity change when a 32-bit microcontroller is used?

  7. Occasionally, you will encounter time-critical code section that is bounded by code that disables and then enables the global (hardware) interrupt mechanism. Is this a good idea? What could happen in a system designed for real time operation?

  8. A real-time operating system allows an advanced microcontroller to simultaneously handle several tasks. Describe the term “context” at it applies to the operating system.

  9. Describe the purpose of semaphores and mutex in a real time operating system.

About the Author

Aaron Dahlen, LCDR USCG (Ret.), serves as an application engineer at DigiKey. He has a unique electronics and automation foundation built over a 27-year military career as a technician and engineer which was further enhanced by 12 years of teaching (interwoven). With an MSEE degree from Minnesota State University, Mankato, Dahlen has taught in an ABET accredited EE program, served as the program coordinator for an EET program, and taught component-level repair to military electronics technicians. Dahlen has returned to his Northern Minnesota home and thoroughly enjoys researching and writing articles such as this. LinkedIn | Aaron Dahlen - Application Engineer - DigiKey

1 Like