Rust & Vendor SDKs (I)

Time for another Rust post. Today we’re going to take a look at chip vendor SDKs to answer the question how well suited they are for use with Rust. Answering this question is somewhat important, as there are no embedded HAL ports for most chips out there today. While there’s decent support for ST and Nordic chips, there’s not much else to be found, so, if we do want to use Rust with these chips we’re left with the vendor provided code, which is usually written in C.

Pitfalls

As I’ve pointed out in numerous other posts, the C preprocessor can be tricky to work with, if C code one wants to interface with uses macros as part of its api. SDKs usually don’t do that, but they often use preprocessor defines quite liberally for configuration, so we will probably have to work with that to some extend.

Also there’s the vendor’s tooling which often tags along some degree of integration with the SDK (e.g. TI’s SysCfg and Code Composer), which will make integration with Rust not necessarily easier.

The Setup

For chips, where we have no startup code and no embedded HAL implementation the easiest way to integrate Rust is often a CMake based build, where we use C right up to main()and call to Rust from there. This avoids almost all problems we might have with linkers, stack setup and the vector table. We’ll see how that works with the different SDKs.

TI MSPM0

The first contender we have a look at is TI’s MSPM0 SDK. Right off the bat, there are some issues we’re facing:

  • TI really wants us to use their XDS110 probes however common tools like openOCD have limited support for these probes
  • The MSPM0 is still quite new, so there is also no support from third party tools.

It seems like we’re stuck using TI’s tools for building and debugging (or IAR, which curiously support the MSPM0 already). The simplest way to get code deployed on an MSPM0 Launchpad is to use TI’s CCS Theia IDE. However the whole toolchain provided by TI is not very mature (at least at the time of writing this), so the setup does get somewhat wonky. So, first things first. We need:

  • The MSPM0 SDK
  • A CCS Installation (ideally a recent one)

Drivers

Looking at the headerfiles, that contain the APIs for the peripheral drivers, things look great:

  • There are very few preprocessor defines used to configure the SDK on this level.
  • The API functions usually take a handle to the peripheral they are supposed to work with, and that handle seems to be an opaque type, i.e. its easy to use from the Rust side.

Sadly, TI provides drivers in two parts the raw “drivers” and the so-called “driverslib”, which expands on the drivers. The driverslib has a couple of issues, that will make our job harder:

  • More defines need to be set for it to compile correctly
  • It uses static inline functions for parts of its API, which are not easily visible for Rust.

Interrupts

TI uses weak symbols to define interrupt handlers. This is nice in the C world as it allows the user to overload only those handlers she needs however, when adding Rust into the mix things do get a bit complicated. Since we compile Rust as a static library any handlers defined in Rust will be dropped by the linker and we will end up with the weakly defined default handlers. This is somewhat frustrating, as we don’t get a warning or anything, our own handlers will just never get called. The only thing indicating the problem will be the fact, that we won’t be able to set breakpoints in our custom handlers. To get this to work we’ll have to write our usual C handler, that will immediately call the Rust handler:

// Irq.c
extern void UART1_IRQHandler_rs();
void UART1_IRQHandler(void)
{
  UART1_IRQHandler_rs();
}
// Irq.rs
#[no_mangle]
extern "C" fn UART1_IRQHandler_rs()
{
  // your code here.
}

The above is inconvenient but not horrible. Also, using weak symbols this way is not unique to TI’s SDK, but is something we’ll see elsewhere as well, so we might as well get used to it.

The Build

Thankfully TI provides almost everthing with standard make files, so there is a plethora of choices on how to integrate and – even better – we can easily see how TI invokes the compiler (this is interesting, if we want to use bindgen). What is somewhat annoying is, that CCS is very opinionated with respect to the way the build is controlled. While integrating into a full CMake build is possible, that is beyond the scope of this article, so we will use a simpler, albeit less comfortable route to get our Rust code compiled. Instead of a full integration we’ll link a pre-compiled static library.

The Build Script

As usual we’ll start with the build script to generate our import library. As mentioned before, we can get all the required information to write the script from the make file generated by TI’s tool for us, those would be:

  • Include Directories
  • Preprocessor Defines

On a whole this is a bit fiddly to get going and will need some trial and error, as the include paths for the toolchain are somewhat implicit. I got a working version with this:

extern crate bindgen;

use std::path::{PathBuf};


fn main() {

    // Tell cargo to invalidate the built crate whenever the wrapper changes
    println!("cargo:rerun-if-changed=wrapper.h");
      
    // The bindgen::Builder is the main entry point
    // to bindgen, and lets you build up options for
    // the resulting bindings.
    let bindings = bindgen::Builder::default()
        // The input header we would like to generate
        // bindings for.
        .use_core()
        .detect_include_paths(true)
        .clang_arg("-IC:/ti/ccs1260/ccs/tools/compiler/ti-cgt-armllvm_3.2.1.LTS/lib")
        .clang_arg("-IC:/ti/ccs1260/ccs/tools/compiler/ti-cgt-armllvm_3.2.1.LTS/include/c")
        .clang_arg("-IC:/ti/ccs1260/ccs/tools/compiler/ti-cgt-armllvm_3.2.1.LTS/include/c++/v1")
        .clang_arg("-IC:/ti/ccs1260/ccs/tools/compiler/ti-cgt-armllvm_3.2.1.LTS/include/armv6m-ti-none-eabi/c++/v1")        
        .clang_arg("-IC:/ti/ccs1260/ccs/tools/compiler/ti-cgt-armllvm_3.2.1.LTS/lib/clang/15.0.7/include")                
        .clang_arg("-IC:/ti/mspm0_sdk_1_30_00_03/source")
        .clang_arg("-IC:/ti/mspm0_sdk_1_30_00_03/source/third_party/CMSIS/Core/Include")
        .clang_arg("-IC:/Users/phili/workspace_v12/empty_mspm0g3107_nortos_ticlang/Debug/syscfg")                                                               
        .clang_arg("-IC:/Users/phili/workspace_v12/empty_mspm0g3107_nortos_ticlang")                        
        .clang_arg("-IC:/Users/phili/workspace_v12/empty_mspm0g3107_nortos_ticlang/Debug")        
        .clang_arg("-D__MSPM0G3507__")
                                                       
        .header("./sdk.h")        
        // Finish the builder and generate the bindings.
        .generate()
        // Unwrap the Result and panic on failure.
        .expect("Unable to generate bindings");

    bindings
        .write_to_file(PathBuf::from("./src/bindings.rs"))
        .expect("Couldn't write bindings!");

}

Notable here are the following:

  • A bunch of the paths point into the CCS Workspace (the “C:/Users/…”) – these will obviously need to be adapted to your use case. Ideally these paths would be populated by an environment variable or similar, as to be able to easily share the crate.
  • The other paths are mostly specific to the installation of your TI tools. I’ve opted to install into defaul directories as suggested by TI’s installers, so you should be fine.
  • Note that you will have to run cargo build with the correct target set, otherwise you will get weird errors from the build script. For the M0 use “thumbv6m-none-eabi”.

You’ll notice, that bindgen gets passed the file “sdk.h”. This file pulls in all relevant includes. Luckily TI makes this pretty easy for us, and that is a oneliner:

#include "ti_msp_dl_config.h"

Assuming we have previously defined and configured UART0 in SysCfg for this project we can now do the following in our Rust crate:

#![no_std]

use core::panic::PanicInfo;

mod bindings;

#[no_mangle]
pub extern "C" fn rust_main() -> !
{
    loop {        
        unsafe
        {
            bindings::DL_UART_transmitDataBlocking(bindings::UART0, 0xAB);
        }
    }
}

#[panic_handler]
pub fn panic_handler(info: &PanicInfo) -> !
{
    loop {}
}

Compiling this will drop our library file into the target/thumbv6m-none-eabi/ folder of the Rust crate (ignore the warnings – the generated code is not what Rust likes and it will complain alot about naming/snake case and other things). Now we can switch to CCS and link that into our application and call into the Rust code (I assume the reader is capable of instructing the linker to link the static lib we just compiled):

extern void rust_main();

int main(void)
{
    SYSCFG_DL_init();

    rust_main();

    while (1) {
    }
}

Should work, shouldn’t it?

Well, no:

"C:/ti/ccs1260/ccs/tools/compiler/ti-cgt-armllvm_3.2.1.LTS/bin/tiarmclang.exe" @"syscfg/device.opt"  -march=thumbv6m -mcpu=cortex-m0plus -mfloat-abi=soft -mlittle-endian -mthumb -O2 -gdwarf-3 -Wl,-m"empty_mspm0g3107_nortos_ticlang.map" -Wl,-i"C:/ti/mspm0_sdk_1_30_00_03/source" -Wl,-i"C:/Users/phili/workspace_v12/empty_mspm0g3107_nortos_ticlang/Debug/syscfg" -Wl,-i"C:/ti/ccs1260/ccs/tools/compiler/ti-cgt-armllvm_3.2.1.LTS/lib" -Wl,--diag_wrap=off -Wl,--display_error_number -Wl,--warn_sections -Wl,--xml_link_info="empty_mspm0g3107_nortos_ticlang_linkInfo.xml" -Wl,--rom_model -o "empty_mspm0g3107_nortos_ticlang.out" "./empty_mspm0g3107.o" "./syscfg/ti_msp_dl_config.o" "./startup_mspm0g310x_ticlang.o" -Wl,-l"syscfg/device_linker.cmd"  -Wl,-l"E:/code/MSPM0Rust/rustm0/target/thumbv6m-none-eabi/debug/librustm0.a" -Wl,-ldevice.cmd.genlibs -Wl,-llibc.a 
makefile:138: recipe for target 'empty_mspm0g3107_nortos_ticlang.out' failed
 
 undefined first referenced                                                                                                     
  symbol       in file                                                                                                          
 --------- ----------------                                                                                                     
 UART0     E:/code/MSPM0Rust/rustm0/target/thumbv6m-none-eabi/debug/librustm0.a<rustm0-9be9897170d9dd86.4trm7gtge842hiap.rcgu.o>
 
error #10234-D: unresolved symbols remain
error #10010: errors encountered during linking; "empty_mspm0g3107_nortos_ticlang.out" not built

We are greeted with a – somewhat weird – linker error. That is somewhat unfortunate, as the UART0 symbol shows up in the code that was generated by bindgen and it also shows up in the C code. So what’s wrong here? I’m not quite sure what the concrete problem is, but I guess it has to do with the visibility of static variables across compilation units. This is just a guess though. The easiest fix for this issue is to add a getter function to the C code, that will return a pointer to the required UART instance:

#include "ti_msp_dl_config.h"

extern void rust_main();

UART_Regs* getU0()
{
    return UART0;
}

int main(void)
{
    SYSCFG_DL_init();

    rust_main();

    while (1) {
    }
}

And do the matching thing on the Rust side:

#![no_std]

use core::panic::PanicInfo;

mod bindings;

extern "C"
{
    fn getU0() -> *mut bindings::UART_Regs;
}

#[no_mangle]
pub extern "C" fn rust_main() -> !
{
    loop {        
        unsafe
        {
          bindings::DL_UART_transmitDataBlocking(getU0(), 0xAB);
        }
    }
}

#[panic_handler]
pub fn panic_handler(info: &PanicInfo) -> !
{
    loop {}
}

The above will compile just fine.

Wrap Up, Part 1

So, we got Rust to compile and link for the MSPM0, and we are also able to call into the MSP’s SDK code from Rust. The required effort to get to this point was about 1.5 hours, with most of the time being spent on figuring out the build script, as the rest is actually pretty basic stuff. I’m somewhat impressed, that we only needed a single “define” in the script to get stuff to work, I would’ve expected much worse. I’ve obviously not tested the whole API, however (and, truth be told: The goal was to see the effort required to get stuff to compile – I did not have a launchpad available so we only tested compilation!) the generated binding looks good enough for use on casual inspection. The linker issue in the end is somewhat unfortunate, but I guess that, given some more effort this could be sorted out, so that the workaround is not needed. What somewhat bugs me is the integration with the firmware build. Since TI insists on generating make files ad-hoc during the build, which are then executed directly, getting complete integration will need quite a bit of effort (I’ve worked with projects that successfully did that, so I know it’s possible). Therefore I’ll rate my developer experience here at 3/5.

1 thought on “Rust & Vendor SDKs (I)”

Leave a Reply

Your email address will not be published. Required fields are marked *