Skip to content

Latest commit

 

History

History
196 lines (131 loc) · 11 KB

DR-002-telemetry-logging.md

File metadata and controls

196 lines (131 loc) · 11 KB

DR-002: Telemetry Logging — SD Card + PCF8523 RTC Breakouts

Status: Accepted Date: 2026-02-08 Updated: 2026-02-23 Context: Black box logging for PID tuning, BNO085 driver development, and post-experiment analysis

Context

The test bench needs persistent telemetry logging for two use cases:

  1. PID tuning and control loop analysis — Recording angle, PID terms (P/I/D contributions), and motor throttles over time to diagnose oscillation, overshoot, and steady-state error. Without logs, tuning is guesswork.

  2. BNO085 driver development — The IMU driver needs characterization before it can replace the AS5600 encoder as the primary control input. Logging both sensors simultaneously with real-time timestamps enables measuring IMU lag, angle error, drift, and report rate consistency.

Currently there is no persistent logging — the only output is the display and serial print. Data is lost when the experiment ends.

Requirements

  • Real-time timestamps (wall clock, not just ticks_ms) for correlating logs across experiments
  • Persistent storage that survives power cycles and holds multiple experiment runs
  • Minimal impact on the 50 Hz control loop — logging must not starve the PID
  • CSV format for easy analysis with Python/pandas on PC

Hardware Choice

Two discrete breakout boards replace the all-in-one PiCowbell used previously:

Board Adafruit Component Details
Micro SD SPI Breakout #4682 MicroSD SPI, 3V only — MOSI=GPIO 19, MISO=GPIO 16, SCK=GPIO 18, CS=GPIO 17
PCF8523 RTC Breakout #5189 RTC PCF8523, CR1220 coin cell, I2C address 0x68, shared I2C Bus 0 (GPIO 0/1)

Separate boards remove the fixed-wiring constraint of the PiCowbell and allow independent placement.

Pin Conflicts

Conflict — SPI0 vs Pimoroni Display Pack (resolved — see DR-004): The SD card breakout and the Pimoroni Pico Display Pack both claim SPI0 on GPIO 16–19 with incompatible pin roles:

GPIO SD card breakout Pimoroni Display
16 SPI0 MISO (data from card) DC (data/command select)
17 SPI0 CS (chip select) CS (chip select)
18 SPI0 SCK SPI0 SCK
19 SPI0 MOSI SPI0 MOSI

SPI buses can host multiple devices sharing SCK/MOSI/MISO with separate CS lines. However, these two boards cannot coexist because: (1) GPIO 17 is CS for both — no way to select one without selecting the other, and (2) GPIO 16 serves fundamentally different electrical functions (bidirectional data vs unidirectional control signal). When SdSink initializes SPI0 for SD card access, it reconfigures the bus the display depends on, freezing the LCD.

SD card detect on GPIO 15 — skipped. The SD library detects the card by attempting to mount. Button Y (GPIO 15) is more valuable for the UI.

I2C bus mapping

The PCF8523 RTC shares I2C Bus 0 (GPIO 0/1) with AS5600 (0x36) and BNO085 (0x4A). No address conflict (0x68). The RTC is read once per session before the control loop starts, so it does not add any runtime traffic to the latency-sensitive sensor bus.

Decision

SD card detect on GPIO 15 — skip it. The SD library can detect the card by attempting to mount. Button Y (GPIO 15) is more valuable for the UI.

Resulting bus topology

I2C Bus 0 (GPIO 0/1, 400 kHz)
├── AS5600 encoder  [0x36]
├── BNO085 IMU      [0x4A]
└── PCF8523 RTC     [0x68]   ← read once at session start, no runtime traffic

SPI Bus 0 (GPIO 16/17/18/19)       ← conflict resolved: LCD disconnected (DR-004)
└── MicroSD card breakout

SPI0 conflict status: Resolved in DR-004. The display pack LCD is disconnected; only buttons (GPIO 12–15) and RGB LED (GPIO 6/7/8) are wired. SD card has exclusive use of SPI0. Motors moved from GPIO 6/7 to GPIO 10/11 to free RGB LED pins.

Timestamp strategy

The RTC is used only once per session to generate a human-readable log filename. All row timestamps use time.ticks_ms() (Pico's internal microsecond timer), which is a near-free register read. This avoids the cost of an I2C transaction per log row and provides the precise inter-sample timing needed for PID analysis. The RTC wall-clock time in the filename provides session correlation across days.

Log format

CSV with comma delimiter (standard CSV):

T_MS,ENC_DEG,IMU_DEG,ERR,P,I,D,PID_OUT,M1,M2
12345,+0.5,,+0.5,2.50,0.10,0.00,2.60,303,297

Fields:

  • T_MS — ticks_ms since boot
  • ENC_DEG — AS5600 angle in degrees (ground truth)
  • IMU_DEG — BNO085 angle in degrees (empty until M2)
  • ERR — PID error term
  • P, I, D — Individual PID contributions
  • PID_OUT — Raw PID output before clamping to motor range (shows PID saturation)
  • M1, M2 — Motor throttle values (integers)

Write strategy

Started with direct writes (no RAM buffering) for simplicity. Each record() call writes one line directly to the open file. This is the simplest approach and avoids buffer management complexity. RAM buffering can be added later if SD write latency causes measurable PID jitter.

File naming and organization

SD card root/
└── blackbox/
    ├── log_2026-02-14_15-30-00.csv
    ├── log_2026-02-14_16-45-12.csv
    └── ...

Files use the pattern log_YYYY-MM-DD_hh-mm-ss.csv with the RTC timestamp captured at session start. The blackbox/ subdirectory keeps telemetry logs separate from other data that may be stored on the SD card. Each file gets a CSV header row on creation.

Sampling & decoupling

The telemetry pipeline separates data collection from I/O through a facade pattern:

  • TelemetryRecorder — facade called from the main loop. Accepts all telemetry fields per cycle, handles decimation, and delegates output to a pluggable sink.
  • PrintSink — prints CSV rows to REPL serial console. No buffering needed.
  • SdSink — owns the full SD card lifecycle (mount, file create, write, unmount). Constructed with SPI and RTC pin numbers; reads the RTC once to generate the filename; mounts the SD card; creates the log file. On close, flushes the file and unmounts the SD card.

The SdSink encapsulates all hardware interaction (SPI, SoftI2C, SD card driver, filesystem mount/unmount) so the main loop only deals with pin constants and the sink interface.

Configurable decimation via TELEMETRY_SAMPLE_EVERY constant in main.py:

  • Set high (e.g., 10) for REPL output to avoid serial flood
  • Set to 1 for SD card logging to capture full-rate data
  • Decimation is cycle-count based: every N-th call to record() emits a row

SD card driver fix

The upstream sdcard.py from micropython-lib has a bug: the CRC byte's stop bit (bit 0) is not set for commands after CMD0/CMD8. The SD SPI command frame requires the end bit to always be 1 (see SD SPI command format). Most cards are lenient, but some (e.g., Philips SDHC) strictly enforce it and refuse all commands after CMD8. Fix: buf[5] = crc | 0x01 in sdcard.py.

Crash Logging

Session telemetry (SD card) is unavailable the moment the code crashes — the SD card may itself be the cause of the failure, or SPI may not yet be initialised. To preserve the exception for diagnosis, a separate crash log is written to the Pico's onboard flash.

Design constraints

  • Onboard flash only — SD card is an external peripheral and may be unavailable. Onboard flash is always present and requires no initialisation beyond the standard filesystem already mounted by MicroPython.
  • Overwrite strategy — A fixed path /crash.log is opened in write mode ("w") on every fault, replacing any previous log. This bounds storage consumption to a single file regardless of how many crashes occur.
  • No external dependencies — The crash handler uses only sys (for sys.print_exception) and utime (for ticks_ms timestamp). No I2C, SPI, GPIO, or sensor access.
  • Silent failure — The handler wraps its own I/O in a bare try/except. If flash is corrupt or full, the crash handler does nothing rather than raising a secondary exception that would obscure the original.

Content

ticks_ms: <ms since boot>
Traceback (most recent call last):
  File "main.py", line N, in <function>
<ExceptionType>: <message>

ticks_ms gives an approximate indication of how far into the boot sequence or run the crash occurred.

Integration

crash_log.py is a standalone module (deployed to Pico root alongside main.py). The top-level entry point in main.py wraps the main() call:

try:
    main()
except Exception as e:
    write_crash_log(e)
    raise

This catches all exceptions regardless of where they originate — including failures during hardware initialisation before the inner try block inside main(). The exception is still re-raised so the REPL/serial console shows the normal traceback.

Consequences

Positive

  • RTC timestamps in filenames enable correlating experiments across days/sessions
  • SD card provides practically unlimited storage for extended experiments
  • PCF8523 shares the sensor I2C bus — no extra pins needed; read once before loop starts, so no runtime latency impact
  • CSV format works directly with pandas, matplotlib, and the existing analyse_report_rate.py from BNO085 repo
  • SdSink encapsulates all SD/RTC hardware — main loop stays clean
  • Direct writes keep the implementation simple; buffering can be added if needed

Fail-fast error handling

Telemetry I/O errors (SD write failure, card ejection, filesystem full) are not caught inside the control loop. An exception in SdSink.write() propagates up, exits the PID loop, and the finally block in main() stops the motors.

This is intentional: the test bench exists to collect data for analysis and tuning. If telemetry is not being captured, continuing the run produces no useful output — the lever stabilizing without data is wasted effort. Failing fast also ensures the operator notices the problem immediately rather than discovering a truncated log file later.

A production flight controller would isolate telemetry failures from the control loop. That trade-off does not apply here.

Risks

  • SPI0 pin conflict — Resolved in DR-004 (LCD disconnected, SD card has exclusive SPI0 access).
  • SPI bandwidth — SD writes take time. If PID jitter is observed after the SPI conflict is resolved, add RAM buffering.
  • Power loss — With direct writes, the last few lines may be lost. Acceptable for a test bench.
  • SD card reliability — Frequent small writes can wear SD cards. Log files are small (few MB per run), so this is not a practical concern.
  • PCF8523 drift — RTC accuracy is ±2 ppm (~1 minute/year). Adequate for filename timestamps; not a precision time source.

Dependencies

  • sdcard.py MicroPython driver (from micropython-lib, with stop bit fix applied)
  • Motor pin reassignment in main.py (final assignment GPIO 10/11 — see DR-004)