Rust on Apache NuttX OS

📝 12 Jan 2022

PineDio Stack BL604 RISC-V Board

Apache NuttX is an embedded operating system that’s portable across many platforms (8-bit to 64-bit) and works like a tiny version of Linux (because it’s POSIX Compliant).

Can we create (safer) Embedded Apps with Rust on NuttX?

Can we take a Device Driver from Rust Embedded… And run it on NuttX?

Today we shall…

  1. Build and run Rust programs on NuttX

  2. Access GPIO and SPI ports with Rust Embedded HAL

  3. Run the Semtech SX1262 LoRa Driver from Rust Embedded

  4. And transmit a LoRa Message over the airwaves with Rust on NuttX!

We tested Rust on NuttX with PineDio Stack BL604 RISC-V Board (pic above) and its onboard Semtech SX1262 Transceiver.

But it should work on ESP32, Arm and other NuttX platforms. (With some tweaking)

Caution: Work in Progress! Some spots are rough and rocky, I’m hoping the NuttX and Rust Communities could help to fill in the potholes before release 🙏

Rust running on NuttX

§1 Rust Meets NuttX

This is the simplest Rust program that will run on NuttX and print “Hello World!”: lib.rs

#![no_std]  //  Use the Rust Core Library instead of the Rust Standard Library, which is not compatible with embedded systems

#[no_mangle]  //  Don't mangle the function name
extern "C" fn rust_main() {  //  Declare `extern "C"` because it will be called by NuttX

  extern "C" {  //  Import C Function
    /// Print a message to the serial console (from C stdio library)
    fn puts(s: *const u8) -> i32;
  }

  unsafe {  //  Mark as unsafe because we are calling C
    //  Print a message to the serial console
    puts(
      b"Hello World!\0"  //  Byte String terminated with null
        .as_ptr()        //  Convert to pointer
    );
  }
}

Let’s break it down from the top…

//  Use the Rust Core Library instead of the Rust Standard Library,
//  which is not compatible with embedded systems
#![no_std]

We select the Rust Core Library (for embedded platforms), which is a subset of the Rust Standard Library (for desktops and servers).

Next we declare the Rust Function that will be called by NuttX…

//  Don't mangle the function name
#[no_mangle]
//  Declare `extern "C"` because it will be called by NuttX
extern "C" fn rust_main() {

(Why is it named “rust_main”? We’ll find out in a while)

NuttX provides the “puts” function because it’s POSIX Compliant (like Linux), so we import it from C…

  //  Import C Function
  extern "C" {
    /// Print a message to the serial console (from C stdio library)
    fn puts(s: *const u8) -> i32;
  }

This declares that “puts”

We call “puts” like so…

  //  Mark as unsafe because we are calling C
  unsafe {
    //  Print a message to the serial console
    puts(
      b"Hello World!\0"  //  Byte String terminated with null
        .as_ptr()        //  Convert to pointer
    );
  }

Passing a string from Rust to C looks rather cumbersome…

Though it looks messy, the Rust code above runs perfectly fine from the NuttX Shell

nsh> rust_test

Hello World!

We’ll make it neater in the next chapter.

Is there anything we missed?

We need to define a Panic Handler that will be called when a Runtime Error or Assertion Failure occurs.

(Our Panic Handler is defined here)

§2 Putting Things Neatly

Do we really need the cumbersome syntax for “puts” when we print things?

We can do better! Let’s wrap this cumbersome code…

//  Mark as unsafe because we are calling C
unsafe {
  //  Print a message to the serial console
  puts(
    b"Hello World!\0"  //  Byte String terminated with null
      .as_ptr()        //  Convert to pointer
  );
}

…with a Rust Macro. And we’ll get this…

//  Print a message to the serial console
println!("Hello World!");

Much neater! We’ll see later that “println!” supports Formatted Output too.

(println! is defined here. Thanks Huang Qi! 👍)

(puts is wrapped here)

Why is our Rust Function named rust_main instead of main?

Our Rust code (rust_main) is compiled into a Static Library that will be linked into the NuttX Firmware.

Our NuttX Firmware contains a NuttX App (rust_test) that calls rust_main from C: rust_test_main.c

//  Rust Function defined in rust/src/lib.rs
void rust_main(void);

//  Our Main Function in C...
int main(int argc, FAR char *argv[]) {
  //  Calls the Rust Function
  rust_main();
  return 0;
}

Thus it’s indeed possible to call Rust from C… And C from Rust!

(More about the Rust build script in the Appendix)

Rust opening GPIO Ports on NuttX

§3 Flipping GPIO

Since we can call NuttX Functions from Rust, let’s flip a GPIO High and Low the POSIX way: lib.rs

//  Open GPIO Output
let cs = unsafe {
  open(b"/dev/gpio1\0".as_ptr(), O_RDWR) 
};
assert!(cs > 0);

We open the GPIO Output at “/dev/gpio1” with read-write access.

Then we call ioctl to set the GPIO Output to Low

//  Set GPIO Output to Low
let ret = unsafe { 
  ioctl(cs, GPIOC_WRITE, 0) 
};
assert!(ret >= 0);

We sleep for 1 second…

//  Sleep 1 second
unsafe { 
  sleep(1); 
}

We set the GPIO Output to High

//  Set GPIO Output to High
let ret = unsafe { 
  ioctl(cs, GPIOC_WRITE, 1) 
};
assert!(ret >= 0);

Finally we close the GPIO Output

//  Close the GPIO Output
unsafe {
  close(cs);
}

This code works OK for blinking an LED on a GPIO pin, but we’ll do something more ambitious… Transfer data over SPI!

Won’t this code get really messy when we do lots of GPIO and SPI?

Yep it might get terribly messy! (Like this)

In a while we’ll mop this up with Rust Embedded HAL.

§4 Import NuttX Functions

How did we import the NuttX Functions: open, ioctl, sleep, close, …?

We imported the NuttX Functions like so: lib.rs

extern "C" {  //  Import NuttX Functions. TODO: Import with bindgen
  pub fn open(path: *const u8, oflag: i32, ...) -> i32;
  pub fn read(fd: i32, buf: *mut u8, count: u32) -> i32;
  pub fn write(fd: i32, buf: *const u8, count: u32) -> i32;
  pub fn close(fd: i32) -> i32;
  pub fn ioctl(fd: i32, request: i32, ...) -> i32;  //  On NuttX: request is i32, not u64 like Linux
  pub fn sleep(secs: u32) -> u32;
  pub fn usleep(usec: u32) -> u32;
  pub fn exit(status: u32) -> !;  //  Does not return
}

We (very carefully) imported the NuttX Constants as well: lib.rs

//  Import NuttX Constants. TODO: Import with bindgen from https://github.com/lupyuen/nuttx/blob/rust/include/nuttx/ioexpander/gpio.h
pub const GPIOC_WRITE: i32 = _GPIOBASE | 1;  //  _GPIOC(1)
pub const GPIOC_READ:  i32 = _GPIOBASE | 2;  //  _GPIOC(2)
pub const _GPIOBASE:   i32 = 0x2300;         //  GPIO driver commands
pub const O_RDWR:      i32 = O_RDOK|O_WROK;  //  Open for both read & write access

(Someday we should auto-generate the Rust Bindings for NuttX with the bindgen tool)

Rust Embedded HAL

§5 Rust Embedded HAL

What is Rust Embedded HAL?

Rust Embedded HAL (Hardware Abstraction Layer) defines a standard interface that’s used by Rust Embedded Device Drivers to access the hardware: GPIO, SPI, I2C, …

(Check out the Rust Embedded Drivers)

What if we implement Rust Embedded HAL for NuttX: GPIO, SPI, I2C, …?

That would be super interesting… It means that we can pick any Rust Embedded Driver and run it on NuttX! (Theoretically)

In a while we’ll test the Semtech SX1262 LoRa Driver from Rust Embedded, and see if it works on NuttX!

How do we call Rust Embedded HAL from NuttX?

We have created a NuttX Embedded HAL that implements the Rust Embedded HAL on NuttX…

(More details in the Appendix)

To call it, we add embedded-hal and nuttx-embedded-hal as dependencies to our Cargo.toml

## External Rust libraries used by this module.  See crates.io.
[dependencies]

## Rust Embedded HAL: https://crates.io/crates/embedded-hal
embedded-hal = "0.2.7"  

## NuttX Embedded HAL: https://crates.io/crates/nuttx-embedded-hal
nuttx-embedded-hal = "1.0.10"  

## SX126x LoRa Radio Driver fixed for NuttX
sx126x = { git = "https://github.com/lupyuen/sx126x-rs-nuttx" }  

(Always use the latest version of nuttx-embedded-hal)

(We’ll see the sx126x driver in a while)

We import the Rust Embedded Traits (GPIO, SPI and Delay) that we’ll call from our app: lib.rs

//  Import Embedded Traits
use embedded_hal::{       //  Rust Embedded HAL
  digital::v2::OutputPin, //  GPIO Output
  blocking::{             //  Blocking I/O
    delay::DelayMs,       //  Delay Interface
    spi::Transfer,        //  SPI Transfer
  },
};

To open GPIO Output “/dev/gpio1” we do this: lib.rs

//  Open GPIO Output
let mut cs = nuttx_embedded_hal::OutputPin
  ::new("/dev/gpio1")
  .expect("open gpio failed");

(This halts with an error if “/dev/gpio1” doesn’t exist)

We declare it as mut (mutable) because we expect its Internal State to change as we flip the GPIO.

Next we fetch the Delay Interface that we’ll call to sleep…

//  Get a Delay Interface
let mut delay = nuttx_embedded_hal::Delay;

Then we set the GPIO Output to Low

//  Set GPIO Output to Low
cs.set_low()
  .expect("cs failed");

(“expect” works like an Assertion Check)

We sleep for 1 second…

//  Wait 1 second (1,000 milliseconds)
delay.delay_ms(1000_u32);

(“u32” says that this is an unsigned 32-bit integer)

Finally we set the GPIO Output to High

//  Set GPIO Output to High
cs.set_high()
  .expect("cs failed");

Rust Embedded HAL makes GPIO programming more fun! Let’s do SPI now.

Inside PineDio Stack BL604

§6 SPI Transfer

Let’s test SPI Data Transfer to the Semtech SX1262 LoRa Transceiver.

For PineDio Stack BL604 with its onboard SX1262 (pic above), we control SPI Chip Select ourselves via GPIO Output “/dev/gpio1”

We begin by opening the GPIO Output for SPI Chip Select: lib.rs

/// Test the NuttX Embedded HAL by reading SX1262 Register 8
fn test_hal() {

  //  Open GPIO Output for SX1262 Chip Select
  let mut cs = nuttx_embedded_hal::OutputPin
    ::new("/dev/gpio1")
    .expect("open gpio failed");

Next we open the SPI Bus

  //  Open SPI Bus for SX1262
  let mut spi = nuttx_embedded_hal::Spi
    ::new("/dev/spitest0")
    .expect("open spi failed");

“/dev/spitest0” is our SPI Test Driver that simplifies SPI programming. (See this)

Before talking to SX1262, we set Chip Select to Low

  //  Set SX1262 Chip Select to Low
  cs.set_low()
    .expect("cs failed");

We transmit 5 bytes of data to SX1262 over SPI…

  //  Define the SX1262 Command: Read Register 8
  let mut data: [ u8; 5 ] = [ 0x1d, 0x00, 0x08, 0x00, 0x00 ];

  //  Transfer the command to SX1262 over SPI
  spi.transfer(&mut data)
    .expect("spi failed");

The data transmitted over SPI is the SX1262 Command that will read SX1262 Register 8

  1D 00 08 00 00

We pass the data as a Mutable Reference&mut” because we expect the contents to be changed during the SPI Transfer.

The value of SX1262 Register 8 is returned as the last byte of the SPI Response…

  println!("test_hal: SX1262 Register 8 is 0x{:02x}", data[4]);

We set Chip Select to High

  //  Set SX1262 Chip Select to High
  cs.set_high()
    .expect("cs failed");

And we’re done! Running this Rust code on NuttX shows…

nsh> rust_test
...
test_hal: SX1262 Register 8 is 0x80

(See the Output Log)

That’s the correct value of SX1262 Register 8: 0x80!

(Later we’ll talk about building and flashing the NuttX Firmware)

Calling the Rust Driver for LoRa SX1262

§7 Rust Driver for LoRa SX1262

Can we pick ANY Device Driver from Rust Embedded

And run it on NuttX?

Now that we have a (barebones) Rust Embedded HAL for NuttX, let’s find out!

We’ll test this Rust Embedded Driver for Semtech SX1262…

That we tweaked slightly from tweedegolf/sx126x-rs

(Details in the Appendix. Thanks Tweede golf! 👍)

Let’s do the same test as last chapter: Read SX1262 Register 8

We begin by opening the GPIO Input, Output and Interrupt Pins for SX1262: sx1262.rs

/// Test the SX1262 Driver by reading a register.
/// Based on https://github.com/tweedegolf/sx126x-rs/blob/master/examples/stm32f103-ping-pong.rs
pub fn test_sx1262() {

  //  Open GPIO Input for SX1262 Busy Pin
  let lora_busy = nuttx_embedded_hal::InputPin
    ::new("/dev/gpio0")
    .expect("open gpio failed");

  //  Open GPIO Output for SX1262 Chip Select
  let lora_nss = nuttx_embedded_hal::OutputPin
    ::new("/dev/gpio1")
    .expect("open gpio failed");

  //  Open GPIO Interrupt for SX1262 DIO1 Pin
  let lora_dio1 = nuttx_embedded_hal::InterruptPin
    ::new("/dev/gpio2")
    .expect("open gpio failed");

(We won’t handle interrupts today)

The NRESET and Antenna Pins are unused for now…

  //  TODO: Open GPIO Output for SX1262 NRESET Pin
  let lora_nreset = nuttx_embedded_hal::UnusedPin
    ::new()
    .expect("open gpio failed");

  //  TODO: Open GPIO Output for SX1262 Antenna Pin
  let lora_ant = nuttx_embedded_hal::UnusedPin
    ::new()
    .expect("open gpio failed");

  //  Open SPI Bus for SX1262
  let mut spi1 = nuttx_embedded_hal::Spi
    ::new("/dev/spitest0")
    .expect("open spi failed");

And we open the SPI Bus like before.

We define the pins for our SX1262 Driver…

  //  Define the SX1262 Pins
  let lora_pins = (
    lora_nss,    // /dev/gpio1
    lora_nreset, // TODO
    lora_busy,   // /dev/gpio0
    lora_ant,    // TODO
    lora_dio1,   // /dev/gpio2
  );

  //  Init a busy-waiting delay
  let delay = &mut nuttx_hal::Delay;

We initialise the SX1262 Driver

  //  Build the SX1262 Configuration
  let conf = build_config();

  //  Construct the SX1262 Driver
  let mut lora = SX126x::new(lora_pins);

  //  Init the SX1262 Driver
  lora.init(&mut spi1, delay, conf)
    .expect("sx1262 init failed");

(build_config is defined here)

Lastly we read SX1262 Register 8 and print the result…

  //  Init Result Buffer as 1 byte of 0x00
  let mut result: [ u8; 1 ] = [ 0; 1 ];

  //  Read SX1262 Register 8 into Result Buffer
  lora.read_register(&mut spi1, delay, 8, &mut result)
    .expect("sx1262 read register failed");

  //  Show the register value
  println!("test_sx1262: SX1262 Register 8 is 0x{:02x}", result[0]);

When we run the Rust code we’ll see…

nsh> rust_test
...
test_sx1262: SX1262 Register 8 is 0x80

(See the Output Log)

Which is the same result from the previous chapter. Yep the Rust Driver works OK with our NuttX Embedded HAL!

Let’s test the Rust Driver to the limit… And send a LoRa Message over the airwaves!

Transmit LoRa Message

§8 Transmit LoRa Message

For our final test we shall transmit a LoRa Message with the Rust Driver for SX1262.

We configure the LoRa Frequency for our region like so: sx1262.rs

/// TODO: Change this to your LoRa Frequency
//  const RF_FREQUENCY: u32 = 868_000_000;  //  868 MHz (EU)
//  const RF_FREQUENCY: u32 = 915_000_000;  //  915 MHz (US)
const RF_FREQUENCY: u32 = 923_000_000;  //  923 MHz (Asia)

We prepare for LoRa Transmission by setting some SX1262 Registers: sx1262.rs

/// Transmit a LoRa Message.
/// Based on https://github.com/tweedegolf/sx126x-rs/blob/master/examples/stm32f103-ping-pong.rs
pub fn test_sx1262() {
  //  Omitted: Init the SX1262 Driver
  ...
  //  Write SX1262 Registers to prepare for transmitting LoRa message.
  //  Based on https://gist.github.com/lupyuen/5fdede131ad0e327478994872f190668
  //  and https://docs.google.com/spreadsheets/d/14Pczf2sP_Egnzi5_nikukauL2iTKA03Qgq715e50__0/edit?usp=sharing

  //  Write Register 0x889: 0x04 (TxModulation)
  lora.write_register(&mut spi1, delay, Register::TxModulaton, &[0x04])
    .expect("write register failed");

  //  Write Register 0x8D8: 0xFE (TxClampConfig)
  lora.write_register(&mut spi1, delay, Register::TxClampConfig, &[0xFE])
    .expect("write register failed");

  //  Write Register 0x8E7: 0x38 (Over Current Protection)
  lora.write_register(&mut spi1, delay, Register::OcpConfiguration, &[0x38])
    .expect("write register failed");

  //  Write Register 0x736: 0x0D (Inverted IQ)
  lora.write_register(&mut spi1, delay, Register::IqPolaritySetup, &[0x0D])
    .expect("write register failed");

(More about this)

Then we transmit a LoRa Message over the airwaves…

  //  Send a LoRa message
  lora.write_bytes(
    &mut spi1,  //  SPI Interface
    delay,      //  Delay Interface
    b"Hello from Rust on NuttX!",  //  Payload
    0.into(),   //  Disable Transmit Timeout
    8,          //  Preamble Length
    packet::lora::LoRaCrcType::CrcOn,  //  Enable CRC
  ).expect("send failed");

Containing the Message Payload

Hello from Rust on NuttX!

And we’re done! We’ll see the results in a while. But first we run through the steps to build and flash our Rusty NuttX Firmware.

§9 Download Source Code

To run Rust on NuttX, download the modified source code for NuttX OS and NuttX Apps

mkdir nuttx
cd nuttx
git clone --recursive --branch rusti2c https://github.com/lupyuen/nuttx nuttx
git clone --recursive --branch rusti2c https://github.com/lupyuen/nuttx-apps apps

Or if we prefer to add the Rust Library and App to our NuttX Project, follow these instructions…

  1. “Install Rust Library”

  2. “Install Rust Test App”

  3. “Install SPI Test Driver”

(For PineDio Stack BL604: The Rust Library and App are already preinstalled)

§10 Build The Firmware

Let’s build the NuttX Firmware that contains our Rust App

  1. Install the build prerequisites…

    “Install Prerequisites”

  2. Assume that we have downloaded the NuttX Source Code

    “Download Source Code”

  3. Edit the Pin Definitions

    ## For BL602 and BL604:
    nuttx/boards/risc-v/bl602/bl602evb/include/board.h
    
    ## For ESP32: Change "esp32-devkitc" to our ESP32 board 
    nuttx/boards/xtensa/esp32/esp32-devkitc/src/esp32_gpio.c
    

    Check that the Semtech SX1262 Pins are configured correctly in board.h or esp32_gpio.c

    (Which pins can be used? See this)

    “Connect SX1262 Transceiver”

  4. Configure the build…

    cd nuttx
    
    ## For BL602: Configure the build for BL602
    ./tools/configure.sh bl602evb:nsh
    
    ## For PineDio Stack BL604: Configure the build for BL604
    ./tools/configure.sh bl602evb:pinedio
    
    ## For ESP32: Configure the build for ESP32.
    ## TODO: Change "esp32-devkitc" to our ESP32 board.
    ./tools/configure.sh esp32-devkitc:nsh
    
    ## Edit the Build Config
    make menuconfig 
    
  5. Enable the GPIO Driver in menuconfig…

    “Enable GPIO Driver”

  6. Enable the SPI Peripheral, SPI Character Driver and SPI Test Driver

    “Enable SPI”

  7. Enable GPIO and SPI Logging for easier troubleshooting…

    “Enable Logging”

  8. Enable Stack Canaries for stack checking…

    Check the box for “Build Setup”“Debug Options”“Compiler Stack Canaries”

  9. Enable Stack Backtrace for easier troubleshooting…

    Check the box for “RTOS Features”“Stack Backtrace”

    (See this)

  10. Enable our Rust Library

    Check the box for “Library Routines”“Rust Library”

  11. Enable our Rust Test App

    Check the box for “Application Configuration”“Examples”“Rust Test App”

  12. Save the configuration and exit menuconfig

    (See the .config for BL602 and BL604)

  13. For ESP32: Edit the function esp32_bringup in this file…

    ## Change "esp32-devkitc" to our ESP32 board 
    nuttx/boards/xtensa/esp32/esp32-devkitc/src/esp32_bringup.c
    

    And call spi_test_driver_register to register our SPI Test Driver.

    (See this)

  14. Build, flash and run the NuttX Firmware on BL602 or ESP32…

    “Build, Flash and Run NuttX”

PineDio Stack BL604 with Antenna

§11 Run The Firmware

We’re ready to run the NuttX Firmware and test our Rust App!

  1. Before testing, remember to connect the LoRa Antenna, as shown in the pic above.

    (So we don’t fry the SX1262 Transceiver as we charge up the Power Amplifier)

  2. In the NuttX Shell, list the NuttX Devices

    ls /dev
    
  3. We should see…

    /dev:
      gpio0
      gpio1
      gpio2
      spi0
      spitest0
      ...
    

    Our SPI Test Driver appears as “/dev/spitest0”

    The SX1262 Pins for Busy, Chip Select and DIO1 should appear as “/dev/gpio0” (GPIO Input), “gpio1” (GPIO Output) and “gpio2” (GPIO Interrupt) respectively.

  4. In the NuttX Shell, run our Rust App

    rust_test
    
  5. We should see Rust on NuttX transmitting our LoRa Message

    Sending LoRa message...
    Frequency: 923000000
    ...
    Done!
    

    (See the Output Log)

Let’s check whether Rust on NuttX has successfully transmitted our LoRa Message.

PineDio Stack BL604 RISC-V Board with onboard Semtech SX1262 LoRa Transceiver (left)… Sniffed wirelessly with Airspy R2 Software Defined Radio (right)

PineDio Stack BL604 RISC-V Board with onboard Semtech SX1262 LoRa Transceiver (left)… Sniffed wirelessly with Airspy R2 Software Defined Radio (right)

§12 Verify LoRa Message

Did Rust on NuttX transmit our LoRa Message successfully?

Let’s verify the LoRa Transmission in two ways…

  1. With a Spectrum Analyser

  2. With a LoRa Receiver

§12.1 Spectrum Analyser

We use a Spectrum Analyser (like Airspy R2, pic above) to sniff the airwaves…

LoRa Chirp recorded by Cubic SDR connected to Airspy R2 SDR

This shows that our LoRa Message was transmitted…

  1. At the right Radio Frequency

    (923 MHz)

  2. With sufficient power

    (Because of the red bar)

LoRa Messages have a characteristic criss-cross shape known as LoRa Chirp. More about this…

RAKwireless WisBlock LPWAN Module mounted on WisBlock Base Board

§12.2 LoRa Receiver

Next we use RAKwireless WisBlock (pic above) as a LoRa Receiver. We run this Arduino code on WisBlock…

Check that the LoRa Parameters are correct…

In the NuttX Shell, enter this to transmit a LoRa Message…

rust_test

On WisBlock we should see the received LoRa Message

RAKwireless WisBlock receives LoRa Message from Rust on NuttX

Which is ASCII for…

Hello from Rust on NuttX!

Our SX1262 Rust Driver has successfully transmitted a LoRa Message to RAKwireless WisBlock!

PineDio Stack BL604 RISC-V Board (left) talking LoRaWAN to RAKwireless WisGate LoRaWAN Gateway (right)

PineDio Stack BL604 RISC-V Board (left) talking LoRaWAN to RAKwireless WisGate LoRaWAN Gateway (right)

§13 LoRaWAN Support

What about LoRaWAN on Rust?

We need LoRaWAN if we wish to route LoRa Packets securely to a Local Area Network (ChirpStack) or to the internet (The Things Network).

Sadly we haven’t found a Complete LoRaWAN Stack for Rust yet.

(Probably because LoRaWAN is super complex… We need to sync up the Regional Parameters with the LoRaWAN Spec whenever LoRaWAN Regions are added or modified)

But we have a working LoRaWAN Stack for NuttX (in C) that’s based on the official LoRaWAN Stack by Semtech…

So perhaps our Rust code could call out to the LoRaWAN Stack in C and interoperate.

§14 What’s Next

In the next article we’ll talk about Rust and I2C on NuttX…

If you’re keen to make Rust on NuttX better, please lemme know! 🙏

Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.github.io/src/rust2.md

§15 Notes

  1. This article is the expanded version of this Twitter Thread

  2. This article was inspired by Huang Qi’s Rust Wrapper for NuttX…

    no1wudi/nuttx.rs

    Which has many features that will be very useful for our implementation of Rust Embedded HAL.

  3. Since NuttX behaves like Linux, can we use the libc crate to import the POSIX Functions?

    Possibly, if we extend libc to cover NuttX.

    Note that the Function Signatures are slightly different: libc declares ioctl as…

    fn ioctl(fd: i32, request: u64, ...) -> i32

    (Source)

    Whereas NuttX declares ioctl as…

    fn ioctl(fd: i32, request: i32, ...) -> i32

    (Source)

    The type of the request parameter is different: u64 vs i32.

    So beware!

  4. What about the nix crate?

    nix doesn’t support no_std yet, so sorry nope.

    (See this)

  5. Instead of no_std, can we run the Standard Rust Library on NuttX?

    Sony worked on porting Standard Rust Library to NuttX, but it appears to be incomplete.

    (See this)

GPIO HAL

§16 Appendix: Rust Embedded HAL for NuttX

This section explains how we implemented the Rust Embedded HAL for NuttX

§16.1 GPIO HAL

Let’s look at the HAL for GPIO Output (OutputPin), since GPIO Input (InputPin) and GPIO Interrupt (InterruptPin) are implemented the same way.

Our OutputPin Struct contains a NuttX File Descriptor: nuttx-embedded-hal/src/hal.rs

/// NuttX GPIO Output Struct
pub struct OutputPin {
  /// NuttX File Descriptor
  fd: i32,
}

We set the File Descriptor when we create the OutputPin: nuttx-embedded-hal/src/hal.rs

/// NuttX Implementation of GPIO Output
impl OutputPin {
  /// Create a GPIO Output Pin from a Device Path (e.g. "/dev/gpio1")
  pub fn new(path: &str) -> Result<Self, i32> {
    //  Open the NuttX Device Path (e.g. "/dev/gpio1") for read-write
    let fd = open(path, O_RDWR);
    if fd < 0 { return Err(fd) }

    //  Return the pin
    Ok(Self { fd })
  }
}

(open is defined here)

To set the OutputPin High or Low, we call ioctl on the File Descriptor: nuttx-embedded-hal/src/hal.rs

/// NuttX Implementation of GPIO Output
impl v2::OutputPin for OutputPin {
  /// Error Type
  type Error = i32;

  /// Set the GPIO Output to High
  fn set_high(&mut self) -> Result<(), Self::Error> {
    let ret = unsafe { 
      ioctl(self.fd, GPIOC_WRITE, 1) 
    };
    assert!(ret >= 0);
    Ok(())
  }

  /// Set the GPIO Output to low
  fn set_low(&mut self) -> Result<(), Self::Error> {
    let ret = unsafe { 
      ioctl(self.fd, GPIOC_WRITE, 0) 
    };
    assert!(ret >= 0);
    Ok(())
  }
}

When we’re done with OutputPin, we close the File Descriptor: nuttx-embedded-hal/src/hal.rs

/// NuttX Implementation of GPIO Output
impl Drop for OutputPin {
  /// Close the GPIO Output
  fn drop(&mut self) {
    unsafe { close(self.fd) };
  }
}

Check out the GPIO demo and docs…

SPI HAL

§16.2 SPI HAL

Now we study the SPI HAL for NuttX.

Our Spi Struct also contains a File Descriptor: nuttx-embedded-hal/src/hal.rs

/// NuttX SPI Struct
pub struct Spi {
  /// NuttX File Descriptor
  fd: i32,
}

/// NuttX Implementation of SPI Bus
impl Spi {
  /// Create an SPI Bus from a Device Path (e.g. "/dev/spitest0")
  pub fn new(path: &str) -> Result<Self, i32> {
    //  Open the NuttX Device Path (e.g. "/dev/spitest0") for read-write
    let fd = open(path, O_RDWR);
    if fd < 0 { return Err(fd) }

    //  Return the SPI Bus
    Ok(Self { fd })
  }
}

/// NuttX Implementation of SPI Bus
impl Drop for Spi {
  /// Close the SPI Bus
  fn drop(&mut self) {
    unsafe { close(self.fd) };
  }
}

We open and close the File Descriptor the same way as OutputPin.

To do SPI Write, we write to the File Descriptor: nuttx-embedded-hal/src/hal.rs

/// NuttX Implementation of SPI Write
impl spi::Write<u8> for Spi{
  /// Error Type
  type Error = i32;

  /// Write SPI data
  fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> {
    //  Transmit data
    let bytes_written = unsafe { 
        write(self.fd, words.as_ptr(), words.len() as u32) 
    };
    assert_eq!(bytes_written, words.len() as i32);
    Ok(())
  }
}

SPI Transfer works the same way, except that we also copy the SPI Response and return it to the caller: nuttx-embedded-hal/src/hal.rs

/// NuttX Implementation of SPI Transfer
impl spi::Transfer<u8> for Spi {
  /// Error Type
  type Error = i32;

  /// Transfer SPI data
  fn transfer<'w>(&mut self, words: &'w mut [u8]) -> Result<&'w [u8], Self::Error> {
    //  Transmit data
    let bytes_written = unsafe { 
        write(self.fd, words.as_ptr(), words.len() as u32) 
    };
    assert_eq!(bytes_written, words.len() as i32);

    //  Read response
    let bytes_read = unsafe { 
        read(self.fd, words.as_mut_ptr(), words.len() as u32) 
    };
    assert_eq!(bytes_read, words.len() as i32);

    //  Return response
    Ok(words)
  }
}

Check out the SPI demo and docs…

§16.3 I2C HAL

The implementation of I2C HAL for NuttX is described here…

Check out the I2C demo and docs…

§16.4 Delay HAL

We have also implemented the Delay HAL for NuttX…

Fixing SX1262 Driver for NuttX

§17 Appendix: Fix SX1262 Driver for NuttX

In this article we used this Rust Embedded Driver for Semtech SX1262…

That we tweaked slightly from…

(Thanks Tweede golf! 👍)

Let’s look at the modifications that we made.

SPI Transfers in small chunks

§17.1 Merge SPI Requests

While testing sx126x-rs, we discovered that the SPI Requests were split into 1-byte or 2-byte chunks. (Pic above)

This fails on NuttX because the SPI Request needs to be in one contiguous block as Chip Select flips from High to Low and High.

To fix this, we buffer all SPI Requests in the Chip Select Guard: sx126x-rs-nuttx/src/sx/slave_select.rs

impl<'nss, 'spi, TNSS, TSPI, TSPIERR> Transfer<u8> for SlaveSelectGuard<'nss, 'spi, TNSS, TSPI>
where
  TNSS: OutputPin,
  TSPI: Write<u8, Error = TSPIERR> + Transfer<u8, Error = TSPIERR>,
{
  type Error = SpiError<TSPIERR>;
  fn transfer<'w>(&mut self, words: &'w mut [u8]) -> Result<&'w [u8], Self::Error> {
    unsafe {
      //  Prevent a second transfer
      debug_assert!(!TRANSFERRED);

      //  Copy the transmit data to the buffer
      BUF[BUFLEN..(BUFLEN + words.len())]
        .clone_from_slice(words);
      BUFLEN += words.len();

      //  Transfer the data over SPI
      let res = self.spi.transfer(&mut BUF[0..BUFLEN])
        .map_err(SpiError::Transfer);

      //  Copy the result from SPI
      words.clone_from_slice(&BUF[BUFLEN - words.len()..BUFLEN]);

      //  Empty the buffer
      BUFLEN = 0;

      //  Prevent a second write or transfer
      TRANSFERRED = true;
      res
    }
  }
}

/// Buffer for SPI Transfer. Max packet size (256) + 2 bytes for Write Buffer Command.
static mut BUF: [ u8; 258 ] = [ 0; 258 ];

/// Length of buffer for SPI Transfer
static mut BUFLEN: usize = 0;

/// True if we have just executed an SPI Transfer
static mut TRANSFERRED: bool = false;

Then we patched the driver code to ensure that all SPI Request chains consist of…

Such that we flush the buffer of SPI Requests only after the final SPI Write or final SPI Transfer.

So this chain of SPI Requests…

spi.transfer(&mut [0x1D])
  .and_then(|_| spi.transfer(&mut start_addr))
  .and_then(|_| spi.transfer(&mut [0x00]))
  .and_then(|_| spi.transfer(result))?;

After patching becomes…

spi.write(&[0x1D])  //  Changed from `transfer` to `write`
  .and_then(|_| spi.write(&start_addr))  //  Changed from `transfer` to `write`
  .and_then(|_| spi.write(&[0x00]))      //  Changed from `transfer` to `write`
  .and_then(|_| spi.transfer(result))?;  //  Final transfer is OK

(Source)

The driver works OK on NuttX after merging the SPI Requests…

SPI Transfers after merging

§17.2 Read Register

We inserted a null byte for the Read Register command, because Read Requests should have minimum 5 bytes (instead of 4): sx126x-rs-nuttx/src/sx/mod.rs

/// Read data from a register
pub fn read_register<'spi>(
  &'spi mut self,
  spi: &'spi mut TSPI,
  delay: &mut impl DelayUs<u32>,
  start_addr: u16,
  result: &mut [u8],
) -> Result<(), SxError<TSPIERR, TPINERR>> {
  debug_assert!(result.len() >= 1);
  let start_addr = start_addr.to_be_bytes();
  let mut spi = self.slave_select(spi, delay)?;

  spi.write(&[0x1D])
    .and_then(|_| spi.write(&start_addr))
    //  Inserted this null byte
    .and_then(|_| spi.write(&[0x00]))
    .and_then(|_| spi.transfer(result))?;
  Ok(())
}

§17.3 Set Registers

The following registers need to be set for the LoRa Transmission to work correctly: rust_test/rust/src/sx1262.rs

//  Write SX1262 Registers to prepare for transmitting LoRa message.
//  Based on https://gist.github.com/lupyuen/5fdede131ad0e327478994872f190668
//  and https://docs.google.com/spreadsheets/d/14Pczf2sP_Egnzi5_nikukauL2iTKA03Qgq715e50__0/edit?usp=sharing

//  Write Register 0x889: 0x04 (TxModulation)
lora.write_register(&mut spi1, delay, Register::TxModulaton, &[0x04])
  .expect("write register failed");

//  Write Register 0x8D8: 0xFE (TxClampConfig)
lora.write_register(&mut spi1, delay, Register::TxClampConfig, &[0xFE])
  .expect("write register failed");

//  Write Register 0x8E7: 0x38 (Over Current Protection)
lora.write_register(&mut spi1, delay, Register::OcpConfiguration, &[0x38])
  .expect("write register failed");

//  Write Register 0x736: 0x0D (Inverted IQ)
lora.write_register(&mut spi1, delay, Register::IqPolaritySetup, &[0x0D])
  .expect("write register failed");

We derived the registers from the log generated by the SX1262 driver in C…

And by comparing the SPI Output of the C and Rust Drivers…

The C Driver for SX1262 is described here…

§17.4 Adapt For RISC-V

The sx126x-rs crate depends on the cortex-m crate, which works only on Arm, not RISC-V (BL602).

We defined the following functions to fill in for the missing functions on RISC-V: rust_test/rust/src/sx1262.rs

/// Read Priority Mask Register. Missing function called by sx126x crate (Arm only, not RISC-V).
/// See https://github.com/rust-embedded/cortex-m/blob/master/src/register/primask.rs#L29
#[cfg(not(target_arch = "arm"))]  //  If architecture is not Arm...
#[no_mangle]
extern "C" fn __primask_r() -> u32 { 0 }

/// Disables all interrupts. Missing function called by sx126x crate (Arm only, not RISC-V).
/// See https://github.com/rust-embedded/cortex-m/blob/master/src/interrupt.rs#L29
#[cfg(not(target_arch = "arm"))]  //  If architecture is not Arm...
#[no_mangle]
extern "C" fn __cpsid() {}

/// Enables all interrupts. Missing function called by sx126x crate (Arm only, not RISC-V).
/// See https://github.com/rust-embedded/cortex-m/blob/master/src/interrupt.rs#L39
#[cfg(not(target_arch = "arm"))]  //  If architecture is not Arm...
#[no_mangle]
extern "C" fn __cpsie() {}

/// No operation. Missing function called by sx126x crate (Arm only, not RISC-V).
/// See https://github.com/rust-embedded/cortex-m/blob/master/src/asm.rs#L35
#[cfg(not(target_arch = "arm"))]  //  If architecture is not Arm...
#[no_mangle]
extern "C" fn __nop() {}

We haven’t tested the driver for receiving LoRa Messages, we might need more fixes for NuttX on RISC-V.

(But then again we might not need to receive LoRa Messages if we’re building a simple IoT Sensor)

Rust Build Script for NuttX

§18 Appendix: Rust Build Script for NuttX

Let’s study the Build Script for Rust on NuttX…

And how it compiles the following into the NuttX Firmware…

See also the Build Log for Rust on NuttX…

§18.1 Rust Target

Our Build Script begins by defining the Rust Target for the build: rust_test/run.sh

##  Rust target: Custom target for BL602 and BL604
##  https://docs.rust-embedded.org/embedonomicon/compiler-support.html#built-in-target
##  https://docs.rust-embedded.org/embedonomicon/custom-target.html
rust_build_target=$PWD/riscv32imacf-unknown-none-elf.json
rust_build_target_folder=riscv32imacf-unknown-none-elf

##  Rust target: Standard target
##  rust_build_target=riscv32imac-unknown-none-elf
##  rust_build_target_folder=riscv32imac-unknown-none-elf

For BL602 and BL604: We’re using the Custom Rust Target at…

apps/examples/rust_test/riscv32imacf-unknown-none-elf.json

This Custom Rust Target supports floating point on 32-bit RISC-V. (The standard 32-bit RISC-V target doesn’t support floating point)

(More about Custom Rust Targets)

For ESP32-C3 (RISC-V): Set “rust_build_target” and “rust_build_target_folder” to the Standard Rust Target riscv32imc-unknown-none-elf

Then run this command to install the Rust Target…

rustup target add riscv32imc-unknown-none-elf

(See this)

For ESP32 (Xtensa): Set “rust_build_target” and “rust_build_target_folder” to the ESP32 Rust Target xtensa-esp32-none-elf

We need to install the Rust compiler fork with Xtensa support. (See this)

§18.2 Rust Build Options

Next we define the Rust Build Options: rust_test/run.sh

##  Rust build options: Build the Rust Core Library for our custom target
rust_build_options="--target $rust_build_target -Z build-std=core"

For BL602 and BL604: Since we’re using a Custom Rust Target, we need to build the Rust Core Library for our target. That’s why we need “-Z build-std=core” for the Rust Build Options…

--target nuttx/apps/examples/rust_test/riscv32imacf-unknown-none-elf.json \
  -Z build-std=core

(More about building Rust Core Library)

For ESP32 and ESP32-C3: Since we’re using a Standard Rust Target, remove “-Z build-std=core” from “rust_build_options”.

The Rust Build Options will look like…

--target riscv32imc-unknown-none-elf

§18.3 Define Libraries

Next we define the libraries that will be modified during the build…

That’s how we inject our Rust Code into the NuttX Build: We overwrite the Compiled Stub Library by the Compiled Rust Library.

The Stub Library is defined like so: rust_test/run.sh

##  Location of the Stub Library.  We will replace this stub by the Rust Library
##  rust_app_dest will be set to ../../../nuttx/staging/librust.a
rust_app_dir=$NUTTX_PATH/staging
rust_app_dest=$rust_app_dir/librust.a

The Rust Library is defined below: rust_test/run.sh

##  Location of the compiled Rust Library
##  rust_app_build will be set to rust/target/riscv32imacf-unknown-none-elf/debug/libapp.a
rust_build_dir=$PWD/rust/target/$rust_build_target_folder/$rust_build_profile
rust_app_build=$rust_build_dir/libapp.a

§18.4 Build Stub Library

Our script builds NuttX twice.

For the first build, we compile NuttX with the Stub Library: rust_test/run.sh

##  Build the firmware with the Stub Library, ignoring references to the Rust Library
pushd $NUTTX_PATH
make || echo "----- Ignore undefined references to Rust Library"
popd

Which fails to link because rust_main is undefined. Our script ignores the error and continues.

§18.5 Build Rust Library

Now we build the Rust Library: rust_test/run.sh

##  Build the Rust Library
pushd rust
rustup default nightly
cargo clippy $rust_build_options
cargo build  $rust_build_options
popd

Which expands to…

cargo build \
  --target nuttx/apps/examples/rust_test/riscv32imacf-unknown-none-elf.json \
  -Z build-std=core

(For BL602 and BL604)

This generates a Static Library at…

apps/examples/rust_test/rust/target/riscv32imacf-unknown-none-elf/debug/libapp.a

The Rust Build looks like this…

Rust builds OK

§18.6 Replace Stub Libary by Rust Library

We take the Static Library (generated by the Rust Compiler) and overwrite the Stub Library: rust_test/run.sh

##  Replace the Stub Library by the compiled Rust Library
##  Stub Library: ../../../nuttx/staging/librust.a
##  Rust Library: rust/target/riscv32imacf-unknown-none-elf/debug/libapp.a
cp $rust_app_build $rust_app_dest

Which is located at…

nuttx/staging/librust.a

Finally we do the second NuttX build: rust_test/run.sh

##  Link the Rust Library to the firmware
pushd $NUTTX_PATH
make
popd

Which links the Rust Static Library (and rust_main) into the NuttX Firmware.

Our build for Rust on NuttX is complete! nuttx.bin contains our NuttX Firmware, with Rust embedded inside.

§19 Appendix: Build, Flash and Run NuttX

(For BL602 and ESP32)

Below are the steps to build, flash and run NuttX on BL602 and ESP32.

The instructions below will work on Linux (Ubuntu), WSL (Ubuntu) and macOS.

(Instructions for other platforms)

(See this for Arch Linux)

§19.1 Build NuttX

Follow these steps to build NuttX for BL602 or ESP32…

  1. Install the build prerequisites…

    “Install Prerequisites”

  2. Install Rust from rustup.rs

  3. Assume that we have downloaded the NuttX Source Code and configured the build…

    “Download Source Code”

    “Build the Firmware”

  4. Edit the file…

    apps/examples/rust_test/rust/src/sx1262.rs
    

    And set the LoRa Frequency

    “Transmit LoRa Message”

  5. To build NuttX with Rust, enter this…

    pushd apps/examples/rust_test
    ./run.sh
    popd
    
  6. We should see…

    LD: nuttx
    CP: nuttx.hex
    CP: nuttx.bin
    

    (See the complete log for BL602)

  7. Ignore the errors at the “Flash NuttX” and “Run NuttX” steps

  8. For WSL: Copy the NuttX Firmware to the c:\blflash directory in the Windows File System…

    ##  /mnt/c/blflash refers to c:\blflash in Windows
    mkdir /mnt/c/blflash
    cp nuttx.bin /mnt/c/blflash
    

    For WSL we need to run blflash under plain old Windows CMD (not WSL) because it needs to access the COM port.

  9. In case of problems, refer to the NuttX Docs

    “BL602 NuttX”

    “ESP32 NuttX”

    “Installing NuttX”

Building NuttX

§19.2 Flash NuttX

For ESP32: See instructions here (Also check out this article)

For BL602: Follow these steps to install blflash

  1. “Install rustup”

  2. “Download and build blflash”

We assume that our Firmware Binary File nuttx.bin has been copied to the blflash folder.

Set BL602 / BL604 to Flashing Mode and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to High (Like this)

  2. Disconnect the USB cable and reconnect

    Or use the Improvised Reset Button (Here’s how)

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the H Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Connect BL10 to the USB port

  2. Press and hold the D8 Button (GPIO 8)

  3. Press and release the EN Button (Reset)

  4. Release the D8 Button

For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to 3.3V

  3. Reconnect the board to the USB port

Enter these commands to flash nuttx.bin to BL602 / BL604 over UART…

## For Linux: Change "/dev/ttyUSB0" to the BL602 / BL604 Serial Port
blflash flash nuttx.bin \
  --port /dev/ttyUSB0 

## For macOS: Change "/dev/tty.usbserial-1410" to the BL602 / BL604 Serial Port
blflash flash nuttx.bin \
  --port /dev/tty.usbserial-1410 \
  --initial-baud-rate 230400 \
  --baud-rate 230400

## For Windows: Change "COM5" to the BL602 / BL604 Serial Port
blflash flash c:\blflash\nuttx.bin --port COM5

(See the Output Log)

For WSL: Do this under plain old Windows CMD (not WSL) because blflash needs to access the COM port.

(Flashing WiFi apps to BL602 / BL604? Remember to use bl_rfbin)

(More details on flashing firmware)

Flashing NuttX

§19.3 Run NuttX

For ESP32: Use Picocom to connect to ESP32 over UART…

picocom -b 115200 /dev/ttyUSB0

(More about this)

For BL602: Set BL602 / BL604 to Normal Mode (Non-Flashing) and restart the board…

For PineDio Stack BL604:

  1. Set the GPIO 8 Jumper to Low (Like this)

  2. Disconnect the USB cable and reconnect

    Or use the Improvised Reset Button (Here’s how)

For PineCone BL602:

  1. Set the PineCone Jumper (IO 8) to the L Position (Like this)

  2. Press the Reset Button

For BL10:

  1. Press and release the EN Button (Reset)

For Ai-Thinker Ai-WB2, Pinenut and MagicHome BL602:

  1. Disconnect the board from the USB Port

  2. Connect GPIO 8 to GND

  3. Reconnect the board to the USB port

After restarting, connect to BL602 / BL604’s UART Port at 2 Mbps like so…

For Linux:

screen /dev/ttyUSB0 2000000

For macOS: Use CoolTerm (See this)

For Windows: Use putty (See this)

Alternatively: Use the Web Serial Terminal (See this)

Press Enter to reveal the NuttX Shell

NuttShell (NSH) NuttX-10.2.0-RC0
nsh>

Congratulations NuttX is now running on BL602 / BL604!

(More details on connecting to BL602 / BL604)

Running NuttX

Loads of fun with Rust, NuttX and LoRa on PineDio Stack BL604

Loads of fun with Rust, NuttX and LoRa on PineDio Stack BL604