Article

Rust for Mission Critical Applications

Published on 8 min read

    As you may be aware, Ferrous Systems has done a lot of work around Rust and Embedded Systems; be they "big" embedded systems that run on real-time OS like QNX, or "small" embedded systems that run on bare-metal. We were delighted to have this recognised by the European Space Agency back in May, when their Software Product Assurance group invited us to give a talk on Rust at their upcoming Workshop in Madrid.

    There's now a sort of "standard pitch" for Embedded Rust, and many enthusiasts of the language will be accustomed to sharing its benefits with colleagues and associates. So I thought I'd use my talk at ESA to briefly cover all that, and then tell two stories.

    The first is the story of how we made Ferrocene, and how Ferrocene is just Rust, but aimed at safety-critical and security applications. I talk about the changes we've made, and the road to ISO 26262 qualification and all that entails. But don't worry, you're not missing out! We're covering the same topics in a series of blog posts here, and of course there's our big announcement on 4 October (which you should sign up to attend as you won't want to miss it).

    Our second story is about getting Embedded Rust on to a new platform. The Arm Cortex-M experience is now fairly smooth, thanks to the amazing work of The Rust Project and its Rust Embedded Working Group. But whilst the space crowd do use some Arm processors, I was aware they still have a fondness for something you might have written off as a retro 90's throwback - SPARC.

    Gaisler GR716B rad-hardened CPU in specialised packaging

    Credit: Frontgrade Gaisler

    Back in 1997, ESA was looking to develop its own fault-tolerant, radiation-hardened processors for their space missions. Space is an incredibly harsh environment and your average microcontroller will crash within moments due to energetic particles passing through your silicon and randomly flipping its precious 1s and 0s. They wanted to design the processor around an open architecture, and they settled upon Version 8 of the the SPARC Instruction Set Architecture. Originally designed for Sun's UNIX workstations (first the Sun-4, then the SuperSPARC and UltraSPARC), the SPARC architecture had been in the hands of SPARC International since 1989.

    If you are only familiar with modern Arm and x86 processors, it is worth doing a little digging into SPARC. The register windowing system is quite unlike what you might be used to.

    ESA still uses variants of the SPARC-based LEON processor in projects, because it has been proven reliable over many missions. But does Rust run on SPARC? I was bound to get the question at the ESA SWPA Workshop, so I figured I had best find out first.

    Rust supports many architectures by virtue of using LLVM as the code-generating back-end. And indeed, SPARC was one of the instruction sets that Rust's LLVM knows how to generate. However, Rust only had targets for sparcv9-sun-solaris (SPARC V9 running Sun's Solaris UNIX operating system) and sparc64-unknown-linux-gnu (a 64-bit UltraSPARC running Linux). I wanted bare-metal SPARC V8.

    Luckily, Rust doesn't actually care all that much about your target's chosen architecture - that's largely LLVM's job. So much so that you can just give Rust a new target file at compile time - one that says "Hey, please make me SPARC instructions, use this linker, and the machine natively understands integers, floats and pointers that are this wide". First I needed a suitable linker, so I used BCC2 from Frontgrade Gaisler. Frontgrade Gaisler produce processors based on the LEON core, as well as open-source VHDL for use with FPGAs and an open-source toolchain for them. I selected their sparc-gaisler-elf-clang compiler from their BCC2 toolchain, as both clang and Rust are LLVM based.

    So, I wrote this target file, sparc-unknown-none-elf.json:

    {
      "arch": "sparc",
      "data-layout": "E-m:e-p:32:32-i64:64-f128:64-n32-S64",
      "emit-debug-gdb-scripts": false,
      "is-builtin": false,
      "linker": "sparc-elf-gcc",
      "no-default-libraries": false,
      "target-endian": "big",
      "linker-flavor": "gcc",
      "llvm-target": "sparc-unknown-none-elf",
      "max-atomic-width": 32,
      "panic-strategy": "abort",
      "relocation-model": "static",
      "target-pointer-width": "32"
    }
    
    

    You can use this target file by executing cargo build --target=sparc-unknown-none-elf.json, but this only works with nightly Rust because target files are not stabilised.

    Having created a target, I needed some suitable code to build with it - the classic "Hello World" assumes you have listd and that is not available here. Instead, I created a simple Rust binary that used putchar function from the BCC2 C library for console output. The program prints a simple 10 x 10 multiplication square:

    #![no_std]
    #![no_main]
    
    use core::fmt::Write;
    
    extern "C" {
        fn putchar(ch: i32);
        fn _exit(code: i32) -> !;
    }
    
    /// Represents the standard-output available in tsim.
    ///
    /// Uses the `putchar` C function to print text.
    struct Console;
    
    impl core::fmt::Write for Console {
        fn write_str(&mut self, message: &str) -> core::fmt::Result {
            for b in message.bytes() {
                unsafe {
                    putchar(b as i32);
                }
            }
            Ok(())
        }
    }
    
    /// A C-shim which libc knows how to call on start-up.
    ///
    /// Just jumps to [`rust_main`].
    #[no_mangle]
    pub extern "C" fn main() -> i32 {
        if let Err(e) = rust_main() {
            panic!("Main returned {:?}", e);
        } else {
            0
        }
    
    }
    
    /// The main function for our Rust program
    fn rust_main() -> Result<(), core::fmt::Error> {
        let mut console = Console;
        writeln!(console, "Hello, this is Rust!")?;
        write!(console, "    ")?;
        for y in 0..10 {
            write!(console, "{:2} ", y)?;
        }
        writeln!(console)?;
        for x in 0..10 {
            write!(console, "{:2}: ", x)?;
            for y in 0..10 {
                write!(console, "{:2} ", x * y)?;
            }
            writeln!(console)?;
        }
        panic!("I am a panic");
    }
    
    /// Called when a panic occurs.
    #[panic_handler]
    fn panic(panic: &core::panic::PanicInfo) -> ! {
        let mut console = Console;
        let _ = writeln!(console, "PANIC: {:?}", panic);
        unsafe {
            _exit(1);
        }
    }
    
    

    After setting our cargo configuration (.cargo/config.toml) to build libcore from source, and to pass the right arguments to BCC2 (-mcpu=leon3, -qbsp=leon3 and -latomic), I could make a bare-metal SPARC binary!

    A binary is great, but now I needed a LEON3 to test it on. Helpfully, Frontgrade Gaisler have an evaluation version of TSIM3, a LEON3 processor simulator. One nice feature of TSIM3 and BCC2 is that that the putchar function in BCC2's C library causes characters to be printed to the console within the TSIM3 simulator - no messing around setting up UARTs!

    $ tsim-leon3 ./target/sparc-unknown-none-elf/debug/sparc-demo-rust
    
    TSIM3 LEON3 SPARC simulator, version 3.1.9
    
    Copyright (C) 2023, Frontgrade Gaisler - all rights reserved.
    For latest updates, go to https://www.gaisler.com/
    Comments or bug-reports to support@gaisler.com
    
    Number of CPUs: 1
    icache: 4 * 4 KiB, 16 bytes/line (16 KiB total)
    dcache: 4 * 4 KiB, 16 bytes/line (16 KiB total)
    section: .text, addr: 0x40000000, size: 100432 bytes
    section: .rodata, addr: 0x40018850, size: 15776 bytes
    section: .data, addr: 0x4001c5f0, size: 1176 bytes
    read 995 symbols
    
    tsim> run
      Initializing and starting from 0x40000000
    Hello, this is Rust!
         0  1  2  3  4  5  6  7  8  9
     0:  0  0  0  0  0  0  0  0  0  0
     1:  0  1  2  3  4  5  6  7  8  9
     2:  0  2  4  6  8 10 12 14 16 18
     3:  0  3  6  9 12 15 18 21 24 27
     4:  0  4  8 12 16 20 24 28 32 36
     5:  0  5 10 15 20 25 30 35 40 45
     6:  0  6 12 18 24 30 36 42 48 54
     7:  0  7 14 21 28 35 42 49 56 63
     8:  0  8 16 24 32 40 48 56 64 72
     9:  0  9 18 27 36 45 54 63 72 81
    PANIC: PanicInfo { payload: Any { .. }, message: Some(I am a panic), location: Location { file: "src/main.rs", line: 56, col: 5 }, can_unwind: true, force_no_backtrace: false }
    
      Program exited normally on CPU 0.
    tsim>
    
    

    Of course a JSON target file is OK, but it's nicer if these things are built into Rust itself. So I opened PR #113535 and upstreamed the support at Tier 3. So now if you want to try Rust on SPARC for yourself, you just need nightly Rust, the BCC2 toolchain from Gaisler, and the evaluation version of the TSIM3 simulator. And because we are an open-source company, all the example code, including a Dockerfile that sets everything up, is available on our Github at https://github.com/ferrous-systems/sparc-experiments.

    I shared our developments with Frontgrade Gaisler, who were kind enough to test out our examples on a prototype version of the GR765 LEON 5 based system - and it worked!

    If you are attending the ESA Software Product Assurance Workshop, do look out for Florian and I - we hope to talk to everyone about Ferrocene! And if there's a platform you'd like Ferrocene to support for your next mission-critical project, reach out.