Skip to content

Quadrature Encoder Interface (QEI) with Rotary Encoder

This example demonstrates how to use the TM4C123's Quadrature Encoder Interface (QEI) module to read position, direction, and velocity from a rotary encoder such as the HW-040.

Overview

The TM4C123 has two dedicated QEI modules (QEI0 and QEI1) that automatically decode quadrature-encoded signals from rotary encoders. The QEI hardware tracks position and direction without CPU intervention, making it ideal for motor control, user interfaces (rotary knobs), and robotics applications.

Key Features:

  • Automatic quadrature signal decoding (Phase A and Phase B)
  • 32-bit position counter with configurable maximum value
  • Direction detection (clockwise/counter-clockwise)
  • Velocity capture using dedicated timer
  • Digital input filtering for noise immunity
  • Optional index pulse support for absolute positioning

Quadrature Encoding Basics

What is Quadrature Encoding?

Quadrature encoding uses two digital signals (Phase A and Phase B) that are 90° out of phase. By detecting which signal leads, the QEI module determines rotation direction:

Clockwise Rotation (Phase A leads Phase B):
Phase A:  ___┌───┐___┌───┐___
Phase B:  ______┌───┐___┌───
          →      →      →

Counter-Clockwise Rotation (Phase B leads Phase A):
Phase A:  ___┌───┐___┌───┐___
Phase B:  ┌───┐___┌───┐______
          ←      ←      ←

Applications:

  • Motor Feedback: Track motor shaft position and speed
  • User Input: Rotary knobs for volume, menu navigation
  • Robotics: Wheel odometry, joint position sensing
  • CNC Machines: Precise position control

QEI Counting Modes

The TM4C123 QEI can count in two modes:

Mode 1: PhA Edges Only (CAPMODE=0)

  • Counts only Phase A transitions
  • Resolution: 1× (1 count per encoder pulse)

Mode 2: PhA + PhB Edges (CAPMODE=1)Used in this example

  • Counts all transitions on both Phase A and Phase B
  • Resolution: 4× (4 counts per encoder pulse)
  • Provides maximum resolution
Encoder with 30 PPR (Pulses Per Revolution):
- CAPMODE=0: 30 counts/revolution
- CAPMODE=1: 120 counts/revolution (30 × 4)

Velocity Measurement

The QEI module includes a dedicated timer that captures rotation speed:

  1. Load Timer with sample period (e.g., 20ms for 50 Hz)
  2. Count edges during this window
  3. Store count in SPEED register when timer expires
  4. Reload and repeat automatically

Velocity Calculation:

RPM = (SPEED × 60 × Clock) / (Load × PPR × Edges)

For HW-040 (30 PPR) at 50 Hz sampling:
RPM = (SPEED × 60 × 50MHz) / (1,000,000 × 30 × 4)

Hardware Setup

Components Required

  • TM4C123GH6PM LaunchPad
  • HW-040 rotary encoder module (30 PPR)
  • Jumper wires

HW-040 Rotary Encoder Pinout

PinFunctionDescription
CLKPhase AQuadrature signal A
DTPhase BQuadrature signal B
SWSwitchPush button (optional)
+VCC3.3V or 5V power
GNDGroundGround connection

Wiring Connections

This example uses QEI1 module on Port C:

Encoder PinTM4C123 PinGPIO Function
CLK (Phase A)PC6PhA1 (QEI1 Phase A)
DT (Phase B)PC5PhB1 (QEI1 Phase B)
SW (Button)Any GPIODigital input (optional)
+ (VCC)3.3VPower
GNDGNDGround

Connection Notes

  • Always use 3.3V, not 5V, to avoid damaging the TM4C123
  • The HW-040 typically has built-in 10kΩ pull-up resistors
  • If using a bare encoder, enable internal pull-ups on the TM4C123
  • Keep wires short to minimize noise and signal integrity issues

Alternative QEI Module

The TM4C123 also has QEI0 available on:

  • PD6 (PhA0) and PD7 (PhB0)

Just change the code to use QEI0, GPIOD, and adjust pin definitions.

Code Implementation

This example reads position, angle, and RPM from the encoder and outputs the data via UART every 20ms (50 Hz).

UART Dependency

This example requires uart.h and uart.c from Experiment 9 for serial output. Make sure to include these files in your project.

c
#include "TM4C123.h"
#include "uart.h"
#include "main.h"
#include <stdio.h>
#include <stdint.h>

volatile uint32_t POS, COUNT, SPEED;
volatile int32_t ANGLE;

int main(void)
{
    char buffer[128];

    // ------------------ ENABLE CLOCKS ------------------
    SYSCTL->RCGCQEI  |= (1<<1);    // QEI1
    SYSCTL->RCGCGPIO |= (1<<2);    // GPIOC

    __asm__("NOP"); __asm__("NOP"); __asm__("NOP");

    // Initialize delay functions
    SysTick_Init();
    
    UART0_Init();

    // ------------------ CONFIGURE PC5, PC6 ------------------
    GPIOC->LOCK = 0x4C4F434B;
    GPIOC->CR  |= (PhA1 | PhB1);
    GPIOC->AFSEL |= (PhA1 | PhB1);
    GPIOC->PCTL &= ~(0xFF << 20);
    GPIOC->PCTL |=  (0x66 << 20);
    GPIOC->DEN |= (PhA1 | PhB1);

    // ------------------ CONFIGURE QEI1 ------------------
    QEI1->CTL = 0;

    // Enable: QEI, Filtration, CAPMODE (both PhA+PhB), velocity capture
    QEI1->CTL =
        (1<<0)  |   // ENABLE
        (1<<3)  |   // CAPMODE
        (1<<5)  |   // VELEN
        (1<<13);    // FILTEN

    QEI1->MAXPOS = CPR - 1;

    // Set velocity timer period
    QEI1->LOAD = LOAD_VAL;


    UART0_WriteString("QEI + UART ready...\r\n");

    // ------------------ MAIN LOOP ------------------
    while (1)
    {
        POS   = QEI1->POS;
        COUNT = QEI1->COUNT;   // current accumulator (during window)
        SPEED = QEI1->SPEED;   // last completed window
        ANGLE = (POS * 360) / CPR;

        // Integer RPM, safe for fast rotation
        uint32_t rpm_int = SPEED * RPM_FACTOR;

        sprintf(buffer,
                "POS=%u  ANGLE=%d deg  SPEED=%u  RPM=%du\r\n",
                POS, ANGLE, SPEED, rpm_int);

        UART0_WriteString(buffer);

        // Wait for next QEI velocity sample
        // (match SAMPLE_HZ)
        delay_ms(1000 / SAMPLE_HZ);
    }
}
h
#ifndef MAIN_H
#define MAIN_H

#include "TM4C123.h"
#include <stdint.h>

// System clock frequency
#define SystemCoreClock 50000000u
#define CYCLES_PER_US   (SystemCoreClock / 1000000u)

// QEI Pin definitions
#define PhA1 (1 << 6)  // PC6
#define PhB1 (1 << 5)  // PC5

// Encoder & QEI constants
#define CLOCK      50000000UL    // 50 MHz system clock
#define VELDIV     0             // no predivider
#define SAMPLE_HZ  50            // velocity sample frequency (Hz)
#define LOAD_VAL   (CLOCK / SAMPLE_HZ)  // 20ms window
#define PPR        30            // HW-040 pulses per rev
#define EDGES      4             // CAPMODE=1 counts both PhA+PhB edges
#define CPR        (PPR * EDGES) // Counts per revolution

// Precomputed RPM factor (integer-safe)
#define RPM_FACTOR ((60UL * (1UL << VELDIV) * CLOCK) / (LOAD_VAL * PPR * EDGES))

// Inline implementations for delay functions
static inline void SysTick_Init(void)
{
    SysTick->CTRL = 0;
    SysTick->LOAD = CYCLES_PER_US - 1;  // 1us delay at 50MHz
    SysTick->VAL = 0;
    SysTick->CTRL = 0x5;     // Enable with system clock
}

static inline void delay_us(int us)
{
    SysTick->LOAD = (CYCLES_PER_US * us) - 1;
    SysTick->VAL = 0;
    SysTick->CTRL = 0x5; // Enable with system clock
    while ((SysTick->CTRL & 0x10000) == 0);
    SysTick->CTRL = 0;
}

static inline void delay_ms(int ms)
{
    while (ms--)
        delay_us(1000);
}

#endif // MAIN_H

Understanding the Code

1. Clock and GPIO Initialization

c
SYSCTL->RCGCQEI  |= (1<<1);    // Enable QEI1 module clock
SYSCTL->RCGCGPIO |= (1<<2);    // Enable Port C clock

Both the QEI module and GPIO port must be enabled before configuration.

2. GPIO Alternate Function Configuration

c
GPIOC->AFSEL |= (PhA1 | PhB1);     // Enable alternate function
GPIOC->PCTL &= ~(0xFF << 20);      // Clear PC5 and PC6 PCTL bits
GPIOC->PCTL |=  (0x66 << 20);      // Assign QEI function (value = 6)
GPIOC->DEN |= (PhA1 | PhB1);       // Enable digital function

Configures PC5 and PC6 to receive QEI signals instead of regular GPIO.

PCTL Calculation:

  • PC5 uses bits [23:20] = 0x6 (shifted left by 20)
  • PC6 uses bits [27:24] = 0x6 (shifted left by 24)
  • Combined: 0x66 << 20

3. QEI Control Register Configuration

c
QEI1->CTL = (1<<0)  |   // ENABLE: Enable QEI operation
            (1<<3)  |   // CAPMODE: Count PhA + PhB edges (4× resolution)
            (1<<5)  |   // VELEN: Enable velocity capture
            (1<<13);    // FILTEN: Enable digital filtering

Control Bits Explained:

  • ENABLE (bit 0): Turns on the QEI module
  • CAPMODE (bit 3): Enables 4× resolution counting
  • VELEN (bit 5): Activates velocity timer
  • FILTEN (bit 13): Reduces noise on encoder inputs

4. Position Counter Configuration

c
QEI1->MAXPOS = CPR - 1;    // Set maximum position (120 - 1 = 119)

CPR (Counts Per Revolution) = PPR × EDGES = 30 × 4 = 120

The position counter wraps around:

  • Clockwise: 0 → 1 → ... → 119 → 0
  • Counter-clockwise: 0 → 119 → 118 → ... → 0

5. Velocity Timer Configuration

c
QEI1->LOAD = LOAD_VAL;    // 50 MHz / 50 Hz = 1,000,000

Sets the velocity capture window:

  • 50 Hz sampling = 20ms window
  • LOAD_VAL = 50,000,000 / 50 = 1,000,000 clock cycles

6. Reading QEI Values

c
POS   = QEI1->POS;      // Current position (0 to CPR-1)
COUNT = QEI1->COUNT;    // Current velocity accumulator
SPEED = QEI1->SPEED;    // Last completed velocity sample
ANGLE = (POS * 360) / CPR;  // Convert to degrees

Register Differences:

  • POS: Instantaneous position counter
  • COUNT: Active velocity measurement (current window)
  • SPEED: Captured velocity from previous window (used for RPM)

7. RPM Calculation

c
uint32_t rpm_int = SPEED * RPM_FACTOR;

RPM_FACTOR is precomputed in main.h:

c
RPM_FACTOR = (60 × Clock) / (LOAD_VAL × PPR × EDGES)
           = (60 × 50,000,000) / (1,000,000 × 30 × 4)
           = 25

Formula Derivation:

RPM = (Edges per Window × 60) / (PPR × Edges × Time)
    = (SPEED × 60 × Clock) / (LOAD × PPR × EDGES)

Modifying QEI Parameters

Change Encoder Resolution (PPR)

Different encoders have different pulses per revolution:

c
#define PPR 100   // Higher resolution encoder (e.g., E6B2-CWZ6C)
#define PPR 600   // Industrial encoder
#define PPR 1024  // Precision encoder

Note: Update CPR and RPM_FACTOR automatically recalculate in main.h.

Change Sampling Frequency

c
#define SAMPLE_HZ  10    // 100ms updates - slower, less CPU
#define SAMPLE_HZ  50    // 20ms updates - balanced ✅ Used in example
#define SAMPLE_HZ  100   // 10ms updates - faster response

Higher sampling rates provide:

  • ✅ Faster response to speed changes
  • ✅ Better RPM accuracy at high speeds
  • ❌ More CPU/UART overhead

Disable Velocity Capture (Position Only)

If you only need position tracking:

c
QEI1->CTL = (1<<0) |    // ENABLE
            (1<<3) |    // CAPMODE
            (1<<13);    // FILTEN
// Remove VELEN bit

Use Index Pulse (Absolute Reference)

Some encoders have an index pulse (one pulse per revolution):

c
// Connect index to PC4 (IDX1)
QEI1->CTL |= (1<<4);    // RESMODE: Reset on index pulse

// Reset position to 0 on each revolution

References