Introduction

I want to build a small, low-cost, (low-power?) high altitude balloon that "punches above its weight class" in terms of functionality and is grown from the ground up in a Rust ecosystem. The ingredients for this kind of project have been around for a while, and I the last time I tried to do this I got stuck in a few places I'm ready to get past this time.

The previous project suffered from a lack of discipline, way too much complexity, and honest-to-god fear of tools and libraries in Rust.

Here's what I'm thinking...

Rust everywhere

I want to use Rust for the software and firmware. Embedded Rust is an interesting area and evolving quickly. I want to learn it. I really enjoy working in Rust and the Cargo toolchain is incredible.

The embedded Rust ecosystem is growing quickly, and I think it's a good idea to use it. The higher level software (like what would run on a Raspberry Pi) can be written in Rust, and more institutions are starting to use Rust for their code so it's a good skill to have.

Other systems like Bevy and remote protocol or networking tools in Rust are growing quickly as well and I think they are suited perfectly for the kinds of needs I will have with this project. Using a game engine like Bevy for modeling and simulation is somewhat novel but I think it's a good fit since it is also Rust native and is fully featured.

Everything in the loop

I want to develop the software and firmware with tests that can be run directly on the hardware with everything in the loop. I want to be able to test the software with "fake" inputs and outputs that react as I expect them to in real life. The goal is to have everything working on my desk and then have everything run exactly the same way when it's time for flight.

Use what's in my drawer

We don't need to buy anything new or use anything custom. It would be nice, sure, but that's one of the things that killed the last project.

Here's a list of what I found in my drawer (it's more than enough to get started!):

  • tiny gps breakout https://www.sparkfun.com/products/retired/10995
  • rpi 4b
  • rpi zero w 1.1
  • msp432
  • stm32F411e
  • arduino mkr fpga https://www.arduino.cc/en/Guide/MKRVidor4000
  • a TTL camera

We're missing power systems and radios, but I think there's plenty here to get the fundamentals working and add those later. The systems can operate on the bench with "shore power" and a data umbilical. Later on, both of those can be replaced with battery and radio modules that are drop-in replacements. By making this assumption, we pretty much guarantee that we'll understand the power requirements and communication protocols with a high degree of confidence. Then later, we part out those systems accordingly. Since we're not constrained by power or comms in terms of the design, this is totally acceptable.

System Architecture

In the introduction I mentioned that Rust allows us to theoretically write everything in Rust. This is a sketch of how I think the system will look in practice.

sketch

System boundaries

Here are the main categories of systems I see:

  • Sensor - interprets information about the world and convert it into data.
  • State - represents the configuration of a system (and persists it!).
    • Physical state governs interactions with the physical world.
    • Logical state governs interactions within software.
  • Operator - moves, mutates, and/or queries state or sensor data.
  • Actuator - changes a physical state.
  • Transport - moves data between systems.
  • Agent - a system that combines sensors, state, operators, and actuators. Agents send and receive messages (or commands) to or from other agents.
  • User Interface - a system that allows a human to observe or change data.

Let's see if we can break down those systems into "blocks" of systems.

  • The World - Updating the "world state" of physical objects is pretty much a self-contained system. There are known physics rules and the state of those objects continues to evolve by those rules without any external input from an agent.
  • Sensors - Physically, sensors are self-contained systems. They observe data about the world and convert it into some format, then ship it elsewhere. The source of the observation and the destination of the data can be abstracted or simulated or whatever. Sensors are often simple hardware systems. I think we can exclude microcontrollers from this category.
  • Computers - Regardless of whether a computer is a microcontroller or runs an operating system or whatever, in this case I mean it as a system that takes in data, does something with it, and then outputs data. At the most basic level this would be a logger. More complicated systems could be an altitude controller. These systems are often much larger than sensors, but can still be contained relatively cleanly (and often along hardware boundaries).
  • User Interfaces - CLIs or UIs that allow a human to observe or interact with a system are also nearly-completely self-contained systems and independent from all of the other systems at play.

Each of these "blocks" represents a system boundary that can be swapped in or out with a simulation, abstraction, or an actual physical system. (The obvious exception here is the "world" block, but hopefully it is equally obvious how to swap that out with a simulation or real system... you fly it!)

BlockExamplesFlavor of RustProject
SensorsIMU, GPS, etc.Embedded Rust (no-std)ahab
ComputersLogger, altitude controller, etc.Regular Rust (std) or embedded Rust (no-std)ahab
The WorldPhysics engine, weather simulation, etc.Bevy (game engine) and/or regular Rust (std)yahs
User InterfacesCLI, web app, etc.Bevy (game engine), Egui, and/or regular Rust (std)yahs

To simplify things (ha!) we can offload the "world" and UI blocks to other projects like yahs. Codevelopment is good but spaghetti interdependencies are bad.

Data interfaces

The data interfaces are the most important part of the system. They are the glue that holds everything together. Unfortunately, they are also the things that have to be flexible enough to support a simulation, abstraction, or real-world system.

I think we can get over this hump by using data interface patterns that lean on software patterns that can be modeled or abstracted in a simulation environment. So things like message queues, async/await, ethernet and other network protocols, and databases can be modeled in a simulation environment and still be implemented in the hardware in pretty much the same way. That's the claim, anyway.

Hardware

Observables

Here is a (non-exhaustive) list of sensors and the observables they can could provide.

SensorObservables
ClockTime
GPSPosition, velocity
IMUOrientation, Velocity, Angular velocity, Acceleration, Angular acceleration
BarometerAir pressure
Barometric AltitmeterAir pressure, altitude
ThermometerAir temperature
HygrometerAir humidity

And maybe some other telemetry that would be derived from the above sensors.

SourceObservable
Air temperature, humidity, pressureAir density
Ballistic trajectory vs observed velocity and positionWind speed, wind direction
Dead reckoning from accelerationsPosition, velocity

Parts

  • RFM96W LoRa Transceiver (915 MHz). This also comes in a 433 MHz version, but as far as I can tell it's a region lock thing---The Americas use 915 Mhz for the license-free ISM band. The 915 MHz version has an operating power of up to 100 mW and 300 kbps, which should be enough for me.
  • NEO-M9N GPS. According to the datasheet it should work up to 80 km. This is the newest version of this chip but I think modules as old as NEO-M6 would work.
  • BME280 Atmospheric Sensor. The myth, the legend, the classic pressure, humidity, and temperature sensor.

Some other parts that are overkill but would be nice to have:

  • Qwiic MicroPressure Sensor. This is a barometer that has a calibrated sensing range of 60mbar to 2.5bar. I think it would be useful to have a sensitive barometer for higher altitudes where a more general purpose barometer might not be sensitive enough.
  • OpenLog Artemis. This is a data logger board with a built-in IMU, voltage loggers, some high-rate sampling for a few channels or ~250Hz logging in general. It automatically detects, configures, and logs Qwiic boards, including all the sensors I'd want to use for this project.

Getting started with the STM32F411VE

Here are some notes on how to set up a development environment to work on this project. YMMV.

I recommend following this video tutorial to get started.

We'll use the STM32F411VE as our target system.

Setting up your development environment

  1. Install Rust 1.31 or newer.
  2. Add cortex-m targets to your toolchain. We are using Cortex-M4F, so: rustup target add thumbv7em-none-eabihf
  3. Read the Embedded Rust Book in its entirety. Just kidding, but it's a good idea to bookmark it.
  4. If you're using VSCode or one of its derivatives, install the rust-analyzer extension and the Cortex-Debug extension. There are some more details here for advanced configurations.

Setting up the project

Configuring the compiler target

A target triple is a string that describes the target architecture, vendor, operating system, and environment.

<arch><subarch>-<vendor>-<sys>-<env>
  • arch is the architecture, e.g. arm, x86, aarch64, etc.
  • subarch is the subarchitecture, e.g. v7, v8, v9, etc.
  • vendor is the vendor, e.g. none, apple, nvidia, intel, etc.
  • sys is the operating system, e.g. none, linux, windows, macos, etc.
  • env is the environment, e.g. eabihf, gnu, musl, etc.

Both the MSP432 and the STM32F411E use the Cortex-M4F with a floating point unit. This chip is part of the ARMv7e-M architecture family.

The rustc docs show that the target triple for ARMv7E-M is thumbv7em-none-eabi or thumbv7em-none-eabihf.

  • The thumbv7em prefix indicates that our Cortex-M4F uses the Thumb-2 instruction set and the ARMv7E-M architecture.
  • The none vendor indicates that the target does not have an operating system.
  • The eabi or eabihf suffix indicates the ABI to use. Since we have an FPU, we use the eabihf variant.

Now that we know the target triple, we can add it to the cargo toolchain on our host machine and in our .cargo/config.toml file to tell the compiler how to build code for the project's target.

# add the target architecture to the toolchain
rustup target add thumbv7em-none-eabihf

And to make sure the correct target and flags are used every cargo build, we can add the following to our .cargo/config.toml file so we don't have to type it out every time.

[build]
target = "thumbv7em-none-eabihf"     # Cortex-M4F and Cortex-M7F (with FPU)

[target.thumbv7em-none-eabihf]
rustflags = ["-C", "link-arg=-Tlink.x"]

But the IDE might still complain about failing to build the project for the host architecture. We can instruct the IDE extensions to only build for the embedded target by adding some extra configs. The following is an example for VSCode.

// .vscode/settings.json
{
    "rust-analyzer.cargo.allTargets": false,
    "rust-analyzer.cargo.target": "thumbv7em-none-eabihf"
}

The cortex-m-quickstart also includes a build.rs file that sets up the target triple and the linker script, accounting for some of the edge cases that can happen when building to make sure that the linker actually sees the memory.x file and stuff. Be careful though, because the build.rs file also sets the rustflags so we have to remove them from the .cargo/config.toml file if we use the build.rs file.

Configuring the Cortex-M crates

The embedded Rust tools make it easy to build and run programs on the target but require a little extra setup to get running for our specific target.

The first crate to set up is the runtime crate, cortex-m-rt. This crate provides the entry! macro, which is used to mark the entry point of the program, and interrupt handling.

// src/main.rs
#![no_std]
#![no_main]

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    loop {
        // your code goes here
    }
}

The cortex-m-rt crate expects a memory.x file that specifies the flash and RAM memory layouts of the target. According to the datasheet, the flash memory is at 0x0800 0000 - 0x0807 FFFF and the RAM is at 0x2000 0000 - 0x2002 0000.

stm32f411ve memory map

Another thing that is required by the cortex-m-rt runtime is a panic handler. A panic handler accepts a PanicInfo argument and never returns. This code will run when the program (our code or a dependency) panics. It is required!

#![allow(unused)]
#![no_std]

fn main() {
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    // do something
}
}

There are more advanced ways to handle panics, including a number of third-party crates, but for now we'll just set up a basic one: panic-halt. This crate makes an infinite loop happen when a panic occurs, which can be helpful when debugging. We should replace it later. Now we're not actually using the crate's functions, but we need to declare it so the compiler knows it exists.

// src/main.rs
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;

#[entry]
fn main() -> ! {
    loop {
        // your code goes here
    }
}

Flashing the device

There are a few more toolchains to install for ergonomics. llvm-tools enables some low-level debugging and inspection features, and cargo-binutils is a more ergonomic wrapper around llvm-tools.

rustup component add llvm-tools
cargo install cargo-binutils

Here's an example of how to use cargo-binutils to inspect the binary.

❯ cargo size -- -Ax
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
ahab_stm32f11ve  :
section                size         addr
.vector_table         0x400    0x8000000
.text                  0x70    0x8000400
.rodata                   0    0x8000470
.data                     0   0x20000000
.gnu.sgstubs              0    0x8000480
.bss                      0   0x20000000
.uninit                   0   0x20000000
.debug_abbrev        0x145d          0x0
.debug_info         0x23eef          0x0
.debug_aranges       0x1338          0x0
.debug_ranges       0x1b720          0x0
.debug_str          0x3d91c          0x0
.comment               0x99          0x0
.ARM.attributes        0x3a          0x0
.debug_frame         0x4218          0x0
.debug_line         0x2184d          0x0
.debug_loc             0x29          0x0
Total               0xa5691

One last tool is needed to flash the device. probe-rs is a tool that allows you to interact with embedded devices, including flashing. Installation can vary by OS, see probe-rs docs for details. If you're on Windows, you might need to install some drivers.

Let's see if probe-rs supports the STM32F411VE.

❯ probe-rs chip info stm32f411ve
stm32f411ve
Cores (1):
    - main (Armv7em)
NVM: 0x08000000..0x08080000 (512.0 kiB)
RAM: 0x20000000..0x20020000 (128.0 kiB)
NVM: 0x1fffc000..0x1fffc004 (4 B)
NVM: 0x1fff7800..0x1fff7a10 (528 B)

Yep! Now we want to use cargo embed to flash the device. cargo-embed is the big brother of cargo-flash. It can also flash a target just like cargo-flash, but it can also open an RTT terminal as well as a GDB server.

❯ cargo embed --chip stm32f411ve
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
      Config default
      Target E:\repos\philiplinden\ahab\target\thumbv7em-none-eabihf\debug\ahab_stm32f11ve
      Erasing ✔ 100% [####################]  16.00 KiB @  40.12 KiB/s (took 0s)
  Programming ✔ 100% [####################]   2.00 KiB @   4.08 KiB/s (took 0s)
     Finished in 0.49s
        Done processing config default

We can cache the chip info so we don't have to type it out every time using an Embed.toml file that cargo embed will look for in the project root.

# Embed.toml
chip = "stm32f411ve"

Now we can run cargo embed and it will pull the arguments from the config.

❯ cargo embed
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
      Config default
      Target E:\repos\philiplinden\ahab\target\thumbv7em-none-eabihf\debug\ahab_stm32f11ve
      Erasing ✔ 100% [####################]  16.00 KiB @  40.25 KiB/s (took 0s)
  Programming ✔ 100% [####################]   2.00 KiB @   4.15 KiB/s (took 0s)
     Finished in 0.48s
        Done processing config default

😎

Debugging the STM32F11VE

Based on this tutorial.

With RTT

To debug the device, we can use "real time transfer", RTT. RTT is a debugging protocol that allows you to send and receive data to the device from a host computer over the debug interface.

Of course, there's a crate for this: rtt-target. It requires a "critical section" feature, but we can get that from cortex-m. Then enable the RTT feature in the Embed.toml file.

[default.rtt]
enabled = true

With GDB (maybe not supported on Windows)

GDB is a bit more complex to set up than RTT, but it's more powerful.

The GDB toolchain is available on Windows, Linux, and macOS but the installation depends on the operating system. https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads

Then set up the GDB configs in Embed.toml. Don't forget to disable RTT if you're using GDB.

[default.rtt]
enabled = false

[default.gdb]
enabled = true

[default.reset]
# Wait for the debugger to connect before resetting the device
halt_afterwards = true

Flash the device with cargo embed as usual. After it flashes, it halts the device until the GDB server is connected.

❯ cargo embed
      Config default
      Target E:\repos\philiplinden\ahab\target\thumbv7em-none-eabihf\debug\ahab_stm32f11ve
      Erasing ✔ 100% [####################]  16.00 KiB @  40.06 KiB/s (took 0s)
  Programming ✔ 100% [####################]   2.00 KiB @   4.12 KiB/s (took 0s)
     Finished in 0.49s
    GDB stub listening at 127.0.0.1:1337

In a separate terminal, run the GDB server.

❯ arm-none-eabi-gdb target/thumbv7em-none-eabihf/debug/ahab_stm32f11ve
GNU gdb (Arm GNU Toolchain 14.2.Rel1 (Build arm-14.52)) 15.2.90.20241130-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-w64-mingw32 --target=arm-none-eabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.linaro.org/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
target/thumbv7em-none-eabihf/debug/ahab_stm32f11ve: No such file or directory.
(gdb)

And connect to the device. By default, the GDB server listens on port 1337. You can include the local host address or just :1337 to connect to the device.

(gdb) target remote 127.0.0.1:1337
Remote debugging using 127.0.0.1:1337
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x080004e8 in ?? ()

Aaaaaand it doesn't work for me. This seems like an active issue with probe-rs, as indicated by the log message "GDB stub listening...".

Stick to RTT for now, at least when using a Windows host.

Blinky with STM32F411VE

This page is a collection of notes on how to do a blinky program with the STM32F411VE following this tutorial.

Board familiarization

The STM32F411VE has 5 user-programmable LEDs and a few other indicator LEDs.

NameColorNotes
LD1 COMRed/GreenIndicates communications are in progress between the PC and the ST-LINK/V2
LD2 PWRRedIndicates the board is powered
User LD3OrangeUser-programmable at PD13.
User LD4GreenUser-programmable at PD12.
User LD5RedUser-programmable at PD14.
User LD6BlueUser-programmable at PD15.
USB LD7GreenIndicates when VBUS is present on CN5 and is connected to PA9
USB LD8RedIndicates an overcurrent from VBUS of CN5

LEDs

Turn on the green LED, we need to set PD12 to HIGH. To manipulate this pin we need to make reads or writes to the GPIO peripheral that drives it.

From page 32 of the datasheet:

Each of the GPIO pins can be configured by software as output (push-pull or open-drain, with or without pull-up or pull-down), as input (floating, with or without pull-up or pull-down) or as peripheral alternate function. Most of the GPIO pins are shared with digital or analog alternate functions. All GPIOs are high-current-capable and have speed selection to better manage internal noise, power consumption and electromagnetic emission.

  1. Configure PD12 as GPIO_Output. This pin can have alternate functions, so we have to tell it what function we want to use.
  2. Set this pin to HIGH or LOW to turn the LED on or off.

[!TIP]

I ran into a lot of trouble getting the system to build and flash reliably. Eventually I found that embassy needs a specific combination of features to be enabled, and probe-rs needs to have certain options. See captain's log 2024-12-19 for details.

#![no_std]
#![no_main]

use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};

/// The entry point of the program.
///
/// This must never return. We denote this with `-> !` and make sure it happens
/// by including an unbounded loop in the function.
///
/// Print "Hello, world!" on startup then blink the green LED.
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    info!("Hello World!");

    let mut led = Output::new(p.PD12, Level::High, Speed::Low);

    loop {
        info!("high");
        led.set_high();
        Timer::after_millis(300).await;

        info!("low");
        led.set_low();
        Timer::after_millis(300).await;
    }
}

With embassy we can use cargo run to flash and run the program with logs printed to the terminal as if we were running a native program.

❯ cargo run
   Compiling ahab_stm32f11ve v0.1.0 (E:\repos\philiplinden\ahab\crates\ahab_stm32f11ve)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `probe-rs run --chip STM32F411VE --connect-under-reset E:\repos\philiplinden\ahab\target\thumbv7em-none-eabihf\debug\ahab_stm32f11ve`
      Erasing ✔ 100% [####################] 128.00 KiB @  53.98 KiB/s (took 2s)
  Programming ✔ 100% [####################]  72.00 KiB @  17.11 KiB/s (took 4s)
     Finished in 4.21s
0.000000 TRACE BDCR ok: 00008200
└─ embassy_stm32::rcc::bd::{impl#2}::init @ C:\Users\linde\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\embassy-stm32-0.1.0\src\fmt.rs:117
0.000000 DEBUG flash: latency=0
└─ embassy_stm32::rcc::_version::init @ C:\Users\linde\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\embassy-stm32-0.1.0\src\fmt.rs:130
0.000823 INFO  high
└─ ahab_stm32f11ve::____embassy_main_task::{async_fn#0} @ src\main.rs:24
0.301788 INFO  low
└─ ahab_stm32f11ve::____embassy_main_task::{async_fn#0} @ src\main.rs:28
0.602813 INFO  high
└─ ahab_stm32f11ve::____embassy_main_task::{async_fn#0} @ src\main.rs:24
0.903839 INFO  low
└─ ahab_stm32f11ve::____embassy_main_task::{async_fn#0} @ src\main.rs:28

captain's log

2024-12-21

Renamed the stm32 directory to hello-world.

I did some more thinking about the performance considerations of the RPi Zero, MSP432, and STM32F411xx. The MSP432 is the most power-efficient at idle, but I'm not expecting to operate the balloon for weeks at idle. The STM32 board is a bit faster and more power-efficient than the RPi Zero, but not as efficient as the MSP432. The RPi Zero is the fastest and least power-efficient of the three, but it is far more flexible and capable than the other two. If I want to do imaging, the RPi Zero is the only realistic option due to compute and memory. If I leave out imaging, the STM32 is plenty. The dev board for the STM32F411VE comes with handy sensors that will be useful to me too. In the end, I think I'll use the STM32F411VE for flight and the RPi Zero for ground station.

I'm not sure how radio factors into this yet. Maybe the RPi Zero will be necessary for radio and I can fly them as a pair. I have a full size Pi 4B sitting around that I could use for ground station.

The Nucleus HAB used a Pi 4 compute module as the main computer, so using a Pi for flight might be a good idea if I want to continue that project someday.

Turns out I have the MB1115B variant, which uses

2024-12-20

down-detector.

2024-12-19

Today I'll try to blinky: blinky. After that I'll try to do it again with embassy.

From the example, it looks like embassy doesn't replace the cortex-m crates, it just adds convenience functions and ergonomics to it.

I stole some other options from the example's Cargo.toml file too like panic-probe and defmt.

Panic-Probe is a Rust crate that enables probe-run, a tool for running embedded Rust applications like native applications. It provides a panic handler for probe-run, allowing you to write integration tests that run on your embedded device via cargo test.

and

defmt ("de format", short for "deferred formatting") is a highly efficient logging framework that targets resource-constrained devices, like microcontrollers. println!-like formatting, multiple logging levels, compile-time RUST_LOG-like filtering of logs, and timestamped logs.

When compiling, I'm finding that something about embassy-time is causing the build to fail. I can successfully compile and build an embassy script without embassy-time but any time I import a timer, it fails to compile. I might be able to get around this by using use embassy::time::{Duration, Timer};.

Hm, a search engine AI says

Undefined symbol _embassy_time_schedule_wake: The linker error occurs when trying to link the embassy_time crate with the generic-queue feature enabled. The error message indicates that there are multiple definitions of the _embassy_time_schedule_wake symbol. Github Issue

And in that github issue I think I found some breadcrumbs toward a solution.

To use timers, some backend must be provided through crate features – otherwise, linking will fail and complain about a missing _embassy_time_schedule_wake symbol.

Possible backends are:

  • The integrated-timers feature of the embassy-executor crate. If you use this and no other executor, this is preferred.
  • (Something for ESP?)
  • The generic-queue feature of the embassy-time crate. Use this if you use multiple executors, or one not listed above.

This didn't work alone, but I was able to get it to build once I added the time-driver-tim4 and time features to the embassy-stm32 crate alongside the integrated-timers feature in embassy-executor.

I ran into another issue when trying to reflash the board.

It looks very similar to the issue described here which was resolved by adding --connect-under-reset to the probe-rs command. After adding this option, flashing has been reliably working.

I also confirmed that embassy with the memory-x feature does indeed let us delete the memory.x file from our repo and ignore it from the build process!

2024-12-18

There's a rust project that claims to support raspberry pi and stm32 boards, embassy.dev that looks interesting. I'm not sure how it differs from the cortex-m crates I've been using, but it might be worth checking out.

  • It looks like Embassy can auto-generate the memory.x file for the board.
  • The docs are really good and filtered by the specific chipset.
  • Doesn't look like it supports the MSP432.

I also did some shopping for parts, but I didn't order anything yet. Here are the parts I'm considering:

  • RFM96W LoRa Transceiver (915 MHz). This also comes in a 433 MHz version, but as far as I can tell it's a region lock thing---The Americas use 915 Mhz for the license-free ISM band. The 915 MHz version has an operating power of up to 100 mW and 300 kbps, which should be enough for me.
  • NEO-M9N GPS. According to the datasheet it should work up to 80 km. This is the newest version of this chip but I think modules as old as NEO-M6 would work.
  • BME280 Atmospheric Sensor. The myth, the legend, the classic pressure, humidity, and temperature sensor.

Some other parts that are overkill but would be nice to have:

  • Qwiic MicroPressure Sensor. This is a barometer that has a calibrated sensing range of 60mbar to 2.5bar. I think it would be useful to have a sensitive barometer for higher altitudes where a more general purpose barometer might not be sensitive enough.
  • OpenLog Artemis. This is a data logger board with a built-in IMU, voltage loggers, some high-rate sampling for a few channels or ~250Hz logging in general. It automatically detects, configures, and logs Qwiic boards, including all the sensors I'd want to use for this project.
  • Waveshare Environment Sensor HAT. This is basically a glorified BME280 carrier board with a few other sensors like UV, air quality, and convenient Raspberry Pi GPIO headers.
  • BerryGPS-IMU. A fancy all-in-one motion sensor designed to fit perfectly with the Raspberry Pi Pico. Fancy. Overkill. Cool, though. It has a GPS, 10DOF IMU, barometric altimeter, magnetometer, and temperature sensor. This board uses the CAM-M8 GPS module from u-blox, which dies at 50 km altitude.

2024-12-17

Ahab is a fitting name for this project. It is a high altitude balloon. It is cheeky. It is a bit of a jerk. It is a bit of a showoff. It is a bit of a thing I keep chasing and chasing and chasing. It's also a nod to the "alumni hab" brickworks project that became Nucleus.

I'm going to try to keep a log of my thoughts and ideas here, like a captain losing his mind at sea.

Some things I have floating around in my head:

  1. I really like mdBook. I think it's a great way to write documentation.
  2. I found the following things in my drawer.
    • tiny gps breakout https://www.sparkfun.com/products/retired/10995
    • rpi 4b
    • rpi zero w 1.1
    • msp430
    • stm32F411e
    • arduino mkr fpga https://www.arduino.cc/en/Guide/MKRVidor4000
    • a TTL camera
  3. I would really like to build out yahs into a thing that can be used to build and test a high altitude balloon, more than just a thing I use to learn rust.
  4. I want to explore embedded rust.
  5. I want to finish what Nucleus started. We had a lot of good ideas and made a lot of mistakes. I want to finish what we started.

I set up this repository and started writing some documentation, including some notes on what I want out of this project.

I flashed the Raspberry Pi 4B with Raspberry Pi OS Lite and was able to successfully SSH into it on my local network. Even if I end up using the Pi Zero for flight, the 4B is really easy to set up on the network, has plenty of headroom, and will serve as a good testbed for now. It could also host other development services like a web server, database, Jenkins, etc if I need it to.

https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials

I should start using Linear for project management for this. I probably won't stick to it very well, but it will be good practice.

I found some great rust CI examples in the actions-rs repo. I'm going to use them to set up a build workflow for this project.

I set up a template for the STM32F411E board. https://studiofuga.com/blog/2022-10-18-getting-started-with-rust-and-stm32f411/

I set up a template for the MSP432 too.

  • https://github.com/rust-embedded/msp430-quickstart (not this board, but close)
  • https://docs.rs-online.com/3934/A700000006811369.pdf
  • https://github.com/MSP432P401R-Launchpad-Rust
  • https://github.com/msp432-rust/msp432p401r-hal
  • https://github.com/msp432-rust/msp432p401r-pac
  • https://github.com/pcein/msp432p401r
  • https://github.com/pcein/msp432-newio

Turns out the MSP432 uses the TM4C129 chip, which can be used with the cortex-m-quickstart cargo-generate template. I set that up but I'm not sure if the memory.x file is correct.

I ended up fighting the compiler for a while, trying to get the panic handler to work. I ended up just using the panic-semihosting crate for now because that's what would compile. Using panic-halt or panic-abort required using deprecated features I guess? After switching to the panic-semihosting crate, I started running into linker errors. I'll have to figure this out later once I am actually flashing the boards with code I understand, rather than templates.

The root cause looks to be related to this comment from the miri repo:

you added a crate as dependency that depends on std, and built a sysroot without std, and then that fails to build. No surprise here.

Oh well.

I thought some more about how to structure the project so that I can swap out blocks of logic easily between simulated code on my PC, code running on a board in a hardware-in-the-loop simulation, and actual code running on the board in the real world. With Rust, hopefully we can write tests and benchmarks for the software and firmware so that they behave exactly the same way in each case. The tricky bit is how to handle the interface between the simulated environments and the real environments for things like sensors and actuators.

--> design/structure.md

And now, it's time to dive in to embedded rust.

I deleted most of the template code and started from scratch following this tutorial and documenting the process in docs/hello-worldgetting-started.md. I can always re-use the template code if I need it, but building up from scratch is great practice to learn what all of these files do and how they fit together.

Woohoo! I got a (dummy) program to compile for the STM32F411E board! Now to flash it... Nice, it worked!

There is one hang up though. When I cargo build from the root, it tries to compile the embedded crates for the host arch. I want to edit Cargo.toml at the root to effectively do the same thing as specifying the target arch in .cargo/config.toml for each crate.

https://doc.rust-lang.org/cargo/reference/unstable.html#per-package-target

https://nnarain.github.io/2021/08/02/Setting-up-per-package-targets-in-Rust.html

I had to add cargo-features = ["per-package-target"] to the Cargo.toml file, which is an unstable nightly feature.