← Back to Projects

Rocket Avionics Firmware

September 2025 – November 2025

Overview

Embedded C++ firmware for a rocket avionics board, written as part of Northeastern's aerospace club. The firmware runs on a custom PCB built around an Adafruit Feather M0 and handles three sensor drivers (IMU, barometer, flash memory), a 50 Hz acquisition loop, filtering and event detection logic for flight phase identification, and on-board logging for post-flight data offload over serial.

Hardware

The board is built around the Feather M0 (ATSAMD21 microcontroller) with 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 packs timestamp, pressure, temperature, altitude, velocity, net acceleration, flight state, and min/max values into a fixed struct that gets streamed to flash on every tick.

Filtering & Event Detection

The biggest challenge wasn't reading the sensors, it was deciding what counted as a real flight event. The IMU is inherently noisy. Raw readings jump around even when the sensor is sitting still on a desk, which means a single sample is never enough to trust.

To handle this I used a running average over the last 10 samples. The tricky part was calibrating the window size. Too much smoothing and the system reacts too slowly to catch a real event like motor ignition. Too little and noise spikes trigger false positives. For launch detection specifically, the firmware looks for sustained acceleration above 2 to 3 G for more than 100 ms, so a single noisy spike won't falsely trigger it. Similar threshold and duration windows handle apogee detection (sustained near-zero net acceleration combined with peak altitude) and parachute deployment.

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();
}