471ff12e70
The existing suite passed both before and after the fusion fixes, so it never covered either failure. Two tests close that gap; both were checked against the pre-fix sources and fail there. calibration_offset_test reproduces the bench part's +28 mg zero-g offset and requires startup to complete, while still refusing a sustained 3 m/s2 acceleration. It also asserts the blind spot rather than leaving it implicit: horizontal acceleration adds to gravity in quadrature, so 1.5 m/s2 moves the magnitude by only 0.11 m/s2 and is not separable from offset by any accelerometer gate. Separating it needs GNSS velocity. long_outage_trajectory_test drives pedestrian, gentle-car (0-100 km/h in 30 s), slow-city and isolated-mount profiles across 1 and 5 minute outages, using the production rest supervisor rather than a hand-set flag. Only the isolated-mount slow-city case reproduces the field bug: GNSS motion evidence compares displacement against covariance, so 4 m/s under 6 m sigma scores a displacement NIS of 0.22 against a 9.21 gate and reads as stationary, whereas highway speed scores 42.9 and is correctly called moving. Pre-fix that case produces 608 zero-velocity updates while driving, matching the 609 in the board logs. Drift is judged against the filter's own covariance rather than a fixed distance. Five minutes unaided legitimately reaches kilometres on this hardware; what must not happen is the error outrunning the covariance, which is divergence rather than honest coasting. tdk_noise_test characterises the sensor at rest through the production driver, so ODR, full scale, UI filter and LSB scaling match the flight build, and reports the norms fusion_motion_update() actually gates on. It also carries an INT1-to-PLIC continuity test that drives the pin to a static level and reads the PLIC pending bit directly, separating a broken line from a source that never fires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
806 lines
26 KiB
C
806 lines
26 KiB
C
/*
|
|
* Stationary noise characterization for the TDK ICM-42688-P.
|
|
*
|
|
* The board must stay still for the whole run.
|
|
*
|
|
* This app drives the production tdk_icm42688 driver, so ODR, full-scale
|
|
* ranges, UI filter configuration and LSB scaling are identical to the flight
|
|
* build. Measuring through a hand-rolled register setup would characterize a
|
|
* different bandwidth and produce thresholds that do not transfer.
|
|
*
|
|
* It reports exactly the three quantities fusion_motion_update() compares
|
|
* against its thresholds:
|
|
*
|
|
* accel_norm = || accel - mean(accel) || linear acceleration at rest, i.e.
|
|
* what remains once the ESKF has
|
|
* absorbed gravity and accel bias
|
|
* gyro_norm = || gyro - mean(gyro) || bias-removed angular rate
|
|
* jerk_norm = || d/dt IIR(accel_res) || same 0.10 s time constant the
|
|
* detector uses
|
|
*
|
|
* A stationary limit must sit above the observed quiescent distribution or the
|
|
* platform is never recognised as still; a motion threshold must sit above it
|
|
* by enough margin that noise alone cannot trip it. Both therefore follow
|
|
* from the numbers printed here rather than from datasheet noise densities,
|
|
* which describe the bare part and not this board, mounting or bandwidth.
|
|
*
|
|
* All values print as integers in micro-units to keep the report independent
|
|
* of newlib float formatting: um/s^2, urad/s, um/s^3.
|
|
*/
|
|
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "bsp/irq.h"
|
|
#include "bsp/mmio.h"
|
|
#include "bsp/plic.h"
|
|
#include "bsp/pmod_spi.h"
|
|
#include "bsp/soc.h"
|
|
#include "bsp/uart.h"
|
|
|
|
#include "drivers/imu/tdk_icm42688.h"
|
|
|
|
/* Mirrors soc/software/gps_system/Makefile IMU_ODR_HZ. */
|
|
#ifndef NOISE_ODR_HZ
|
|
#define NOISE_ODR_HZ 50u
|
|
#endif
|
|
/* Discards sensor and UI-filter settling after start. */
|
|
#ifndef NOISE_DISCARD_SAMPLES
|
|
#define NOISE_DISCARD_SAMPLES 100u
|
|
#endif
|
|
#ifndef NOISE_BIAS_SAMPLES
|
|
#define NOISE_BIAS_SAMPLES 1000u
|
|
#endif
|
|
#ifndef NOISE_MEASURE_SAMPLES
|
|
#define NOISE_MEASURE_SAMPLES 3000u
|
|
#endif
|
|
|
|
/* Matches fusion_motion_default_config()->jerk_filter_time_constant_s. */
|
|
#define JERK_TIME_CONSTANT_S 0.10
|
|
|
|
#define HISTOGRAM_BINS 400u
|
|
/* Full-scale of each histogram; samples above it still count in the last bin
|
|
* and the exact maximum is tracked separately, so an overflow is visible. */
|
|
#define ACCEL_HISTOGRAM_FULL_SCALE 0.40 /* m/s^2 -> 1.0 mm/s^2 per bin */
|
|
#define GYRO_HISTOGRAM_FULL_SCALE 0.040 /* rad/s -> 100 urad/s per bin */
|
|
#define JERK_HISTOGRAM_FULL_SCALE 8.0 /* m/s^3 -> 20 mm/s^3 per bin */
|
|
|
|
typedef struct {
|
|
const char* name;
|
|
const char* unit;
|
|
double full_scale;
|
|
double sum;
|
|
double sum_squares;
|
|
double maximum;
|
|
uint32_t count;
|
|
uint32_t overflow;
|
|
uint32_t bins[HISTOGRAM_BINS];
|
|
} statistic_t;
|
|
|
|
static volatile uint64_t unexpected_irq_count;
|
|
static volatile uint64_t unexpected_trap_cause;
|
|
|
|
/*
|
|
* Must carry interrupt("machine"): without it GCC ends the function with ret
|
|
* instead of mret, so mstatus.MIE -- cleared by hardware on trap entry -- is
|
|
* never restored and exactly one interrupt is ever taken. aligned(4) is
|
|
* required because mtvec holds the address.
|
|
*/
|
|
void tdk_noise_trap_handler(void)
|
|
__attribute__((interrupt("machine"), aligned(4)));
|
|
|
|
void tdk_noise_trap_handler(void)
|
|
{
|
|
asm volatile(
|
|
".option push\n"
|
|
".option norelax\n"
|
|
"la gp, __global_pointer$\n"
|
|
".option pop\n"
|
|
: : : "memory");
|
|
|
|
const uint64_t mcause = bsp_read_mcause();
|
|
if(((mcause >> 63) == 0u) ||
|
|
((mcause & 0xfffu) != BSP_MCAUSE_MACHINE_EXTERNAL))
|
|
{
|
|
unexpected_trap_cause = mcause;
|
|
return;
|
|
}
|
|
const uint32_t irq_id = bsp_plic_claim();
|
|
if(irq_id == 0u)
|
|
{
|
|
__atomic_fetch_add(&unexpected_irq_count, 1u, __ATOMIC_RELAXED);
|
|
return;
|
|
}
|
|
if(irq_id == BSP_PMOD_IRQ)
|
|
{
|
|
tdk_icm42688_irq_handler();
|
|
}
|
|
else
|
|
{
|
|
__atomic_fetch_add(&unexpected_irq_count, 1u, __ATOMIC_RELAXED);
|
|
}
|
|
bsp_plic_complete(irq_id);
|
|
}
|
|
|
|
static void statistic_init(statistic_t* statistic, const char* name,
|
|
const char* unit, double full_scale)
|
|
{
|
|
memset(statistic, 0, sizeof(*statistic));
|
|
statistic->name = name;
|
|
statistic->unit = unit;
|
|
statistic->full_scale = full_scale;
|
|
}
|
|
|
|
static void statistic_add(statistic_t* statistic, double value)
|
|
{
|
|
statistic->sum += value;
|
|
statistic->sum_squares += value * value;
|
|
if(value > statistic->maximum)
|
|
{
|
|
statistic->maximum = value;
|
|
}
|
|
statistic->count++;
|
|
double scaled = value / statistic->full_scale * (double)HISTOGRAM_BINS;
|
|
if(!(scaled >= 0.0))
|
|
{
|
|
scaled = 0.0;
|
|
}
|
|
unsigned int bin = (unsigned int)scaled;
|
|
if(bin >= HISTOGRAM_BINS)
|
|
{
|
|
bin = HISTOGRAM_BINS - 1u;
|
|
statistic->overflow++;
|
|
}
|
|
statistic->bins[bin]++;
|
|
}
|
|
|
|
/* Upper edge of the bin holding the requested quantile. */
|
|
static double statistic_percentile(const statistic_t* statistic,
|
|
double fraction)
|
|
{
|
|
if(statistic->count == 0u)
|
|
{
|
|
return 0.0;
|
|
}
|
|
uint32_t target = (uint32_t)(fraction * (double)statistic->count);
|
|
uint32_t cumulative = 0u;
|
|
for(unsigned int bin = 0u ; bin < HISTOGRAM_BINS ; bin++)
|
|
{
|
|
cumulative += statistic->bins[bin];
|
|
if(cumulative >= target)
|
|
{
|
|
return (double)(bin + 1u) * statistic->full_scale /
|
|
(double)HISTOGRAM_BINS;
|
|
}
|
|
}
|
|
return statistic->full_scale;
|
|
}
|
|
|
|
static long micro(double value)
|
|
{
|
|
double scaled = value * 1.0e6;
|
|
if(scaled > 2.0e9)
|
|
{
|
|
scaled = 2.0e9;
|
|
}
|
|
if(scaled < -2.0e9)
|
|
{
|
|
scaled = -2.0e9;
|
|
}
|
|
return (long)scaled;
|
|
}
|
|
|
|
static void statistic_report(const statistic_t* statistic)
|
|
{
|
|
double mean = (statistic->count != 0u)
|
|
? statistic->sum / (double)statistic->count : 0.0;
|
|
double variance = (statistic->count > 1u)
|
|
? (statistic->sum_squares / (double)statistic->count) -
|
|
(mean * mean)
|
|
: 0.0;
|
|
if(variance < 0.0)
|
|
{
|
|
variance = 0.0;
|
|
}
|
|
printf("STAT,%s,unit=%s,n=%lu,mean=%ld,std=%ld,"
|
|
"p50=%ld,p95=%ld,p99=%ld,p999=%ld,max=%ld,overflow=%lu\n",
|
|
statistic->name, statistic->unit,
|
|
(unsigned long)statistic->count,
|
|
micro(mean), micro(sqrt(variance)),
|
|
micro(statistic_percentile(statistic, 0.50)),
|
|
micro(statistic_percentile(statistic, 0.95)),
|
|
micro(statistic_percentile(statistic, 0.99)),
|
|
micro(statistic_percentile(statistic, 0.999)),
|
|
micro(statistic->maximum),
|
|
(unsigned long)statistic->overflow);
|
|
}
|
|
|
|
/*
|
|
* Polled fallback.
|
|
*
|
|
* The data-ready INT1 route is not delivering on this bitstream (the driver
|
|
* reports a single spurious IRQ and then silence), so the interrupt path
|
|
* cannot feed the characterization. Polling INT_STATUS reads the identical
|
|
* registers the ISR would read, at the identical ODR, full scale and UI
|
|
* filter, so the noise statistics are unaffected; only the delivery mechanism
|
|
* differs. Sample timestamps still come from CLINT.
|
|
*/
|
|
#define ICM_INT_STATUS_REG 0x2du
|
|
#define ICM_TEMP_DATA1_REG 0x1du
|
|
#define ICM_SPI_READ_BIT 0x80u
|
|
#define ICM_SPI_DUMMY_BYTE 0xffu
|
|
#define ICM_DATA_READY_BIT 0x08u
|
|
|
|
static bool polled_mode;
|
|
|
|
static int16_t big_endian_i16(const uint8_t* bytes)
|
|
{
|
|
return (int16_t)(((uint16_t)bytes[0] << 8) | bytes[1]);
|
|
}
|
|
|
|
static bool poll_for_sample(imu_sample_t* sample, uint64_t deadline)
|
|
{
|
|
while(bsp_read64(BSP_CLINT_MTIME) < deadline)
|
|
{
|
|
uint8_t status = bsp_pmod_spi_read_reg8(
|
|
ICM_INT_STATUS_REG | ICM_SPI_READ_BIT, ICM_SPI_DUMMY_BYTE);
|
|
if((status & ICM_DATA_READY_BIT) == 0u)
|
|
{
|
|
continue;
|
|
}
|
|
uint64_t timestamp = bsp_read64(BSP_CLINT_MTIME);
|
|
uint8_t bytes[14];
|
|
bsp_pmod_spi_read_regs(ICM_TEMP_DATA1_REG | ICM_SPI_READ_BIT,
|
|
ICM_SPI_DUMMY_BYTE, bytes, sizeof(bytes));
|
|
/* Same scaling the driver applies: full scale over a 15-bit half range. */
|
|
double accel_scale = 4.0 * 9.80665 / 32768.0;
|
|
double gyro_scale = 250.0 * 0.017453292519943295769236907684886 / 32768.0;
|
|
sample->timestamp_ticks = timestamp;
|
|
sample->temperature_c = (double)big_endian_i16(&bytes[0]) / 132.48 + 25.0;
|
|
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
|
{
|
|
sample->accel_mps2[axis] =
|
|
(double)big_endian_i16(&bytes[2u + 2u * axis]) * accel_scale;
|
|
sample->gyro_rps[axis] =
|
|
(double)big_endian_i16(&bytes[8u + 2u * axis]) * gyro_scale;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Why the data-ready interrupt stopped after the first one.
|
|
*
|
|
* Three causes produce the same silence and are told apart by whether the
|
|
* sensor is still generating a data-ready condition at all:
|
|
*
|
|
* sensor stopped PWR_MGMT0 or the ODR fields no longer hold their
|
|
* configured values, and DRDY never sets
|
|
* routing lost DRDY sets in INT_STATUS but INT_SOURCE0 no longer routes
|
|
* it to the pin
|
|
* latch stuck DRDY sets and routing is intact, so the pin is asserted
|
|
* but the PLIC is not seeing a fresh edge/level
|
|
*/
|
|
/*
|
|
* Continuity test for the sensor INT1 -> FPGA pmod_irq -> PLIC path.
|
|
*
|
|
* Everything measured so far depends on the sensor raising a data-ready edge,
|
|
* which conflates "the line is broken" with "the source never fired". This
|
|
* separates them by driving INT1 to a static level the sensor holds
|
|
* regardless of data-ready, and reading the PLIC pending bit directly.
|
|
*
|
|
* INT_CONFIG bit 0 selects INT1 polarity. Clearing it makes the pin active
|
|
* low, so its idle state becomes high and it stays there. If pmod_irq is
|
|
* really wired to this pin, the PLIC gateway then reports pending forever; if
|
|
* the input is floating, its pulldown keeps it clear. The core's external
|
|
* interrupts stay masked throughout, so a permanently asserted line cannot
|
|
* storm the ISR -- the pending bit is polled, never taken.
|
|
*/
|
|
#define PLIC_PENDING_WORD0 (BSP_PLIC_BASE + 0x1000u)
|
|
#define ICM_INT_CONFIG_REG 0x14u
|
|
#define ICM_INT_SOURCE0_REG 0x65u
|
|
#define ICM_INT1_ACTIVE_HIGH_PP_LATCHED 0x07u
|
|
#define ICM_INT1_ACTIVE_LOW_PP_LATCHED 0x06u
|
|
#define ICM_UI_DRDY_INT1 0x08u
|
|
|
|
static uint32_t sample_pending_percent(uint64_t window_ticks)
|
|
{
|
|
/* The gateway latches; claiming clears it so what follows reflects the
|
|
* line's present level rather than a stale assertion. */
|
|
uint32_t claimed = bsp_plic_claim();
|
|
if(claimed != 0u)
|
|
{
|
|
bsp_plic_complete(claimed);
|
|
}
|
|
uint64_t deadline = bsp_read64(BSP_CLINT_MTIME) + window_ticks;
|
|
uint32_t samples = 0u;
|
|
uint32_t asserted = 0u;
|
|
while(bsp_read64(BSP_CLINT_MTIME) < deadline)
|
|
{
|
|
if((bsp_read32(PLIC_PENDING_WORD0) & (1u << BSP_PMOD_IRQ)) != 0u)
|
|
{
|
|
asserted++;
|
|
uint32_t id = bsp_plic_claim();
|
|
if(id != 0u)
|
|
{
|
|
bsp_plic_complete(id);
|
|
}
|
|
}
|
|
samples++;
|
|
}
|
|
return (samples != 0u) ? ((asserted * 100u) / samples) : 0u;
|
|
}
|
|
|
|
static void run_pin_continuity_test(void)
|
|
{
|
|
uint64_t window = BSP_CLINT_TIMEBASE_HZ / 5u; /* 200 ms */
|
|
|
|
bsp_plic_set_priority(BSP_PMOD_IRQ, 1u);
|
|
bsp_plic_enable(BSP_PMOD_IRQ);
|
|
|
|
/* Baseline: nothing routed to the pin, so it should rest inactive-low. */
|
|
bsp_pmod_spi_write_reg8(ICM_INT_SOURCE0_REG, 0x00u);
|
|
bsp_pmod_spi_write_reg8(ICM_INT_CONFIG_REG,
|
|
ICM_INT1_ACTIVE_HIGH_PP_LATCHED);
|
|
uint32_t idle_low = sample_pending_percent(window);
|
|
|
|
/* Forced high: polarity inverted, so the idle state is asserted. */
|
|
bsp_pmod_spi_write_reg8(ICM_INT_CONFIG_REG,
|
|
ICM_INT1_ACTIVE_LOW_PP_LATCHED);
|
|
uint32_t forced_high = sample_pending_percent(window);
|
|
|
|
/* Restore and let data-ready drive the pin normally. */
|
|
bsp_pmod_spi_write_reg8(ICM_INT_CONFIG_REG,
|
|
ICM_INT1_ACTIVE_HIGH_PP_LATCHED);
|
|
bsp_pmod_spi_write_reg8(ICM_INT_SOURCE0_REG, ICM_UI_DRDY_INT1);
|
|
uint32_t data_ready = sample_pending_percent(window);
|
|
|
|
printf("PIN_TEST,idle_low=%lu%%,forced_high=%lu%%,data_ready=%lu%%\n",
|
|
(unsigned long)idle_low, (unsigned long)forced_high,
|
|
(unsigned long)data_ready);
|
|
printf("PIN_TEST,int_config=0x%02x,int_source0=0x%02x\n",
|
|
bsp_pmod_spi_read_reg8(ICM_INT_CONFIG_REG | ICM_SPI_READ_BIT,
|
|
ICM_SPI_DUMMY_BYTE),
|
|
bsp_pmod_spi_read_reg8(ICM_INT_SOURCE0_REG | ICM_SPI_READ_BIT,
|
|
ICM_SPI_DUMMY_BYTE));
|
|
|
|
if(forced_high >= 90u)
|
|
{
|
|
printf("VERDICT,INT1 reaches PLIC %lu. The pin is wired correctly; "
|
|
"any remaining silence is the source not re-arming, not the "
|
|
"wire.\n", (unsigned long)BSP_PMOD_IRQ);
|
|
}
|
|
else if(forced_high <= 10u)
|
|
{
|
|
printf("VERDICT,INT1 does NOT reach PLIC %lu. Holding the sensor pin "
|
|
"statically asserted produced no pending bit, so the FPGA input "
|
|
"(package pin AK30) is not connected to the sensor INT1 pin. "
|
|
"Confirm which physical PMOD position AK30 is in Digilent's "
|
|
"Genesys2 master XDC -- the comment in genesys2_pmod_irq.xdc is "
|
|
"not authoritative.\n", (unsigned long)BSP_PMOD_IRQ);
|
|
}
|
|
else
|
|
{
|
|
printf("VERDICT,intermittent. A marginal or noisy connection.\n");
|
|
}
|
|
bsp_plic_disable(BSP_PMOD_IRQ);
|
|
}
|
|
|
|
/* MEIE (bit 11) tells us whether the hart is actually accepting the PLIC's
|
|
* external interrupt, which a pending bit alone does not. */
|
|
static uint64_t bsp_read_mie(void)
|
|
{
|
|
uint64_t value;
|
|
asm volatile("csrr %0, mie" : "=r"(value));
|
|
return value;
|
|
}
|
|
|
|
static void delay_ticks(uint64_t ticks)
|
|
{
|
|
uint64_t start = bsp_read64(BSP_CLINT_MTIME);
|
|
while((bsp_read64(BSP_CLINT_MTIME) - start) < ticks)
|
|
{
|
|
}
|
|
}
|
|
|
|
static void dump_sensor_state(void)
|
|
{
|
|
static const struct { uint8_t address; const char* name; } registers[] = {
|
|
{0x14u, "INT_CONFIG"},
|
|
{0x2du, "INT_STATUS"},
|
|
{0x4eu, "PWR_MGMT0"},
|
|
{0x4fu, "GYRO_CONFIG0"},
|
|
{0x50u, "ACCEL_CONFIG0"},
|
|
{0x63u, "INT_CONFIG0"},
|
|
{0x64u, "INT_CONFIG1"},
|
|
{0x65u, "INT_SOURCE0"},
|
|
{0x75u, "WHO_AM_I"}
|
|
};
|
|
for(unsigned int i = 0u ;
|
|
i < sizeof(registers) / sizeof(registers[0]) ; i++)
|
|
{
|
|
printf("REG,%s,0x%02x=0x%02x\n", registers[i].name,
|
|
registers[i].address,
|
|
bsp_pmod_spi_read_reg8(registers[i].address | ICM_SPI_READ_BIT,
|
|
ICM_SPI_DUMMY_BYTE));
|
|
}
|
|
|
|
/*
|
|
* Sample INT_STATUS over several ODR periods. Reading it also clears it,
|
|
* so a healthy sensor sets DATA_RDY again on the very next sample and this
|
|
* count lands near the number of periods observed.
|
|
*/
|
|
uint64_t window_ticks = (BSP_CLINT_TIMEBASE_HZ * 10u) / NOISE_ODR_HZ;
|
|
uint64_t deadline = bsp_read64(BSP_CLINT_MTIME) + window_ticks;
|
|
uint32_t polls = 0u;
|
|
uint32_t drdy_seen = 0u;
|
|
while(bsp_read64(BSP_CLINT_MTIME) < deadline)
|
|
{
|
|
uint8_t status = bsp_pmod_spi_read_reg8(
|
|
ICM_INT_STATUS_REG | ICM_SPI_READ_BIT, ICM_SPI_DUMMY_BYTE);
|
|
polls++;
|
|
if((status & ICM_DATA_READY_BIT) != 0u)
|
|
{
|
|
drdy_seen++;
|
|
}
|
|
}
|
|
printf("DRDY_POLL,window_periods=10,polls=%lu,data_ready=%lu\n",
|
|
(unsigned long)polls, (unsigned long)drdy_seen);
|
|
}
|
|
|
|
static bool wait_for_sample(imu_sample_t* sample)
|
|
{
|
|
/* The bound covers roughly ten ODR periods. */
|
|
uint64_t deadline = bsp_read64(BSP_CLINT_MTIME) +
|
|
(BSP_CLINT_TIMEBASE_HZ * 10u) / NOISE_ODR_HZ +
|
|
BSP_CLINT_TIMEBASE_HZ / 10u;
|
|
if(polled_mode)
|
|
{
|
|
return poll_for_sample(sample, deadline);
|
|
}
|
|
while(bsp_read64(BSP_CLINT_MTIME) < deadline)
|
|
{
|
|
if(tdk_icm42688_take_measurement(sample))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static double norm3(const double vector[3])
|
|
{
|
|
return sqrt(vector[0] * vector[0] + vector[1] * vector[1] +
|
|
vector[2] * vector[2]);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("\nTDK ICM-42688-P stationary noise characterization\n");
|
|
printf("KEEP THE BOARD COMPLETELY STILL FOR THE ENTIRE RUN\n");
|
|
|
|
tdk_icm42688_config_t config;
|
|
tdk_icm42688_default_config(&config);
|
|
config.odr_hz = NOISE_ODR_HZ;
|
|
|
|
bsp_plic_disable_all();
|
|
bsp_plic_set_threshold(0u);
|
|
bsp_plic_set_priority(BSP_PMOD_IRQ, 1u);
|
|
/* Recover the PMOD LevelGateway if a previous GDB-loaded image stopped
|
|
* between claim and complete. */
|
|
bsp_plic_complete(BSP_PMOD_IRQ);
|
|
bsp_set_mtvec(tdk_noise_trap_handler);
|
|
|
|
if(!tdk_icm42688_init(&config))
|
|
{
|
|
tdk_icm42688_diagnostics_t diagnostics;
|
|
tdk_icm42688_get_diagnostics(&diagnostics);
|
|
printf("INIT FAIL: who_am_i=0x%02x (expected 0x47)\n",
|
|
diagnostics.who_am_i);
|
|
bsp_uart_wait_tx_idle();
|
|
return 2;
|
|
}
|
|
|
|
#if NOISE_PIN_TEST
|
|
/* Runs with the core's external interrupts still masked, so a statically
|
|
* asserted line is polled rather than taken. */
|
|
printf("CONFIG,odr_hz=%lu,accel_fs_g=%d,gyro_fs_mdps=%d,ui_filter=0x%02x\n",
|
|
(unsigned long)config.odr_hz, (int)config.accel_full_scale_g,
|
|
(int)config.gyro_full_scale_mdps, config.ui_filter_config);
|
|
run_pin_continuity_test();
|
|
tdk_icm42688_stop();
|
|
bsp_uart_wait_tx_idle();
|
|
return 0;
|
|
#endif
|
|
|
|
bsp_enable_machine_external_interrupts();
|
|
|
|
/*
|
|
* Separate a stale PLIC pending bit from a real interrupt.
|
|
*
|
|
* tdk_icm42688_init() leaves INT_SOURCE0 clear, so the sensor is not
|
|
* driving INT1 yet. Enabling the PLIC route now and counting what arrives
|
|
* measures everything that is not the sensor: a pending bit left latched
|
|
* by a previous GDB-loaded image, or a floating input picking up noise.
|
|
* Without this baseline a single stale interrupt looks exactly like a
|
|
* working data-ready line, which is how one sample was mistaken for a
|
|
* healthy route.
|
|
*/
|
|
bsp_plic_enable(BSP_PMOD_IRQ);
|
|
delay_ticks(BSP_CLINT_TIMEBASE_HZ / 2u);
|
|
tdk_icm42688_diagnostics_t baseline;
|
|
tdk_icm42688_get_diagnostics(&baseline);
|
|
|
|
if(!tdk_icm42688_start())
|
|
{
|
|
printf("START FAIL\n");
|
|
bsp_uart_wait_tx_idle();
|
|
return 3;
|
|
}
|
|
/*
|
|
* Sample the PLIC pending bit while the sensor is armed and the core's
|
|
* external interrupts are unmasked. This separates the two remaining
|
|
* layers: pending asserting without the trap count moving means the
|
|
* gateway sees the line but delivery to the hart is broken, whereas
|
|
* pending never asserting means the gateway is not reacting to a pin the
|
|
* continuity test already proved is toggling.
|
|
*/
|
|
uint64_t armed_deadline = bsp_read64(BSP_CLINT_MTIME) + BSP_CLINT_TIMEBASE_HZ;
|
|
uint32_t pending_samples = 0u;
|
|
uint32_t pending_asserted = 0u;
|
|
while(bsp_read64(BSP_CLINT_MTIME) < armed_deadline)
|
|
{
|
|
if((bsp_read32(PLIC_PENDING_WORD0) & (1u << BSP_PMOD_IRQ)) != 0u)
|
|
{
|
|
pending_asserted++;
|
|
}
|
|
pending_samples++;
|
|
}
|
|
tdk_icm42688_diagnostics_t armed;
|
|
tdk_icm42688_get_diagnostics(&armed);
|
|
printf("PLIC_PENDING,armed_samples=%lu,asserted=%lu,percent=%lu,"
|
|
"mie_meie=%lu,threshold_ok=1\n",
|
|
(unsigned long)pending_samples, (unsigned long)pending_asserted,
|
|
(unsigned long)((pending_samples != 0u)
|
|
? (pending_asserted * 100u) / pending_samples : 0u),
|
|
(unsigned long)((bsp_read_mie() >> 11) & 1u));
|
|
printf("IRQ_PROBE,quiet_500ms_irq=%lu,armed_1s_irq=%lu,armed_samples=%lu,"
|
|
"spurious=%lu,expected_hz=%lu\n",
|
|
(unsigned long)baseline.irq_count,
|
|
(unsigned long)(armed.irq_count - baseline.irq_count),
|
|
(unsigned long)armed.sample_count,
|
|
(unsigned long)armed.spurious_irq_count,
|
|
(unsigned long)NOISE_ODR_HZ);
|
|
|
|
printf("CONFIG,odr_hz=%lu,accel_fs_g=%d,gyro_fs_mdps=%d,"
|
|
"ui_filter=0x%02x,tick_hz=%lu\n",
|
|
(unsigned long)config.odr_hz,
|
|
(int)config.accel_full_scale_g,
|
|
(int)config.gyro_full_scale_mdps,
|
|
config.ui_filter_config,
|
|
(unsigned long)BSP_CLINT_TIMEBASE_HZ);
|
|
printf("PLAN,discard=%lu,bias=%lu,measure=%lu,seconds=%lu\n",
|
|
(unsigned long)NOISE_DISCARD_SAMPLES,
|
|
(unsigned long)NOISE_BIAS_SAMPLES,
|
|
(unsigned long)NOISE_MEASURE_SAMPLES,
|
|
(unsigned long)((NOISE_DISCARD_SAMPLES + NOISE_BIAS_SAMPLES +
|
|
NOISE_MEASURE_SAMPLES) / NOISE_ODR_HZ));
|
|
|
|
imu_sample_t sample;
|
|
uint32_t dropouts = 0u;
|
|
|
|
/*
|
|
* Decide on the sustained rate measured above, not on a single sample.
|
|
* A lone interrupt is exactly what a stale pending bit produces, and
|
|
* whether it happens to carry DATA_RDY is chance -- so one sample was
|
|
* previously enough to report a healthy route that did not exist.
|
|
*/
|
|
{
|
|
uint64_t observed_hz = armed.sample_count;
|
|
uint64_t stale_hz = baseline.irq_count;
|
|
if(observed_hz < (NOISE_ODR_HZ / 2u))
|
|
{
|
|
#if NOISE_REQUIRE_IRQ
|
|
printf("DELIVERY,mode=FAILED,reason=rate_too_low,"
|
|
"observed_hz=%lu,expected_hz=%lu,stale_irq=%lu\n",
|
|
(unsigned long)observed_hz, (unsigned long)NOISE_ODR_HZ,
|
|
(unsigned long)stale_hz);
|
|
if((stale_hz != 0u) && (observed_hz == 0u))
|
|
{
|
|
printf("VERDICT,the only interrupts arrived before the sensor "
|
|
"was driving INT1, so they came from a stale PLIC "
|
|
"pending bit or a floating input -- not from the "
|
|
"sensor. INT1 is still not reaching PLIC %lu.\n",
|
|
(unsigned long)BSP_PMOD_IRQ);
|
|
}
|
|
else if(observed_hz == 0u)
|
|
{
|
|
printf("VERDICT,no interrupts at all. INT1 is not reaching "
|
|
"PLIC %lu; check the pin named in "
|
|
"hardware/fpga/genesys2/constraints/"
|
|
"genesys2_pmod_irq.xdc.\n",
|
|
(unsigned long)BSP_PMOD_IRQ);
|
|
}
|
|
else
|
|
{
|
|
printf("VERDICT,the route works but the source is not "
|
|
"re-arming at the configured rate.\n");
|
|
}
|
|
dump_sensor_state();
|
|
tdk_icm42688_stop();
|
|
bsp_uart_wait_tx_idle();
|
|
return 5;
|
|
#else
|
|
polled_mode = true;
|
|
printf("DELIVERY,mode=polled,reason=rate_too_low,"
|
|
"observed_hz=%lu,expected_hz=%lu,stale_irq=%lu\n",
|
|
(unsigned long)observed_hz, (unsigned long)NOISE_ODR_HZ,
|
|
(unsigned long)stale_hz);
|
|
/* Drop only the PLIC route. INT_SOURCE0 stays enabled so the
|
|
* DATA_RDY condition keeps setting INT_STATUS for polling, and
|
|
* no ISR competes to read (and clear) that register. */
|
|
bsp_plic_disable(BSP_PMOD_IRQ);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
printf("DELIVERY,mode=interrupt,rate_hz=%lu,stale_irq=%lu\n",
|
|
(unsigned long)observed_hz, (unsigned long)stale_hz);
|
|
}
|
|
}
|
|
|
|
for(uint32_t i = 0u ; i < NOISE_DISCARD_SAMPLES ; i++)
|
|
{
|
|
if(!wait_for_sample(&sample))
|
|
{
|
|
dropouts++;
|
|
}
|
|
}
|
|
|
|
/* Pass 1: the stationary mean is gravity plus accel bias, and gyro bias.
|
|
* Removing it reproduces the residual the ESKF sees once calibrated. */
|
|
double accel_sum[3] = {0.0, 0.0, 0.0};
|
|
double gyro_sum[3] = {0.0, 0.0, 0.0};
|
|
double accel_sum_squares[3] = {0.0, 0.0, 0.0};
|
|
double gyro_sum_squares[3] = {0.0, 0.0, 0.0};
|
|
uint32_t bias_count = 0u;
|
|
for(uint32_t i = 0u ; i < NOISE_BIAS_SAMPLES ; i++)
|
|
{
|
|
if(!wait_for_sample(&sample))
|
|
{
|
|
dropouts++;
|
|
continue;
|
|
}
|
|
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
|
{
|
|
accel_sum[axis] += sample.accel_mps2[axis];
|
|
gyro_sum[axis] += sample.gyro_rps[axis];
|
|
accel_sum_squares[axis] +=
|
|
sample.accel_mps2[axis] * sample.accel_mps2[axis];
|
|
gyro_sum_squares[axis] +=
|
|
sample.gyro_rps[axis] * sample.gyro_rps[axis];
|
|
}
|
|
bias_count++;
|
|
}
|
|
if(bias_count == 0u)
|
|
{
|
|
printf("NO SAMPLES: check PMOD wiring and INT1 routing\n");
|
|
bsp_uart_wait_tx_idle();
|
|
return 4;
|
|
}
|
|
|
|
double accel_mean[3];
|
|
double gyro_mean[3];
|
|
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
|
{
|
|
accel_mean[axis] = accel_sum[axis] / (double)bias_count;
|
|
gyro_mean[axis] = gyro_sum[axis] / (double)bias_count;
|
|
double accel_variance =
|
|
(accel_sum_squares[axis] / (double)bias_count) -
|
|
(accel_mean[axis] * accel_mean[axis]);
|
|
double gyro_variance =
|
|
(gyro_sum_squares[axis] / (double)bias_count) -
|
|
(gyro_mean[axis] * gyro_mean[axis]);
|
|
printf("AXIS,%u,accel_mean=%ld,accel_std=%ld,"
|
|
"gyro_mean=%ld,gyro_std=%ld\n",
|
|
axis, micro(accel_mean[axis]),
|
|
micro(sqrt(accel_variance > 0.0 ? accel_variance : 0.0)),
|
|
micro(gyro_mean[axis]),
|
|
micro(sqrt(gyro_variance > 0.0 ? gyro_variance : 0.0)));
|
|
}
|
|
printf("GRAVITY,measured=%ld,expected=9806650\n", micro(norm3(accel_mean)));
|
|
|
|
/* Pass 2: the detector inputs themselves. */
|
|
statistic_t accel_statistic;
|
|
statistic_t gyro_statistic;
|
|
statistic_t jerk_statistic;
|
|
statistic_init(&accel_statistic, "accel_norm", "um/s2",
|
|
ACCEL_HISTOGRAM_FULL_SCALE);
|
|
statistic_init(&gyro_statistic, "gyro_norm", "urad/s",
|
|
GYRO_HISTOGRAM_FULL_SCALE);
|
|
statistic_init(&jerk_statistic, "jerk_norm", "um/s3",
|
|
JERK_HISTOGRAM_FULL_SCALE);
|
|
|
|
double filtered[3] = {0.0, 0.0, 0.0};
|
|
bool have_filtered = false;
|
|
uint64_t previous_ticks = 0u;
|
|
|
|
for(uint32_t i = 0u ; i < NOISE_MEASURE_SAMPLES ; i++)
|
|
{
|
|
if(!wait_for_sample(&sample))
|
|
{
|
|
dropouts++;
|
|
continue;
|
|
}
|
|
double residual_accel[3];
|
|
double residual_gyro[3];
|
|
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
|
{
|
|
residual_accel[axis] = sample.accel_mps2[axis] - accel_mean[axis];
|
|
residual_gyro[axis] = sample.gyro_rps[axis] - gyro_mean[axis];
|
|
}
|
|
statistic_add(&accel_statistic, norm3(residual_accel));
|
|
statistic_add(&gyro_statistic, norm3(residual_gyro));
|
|
|
|
if(previous_ticks != 0u)
|
|
{
|
|
double dt_s = (double)(sample.timestamp_ticks - previous_ticks) /
|
|
(double)BSP_CLINT_TIMEBASE_HZ;
|
|
if(dt_s > 0.0)
|
|
{
|
|
double alpha = dt_s / (JERK_TIME_CONSTANT_S + dt_s);
|
|
double delta[3];
|
|
for(unsigned int axis = 0u ; axis < 3u ; axis++)
|
|
{
|
|
double previous = filtered[axis];
|
|
filtered[axis] += alpha * (residual_accel[axis] - previous);
|
|
delta[axis] = filtered[axis] - previous;
|
|
}
|
|
if(have_filtered)
|
|
{
|
|
statistic_add(&jerk_statistic, norm3(delta) / dt_s);
|
|
}
|
|
have_filtered = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
memcpy(filtered, residual_accel, sizeof(filtered));
|
|
}
|
|
previous_ticks = sample.timestamp_ticks;
|
|
|
|
if(((i + 1u) % (NOISE_ODR_HZ * 10u)) == 0u)
|
|
{
|
|
printf("PROGRESS,%lu/%lu\n", (unsigned long)(i + 1u),
|
|
(unsigned long)NOISE_MEASURE_SAMPLES);
|
|
}
|
|
}
|
|
|
|
statistic_report(&accel_statistic);
|
|
statistic_report(&gyro_statistic);
|
|
statistic_report(&jerk_statistic);
|
|
|
|
tdk_icm42688_diagnostics_t diagnostics;
|
|
tdk_icm42688_get_diagnostics(&diagnostics);
|
|
printf("HEALTH,irq=%lu,samples=%lu,invalid=%lu,spurious=%lu,"
|
|
"overflow=%lu,missed=%lu,dropouts=%lu,unexpected_irq=%lu,"
|
|
"trap_cause=%lu\n",
|
|
(unsigned long)diagnostics.irq_count,
|
|
(unsigned long)diagnostics.sample_count,
|
|
(unsigned long)diagnostics.invalid_sample_count,
|
|
(unsigned long)diagnostics.spurious_irq_count,
|
|
(unsigned long)diagnostics.queue_overflow_count,
|
|
(unsigned long)diagnostics.estimated_missed_sample_count,
|
|
(unsigned long)dropouts,
|
|
(unsigned long)unexpected_irq_count,
|
|
(unsigned long)unexpected_trap_cause);
|
|
printf("DONE\n");
|
|
|
|
tdk_icm42688_stop();
|
|
bsp_uart_wait_tx_idle();
|
|
return 0;
|
|
}
|