logo
CODEMBIT
Reliable software engineering partner
Home Blog Tools

Embedded Rust for C developers

By Toni Akkala, 9th of August, 2023

Overview

In my previous post, I was setting up the environment to build and debug embedded Rust applications using STM32F0 discovery kit and vscode. In this post, I continue learning Rust in more detail and try to understand some of the concepts I was already using related to embedded development with Rust.

Microcontroller crates

Using svd2rust to create PAC for your microcontroller

Microcontroller manufacturer usually provides a System View Description (SVD) file which describes the microcontroller memory-mapped register contents. svd2rust tool helps you to create a Peripheral Access Crate (PAC) from that SVD file for your microcontroller. Here is an example showing how to create the PAC for the STM32F072RB76 microcontroller.

# Install the tool: cargo install svd2rust # Create a new package for the PAC: cargo new --lib stm32f072 && cd stm32f072 # Download STM32F0x2.svd file from ST website and run the tool # (cortex-m is the default target so it can be omitted): svd2rust -i STM32F0x2.svd --target cortex-m # Download more SVD files: https://github.com/posborne/cmsis-svd/tree/master/data # Add form tool: cargo add form # Remove src folder: rm -rf src # Split lib.rs using form tool: form -i lib.rs -o src/ && rm lib.rs # Format Rust sources: cargo fmt # Modify Cargo.toml to include the following dependencies and features: [dependencies] critical-section = { version = "1.1.1", optional = true } cortex-m = "0.7.7" cortex-m-rt = { version = "0.7.3", optional = true } vcell = "0.1.2" [features] rt = ["cortex-m-rt/device"] # Create new project to use the PAC: cd .. cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart cd pac-test # Configure project for the microcontroller: MEMORY sections, build target and runners, OpenOCD and vscode debugger settings etc. # Add created PAC to the project with the following dependencies: [dependencies] cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] } cortex-m-rt = "0.7.3" cortex-m-semihosting = "0.3.3" panic-halt = "0.2.0" stm32f072 = { version = "0.1.0", path = "../stm32f072", features = ["rt", "critical-section"] } # and finally add Rust sources to use it: #![no_std] #![no_main] // pick a panicking behavior use panic_halt as _; // you can put a breakpoint on `rust_begin_unwind` to catch panics use cortex_m_rt::entry; use stm32f072; fn set_red_led_on() { // Get pointer to the GPIO and Reset and Clock Control let peripherals = stm32f072::Peripherals::take().unwrap(); let gpioc = &peripherals.GPIOC; let rcc = &peripherals.RCC; // Enable the clock for GPIOC peripheral rcc.ahbenr.write(|w| w.iopcen().set_bit()); // Set PC6 pin to output mode gpioc.moder.write(|w| unsafe { w.moder6().bits(0b1) }); // Set PC6 pin to high state (set red LED on) gpioc.odr.modify(|_, w| w .odr6().set_bit() ); } #[entry] fn main() -> ! { set_red_led_on(); loop {} }

Abstracting the hardware with HAL

After you have your PAC ready, you should continue abstracting your hardware on top of that with a Hardware Abstraction Layer (HAL). Rust has an embedded-hal project which defines a set of traits to be implemented, i.e. common interface to the hardware. You might want to implement traits for Serial, LEDs or whatever you need, the stm32f0xx-hal crate in this case. Fortunately, it has already been implemented so I will use that one here. Anyway, it is often good to search for what is already available in crates.io.

# Add dependency to Cargo.toml and specify the feature to use: stm32f0xx-hal = { version = "0.18", features = ["stm32f072"]} # add code to blink the LED: #![no_main] #![no_std] use panic_halt as _; use stm32f0xx_hal as hal; use crate::hal::{pac, prelude::*}; use cortex_m_rt::entry; #[entry] fn main() -> ! { if let Some(mut p) = pac::Peripherals::take() { let mut rcc = p.RCC.configure().sysclk(8.mhz()).freeze(&mut p.FLASH); let gpioc = p.GPIOC.split(&mut rcc); let mut led = cortex_m::interrupt::free(|cs| gpioc.pc6.into_push_pull_output(cs)); loop { // Turn PC6 off a 1000 times in a row for _ in 0..1_000 { led.set_low().ok(); } // Turn PC6 on a 1000 times in a row for _ in 0..1_000 { led.set_high().ok(); } } } loop { continue; } }

In this example, take method guarantees that we have the only reference to the peripheral, ie. singleton with an exclusive access to the hardware. Using this, we first configure the system clock to 8 MHz and then call a split method to get a structure with individual pins. After this, we can control PC6 pin as output and continue writing LED on and off within the loop.

Some crate level attributes

#![no_std]

This attribute is used with embedded systems to link application against core crate instead of std crate. The core crate is a subset from std crate and doesn't make any assumptions on the system it is used on. This means you won't have dynamic memory allocation with heap (collections), math functions, stack overflow protection nor initialization code before the main function.

#![no_main]

This attribute can be used to indicate that we are not using common main function as an application entry point because it makes some assumptions about an execution environment. Instead, we define ENTRY(function) in a linker script to indicate the symbol we want to use as an entry point. For example, we could implement Reset function and map that to the reset vector in vector table as an entry point for the application. There we can do all necessary initialization and then finally call the main function. Please see an example in linker chapter. Note that this attribute has no effect on library crates.

#[repr(*)]

This attribute can be used with structure and union types to control how compiler lays out variables in memory. Rust default representation doesn't provide any guarantees of the memory layout so it shouldn't be used with the code interfacing directly with the hardware or with the C code.

Tip:

// get an address of an variable x core::ptr::addr_of!(x)

#[repr(C)]

This attribute is used to make sure the layout of a type in memory is interoperable with C. Attribute can be combined with the #[repr(align(n))] or with #[repr(packed)].

#[repr(align(n))]

To specify a specific memory alignment, use this attribute. Note that n must be power of two.

#[repr(packed)]

This attribute can be used to prevent padding between structure members. It will also set the alignment of type to 1. You might need to use this when implementing protocols etc.

#[inline]

This attribute does the same as inlining in C. However, you should avoid using this attribute as rustc compiler uses internal heuristics to automatically inline functions when necessary.

Defining how to panic

Panicking is like responding to an exception, for example, when out of bounds indexing is attempted. When using core crate, panicking behavior is undefined. You need to define it by declaring #[panic_handler] function by yourself or by choosing from the crates for panicking. For example, to use panic-halt crate:

#![no_main] #![no_std] // Select panic mode where execution enters // to an endless loop. Use "as _" to make // sure compiler doesn't warn about unused // import as we don't explicitly use it. use panic_halt as _; // more code to cause panics ..

You can also implement the same by declaring a divergent function (i.e. function that never returns) to handle panicking by yourself:

#![no_main] #![no_std] use core::panic::PanicInfo; #[panic_handler] fn panic(_panic: &PanicInfo<'_>) -> ! { loop {} }

Check Debugging chapter for information how to use panic crates for debugging.

Superpowers with unsafe code

Creating a block of unsafe code will disable some of the Rust memory safety guarantees so extra care should be taken when used. The reason why you would use the unsafe code could be one of the following.

Dereferencing a raw pointer

Raw pointers are often used with memory-mapped registers to interface directly with the embedded hardware. Here is an example (from The Embedded Rust Book) how to access memory-mapped SysTick register at address 0xE000E010:

let systick = 0xE000E010 as *mut SysTick; let time = unsafe { (*systick).cvr };

Note that because Rust doesn't have volatile keyword, one need to use specific methods core::ptr::read_volatile and core::ptr::write_volatile with raw pointers to perform volatile read or write. Here is an example (from The Embedded Rust Book) where Interrupt Service Routine (ISR) will set SIGNALLED variable to true and driver function waits until that happens:

static mut SIGNALLED: bool = false; #[interrupt] fn ISR() { // Signal that the interrupt has occurred // (In real code, you should consider a higher level primitive, // such as an atomic type). unsafe { core::ptr::write_volatile(&mut SIGNALLED, true) }; } fn driver() { loop { // Sleep until signalled while unsafe { !core::ptr::read_volatile(&SIGNALLED) } {} // Reset signalled indicator unsafe { core::ptr::write_volatile(&mut SIGNALLED, false) }; // Perform some task that was waiting for the interrupt run_task(); } }

Read more about raw pointers.

Calling an unsafe function or method

Other libraries might have unsafe functions which you need to wrap inside the unsafe block when called. You might also want to use Foreign Function Interface (FFI) to implement an interface to an existing C library. All these function calls must be within unsafe block.

Accessing or modifying a mutable static variable

If you are not using Real Time Operating System (RTOS), you probably doesn't have other threads running causing race conditions and the previous example with interrupts is something you might face. But if you would have other threads and mutable static variables, it is unsafe to access those and you should definitely use some form of concurrency. Here is an example from The Rust Programming Language book how to access this kind of variables with unsafe code:

static mut COUNTER: u32 = 0; fn add_to_count(inc: u32) { unsafe { COUNTER += inc; } } fn thread() { add_to_count(3); unsafe { println!("COUNTER: {}", COUNTER); } }

Accessing fields of unions

You would probably need to use unions if interfacing with the C code. Here is an example how to declare an union, to initialize and access union member (from The Rust Reference):

#[repr(C)] union MyUnion { f1: u32, f2: f32, } let u = MyUnion { f1: 1 }; let f = unsafe { u.f1 };

There are more superpowers which you may read from here.

Rust doesn't have a preprocessor

Rust doesn't have a preprocessor but it offers similar concepts you may use instead. Here are some of them.

Conditional compiling

In Rust, one can use Cargo features to select code blocks at compile time. You could select the code based on architecture or enable only some parts from an external library. You can enable the features you would like to use in your Cargo.toml file. For example, when we added HAL crate to our project, we specified which microcontroller to use. This way we don't need to build code for all microcontroller variants supported by that library.

# Add dependency to Cargo.toml: stm32f0xx-hal = { version = "0.18", features = ["stm32f072"]}

Read here for more about features.

Tip: use the following command to list enabled features for your project:

cargo tree -f "{p} {f}"

Computing array size at compile time

Rust has const fn functions which are evaluated at compile time. You may use these functions to return the size of an array. You may also combine this concept with the Cargo features to select the size of the array based on configuration or something else. This example is directly from the Embedded Rust book to select the buffer size based on enabled features:

const fn array_size() -> usize { #[cfg(feature="use_more_ram")] { 1024 } #[cfg(not(feature="use_more_ram"))] { 128 } } static BUF: [u32; array_size()] = [0u32; array_size()];

Macro system

Rust has a powerful macro system which operates at higher level compared to the C preprocessor. Rust has declarative macros and three kinds of procedural macros. Macros are a way of metaprogramming to write code that creates a new code. If you want simpler code, you should consider using functions. If you want to reduce your code size and are not afraid of a bit more complex definitions, macros are your choice. For example, The Embedonomicon used macro to make sure main is type safe:

#[macro_export] macro_rules! entry { ($path:path) => { #[export_name = "main"] pub unsafe fn __main() -> ! { // type check the given path let f: fn() -> ! = $path; f() } } }

See also how macros are used in RTT debugging. Please read here for more about macros.

Build system

Cargo

In Rust, you will often use Cargo to build your crates. Cargo is a package manager, tool that allows Rust packages to declare dependencies and makes sure you build is repeatable every time. It also makes cross compiling easy because you only need to define --target option and Cargo will then build crate for the correct target device. Of course, you need to first add cross compilation support for your target with rustup target add command: Cargo also helps with the code reuse as it is easy to bring new crates to your build. You may find 3rd party crates from the crates.io.

Build scripts

If you need to customize the build process, this can be done with the build scripts. It is Rust code that is executed on the build machine. You can use it to provide build time information, for example, to embed git hash to firmware version number. Or you might want to build external C library from the sources during the build of your Rust application. Build script can be executed every time file in package has changed and before actually compiling anything. Here is one example how to get the git hash to your build.

# Add these to your build.rs: use std::process::Command; fn main() { // Get git hash and store it to GIT_HASH environment variable. let o = Command::new("git").args(&["rev-parse", "HEAD"]).output().unwrap(); println!("cargo:rustc-env=GIT_HASH={}", String::from_utf8(o.stdout).unwrap()); # Then use it in your code: hprintln!(env!("GIT_HASH")).unwrap();

Tip: Check out also vergen crate.

Optimizations

As usual, optimizations provide a way to balance between the size and the efficiency of the compiled binary code. No optimizations are used by default when building dev profile. This helps with the debugging. When building release profile, by default it will be optimized for the speed (opt-level = 3). There are options to select optimization level or which crates to optimize in your Cargo.toml file. Here are couple of examples:

# don't optimize this crate but optimize # everything else for speed [profile.dev.package.stm32f0] opt-level = 0 [profile.dev.package."*"] opt-level = 2 [profile.release] # optimize binary size for the release build opt-level = "s"

Linker

You may control symbol names and memory section placement using the following attributes:

  • #[export_name = "foo"] - Set the symbol name to foo.
  • #[no_mangle] - Use this function or variable name as its symbol name.
  • #[link_section = ".foo"] - Place this symbol to a memory section named .foo.

Here is an example to specify function name as a symbol name and then link that symbol to the reset vector in the vector table:

#[no_mangle] pub unsafe extern "C" fn Reset() -> ! { // Initialize RAM ... // Call user entry point extern "Rust" { fn main() -> !; } main() } // The reset vector, a pointer into the reset handler #[link_section = ".vector_table.reset_vector"] #[no_mangle] pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

Inspection tools

After installing cargo-binutils, you may use it to inspect compiled binary file contents. Here are couple of examples how to use these tools. For more examples, read cargo-binutils

cargo readobj

This is the output from our PAC test app:

cargo readobj --bin pac-test -- --file-headers ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: ARM Version: 0x1 Entry point address: 0x80000C1 Start of program headers: 52 (bytes into file) Start of section headers: 879112 (bytes into file) Flags: 0x5000200 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 5 Size of section headers: 40 (bytes) Number of section headers: 23 Section header string table index: 21 # Display the section headers: $ cargo readobj --bin pac-test -- -t Finished dev [unoptimized + debuginfo] target(s) in 0.07s There are 23 section headers, starting at offset 0xd6a08: Section Headers: [Nr] Name Type Addr Off Size ES Lk Inf Al Flags ... # Display the symbol table: $ cargo readobj --bin pac-test -- -s Finished dev [unoptimized + debuginfo] target(s) in 0.07s Symbol table '.symtab' contains 223 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS 20zcpl38n8olc0kl 2: 080000fc 0 NOTYPE LOCAL DEFAULT 2 $t.0 3: 0800012c 0 NOTYPE LOCAL DEFAULT 2 $t.1 4: 08000192 0 NOTYPE LOCAL DEFAULT 2 $t.2 ...

cargo-size

Use this tool to get the size of the linker sections in the compiled binary:

# Display binary size in Berkeley format: $ cargo size --bin pac-test Finished dev [unoptimized + debuginfo] target(s) in 0.07s text data bss dec hex filename 2240 0 4 2244 8c4 pac-test # Display binary size in System V format: $ cargo size --bin pac-test -- -A Finished dev [unoptimized + debuginfo] target(s) in 0.07s pac-test : section size addr .vector_table 192 0x8000000 .text 1552 0x80000c0 .rodata 496 0x80006d0 .data 0 0x20000000 .gnu.sgstubs 0 0x80008c0 .bss 4 0x20000000 .uninit 0 0x20000004 .debug_abbrev 9254 0x0 .debug_info 185984 0x0 .debug_aranges 9224 0x0 .debug_ranges 89992 0x0 .debug_str 275714 0x0 .debug_pubnames 96689 0x0 .debug_pubtypes 11199 0x0 .ARM.attributes 50 0x0 .debug_frame 29236 0x0 .debug_line 160007 0x0 .debug_loc 225 0x0 .comment 109 0x0 Total 869927

cargo-objdump

Use this tool to disassemble the compiled binary:

$ cargo objdump --bin pac-test -- --disassemble --no-show-raw-insn --print-imm-hex Finished dev [unoptimized + debuginfo] target(s) in 0.07s pac-test: file format elf32-littlearm Disassembly of section .text: 080000c0 <__stext>: 80000c0: bl 0x800052e <__pre_init> @ imm = #0x46a 80000c4: ldr r0, [pc, #0x20] @ 0x80000e8 <$d.9> 80000c6: ldr r1, [pc, #0x24] @ 0x80000ec <$d.9+0x4> 80000c8: movs r2, #0x0 80000ca: cmp r1, r0 80000cc: beq 0x80000d2 <__stext+0x12> @ imm = #0x2 80000ce: stm r0!, {r2} 80000d0: b 0x80000ca <__stext+0xa> @ imm = #-0xa 80000d2: ldr r0, [pc, #0x1c] @ 0x80000f0 <$d.9+0x8> 80000d4: ldr r1, [pc, #0x1c] @ 0x80000f4 <$d.9+0xc> 80000d6: ldr r2, [pc, #0x20] @ 0x80000f8 <$d.9+0x10> 80000d8: cmp r1, r0 80000da: beq 0x80000e2 <__stext+0x22> @ imm = #0x4 80000dc: ldm r2!, {r3} 80000de: stm r0!, {r3} 80000e0: b 0x80000d8 <__stext+0x18> @ imm = #-0xc 80000e2: bl 0x800039c
@ imm = #0x2b6 80000e6: udf #0x0 080000e8 <$d.9>: 80000e8: 00 00 00 20 .word 0x20000000 80000ec: 04 00 00 20 .word 0x20000004 ...

Debugging

When working with the embedded systems, debugging is one important aspect. It is very annoying if you cannot figure out or doesn't have any means to debug what is wrong with your code. Here are couple of ways to debug your device.

Semihosting

cortex-m-semihosting crate enables to do semihosting on the host. Embedded devices are usually lacking interfaces for debugging, semihosting will enable ARM devices to do I/O on the host running a debugger.

If you use cargo generate to generate new project from https://github.com/rust-embedded/cortex-m-quickstart, it will create a "hello world" application for the semihosting. Wether you are using QEMU or OpenOCD, you need to first enable semihosting:

# With qemu-system-arm, give the following option when starting QEMU: -semihosting-config enable=on,target=native # With OpenOCD after connecting GDB, use monitor command to enable semihosting (gdb) monitor arm semihosting enable semihosting is enabled # main.rs: //! Prints "Hello, world!" on the host console using semihosting #![no_main] #![no_std] use panic_halt as _; use cortex_m_rt::entry; use cortex_m_semihosting::{debug, hprintln}; #[entry] fn main() -> ! { hprintln!("Hello, world!").unwrap(); // exit QEMU // NOTE do not run this on hardware; it can corrupt OpenOCD state debug::exit(debug::EXIT_SUCCESS); loop {} }

There is also panic-semihosting crate if you want to log your panic messages with the semihosting.

Instrumentation Trace Macrocell (ITM)

ITM is faster than semihosting but Cortex-M0 doesn't support it. You also need to have Single Wire Output (SWO) pin connected to the Single Wire Debug (SWD) interface. When you have GDB connected, you need to issue the following commands to send captured ITM to the file itm.fifo. These commands can be found from the openocd.gdb file when using cortex-m-quickstart project as a template:

# # send captured ITM to the file itm.fifo # # (the microcontroller SWO pin must be connected to the programmer SWO pin) # # 8000000 must match the core clock frequency monitor tpiu config internal itm.txt uart off 8000000 # # enable ITM port 0 monitor itm port 0 on

Here is the ITM example from the cortex-m-quickstart project.

#![no_main] #![no_std] use panic_halt as _; use cortex_m::{iprintln, Peripherals}; use cortex_m_rt::entry; #[entry] fn main() -> ! { let mut p = Peripherals::take().unwrap(); let stim = &mut p.ITM.stim[0]; iprintln!(stim, "Hello, world!"); loop {} }

After running this example, there should be itm.fifo file which you can parse with itm-decode tool.

There is also panic-itm crate if you want to log panic messages with ITM.

Real Time Transfer (RTT)

Rust has some crates which implement support for the RTT debugging, rtt-target for example. You can use the following code to print with the RTT, note that messages with debug_* macros are not visible in the release builds:

use rtt_target::{rtt_init_print, rprintln, debug_rprintln}; fn main() -> ! { rtt_init_print!(); loop { debug_rprintln!("Debugging!"); // not present in --release rprintln!("Hello, world!"); } }

If you need to implement a viewer on the host side also with Rust, there are crates like probe-rs-rtt to display messages on the host.

Using C code in Rust

On some cases, you might want to integrate your existing C code to your Rust projects. To do this, you will first need to define an interface to the C code (data types and function signatures) and then compile it to the shared library either with an external build system or with Rust cc crate.

Foreign Function Interface generation

There is a bindgen tool to generate these bindings automatically. For example, here is my c_code "library" I want to use in Rust and the generated bindings for it:

/* File: c_code.h */ typedef struct CStruct { int x; int y; } CStruct; int c_function(int i, char c, CStruct* cs); /* File: c_code.c */ #include "c_code.h" int c_function(int i, char c, CStruct* cs) { // Just do something here int r = 0; if (i == 1) { r = cs->x; } else if (i == 2) { r = cs->y; } else { r = cs->x + cs->y + c; } return r; } /* automatically generated by rust-bindgen 0.66.1 */ /* with command: bindgen --ctypes-prefix=cty --use-core src/c_code.h -o src/bindings.rs */ #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct CStruct { pub x: cty::c_int, pub y: cty::c_int, } extern "C" { pub fn c_function(i: cty::c_int, c: cty::c_char, cs: *mut CStruct) -> cty::c_int; }

Building with cc crate

To build the C code, you need to add the following dependencies to the Cargo.toml file:

[dependencies] cty = "0.2.2" [build-dependencies] cc = "1.0"

The following code must be added to the build.rs file to build C sources with the cc crate:

cc::Build::new().file("src/c_code.c").compile("c_code");

Now, in your Rust source file, you can create a structure and then call the C function. Note that calling external function must be within unsafe block.

mod bindings; #[entry] fn main() -> ! { // Create C struct let mut cs = bindings::CStruct {x: 5, y: 11}; // Create raw pointer for the C struct let raw_pointer = &mut cs as *mut bindings::CStruct; // Call C function within unsafe code block let _result = unsafe { bindings::c_function(42, 5, raw_pointer); };

Using Rust code in C

If yuu current projects are written with C, you might want to trial and implement a part of it using Rust. You can do this by first creating a Rust library with the code you like to export and then specifying crate-type = ["staticlib"] in the Cargo.toml. Building this Rust library creates a shared library which you can then link with your code and generate the C headers with cbindgen tool.

# First create new library for your project: cargo new rust-in-c-test --lib # Add following lines to your Cargo.toml: [lib] name = "rustinc" crate-type = ["staticlib"] # Creates static lib # I modified src/lib.rs to include the following code: #![no_std] use panic_halt as _; # All code exposed outside must not be mangled #[no_mangle] pub extern "C" fn rust_called_from_c(a: i32, b: i32) -> i32 { let sum: i32 = a + b; return sum; } # Create a shared library: cargo build --target thumbv6m-none-eabi # Install cbindgen tool to create C headers from the Rust sources: cargo install --force cbindgen # Download cbindings.toml template: curl -LO https://raw.githubusercontent.com/mozilla/cbindgen/master/template.toml # Create headers for the C code: cbindgen --config template.toml --crate rust-in-c-test --output my_header.h --lang c

Using assembly code in Rust

When using the latest stable Rust, inline assembly can be used to write assembly code directly inside your Rust code.

With older versions, you need to do it manually as described here. The idea is the same as with C code, you implement assembly code to .s file and then build it with the cc crate. Here is an example from The Embedonomicon:

# Create asm.s file with assembly code: .section .text.HardFaultTrampoline .global HardFaultTrampoline .thumb_func HardFaultTrampoline: mrs r0, MSP b HardFault # Add cc crate dependency to Cargo.toml: [build-dependencies] cc = "1.0.25" # Modify build.rs to compile with cc: use cc::Build; fn main() -> Result<(), Box> { ... // assemble the `asm.s` file Build::new().file("asm.s").compile("asm"); // rebuild if `asm.s` changed println!("cargo:rerun-if-changed=asm.s");

Common topics

Iterators for arrays

In Rust, one should use iterators for an array access instead of indexing because every indexed access needs to be bound checked and that is slow and may prevent other compiler optimizations. Here is a simple example how to iterate array items:

let my_array = [1, 2, 3, 4, 5]; for my_array_element in my_array.iter() { // Do something with the array element //*my_array_element; }

Math functions

When you are using core crate, you are not having math functions. The Embedded Rust Book lists couple of alternative crates you may use instead.

Dynamic memory management

If you need to use dynamic data structures, compiler is shipped with alloc crate which enables dynamic memory allocation when using core crate. Another way is to use fixed capacity collections which are provided by the heapless crate. But with this, you need to define beforehand how much memory to reserve and check failures more often. But as usual, there are trade-offs when using these. Please find the examples from here.

Conclusion

I tried to cover several different topics in this post. The most of these should help you to get started with Rust and Cortex-M based boards. You should also check the linked documentation for more detailed information.

While I was testing the codes used here, I noticed that one of the biggest difference between Rust and C is the cargo package manager. It feels so easy to reuse code with crates found from the crates.io.

References