Rust on RISC-V BL602: Simulated with WebAssembly

đź“ť 16 Aug 2021

One year ago I pondered… Can we make Embedded Programming easier for Learners?

Fornite vs Embedded Programming

(Source)

Maybe we need an easier way to build, test and debug our firmware…

Without using actual embedded hardware?

Today we shall explore whether it’s feasible to run Rust Firmware for BL602 RISC-V SoC in a Web Browser…

By simulating the BL602 SoC with WebAssembly!

Read on to find how we created this bare-bones BL602 Simulator in WebAssembly…

(More about BL602 RISC-V SoC)

BL602 Simulator in WebAssembly

1 Rust Firmware for BL602

We start with this BL602 Rust Firmware sdk_app_rust_gpio that blinks the LED: sdk_app_rust_gpio/lib.rs

/// This function will be called by the BL602 command-line interface
#[no_mangle]              //  Don't mangle the function name
extern "C" fn rust_main(  //  Declare `extern "C"` because it will be called by BL602 firmware
  _result: *mut u8,        //  Result to be returned to command-line interface (char *)
  _len:  i32,              //  Size of result buffer (int)
  _argc: i32,              //  Number of command line args (int)
  _argv: *const *const u8  //  Array of command line args (char **)
) {
  //  Show a message on the serial console
  puts("Hello from Rust!");

  //  PineCone Blue LED is connected on BL602 GPIO 11
  const LED_GPIO: u8 = 11;  //  `u8` is 8-bit unsigned integer

  //  Configure the LED GPIO for output (instead of input)
  gpio::enable_output(LED_GPIO, 0, 0)      //  No pullup, no pulldown
    .expect("GPIO enable output failed");  //  Halt on error

  //  Blink the LED 5 times
  for i in 0..10 {  //  Iterates 10 times from 0 to 9 (`..` excludes 10)

    //  Toggle the LED GPIO between 0 (on) and 1 (off)
    gpio::output_set(  //  Set the GPIO output (from BL602 GPIO HAL)
      LED_GPIO,        //  GPIO pin number
      i % 2            //  0 for low, 1 for high
    ).expect("GPIO output failed");  //  Halt on error

    //  Sleep 1 second
    time_delay(                 //  Sleep by number of ticks (from NimBLE Porting Layer)
      time_ms_to_ticks32(1000)  //  Convert 1,000 milliseconds to ticks (from NimBLE Porting Layer)
    );
  }
  //  Return to the BL602 command-line interface
}

What are gpio::enable_output and gpio::output_set?

They are BL602 GPIO Functions defined in the Rust Wrapper for BL602 IoT SDK, as explained here…

time_delay and time_ms_to_ticks32 are also defined in the BL602 Rust Wrapper.

How do we build, flash and run this BL602 Rust Firmware?

To see the blinking BL602 LED, we…

  1. Build this Rust Firmware

    (“cargo build” with a Custom Rust Target)

  2. Link it with the BL602 IoT SDK

  3. Flash the firmware to BL602

    (With blflash)

  4. Connect to BL602 via the USB Serial Port and enter the command…

    rust_main

    (Similar to this)

Can we run this BL602 Rust Firmware in a Web Browser? Without any BL602 hardware?

Let’s find out!

First we compile this BL602 Rust Firmware to WebAssembly…

2 Build BL602 Firmware for WebAssembly

We’ve created a Makefile that builds the above BL602 Rust Firmware into WebAssembly.

Here’s how we use it…

# Configure emscripten. See https://emscripten.org/docs/getting_started/downloads.html
# For Windows: emsdk\emsdk_env.bat
. ~/emsdk/emsdk_env.sh

# Download source code
git clone --recursive https://github.com/lupyuen/bl602-simulator
cd bl602-simulator

# Compile the Rust Firmware, Rust Simulator Library and link with Emscripten
make

# Produces outputs in the `docs` folder: wasm.js, wasm.wasm

This produces the JavaScript and WebAssembly files wasm.js and wasm.wasm, which we’ll run in a Web Browser later.

What’s inside the Makefile?

Our Makefile does the following…

  1. Compile the Rust Firmware into WebAssembly

    (“cargo build” for target “wasm32-unknown-emscripten”)

  2. Link the Rust Firmware with the Emscripten WebAssembly Runtime

    (So that it runs in a Web Browser)

Let’s go into the details…

Compile Rust Firmware into WebAssembly

2.1 Compile Rust Firmware into WebAssembly

To compile our Rust Firmware into WebAssembly, our Makefile calls this command…

# Compile the Rust Firmware and Rust Simulator Library into WebAssembly
cargo build --target wasm32-unknown-emscripten

This compiles three Rust Projects…

  1. Rust Firmware:

    bl602-simulator/sdk_app_rust_gpio/rust

    (The Rust Firmware we’ve seen earlier. Should be portable across BL602 and WebAssembly)

  2. Rust Simulator Library:

    bl602-simulator/bl602-simulator

    (Simulates the BL602 IoT SDK. We’ll see this in a while)

  3. Rust Scripting Library:

    bl602-simulator/bl602-script

    (More about this later)

“cargo build” downloads the BL602 Rust Wrapper automagically from crates.io …

...
Compiling bl602-macros v0.0.2
Compiling bl602-sdk v0.0.6
Compiling app v0.0.1 (bl602-simulator/sdk_app_rust_gpio/rust)
Compiling bl602-simulator v0.0.1 (bl602-simulator/bl602-simulator)
Finished dev [unoptimized + debuginfo] target(s) in 1m 43s

See the complete log

(Great that BL602 Rust Wrapper builds OK for WebAssembly! Yep our WSL machine is slow)

However our Rust Firmware needs a slight tweak at the top to build correctly under WebAssembly: sdk_app_rust_gpio/lib.rs

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

// TODO: For WebAssembly:
#![feature(libc)]  //  Allow C Standard Library, which will be mapped by emscripten to JavaScript

We change no_std to feature(libc) for the build to succeed.

Probably because the Emscripten Runtime behaves more like the Standard C Runtime.

(Someday we might use build.rs to apply this mod automatically during compilation)

Changing no_std to feature(libc)

What are the outputs for “cargo build”?

“cargo build” produces two Static Libraries (Rust Firmware and Rust Simulator)…

target/wasm32-unknown-emscripten/debug/libapp.a
target/wasm32-unknown-emscripten/debug/libbl602_simulator.a

Which we shall link with Emscripten’s WebAssembly Runtime.

Why did “cargo build” emit Static Libraries? Instead of the default Rust Libraries?

Because we specified staticlib in Cargo.toml for the Rust Firmware and Rust Simulator…

# Build this module as a Static Library.
[lib]
name       = "app"          # Output will be named `libapp.a`
crate-type = ["staticlib"]  # And will be a Static Library

Rust Libraries won’t link with Emscripten’s WebAssembly Runtime.

That’s why we switched to Static Libraries.

We’re nearly ready to run our Rust Firmware in WebAssembly! We need a WebAssembly Runtime that will…

  1. Let our Rust Firmware interact with HTML and JavaScript

    (To render the Web Browser UI)

  2. And print messages, errors and exceptions to the Web Browser

We’ll use the Emscripten WebAssembly Runtime.

Our Makefile links the Rust Firmware with Emscripten like so…

# Link the Rust Firmware and Rust Simulator Library with Emscripten
emcc -o wasm/wasm.html \
  target/wasm32-unknown-emscripten/debug/libapp.a \
  target/wasm32-unknown-emscripten/debug/libbl602_simulator.a \
  wasm/wasm.o \
  -g \
  -s WASM=1 \
  -s DISABLE_EXCEPTION_CATCHING=0 \
  -s "EXPORTED_FUNCTIONS=[ '_rust_main', '_clear_simulation_events', '_get_simulation_events' ]" \
  -s "EXTRA_EXPORTED_RUNTIME_METHODS=[ 'cwrap', 'allocate', 'intArrayFromString', 'UTF8ToString' ]"

How to install Emscripten

What are the EXPORTED_FUNCTIONS?

These Rust Functions will be called from JavaScript…

What are the EXTRA_EXPORTED_RUNTIME_METHODS?

These Emscripten Runtime Functions will be exported to JavaScript to allow strings to be passed between JavaScript and our Rust Firmware…

2.3 Copy the WebAssembly outputs

What are the outputs emitted by Emscripten?

Emscripten produces these files after linking our Rust Firmware…

Our Makefile copies the JavaScript and WebAssembly outputs to the docs folder…

# Copy the WebAssembly outputs to the docs folder for GitHub Pages
cp wasm/wasm.js   docs
cp wasm/wasm.wasm docs

So that we may test the WebAssembly outputs with a Local Web Server.

What about the HTML file wasm.html?

We’re using a customised version of wasm.html in the docs folder.

It renders a Simulated BL602 Board, as we shall soon see.

Why did we use the Emscripten WebAssembly Runtime? Instead of the Rust WebAssembly Runtime?

Because we copied the code from an earlier (non-Rust) WebAssembly project…

3 JSON Stream of Simulation Events

Our story so far…

  1. We have compiled our Rust Firmware into WebAssembly

  2. Our firmware runs in a Web Browser and it’s capable of interacting with HTML and JavaScript

    (Thanks to Emscripten)

  3. But our firmware won’t blink any LEDs

    (Because the BL602 IoT SDK is missing from WebAssembly)

What if we simulate the LED with HTML and JavaScript?

Yep we could build a BL602 Simulator in HTML and JavaScript.

And we can make our Rust Firmware talk to the BL602 Simulator…

By emitting a JSON Stream of BL602 Simulation Events!

JSON Stream of BL602 Simulation Events

What’s a BL602 Simulation Event?

When our firmware needs to set the GPIO Output to High or Low (to flip an LED On/Off)…

//  Switch the LED On
gpio::output_set(  //  Set the GPIO output for...
  11,              //  GPIO pin number
  0                //  0 for On, 1 for Off
)...

It sends a Simulation Event to the BL602 Simulator (in JSON format)…

{ "gpio_output_set": { 
  "pin":  11, 
  "value": 0 
} }

Which will be handled by the BL602 Simulator to flip the Simulated LED on or off.

Is our firmware directly controlling the BL602 Simulator?

Not quite. Our firmware is indirectly controlling the BL602 Simulator by sending Simulation Events.

(More about this Inversion of Control)

What about time delays?

Our firmware shall generate Simulation Events for time delays.

To handle such events, our BL602 Simulator pauses for the specified duration.

(It’s like playing a MIDI Stream)

Hence this firmware code…

//  Sleep 1,000 milliseconds (or 1 second)
time_delay(1000);

Shall generate this Time Delay Simulation Event…

{ "time_delay": { "ticks": 1000 } }

What’s inside the JSON Stream of Simulation Events?

To simulate our firmware on the BL602 Simulator, we shall transmit an array of Simulation Events (in JSON format) from our firmware to the BL602 Simulator.

Thus our Rust Blinky Firmware shall generate this JSON Stream of Simulation Events…

[ { "gpio_output_set": { "pin": 11, "value": 0 } }, 
  { "time_delay":      { "ticks": 1000 } }, 

  { "gpio_output_set": { "pin": 11, "value": 1 } }, 
  { "time_delay":      { "ticks": 1000 } }, 
  ... 
]

That will simulate a blinking BL602 LED.

Let’s generate the Simulation Events now.

4 Generate Simulation Events

How shall we generate this JSON Simulation Event…

{ "gpio_output_set": { 
  "pin":  11, 
  "value": 0 
} }

When we call this Rust Function?

//  Switch the LED On
gpio::output_set(  //  Set the GPIO output for...
  11,              //  GPIO pin number
  0                //  0 for On, 1 for Off
)...

We start by defining the Enum Type for the Simulation Event: bl602-simulator/lib.rs

//  Import the serde crate for JSON Serialization
use serde::{Serialize, Deserialize};

/// Event to be simulated by the BL602 Simulator
#[derive(Serialize, Deserialize, Debug)]
enum SimulationEvent {
  /// GPIO Set Output:
  /// `{ "gpio_output_set": { "pin": 11, "value": 1 }`
  gpio_output_set {
    pin:   u8,
    value: u8,
  },
}

To represent a stream of events, we create a Vector of Simulation Events…

// Create a vector of simulation events (i.e. event array)
let mut simulation_events: Vec<SimulationEvent> = Vec::new();

Here’s how we create a Simulation Event for GPIO Set Output and add it to the stream…

// Create a GPIO Set Output event
let ev = SimulationEvent::gpio_output_set { 
  pin:  11,
  value: 0,
};

// Add the event to the vector
simulation_events.push(ev);

Thanks to the Serde Crate, we may serialize the Vector of Simulation Events like so…

// Convert vector of events to a JSON string
let serialized = serde_json::to_string(&simulation_events)
  .unwrap();

// Print the serialized JSON events
println!("{}", serialized);

The result is a JSON Array of Simulation Events…

[{"gpio_output_set":{"pin":11,"value":0}}]

Exactly what we need!

4.1 Time Delay Event

What about the Time Delay Event?

{ "time_delay": { "ticks": 1000 } }

We add Time Delay to our Enum Type like so: bl602-simulator/lib.rs

/// Event to be simulated by the BL602 Simulator
enum SimulationEvent {
    /// Omitted: GPIO Set Output
    ...
    /// Time Delay:
    /// `{ "time_delay": { "ticks": 1000 } }`
    time_delay {
        ticks: u32,
    },
}

And we create the Time Delay Event like so…

// Create a Time Delay event
let ev = SimulationEvent::time_delay { 
  ticks: 1000,
};

// Add the event to the vector
simulation_events.push(ev);

Serde Crate does the rest!

Generating Simulation Events in Rust

(Source)

4.2 Intercept Calls to BL602 IoT SDK

We’ve just figured out how to compose the JSON Stream of Simulation Events.

Now let’s do this inside the calls to BL602 IoT SDK…

//  Switch the LED On
gpio::output_set(  //  Set the GPIO output for...
  11,              //  GPIO pin number
  0                //  0 for On, 1 for Off
)...

Where is the Rust Wrapper Function gpio::output_set defined?

From the previous article we see that the Wrapper Function is generated by bindgen and safe_wrap: sdk-expanded.rs

/// BL602 Rust Wrapper Function that sets the GPIO output
pub fn output_set(pin: u8, value: u8) -> BlResult<()> {
  //  Import the C function from BL602 IoT SDK
  extern "C" {
    pub fn bl_gpio_output_set(pin: u8, value: u8) -> ::cty::c_int;
  }
  unsafe {
    //  Call the BL602 IoT SDK
    let res = bl_gpio_output_set(pin as u8, value as u8);
    //  Return the result
    match res { 0 => Ok(()), _ => Err(BlError::from(res)), }
  }
}

(More about this)

This code calls bl_gpio_output_set, which is defined in the BL602 IoT SDK.

But bl_gpio_output_set won’t work on WebAssembly right?

Correcto! Because BL602 IoT SDK doesn’t exist on WebAssembly!

To fix this we introduce the Rust Simulator Library, which pretends to be the BL602 IoT SDK for WebAssembly.

Here’s how it works: bl602-simulator/lib.rs

/// Set the output value of a GPIO Pin. See `bl_gpio_output_set` in "Read and Write GPIO" <https://lupyuen.github.io/articles/led#read-and-write-gpio>
#[no_mangle]  //  Don't mangle the function name
extern "C" fn bl_gpio_output_set(pin: u8, value: u8) -> c_int {
  //  Omitted: If the GPIO Pin has not been configured for Output, halt
  ...
  //  Create a GPIO Set Output event
  let ev = SimulationEvent::gpio_output_set { 
    pin,
    value,
  };
  //  Add the event to the JSON Stream of Simulation Events.
  //  Unsafe because `SIMULATION_EVENTS` is a Static Variable.
  unsafe {
    SIMULATION_EVENTS.push(ev);
  }
  //  Return OK
  0
}

See what we did there? To flip the LED on / off…

  1. Our Rust Firmware calls gpio::output_set

    gpio::output_set(11, 0)
  2. Which is a wrapper function that calls bl_gpio_output_set

    fn output_set(pin: u8, value: u8) -> BlResult<()> {
      //  Call the BL602 IoT SDK
      bl_gpio_output_set(pin as u8, value as u8);
  3. Which adds the Set GPIO Output event to the JSON Stream of Simulation Events

    fn bl_gpio_output_set(pin: u8, value: u8) -> c_int {
      //  Create a GPIO Set Output event
      let ev = SimulationEvent::gpio_output_set { pin, value };
      //  Add the event to the JSON Stream of Simulation Events
      SIMULATION_EVENTS.push(ev);

And that’s how we intercept calls to BL602 IoT SDK… To emit a JSON Stream of Simulation Events!

Generating Simulation Events in Rust

(Source)

4.3 What about C?

Could we have done this in C instead of Rust?

Yep but it’s gonna get messy when we compose JSON in C.

Here’s the original implementation in C before converting to Rust…

Generating Simulation Events in C

(Source)

5 HTML and JavaScript Interface

We’ve done the Top Half of this pic: Emitting a JSON Stream of BL602 Simulation Events…

JSON Stream of BL602 Simulation Events

Now we do the Bottom Half: Web Browser Interface in HTML and JavaScript!

First we save this sketchy pic of a PineCone BL602 Board as a PNG file: pinecone.png

Creating the BL602 simulator image

We load the PNG file in our web page: simulator.js

/// Wait for emscripten to be initialised
Module.onRuntimeInitialized = function() {
  // Load the simulator pic and render it
  const image = new Image();
  image.onload = renderSimulator;  //  Draw when image has loaded
  image.src = 'pinecone.png';      //  Image to be loaded
};

When the pic has been loaded, renderSimulator renders the pic: simulator.js

/// Render the simulator pic. Based on https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
function renderSimulator() {
  //  Get the HTML canvas and context
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  //  Resize the canvas
  canvas.width  = 400;
  canvas.height = 300;

  //  Draw the image to fill the canvas
  ctx.drawImage(this, 0, 0, canvas.width, canvas.height);
}

What’s the canvas?

Emscripten has helpfully generated a HTML Canvas in wasm.html …

<canvas id="canvas" class="emscripten" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>

renderSimulator renders our BL602 pic to the HTML Canvas like so…

BL602 Simulator in WebAssembly

What about the LED?

To simulate the LED switching on (or off), let’s draw a blue rectangle (or grey rectangle) onto the HTML Canvas: simulator.js

//  Get the HTML Canvas Context
const ctx = document.getElementById('canvas').getContext('2d');

//  For LED On: Set the fill colour to Blue
ctx.fillStyle = '#B0B0FF';  //  Blue

//  For LED Off: Set the fill colour to Grey
//  ctx.fillStyle = '#CCCCCC';  //  Grey

//  Draw the LED colour
ctx.fillRect(315, 116, 35, 74);

5.1 Run Rust Firmware

Watch what happens when we click the “Run” Button in our BL602 Simulator: simulator.js

/// Run the command in the input box
function runScript() {
  //  Omitted: Read the command from input box and convert to a function (like `rust_main`)
  ...
  //  Clear the JSON Stream of Simulation Events in WebAssembly
  Module._clear_simulation_events();

We start by clearing the JSON Stream of Simulation Events.

(More about this in the Appendix)

Next we call the rust_main function from our Rust Firmware…

  //  Execute the WebAssembly Function defined in Rust.
  //  TODO: Pass the command-line args
  Module._rust_main();  //  Omitted: Checking whether `rust_main` exists

(Yep that’s a Quantum Leap from JavaScript to WebAssembly to Rust and back!)

Remember: Our Rust Firmware doesn’t run in Real Time.

Our Rust Firmware completes in an instant and emits a stream of events. (Including Time Delays)

We fetch the stream of events emitted by our Rust Firmware…

  //  Get the JSON string of Simulation Events from WebAssembly. Looks like...
  //  [ { "gpio_output_set": { "pin": 11, "value": 1 } }, 
  //    { "time_delay": { "ticks": 1000 } }, ... ]
  const json_ptr = Module._get_simulation_events();

(More about this in the Appendix)

And convert it from WebAssembly to JSON…

  //  Convert the JSON string from WebAssembly to JavaScript
  const json = Module.UTF8ToString(json_ptr);

  //  Parse the JSON Stream of Simulation Events
  simulation_events = JSON.parse(json);

Inside simulation_events we have a JSON Stream of Simulation Events, ready for processing!

5.2 Handle Simulation Events

Our JavaScript code has received the JSON Stream of Simulation Events from the Rust Firmware…

[ 
  { "gpio_output_set": { "pin": 11, "value": 1 } }, 
  { "time_delay": { "ticks": 1000 } },
  ...
]

Let’s process the events: simulator.js

/// Simulate the BL602 Simulation Events recorded in simulate_events, which contains...
///  [ { "gpio_output_set": { "pin": 11, "value": 1 } }, 
///    { "time_delay": { "ticks": 1000 } }, ... ]
function simulateEvents() {
  //  Take the first event and update the queue
  if (simulation_events.length == 0) { return; }
  const event = simulation_events.shift();
  //  event looks like:
  //  { "gpio_output_set": { "pin": 11, "value": 1 } }

  //  Get the event type and parameters
  const event_type = Object.keys(event)[0];
  const args = event[event_type];

  //  Timeout in milliseconds to the next event
  let timeout = 1;

Here we take the first event from the stream.

Then we handle the event: Set GPIO Output or Time Delay…

  //  Handle each event type
  switch (event_type) {

    //  Set GPIO output
    //  { "gpio_output_set": { "pin": 11, "value": 1 } }
    case "gpio_output_set": timeout += gpio_output_set(args.pin, args.value); break;

    //  Delay
    //  { "time_delay": { "ticks": 1000 } }
    case "time_delay": timeout += time_delay(args.ticks); break;

    //  Unknown event type
    default: throw new Error("Unknown event type: " + event_type);
  }

We use a timer to iterate through the events in the stream…

  //  Simulate the next event
  if (simulation_events.length > 0) {
    window.setTimeout("simulateEvents()", timeout);
  }
}

What happens inside gpio_output_set, the event handler for Set GPIO Output?

gpio_output_set renders the Simulated BL602 LED: simulator.js

/// Simulate setting GPIO pin output to value 0 (Low) or 1 (High):
/// { "gpio_output_set": { "pin": 11, "value": 1 } }
function gpio_output_set(pin, value) {
  //  Get the HTML Canvas Context
  const ctx = document.getElementById('canvas').getContext('2d');

  //  Set the simulated LED colour depending on value
  switch (value) {
    //  Set GPIO to Low (LED on)
    case 0: ctx.fillStyle = '#B0B0FF'; break;  //  Blue

    //  Set GPIO to High (LED off)
    case 1: ctx.fillStyle = '#CCCCCC'; break;  //  Grey

    //  Unknown value
    default: throw new Error("Unknown gpio_output_set value: " + args.value);
  }

  //  Draw the LED colour
  ctx.fillRect(315, 116, 35, 74);

  //  Simulate next event in 0 milliseconds
  return 0;
}

(Yep we’ve seen this code earlier)

That’s how we blink the Simulated LED through the stream of simulation events!

What about time_delay, the event handler for Time Delays?

time_delay is explained here: “Simulate Delays”

(Hint: It simulates Time Delays by calling the JavaScript Timer that we’ve seen earlier)

6 Run BL602 Firmware in Simulator

Try the BL602 Rust Firmware Simulator for yourself!

Click the Run Button and watch the LED blink!

BL602 Simulator in WebAssembly

To run the BL602 Simulator on our computer (Linux, macOS and Windows)…

  1. Build the BL602 Rust Firmware and the BL602 Simulator

    (Instructions here)

  2. Start a Local Web Server, because WebAssembly won’t run from a filesystem

    (Web Server for Chrome works fine)

  3. Browse to docs/wasm.html

  4. Click Run

7 Easier Embedded Development?

Is this easier than building and testing firmware on Real BL602 Hardware?

BL602 Simulator could potentially shorten the Code - Build - Flash - Test Cycle for Embedded Development…

  1. Code the firmware in Rust

  2. Build the firmware for WebAssembly

    (With a single “make” command on Linux / macOS / Windows)

  3. Test and Debug the firmware in the Simulator

    (No BL602 hardware needed, just a Web Browser)

  4. Repeat until the firmware is hunky dory

  5. Flash the firmware to BL602

    (Remember: Flashing BL602 via UART is kinda cumbersome)

But not all firmware can be simulated right?

True, there are limits to what we can simulate.

(Might be tricky to simulate Analog Inputs… Do we draw a graph?)

Even so, the simulator could be really helpful for learners who are building basic firmware.

(Maybe attract more Embedded Learners too!)

What about the Embedded Pros?

Someday BL602 Simulator might also be helpful for Embedded Pros who are building complex firmware…

  1. Automated Testing of BL602 Firmware

    Remember that our firmware emits a JSON Stream of Simulation Events?

    This JSON Stream is perfect for checking whether our firmware is behaving as expected… Just “diff” the Expected and Actual JSON Streams!

  2. Tracing Calls to BL602 IoT SDK for debugging

    (Like an embedded “strace”)

  3. Validating Calls to BL602 IoT SDK

    (More about this in the next chapter)

Can we simulate C Firmware? (Instead of Rust Firmware)

We could probably simulate C Firmware if we…

  1. Tweak the BL602 C Firmware to build with Emscripten

    (By modding the C Header Files and Makefiles)

  2. And link the compiled C Firmware with our Rust Simulator Library

Remember that the BL602 Shim Functions in our Rust Simulator Library are declared “extern C”?

#[no_mangle]  //  Don't mangle the function name
extern "C" fn bl_gpio_output_set(pin: u8, value: u8) -> c_int { ...

Yep this means they can be called from C Firmware!

And the BL602 Shim Functions will emit simulation events… Our C Firmware will work exactly like Rust Firmware!

8 Validate Calls to BL602 IoT SDK

What if the Embedded HAL (like BL602 IoT SDK) could tell us how to fix our code?

(Wouldn’t that be great, especially for learners?)

Yep we can help Embedded Learners when we catch BL602 SDK Calling Errors and explain the errors in a friendly way.

Watch what happens when set the output for a GPIO Pin without configuring the GPIO for Output…

Simulator halts with a friendly message

Our simulator halts with a friendly message… And explains how we can fix it!

How does our simulator validate calls to BL602 IoT SDK?

BL602 Simulator remembers the configuration of every GPIO Pin: bl602-simulator/lib.rs

/// Configuration for a BL602 GPIO Pin
#[derive(Clone, Copy, Debug, PartialEq)]
enum GpioConfig {
  /// GPIO Pin is unconfigured
  Unconfigured,
  /// GPIO Pin is configured for Input
  Input,
  /// GPIO Pin is configured for Output
  Output,
}

/// Configurations for all BL602 GPIO Pins
static mut GPIO_CONFIGS: [GpioConfig; 32] = [GpioConfig::Unconfigured; 32];

We update the GPIO Configuration whenever the GPIO is configured for Input or Output: lib.rs

/// Configure a GPIO Pin for Output Mode. See `bl_gpio_enable_output` in "Enable GPIO" <https://lupyuen.github.io/articles/led#enable-gpio>
#[no_mangle]  //  Don't mangle the function name
extern "C" fn bl_gpio_enable_output(pin: u8, _pullup: u8, _pulldown: u8) -> c_int {
  //  Remember that the GPIO Pin has been configured for Output
  GPIO_CONFIGS[pin as usize] = GpioConfig::Output;

While setting the GPIO output value, we raise an error if the GPIO Configuration is incorrect: lib.rs

/// Set the output value of a GPIO Pin. See `bl_gpio_output_set` in "Read and Write GPIO" <https://lupyuen.github.io/articles/led#read-and-write-gpio>
#[no_mangle]  //  Don't mangle the function name
extern "C" fn bl_gpio_output_set(pin: u8, value: u8) -> c_int {
  //  If the GPIO Pin has not been configured for Output, halt
  assert!(
    GPIO_CONFIGS[pin as usize] == GpioConfig::Output,
    "GPIO {} is {:?}, unable to set the GPIO Output Value. Please configure the GPIO for Output with `gpio::enable_output(pin, pullup, pulldown)` or `bl_gpio_enable_output(pin, pullup, pulldown)`. See \"Enable GPIO\" <https://lupyuen.github.io/articles/led#enable-gpio>",
    pin, GPIO_CONFIGS[pin as usize]
  );

That’s how we make BL602 Simulator a little more helpful for Embedded Learners… By validating the calls to BL602 IoT SDK!

Validate Calls to BL602 IoT SDK

9 PineDio Stack BL604

Simulating a plain BL602 board (like PineCone BL602) is pointless, innit?

Yep simulating a PineCone BL602 Board ain’t particularly exciting because it only has…

  1. One RGB LED

  2. One Jumper (GPIO 8)

  3. And everything else needs to be wired to the GPIO Pins

    (Which makes it harder to simulate actually)

Compare this with the PineDio Stack BL604 which has…

  1. SPI Display (with LVGL Graphics Library)

  2. LoRa SX1262 Transceiver

  3. Motion Sensor

  4. Heart Rate Sensor

  5. Battery Charging Chip

All this in a compact 3.5 cm² form factor!

It makes a lot more sense to simulate the PineDio Stack, because it’s a super interesting gadget for Embedded Learners.

Stay tuned for an updated simulator with support for LVGL, LoRa and LoRaWAN!

PineDio Stack Schematic

10 Scripting for BL602 Simulator

To make BL602 Simulator even more useful for Embedded Learners, we’re adding the Rhai Scripting Engine to the simulator…

Rhai Script vs Rust Firmware

(Source)

Thus we’ll allow BL602 Simulator to be used in two ways…

  1. The Traditional Way:

    Code the program in Rust. Compile to WebAssembly. Test with Simulator.

    …OR…

  2. The Scripted REPL Way:

    Code the program in Rhai Script. (Which looks like Rust)

    Type the Rhai Script directly into the Web Browser. (No compiler needed)

    Test with Simulator.

Why would we need The Scripted REPL Way?

Because Scripted REPL platforms like uLisp and MicroPython are still popular with Embedded Learners.

For BL602, perhaps learners could start with (Rust-like) Rhai Script…

And upgrade to Rust (or C) when they’re ready.

Check out the new article…

Rhai Scripting for BL602 Simulator

(Source)

10.1 Drag and Drop Scripting

I sense another upcoming enhancement?

Yes! Since we’re adding a Scripting Engine to the simulator…

Why not make it super easy to create scripts: The Drag-and-Drop Way!

BL602 Simulator with Blockly and Rhai Script

(Source)

Can we do this through a Desktop App? (Instead of Web Browser)

Possibly, if we wrap the Web Browser Interface into a Desktop App with Tauri.

(More about Tauri)

10.2 Run Scripts on BL602

Rhai Scripts run OK on our simulator with WebAssembly. But will the scripts run on Real BL602 Hardware?

Sadly no. Rhai Scripting Engine is too heavy for BL602. (See this)

But we could auto-convert / transcode Rhai Script to uLisp, which runs fine on BL602.

(More about Rhai Transcoding in the Appendix)

Transcode Rhai Script to uLisp

(Source)

Can we transmit uLisp to BL602 from the Web Browser?

Yes, we may automagically transmit the transcoded uLisp from Web Browser to BL602 with the Web Serial API.

(More about Web Serial API)

Which means our learners will…

  1. Use a Web Browser to drag and drop the blocks to create a visual program

  2. Which will auto-generate the Rhai Script for the visual program

  3. And the Rhai Script will be auto-transmitted to BL602 for execution

    (After the Rhai Script has been transcoded to uLisp)

You sound pretty confident about Drag-and-Drop Scripting. Have we done this before?

Yep we’ve previously experimented with Blockly (Scratch), uLisp and Rust.

Now that we’re switching to Rhai Script, things might get simpler…

(In the last article above we did some complicated Type Inference in Rust. Thankfully that’s no longer necessary for Rhai Script)

Blockly for BL602 / BL604 and Rhai Script

(Source)

11 What’s Next

We have a lot of work coming up!

  1. Rhai Scripting Engine (See this)

  2. Drag-and-Drop Scripting (with Blockly)

  3. Transcoding Rhai Script to uLisp (See this)

  4. Integrating uLisp with BL602 IoT SDK (See this)

(Follow the updates in this Twitter Thread)

And soon we shall test all this on PineDio Stack BL604 with LoRa SX1262… As we explore whether it’s feasible to teach Rust (or Rhai) as a Safer Way to create firmware for BL602 and BL604.

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/rustsim.md

12 Notes

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

  2. Mbed OS has a simulator based on C++ and Emscripten. (See this)

13 Appendix: Rhai Scripts on BL602

We can run Rhai Scripts in our Web Browser thanks to WebAssembly…

How will we run Rhai Scripts on Real BL602 Hardware?

Sadly Rhai Scripting Engine is too heavy for BL602. (See this)

But we could auto-convert / transcode Rhai Script to uLisp, which runs fine on BL602.

We’ll do the transcoding in the Web Browser with WebAssembly, since it has a lot more RAM than BL602.

Transcode Rhai Script to uLisp

(Source)

Why uLisp?

Because uLisp is a tiny Lisp Interpreter (coded in C) that runs well on BL602 with little RAM.

(More about uLisp on BL602)

Transcoded uLisp will be in the S-Expression Format. (Which looks a little like WebAssembly)

Hence this Rust-like Rhai Script…

gpio::output_set(11, 0);

Shall be transcoded to this uLisp S-Expression…

( bl_gpio_output_set 11 0 )

But will uLisp let us call C functions defined in BL602 IoT SDK?

Yep uLisp lets us expose a C function from BL602 IoT SDK like so: ulisp.c

//  Expose the C function `bl_gpio_output_set` to uLisp:
//  `int bl_gpio_output_set(uint8_t pin, uint8_t value)`
object *fn_bl_gpio_output_set(object *args, object *env) {
  //  Fetch the `pin` parameter from uLisp
  assert(args != NULL);
  int pin = checkinteger(BL_GPIO_OUTPUT_SET, car(args));
  args = cdr(args);

  //  Fetch the `value` parameter from uLisp
  assert(args != NULL);
  int value = checkinteger(BL_GPIO_OUTPUT_SET, car(args));
  args = cdr(args);

  //  No more parameters
  assert(args == NULL);

  //  Call the C function `bl_gpio_output_set`
  int result = bl_gpio_output_set(pin, value);

  //  Return the result to uLisp
  return number(result);
}

Which will be called from uLisp like so…

( bl_gpio_output_set 11 0 )

(More about this)

How shall we transcode Rhai Script to uLisp?

The Rhai Scripting Engine compiles Rhai Script into an Abstract Syntax Tree. (See this)

We shall traverse the nodes in the tree and emit uLisp S-Expressions.

Thus this Rhai Script…

gpio::output_set(11, 0);

Shall emit this uLisp S-Expression…

( bl_gpio_output_set 11 0 )

The transcoding implementation will probably look similar to…

Rhai Abstract Syntax Tree

(Source)

Why are we doing this in Rust?

Because thanks to bindgen, we have complete info on the BL602 IoT SDK interfaces (functions, parameters, return types).

Which lets us manipulate the BL602 SDK interfaces and do cool things like…

  1. Generate the uLisp Shims for BL602 IoT SDK

  2. Generate the Rhai Shims for BL602 IoT SDK

  3. Transcode Rhai Calls (BL602 IoT SDK) into uLisp

  4. Generate the Rust Wrapper for BL602 IoT SDK

    (Via the safe_wrap Procedural Macro)

14 Appendix: Rust Simulation Events

How is the JSON Stream of Simulation Events accessed via the Rust Simulator Library?

Remember that we maintain a Vector of Simulation Events in Rust: bl602-simulator/lib.rs

/// Vector of Simulation Events (i.e. event array)
static mut SIMULATION_EVENTS: Vec<SimulationEvent> = Vec::new();

But we can’t expose this Rust Vector to WebAssembly and JavaScript.

Thus we define an Event Buffer that exposes the vector as a JSON String…

/// String Buffer that returns the JSON Stream of Simulation Events:
/// `[ { "gpio_output_set": { "pin": 11, "value": 1 } }, 
///   { "time_delay": { "ticks": 1000 } }, 
///   ... 
/// ]`
static mut EVENT_BUFFER: [u8; 1024] = [0; 1024];

When our JavaScript code calls get_simulation_events to fetch the Simulation Events, we convert the Rust Vector to JSON and copy it into the Event Buffer: lib.rs

/// Return the JSON Stream of Simulation Events
#[no_mangle]  //  Don't mangle the function name
extern "C" fn get_simulation_events() -> *const u8 {
    //  Convert vector of events to a JSON string
    let mut serialized = unsafe {
        serde_json::to_string(&SIMULATION_EVENTS)
    }.unwrap();

    //  Terminate the JSON string with null, since we will be returning to C
    serialized.push('\0');

    //  Check that JSON string fits into the Event Buffer
    assert!(serialized.len() <= unsafe { EVENT_BUFFER.len() });

    //  Copy the JSON string to the Event Buffer
    unsafe {                            //  Unsafe because we are copying raw memory
        std::ptr::copy(                 //  Copy the memory...
            serialized.as_ptr(),        //  From Source (JSON String)
            EVENT_BUFFER.as_mut_ptr(),  //  To Destination (mutable pointer to Event Buffer)
            serialized.len()            //  Number of Items (each item is 1 byte)
        );    
    }
      
    //  Return the Event Buffer
    unsafe {
        EVENT_BUFFER.as_ptr()
    }
}

Yep it’s possible that our serialized vector won’t fit into the Event Buffer.

To mitigate this, we ought to check the serialized vector size whenever we add an event…

/// Add an Simulation Event
fn add_event(ev: SimulationEvent) {
    //  Add the event to the vector
    SIMULATION_EVENTS.push(ev);

    //  Convert vector of events to a JSON string
    let mut serialized = unsafe {
        serde_json::to_string(&SIMULATION_EVENTS)
    }.unwrap();

    //  If the JSON string doesn't fit into the Event Buffer...
    if (serialized.len() + 1 > unsafe { EVENT_BUFFER.len() }) {
        //  Remove the event from the vector and stop the simulation

Here’s how we initialise the Vector of Simulation Events before use: lib.rs

/// Clear the JSON Stream of Simulation Events
#[no_mangle]  //  Don't mangle the function name
extern "C" fn clear_simulation_events() {
    //  Clear the vector of Simulation Events
    unsafe {
        SIMULATION_EVENTS.clear();
    }
    //  Show Rust Backtrace on error
    std::env::set_var("RUST_BACKTRACE", "full");
}