The Rust programming language has generated a lot of enthusiasm in many developer ecosystems, especially security-focused ones, inspiring a wave of interesting projects like Oreboot, Precursor’s xous and Tock OS. Rust is a rapidly evolving and extremely versatile language that offers many crucial safety features built into the language design choices and toolchain themselves - and many new low-level projects choose it for the promise of a modern, powerful yet secure codebase.
Choosing the right tool for the job is especially important in modern embedded systems design. Building constrained yet capable devices requires a special type of passion - which Antmicro is often associated with - that often also implies strong preferences regarding tooling, development environments as well as the affinity for writing - and rewriting - utilities that make up your development workflow.
One of our ‘little tools’ that turned into a full-blown open source framework, now in use not only by Antmicro but also companies such as Google, Intel, Arm and Microchip, is Renode, our open source simulation framework. Our Renode team strives to make it easier for developers to test and simulate even complex multinode systems regardless of the languages and tools used. Until recently, it has been possible to design Renode peripheral models in a variety of languages such as C#, Python and C. Today we would like to introduce an integration layer with Rust that makes it possible to write peripherals in your crab's favorite language.
Adding Rust peripherals in Renode
To illustrate Rust peripheral support in Renode (still in early beta phase), we have released a sample repository with a proof of concept Rust UART peripheral co-inhabiting the same simulation as our ‘regular’ models on a SiFive RISC‑V platform. While a small RISC‑V MCU was chosen to make the example reasonably contrived, this could of course be easily adapted to run with larger application cores such as RV64GC and other architectures.
Unlike other languages supported in Renode so far, the Rust capability was added by integrating Web Assembly into Renode’s core infrastructure. We have been able to focus on writing UART peripheral logic in Rust and keep it closely integrated with the existing Renode backend by implementing a common interface for the UART peripheral:
public abstract class WASMExports {
public abstract void Reset();
public abstract void WriteChar(int value);
public abstract int ReadDoubleWord(long offset);
public abstract void WriteDoubleWord(long offset, int value);
}
Corresponding functions on the Rust side are marked with the export_name
attribute and exported to the Renode host where they are then transparently coupled with the existing infrastructure:
#[export_name = "Reset"] // exported to wasm
pub unsafe extern fn reset() {
clear_buffer();
reset_registers();
update_interrupts();
}
Rust-based peripheral models can have access to all of Renode’s functionality by importing and calling methods implemented directly in C#. This way Rust peripherals can use existing Renode framework mechanisms like e.g. the logging infrastructure.
In our example, we imported two functions allowing to control the interrupt signal and propagate bytes/characters written to the UART data register and packed into a uart
module.
This way the model is not limited to handling bus access to registers, but is instead fully connected to the UART infrastructure and the rest of the simulation (i.e., can send out data and inform the CPU about the internal state change using IRQ signals).
#[link(wasm_import_module = "uart")]
extern { // imported from C#
#[link_name = "SetIRQ"]
fn set_irq_inner(value: i32);
#[link_name = "InvokeCharReceived"]
fn invoke_char_received_inner(character: i32);
}
Calling functions through the Foreign Function Interface (FFI) is unfortunately explicitly unsafe as the Rust compiler cannot assume liability for exposed interfaces. Therefore, the corresponding functions are declared with the unsafe
keyword.
The main UART mechanics including registers and char buffer reside on the Rust side. For simplicity these structures were implemented as mutable statics and accessed in different places.
Take a look at how the reading from RECEIVE_DATA
and writing to TRANSMIT_DATA
were implemented in Rust.
Reading from the RECEIVE_DATA
register:
RECEIVE_DATA_OFFSET => {
if QUEUE_COUNT == 0 { //EMPTY
RECEIVE_DATA |= 1 << 31;
} else {
RECEIVE_DATA &= ! (1 << 31);
}
let mut bytes: [u8; 4] = RECEIVE_DATA.to_le_bytes();
let output: &mut [u8] = &mut bytes;
{
let mut writer = BitWriter::endian(output, LittleEndian);
let (success, character) = try_get_character();
let byts: [u8; 1] = [character];
if success {
writer.write_bytes(&byts).unwrap();
}
}
RECEIVE_DATA = u32::from_le_bytes(bytes);
return RECEIVE_DATA;
}
Writing to the TRANSMIT_DATA
register:
TRANSMIT_DATA_OFFSET => {
TRANSMIT_DATA = value;
let bytes = value.to_le_bytes();
let mut cursor = Cursor::new(&bytes);
{
let mut reader = BitReader::endian(&mut cursor, LittleEndian);
let character = reader.read(8).unwrap();
if TRANSMIT_ENABLE {
transmit_character(character);
update_interrupts();
}
}
}
Rust peripheral modules can even use cargo crates as long as they compile successfully to a wasm binary. To compile the peripheral, run:
cargo build --target wasm32-unknown-unknown --release --lib
If compilation passes without errors, there is a big chance that everything will work out of the box.
To give it a try, follow the steps described in the example repository. This is work in progress and we’re looking forward to hearing from you if you consider it a useful development - we are definitely keen to improve and extend this based on real usage scenarios.
Work with us
Whether your company is an avid Rust adopter or not, we can help you create products and tools for edge and cloud AI deployments. If you want to learn more, check out our presentation and see our Open Source Portal.