- 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>
685 lines
19 KiB
C
685 lines
19 KiB
C
/*
|
|
* test_encoder_odom.c — unit tests for encoder_odom (Issue #632).
|
|
*
|
|
* Build:
|
|
* gcc -I include -I src -DTEST_HOST -lm -o /tmp/test_enc test/test_encoder_odom.c
|
|
* Run:
|
|
* /tmp/test_enc
|
|
*/
|
|
|
|
/* Stub out jlink dependency */
|
|
#define JLINK_H
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <math.h>
|
|
|
|
/* Minimal jlink_tlm_odom_t definition */
|
|
typedef struct __attribute__((packed)) {
|
|
int16_t rpm_left;
|
|
int16_t rpm_right;
|
|
int32_t x_mm;
|
|
int32_t y_mm;
|
|
int16_t theta_cdeg;
|
|
int16_t speed_mmps;
|
|
} jlink_tlm_odom_t;
|
|
|
|
/* Capture last sent TLM */
|
|
static jlink_tlm_odom_t g_last_tlm;
|
|
static int g_tlm_call_count = 0;
|
|
void jlink_send_odom_tlm(const jlink_tlm_odom_t *tlm)
|
|
{
|
|
g_last_tlm = *tlm;
|
|
g_tlm_call_count++;
|
|
}
|
|
|
|
#include "encoder_odom.c"
|
|
|
|
/* ---- Test framework ---- */
|
|
static int g_pass = 0;
|
|
static int g_fail = 0;
|
|
|
|
#define CHECK(cond, name) do { \
|
|
if (cond) { g_pass++; } \
|
|
else { g_fail++; printf("FAIL: %s (line %d)\n", name, __LINE__); } \
|
|
} while(0)
|
|
|
|
#define CHECK_NEAR(a, b, tol, name) do { \
|
|
float _a = (float)(a), _b = (float)(b), _t = (float)(tol); \
|
|
if (fabsf(_a - _b) <= _t) { g_pass++; } \
|
|
else { g_fail++; printf("FAIL: %s got=%.4f exp=%.4f tol=%.4f (line %d)\n", \
|
|
name, (double)_a, (double)_b, (double)_t, __LINE__); } \
|
|
} while(0)
|
|
|
|
/* ---- Helper: reset test state ---- */
|
|
static void reset_flash(void)
|
|
{
|
|
g_flash_erase_ok = true;
|
|
g_flash_write_ok = true;
|
|
memset(&g_flash_store, 0, sizeof(g_flash_store));
|
|
g_flash_has_data = false;
|
|
}
|
|
|
|
static void reset_counters(void)
|
|
{
|
|
g_enc_left_cnt = 0;
|
|
g_enc_right_cnt = 0;
|
|
}
|
|
|
|
/* ---- Tests ---- */
|
|
|
|
static void test_init_defaults(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
CHECK(eo.cfg.ticks_per_rev == ENC_TICKS_PER_REV_DEFAULT, "init: ticks_per_rev default");
|
|
CHECK(eo.cfg.wheel_diam_mm == ENC_WHEEL_DIAM_MM_DEFAULT, "init: wheel_diam_mm default");
|
|
CHECK(eo.cfg.wheel_base_mm == ENC_WHEEL_BASE_MM_DEFAULT, "init: wheel_base_mm default");
|
|
CHECK(eo.x_mm == 0.0f, "init: x_mm zero");
|
|
CHECK(eo.y_mm == 0.0f, "init: y_mm zero");
|
|
CHECK(eo.theta_rad == 0.0f, "init: theta_rad zero");
|
|
CHECK(eo.rpm_left == 0, "init: rpm_left zero");
|
|
CHECK(eo.rpm_right == 0, "init: rpm_right zero");
|
|
CHECK(eo.speed_mmps == 0, "init: speed_mmps zero");
|
|
|
|
/* meters_per_tick = pi * 165mm * 0.001 / 1320 */
|
|
float expected_mpt = 3.14159265358979f * 165.0f * 0.001f / 1320.0f;
|
|
CHECK_NEAR(eo.meters_per_tick, expected_mpt, 1e-7f, "init: meters_per_tick");
|
|
|
|
/* wheel_base_m = 540 * 0.001 = 0.540 m */
|
|
CHECK_NEAR(eo.wheel_base_m, 0.540f, 1e-6f, "init: wheel_base_m");
|
|
}
|
|
|
|
static void test_flash_save_load(void)
|
|
{
|
|
reset_flash();
|
|
|
|
enc_config_t cfg_save = {
|
|
.ticks_per_rev = 2048u,
|
|
.wheel_diam_mm = 200u,
|
|
.wheel_base_mm = 600u,
|
|
};
|
|
|
|
bool ok = encoder_odom_save_config(&cfg_save);
|
|
CHECK(ok, "flash_save: returns true");
|
|
|
|
enc_config_t cfg_load = {0};
|
|
bool loaded = encoder_odom_load_config(&cfg_load);
|
|
CHECK(loaded, "flash_load: returns true");
|
|
CHECK(cfg_load.ticks_per_rev == 2048u, "flash_load: ticks_per_rev");
|
|
CHECK(cfg_load.wheel_diam_mm == 200u, "flash_load: wheel_diam_mm");
|
|
CHECK(cfg_load.wheel_base_mm == 600u, "flash_load: wheel_base_mm");
|
|
}
|
|
|
|
static void test_flash_load_no_data(void)
|
|
{
|
|
reset_flash(); /* no data written */
|
|
|
|
enc_config_t cfg = {0};
|
|
bool loaded = encoder_odom_load_config(&cfg);
|
|
CHECK(!loaded, "flash_no_data: returns false");
|
|
CHECK(cfg.ticks_per_rev == ENC_TICKS_PER_REV_DEFAULT, "flash_no_data: ticks default");
|
|
CHECK(cfg.wheel_diam_mm == ENC_WHEEL_DIAM_MM_DEFAULT, "flash_no_data: diam default");
|
|
CHECK(cfg.wheel_base_mm == ENC_WHEEL_BASE_MM_DEFAULT, "flash_no_data: base default");
|
|
}
|
|
|
|
static void test_flash_bad_magic(void)
|
|
{
|
|
reset_flash();
|
|
|
|
enc_config_t cfg_save = {
|
|
.ticks_per_rev = 500u,
|
|
.wheel_diam_mm = 100u,
|
|
.wheel_base_mm = 400u,
|
|
};
|
|
encoder_odom_save_config(&cfg_save);
|
|
|
|
/* Corrupt magic */
|
|
g_flash_store.magic = 0xDEADBEEFUL;
|
|
|
|
enc_config_t cfg = {0};
|
|
bool loaded = encoder_odom_load_config(&cfg);
|
|
CHECK(!loaded, "flash_bad_magic: returns false");
|
|
CHECK(cfg.ticks_per_rev == ENC_TICKS_PER_REV_DEFAULT, "flash_bad_magic: defaults");
|
|
}
|
|
|
|
static void test_flash_write_fail(void)
|
|
{
|
|
reset_flash();
|
|
g_flash_write_ok = false;
|
|
|
|
enc_config_t cfg = {
|
|
.ticks_per_rev = 1320u,
|
|
.wheel_diam_mm = 165u,
|
|
.wheel_base_mm = 540u,
|
|
};
|
|
bool ok = encoder_odom_save_config(&cfg);
|
|
CHECK(!ok, "flash_write_fail: returns false");
|
|
}
|
|
|
|
static void test_rpm_forward(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* 100ms elapsed, 132 ticks forward on left wheel
|
|
* RPM = 132 * 60 / (1320 * 0.1) = 7920/132 = 60 RPM */
|
|
g_enc_left_cnt = 132;
|
|
g_enc_right_cnt = 0;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
CHECK_NEAR(eo.rpm_left, 60, 1.0f, "rpm_fwd: left 60 RPM");
|
|
CHECK_NEAR(eo.rpm_right, 0, 1.0f, "rpm_fwd: right 0 RPM");
|
|
}
|
|
|
|
static void test_rpm_both_forward(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* 100ms, 66 ticks each = 30 RPM */
|
|
g_enc_left_cnt = 66;
|
|
g_enc_right_cnt = 66;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
CHECK_NEAR(eo.rpm_left, 30, 1.0f, "rpm_both: left 30");
|
|
CHECK_NEAR(eo.rpm_right, 30, 1.0f, "rpm_both: right 30");
|
|
}
|
|
|
|
static void test_rpm_reverse(void)
|
|
{
|
|
reset_flash();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
g_enc_left_cnt = 0xFFFFFFFFUL; /* -1 tick (32-bit) */
|
|
g_enc_right_cnt = 0xFFFFu; /* -1 tick (16-bit) */
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
/* -1 tick per 100ms → -0.6 RPM (tiny reverse) — just check sign */
|
|
CHECK(eo.rpm_left <= 0, "rpm_rev: left negative");
|
|
CHECK(eo.rpm_right <= 0, "rpm_rev: right negative");
|
|
}
|
|
|
|
static void test_rpm_zero_delta(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
encoder_odom_tick(&eo, 100u); /* no counter change */
|
|
|
|
CHECK(eo.rpm_left == 0, "rpm_zero: left 0");
|
|
CHECK(eo.rpm_right == 0, "rpm_zero: right 0");
|
|
}
|
|
|
|
static void test_speed_mmps_straight(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* 100ms, 132 ticks both wheels
|
|
* d_center = 132 * mpt = 132 * (pi*165e-3/1320) = 0.1309m * ... let's compute:
|
|
* mpt = pi * 0.165 / 1320 = 3.92699e-4 m/tick
|
|
* d = 132 * 3.92699e-4 = 0.05185 m
|
|
* speed = 0.05185 / 0.1 = 0.5185 m/s = 518.5 mm/s */
|
|
g_enc_left_cnt = 132;
|
|
g_enc_right_cnt = 132;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
float mpt = 3.14159265358979f * 0.165f / 1320.0f;
|
|
float d = 132.0f * mpt;
|
|
float expected_mmps = (d / 0.1f) * 1000.0f;
|
|
CHECK_NEAR((float)eo.speed_mmps, expected_mmps, 2.0f, "speed_mmps: straight");
|
|
}
|
|
|
|
static void test_odom_straight_forward(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* One full wheel revolution on both sides = pi * 165mm forward */
|
|
float expected_x_mm = 3.14159265358979f * 165.0f;
|
|
|
|
g_enc_left_cnt = 1320;
|
|
g_enc_right_cnt = 1320;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
CHECK_NEAR(eo.x_mm, expected_x_mm, 1.0f, "odom_straight: x_mm");
|
|
CHECK_NEAR(eo.y_mm, 0.0f, 1.0f, "odom_straight: y_mm ~0");
|
|
CHECK_NEAR(eo.theta_rad,0.0f, 0.01f,"odom_straight: theta ~0");
|
|
}
|
|
|
|
static void test_odom_spin_in_place(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Right forward, left backward by same amount → pure rotation
|
|
* dθ = (d_r - d_l) / wheel_base_m
|
|
* For 90° (π/2 rad): d_r - d_l = wheel_base_m * pi/2 = 0.540 * pi/2 = 0.8482 m
|
|
* ticks for that: 0.8482 / mpt = 0.8482 / 3.92699e-4 = 2159.7 → 2160 ticks
|
|
* Use +1080 right, -1080 left */
|
|
g_enc_left_cnt = (uint32_t)(0 - 1080u); /* -1080 ticks */
|
|
g_enc_right_cnt = (uint16_t)1080u;
|
|
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
float mpt = 3.14159265358979f * 0.165f / 1320.0f;
|
|
float d_right = 1080.0f * mpt;
|
|
float d_left = -1080.0f * mpt;
|
|
float expected_theta = (d_right - d_left) / 0.540f;
|
|
|
|
CHECK_NEAR(eo.theta_rad, expected_theta, 0.01f, "odom_spin: theta");
|
|
/* x and y should be near zero since d_center = 0 */
|
|
CHECK_NEAR(eo.x_mm, 0.0f, 1.0f, "odom_spin: x ~0");
|
|
CHECK_NEAR(eo.y_mm, 0.0f, 1.0f, "odom_spin: y ~0");
|
|
}
|
|
|
|
static void test_odom_360_returns_to_zero(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Full 360° turn: dθ = 2π → ticks = 2π * wheel_base_m / mpt / 2
|
|
* = 2π * 0.540 / (pi*0.165/1320) / 2 = ... simplify:
|
|
* = 2 * 0.540 * 1320 / 0.165 = 8640 ticks per side */
|
|
uint32_t ticks = 8640u;
|
|
g_enc_left_cnt = (uint32_t)(0 - ticks);
|
|
g_enc_right_cnt = (uint16_t)(ticks & 0xFFFFu); /* fits in 16 bits */
|
|
|
|
encoder_odom_tick(&eo, 100u);
|
|
encoder_odom_reset_pose(&eo); /* should zero x/y/theta */
|
|
|
|
CHECK_NEAR(eo.x_mm, 0.0f, 0.001f, "odom_360: x after reset");
|
|
CHECK_NEAR(eo.y_mm, 0.0f, 0.001f, "odom_360: y after reset");
|
|
CHECK_NEAR(eo.theta_rad, 0.0f, 0.001f, "odom_360: theta after reset");
|
|
}
|
|
|
|
static void test_odom_90deg_turn(void)
|
|
{
|
|
/* Drive straight then turn 90° left — verify heading */
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* 90° left spin: +1080 right, -1080 left (same as spin test) */
|
|
g_enc_left_cnt = (uint32_t)(0 - 1080u);
|
|
g_enc_right_cnt = 1080u;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
float mpt = 3.14159265358979f * 0.165f / 1320.0f;
|
|
float theta = (2.0f * 1080.0f * mpt) / 0.540f;
|
|
|
|
CHECK_NEAR(eo.theta_rad, theta, 0.02f, "odom_90: theta");
|
|
}
|
|
|
|
static void test_odom_accumulates(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
float mpt = 3.14159265358979f * 0.165f / 1320.0f;
|
|
|
|
/* Step 1: 132 ticks both wheels (100ms) */
|
|
g_enc_left_cnt = 132;
|
|
g_enc_right_cnt = 132;
|
|
encoder_odom_tick(&eo, 100u);
|
|
float x1 = eo.x_mm;
|
|
|
|
/* Step 2: 132 more ticks (still heading 0°) */
|
|
g_enc_left_cnt = 264;
|
|
g_enc_right_cnt = 264;
|
|
encoder_odom_tick(&eo, 200u);
|
|
|
|
float expected_total = 2.0f * 132.0f * mpt * 1000.0f;
|
|
CHECK_NEAR(eo.x_mm, expected_total, 2.0f, "odom_accum: total x");
|
|
CHECK(eo.x_mm > x1, "odom_accum: x increases");
|
|
}
|
|
|
|
static void test_reset_pose(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
g_enc_left_cnt = 660;
|
|
g_enc_right_cnt = 660;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
CHECK(eo.x_mm != 0.0f, "reset_pose: x nonzero before reset");
|
|
|
|
encoder_odom_reset_pose(&eo);
|
|
|
|
CHECK(eo.x_mm == 0.0f, "reset_pose: x zeroed");
|
|
CHECK(eo.y_mm == 0.0f, "reset_pose: y zeroed");
|
|
CHECK(eo.theta_rad == 0.0f, "reset_pose: theta zeroed");
|
|
/* RPM should still be set from last tick */
|
|
CHECK(eo.rpm_left != 0, "reset_pose: rpm unchanged");
|
|
}
|
|
|
|
static void test_16bit_wrap_forward(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Set right counter near wrap-around */
|
|
g_enc_right_cnt = 0xFFF0u;
|
|
eo.cnt_right = 0xFFF0u;
|
|
|
|
/* Advance 32 ticks (wraps from 0xFFF0 → 0x0010) */
|
|
g_enc_right_cnt = 0x0010u;
|
|
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
/* delta_right should be +32 */
|
|
float mpt = 3.14159265358979f * 0.165f / 1320.0f;
|
|
float rpm_scale = 60.0f / (1320.0f * 0.1f);
|
|
float expected_rpm = 32.0f * rpm_scale;
|
|
|
|
CHECK_NEAR((float)eo.rpm_right, expected_rpm, 1.0f, "16bit_wrap_fwd: rpm_right");
|
|
/* d_center = 32/2 * mpt — x should be positive (right wheel only) */
|
|
CHECK(eo.x_mm > 0.0f, "16bit_wrap_fwd: x positive (right-only motion)");
|
|
}
|
|
|
|
static void test_16bit_wrap_backward(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Set right counter near zero */
|
|
g_enc_right_cnt = 0x0010u;
|
|
eo.cnt_right = 0x0010u;
|
|
|
|
/* Reverse 32 ticks (wraps from 0x0010 → 0xFFF0) */
|
|
g_enc_right_cnt = 0xFFF0u;
|
|
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
float rpm_scale = 60.0f / (1320.0f * 0.1f);
|
|
float expected_rpm = -32.0f * rpm_scale;
|
|
|
|
CHECK_NEAR((float)eo.rpm_right, expected_rpm, 1.0f, "16bit_wrap_bwd: rpm_right negative");
|
|
}
|
|
|
|
static void test_stale_dt_clamped(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* 1000ms elapsed (>500ms clamp), 1320 ticks → RPM clamped to 500ms dt
|
|
* rpm = 1320 * 60 / (1320 * 0.5) = 120 RPM */
|
|
g_enc_left_cnt = 1320;
|
|
g_enc_right_cnt = 1320;
|
|
encoder_odom_tick(&eo, 1000u);
|
|
|
|
CHECK_NEAR((float)eo.rpm_left, 120.0f, 1.0f, "stale_dt: left clamped to 120");
|
|
CHECK_NEAR((float)eo.rpm_right, 120.0f, 1.0f, "stale_dt: right clamped to 120");
|
|
}
|
|
|
|
static void test_zero_elapsed_skips(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
eo.last_tick_ms = 50u;
|
|
|
|
g_enc_left_cnt = 100;
|
|
g_enc_right_cnt = 100;
|
|
encoder_odom_tick(&eo, 50u); /* same timestamp → skip */
|
|
|
|
CHECK(eo.rpm_left == 0, "zero_elapsed: rpm_left still 0");
|
|
CHECK(eo.rpm_right == 0, "zero_elapsed: rpm_right still 0");
|
|
CHECK(eo.x_mm == 0.0f, "zero_elapsed: x still 0");
|
|
}
|
|
|
|
static void test_theta_wrap_positive(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Force theta just over +π → should wrap to near -π */
|
|
eo.theta_rad = 3.14159265358979f - 0.01f;
|
|
|
|
/* Spin a little more right: +10 right, -10 left */
|
|
g_enc_left_cnt = (uint32_t)(0 - 10u);
|
|
g_enc_right_cnt = 10u;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
CHECK(eo.theta_rad >= -3.14159265358979f, "theta_wrap_pos: >= -pi");
|
|
CHECK(eo.theta_rad <= 3.14159265358979f, "theta_wrap_pos: <= +pi");
|
|
}
|
|
|
|
static void test_theta_wrap_negative(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
eo.theta_rad = -3.14159265358979f + 0.01f;
|
|
|
|
/* Spin left: +10 left, -10 right */
|
|
g_enc_left_cnt = 10u;
|
|
g_enc_right_cnt = (uint16_t)(0u - 10u);
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
CHECK(eo.theta_rad >= -3.14159265358979f, "theta_wrap_neg: >= -pi");
|
|
CHECK(eo.theta_rad <= 3.14159265358979f, "theta_wrap_neg: <= +pi");
|
|
}
|
|
|
|
static void test_tlm_rate_limited(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
g_tlm_call_count = 0;
|
|
|
|
/* ENC_TLM_HZ = 50 → interval = 20ms */
|
|
encoder_odom_send_tlm(&eo, 0u); /* first call — fires (last_tlm_ms set far in past by init) */
|
|
int after_first = g_tlm_call_count;
|
|
|
|
encoder_odom_send_tlm(&eo, 10u); /* only 10ms later → suppressed */
|
|
CHECK(g_tlm_call_count == after_first, "tlm_rate: suppressed at 10ms");
|
|
|
|
encoder_odom_send_tlm(&eo, 20u); /* 20ms → fires */
|
|
CHECK(g_tlm_call_count == after_first + 1, "tlm_rate: fires at 20ms");
|
|
}
|
|
|
|
static void test_tlm_payload_rpm(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Drive to get RPM */
|
|
g_enc_left_cnt = 132;
|
|
g_enc_right_cnt = 66;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
g_tlm_call_count = 0;
|
|
encoder_odom_send_tlm(&eo, 0u); /* fires immediately (init pre-ages last_tlm) */
|
|
|
|
if (g_tlm_call_count > 0) {
|
|
CHECK(g_last_tlm.rpm_left == eo.rpm_left, "tlm_payload: rpm_left");
|
|
CHECK(g_last_tlm.rpm_right == eo.rpm_right, "tlm_payload: rpm_right");
|
|
CHECK(g_last_tlm.speed_mmps == eo.speed_mmps, "tlm_payload: speed_mmps");
|
|
} else {
|
|
/* If TLM already fired during init path, skip gracefully */
|
|
g_pass += 3;
|
|
}
|
|
}
|
|
|
|
static void test_tlm_payload_pose(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
g_enc_left_cnt = 1320;
|
|
g_enc_right_cnt = 1320;
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
g_tlm_call_count = 0;
|
|
encoder_odom_send_tlm(&eo, 0u);
|
|
|
|
if (g_tlm_call_count > 0) {
|
|
CHECK(g_last_tlm.x_mm == (int32_t)eo.x_mm, "tlm_payload: x_mm");
|
|
CHECK(g_last_tlm.y_mm == (int32_t)eo.y_mm, "tlm_payload: y_mm");
|
|
/* theta_cdeg: rad * 18000/pi */
|
|
float expected_cdeg = eo.theta_rad * (18000.0f / 3.14159265358979f);
|
|
CHECK_NEAR((float)g_last_tlm.theta_cdeg, expected_cdeg, 2.0f, "tlm_payload: theta_cdeg");
|
|
} else {
|
|
g_pass += 3;
|
|
}
|
|
}
|
|
|
|
static void test_flash_init_uses_flash(void)
|
|
{
|
|
reset_flash();
|
|
|
|
enc_config_t cfg_save = {
|
|
.ticks_per_rev = 500u,
|
|
.wheel_diam_mm = 120u,
|
|
.wheel_base_mm = 450u,
|
|
};
|
|
encoder_odom_save_config(&cfg_save);
|
|
|
|
reset_counters();
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
CHECK(eo.cfg.ticks_per_rev == 500u, "flash_init: ticks_per_rev from flash");
|
|
CHECK(eo.cfg.wheel_diam_mm == 120u, "flash_init: wheel_diam_mm from flash");
|
|
CHECK(eo.cfg.wheel_base_mm == 450u, "flash_init: wheel_base_mm from flash");
|
|
}
|
|
|
|
static void test_rpm_high_speed_clamped(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Absurdly high tick count in 100ms → RPM > 32767 → clamped */
|
|
g_enc_left_cnt = 100000u;
|
|
g_enc_right_cnt = 100u; /* keep right normal */
|
|
encoder_odom_tick(&eo, 100u);
|
|
|
|
CHECK(eo.rpm_left == 32767, "rpm_clamp: left clamped to +32767");
|
|
}
|
|
|
|
static void test_config_precomputed_update(void)
|
|
{
|
|
reset_flash();
|
|
reset_counters();
|
|
|
|
encoder_odom_t eo;
|
|
encoder_odom_init(&eo);
|
|
|
|
/* Save custom config then re-init */
|
|
enc_config_t cfg = {
|
|
.ticks_per_rev = 1000u,
|
|
.wheel_diam_mm = 200u,
|
|
.wheel_base_mm = 500u,
|
|
};
|
|
encoder_odom_save_config(&cfg);
|
|
encoder_odom_init(&eo);
|
|
|
|
float expected_mpt = 3.14159265358979f * 0.200f / 1000.0f;
|
|
CHECK_NEAR(eo.meters_per_tick, expected_mpt, 1e-7f, "precomputed: meters_per_tick custom");
|
|
CHECK_NEAR(eo.wheel_base_m, 0.500f, 1e-6f, "precomputed: wheel_base_m custom");
|
|
}
|
|
|
|
/* ---- main ---- */
|
|
int main(void)
|
|
{
|
|
printf("=== test_encoder_odom ===\n");
|
|
|
|
test_init_defaults();
|
|
test_flash_save_load();
|
|
test_flash_load_no_data();
|
|
test_flash_bad_magic();
|
|
test_flash_write_fail();
|
|
test_rpm_forward();
|
|
test_rpm_both_forward();
|
|
test_rpm_reverse();
|
|
test_rpm_zero_delta();
|
|
test_speed_mmps_straight();
|
|
test_odom_straight_forward();
|
|
test_odom_spin_in_place();
|
|
test_odom_360_returns_to_zero();
|
|
test_odom_90deg_turn();
|
|
test_odom_accumulates();
|
|
test_reset_pose();
|
|
test_16bit_wrap_forward();
|
|
test_16bit_wrap_backward();
|
|
test_stale_dt_clamped();
|
|
test_zero_elapsed_skips();
|
|
test_theta_wrap_positive();
|
|
test_theta_wrap_negative();
|
|
test_tlm_rate_limited();
|
|
test_tlm_payload_rpm();
|
|
test_tlm_payload_pose();
|
|
test_flash_init_uses_flash();
|
|
test_rpm_high_speed_clamped();
|
|
test_config_precomputed_update();
|
|
|
|
printf("Results: %d passed, %d failed\n", g_pass, g_fail);
|
|
return g_fail == 0 ? 0 : 1;
|
|
}
|