Intermediate Encoder

Encoder Reading

Read quadrature encoders for precise position and velocity tracking

Overview

Quadrature encoders provide precise position feedback using two phase-shifted signals (A and B). This example demonstrates:

  • Encoder configuration - Setting up quadrature inputs
  • Position tracking - Reading absolute and relative position
  • Velocity calculation - Computing speed from position changes
  • Direction detection - Determining rotation direction
  • Resolution modes - 1x, 2x, and 4x counting

Hardware Setup

Rotary Encoder

  • • Quadrature rotary encoder
  • • A phase → Pin 10
  • • B phase → Pin 11
  • • VCC → +3.3V or +5V
  • • GND → GND

Motor Encoder

  • • Motor with built-in encoder
  • • Encoder A → Pin 12
  • • Encoder B → Pin 13
  • • Power and ground as required
  • • Pull-up resistors if needed

💡 Tip: Most encoders need pull-up resistors (1-10kΩ) on the A and B signals for reliable operation.

Basic Encoder Reading

use pokeys_lib::*;
use std::{thread, time::Duration};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("🔄 PoKeys Encoder Reading Example");
    println!("=================================");
    
    // Connect to device
    let device_count = enumerate_usb_devices()?;
    if device_count == 0 {
        println!("❌ No PoKeys devices found!");
        return Ok(());
    }
    
    let mut device = connect_to_device(0)?;
    println!("✅ Connected to: {}", device.get_device_name()?);
    
    // Configure encoder 0 on pins 10 and 11 with 4x sampling.
    // Normal (per-encoder) EncoderOptions does not have an invert_direction
    // field — swap pins A and B if you need to reverse direction, or use the
    // fast / ultra-fast encoder APIs which expose a hardware invert flag.
    let encoder_options = EncoderOptions::with_4x_sampling();

    device.configure_encoder(0, 10, 11, encoder_options)?;
    println!("⚙️  Configured encoder 0 on pins 10 (A) and 11 (B)");
    
    // Reset encoder position to zero
    device.reset_encoder(0)?;
    println!("🔄 Reset encoder position to zero");
    
    println!("📊 Encoder readings (turn the encoder):");
    println!("Time | Position | Delta | Direction | Speed (counts/s)");
    println!("-----|----------|-------|-----------|----------------");
    
    let mut last_position = 0i32;
    let mut last_time = std::time::Instant::now();
    
    for i in 0..200 {
        // Read current encoder position
        let current_position = device.get_encoder_value(0)?;
        let current_time = std::time::Instant::now();
        
        // Calculate delta and speed
        let delta = current_position - last_position;
        let time_diff = current_time.duration_since(last_time).as_secs_f32();
        let speed = if time_diff > 0.0 { delta as f32 / time_diff } else { 0.0 };
        
        // Determine direction
        let direction = match delta {
            d if d > 0 => "CW ",
            d if d < 0 => "CCW",
            _ => "---",
        };
        
        println!("{:>4} | {:>8} | {:>5} | {:>9} | {:>13.1}", 
                 i, current_position, delta, direction, speed);
        
        last_position = current_position;
        last_time = current_time;
        
        thread::sleep(Duration::from_millis(100));
    }
    
    println!("🎉 Encoder reading completed!");
    Ok(())
}

Multi-Encoder Position Control

use pokeys_lib::*;
use std::{collections::VecDeque, thread, time::{Duration, Instant}};

struct EncoderTracker {
    id: u8,
    name: String,
    position: i32,
    velocity_samples: VecDeque<f32>,
    last_position: i32,
    last_time: Instant,
    counts_per_revolution: i32,
}

impl EncoderTracker {
    fn new(id: u8, name: &str, counts_per_revolution: i32) -> Self {
        Self {
            id,
            name: name.to_string(),
            position: 0,
            velocity_samples: VecDeque::new(),
            last_position: 0,
            last_time: Instant::now(),
            counts_per_revolution,
        }
    }
    
    fn update(&mut self, device: &mut Device) -> Result<(), Box<dyn std::error::Error>> {
        let current_time = Instant::now();
        self.position = device.get_encoder_value(self.id)?;
        
        // Calculate velocity
        let delta_pos = self.position - self.last_position;
        let delta_time = current_time.duration_since(self.last_time).as_secs_f32();
        
        if delta_time > 0.0 {
            let velocity = delta_pos as f32 / delta_time;
            self.velocity_samples.push_back(velocity);
            
            // Keep only last 10 samples for smoothing
            if self.velocity_samples.len() > 10 {
                self.velocity_samples.pop_front();
            }
        }
        
        self.last_position = self.position;
        self.last_time = current_time;
        Ok(())
    }
    
    fn get_average_velocity(&self) -> f32 {
        if self.velocity_samples.is_empty() {
            return 0.0;
        }
        let sum: f32 = self.velocity_samples.iter().sum();
        sum / self.velocity_samples.len() as f32
    }
    
    fn get_revolutions(&self) -> f32 {
        self.position as f32 / self.counts_per_revolution as f32
    }
    
    fn get_rpm(&self) -> f32 {
        (self.get_average_velocity() / self.counts_per_revolution as f32) * 60.0
    }
}

fn multi_encoder_example() -> Result<(), Box<dyn std::error::Error>> {
    let mut device = connect_to_device(0)?;
    
    // Configure multiple encoders with 4x sampling.
    let encoder_options = EncoderOptions::with_4x_sampling();

    // Encoder 0: Rotary knob (24 counts/rev)
    device.configure_encoder(0, 10, 11, encoder_options)?;
    device.reset_encoder(0)?;
    
    // Encoder 1: Motor encoder (1000 counts/rev)
    device.configure_encoder(1, 12, 13, encoder_options)?;
    device.reset_encoder(1)?;
    
    println!("🔄 Multi-Encoder Position Control");
    println!("=================================");
    
    let mut knob = EncoderTracker::new(0, "Rotary Knob", 24);
    let mut motor = EncoderTracker::new(1, "Motor", 1000);
    
    println!("Knob: Pos | Rev | RPM  || Motor: Pos  | Rev  | RPM");
    println!("----------|-----|------||-------------|------|----");
    
    for _ in 0..300 {
        // Update both encoders
        knob.update(&mut device)?;
        motor.update(&mut device)?;
        
        println!("{:>9} |{:>4.1} |{:>5.0} || {:>10} |{:>5.2} |{:>4.0}",
                 knob.position,
                 knob.get_revolutions(),
                 knob.get_rpm(),
                 motor.position,
                 motor.get_revolutions(),
                 motor.get_rpm());
        
        thread::sleep(Duration::from_millis(100));
    }
    
    Ok(())
}

Quadrature Encoder Theory

Signal Phases

Quadrature encoders generate two square wave signals (A and B) that are 90° out of phase:

  • Clockwise: A leads B
  • Counter-clockwise: B leads A
  • Resolution: Depends on counting mode

Counting Modes

  • 1x: Count A edges only
  • 2x: Count A rising and falling
  • 4x: Count all A and B edges
  • 4x mode provides highest resolution

Resolution Calculation

// For a 1000 PPR (Pulses Per Revolution) encoder:
// 1x mode: 1000 counts/revolution
// 2x mode: 2000 counts/revolution  
// 4x mode: 4000 counts/revolution

let degrees_per_count = 360.0 / (ppr * multiplier);
let position_degrees = encoder_counts * degrees_per_count;

Common Applications

Position Control

// Simple position control loop
let target_position = 1000; // counts
let current_position = device.get_encoder_value(0)?;
let error = target_position - current_position;

if error.abs() > 5 { // deadband
    let pwm_output = (error as f32 * 0.1).clamp(-100.0, 100.0);
    device.set_pwm_duty_cycle(4, pwm_output.abs())?;
    
    // Set direction pin based on error sign
    device.set_digital_output(5, error > 0)?;
}

Velocity Control

// PID velocity control
let target_rpm = 100.0;
let current_rpm = calculate_rpm(&encoder_tracker);
let error = target_rpm - current_rpm;

pid_controller.update(error);
let output = pid_controller.get_output();

device.set_pwm_duty_cycle(4, output.abs())?;
device.set_digital_output(5, output > 0.0)?;

Troubleshooting

Encoder not counting

  • • Check power supply to encoder
  • • Verify A and B signal connections
  • • Add pull-up resistors (1-10kΩ)
  • • Check signal voltage levels (should be 0-3.3V or 0-5V)

Wrong direction

  • • Normal (per-encoder) encoders: swap A and B signal connections or swap the channel pins in configure_encoder
  • • Fast encoders: set invert_direction_1/_2/_3 in FastEncoderOptions
  • • Ultra-fast encoder: set invert_direction in UltraFastEncoderOptions

Missed counts at high speed

  • • Use shorter, shielded cables
  • • Add hardware filtering (RC filter)
  • • Reduce maximum rotation speed
  • • Check for electrical noise sources

Next Steps