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.
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!)
Block | Examples | Flavor of Rust | Project |
---|---|---|---|
Sensors | IMU, GPS, etc. | Embedded Rust (no-std) | ahab |
Computers | Logger, altitude controller, etc. | Regular Rust (std) or embedded Rust (no-std) | ahab |
The World | Physics engine, weather simulation, etc. | Bevy (game engine) and/or regular Rust (std) | yahs |
User Interfaces | CLI, 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.
Sensor | Observables |
---|---|
Clock | Time |
GPS | Position, velocity |
IMU | Orientation, Velocity, Angular velocity, Acceleration, Angular acceleration |
Barometer | Air pressure |
Barometric Altitmeter | Air pressure, altitude |
Thermometer | Air temperature |
Hygrometer | Air humidity |
And maybe some other telemetry that would be derived from the above sensors.
Source | Observable |
---|---|
Air temperature, humidity, pressure | Air density |
Ballistic trajectory vs observed velocity and position | Wind speed, wind direction |
Dead reckoning from accelerations | Position, 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
- Install Rust 1.31 or newer.
- Add cortex-m targets to your toolchain. We are using Cortex-M4F, so:
rustup target add thumbv7em-none-eabihf
- Read the Embedded Rust Book in its entirety. Just kidding, but it's a good idea to bookmark it.
- 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
oreabihf
suffix indicates the ABI to use. Since we have an FPU, we use theeabihf
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
.
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.
Name | Color | Notes |
---|---|---|
LD1 COM | Red/Green | Indicates communications are in progress between the PC and the ST-LINK/V2 |
LD2 PWR | Red | Indicates the board is powered |
User LD3 | Orange | User-programmable at PD13 . |
User LD4 | Green | User-programmable at PD12 . |
User LD5 | Red | User-programmable at PD14 . |
User LD6 | Blue | User-programmable at PD15 . |
USB LD7 | Green | Indicates when VBUS is present on CN5 and is connected to PA9 |
USB LD8 | Red | Indicates an overcurrent from VBUS of CN5 |
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.
- Configure
PD12
asGPIO_Output
. This pin can have alternate functions, so we have to tell it what function we want to use. - Set this pin to
HIGH
orLOW
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
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 forprobe-run
, allowing you to write integration tests that run on your embedded device viacargo 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-timeRUST_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 theembassy-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:
- I really like mdBook. I think it's a great way to write documentation.
- 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
- 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.
- I want to explore embedded rust.
- 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.
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.