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:
- Load Timer with sample period (e.g., 20ms for 50 Hz)
- Count edges during this window
- Store count in SPEED register when timer expires
- 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
| Pin | Function | Description |
|---|---|---|
| CLK | Phase A | Quadrature signal A |
| DT | Phase B | Quadrature signal B |
| SW | Switch | Push button (optional) |
| + | VCC | 3.3V or 5V power |
| GND | Ground | Ground connection |
Wiring Connections
This example uses QEI1 module on Port C:
| Encoder Pin | TM4C123 Pin | GPIO Function |
|---|---|---|
| CLK (Phase A) | PC6 | PhA1 (QEI1 Phase A) |
| DT (Phase B) | PC5 | PhB1 (QEI1 Phase B) |
| SW (Button) | Any GPIO | Digital input (optional) |
| + (VCC) | 3.3V | Power |
| GND | GND | Ground |
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.
#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);
}
}#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_HUnderstanding the Code
1. Clock and GPIO Initialization
SYSCTL->RCGCQEI |= (1<<1); // Enable QEI1 module clock
SYSCTL->RCGCGPIO |= (1<<2); // Enable Port C clockBoth the QEI module and GPIO port must be enabled before configuration.
2. GPIO Alternate Function Configuration
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 functionConfigures 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
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 filteringControl 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
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
QEI1->LOAD = LOAD_VAL; // 50 MHz / 50 Hz = 1,000,000Sets the velocity capture window:
- 50 Hz sampling = 20ms window
- LOAD_VAL = 50,000,000 / 50 = 1,000,000 clock cycles
6. Reading QEI Values
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 degreesRegister Differences:
- POS: Instantaneous position counter
- COUNT: Active velocity measurement (current window)
- SPEED: Captured velocity from previous window (used for RPM)
7. RPM Calculation
uint32_t rpm_int = SPEED * RPM_FACTOR;RPM_FACTOR is precomputed in main.h:
RPM_FACTOR = (60 × Clock) / (LOAD_VAL × PPR × EDGES)
= (60 × 50,000,000) / (1,000,000 × 30 × 4)
= 25Formula 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:
#define PPR 100 // Higher resolution encoder (e.g., E6B2-CWZ6C)
#define PPR 600 // Industrial encoder
#define PPR 1024 // Precision encoderNote: Update CPR and RPM_FACTOR automatically recalculate in main.h.
Change Sampling Frequency
#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 responseHigher 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:
QEI1->CTL = (1<<0) | // ENABLE
(1<<3) | // CAPMODE
(1<<13); // FILTEN
// Remove VELEN bitUse Index Pulse (Absolute Reference)
Some encoders have an index pulse (one pulse per revolution):
// Connect index to PC4 (IDX1)
QEI1->CTL |= (1<<4); // RESMODE: Reset on index pulse
// Reset position to 0 on each revolutionReferences
- TM4C123 Datasheet - Section 14 (QEI Module)
- TM4C123 Data Sheet - Table 10-2 (GPIO Pins and Alternate Functions)
- Experiment 9: UART (for serial output)