saltylab-firmware/include/encoder_odom.h
sl-controls 779f9d00e2 feat: Encoder odometry and wheel speed feedback (Issue #632)
- TIM2 (32-bit) left encoder, TIM3 (16-bit) right encoder in mode 3
- RPM calculation with int16 clamp; 16-bit wrap handled via signed delta
- Differential-drive odometry: x/y/theta Euler-forward integration
- Flash config (sector 7, 0x0807FF00) for ticks_per_rev/wheel_diam/base
- JLINK_TLM_ODOM (0x8C) at 50 Hz: rpm_l/r, x_mm, y_mm, theta_cdeg, speed_mmps
- 75/75 unit tests passing (TEST_HOST build)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 16:34:38 -04:00

152 lines
5.8 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#ifndef ENCODER_ODOM_H
#define ENCODER_ODOM_H
#include <stdint.h>
#include <stdbool.h>
/*
* encoder_odom — quadrature encoder reading and differential-drive odometry
* (Issue #632).
*
* HARDWARE:
* Left encoder : TIM2 (32-bit) in encoder mode 3
* CH1 = PA15 (AF1), CH2 = PB3 (AF1)
* Right encoder : TIM3 (16-bit) in encoder mode 3
* CH1 = PC6 (AF2), CH2 = PC7 (AF2)
*
* Both channels count on every edge (×4 resolution).
* TIM2 ARR = 0xFFFFFFFF (32-bit, never overflows in practice).
* TIM3 ARR = 0xFFFF (16-bit, delta decoded via int16_t subtraction).
*
* ODOMETRY MODEL (differential drive):
*
* meters_per_tick = (π × wheel_diam_mm × 1e-3) / ticks_per_rev
*
* d_left = Δticks_left × meters_per_tick
* d_right = Δticks_right × meters_per_tick
*
* d_center = (d_left + d_right) / 2
* dθ = (d_right - d_left) / wheel_base_mm × 1e-3 (radians)
*
* x += d_center × cos(θ)
* y += d_center × sin(θ)
* θ += dθ
*
* For small dt this is the standard Euler-forward integration; suitable for
* the 50 Hz odometry tick rate.
*
* RPM:
* rpm = Δticks × 60.0 / (ticks_per_rev × dt_s)
*
* FLASH CONFIG (ENC_FLASH_ADDR in sector 7):
* Stores ticks_per_rev, wheel_diam_mm, wheel_base_mm validated by magic.
* Falls back to compile-time defaults on magic mismatch.
* Sector 7 is shared with PID flash; saving encoder config must be
* coordinated with pid_flash_save_all() to avoid mutual erasure.
*
* TELEMETRY:
* JLINK_TLM_ODOM (0x8C) published at ENC_TLM_HZ (50 Hz):
* jlink_tlm_odom_t { int16 rpm_left, int16 rpm_right,
* int32 x_mm, int32 y_mm,
* int16 theta_cdeg, int16 speed_mmps }
* 16 bytes, 22-byte frame.
*/
/* ---- Default hardware parameters (override in flash config) ---- */
/* Hoverboard 6.5" wheels with typical geared-motor encoder: */
#define ENC_TICKS_PER_REV_DEFAULT 1320u /* 33 CPR × 40:1 gear = 1320 ticks/rev */
#define ENC_WHEEL_DIAM_MM_DEFAULT 165u /* 6.5" ≈ 165 mm diameter */
#define ENC_WHEEL_BASE_MM_DEFAULT 540u /* ~540 mm axle-to-axle separation */
/* ---- Flash config ---- */
/* Stored in sector 7 immediately before the PID schedule area (0x0807FF40).
* 64-byte block: magic(4) + config(12) + pad(48). */
#define ENC_FLASH_ADDR 0x0807FF00UL
#define ENC_FLASH_MAGIC 0x534C4503UL /* 'SLE\x03' — encoder config v3 */
typedef struct __attribute__((packed)) {
uint32_t magic; /* ENC_FLASH_MAGIC when valid */
uint32_t ticks_per_rev; /* encoder ticks per full wheel revolution */
uint16_t wheel_diam_mm; /* wheel outer diameter (mm) */
uint16_t wheel_base_mm; /* lateral wheel separation centre-to-centre (mm) */
uint8_t _pad[48]; /* reserved — total 64 bytes */
} enc_flash_config_t;
/* ---- Runtime configuration ---- */
typedef struct {
uint32_t ticks_per_rev;
uint16_t wheel_diam_mm;
uint16_t wheel_base_mm;
} enc_config_t;
/* ---- Runtime state ---- */
typedef struct {
/* Encoder counters (last sampled) */
uint32_t cnt_left; /* last TIM2->CNT */
uint16_t cnt_right; /* last TIM3->CNT */
/* Wheel speeds */
int16_t rpm_left; /* left wheel RPM (signed; + = forward) */
int16_t rpm_right; /* right wheel RPM (signed) */
int16_t speed_mmps; /* linear speed of centre point (mm/s) */
/* Pose (relative to last reset) */
float x_mm; /* forward displacement (mm) */
float y_mm; /* lateral displacement (mm, + = left) */
float theta_rad; /* heading (radians, + = CCW from start) */
/* Internal */
float meters_per_tick; /* pre-computed from config */
float wheel_base_m; /* wheel_base_mm / 1000.0 */
uint32_t last_tick_ms; /* HAL_GetTick() at last encoder_odom_tick() */
uint32_t last_tlm_ms; /* HAL_GetTick() at last TLM transmission */
enc_config_t cfg; /* active hardware parameters */
} encoder_odom_t;
/* ---- Configuration ---- */
#define ENC_TLM_HZ 50u /* JLINK_TLM_ODOM transmit rate (Hz) */
/* ---- API ---- */
/*
* encoder_odom_init(eo) — configure TIM2/TIM3 in encoder mode, load flash
* config (falling back to defaults), reset pose.
* Call once during system init.
*/
void encoder_odom_init(encoder_odom_t *eo);
/*
* encoder_odom_tick(eo, now_ms) — sample encoder counters, update RPM and
* integrate odometry. Call from main loop at any rate ≥ 10 Hz (50 Hz ideal).
*/
void encoder_odom_tick(encoder_odom_t *eo, uint32_t now_ms);
/*
* encoder_odom_reset_pose(eo) — zero x/y/theta without resetting counters or
* config. Call whenever odometry reference frame should be re-anchored.
*/
void encoder_odom_reset_pose(encoder_odom_t *eo);
/*
* encoder_odom_save_config(cfg) — write enc_flash_config_t to ENC_FLASH_ADDR.
* WARNING: erases sector 7 — must NOT be called while armed and must be
* coordinated with PID flash saves (both records are in sector 7).
* Returns true on success.
*/
bool encoder_odom_save_config(const enc_config_t *cfg);
/*
* encoder_odom_load_config(cfg) — load config from flash.
* Returns true if flash magic valid; false = defaults applied to *cfg.
*/
bool encoder_odom_load_config(enc_config_t *cfg);
/*
* encoder_odom_send_tlm(eo, now_ms) — transmit JLINK_TLM_ODOM (0x8C) frame.
* Rate-limited to ENC_TLM_HZ; safe to call every tick.
*/
void encoder_odom_send_tlm(const encoder_odom_t *eo, uint32_t now_ms);
#endif /* ENCODER_ODOM_H */