Part 3: Time from Cloning to Flashing - Rust vs. STM32CubeMX
Introduction
In the previous parts of this series, we explored the benefits of using Rust for embedded systems development and highlighted key crates within the Embedded Rust ecosystem. Now, let's compare the time and effort required to set up a development environment and deploy a simple application to a STM32 Microcontroller using Rust versus the traditional C/C++ approach with vendor-specific software like STM32CubeMX/STM32CubeIDE. This comparison will illustrate the streamlined experience that Rust offers. While vendor-specific software often has advantages in terms of feature-completeness and being tailored to the target platform, this specific case highlight the stark contrast in user experience.
Setting Up and Deploying with Rust
1. Install Rust Toolchain
Time: around 30 s (depending on your internet connection)
The first step in setting up an Embedded Rust environment is to install the Rust toolchain. This process is remarkably straightforward thanks to rustup
, Rust's toolchain installer and version manager.
|
This single command installs rustup
, rustc
(the Rust compiler), cargo
(the Rust package manager) and other essential tools.
Follow the instructions on the screen and don't forget to restart your shell or source the configuration to have your PATH updated.
2. Install probe-rs
Time: around 10 s (depending on your internet connection)
To copy the binary from the Host PC to the MCU, we will use probe-rs.
The latest installation instructions are found on the tool's website. At the time of writing, probe is installed like this:
|
Caution: If you are on Linux you might need to set some udev rules (or use sudo
) or you might get permission errors.
See the the probe-rs documentation for details on how to do this.
3. Add Target for ARM Cortex-M (optional)
Time: around 30 s (depending on your internet connection)
If you want to go super manual mode and don't have a properly set up git repository, you might need to install the correct target. However, if you have access to a pre-configured project that includes a rust-toolchain.toml
file, cargo
will automatically make rustup
download and install the correct toolchain for you. Easy. Then this step takes 0 seconds, but the download in the later step will take a few seconds depending on your internet connection.
Sometimes this is not the case, unfortunately. Then you need to install it manually, for example:
This command ensures that the Rust compiler can generate code for the ARM Cortex-M architecture.
4. Clone Example Repository
Time: around 5 s (depending on your internet connection)
Clone an example repository that contains the necessary setup and configurations for your microcontroller. For instance, a typical command might be:
5. Build the Project
Time: around 30 to 90 s (depending on your internet connection, cpu speed and whether the target is already installed)
Navigate to the example directory and build the project using Cargo:
# Or for release build (logging may not work as well)
Cargo will automatically handle dependencies, configuration, and compilation.
6. Flash and Run the Firmware
Time: around 10 s
Finally, use probe-rs
to flash the compiled firmware onto the microcontroller.
Using the pre-configured .cargo/config.toml
with the custom "runner", we can run the firmware like a normal Rust binary (you could even skip the cargo build
step before):
# Or for release build (logging may not work as well)
Again, if no config files are present or you want to go super manual:
# Or for release build (logging may not work as well)
With these steps, you can quickly set up, build, and deploy an embedded application using Rust. The total time required for all steps was less than five minutes. And if you are onboarding a new developer to your team, this time is all that is required to get them to develop embedded software. From here on they can poke into the code, explore the architecture, make changes, try out stuff and open their first pull request on the new team.
Troubleshooting
Linker cc
not found
You might be on a super-minimum OS (maybe Debian?) and not even have a C toolchain installed.
This is usually fixed by installing one:
Flashing fails SwdDpWait
WARN probe_rs::probe::stlink: send_jtag_command 242 failed: SwdDpWait
WARN probe_rs::probe::stlink: got SwdDpWait/SwdApWait, retrying.
Try passing --connect-under-reset
to probe-rs or press the Reset button on your board. Sometimes the board's integrated debugger gets stuck...
probe-rs run [...] --connect-under-reset
# or with cargo (the additional `--` passes the command from cargo on to probe-rs)
cargo run -- --connect-under-reset
Setting Up and Deploying with STM32CubeMX
1. Download and Install STM32CubeMX, then download necessary software packages
Time: around 7 min including account creation
First, download STM32CubeMX (about 600 MB) from the STMicroelectronics website and install it.
Well, yeah... no. First create an account and accepted the terms and conditions. Then you download. You can also download as a guest, but this also means entering your name, company and mail address and then getting a download link via mail. Since we need to download multiple software packages, it is actually faster to create an account in this case, as we need to open our mailbox anyway, either for the download or the account confirmation. Also, there has been an obnoxious bug on the ST.com website that causes the donwload button to disappear once you are logged in and it only reappears after 15-30 seconds of inactivity.
Then download the needed software packages for your application/board in STM32CubeMX.
2. Download and install STM32CubeIDE
Time: around 5 min including account creation
Then download STM32CubeIDE as we will need it later (about 1 GB), unpack and install it.
3. Set Up Project in STM32CubeMX
Time: around 6 min
Launch STM32CubeMX and create a new project. Select the target microcontroller (e.g., STM32F469NI), or target board (e.g. DISCO-F469NI), configure the necessary peripherals, and generate the initialization code. This process involves several steps in the graphical interface:
- Selecting the microcontroller.
- Configuring clocks, GPIOs, and other peripherals.
- Generating the code.
To speed up the process, we will leave all peripherals in their default mode, since we downloaded an example program above as well.
4. Import into IDE
Time: around 6 min when opening for the first time
Import the generated project into an Integrated Development Environment (IDE) such as STM32CubeIDE or an alternative like Keil or IAR. This step may involve:
- Setting up a new workspace.
- Importing the project files.
- Configuring the build settings.
5. Write Application Code
Time: not considered
Edit the generated code to implement the desired functionality, such as blinking an LED. This involves:
- Writing or modifying C code.
- Managing include paths and linker scripts.
- Ensuring the correct initialization and main loop logic.
Since we downloaded an example in the above case for the Rust toolchain, we won't consider this step. There are some STM32CubeMX-Compatible examples available in the project configurator, so we assume that one of those was suitable. If we want to add a blinking LED to our project, we can do that, for example like so in the main loop:
; // toggle LED1
; // wait 500 ms for each state
We have already explained in other blog posts that application code can be developed faster and better in rust than in C. This blog post is just about comparing the tooling and setup times.
6. Build and Flash
Time: around 3 min when configuring for a new project
Build the project using the IDE's build tools and flash the firmware onto the microcontroller using a compatible programmer/debugger (e.g., ST-Link). This step may involve:
- Configuring the debugger.
- Connecting the hardware.
- Flashing the firmware.
In total, all these steps combined took about 27 minutes, just shy of half an hour, but this a developer with experience with the software that knew which buttons to press. It is easy to see that a less experienced developer could easily waste several hours just setting up the software.
Comparison of Rust and STM32CubeMX
Toolchain Integration
- Rust: The Rust ecosystem offers a unified and integrated toolchain.
rustup
andcargo
manage everything from compiler installation to dependency resolution and project building. The process is straightforward and consistent across different projects. - STM32CubeMX: The traditional C/C++ approach requires multiple tools from different vendors, including STM32CubeMX for project setup, a separate toolchain for compilation, and an IDE for development and flashing. This fragmented approach can lead to compatibility issues and a steeper learning curve.
Ease of Setup
- Rust: Setting up the Rust toolchain is quick and simple with a single command. Adding the target and cloning an example repository are similarly straightforward, minimizing the time and effort required to get started.
- STM32CubeMX: Setting up STM32CubeMX involves multiple steps, including downloading and installing various tools, configuring the project in a graphical interface, and dealing with IDE-specific settings. This process is more time-consuming and complex.
Consistency
- Rust: The use of Cargo ensures a consistent build process across projects. Dependencies, configurations, and build steps are all managed through a single tool, reducing the risk of errors and inconsistencies.
- STM32CubeMX: The traditional approach can vary significantly between projects and environments. Different versions of tools, IDEs, and configurations can lead to inconsistencies and increased maintenance efforts.
Developer Experience
- Rust: Rust's modern syntax, safety features, and integrated toolchain provide a smooth and efficient development experience. Features like async/await in
embassy
further enhance productivity for complex tasks. The Rust toolchain works great cross-platform. - STM32CubeMX: While powerful, the traditional approach can be cumbersome, particularly for new developers. Managing multiple tools, dealing with low-level C code, and navigating complex IDEs can be challenging. Some tools are only available on Windows, limiting flexibility for developers and company-wide IT decisions.
Conclusion
The comparison highlights the streamlined and efficient nature of the Rust ecosystem for embedded development. Rust's integrated toolchain, consistency, and modern language features offer a significant advantage over the traditional C/C++ approach with STM32CubeMX. While both approaches can achieve the same end result, Rust simplifies the setup and development process, making it an attractive option for embedded systems engineers.
In this series, we've seen how Rust's safety, concurrency, and performance benefits, combined with a robust ecosystem and efficient setup process, position it as a powerful choice for embedded systems development. As the Embedded Rust community continues to grow and improve, the language's adoption in the embedded domain is likely to increase, bringing modern software engineering practices to a field that demands reliability and efficiency.