/* * 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 #include /* ---- 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); }