← Back to Projects

Rocket Avionics Firmware

September 2025 – November 2025

Overview

Embedded C++ data acquisition firmware for the rocket avionics stack at Northeastern's aerospace club. The firmware runs on an Adafruit Feather M0 and handles three sensor drivers (IMU, barometer, flash memory), a 50 Hz acquisition loop, and on-board logging with a serial command interface for post-flight data offload.

My contribution was the driver and logging layer. The board itself was the club's, and the flight-phase state machine described below was specified but never landed in the flight build.

Hardware

The stack runs on an Adafruit Feather M0 with an ATSAMD21 microcontroller. That is an off-the-shelf dev board, not a board I designed. It carries three sensors:

IMU Driver

The IMU driver talks to the ICM-20602 over I2C at 400 kHz. To read a full sample, the driver writes the accelerometer register address (ACCEL_XOUT_H), then requests 14 bytes back in a single transaction: 6 bytes accel, 2 bytes temperature, 6 bytes gyro.

The sensor returns each measurement as two separate bytes (high and low). To reconstruct the 16-bit signed values, the driver shifts the high byte left 8 positions and ORs it with the low byte:

Wire.beginTransmission(ICM20602_ADDR);
Wire.write(ACCEL_XOUT_H);
Wire.endTransmission(false);
Wire.requestFrom(ICM20602_ADDR, (uint8_t)14);

uint8_t buf[14];
for (int i = 0; i < 14; i++) {
    buf[i] = Wire.read();
}

accelData[0] = (int16_t)((buf[0] << 8) | buf[1]);
accelData[1] = (int16_t)((buf[2] << 8) | buf[3]);
accelData[2] = (int16_t)((buf[4] << 8) | buf[5]);
tempRaw      = (int16_t)((buf[6] << 8) | buf[7]);
gyroData[0]  = (int16_t)((buf[8] << 8) | buf[9]);
gyroData[1]  = (int16_t)((buf[10] << 8) | buf[11]);
gyroData[2]  = (int16_t)((buf[12] << 8) | buf[13]);

Reading all seven channels in one transaction is faster than separate reads and keeps the samples time-aligned. After the parse step the raw counts are scaled to m/s² and rad/s using the configured full-scale ranges.

Flash Logging

The S25FL512 driver handles SPI writes to flash in 512-byte page chunks, with a serial CLI that exposes erase, offload, and print commands for post-flight data recovery. Each log record is a fixed-size struct streamed to flash on every tick. Timestamp, pressure, temperature, and altitude carry real measurements. The struct also reserves fields for velocity, net acceleration, and flight state, which the flight build left at zero.

Event Detection

The hardest part of the project was not reading the sensors, it was deciding what counted as a real flight event. The IMU is noisy enough that readings jump around even sitting still on a desk, so a single sample is never enough to trust.

The approach I worked out was a running average over the last 10 samples, with the window size as the main tuning knob. Too much smoothing and the system reacts too slowly to catch motor ignition. Too little and noise spikes trigger false positives. Launch detection would look for sustained acceleration above 2 to 3 G for more than 100 ms, so a single spike could not fire it, and apogee and parachute deployment would use the same threshold and duration pattern.

None of this made it into the flight build. The log record reserves fields for velocity, net acceleration, and flight state, but the firmware writes zeros to all three. What actually flew was raw sample logging at 50 Hz, with flight phases intended to be derived offline from the logged data. Implementing the state machine on board is the obvious next step.

Main Loop

The main loop runs at 50 Hz, scheduled with a simple tickEndTime guard. Every 20 ms it reads all sensors, computes derived values like net acceleration and altitude, packs the result into a log record, runs the CLI handler, and toggles a status LED so the board has visual feedback during ground testing.

void loop() {
    while (millis() < tickEndTime) {}
    tickEndTime = millis() + 20;

    barometer.read();
    imu.read();

    const float pressurePa = barometer.getPressurePa();
    const float altitudeM = MS5607::calculateAltitudeM(pressurePa);
    const Vector3D_s accelerationMSS = imu.getAccelerationsMSS();
    const float netAccelerationMss = sqrt(
        accelerationMSS.x * accelerationMSS.x +
        accelerationMSS.y * accelerationMSS.y +
        accelerationMSS.z * accelerationMSS.z);

    logData.timestampMs = millis();
    logData.altitudeM = altitudeM;
    // ... pack remaining fields and log
    runCLI();
}