- 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>
348 lines
10 KiB
C
348 lines
10 KiB
C
/*
|
||
* encoder_odom.c — quadrature encoder reading and differential-drive
|
||
* odometry for Issue #632.
|
||
*
|
||
* TIM2 (32-bit) = left encoder, TIM3 (16-bit) = right encoder.
|
||
* Both configured in encoder mode 3 (count on both A and B edges × 4).
|
||
*
|
||
* RPM formula:
|
||
* rpm = delta_ticks * 60 / (ticks_per_rev * dt_s)
|
||
*
|
||
* Odometry (Euler-forward differential drive):
|
||
* d_l = delta_left * meters_per_tick
|
||
* d_r = delta_right * meters_per_tick
|
||
* d_c = (d_l + d_r) / 2
|
||
* dθ = (d_r - d_l) / wheel_base_m
|
||
* x += d_c * cos(θ)
|
||
* y += d_c * sin(θ)
|
||
* θ += dθ
|
||
*/
|
||
|
||
#include "encoder_odom.h"
|
||
#include "jlink.h"
|
||
#include <math.h>
|
||
#include <string.h>
|
||
|
||
/* ---- Platform abstraction (stubbed in TEST_HOST builds) ---- */
|
||
|
||
#ifndef TEST_HOST
|
||
|
||
#include "stm32f7xx_hal.h"
|
||
#include "config.h"
|
||
|
||
/* Read 32-bit left encoder counter (TIM2) */
|
||
static inline uint32_t enc_read_left(void)
|
||
{
|
||
return (uint32_t)TIM2->CNT;
|
||
}
|
||
|
||
/* Read 16-bit right encoder counter (TIM3) */
|
||
static inline uint16_t enc_read_right(void)
|
||
{
|
||
return (uint16_t)TIM3->CNT;
|
||
}
|
||
|
||
/* Configure TIM2 in encoder mode (left wheel) */
|
||
static void enc_tim2_init(void)
|
||
{
|
||
/* Enable peripheral clock */
|
||
__HAL_RCC_TIM2_CLK_ENABLE();
|
||
__HAL_RCC_GPIOA_CLK_ENABLE();
|
||
__HAL_RCC_GPIOB_CLK_ENABLE();
|
||
|
||
/* PA15 = TIM2_CH1 (AF1), PB3 = TIM2_CH2 (AF1) — floating input */
|
||
GPIO_InitTypeDef g = {0};
|
||
g.Mode = GPIO_MODE_AF_PP;
|
||
g.Pull = GPIO_PULLUP;
|
||
g.Speed = GPIO_SPEED_FREQ_LOW;
|
||
g.Alternate = GPIO_AF1_TIM2;
|
||
|
||
g.Pin = GPIO_PIN_15;
|
||
HAL_GPIO_Init(GPIOA, &g);
|
||
|
||
g.Pin = GPIO_PIN_3;
|
||
HAL_GPIO_Init(GPIOB, &g);
|
||
|
||
/* Encoder mode 3: count on both TI1 and TI2 edges */
|
||
TIM2->CR1 = 0;
|
||
TIM2->SMCR = TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1; /* SMS = 0x3 */
|
||
TIM2->CCMR1 = (TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0); /* IC1→TI1, IC2→TI2 */
|
||
TIM2->CCER = 0; /* no polarity inversion (change CC1P/CC2P to reverse) */
|
||
TIM2->ARR = 0xFFFFFFFFUL;
|
||
TIM2->CNT = 0;
|
||
TIM2->CR1 |= TIM_CR1_CEN;
|
||
}
|
||
|
||
/* Configure TIM3 in encoder mode (right wheel) */
|
||
static void enc_tim3_init(void)
|
||
{
|
||
__HAL_RCC_TIM3_CLK_ENABLE();
|
||
__HAL_RCC_GPIOC_CLK_ENABLE();
|
||
|
||
/* PC6 = TIM3_CH1 (AF2), PC7 = TIM3_CH2 (AF2) */
|
||
GPIO_InitTypeDef g = {0};
|
||
g.Mode = GPIO_MODE_AF_PP;
|
||
g.Pull = GPIO_PULLUP;
|
||
g.Speed = GPIO_SPEED_FREQ_LOW;
|
||
g.Alternate = GPIO_AF2_TIM3;
|
||
|
||
g.Pin = GPIO_PIN_6 | GPIO_PIN_7;
|
||
HAL_GPIO_Init(GPIOC, &g);
|
||
|
||
TIM3->CR1 = 0;
|
||
TIM3->SMCR = TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1;
|
||
TIM3->CCMR1 = (TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0);
|
||
TIM3->CCER = 0;
|
||
TIM3->ARR = 0xFFFF;
|
||
TIM3->CNT = 0;
|
||
TIM3->CR1 |= TIM_CR1_CEN;
|
||
}
|
||
|
||
/* Flash helpers */
|
||
static bool flash_write_words(uint32_t addr, const void *data, uint32_t len)
|
||
{
|
||
const uint32_t *src = (const uint32_t *)data;
|
||
uint32_t words = (len + 3u) / 4u;
|
||
for (uint32_t i = 0; i < words; i++) {
|
||
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
|
||
addr + i * 4u, src[i]) != HAL_OK) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
static bool flash_erase_sector7(void)
|
||
{
|
||
FLASH_EraseInitTypeDef erase = {
|
||
.TypeErase = FLASH_TYPEERASE_SECTORS,
|
||
.Sector = FLASH_SECTOR_7,
|
||
.NbSectors = 1,
|
||
.VoltageRange = FLASH_VOLTAGE_RANGE_3,
|
||
};
|
||
uint32_t err = 0;
|
||
return (HAL_FLASHEx_Erase(&erase, &err) == HAL_OK) &&
|
||
(err == 0xFFFFFFFFUL);
|
||
}
|
||
|
||
#else /* TEST_HOST stubs */
|
||
|
||
/* Test-controlled counter values */
|
||
static uint32_t g_enc_left_cnt = 0;
|
||
static uint16_t g_enc_right_cnt = 0;
|
||
|
||
static inline uint32_t enc_read_left(void) { return g_enc_left_cnt; }
|
||
static inline uint16_t enc_read_right(void) { return g_enc_right_cnt; }
|
||
|
||
static void enc_tim2_init(void) {}
|
||
static void enc_tim3_init(void) {}
|
||
|
||
static bool g_flash_erase_ok = true;
|
||
static bool g_flash_write_ok = true;
|
||
static enc_flash_config_t g_flash_store;
|
||
static bool g_flash_has_data = false;
|
||
|
||
static bool flash_erase_sector7(void) { return g_flash_erase_ok; }
|
||
static bool flash_write_words(uint32_t addr, const void *data, uint32_t len)
|
||
{
|
||
(void)addr;
|
||
if (!g_flash_write_ok) return false;
|
||
memcpy(&g_flash_store, data, len < sizeof(g_flash_store) ? len : sizeof(g_flash_store));
|
||
g_flash_has_data = true;
|
||
return true;
|
||
}
|
||
|
||
#endif /* TEST_HOST */
|
||
|
||
/* ---- Config defaults ---- */
|
||
static void cfg_set_defaults(enc_config_t *cfg)
|
||
{
|
||
cfg->ticks_per_rev = ENC_TICKS_PER_REV_DEFAULT;
|
||
cfg->wheel_diam_mm = ENC_WHEEL_DIAM_MM_DEFAULT;
|
||
cfg->wheel_base_mm = ENC_WHEEL_BASE_MM_DEFAULT;
|
||
}
|
||
|
||
/* ---- Pre-compute derived constants from config ---- */
|
||
static void enc_update_precomputed(encoder_odom_t *eo)
|
||
{
|
||
eo->meters_per_tick = (3.14159265358979f * (float)eo->cfg.wheel_diam_mm * 0.001f)
|
||
/ (float)eo->cfg.ticks_per_rev;
|
||
eo->wheel_base_m = (float)eo->cfg.wheel_base_mm * 0.001f;
|
||
}
|
||
|
||
/* ---- encoder_odom_load_config() ---- */
|
||
bool encoder_odom_load_config(enc_config_t *cfg)
|
||
{
|
||
#ifndef TEST_HOST
|
||
const enc_flash_config_t *p = (const enc_flash_config_t *)ENC_FLASH_ADDR;
|
||
if (p->magic == ENC_FLASH_MAGIC &&
|
||
p->ticks_per_rev > 0u &&
|
||
p->wheel_diam_mm > 0u &&
|
||
p->wheel_base_mm > 0u)
|
||
{
|
||
cfg->ticks_per_rev = p->ticks_per_rev;
|
||
cfg->wheel_diam_mm = p->wheel_diam_mm;
|
||
cfg->wheel_base_mm = p->wheel_base_mm;
|
||
return true;
|
||
}
|
||
#else
|
||
if (g_flash_has_data && g_flash_store.magic == ENC_FLASH_MAGIC &&
|
||
g_flash_store.ticks_per_rev > 0u &&
|
||
g_flash_store.wheel_diam_mm > 0u &&
|
||
g_flash_store.wheel_base_mm > 0u)
|
||
{
|
||
cfg->ticks_per_rev = g_flash_store.ticks_per_rev;
|
||
cfg->wheel_diam_mm = g_flash_store.wheel_diam_mm;
|
||
cfg->wheel_base_mm = g_flash_store.wheel_base_mm;
|
||
return true;
|
||
}
|
||
#endif
|
||
cfg_set_defaults(cfg);
|
||
return false;
|
||
}
|
||
|
||
/* ---- encoder_odom_save_config() ---- */
|
||
bool encoder_odom_save_config(const enc_config_t *cfg)
|
||
{
|
||
enc_flash_config_t rec;
|
||
memset(&rec, 0xFF, sizeof(rec));
|
||
rec.magic = ENC_FLASH_MAGIC;
|
||
rec.ticks_per_rev = cfg->ticks_per_rev;
|
||
rec.wheel_diam_mm = cfg->wheel_diam_mm;
|
||
rec.wheel_base_mm = cfg->wheel_base_mm;
|
||
|
||
#ifndef TEST_HOST
|
||
HAL_FLASH_Unlock();
|
||
bool ok = flash_erase_sector7() &&
|
||
flash_write_words(ENC_FLASH_ADDR, &rec, sizeof(rec));
|
||
HAL_FLASH_Lock();
|
||
return ok;
|
||
#else
|
||
return flash_write_words(ENC_FLASH_ADDR, &rec, sizeof(rec));
|
||
#endif
|
||
}
|
||
|
||
/* ---- encoder_odom_init() ---- */
|
||
void encoder_odom_init(encoder_odom_t *eo)
|
||
{
|
||
memset(eo, 0, sizeof(*eo));
|
||
|
||
/* Load or default config */
|
||
encoder_odom_load_config(&eo->cfg);
|
||
enc_update_precomputed(eo);
|
||
|
||
/* Initialize timers */
|
||
enc_tim2_init();
|
||
enc_tim3_init();
|
||
|
||
/* Snapshot initial counter values (treat as zero reference) */
|
||
eo->cnt_left = enc_read_left();
|
||
eo->cnt_right = enc_read_right();
|
||
|
||
/* Initialize TLM timer so first call fires immediately */
|
||
eo->last_tlm_ms = (uint32_t)(-(uint32_t)(1000u / (ENC_TLM_HZ > 0u ? ENC_TLM_HZ : 1u)));
|
||
}
|
||
|
||
/* ---- encoder_odom_reset_pose() ---- */
|
||
void encoder_odom_reset_pose(encoder_odom_t *eo)
|
||
{
|
||
eo->x_mm = 0.0f;
|
||
eo->y_mm = 0.0f;
|
||
eo->theta_rad = 0.0f;
|
||
}
|
||
|
||
/* ---- encoder_odom_tick() ---- */
|
||
void encoder_odom_tick(encoder_odom_t *eo, uint32_t now_ms)
|
||
{
|
||
/* Compute dt */
|
||
uint32_t elapsed_ms = now_ms - eo->last_tick_ms;
|
||
if (elapsed_ms == 0u) return; /* avoid divide-by-zero at high call rates */
|
||
if (elapsed_ms > 500u) elapsed_ms = 500u; /* clamp: stale data guard */
|
||
float dt_s = (float)elapsed_ms * 0.001f;
|
||
eo->last_tick_ms = now_ms;
|
||
|
||
/* Read counters */
|
||
uint32_t new_left = enc_read_left();
|
||
uint16_t new_right = enc_read_right();
|
||
|
||
/* Signed delta — handles 32/16-bit wrap correctly */
|
||
int32_t delta_left = (int32_t)(new_left - eo->cnt_left);
|
||
int16_t delta_right = (int16_t)(new_right - eo->cnt_right);
|
||
|
||
eo->cnt_left = new_left;
|
||
eo->cnt_right = (uint16_t)new_right;
|
||
|
||
/* RPM */
|
||
float rpm_scale = 60.0f / ((float)eo->cfg.ticks_per_rev * dt_s);
|
||
float rpm_l = (float)delta_left * rpm_scale;
|
||
float rpm_r = (float)delta_right * rpm_scale;
|
||
|
||
/* Clamp to int16 range */
|
||
if (rpm_l > 32767.0f) rpm_l = 32767.0f;
|
||
if (rpm_l < -32768.0f) rpm_l = -32768.0f;
|
||
if (rpm_r > 32767.0f) rpm_r = 32767.0f;
|
||
if (rpm_r < -32768.0f) rpm_r = -32768.0f;
|
||
eo->rpm_left = (int16_t)rpm_l;
|
||
eo->rpm_right = (int16_t)rpm_r;
|
||
|
||
/* Wheel displacements (metres) */
|
||
float d_left = (float)delta_left * eo->meters_per_tick;
|
||
float d_right = (float)delta_right * eo->meters_per_tick;
|
||
|
||
/* Linear / angular increments */
|
||
float d_center = (d_left + d_right) * 0.5f;
|
||
float d_theta = (d_right - d_left) / eo->wheel_base_m;
|
||
|
||
/* Linear speed (mm/s), clamped to int16 */
|
||
float speed_raw = (d_center / dt_s) * 1000.0f; /* m/s → mm/s */
|
||
if (speed_raw > 32767.0f) speed_raw = 32767.0f;
|
||
if (speed_raw < -32768.0f) speed_raw = -32768.0f;
|
||
eo->speed_mmps = (int16_t)speed_raw;
|
||
|
||
/* Integrate pose (Euler-forward) */
|
||
float cos_h = cosf(eo->theta_rad);
|
||
float sin_h = sinf(eo->theta_rad);
|
||
eo->x_mm += d_center * cos_h * 1000.0f;
|
||
eo->y_mm += d_center * sin_h * 1000.0f;
|
||
eo->theta_rad += d_theta;
|
||
|
||
/* Wrap theta to (-π, π] */
|
||
while (eo->theta_rad > 3.14159265358979f) eo->theta_rad -= 2.0f * 3.14159265358979f;
|
||
while (eo->theta_rad < -3.14159265358979f) eo->theta_rad += 2.0f * 3.14159265358979f;
|
||
}
|
||
|
||
/* ---- encoder_odom_send_tlm() ---- */
|
||
void encoder_odom_send_tlm(const encoder_odom_t *eo, uint32_t now_ms)
|
||
{
|
||
if (ENC_TLM_HZ == 0u) return;
|
||
|
||
uint32_t interval_ms = 1000u / ENC_TLM_HZ;
|
||
if ((now_ms - eo->last_tlm_ms) < interval_ms) return;
|
||
/* Cast away const for timestamp — only mutable field */
|
||
((encoder_odom_t *)eo)->last_tlm_ms = now_ms;
|
||
|
||
jlink_tlm_odom_t tlm;
|
||
tlm.rpm_left = eo->rpm_left;
|
||
tlm.rpm_right = eo->rpm_right;
|
||
|
||
/* x/y: float mm → int32_t mm (saturate at ±2147 km — more than adequate) */
|
||
float xf = eo->x_mm;
|
||
float yf = eo->y_mm;
|
||
if (xf > 2147483647.0f) xf = 2147483647.0f;
|
||
if (xf < -2147483648.0f) xf = -2147483648.0f;
|
||
if (yf > 2147483647.0f) yf = 2147483647.0f;
|
||
if (yf < -2147483648.0f) yf = -2147483648.0f;
|
||
tlm.x_mm = (int32_t)xf;
|
||
tlm.y_mm = (int32_t)yf;
|
||
|
||
/* theta: radians → centidegrees (0.01° steps) */
|
||
float theta_cdeg = eo->theta_rad * (18000.0f / 3.14159265358979f);
|
||
if (theta_cdeg > 32767.0f) theta_cdeg = 32767.0f;
|
||
if (theta_cdeg < -32768.0f) theta_cdeg = -32768.0f;
|
||
tlm.theta_cdeg = (int16_t)theta_cdeg;
|
||
|
||
tlm.speed_mmps = eo->speed_mmps;
|
||
|
||
jlink_send_odom_tlm(&tlm);
|
||
}
|