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, plus the acceleration thresholding in C++ that identifies launch and apogee from the logged data. The board itself was the club's.
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:
- ICM-20602 IMU over I2C: 3-axis accelerometer and gyroscope, configured for ±16 g and ±2000 dps
- MS5607 barometer: pressure and temperature for altitude calculation
- S25FL512 flash memory over SPI: 64 MB on-board storage for flight logs
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, carrying timestamp, pressure, temperature, altitude, and the raw inertial channels. Derived quantities like net acceleration and flight state are resolved afterward, from the offloaded log rather than on the board.
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 answer was a running average over the last 10 samples, with the window size as the main tuning knob. Too much smoothing and a real event like motor ignition gets blurred past. Too little and noise spikes read as events. Launch is taken as sustained acceleration above 2 to 3 G for more than 100 ms, so a single spike cannot trigger it, and apogee uses the same threshold-and-duration pattern against near-zero net acceleration at peak altitude.
The thresholding runs in C++ over the offloaded log rather than live on the board. The flight build's job is to capture clean samples at 50 Hz and get them into flash; deciding which samples were launch and which were apogee happens afterward, where a window can be re-tuned and the whole flight re-scored without another launch. Moving the detection on board is what a flight computer that needs to actuate something would require.
Main Loop
The main loop runs at 50 Hz, scheduled with a simple tickEndTime guard. Every
20 ms it reads all sensors, computes altitude from pressure, packs the 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();
}