calibrate enclosure motion outputs
This commit is contained in:
@@ -8,7 +8,8 @@ This milestone does four things:
|
|||||||
1. Detects and verifies both sensors by their identification registers.
|
1. Detects and verifies both sensors by their identification registers.
|
||||||
2. Configures each sensor for a nominal 100 Hz raw output rate.
|
2. Configures each sensor for a nominal 100 Hz raw output rate.
|
||||||
3. Emits timestamped, sensor-native raw readings over the XIAO USB connection.
|
3. Emits timestamped, sensor-native raw readings over the XIAO USB connection.
|
||||||
4. Maps both sensors into a shared enclosure coordinate frame for validation.
|
4. Maps both sensors into a shared enclosure coordinate frame and emits both raw
|
||||||
|
and calibrated readings.
|
||||||
|
|
||||||
BLE transport and phone-side storage come after the wired sensor path is proven.
|
BLE transport and phone-side storage come after the wired sensor path is proven.
|
||||||
|
|
||||||
@@ -48,7 +49,11 @@ enclosure Y = -native X
|
|||||||
enclosure Z = native Z
|
enclosure Z = native Z
|
||||||
```
|
```
|
||||||
|
|
||||||
No software calibration, software filtering, or sensor fusion is performed yet.
|
Software calibration is applied after enclosure-axis mapping. Accelerometer
|
||||||
|
offset and per-axis scale were measured with a six-face enclosure test. Gyroscope
|
||||||
|
zero-rate bias and polarity were measured; its 17.5 mdps/LSB scale remains the
|
||||||
|
nominal datasheet value. Sensor-native and mapped raw counts remain in every
|
||||||
|
record for diagnostics. No software filtering or sensor fusion is performed yet.
|
||||||
|
|
||||||
The ESP32-C3 polls at exactly 100 Hz, but each sensor has an independent internal
|
The ESP32-C3 polls at exactly 100 Hz, but each sensor has an independent internal
|
||||||
sample clock. The status registers are read immediately before each XYZ read so a
|
sample clock. The status registers are read immediately before each XYZ read so a
|
||||||
@@ -79,13 +84,14 @@ Exit the serial monitor with `Ctrl-]`.
|
|||||||
After startup metadata, records use CSV:
|
After startup metadata, records use CSV:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
sequence,poll_timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw,gyro_x_raw,gyro_y_raw,gyro_z_raw,accel_native_x_raw,accel_native_y_raw,accel_native_z_raw,gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw,accel_int_source,gyro_status,loop_overrun_count
|
sequence,poll_timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw,gyro_x_raw,gyro_y_raw,gyro_z_raw,accel_x_mg,accel_y_mg,accel_z_mg,gyro_x_mdps,gyro_y_mdps,gyro_z_mdps,accel_native_x_raw,accel_native_y_raw,accel_native_z_raw,gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw,accel_int_source,gyro_status,loop_overrun_count
|
||||||
```
|
```
|
||||||
|
|
||||||
`poll_timestamp_us` is the ESP32-C3 monotonic time immediately before the status
|
`poll_timestamp_us` is the ESP32-C3 monotonic time immediately before the status
|
||||||
and data reads. It is not the sensors' physical sample time. The axes in the first
|
and data reads. It is not the sensors' physical sample time. The axes in the first
|
||||||
six sample columns use the enclosure frame above. Native columns remain available
|
six sample columns use the enclosure frame above. Calibrated acceleration is in
|
||||||
for diagnostics.
|
integer milligravity (`mg`), and bias-corrected angular rate is in integer
|
||||||
|
millidegrees per second (`mdps`). Native columns remain available for diagnostics.
|
||||||
|
|
||||||
Status bits:
|
Status bits:
|
||||||
|
|
||||||
|
|||||||
@@ -56,5 +56,5 @@ in firmware.
|
|||||||
|
|
||||||
The mounted sensor axes and the firmware's enclosure-frame axis assignments are
|
The mounted sensor axes and the firmware's enclosure-frame axis assignments are
|
||||||
consistent. These tests established the dominant gyro axis but did not establish
|
consistent. These tests established the dominant gyro axis but did not establish
|
||||||
gyro polarity; directed positive and negative rotations remain part of the
|
gyro polarity. The subsequent 2026-08-17 calibration pass used directed positive
|
||||||
calibration pass.
|
and negative rotations and confirmed polarity on all three axes.
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Sensor Calibration — 2026-08-17
|
||||||
|
|
||||||
|
Calibration used one continuous 83,229-record hardware capture. The enclosure
|
||||||
|
was held stationary on all six faces, then returned flat for three explicitly
|
||||||
|
directed rotations around each enclosure axis. Position changes remained in the
|
||||||
|
recording so freshness behavior could also be checked during motion.
|
||||||
|
|
||||||
|
The raw capture is `captures/calibration_session_20260817.csv` and is
|
||||||
|
intentionally excluded from source control.
|
||||||
|
|
||||||
|
## Capture integrity
|
||||||
|
|
||||||
|
- Sequence gaps and resets: 0
|
||||||
|
- Timestamp anomalies: 0
|
||||||
|
- Rejected or ignored records: 0
|
||||||
|
- Acquisition-loop overruns: 0
|
||||||
|
- ADXL345 overruns: 0
|
||||||
|
- ADXL345 DATA_READY clear: 4,282 records
|
||||||
|
- L3G4200D ZYXDA clear: 0 records
|
||||||
|
- L3G4200D ZYXOR set: 2,456 records
|
||||||
|
|
||||||
|
## Accelerometer
|
||||||
|
|
||||||
|
Only records with ADXL345 DATA_READY set were used in the stationary face means.
|
||||||
|
The paired positive and negative faces produced these enclosure-frame
|
||||||
|
coefficients:
|
||||||
|
|
||||||
|
| Axis | Offset (counts) | Scale (counts/g) | Scale (mg/LSB) |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| X | -1.417678 | 258.890661 | 3.862635 |
|
||||||
|
| Y | -4.400892 | 259.825135 | 3.848742 |
|
||||||
|
| Z | +11.776132 | 245.755573 | 4.069084 |
|
||||||
|
|
||||||
|
Firmware converts a mapped raw value to integer milligravity with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
accel_mg = round((raw_counts - offset_counts) * 1000 / counts_per_g)
|
||||||
|
```
|
||||||
|
|
||||||
|
The ADXL345 hardware offset registers remain zero. Calibration is deliberately
|
||||||
|
performed in software so raw readings remain recoverable and the coefficients
|
||||||
|
remain explicit.
|
||||||
|
|
||||||
|
## Gyroscope
|
||||||
|
|
||||||
|
The central five thousand samples of the flat stationary interval produced:
|
||||||
|
|
||||||
|
| Axis | Zero-rate bias (counts) | Stationary SD (counts) |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| X | +9.0482 | 9.8933 |
|
||||||
|
| Y | -153.6198 | 9.5714 |
|
||||||
|
| Z | -7.0238 | 10.4259 |
|
||||||
|
|
||||||
|
The instructed positive motion was top/USB-edge lift for +X, left-edge lift for
|
||||||
|
+Y, and counterclockwise rotation viewed from the cover for +Z. Every outward
|
||||||
|
stroke was positive on its intended gyro channel, and every return stroke was
|
||||||
|
negative. Axis assignment and polarity are therefore confirmed.
|
||||||
|
|
||||||
|
Firmware subtracts the measured zero-rate bias and converts with the nominal
|
||||||
|
L3G4200D +/-500 dps scale of 17.5 mdps/LSB. Gyro scale itself was not measured
|
||||||
|
because no controlled angular-rate reference was available.
|
||||||
|
|
||||||
|
## Freshness under motion
|
||||||
|
|
||||||
|
Using gyro magnitude greater than 500 counts from stationary bias as a
|
||||||
|
conservative motion selector gave 8,233 moving records. Of the 425 records with
|
||||||
|
ADXL345 DATA_READY clear, 300 (70.6%) exactly repeated the preceding XYZ tuple.
|
||||||
|
Of 7,808 records with DATA_READY set, 85 (1.1%) repeated the tuple. Identical
|
||||||
|
fresh samples remain possible from quantization, pauses, or rotation about the
|
||||||
|
gravity vector, but the strong enrichment confirms that the status flag is
|
||||||
|
practically useful under motion.
|
||||||
|
|
||||||
|
The clear flag remains conservative: 125 moving records changed values despite
|
||||||
|
DATA_READY being clear at the earlier status transaction, consistent with a new
|
||||||
|
sample arriving between the separate status and data reads.
|
||||||
|
|
||||||
|
## Firmware verification
|
||||||
|
|
||||||
|
The calibrated CSV-v3 firmware was built, flashed to the assembled prototype,
|
||||||
|
and checked with a 3,814-record flat smoke capture:
|
||||||
|
|
||||||
|
- Sequence gaps, resets, timestamp anomalies, and rejected records: 0
|
||||||
|
- Acquisition-loop and ADXL345 overruns: 0
|
||||||
|
- Independently recomputed calibrated fields differing from firmware output: 0
|
||||||
|
- Mean calibrated acceleration: (+19.5, -5.6, +999.9) mg
|
||||||
|
- Mean bias-corrected gyro: (+25.4, -62.9, -54.8) mdps
|
||||||
|
|
||||||
|
The small residual horizontal acceleration is consistent with the enclosure not
|
||||||
|
being perfectly level. The largest residual gyro mean is 0.063 dps.
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
#include <math.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "adxl345.h"
|
#include "adxl345.h"
|
||||||
@@ -19,6 +20,20 @@
|
|||||||
#define TRIKKE_SAMPLE_RATE_HZ 100
|
#define TRIKKE_SAMPLE_RATE_HZ 100
|
||||||
#define TRIKKE_SAMPLE_TICKS pdMS_TO_TICKS(1000 / TRIKKE_SAMPLE_RATE_HZ)
|
#define TRIKKE_SAMPLE_TICKS pdMS_TO_TICKS(1000 / TRIKKE_SAMPLE_RATE_HZ)
|
||||||
|
|
||||||
|
// Software calibration from the 2026-08-17 enclosure six-face capture.
|
||||||
|
// Accelerometer coefficients are measured. Gyroscope scale is nominal; its
|
||||||
|
// zero-rate biases and all three axis polarities were measured.
|
||||||
|
#define TRIKKE_ACCEL_X_OFFSET_COUNTS (-1.417678f)
|
||||||
|
#define TRIKKE_ACCEL_Y_OFFSET_COUNTS (-4.400892f)
|
||||||
|
#define TRIKKE_ACCEL_Z_OFFSET_COUNTS (11.776132f)
|
||||||
|
#define TRIKKE_ACCEL_X_COUNTS_PER_G (258.890661f)
|
||||||
|
#define TRIKKE_ACCEL_Y_COUNTS_PER_G (259.825135f)
|
||||||
|
#define TRIKKE_ACCEL_Z_COUNTS_PER_G (245.755573f)
|
||||||
|
#define TRIKKE_GYRO_X_BIAS_COUNTS (9.0482f)
|
||||||
|
#define TRIKKE_GYRO_Y_BIAS_COUNTS (-153.6198f)
|
||||||
|
#define TRIKKE_GYRO_Z_BIAS_COUNTS (-7.0238f)
|
||||||
|
#define TRIKKE_GYRO_MDPS_PER_LSB (17.5f)
|
||||||
|
|
||||||
static const char *TAG = "trikke";
|
static const char *TAG = "trikke";
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -48,6 +63,30 @@ static trikke_axes_sample_t map_gyro_to_enclosure(const l3g4200d_sample_t *nativ
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static trikke_axes_sample_t calibrate_accel_mg(const trikke_axes_sample_t *raw)
|
||||||
|
{
|
||||||
|
return (trikke_axes_sample_t) {
|
||||||
|
.x = lroundf(((float)raw->x - TRIKKE_ACCEL_X_OFFSET_COUNTS) *
|
||||||
|
1000.0f / TRIKKE_ACCEL_X_COUNTS_PER_G),
|
||||||
|
.y = lroundf(((float)raw->y - TRIKKE_ACCEL_Y_OFFSET_COUNTS) *
|
||||||
|
1000.0f / TRIKKE_ACCEL_Y_COUNTS_PER_G),
|
||||||
|
.z = lroundf(((float)raw->z - TRIKKE_ACCEL_Z_OFFSET_COUNTS) *
|
||||||
|
1000.0f / TRIKKE_ACCEL_Z_COUNTS_PER_G),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static trikke_axes_sample_t calibrate_gyro_mdps(const trikke_axes_sample_t *raw)
|
||||||
|
{
|
||||||
|
return (trikke_axes_sample_t) {
|
||||||
|
.x = lroundf(((float)raw->x - TRIKKE_GYRO_X_BIAS_COUNTS) *
|
||||||
|
TRIKKE_GYRO_MDPS_PER_LSB),
|
||||||
|
.y = lroundf(((float)raw->y - TRIKKE_GYRO_Y_BIAS_COUNTS) *
|
||||||
|
TRIKKE_GYRO_MDPS_PER_LSB),
|
||||||
|
.z = lroundf(((float)raw->z - TRIKKE_GYRO_Z_BIAS_COUNTS) *
|
||||||
|
TRIKKE_GYRO_MDPS_PER_LSB),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static esp_err_t init_i2c(i2c_master_bus_handle_t *bus)
|
static esp_err_t init_i2c(i2c_master_bus_handle_t *bus)
|
||||||
{
|
{
|
||||||
const i2c_master_bus_config_t config = {
|
const i2c_master_bus_config_t config = {
|
||||||
@@ -106,9 +145,16 @@ void app_main(void)
|
|||||||
// Discard the gyroscope's visible startup transient before beginning the stream.
|
// Discard the gyroscope's visible startup transient before beginning the stream.
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
|
||||||
printf("# format=trikke_freshness_validation_v2\n");
|
printf("# format=trikke_calibrated_v3\n");
|
||||||
printf("# nominal_accel_scale_g_per_lsb=0.0039,"
|
printf("# accel_calibration=offset_counts:(%.6f,%.6f,%.6f),"
|
||||||
"nominal_gyro_scale_dps_per_lsb=0.0175\n");
|
"counts_per_g:(%.6f,%.6f,%.6f)\n",
|
||||||
|
TRIKKE_ACCEL_X_OFFSET_COUNTS, TRIKKE_ACCEL_Y_OFFSET_COUNTS,
|
||||||
|
TRIKKE_ACCEL_Z_OFFSET_COUNTS, TRIKKE_ACCEL_X_COUNTS_PER_G,
|
||||||
|
TRIKKE_ACCEL_Y_COUNTS_PER_G, TRIKKE_ACCEL_Z_COUNTS_PER_G);
|
||||||
|
printf("# gyro_calibration=bias_counts:(%.4f,%.4f,%.4f),"
|
||||||
|
"nominal_mdps_per_lsb:%.1f,polarity:verified\n",
|
||||||
|
TRIKKE_GYRO_X_BIAS_COUNTS, TRIKKE_GYRO_Y_BIAS_COUNTS,
|
||||||
|
TRIKKE_GYRO_Z_BIAS_COUNTS, TRIKKE_GYRO_MDPS_PER_LSB);
|
||||||
printf("# enclosure_axes=+x:right,+y:top,+z:toward_cover\n");
|
printf("# enclosure_axes=+x:right,+y:top,+z:toward_cover\n");
|
||||||
printf("# mapping=accel(x,y,z)=(native_y,-native_x,native_z);"
|
printf("# mapping=accel(x,y,z)=(native_y,-native_x,native_z);"
|
||||||
"gyro(x,y,z)=(native_x,native_y,native_z)\n");
|
"gyro(x,y,z)=(native_x,native_y,native_z)\n");
|
||||||
@@ -116,6 +162,8 @@ void app_main(void)
|
|||||||
"gyro_data_ready:0x08,gyro_overrun:0x80\n");
|
"gyro_data_ready:0x08,gyro_overrun:0x80\n");
|
||||||
printf("sequence,poll_timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw,"
|
printf("sequence,poll_timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw,"
|
||||||
"gyro_x_raw,gyro_y_raw,gyro_z_raw,"
|
"gyro_x_raw,gyro_y_raw,gyro_z_raw,"
|
||||||
|
"accel_x_mg,accel_y_mg,accel_z_mg,"
|
||||||
|
"gyro_x_mdps,gyro_y_mdps,gyro_z_mdps,"
|
||||||
"accel_native_x_raw,accel_native_y_raw,accel_native_z_raw,"
|
"accel_native_x_raw,accel_native_y_raw,accel_native_z_raw,"
|
||||||
"gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw,"
|
"gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw,"
|
||||||
"accel_int_source,gyro_status,loop_overrun_count\n");
|
"accel_int_source,gyro_status,loop_overrun_count\n");
|
||||||
@@ -139,8 +187,14 @@ void app_main(void)
|
|||||||
if (accel_err == ESP_OK && gyro_err == ESP_OK) {
|
if (accel_err == ESP_OK && gyro_err == ESP_OK) {
|
||||||
const trikke_axes_sample_t enclosure_accel = map_accel_to_enclosure(&accel);
|
const trikke_axes_sample_t enclosure_accel = map_accel_to_enclosure(&accel);
|
||||||
const trikke_axes_sample_t enclosure_gyro = map_gyro_to_enclosure(&gyro);
|
const trikke_axes_sample_t enclosure_gyro = map_gyro_to_enclosure(&gyro);
|
||||||
|
const trikke_axes_sample_t calibrated_accel =
|
||||||
|
calibrate_accel_mg(&enclosure_accel);
|
||||||
|
const trikke_axes_sample_t calibrated_gyro =
|
||||||
|
calibrate_gyro_mdps(&enclosure_gyro);
|
||||||
|
|
||||||
printf("%" PRIu32 ",%" PRId64 ",%" PRId32 ",%" PRId32 ",%" PRId32
|
printf("%" PRIu32 ",%" PRId64 ",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||||
|
",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||||
|
",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||||
",%" PRId32 ",%" PRId32 ",%" PRId32
|
",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||||
",%" PRId16 ",%" PRId16 ",%" PRId16
|
",%" PRId16 ",%" PRId16 ",%" PRId16
|
||||||
",%" PRId16 ",%" PRId16 ",%" PRId16
|
",%" PRId16 ",%" PRId16 ",%" PRId16
|
||||||
@@ -148,6 +202,8 @@ void app_main(void)
|
|||||||
sequence, timestamp_us,
|
sequence, timestamp_us,
|
||||||
enclosure_accel.x, enclosure_accel.y, enclosure_accel.z,
|
enclosure_accel.x, enclosure_accel.y, enclosure_accel.z,
|
||||||
enclosure_gyro.x, enclosure_gyro.y, enclosure_gyro.z,
|
enclosure_gyro.x, enclosure_gyro.y, enclosure_gyro.z,
|
||||||
|
calibrated_accel.x, calibrated_accel.y, calibrated_accel.z,
|
||||||
|
calibrated_gyro.x, calibrated_gyro.y, calibrated_gyro.z,
|
||||||
accel.x, accel.y, accel.z,
|
accel.x, accel.y, accel.z,
|
||||||
gyro.x, gyro.y, gyro.z,
|
gyro.x, gyro.y, gyro.z,
|
||||||
accel_int_source, gyro_status, loop_overrun_count);
|
accel_int_source, gyro_status, loop_overrun_count);
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ EXPECTED_COLUMNS = [
|
|||||||
"gyro_x_raw",
|
"gyro_x_raw",
|
||||||
"gyro_y_raw",
|
"gyro_y_raw",
|
||||||
"gyro_z_raw",
|
"gyro_z_raw",
|
||||||
|
"accel_x_mg",
|
||||||
|
"accel_y_mg",
|
||||||
|
"accel_z_mg",
|
||||||
|
"gyro_x_mdps",
|
||||||
|
"gyro_y_mdps",
|
||||||
|
"gyro_z_mdps",
|
||||||
"accel_native_x_raw",
|
"accel_native_x_raw",
|
||||||
"accel_native_y_raw",
|
"accel_native_y_raw",
|
||||||
"accel_native_z_raw",
|
"accel_native_z_raw",
|
||||||
|
|||||||
Reference in New Issue
Block a user