Source code for drivers.encoder

from pyb import Pin, Timer
from utime import ticks_us, ticks_diff   # used for dt calculation in update()
from constants import *
import gc

UINT16_MAX = 0xFFFF

[docs] class Encoder: """Quadrature encoder interface wrapper. - Uses a hardware Timer configured in ENC_AB mode with two channels (channel 1 -> A, channel 2 -> B). - Tracks total accumulated encoder position accounting for timer reloads (overflow / underflow). - Provides `update()` to sample the hardware counter and compute: - position: total encoder counts (signed, extended across reloads) - delta: change in position since last update (counts) - dt: elapsed time since last update (milliseconds, float) - Convenience methods: - get_position() -> wheel linear position (mm) - get_velocity() -> linear velocity (mm/s) - zero() -> reset the encoder zero (tare) """ def __init__(self, tim, chA_pin, chB_pin): """ Create and configure timer channels for quadrature decoding. Parameters (comment-style hints, safe for MicroPython): tim -- pyb.Timer instance already created (Timer(N)) chA_pin -- pin identifier for channel A (connected to timer CH1) chB_pin -- pin identifier for channel B (connected to timer CH2) """ # Configure A/B pins as inputs pin_a = Pin(chA_pin, Pin.IN) pin_b = Pin(chB_pin, Pin.IN) # Hold reference to timer and initialize for encoder mode. # We set period to 0xFFFF to allow full 16-bit counting and register # a callback to detect reloads (overflow/underflow). self.timer = tim self.timer.init(period=UINT16_MAX, prescaler=0, callback=self._callback) # Attach timer channels for encoder A/B inputs self.timer.channel(1, pin=pin_a, mode=Timer.ENC_AB) self.timer.channel(2, pin=pin_b, mode=Timer.ENC_AB) # State variables self.position = 0 # Total accumulated position (extended counts) self.prev_count = 0 # Last recorded extended count (for delta) self.prev_ticks = ticks_us() # Last timestamp in microseconds (0 initially) self.delta = 0 # Change in extended counts since last update self.dt = 0 # Elapsed time since last update (ms, float) # Timer reload bookkeeping: # increment when timer wrapped from 0xFFFF -> 0 (overflow) # decrement when timer wrapped from 0 -> 0xFFFF (underflow) self.timer_reloads = 0 def _callback(self, timer_obj): """ Timer callback invoked on timer reload. The hardware timer invokes this callback when it reloads. We track whether a reload represented an overflow or an underflow by sampling the timer counter value and adjusting `self.timer_reloads` accordingly. A try/except is used because, on some builds/hardware, reading the timer inside the callback has occasionally raised unexpected errors. """ # The original code swallowed those exceptions; we preserve that behavior. try: cnt = timer_obj.counter() # Overflow: counter wrapped 0xFFFF -> 0 (observed value is 0) if cnt == 0: self.timer_reloads += 1 # Underflow: counter wrapped 0 -> 0xFFFF (observed value is 0xFFFF) elif cnt == 0xFFFF: self.timer_reloads -= 1 except Exception: # Preserve original behavior: ignore occasional read errors. # (TODO: investigate root cause separately) pass
[docs] def update(self): """ Sample the hardware counter and update position/delta/dt. - Reads the raw 16-bit counter - Extends it using `self.timer_reloads` to build a monotonic `position` - Computes `delta = position - prev_count` - Updates `prev_count` and computes elapsed time `dt` (ms) """ # Read raw 16-bit counter (0..65535) raw_count = self.timer.counter() # Extend the raw counter across reloads: position is raw_count + # UINT16_MAX * number_of_reloads. This preserves sign/monotonicity. self.position = raw_count + (UINT16_MAX * self.timer_reloads) # Delta (change in extended counts) since last update self.delta = self.position - self.prev_count # Save current extended count for next iteration self.prev_count = self.position # Time delta in milliseconds (float) now_us = ticks_us() self.dt = ticks_diff(now_us, self.prev_ticks) / 1000.0 self.prev_ticks = now_us
[docs] def get_position(self): """ Convert the most recently-updated encoder `position` (counts) into a linear wheel displacement in millimeters. Formula: rotations = counts / ECOUNTS_PER_WREV distance = rotations * (2 * pi * wheel_radius) """ return (self.position / ECOUNTS_PER_WREV) * 2 * PI * WHEEL_RADIUS
[docs] def get_velocity(self): """ Compute linear velocity (mm/s) using the most recent `delta` and `dt`. Returns 0 if dt is zero (to avoid division by zero). The computation follows:: counts_per_ms = delta / dt rotations_per_s = (counts_per_ms * 1000) / ECOUNTS_PER_WREV velocity_mm_s = rotations_per_s * (2 * pi * wheel_radius) """ if self.dt == 0: return 0 ecounts_per_ms = self.delta / self.dt return (ecounts_per_ms * 1e3 / ECOUNTS_PER_WREV) * 2 * PI * WHEEL_RADIUS
[docs] def zero(self): """ Tare / zero the encoder. - Reset reload counters and prev_count - Reset the hardware timer counter to 0 - Call update() so subsequent reads are consistent with the new zero """ self.timer_reloads = 0 self.prev_count = 0 self.position = 0 # Reset hardware counter self.timer.counter(0) # Refresh derived state so we don't return stale values immediately self.update()
import gc