Quadrature encoders with Raspberry Pi Pico PIO

When you want to control a DC motor, there are two ways in which this could be approached. It can be controlled (a closed-loop approach) or simply driven (an open-loop approach). The open loop approach is the easiest one since it does not relay on any addition knowledge about the system or the motor itself. The control process is straightforward. You send the control signal and hope that the DC motor will rotate at the desired speed. However, in many situations, it is insufficient. For example, when a car goes uphill, it requires more torque, so the power output needs to be adjusted. In the end, the same control signal will not give the same results as the environment changes.

In this post the main emphasis is put on quadrature encoders. The quadrature encoder is a sensor mounted next to motor which allows to measure how fast the shaft is turning. Going further, the usual implementation of the quadrature encoder sensor counts how many times the state has changed. However, before going into details, let us focus first on quadrature encoders and how they work.

Quadrature encoder principle

The quadrature encoder produces two signals as depicted in the image below. It could be said that channel A is shifted with phase by 90 degrees in reference to channel B. Both channels can be seen as two PWM signals with 50% duty, but shifted. When you concatenate the values of each channel as binary value you might notice that when changing from one state to the other only a single bit is being changed. This is a specific code called the Grey code. They are used to detect discrepancy in the signal coming from mechanical devices and also allow one to implement error correction.

Using quadrature encoders, you can determine how fast a motor is spinning and in which direction. To determine the speed, the easiest approach is to increment an internal counter each time when there is a change of channel A or channel B. In order to determine the direction, it is required to check in which state we were and to what state we have transitioned. The direction is conventional. As long as we do not change the principle, we will be able to determine if there was a direction change. In the above picture we can assume that the change of code is of the following order.

00 -> 01 -> 11 -> 10 -> 00

is in the forward direction, while

00 -> 10 -> 11 -> 01 -> 00

is in the opposite direction.

Downside of counting impulses

Let us assume that we have implemented the above-mentioned method. It is very efficient, but there is one problem with it. This method can be applied to situations where the motor shaft is spinning fast. Usually, we would assume a given time frame and count how many events (state changes) we have registered. To determine the velocity we would then divide it by the length of time window like

v = count / dt,

where count is the number of impulses while dt is the time length.

Immediately, one can see that there is a problem. When we want to update the velocity frequently, we would need to decrease the time window. This, in turn, decreases the resolution of our measurement. It is not a problem if the amount of impulses in given time frame is high, but it is noticeable issue for slow spinning motors. Is there any way this could be improved? Yes, there is!

Time gap measurement

Instead of calculating how many impulses there were, it is better to calculate how long apart from each other they have happened. When a motor is spinning slowly, producing an impulse every 2.1 seconds, we can precisely determine with what speed it is (should be) spinning. With previous approach, while assuming that the time window is equal to 2 seconds, we would have information that the motor is stopped and it is spinning slowly alternately.

Of course, as pointed out, this method works if the environment does not change significantly, and we do not change the control signal. However, let us assume that instead of detecting an impulse every 2.1 seconds, we detect them every 2.1 millisecond. This considerably changes the situation. A common approach to benefit from those two methods is to switch between them depending on given situation. In other words, we would introduce switch hysteresis. When the impulses are short apart from each other, we could switch to the first method. Consequently, when the amount of impulses is below set threshold, we could switch back to the second technique.

PIO in RP2040

A great way to implement both techniques efficiently is by using the PIO peripheral (Programmable Input Output) available on Raspberry Pi Pico microcontroller, RP2040. It can be perceived as a programmable state machine which runs separately to main core, thus relieves it from computations.

Here, the second technique with time gap measurement is presented. The example is implemented in microPython. It uses interrupts every time there is a change in the quadrature encoder’s lines.

@asm_pio()
def encoder_pio():
    set(x, 0)
    set(y, 0)
    mov(isr, x)
    label("start")
    mov(isr,null)
    in_(pins, 2)
    mov(x,isr)
    jmp(x_not_y, "change")
    jmp("start")
    label("change")
    mov(y,x)
    push()
    irq(noblock,rel(0))
    jmp("start")
    wrap()

The machine stores the state of the encoder channels and every time it detects a change, it saves the current state followed by the activation of the interrupt. To make it a bit more usable, let us create an Encoder class which would handle the computations.

class Encoder:
    FORWARD = 0
    BACKWARD = 1
    FAULT = 2
    def __init__(self, ID=0):
        self._last_time = 0
        self._time = 0        
        self._diff = 0
        self._id = ID
        
        self._dir_forward = (2, 0, 3, 1) # 0 -> 2 -> 3 -> 1 -> 0
        self._dir_backward = (1, 3, 0, 2) # 1 -> 3 -> 2 -> 0 -> 1
        self._last_state = 0
        self._state = 0
        self._dir = Encoder.FORWARD
        
    def update_state(self, next_state):
        self._last_state = self._state
        self._state = next_state & 0x03
        
    def update_dir(self):
        if self._dir_forward[self._last_state] == self._state:
            self._dir = Encoder.FORWARD
        elif self._dir_backward[self._last_state] == self._state:
            self._dir = Encoder.BACKWARD
        else:
            self._dir = Encoder.FAULT
        return self._dir
    
    @property
    def dir(self):
        return self._dir
        
    
    def update_time(self, new_time):
        self._last_time = self._time
        self._time = new_time
        
    def update_diff(self):
        self._diff = time.ticks_diff(self._time, self._last_time)
        return self._diff
        
    @property
    def diff(self):
        return self._diff
    
    @micropython.native
    def irq_handler(self, sm):
        t = time.ticks_us()
        self.update_time(t)
        self.update_diff()
        self.update_state(sm.get())
        self.update_dir()
        
    @property
    def state(self):
        return self._state

Look a bit closer at the irq_handler() method.

  @micropython.native
    def irq_handler(self, sm):
        t = time.ticks_us()
        self.update_time(t)
        self.update_diff()
        self.update_state(sm.get())
        self.update_dir()

This method will be called every time there is encoder’s state change. What it does is as follows. It takes current time expressed in microseconds, and after that it updates the internal time so we know when the previous change has occurred and when the current change has occurred. Then a derivative is being calculated which in fact is nothing more than subtracting the time of the previous change from the time of the recent change. For this tick_diff() function is being used just to accommodate for time overflow. Next, we retrieve the current state of the quadrature encoder and then determine the rotation direction using current and previous state.

Now, we can create an object and initialise the PIO state machine like this.

    enc = StateMachine(0, encoder_pio, freq=100_000, in_base=Pin(2))
    enc.irq(enc.irq_handler)
    enc.active(1)

What is important are the microcontroller pins, which are connected to the quadrature encoder. It is important to connect two adjacent pins and pass the pin with a lower number during the configuration. In the above example pins number 2 and 3 would be used. This is due to the way how IN instruction from PIO is implemented. It reads in the value of subsequent pins given the input base pin.

To retrieve speed we can get the calculated derivative from Encoder object

value = enc.diff

Applying some coefficient to scale the value of the derivative, we can measure motor speed rotation. This is directly correlated with the resolution of the (rotary) quadrature encoder and gear ratio if the motor has one.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.