checkpoint: working sensor acquisition and axis validation
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
build/
|
||||
sdkconfig
|
||||
captures/
|
||||
sdkconfig.old
|
||||
dependencies.lock
|
||||
managed_components/
|
||||
.DS_Store
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"idf.currentSetup": "/Users/jay/.espressif/v6.0.2/esp-idf",
|
||||
"idf.port": "/dev/tty.usbmodem1134101",
|
||||
"idf.customExtraVars": {},
|
||||
"clangd.path": "/Users/jay/.espressif/tools/esp-clang/esp-20.1.1_20250829/esp-clang/bin/clangd",
|
||||
"clangd.arguments": [
|
||||
"--background-index",
|
||||
"--query-driver=**",
|
||||
"--compile-commands-dir=/Users/jay/embedded_dev/trikkeSensor/build"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(trikke_sensor)
|
||||
@@ -0,0 +1,81 @@
|
||||
# Trikke Motion Telemetry Logger
|
||||
|
||||
Prototype v0 firmware for a Seeed Studio XIAO ESP32-C3 with an ADXL345
|
||||
accelerometer and L3G4200D gyroscope on a shared I2C bus.
|
||||
|
||||
This milestone does four things:
|
||||
|
||||
1. Detects and verifies both sensors by their identification registers.
|
||||
2. Configures each sensor for 100 Hz raw acquisition.
|
||||
3. Emits timestamped, sensor-native raw readings over the XIAO USB connection.
|
||||
4. Maps both sensors into a shared enclosure coordinate frame for validation.
|
||||
|
||||
BLE transport and phone-side storage come after the wired sensor path is proven.
|
||||
|
||||
## Wiring
|
||||
|
||||
| Signal | XIAO pin | ESP32-C3 GPIO |
|
||||
| --- | --- | --- |
|
||||
| SDA | D4 | GPIO6 |
|
||||
| SCL | D5 | GPIO7 |
|
||||
| Sensor power | 3V3 | — |
|
||||
| Sensor ground | GND | — |
|
||||
|
||||
Both breakouts share SDA, SCL, 3V3, and GND. The firmware checks both possible
|
||||
7-bit I2C addresses for each device:
|
||||
|
||||
- ADXL345: `0x53` or `0x1D`; expected `DEVID` is `0xE5`.
|
||||
- L3G4200D: `0x69` or `0x68`; expected `WHO_AM_I` is `0xD3`.
|
||||
|
||||
## Sensor configuration
|
||||
|
||||
- ADXL345: 100 Hz output rate, full-resolution mode, +/-8 g. Nominal scale is
|
||||
3.9 mg/LSB.
|
||||
- L3G4200D: 100 Hz output rate, 25 Hz bandwidth, +/-500 dps. Nominal scale is
|
||||
17.5 mdps/LSB.
|
||||
|
||||
The enclosure coordinate frame is:
|
||||
|
||||
- +X points right in the reference photograph.
|
||||
- +Y points toward the top of the enclosure.
|
||||
- +Z points out of the board toward the enclosure cover.
|
||||
|
||||
The L3G4200D already matches that frame. The ADXL345 mapping is:
|
||||
|
||||
```text
|
||||
enclosure X = native Y
|
||||
enclosure Y = -native X
|
||||
enclosure Z = native Z
|
||||
```
|
||||
|
||||
No calibration, filtering, or sensor fusion is performed yet.
|
||||
|
||||
## Build and flash
|
||||
|
||||
ESP-IDF 6.0.2 is installed at:
|
||||
|
||||
```text
|
||||
/Users/jay/.espressif/v6.0.2/esp-idf
|
||||
```
|
||||
|
||||
For each new terminal:
|
||||
|
||||
```sh
|
||||
source /Users/jay/.espressif/v6.0.2/esp-idf/export.sh
|
||||
idf.py build
|
||||
idf.py -p /dev/cu.usbmodem1134101 flash monitor
|
||||
```
|
||||
|
||||
Exit the serial monitor with `Ctrl-]`.
|
||||
|
||||
## USB output
|
||||
|
||||
After startup metadata, records use CSV:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
`timestamp_us` is the ESP32-C3 monotonic microsecond timer since boot. The axes
|
||||
in the first six sample columns use the enclosure frame above. The appended native
|
||||
columns are temporary diagnostics for the coordinated orientation test.
|
||||
@@ -0,0 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS "adxl345.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "adxl345.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#define ADXL345_REG_DEVID 0x00
|
||||
#define ADXL345_REG_BW_RATE 0x2C
|
||||
#define ADXL345_REG_POWER_CTL 0x2D
|
||||
#define ADXL345_REG_DATA_FORMAT 0x31
|
||||
#define ADXL345_REG_DATAX0 0x32
|
||||
|
||||
#define ADXL345_DEVID_VALUE 0xE5
|
||||
#define ADXL345_BW_RATE_100_HZ 0x0A
|
||||
#define ADXL345_POWER_MEASURE 0x08
|
||||
#define ADXL345_FORMAT_FULL_8G 0x0A
|
||||
#define ADXL345_TIMEOUT_MS 100
|
||||
|
||||
static esp_err_t read_registers(i2c_master_dev_handle_t device, uint8_t start_register,
|
||||
uint8_t *data, size_t length)
|
||||
{
|
||||
return i2c_master_transmit_receive(device, &start_register, 1, data, length,
|
||||
ADXL345_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
static esp_err_t write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value)
|
||||
{
|
||||
const uint8_t bytes[] = {reg, value};
|
||||
return i2c_master_transmit(device, bytes, sizeof(bytes), ADXL345_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
static esp_err_t verify_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t expected)
|
||||
{
|
||||
uint8_t actual = 0;
|
||||
esp_err_t err = read_registers(device, reg, &actual, 1);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
return actual == expected ? ESP_OK : ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
esp_err_t adxl345_init(adxl345_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz)
|
||||
{
|
||||
if (sensor == NULL || bus == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
*sensor = (adxl345_t){0};
|
||||
static const uint8_t candidate_addresses[] = {0x53, 0x1D};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidate_addresses); ++i) {
|
||||
const i2c_device_config_t config = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = candidate_addresses[i],
|
||||
.scl_speed_hz = bus_speed_hz,
|
||||
};
|
||||
|
||||
i2c_master_dev_handle_t device = NULL;
|
||||
esp_err_t err = i2c_master_bus_add_device(bus, &config, &device);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
uint8_t device_id = 0;
|
||||
err = read_registers(device, ADXL345_REG_DEVID, &device_id, 1);
|
||||
if (err == ESP_OK && device_id == ADXL345_DEVID_VALUE) {
|
||||
sensor->device = device;
|
||||
sensor->address = candidate_addresses[i];
|
||||
break;
|
||||
}
|
||||
|
||||
i2c_master_bus_rm_device(device);
|
||||
}
|
||||
|
||||
if (sensor->device == NULL) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Configure while in standby, then enter measurement mode.
|
||||
esp_err_t err = write_register(sensor->device, ADXL345_REG_POWER_CTL, 0x00);
|
||||
if (err == ESP_OK) {
|
||||
err = write_register(sensor->device, ADXL345_REG_DATA_FORMAT, ADXL345_FORMAT_FULL_8G);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = write_register(sensor->device, ADXL345_REG_BW_RATE, ADXL345_BW_RATE_100_HZ);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = write_register(sensor->device, ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = verify_register(sensor->device, ADXL345_REG_DATA_FORMAT, ADXL345_FORMAT_FULL_8G);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = verify_register(sensor->device, ADXL345_REG_BW_RATE, ADXL345_BW_RATE_100_HZ);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = verify_register(sensor->device, ADXL345_REG_POWER_CTL, ADXL345_POWER_MEASURE);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample)
|
||||
{
|
||||
if (sensor == NULL || sensor->device == NULL || sample == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t data[6] = {0};
|
||||
esp_err_t err = read_registers(sensor->device, ADXL345_REG_DATAX0, data, sizeof(data));
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
sample->x = (int16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
|
||||
sample->y = (int16_t)((uint16_t)data[2] | ((uint16_t)data[3] << 8));
|
||||
sample->z = (int16_t)((uint16_t)data[4] | ((uint16_t)data[5] << 8));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t adxl345_address(const adxl345_t *sensor)
|
||||
{
|
||||
return sensor != NULL ? sensor->address : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
i2c_master_dev_handle_t device;
|
||||
uint8_t address;
|
||||
} adxl345_t;
|
||||
|
||||
typedef struct {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t z;
|
||||
} adxl345_sample_t;
|
||||
|
||||
/** 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);
|
||||
|
||||
/** Read one sensor-native, uncalibrated XYZ sample. */
|
||||
esp_err_t adxl345_read_raw(const adxl345_t *sensor, adxl345_sample_t *sample);
|
||||
|
||||
uint8_t adxl345_address(const adxl345_t *sensor);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS "l3g4200d.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_driver_i2c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
i2c_master_dev_handle_t device;
|
||||
uint8_t address;
|
||||
} l3g4200d_t;
|
||||
|
||||
typedef struct {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t z;
|
||||
} l3g4200d_sample_t;
|
||||
|
||||
/** 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);
|
||||
|
||||
/** Read one sensor-native, uncalibrated XYZ sample. */
|
||||
esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample);
|
||||
|
||||
uint8_t l3g4200d_address(const l3g4200d_t *sensor);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "l3g4200d.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#define L3G4200D_REG_WHO_AM_I 0x0F
|
||||
#define L3G4200D_REG_CTRL1 0x20
|
||||
#define L3G4200D_REG_CTRL2 0x21
|
||||
#define L3G4200D_REG_CTRL3 0x22
|
||||
#define L3G4200D_REG_CTRL4 0x23
|
||||
#define L3G4200D_REG_CTRL5 0x24
|
||||
#define L3G4200D_REG_OUT_X_L 0x28
|
||||
|
||||
#define L3G4200D_WHO_AM_I_VALUE 0xD3
|
||||
#define L3G4200D_CTRL1_100_HZ 0x1F
|
||||
#define L3G4200D_CTRL4_500_DPS 0x90
|
||||
#define L3G4200D_AUTO_INCREMENT 0x80
|
||||
#define L3G4200D_TIMEOUT_MS 100
|
||||
|
||||
static esp_err_t read_registers(i2c_master_dev_handle_t device, uint8_t start_register,
|
||||
uint8_t *data, size_t length)
|
||||
{
|
||||
return i2c_master_transmit_receive(device, &start_register, 1, data, length,
|
||||
L3G4200D_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
static esp_err_t write_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t value)
|
||||
{
|
||||
const uint8_t bytes[] = {reg, value};
|
||||
return i2c_master_transmit(device, bytes, sizeof(bytes), L3G4200D_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
static esp_err_t verify_register(i2c_master_dev_handle_t device, uint8_t reg, uint8_t expected)
|
||||
{
|
||||
uint8_t actual = 0;
|
||||
esp_err_t err = read_registers(device, reg, &actual, 1);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
return actual == expected ? ESP_OK : ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
esp_err_t l3g4200d_init(l3g4200d_t *sensor, i2c_master_bus_handle_t bus, uint32_t bus_speed_hz)
|
||||
{
|
||||
if (sensor == NULL || bus == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
*sensor = (l3g4200d_t){0};
|
||||
static const uint8_t candidate_addresses[] = {0x69, 0x68};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidate_addresses); ++i) {
|
||||
const i2c_device_config_t config = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = candidate_addresses[i],
|
||||
.scl_speed_hz = bus_speed_hz,
|
||||
};
|
||||
|
||||
i2c_master_dev_handle_t device = NULL;
|
||||
esp_err_t err = i2c_master_bus_add_device(bus, &config, &device);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
uint8_t device_id = 0;
|
||||
err = read_registers(device, L3G4200D_REG_WHO_AM_I, &device_id, 1);
|
||||
if (err == ESP_OK && device_id == L3G4200D_WHO_AM_I_VALUE) {
|
||||
sensor->device = device;
|
||||
sensor->address = candidate_addresses[i];
|
||||
break;
|
||||
}
|
||||
|
||||
i2c_master_bus_rm_device(device);
|
||||
}
|
||||
|
||||
if (sensor->device == NULL) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Configure control registers before enabling normal mode in CTRL1.
|
||||
esp_err_t err = write_register(sensor->device, L3G4200D_REG_CTRL1, 0x00);
|
||||
if (err == ESP_OK) {
|
||||
err = write_register(sensor->device, L3G4200D_REG_CTRL2, 0x00);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = write_register(sensor->device, L3G4200D_REG_CTRL3, 0x00);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
// Block data updates while a six-byte sample is being read; select +/-500 dps.
|
||||
err = write_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = write_register(sensor->device, L3G4200D_REG_CTRL5, 0x00);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
// 100 Hz ODR, 25 Hz bandwidth, normal mode, all axes enabled.
|
||||
err = write_register(sensor->device, L3G4200D_REG_CTRL1, L3G4200D_CTRL1_100_HZ);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = verify_register(sensor->device, L3G4200D_REG_CTRL1, L3G4200D_CTRL1_100_HZ);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = verify_register(sensor->device, L3G4200D_REG_CTRL4, L3G4200D_CTRL4_500_DPS);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t l3g4200d_read_raw(const l3g4200d_t *sensor, l3g4200d_sample_t *sample)
|
||||
{
|
||||
if (sensor == NULL || sensor->device == NULL || sample == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint8_t data[6] = {0};
|
||||
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));
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
sample->x = (int16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
|
||||
sample->y = (int16_t)((uint16_t)data[2] | ((uint16_t)data[3] << 8));
|
||||
sample->z = (int16_t)((uint16_t)data[4] | ((uint16_t)data[5] << 8));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t l3g4200d_address(const l3g4200d_t *sensor)
|
||||
{
|
||||
return sensor != NULL ? sensor->address : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Enclosure Axis Validation — 2026-08-16
|
||||
|
||||
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
|
||||
top, and +Z toward the cover.
|
||||
|
||||
## Capture integrity
|
||||
|
||||
- 50,001 consecutive samples over 500.000 seconds
|
||||
- Effective sample rate: exactly 100 Hz
|
||||
- Timestamp interval: 10,000 us for every sample
|
||||
- Sequence gaps: 0
|
||||
- Axis-mapping mismatches between mapped and native columns: 0
|
||||
|
||||
The raw capture is `captures/enclosure_axis_test.csv` and is intentionally
|
||||
excluded from source control.
|
||||
|
||||
## Accelerometer orientation results
|
||||
|
||||
| Position | Mean enclosure counts | Expected dominant axis | Result |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Flat, cover up | (+4, -5, +258) | +Z | Pass |
|
||||
| Left edge down | (+258, -3, +10) | +X | Pass |
|
||||
| Right edge down | (-260, -9, +16) | -X | Pass |
|
||||
| Battery edge down | (-3, +255, +11) | +Y | Pass |
|
||||
| Top edge down | (+5, -264, +15) | -Y | Pass |
|
||||
| Cover side down | (-7, -7, -234) | -Z | Pass |
|
||||
|
||||
The paired-face results give an initial calibration estimate of:
|
||||
|
||||
```text
|
||||
offset_counts = (-1.30, -4.45, +12.20)
|
||||
counts_per_g = (258.90, 259.85, 245.70)
|
||||
```
|
||||
|
||||
These values are recorded as candidates only; calibration is not yet applied
|
||||
in firmware.
|
||||
|
||||
## Gyroscope rotation results
|
||||
|
||||
- The deliberate X rotation was X-dominant in 1,261 dynamic samples, versus
|
||||
44 Y-dominant and 35 Z-dominant samples.
|
||||
- The deliberate Z rotation was Z-dominant in all 1,008 dynamic samples. No X
|
||||
or Y sample crossed the same 500-count activity threshold.
|
||||
- Earlier side-to-side orientation changes were Y-dominant, confirming the
|
||||
remaining gyro axis.
|
||||
|
||||
The mounted sensor axes and the firmware's enclosure-frame mapping are
|
||||
consistent.
|
||||
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "trikke_sensor_main.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES adxl345 l3g4200d esp_timer esp_driver_gpio esp_driver_i2c
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "adxl345.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "l3g4200d.h"
|
||||
|
||||
// Seeed Studio XIAO ESP32-C3: D4/SDA = GPIO6, D5/SCL = GPIO7.
|
||||
#define TRIKKE_I2C_PORT I2C_NUM_0
|
||||
#define TRIKKE_I2C_SDA_GPIO GPIO_NUM_6
|
||||
#define TRIKKE_I2C_SCL_GPIO GPIO_NUM_7
|
||||
#define TRIKKE_I2C_FREQ_HZ 400000
|
||||
#define TRIKKE_SAMPLE_RATE_HZ 100
|
||||
#define TRIKKE_SAMPLE_TICKS pdMS_TO_TICKS(1000 / TRIKKE_SAMPLE_RATE_HZ)
|
||||
|
||||
static const char *TAG = "trikke";
|
||||
|
||||
typedef struct {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t z;
|
||||
} trikke_axes_sample_t;
|
||||
|
||||
static trikke_axes_sample_t map_accel_to_enclosure(const adxl345_sample_t *native)
|
||||
{
|
||||
// Enclosure frame: +X right, +Y toward the top, +Z toward the cover.
|
||||
// Mounted ADXL345: native +Y right, native +X down, native +Z toward cover.
|
||||
return (trikke_axes_sample_t) {
|
||||
.x = native->y,
|
||||
.y = -(int32_t)native->x,
|
||||
.z = native->z,
|
||||
};
|
||||
}
|
||||
|
||||
static trikke_axes_sample_t map_gyro_to_enclosure(const l3g4200d_sample_t *native)
|
||||
{
|
||||
// The mounted L3G4200D axes already match the enclosure frame.
|
||||
return (trikke_axes_sample_t) {
|
||||
.x = native->x,
|
||||
.y = native->y,
|
||||
.z = native->z,
|
||||
};
|
||||
}
|
||||
|
||||
static esp_err_t init_i2c(i2c_master_bus_handle_t *bus)
|
||||
{
|
||||
const i2c_master_bus_config_t config = {
|
||||
.i2c_port = TRIKKE_I2C_PORT,
|
||||
.sda_io_num = TRIKKE_I2C_SDA_GPIO,
|
||||
.scl_io_num = TRIKKE_I2C_SCL_GPIO,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags.enable_internal_pullup = true,
|
||||
};
|
||||
|
||||
return i2c_new_master_bus(&config, bus);
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
// Emit each CSV record immediately while testing over USB.
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
|
||||
ESP_LOGI(TAG, "Trikke motion telemetry prototype v0");
|
||||
ESP_LOGI(TAG, "I2C: SDA=GPIO%d, SCL=GPIO%d, clock=%d Hz",
|
||||
TRIKKE_I2C_SDA_GPIO, TRIKKE_I2C_SCL_GPIO, TRIKKE_I2C_FREQ_HZ);
|
||||
|
||||
i2c_master_bus_handle_t bus = NULL;
|
||||
esp_err_t err = init_i2c(&bus);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "I2C initialization failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
adxl345_t accelerometer = {0};
|
||||
err = adxl345_init(&accelerometer, bus, TRIKKE_I2C_FREQ_HZ);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG,
|
||||
"ADXL345 not found at 0x53 or 0x1D (expected DEVID 0xE5): %s",
|
||||
esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "ADXL345 detected at 0x%02X; 100 Hz, +/-8 g, full resolution",
|
||||
adxl345_address(&accelerometer));
|
||||
|
||||
l3g4200d_t gyroscope = {0};
|
||||
err = l3g4200d_init(&gyroscope, bus, TRIKKE_I2C_FREQ_HZ);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG,
|
||||
"L3G4200D not found at 0x69 or 0x68 (expected WHO_AM_I 0xD3): %s",
|
||||
esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "L3G4200D detected at 0x%02X; 100 Hz, 25 Hz BW, +/-500 dps",
|
||||
l3g4200d_address(&gyroscope));
|
||||
|
||||
// Discard the gyroscope's visible startup transient before beginning the stream.
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
printf("# format=trikke_axis_validation_v1\n");
|
||||
printf("# accel_scale_g_per_lsb=0.0039,gyro_scale_dps_per_lsb=0.0175\n");
|
||||
printf("# enclosure_axes=+x:right,+y:top,+z:toward_cover\n");
|
||||
printf("# mapping=accel(x,y,z)=(native_y,-native_x,native_z);"
|
||||
"gyro(x,y,z)=(native_x,native_y,native_z)\n");
|
||||
printf("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\n");
|
||||
|
||||
uint32_t sequence = 0;
|
||||
uint32_t read_error_count = 0;
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
|
||||
while (true) {
|
||||
adxl345_sample_t accel = {0};
|
||||
l3g4200d_sample_t gyro = {0};
|
||||
const int64_t timestamp_us = esp_timer_get_time();
|
||||
|
||||
const esp_err_t accel_err = adxl345_read_raw(&accelerometer, &accel);
|
||||
const esp_err_t gyro_err = l3g4200d_read_raw(&gyroscope, &gyro);
|
||||
|
||||
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_gyro = map_gyro_to_enclosure(&gyro);
|
||||
|
||||
printf("%" PRIu32 ",%" PRId64 ",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||
",%" PRId32 ",%" PRId32 ",%" PRId32
|
||||
",%" PRId16 ",%" PRId16 ",%" PRId16
|
||||
",%" PRId16 ",%" PRId16 ",%" PRId16 "\n",
|
||||
sequence, timestamp_us,
|
||||
enclosure_accel.x, enclosure_accel.y, enclosure_accel.z,
|
||||
enclosure_gyro.x, enclosure_gyro.y, enclosure_gyro.z,
|
||||
accel.x, accel.y, accel.z,
|
||||
gyro.x, gyro.y, gyro.z);
|
||||
} else {
|
||||
++read_error_count;
|
||||
ESP_LOGE(TAG,
|
||||
"sample %" PRIu32 " read failed (accel=%s, gyro=%s, total_errors=%" PRIu32 ")",
|
||||
sequence, esp_err_to_name(accel_err), esp_err_to_name(gyro_err),
|
||||
read_error_count);
|
||||
}
|
||||
|
||||
++sequence;
|
||||
xTaskDelayUntil(&last_wake, TRIKKE_SAMPLE_TICKS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# XIAO ESP32-C3 USB-C connector uses the chip's USB Serial/JTAG console.
|
||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
||||
|
||||
# A 1 kHz RTOS tick gives the 100 Hz acquisition task a 1 ms scheduler quantum.
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
# The XIAO ESP32-C3 carries 4 MB of flash.
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture Trikke sensor CSV output until interrupted with Ctrl-C."""
|
||||
|
||||
import argparse
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import serial
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--port", default="/dev/cu.usbmodem1134101")
|
||||
parser.add_argument("--baud", type=int, default=115200)
|
||||
parser.add_argument("--output", type=Path)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
output = args.output or Path("captures") / datetime.now().strftime(
|
||||
"motion_%Y%m%d_%H%M%S.csv"
|
||||
)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
stop_requested = False
|
||||
|
||||
def request_stop(_signum: int, _frame: object) -> None:
|
||||
nonlocal stop_requested
|
||||
stop_requested = True
|
||||
|
||||
signal.signal(signal.SIGINT, request_stop)
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
|
||||
sample_count = 0
|
||||
print(f"Recording {args.port} to {output}; press Ctrl-C to stop", flush=True)
|
||||
|
||||
try:
|
||||
with serial.Serial(args.port, args.baud, timeout=0.25) as sensor, output.open(
|
||||
"w", encoding="utf-8", newline=""
|
||||
) as capture:
|
||||
while not stop_requested:
|
||||
raw_line = sensor.readline()
|
||||
if not raw_line:
|
||||
continue
|
||||
|
||||
line = raw_line.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
capture.write(line + "\n")
|
||||
|
||||
if line and line[0].isdigit() and line.count(",") == 13:
|
||||
sample_count += 1
|
||||
if sample_count % 500 == 0:
|
||||
capture.flush()
|
||||
print(f" {sample_count} samples captured", flush=True)
|
||||
except serial.SerialException as exc:
|
||||
print(f"Serial error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Stopped after {sample_count} samples; saved {output}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user