harden sensor acquisition after audit
This commit is contained in:
+1
-1
@@ -2,6 +2,6 @@ build/
|
|||||||
sdkconfig
|
sdkconfig
|
||||||
captures/
|
captures/
|
||||||
sdkconfig.old
|
sdkconfig.old
|
||||||
dependencies.lock
|
|
||||||
managed_components/
|
managed_components/
|
||||||
|
__pycache__/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ accelerometer and L3G4200D gyroscope on a shared I2C bus.
|
|||||||
This milestone does four things:
|
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 100 Hz raw acquisition.
|
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 for validation.
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ Both breakouts share SDA, SCL, 3V3, and GND. The firmware checks both possible
|
|||||||
|
|
||||||
## Sensor configuration
|
## Sensor configuration
|
||||||
|
|
||||||
- ADXL345: 100 Hz output rate, full-resolution mode, +/-8 g. Nominal scale is
|
- ADXL345: nominal 100 Hz output rate, full-resolution mode, +/-8 g. Nominal scale is
|
||||||
3.9 mg/LSB.
|
3.9 mg/LSB.
|
||||||
- L3G4200D: 100 Hz output rate, 25 Hz bandwidth, +/-500 dps. Nominal scale is
|
- L3G4200D: nominal 100 Hz output rate, LPF2 selected with a 25 Hz cutoff,
|
||||||
17.5 mdps/LSB.
|
+/-500 dps. Nominal scale is 17.5 mdps/LSB.
|
||||||
|
|
||||||
The enclosure coordinate frame is:
|
The enclosure coordinate frame is:
|
||||||
|
|
||||||
@@ -48,7 +48,13 @@ enclosure Y = -native X
|
|||||||
enclosure Z = native Z
|
enclosure Z = native Z
|
||||||
```
|
```
|
||||||
|
|
||||||
No calibration, filtering, or sensor fusion is performed yet.
|
No software calibration, software filtering, or sensor fusion is performed yet.
|
||||||
|
|
||||||
|
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
|
||||||
|
consumer can distinguish a fresh sample from a repeated poll and identify gyro
|
||||||
|
overruns. Hardware data-ready interrupts and FIFO acquisition are deferred to the
|
||||||
|
buffering milestone.
|
||||||
|
|
||||||
## Build and flash
|
## Build and flash
|
||||||
|
|
||||||
@@ -73,9 +79,28 @@ Exit the serial monitor with `Ctrl-]`.
|
|||||||
After startup metadata, records use CSV:
|
After startup metadata, records use CSV:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
sequence,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
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
`timestamp_us` is the ESP32-C3 monotonic microsecond timer since boot. The axes
|
`poll_timestamp_us` is the ESP32-C3 monotonic time immediately before the status
|
||||||
in the first six sample columns use the enclosure frame above. The appended native
|
and data reads. It is not the sensors' physical sample time. The axes in the first
|
||||||
columns are temporary diagnostics for the coordinated orientation test.
|
six sample columns use the enclosure frame above. Native columns remain available
|
||||||
|
for diagnostics.
|
||||||
|
|
||||||
|
Status bits:
|
||||||
|
|
||||||
|
- `accel_int_source & 0x80`: an unread ADXL345 sample existed when status was
|
||||||
|
checked. If clear, treat the following sample as stale/untrusted; a sample can
|
||||||
|
arrive in the short interval between the status and data transactions.
|
||||||
|
- `accel_int_source & 0x01`: ADXL345 unread data was overwritten.
|
||||||
|
- `gyro_status & 0x08`: L3G4200D sample is fresh.
|
||||||
|
- `gyro_status & 0x80`: L3G4200D data overran before it was read.
|
||||||
|
- `loop_overrun_count`: cumulative acquisition deadlines missed; the loop
|
||||||
|
resynchronizes after a miss instead of issuing catch-up bursts.
|
||||||
|
|
||||||
|
The capture tool auto-detects a single `/dev/cu.usbmodem*` device, writes only
|
||||||
|
validated numeric records to a real CSV, and reports sequence or timing problems:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/capture_serial.py
|
||||||
|
```
|
||||||
|
|||||||
@@ -3,10 +3,16 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
#define ADXL345_REG_DEVID 0x00
|
#define ADXL345_REG_DEVID 0x00
|
||||||
|
#define ADXL345_REG_OFSX 0x1E
|
||||||
|
#define ADXL345_REG_OFSY 0x1F
|
||||||
|
#define ADXL345_REG_OFSZ 0x20
|
||||||
#define ADXL345_REG_BW_RATE 0x2C
|
#define ADXL345_REG_BW_RATE 0x2C
|
||||||
#define ADXL345_REG_POWER_CTL 0x2D
|
#define ADXL345_REG_POWER_CTL 0x2D
|
||||||
|
#define ADXL345_REG_INT_ENABLE 0x2E
|
||||||
|
#define ADXL345_REG_INT_SOURCE 0x30
|
||||||
#define ADXL345_REG_DATA_FORMAT 0x31
|
#define ADXL345_REG_DATA_FORMAT 0x31
|
||||||
#define ADXL345_REG_DATAX0 0x32
|
#define ADXL345_REG_DATAX0 0x32
|
||||||
|
#define ADXL345_REG_FIFO_CTL 0x38
|
||||||
|
|
||||||
#define ADXL345_DEVID_VALUE 0xE5
|
#define ADXL345_DEVID_VALUE 0xE5
|
||||||
#define ADXL345_BW_RATE_100_HZ 0x0A
|
#define ADXL345_BW_RATE_100_HZ 0x0A
|
||||||
@@ -46,7 +52,8 @@ esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t
|
|||||||
*sensor = (adxl345_t){0};
|
*sensor = (adxl345_t){0};
|
||||||
static const uint8_t candidate_addresses[] = {0x53, 0x1D};
|
static const uint8_t candidate_addresses[] = {0x53, 0x1D};
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(candidate_addresses); ++i) {
|
for (size_t i = 0;
|
||||||
|
i < sizeof(candidate_addresses) / sizeof(candidate_addresses[0]); ++i) {
|
||||||
const i2c_device_config_t config = {
|
const i2c_device_config_t config = {
|
||||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||||
.device_address = candidate_addresses[i],
|
.device_address = candidate_addresses[i],
|
||||||
@@ -74,8 +81,24 @@ esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t
|
|||||||
return ESP_ERR_NOT_FOUND;
|
return ESP_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure while in standby, then enter measurement mode.
|
// The ADXL345 is not reset by an ESP32 warm reboot. Establish every state
|
||||||
|
// that could otherwise survive from an earlier firmware run.
|
||||||
esp_err_t err = write_register(sensor->device, ADXL345_REG_POWER_CTL, 0x00);
|
esp_err_t err = write_register(sensor->device, ADXL345_REG_POWER_CTL, 0x00);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = write_register(sensor->device, ADXL345_REG_INT_ENABLE, 0x00);
|
||||||
|
}
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = write_register(sensor->device, ADXL345_REG_FIFO_CTL, 0x00);
|
||||||
|
}
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = write_register(sensor->device, ADXL345_REG_OFSX, 0x00);
|
||||||
|
}
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = write_register(sensor->device, ADXL345_REG_OFSY, 0x00);
|
||||||
|
}
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = write_register(sensor->device, ADXL345_REG_OFSZ, 0x00);
|
||||||
|
}
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
err = write_register(sensor->device, ADXL345_REG_DATA_FORMAT, ADXL345_FORMAT_FULL_8G);
|
err = write_register(sensor->device, ADXL345_REG_DATA_FORMAT, ADXL345_FORMAT_FULL_8G);
|
||||||
}
|
}
|
||||||
@@ -95,17 +118,45 @@ esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t
|
|||||||
err = verify_register(sensor->device, ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE);
|
err = verify_register(sensor->device, ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
adxl345_deinit(sensor);
|
||||||
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample)
|
esp_err_t adxl345_deinit(adxl345_t *sensor)
|
||||||
{
|
{
|
||||||
if (sensor == NULL || sensor->device == NULL || sample == NULL) {
|
if (sensor == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (sensor->device == NULL) {
|
||||||
|
sensor->address = 0;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = i2c_master_bus_rm_device(sensor->device);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
*sensor = (adxl345_t){0};
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample,
|
||||||
|
uint8_t *interrupt_source)
|
||||||
|
{
|
||||||
|
if (sensor == NULL || sensor->device == NULL || sample == NULL ||
|
||||||
|
interrupt_source == NULL) {
|
||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t err = read_registers(sensor->device, ADXL345_REG_INT_SOURCE,
|
||||||
|
interrupt_source, 1);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t data[6] = {0};
|
uint8_t data[6] = {0};
|
||||||
esp_err_t err = read_registers(sensor->device, ADXL345_REG_DATAX0, data, sizeof(data));
|
err = read_registers(sensor->device, ADXL345_REG_DATAX0, data, sizeof(data));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -120,4 +171,3 @@ uint8_t adxl345_address(const adxl345_t *sensor)
|
|||||||
{
|
{
|
||||||
return sensor != NULL ? sensor->address : 0;
|
return sensor != NULL ? sensor->address : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,21 @@ typedef struct {
|
|||||||
int16_t z;
|
int16_t z;
|
||||||
} adxl345_sample_t;
|
} adxl345_sample_t;
|
||||||
|
|
||||||
|
#define ADXL345_INT_SOURCE_DATA_READY 0x80
|
||||||
|
#define ADXL345_INT_SOURCE_OVERRUN 0x01
|
||||||
|
|
||||||
/** Detect, identify, and configure an ADXL345 for 100 Hz, full-resolution +/-8 g. */
|
/** Detect, identify, and configure an ADXL345 for 100 Hz, full-resolution +/-8 g. */
|
||||||
esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz);
|
esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz);
|
||||||
|
|
||||||
/** Read one sensor-native, uncalibrated XYZ sample. */
|
/** Remove the sensor from its I2C bus. Safe to call on an uninitialized handle. */
|
||||||
esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample);
|
esp_err_t adxl345_deinit(adxl345_t *sensor);
|
||||||
|
|
||||||
|
/** Read INT_SOURCE followed by one sensor-native, uncalibrated XYZ sample. */
|
||||||
|
esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample,
|
||||||
|
uint8_t *interrupt_source);
|
||||||
|
|
||||||
uint8_t adxl345_address(const adxl345_t *sensor);
|
uint8_t adxl345_address(const adxl345_t *sensor);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,21 @@ typedef struct {
|
|||||||
int16_t z;
|
int16_t z;
|
||||||
} l3g4200d_sample_t;
|
} l3g4200d_sample_t;
|
||||||
|
|
||||||
|
#define L3G4200D_STATUS_ZYXDA 0x08
|
||||||
|
#define L3G4200D_STATUS_ZYXOR 0x80
|
||||||
|
|
||||||
/** Detect, identify, and configure an L3G4200D for 100 Hz, 25 Hz BW, +/-500 dps. */
|
/** Detect, identify, and configure an L3G4200D for 100 Hz, 25 Hz BW, +/-500 dps. */
|
||||||
esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz);
|
esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz);
|
||||||
|
|
||||||
/** Read one sensor-native, uncalibrated XYZ sample. */
|
/** Remove the sensor from its I2C bus. Safe to call on an uninitialized handle. */
|
||||||
esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample);
|
esp_err_t l3g4200d_deinit(l3g4200d_t *sensor);
|
||||||
|
|
||||||
|
/** Read STATUS_REG followed by one sensor-native, uncalibrated XYZ sample. */
|
||||||
|
esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample,
|
||||||
|
uint8_t *status);
|
||||||
|
|
||||||
uint8_t l3g4200d_address(const l3g4200d_t *sensor);
|
uint8_t l3g4200d_address(const l3g4200d_t *sensor);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
#define L3G4200D_REG_CTRL3 0x22
|
#define L3G4200D_REG_CTRL3 0x22
|
||||||
#define L3G4200D_REG_CTRL4 0x23
|
#define L3G4200D_REG_CTRL4 0x23
|
||||||
#define L3G4200D_REG_CTRL5 0x24
|
#define L3G4200D_REG_CTRL5 0x24
|
||||||
|
#define L3G4200D_REG_STATUS 0x27
|
||||||
#define L3G4200D_REG_OUT_X_L 0x28
|
#define L3G4200D_REG_OUT_X_L 0x28
|
||||||
|
|
||||||
#define L3G4200D_WHO_AM_I_VALUE 0xD3
|
#define L3G4200D_WHO_AM_I_VALUE 0xD3
|
||||||
#define L3G4200D_CTRL1_100_HZ 0x1F
|
#define L3G4200D_CTRL1_100_HZ 0x1F
|
||||||
#define L3G4200D_CTRL4_500_DPS 0x90
|
#define L3G4200D_CTRL4_500_DPS 0x90
|
||||||
|
#define L3G4200D_CTRL5_LPF2_OUT 0x02
|
||||||
#define L3G4200D_AUTO_INCREMENT 0x80
|
#define L3G4200D_AUTO_INCREMENT 0x80
|
||||||
#define L3G4200D_TIMEOUT_MS 100
|
#define L3G4200D_TIMEOUT_MS 100
|
||||||
|
|
||||||
@@ -48,7 +50,8 @@ esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_
|
|||||||
*sensor = (l3g4200d_t){0};
|
*sensor = (l3g4200d_t){0};
|
||||||
static const uint8_t candidate_addresses[] = {0x69, 0x68};
|
static const uint8_t candidate_addresses[] = {0x69, 0x68};
|
||||||
|
|
||||||
for (size_t i = 0; i < sizeof(candidate_addresses); ++i) {
|
for (size_t i = 0;
|
||||||
|
i < sizeof(candidate_addresses) / sizeof(candidate_addresses[0]); ++i) {
|
||||||
const i2c_device_config_t config = {
|
const i2c_device_config_t config = {
|
||||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||||
.device_address = candidate_addresses[i],
|
.device_address = candidate_addresses[i],
|
||||||
@@ -89,7 +92,9 @@ esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_
|
|||||||
err = write_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS);
|
err = write_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS);
|
||||||
}
|
}
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
err = write_register(sensor->device, L3G4200D_REG_CTRL5, 0x00);
|
// Route LPF2 to the output. CTRL1 BW=01 selects its 25 Hz cutoff.
|
||||||
|
err = write_register(sensor->device, L3G4200D_REG_CTRL5,
|
||||||
|
L3G4200D_CTRL5_LPF2_OUT);
|
||||||
}
|
}
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
// 100 Hz ODR, 25 Hz bandwidth, normal mode, all axes enabled.
|
// 100 Hz ODR, 25 Hz bandwidth, normal mode, all axes enabled.
|
||||||
@@ -101,19 +106,49 @@ esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_
|
|||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
err = verify_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS);
|
err = verify_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS);
|
||||||
}
|
}
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = verify_register(sensor->device, L3G4200D_REG_CTRL5,
|
||||||
|
L3G4200D_CTRL5_LPF2_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
l3g4200d_deinit(sensor);
|
||||||
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample)
|
esp_err_t l3g4200d_deinit(l3g4200d_t *sensor)
|
||||||
{
|
{
|
||||||
if (sensor == NULL || sensor->device == NULL || sample == NULL) {
|
if (sensor == NULL) {
|
||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
if (sensor->device == NULL) {
|
||||||
|
sensor->address = 0;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = i2c_master_bus_rm_device(sensor->device);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
*sensor = (l3g4200d_t){0};
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample,
|
||||||
|
uint8_t *status)
|
||||||
|
{
|
||||||
|
if (sensor == NULL || sensor->device == NULL || sample == NULL || status == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = read_registers(sensor->device, L3G4200D_REG_STATUS, status, 1);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t data[6] = {0};
|
uint8_t data[6] = {0};
|
||||||
uint8_t start_register = L3G4200D_REG_OUT_X_L | L3G4200D_AUTO_INCREMENT;
|
uint8_t start_register = L3G4200D_REG_OUT_X_L | L3G4200D_AUTO_INCREMENT;
|
||||||
esp_err_t err = read_registers(sensor->device, start_register, data, sizeof(data));
|
err = read_registers(sensor->device, start_register, data, sizeof(data));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -128,4 +163,3 @@ uint8_t l3g4200d_address(const l3g4200d_t *sensor)
|
|||||||
{
|
{
|
||||||
return sensor != NULL ? sensor->address : 0;
|
return sensor != NULL ? sensor->address : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Audit Hardening — 2026-08-17
|
||||||
|
|
||||||
|
This pass addresses the findings from the independent prototype-v0 audit. It
|
||||||
|
does not add calibration, BLE, persistent storage, filtering beyond the selected
|
||||||
|
L3G4200D hardware filter, or sensor fusion.
|
||||||
|
|
||||||
|
## Git checkpoints
|
||||||
|
|
||||||
|
- `f1ac20f`: exact pre-hardening firmware and axis-validation baseline
|
||||||
|
- The verified hardening changes are committed separately after this report
|
||||||
|
|
||||||
|
No remote repository is configured; these are local history checkpoints.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- A clean build now selects `esp32c3` from `sdkconfig.defaults`.
|
||||||
|
- ADXL345 `INT_SOURCE` and L3G4200D `STATUS_REG` are read immediately before
|
||||||
|
their corresponding six-byte XYZ reads.
|
||||||
|
- Raw status bytes and a cumulative loop-overrun count are appended to CSV.
|
||||||
|
- A delayed acquisition loop resynchronizes rather than issuing catch-up bursts.
|
||||||
|
- ADXL345 FIFO, interrupt enables, and hardware offset registers are explicitly
|
||||||
|
reset during initialization so warm MCU resets are deterministic.
|
||||||
|
- L3G4200D LPF2 is routed to the output with `CTRL_REG5=0x02`, making the
|
||||||
|
configured 25 Hz cutoff effective.
|
||||||
|
- Both sensor drivers remove their I2C device handles after an initialization
|
||||||
|
failure and expose deinitializers.
|
||||||
|
- Candidate-address loops use an element count rather than byte size.
|
||||||
|
- The capture utility auto-detects a single USB modem port, writes numeric CSV
|
||||||
|
only, and reports sequence gaps, resets, timestamp anomalies, malformed
|
||||||
|
records, and ignored non-record lines.
|
||||||
|
- Documentation now distinguishes the MCU poll timestamp and nominal scale from
|
||||||
|
sensor sample timing and measured per-axis scale.
|
||||||
|
|
||||||
|
## Build validation
|
||||||
|
|
||||||
|
- Normal incremental build: pass
|
||||||
|
- Clean temporary copy with no `sdkconfig` or `build/`: pass
|
||||||
|
- Clean configuration selected `CONFIG_IDF_TARGET="esp32c3"` from
|
||||||
|
`sdkconfig.defaults`
|
||||||
|
- Hardened binary size: `0x242d0` bytes; 86% of the application partition free
|
||||||
|
|
||||||
|
## Hardware validation
|
||||||
|
|
||||||
|
The hardened firmware was flashed to the assembled XIAO ESP32-C3 prototype.
|
||||||
|
Both devices initialized successfully after a USB-triggered warm reset, including
|
||||||
|
all new register writes and the L3G4200D CTRL5 readback.
|
||||||
|
|
||||||
|
The capture `captures/hardening_freshness.csv` contains:
|
||||||
|
|
||||||
|
- 3,796 consecutive records
|
||||||
|
- Sequence gaps: 0
|
||||||
|
- Sequence resets: 0
|
||||||
|
- Timestamp anomalies: 0
|
||||||
|
- Timestamp interval: 10,000 us for all 3,795 intervals
|
||||||
|
- Rejected records: 0
|
||||||
|
- Acquisition loop overruns: 0
|
||||||
|
- ADXL345 DATA_READY clear at status time: 192 records (5.058%)
|
||||||
|
- ADXL345 overrun set: 0 records
|
||||||
|
- L3G4200D ZYXDA clear: 0 records
|
||||||
|
- L3G4200D ZYXOR set: 112 records (2.950%)
|
||||||
|
|
||||||
|
The ADXL345 clear events have a coherent 27/28-poll pattern, including adjacent
|
||||||
|
clear events when a device update likely occurs between the separate status and
|
||||||
|
data transactions. A clear bit is therefore conservatively treated as
|
||||||
|
stale/untrusted; a set bit proves unread data existed before the read. The gyro
|
||||||
|
overrun pattern directly confirms that its internal output clock is faster than
|
||||||
|
the MCU poll on this unit.
|
||||||
|
|
||||||
|
These results validate the audit finding that exact 100 Hz MCU polling does not
|
||||||
|
mean either sensor is synchronized to that clock. Status labeling is the scoped
|
||||||
|
prototype fix. Interrupt/FIFO-driven acquisition remains a later buffering task.
|
||||||
|
|
||||||
|
A final post-build/post-flash smoke capture added another 1,634 consecutive
|
||||||
|
records with zero gaps, timestamp anomalies, rejected records, ADXL345 overruns,
|
||||||
|
or acquisition-loop overruns. It again exposed ADXL345 DATA_READY clear on 81
|
||||||
|
polls and L3G4200D overrun on 48 polls.
|
||||||
@@ -4,10 +4,19 @@ The enclosure was tested with the battery end treated as the bottom of the
|
|||||||
reference orientation. The shared enclosure frame is +X right, +Y toward the
|
reference orientation. The shared enclosure frame is +X right, +Y toward the
|
||||||
top, and +Z toward the cover.
|
top, and +Z toward the cover.
|
||||||
|
|
||||||
|
## Audit clarification
|
||||||
|
|
||||||
|
The timing results below validate the ESP32-C3 polling loop, not the independent
|
||||||
|
sensor sample clocks. A subsequent review of the capture found a coherent repeated
|
||||||
|
ADXL345 sample approximately every 27 polls, consistent with an actual device ODR
|
||||||
|
near 96.3 Hz. This does not affect the static six-position axis result. Firmware v2
|
||||||
|
adds ADXL345 and L3G4200D freshness/overrun status to every record so dynamic data
|
||||||
|
can be interpreted correctly.
|
||||||
|
|
||||||
## Capture integrity
|
## Capture integrity
|
||||||
|
|
||||||
- 50,001 consecutive samples over 500.000 seconds
|
- 50,001 consecutive samples over 500.000 seconds
|
||||||
- Effective sample rate: exactly 100 Hz
|
- Effective MCU polling rate: exactly 100 Hz
|
||||||
- Timestamp interval: 10,000 us for every sample
|
- Timestamp interval: 10,000 us for every sample
|
||||||
- Sequence gaps: 0
|
- Sequence gaps: 0
|
||||||
- Axis-mapping mismatches between mapped and native columns: 0
|
- Axis-mapping mismatches between mapped and native columns: 0
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ void app_main(void)
|
|||||||
ESP_LOGE(TAG,
|
ESP_LOGE(TAG,
|
||||||
"ADXL345 not found at 0x53 or 0x1D (expected DEVID 0xE5): %s",
|
"ADXL345 not found at 0x53 or 0x1D (expected DEVID 0xE5): %s",
|
||||||
esp_err_to_name(err));
|
esp_err_to_name(err));
|
||||||
|
i2c_del_master_bus(bus);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "ADXL345 detected at 0x%02X; 100 Hz, +/-8 g, full resolution",
|
ESP_LOGI(TAG, "ADXL345 detected at 0x%02X; 100 Hz, +/-8 g, full resolution",
|
||||||
@@ -95,6 +96,8 @@ void app_main(void)
|
|||||||
ESP_LOGE(TAG,
|
ESP_LOGE(TAG,
|
||||||
"L3G4200D not found at 0x69 or 0x68 (expected WHO_AM_I 0xD3): %s",
|
"L3G4200D not found at 0x69 or 0x68 (expected WHO_AM_I 0xD3): %s",
|
||||||
esp_err_to_name(err));
|
esp_err_to_name(err));
|
||||||
|
adxl345_deinit(&accelerometer);
|
||||||
|
i2c_del_master_bus(bus);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "L3G4200D detected at 0x%02X; 100 Hz, 25 Hz BW, +/-500 dps",
|
ESP_LOGI(TAG, "L3G4200D detected at 0x%02X; 100 Hz, 25 Hz BW, +/-500 dps",
|
||||||
@@ -103,27 +106,35 @@ 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_axis_validation_v1\n");
|
printf("# format=trikke_freshness_validation_v2\n");
|
||||||
printf("# accel_scale_g_per_lsb=0.0039,gyro_scale_dps_per_lsb=0.0175\n");
|
printf("# nominal_accel_scale_g_per_lsb=0.0039,"
|
||||||
|
"nominal_gyro_scale_dps_per_lsb=0.0175\n");
|
||||||
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");
|
||||||
printf("sequence,timestamp_us,accel_x_raw,accel_y_raw,accel_z_raw,"
|
printf("# status_masks=accel_data_ready:0x80,accel_overrun:0x01,"
|
||||||
|
"gyro_data_ready:0x08,gyro_overrun:0x80\n");
|
||||||
|
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_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\n");
|
"gyro_native_x_raw,gyro_native_y_raw,gyro_native_z_raw,"
|
||||||
|
"accel_int_source,gyro_status,loop_overrun_count\n");
|
||||||
|
|
||||||
uint32_t sequence = 0;
|
uint32_t sequence = 0;
|
||||||
uint32_t read_error_count = 0;
|
uint32_t read_error_count = 0;
|
||||||
|
uint32_t loop_overrun_count = 0;
|
||||||
TickType_t last_wake = xTaskGetTickCount();
|
TickType_t last_wake = xTaskGetTickCount();
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
adxl345_sample_t accel = {0};
|
adxl345_sample_t accel = {0};
|
||||||
l3g4200d_sample_t gyro = {0};
|
l3g4200d_sample_t gyro = {0};
|
||||||
|
uint8_t accel_int_source = 0;
|
||||||
|
uint8_t gyro_status = 0;
|
||||||
const int64_t timestamp_us = esp_timer_get_time();
|
const int64_t timestamp_us = esp_timer_get_time();
|
||||||
|
|
||||||
const esp_err_t accel_err = adxl345_read_raw(&accelerometer, &accel);
|
const esp_err_t accel_err = adxl345_read_raw(&accelerometer, &accel,
|
||||||
const esp_err_t gyro_err = l3g4200d_read_raw(&gyroscope, &gyro);
|
&accel_int_source);
|
||||||
|
const esp_err_t gyro_err = l3g4200d_read_raw(&gyroscope, &gyro, &gyro_status);
|
||||||
|
|
||||||
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);
|
||||||
@@ -132,12 +143,14 @@ void app_main(void)
|
|||||||
printf("%" PRIu32 ",%" PRId64 ",%" PRId32 ",%" PRId32 ",%" PRId32
|
printf("%" PRIu32 ",%" PRId64 ",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||||
",%" PRId32 ",%" PRId32 ",%" PRId32
|
",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||||
",%" PRId16 ",%" PRId16 ",%" PRId16
|
",%" PRId16 ",%" PRId16 ",%" PRId16
|
||||||
",%" PRId16 ",%" PRId16 ",%" PRId16 "\n",
|
",%" PRId16 ",%" PRId16 ",%" PRId16
|
||||||
|
",%" PRIu8 ",%" PRIu8 ",%" PRIu32 "\n",
|
||||||
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,
|
||||||
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);
|
||||||
} else {
|
} else {
|
||||||
++read_error_count;
|
++read_error_count;
|
||||||
ESP_LOGE(TAG,
|
ESP_LOGE(TAG,
|
||||||
@@ -147,6 +160,10 @@ void app_main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
++sequence;
|
++sequence;
|
||||||
xTaskDelayUntil(&last_wake, TRIKKE_SAMPLE_TICKS);
|
if (xTaskDelayUntil(&last_wake, TRIKKE_SAMPLE_TICKS) == pdFALSE) {
|
||||||
|
// Do not issue a burst of back-to-back samples after a stalled output path.
|
||||||
|
++loop_overrun_count;
|
||||||
|
last_wake = xTaskGetTickCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,3 +1,7 @@
|
|||||||
|
# Ensure a clean checkout targets the XIAO's ESP32-C3 rather than ESP-IDF's
|
||||||
|
# default ESP32 target.
|
||||||
|
CONFIG_IDF_TARGET="esp32c3"
|
||||||
|
|
||||||
# XIAO ESP32-C3 USB-C connector uses the chip's USB Serial/JTAG console.
|
# XIAO ESP32-C3 USB-C connector uses the chip's USB Serial/JTAG console.
|
||||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
||||||
|
|
||||||
@@ -6,4 +10,3 @@ CONFIG_FREERTOS_HZ=1000
|
|||||||
|
|
||||||
# The XIAO ESP32-C3 carries 4 MB of flash.
|
# The XIAO ESP32-C3 carries 4 MB of flash.
|
||||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||||
|
|
||||||
|
|||||||
+111
-11
@@ -2,6 +2,7 @@
|
|||||||
"""Capture Trikke sensor CSV output until interrupted with Ctrl-C."""
|
"""Capture Trikke sensor CSV output until interrupted with Ctrl-C."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import glob
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -9,17 +10,61 @@ from pathlib import Path
|
|||||||
|
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
|
EXPECTED_COLUMNS = [
|
||||||
|
"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",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--port", default="/dev/cu.usbmodem1134101")
|
parser.add_argument("--port", help="serial port; auto-detected when omitted")
|
||||||
parser.add_argument("--baud", type=int, default=115200)
|
parser.add_argument(
|
||||||
|
"--baud",
|
||||||
|
type=int,
|
||||||
|
default=115200,
|
||||||
|
help="serial API baud rate (ignored by USB Serial/JTAG)",
|
||||||
|
)
|
||||||
parser.add_argument("--output", type=Path)
|
parser.add_argument("--output", type=Path)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_port(requested_port: str | None) -> str:
|
||||||
|
if requested_port:
|
||||||
|
return requested_port
|
||||||
|
|
||||||
|
candidates = sorted(glob.glob("/dev/cu.usbmodem*"))
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError("no /dev/cu.usbmodem* serial device found")
|
||||||
|
if len(candidates) > 1:
|
||||||
|
joined = ", ".join(candidates)
|
||||||
|
raise RuntimeError(f"multiple serial devices found ({joined}); specify --port")
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
try:
|
||||||
|
port = resolve_port(args.port)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"Port error: {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
output = args.output or Path("captures") / datetime.now().strftime(
|
output = args.output or Path("captures") / datetime.now().strftime(
|
||||||
"motion_%Y%m%d_%H%M%S.csv"
|
"motion_%Y%m%d_%H%M%S.csv"
|
||||||
)
|
)
|
||||||
@@ -35,30 +80,85 @@ def main() -> int:
|
|||||||
signal.signal(signal.SIGTERM, request_stop)
|
signal.signal(signal.SIGTERM, request_stop)
|
||||||
|
|
||||||
sample_count = 0
|
sample_count = 0
|
||||||
print(f"Recording {args.port} to {output}; press Ctrl-C to stop", flush=True)
|
missing_sequence_count = 0
|
||||||
|
sequence_reset_count = 0
|
||||||
|
timing_anomaly_count = 0
|
||||||
|
ignored_line_count = 0
|
||||||
|
rejected_record_count = 0
|
||||||
|
previous_sequence = None
|
||||||
|
previous_timestamp_us = None
|
||||||
|
print(f"Recording {port} to {output}; press Ctrl-C to stop", flush=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with serial.Serial(args.port, args.baud, timeout=0.25) as sensor, output.open(
|
with serial.Serial(port, args.baud, timeout=0.25) as sensor, output.open(
|
||||||
"w", encoding="utf-8", newline=""
|
"w", encoding="utf-8", newline=""
|
||||||
) as capture:
|
) as capture:
|
||||||
|
capture.write(",".join(EXPECTED_COLUMNS) + "\n")
|
||||||
|
|
||||||
while not stop_requested:
|
while not stop_requested:
|
||||||
raw_line = sensor.readline()
|
raw_line = sensor.readline()
|
||||||
if not raw_line:
|
if not raw_line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||||
capture.write(line + "\n")
|
fields = line.split(",")
|
||||||
|
|
||||||
if line and line[0].isdigit() and line.count(",") == 13:
|
if line.startswith("sequence,"):
|
||||||
sample_count += 1
|
if fields != EXPECTED_COLUMNS:
|
||||||
if sample_count % 500 == 0:
|
print("Firmware CSV header does not match capture schema", file=sys.stderr)
|
||||||
capture.flush()
|
return 3
|
||||||
print(f" {sample_count} samples captured", flush=True)
|
continue
|
||||||
|
if not line or not line[0].isdigit():
|
||||||
|
ignored_line_count += 1
|
||||||
|
continue
|
||||||
|
if len(fields) != len(EXPECTED_COLUMNS):
|
||||||
|
rejected_record_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
values = [int(field) for field in fields]
|
||||||
|
except ValueError:
|
||||||
|
rejected_record_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
sequence = values[0]
|
||||||
|
timestamp_us = values[1]
|
||||||
|
if previous_sequence is not None:
|
||||||
|
expected_sequence = (previous_sequence + 1) & 0xFFFFFFFF
|
||||||
|
if sequence != expected_sequence:
|
||||||
|
if sequence > expected_sequence:
|
||||||
|
missing_sequence_count += sequence - expected_sequence
|
||||||
|
else:
|
||||||
|
sequence_reset_count += 1
|
||||||
|
print(
|
||||||
|
f" sequence discontinuity: expected {expected_sequence}, got {sequence}",
|
||||||
|
file=sys.stderr,
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
previous_timestamp_us is not None
|
||||||
|
and timestamp_us - previous_timestamp_us != 10_000
|
||||||
|
):
|
||||||
|
timing_anomaly_count += 1
|
||||||
|
|
||||||
|
capture.write(line + "\n")
|
||||||
|
previous_sequence = sequence
|
||||||
|
previous_timestamp_us = timestamp_us
|
||||||
|
sample_count += 1
|
||||||
|
if sample_count % 500 == 0:
|
||||||
|
capture.flush()
|
||||||
|
print(f" {sample_count} samples captured", flush=True)
|
||||||
except serial.SerialException as exc:
|
except serial.SerialException as exc:
|
||||||
print(f"Serial error: {exc}", file=sys.stderr)
|
print(f"Serial error: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print(f"Stopped after {sample_count} samples; saved {output}", flush=True)
|
print(
|
||||||
|
f"Stopped after {sample_count} samples; missing={missing_sequence_count}, "
|
||||||
|
f"resets={sequence_reset_count}, timing_anomalies={timing_anomaly_count}, "
|
||||||
|
f"rejected_records={rejected_record_count}, ignored_lines={ignored_line_count}; "
|
||||||
|
f"saved {output}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user