Rust on the ARM Cortex M3

A couple of weekends ago, I decided to try to get some Rust running on the ARM Cortex M3. Until this point, all of the firmware I had written, personally and professionally, was done in C and Assembly. Given the safety guarantees and high performance provided by Rust, it was a very enticing choice. I was curious to see how involved the process would be and if I could ditch C for future projects (spoiler: yes).

Setting up the Development Environment

For those who aren’t familiar with Rust, it can easily be installed via rustup. Rustup can then be used to install Rust itself (using the nightly toolchain). I use NixOS on my machine, so I had to do a little work with patchelf to make sure the binaries would run properly. Most people won’t have to worry about this though. I also had to download the Rust sources for the next step.

$ rustup component add rust-src

Cargo is the package manager and build system used throughout the Rust ecosystem. It’s an impressive tool but as of today it doesn’t handle cross-compilation completely. Fortunately, Jorge Aparicio (this is a name you are going to see a lot) has put together xargo to tide us over. This is effectively a drop-in replacement for cargo and can even be installed using cargo!

$ cargo install xargo

Now that we have all of the tools, we can start our project.

$ cargo init --bin arm-test

Let’s go ahead and add a few options to Cargo.toml .

[package] name = "arm-test" version = "0.0.0" [profile.dev] lto = true [profile.release] lto = true

This will enable link-time optimizations which give us potentially smaller binaries and allows us to avoid needing to define the __aeabi_unwind_cpp_pr0 symbol.

Setting up the Hardware

For these experiments, I used the STK-3600 evaluation board from Silicon Labs. This board uses the EFM32LG990F256, which features an ARM Cortex M3 core. Since this board has a built-in SEGGER J-Link debugger, I just had to install their JLinkGDBServer (overall a pretty terrible tool) to get going. In the future, I need to migrate to using OpenOCD, but one step at a time.

The Simplest Program

The first step I took was trying to get a very simple program running.

#![feature(asm)] macro_rules! breakpoint { ( $arg:expr ) => ( unsafe { asm! ( "BKPT $0" : : "i" ( $arg ) : : "volatile" ) } ) } pub fn main () { breakpoint! ( 1 ); }

The hope was that the program would immediately break to the hardware debugger so I could catch it in GDB. Knowing this couldn’t possibly build, I tried anyway.

$ xargo build Compiling core v0.0.0 (file:///home/alex/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore) Finished release [optimized] target(s) in 26.55 secs Compiling arm-test v0.0.0 (file:///home/alex/arm-test) error[E0463]: can't find crate for `std` error: aborting due to previous error error: Could not compile `arm-test`. To learn more, run the command again with --verbose.

Removing the Standard Library

Okay, so it couldn’t find the std crate. Not surprising given that there is no libc for this platform. Let’s go ahead and tell Rust not to use std by adding #![no_std] .

$ xargo build Compiling arm-test v0.0.0 (file:///home/alex/arm-test) error: language item required, but not found: `panic_fmt` error: aborting due to previous error error: Could not compile `arm-test`. To learn more, run the command again with --verbose.

Hmm, this is telling us that Rust needs a way to panic but because we’ve omitted std , it doesn’t know how. We can go ahead and write our own panic routine.

#[lang = "panic_fmt" ] fn rust_begin_panic ( _ msg : core :: fmt :: Arguments , _ file : & 'static str , _ line : u32 ) -> ! { loop {} }

Let’s try compiling and linking again.

$ xargo build Compiling arm-test v0.0.0 (file:///home/alex/arm-test) error: <inline asm>:1:2: error: invalid instruction mnemonic 'bkpt' BKPT $1 ^ --> src/main.rs:7:18 | 7 | unsafe { asm!("BKPT $0" : : "i"($arg) : : "volatile") } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ... 12 | breakpoint!(1); | --------------- in this macro invocation error: aborting due to previous error error: Could not compile `arm-test`. To learn more, run the command again with --verbose.

Wooo! Now it’s complaining that bkpt isn’t a valid instruction. This makes perfect sense because we forgot to mention that we were targeting a different platform from our host.

Targeting ARM

We can go ahead and create .cargo/config with the following contents:

[build] target = "thumbv7m-none-eabi"

This target triple says that we want to use the Thumb instruction set, targeting an ARM v7 ISA, with no EABI.

Now we can try to build again.

$ xargo build Compiling arm-test v0.0.0 (file:///home/alex/arm-test) error: linking with `arm-none-eabi-gcc` failed: exit code: 1 | = note: "arm-none-eabi-gcc" "-L" "/home/alex/.xargo/lib/rustlib/thumbv7m-none-eabi/lib" "/home/alex/arm-test/target/thumbv7m-none-eabi/debug/deps/arm_test-e1c4f2365854cb65.0.o" "-o" "/home/alex/arm-test/target/thumbv7m-none-eabi/debug/deps/arm_test-e1c4f2365854cb65" "-Wl,--gc-sections" "-nodefaultlibs" "-L" "/home/alex/arm-test/target/thumbv7m-none-eabi/debug/deps" "-L" "/home/alex/arm-test/target/debug/deps" "-L" "/home/alex/.xargo/lib/rustlib/thumbv7m-none-eabi/lib" "-Wl,-Bstatic" "-Wl,-Bdynamic" = note: /nix/store/2s66715fca9vq0684xrlnhf2zp04avr2-gcc-arm-embedded-5.4-2016q2-20160622/bin/../lib/gcc/arm-none-eabi/5.4.1/../../../../arm-none-eabi/lib/crt0.o: In function `_start': (.text+0x90): undefined reference to `memset' /nix/store/2s66715fca9vq0684xrlnhf2zp04avr2-gcc-arm-embedded-5.4-2016q2-20160622/bin/../lib/gcc/arm-none-eabi/5.4.1/../../../../arm-none-eabi/lib/crt0.o: In function `_start': (.text+0xe0): undefined reference to `__libc_init_array' /nix/store/2s66715fca9vq0684xrlnhf2zp04avr2-gcc-arm-embedded-5.4-2016q2-20160622/bin/../lib/gcc/arm-none-eabi/5.4.1/../../../../arm-none-eabi/lib/crt0.o: In function `_start': (.text+0xec): undefined reference to `main' /nix/store/2s66715fca9vq0684xrlnhf2zp04avr2-gcc-arm-embedded-5.4-2016q2-20160622/bin/../lib/gcc/arm-none-eabi/5.4.1/../../../../arm-none-eabi/lib/crt0.o: In function `_start': (.text+0xf0): undefined reference to `exit' collect2: error: ld returned 1 exit status error: aborting due to previous error error: Could not compile `arm-test`. To learn more, run the command again with --verbose.

Progress! We successfully compiled our program but failed to link. We’ll need to let the linker know not to try to pull in the standard “start” objects. Since the processor is jumping straight into our program there is no need for them.

[target.thumbv7m-none-eabi] rustflags = [ "-C" , "link-arg=-nostartfiles" , ]

And let’s build once more.

$ xargo build Compiling arm-test v0.0.0 (file:///home/alex/arm-test) Finished dev [unoptimized + debuginfo] target(s) in 1.56 secs

Yes! It built. Let’s see what we got.

$ arm-none-eabi-size target/thumbv7m-none-eabi/debug/arm-test text data bss dec hex filename 0 0 0 0 0 target/thumbv7m-none-eabi/debug/arm-test

Holy cow. They weren’t kidding when they said Rust was fast…

Linker Script

The reason we have a zero-byte binary is because we haven’t yet written our linker script. Our what? The often-overlooked linker script is responsible for directing the linker to place particular symbols and sections in particular places in the resulting binary. Given this particular processor, I can write the following script into layout.ld :

MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K } SECTIONS { /* Set stack top to end of RAM */ __StackTop = ORIGIN(RAM) + LENGTH(RAM); .text : { LONG(__StackTop); KEEP(*(.reset_handler)); *(.text*) } > FLASH }

Most of the details of this script come straight out of the datasheet for the processor, but this ends up writing a pointer to the end of the stack into the first word, a pointer to something called .reset_handler into the next word, and then the reset of the text section into the subsequent bytes.

In order to use this new linker script, we’ll need to add "-C", "link-arg=-Tlayout.ld", into the rustflags in our Cargo config. We’ll also need to declare .reset_handler and while we are at it, we can mark the main function “naked” by adding the following above our main function:

#[naked] #[link_section = ".reset_handler" ]

The “naked” attribute tells Rust not to bother setting up the stack.

Now if we build, we’ll see the following:

$ arm-none-eabi-size target/thumbv7m-none-eabi/debug/arm-test text data bss dec hex filename 22 0 0 22 16 target/thumbv7m-none-eabi/debug/arm-test

That’s a pretty small binary. Does it work? Let’s look at the disassembly.

$ arm-none-eabi-objdump -d target/thumbv7m-none-eabi/debug/arm-test target/thumbv7m-none-eabi/debug/arm-test: file format elf32-littlearm Disassembly of section .text: 00000000 <_ZN11arm_test4main17h664bd0a871575671E-0x4>: 0: 20008000 .word 0x20008000 00000004 <_ZN11arm_test4main17h664bd0a871575671E>: 4: e7ff b.n 6 <_ZN11arm_test4main17h664bd0a871575671E+0x2> 6: be01 bkpt 0x0001 8: 4770 bx lr a: 0000 movs r0, r0 ...

We can see that the first word is a pointer to the end of the stack, the second word is a pointer to main() (hence the goofy branch instruction), and the remainder is our program; just as the linker script described!

Wrapping Up

I have some bad news. That program isn’t going to work very well. If we look at the datasheet more carefully, we’ll see that our text section trampled all over the processor’s exception vectors. We’ll have to push our text section back a little bit. The resulting linker script will end up as follows:

MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K } SECTIONS { /* Set stack top to end of RAM */ __StackTop = ORIGIN(RAM) + LENGTH(RAM); .text : { LONG(__StackTop); KEEP(*(.reset_handler)); . = 0xD8; *(.text*) } > FLASH }

Our source will look as follows:

#![feature(asm, lang_items, naked_functions)] #![no_std] #![no_main] macro_rules! breakpoint { ( $arg:expr ) => ( unsafe { asm! ( "BKPT $0" : : "i" ( $arg ) : : "volatile" ) } ) } #[naked] pub fn main () { breakpoint! ( 1 ); } #[link_section = ".reset_handler" ] static RESET : fn () = main ; #[lang = "panic_fmt" ] fn rust_begin_panic ( _ msg : core :: fmt :: Arguments , _ file : & 'static str , _ line : u32 ) -> ! { loop {} }

And the .cargo/config will contain the following:

[build] target = "thumbv7m-none-eabi" [target.thumbv7m-none-eabi] rustflags = [ "-C" , "link-arg=-nostartfiles" , "-C" , "link-arg=-Tlayout.ld" , ]

The result of this is that the text section will have moved beyond the exception and interrupt service routines.

$ arm-none-eabi-objdump -d target/thumbv7m-none-eabi/debug/arm-test target/thumbv7m-none-eabi/debug/arm-test: file format elf32-littlearm Disassembly of section .text: 00000000 <_ZN11arm_test5RESET17hff9b8922f20d7d99E-0x4>: 0: 20008000 .word 0x20008000 00000004 <_ZN11arm_test5RESET17hff9b8922f20d7d99E>: 4: 000000d9 00000000 00000000 00000000 ................ ... 000000d8 <_ZN11arm_test4main17h664bd0a871575671E>: d8: e7ff b.n da <_ZN11arm_test4main17h664bd0a871575671E+0x2> da: be01 bkpt 0x0001 dc: 4770 bx lr

If we flash this program and run it via GDB, we’ll see that it now works!

(gdb) load target/thumbv7m-none-eabi/debug/arm-test Loading section .text, size 0xde lma 0x0 Loading section .ARM.exidx.text._ZN11arm_test4main17h664bd0a871575671E, size 0x8 lma 0xe0 Start address 0x0, load size 230 Transfer rate: 18 KB/sec, 115 bytes/write. (gdb) c Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. arm_test::main () at /home/alex/arm-test/src/main.rs:13 13 breakpoint!(1);

And that address is exactly where we expected it to break. Simple!

What’s Next

This has turned into a longer post than I originally intended. I also wanted to cover hard-fault handlers, but we can save that for next time.

I’d also like to thank Jorge Aparicio and Brandon Edens for helping me through this whole process.