Simulate RISC-V BL602 with WebAssembly, uLisp and Blockly

📝 27 May 2021

Drag-and-drop uLisp programs for microcontrollers… And run them WITHOUT a microcontroller!

What if we…

  1. Compile the uLisp Interpreter (from the previous article) to WebAssembly

  2. Use the WebAssembly version of uLisp to simulate BL602 in a Web Browser

    (Including GPIO, I2C, SPI, Display Controller, Touch Controller, LVGL, LoRa… Similar to this)

  3. Integrate the BL602 Simulator with Blockly

    (Also from the previous article)

  4. So that Embedded Developers may preview their Blockly uLisp Apps in the Web Browser?

Today we shall build a simple simulator for the BL602 RISC-V + WiFi SoC that will run Blockly uLisp Apps in a Web Browser.

(No BL602 hardware needed!)

BL602 Simulator with Blockly and uLisp in WebAssembly

BL602 Simulator with Blockly and uLisp in WebAssembly

1 Emscripten and WebAssembly

What is Emscripten?

Emscripten compiles C programs into WebAssembly so that we can run them in a Web Browser.

(Think of WebAssembly as a kind of Machine Code that runs natively in any Web Browser)

Here’s how we compile our uLisp Interpreter ulisp.c (from the last article) with the Emscripten Compiler emcc

emcc -g -s WASM=1 \
    src/ulisp.c wasm/wasm.c \
    -o ulisp.html \
    -I include \
    -s "EXPORTED_FUNCTIONS=[ '_setup_ulisp', '_execute_ulisp', '_clear_simulation_events', '_get_simulation_events' ]" \
    -s "EXTRA_EXPORTED_RUNTIME_METHODS=[ 'cwrap', 'allocate', 'intArrayFromString', 'UTF8ToString' ]"

(See the Makefile wasm.mk. More about wasm.c in a while)

C programs that call the Standard C Libraries should build OK with Emscripten: printf, <stdio.h>, <stdlib.h>, <string.h>, …

The Emscripten Compiler generates 3 output files…

(Instructions for installing Emscripten)

Compiling uLisp to WebAssembly with Emscripten

What are the EXPORTED_FUNCTIONS?

-s "EXPORTED_FUNCTIONS=[ '_setup_ulisp', '_execute_ulisp', '_clear_simulation_events', '_get_simulation_events' ]"

These are the C functions from our uLisp Interpreter ulisp.c that will be exported to JavaScript.

Our uLisp Interpreter won’t do anything meaningful in a Web Browser unless these 2 functions are called…

  1. _setup_ulisp: Initialise the uLisp Interpreter

  2. _execute_ulisp: Execute a uLisp script

(We’ll see the other 2 functions later)

How do we call the EXPORTED_FUNCTIONS from JavaScript?

Here’s how we call the WebAssembly functions _setup_ulisp and _execute_ulisp from JavaScript: ulisp.html

/// Wait for emscripten to be initialised
Module.onRuntimeInitialized = function() {
  //  Init uLisp interpreter
  Module._setup_ulisp();

  //  Set the uLisp script 
  var scr = "( list 1 2 3 )";

  //  Allocate WebAssembly memory for the script
  var ptr = Module.allocate(intArrayFromString(scr), ALLOC_NORMAL);

  //  Execute the uLisp script in WebAssembly
  Module._execute_ulisp(ptr);

  //  Free the WebAssembly memory allocated for the script
  Module._free(ptr);
};

(More about allocate and free)

To run this in a Web Browser, we browse to ulisp.html in a Local Web Server. (Sorry, WebAssembly won’t run from a Local Filesystem)

Our uLisp Interpreter in WebAssembly shows the result…

(1 2 3)

Testing uLisp compiled with Emscripten

But ulisp.c contains references to the BL602 IoT SDK, so it won’t compile for WebAssembly?

For now, we replace the hardware-specific functions for BL602 by Stub Functions (which will be fixed in a while)…

#ifdef __EMSCRIPTEN__  //  If building for WebAssembly...
//  Use stubs for BL602 functions, will fix later.
int bl_gpio_enable_input(uint8_t pin, uint8_t pullup, uint8_t pulldown) 
    { return 0; }
int bl_gpio_enable_output(uint8_t pin, uint8_t pullup, uint8_t pulldown) 
    { return 0; }
int bl_gpio_output_set(uint8_t pin, uint8_t value) 
    { return 0; }
uint32_t time_ms_to_ticks32(uint32_t millisec) 
    { return millisec; }
void time_delay(uint32_t millisec)
    {}

#else                    //  If building for BL602...
#include <bl_gpio.h>     //  For BL602 GPIO Hardware Abstraction Layer
#include "nimble_npl.h"  //  For NimBLE Porting Layer (mulitasking functions)
#endif  //  __EMSCRIPTEN__

The symbol __EMSCRIPTEN__ is defined when we use the Emscripten compiler.

(Yep it’s possible to reuse the same ulisp.c for BL602 and WebAssembly!)

BL602 IoT SDK stubbed out

2 REPL in a Web Browser

uLisp in WebAssembly looks underwhelming. Where’s the REPL (Read-Evaluate-Print Loop)?

As we’ve seen, printf works perfectly fine in WebAssembly… The output appears automagically in the HTML Text Box provided by Emscripten.

Console Input is a little more tricky. Let’s…

  1. Add a HTML Text Box for input

  2. Execute the input text with uLisp

Here’s how we add the HTML Text Box: ulisp.html

<!-- HTML Text Box for input -->
<textarea id="input"></textarea>

<!-- HTML Button that runs the uLisp script -->
<input id="run" type="button" value="Run" onclick="runScript()"></input>

Also we add a Run” Button that will execute the uLisp Script entered into the Text Box.

Let’s refactor our JavaScript to separate the uLisp Initialisation and Execution.

Here’s how we initialise the uLisp Interpreter: ulisp.html

/// Wait for emscripten to be initialised
Module.onRuntimeInitialized = function() {
  //  Init uLisp interpreter
  Module._setup_ulisp();
};

In the runScript function (called by the “Run” Button), we grab the uLisp Script from the text box and run it…

/// Run the script in the input box
function runScript() {
  //  Get the uLisp script from the input text box
  var scr = document.getElementById("input").value;

  //  Allocate WebAssembly memory for the script
  var ptr = Module.allocate(intArrayFromString(scr), ALLOC_NORMAL);

  //  Execute the uLisp script
  Module._execute_ulisp(ptr);

  //  Free the WebAssembly memory allocated for the script
  Module._free(ptr);
}

And our uLisp REPL in WebAssembly is done!

uLisp REPL in WebAssembly

3 Render the BL602 Simulator

How shall we render the Simulated BL602 Board?

Remember how we built the uLisp REPL with HTML and JavaScript?

Let’s do the same for the BL602 Simulator

BL602 Simulator in HTML and JavaScript

First we save this sketchy image 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: ulisp.html

/// Wait for emscripten to be initialised
Module.onRuntimeInitialized = function() {
  //  Omitted: Init uLisp interpreter
  ...
  // 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
};

This code calls the renderSimulator function when our BL602 image has been loaded into memory.

Emscripten has helpfully generated a HTML Canvas in ulisp.html

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

In the renderSimulator function, let’s render our BL602 image onto the HTML Canvas: ulisp.html

/// 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);
}

Our rendered BL602 Simulator looks like this…

Rendering the BL602 simulator image

What about the LED?

To simulate the LED switching on, let’s draw a blue rectangle onto the HTML Canvas: ulisp.html

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

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

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

Our rendered BL602 LED looks good…

Rendering the LED

And to simulate the LED switching off, we draw a grey rectangle: ulisp.html

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

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

Now we wire up the Simulated BL602 LED to uLisp!

4 Simulate BL602 Hardware

Our story so far…

  1. Our uLisp Interpreter lives in WebAssembly (compiled from C with Emscripten)

  2. Our BL602 Simulator lives in JavaScript (rendered onto a HTML Canvas)

How shall we connect uLisp to the BL602 Simulator… And blink the Simulated LED?

Oh yes we have ways of making uLisp talk to BL602 Simulator… From WebAssembly to JavaScript!

Here’s one way: A JSON Stream of BL602 Simulation Events

BL602 Simulator Design

What’s a BL602 Simulation Event?

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

( digitalwrite 11 :high )

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

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

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

(Yes the blue LED we’ve seen earlier)

Is uLisp directly controlling the BL602 Simulator?

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

(There are good reasons for doing this Inversion of Control, as well shall learn in a while)

What about time delays like ( delay 1000 )?

uLisp generates 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 uLisp script…

( delay 1000 )

Will generate this Simulation Event…

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

What’s a JSON Stream of Simulation Events?

To simulate a uLisp program on the BL602 Simulator, we shall pass an array of Simulation Events (in JSON format) from uLisp to the BL602 Simulator.

This (partial) uLisp program that sets the GPIO Output and waits 1 second…

( list
  ( digitalwrite 11 :high )
  ( delay 1000 )
  ...
)

Will generate this JSON Stream of Simulation Events

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

That will simulate a blinking BL602 LED (eventually).

5 Add a Simulation Event

Let’s watch how uLisp adds an event to the JSON Stream of Simulation Events.

We define a string buffer for the JSON array of events: wasm.c

/// Buffer for JSON Stream of Simulation Events
static char events[1024] = "[]";

To append a GPIO Output Event to the buffer, uLisp calls the function bl_gpio_output_set from wasm.c

/// Add a GPIO event to set output (0 for low, 1 for high)
int bl_gpio_output_set(uint8_t pin, uint8_t value) {
    //  How many chars in the Simulation Events buffer to keep
    int keep = 
        strlen(events)  //  Keep the existing events
        - 1;            //  Skip the trailing "]"

    //  Append the GPIO Output Event to the buffer
    snprintf(
        events + keep,
        sizeof(events) - keep,
        ", { \"gpio_output_set\": { "
            "\"pin\": %d, "
            "\"value\": %d "
        "} } ]",
        pin,
        value
    );
    return 0; 
}

This code appends a JSON event to the string buffer, which will look like this…

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

We’ll fix the leading comma “,” in a while.

Add an event to the JSON Stream of Simulation Events

How is bl_gpio_output_set called by uLisp?

When we enter this uLisp script to set the GPIO Output…

( digitalwrite 11 :high )

The uLisp Interpreter calls fn_digitalwrite defined in ulisp.c

/// Set the GPIO Output to High or Low
object *fn_digitalwrite (object *args, object *env) {
    //  Omitted: Parse the GPIO pin number and High / Low
    ...
    //  Set the GPIO output (from BL602 GPIO HAL)
    int rc = bl_gpio_output_set(
        pin,  //  GPIO pin number
        mode  //  0 for low, 1 for high
    );

Which calls our function bl_gpio_output_set to add the GPIO Output Event.

Will this work when running on real BL602 hardware?

Yep it does! bl_gpio_output_set is a real function defined in the BL602 IoT SDK for setting the GPIO Output.

Thus fn_digitalwrite (and the rest of uLisp) works fine on Real BL602 (hardware) and Simulated BL602 (WebAssembly).

6 Get the Simulation Events

uLisp (in WebAssembly) has generated the JSON Stream of BL602 Simulation Events. How will our BL602 Simulator (in JavaScript) fetch the Simulation Events?

To fetch the Simulation Events, we expose a getter function in WebAssembly like so: wasm.c

/// Return the JSON Stream of Simulation Events
const char *get_simulation_events(void) {
  assert(events[0] == '[');
  assert(events[strlen(events) - 1] == ']');

  //  Erase the leading comma: "[,...]" becomes "[ ...]"
  if (events[1] == ',') { events[1] = ' '; }
  return events;
}

get_simulation_events returns the WebAssembly string buffer that contains the Simulation Events (in JSON format).

Clearing and getting Simulation Events

Switching over from uLisp WebAssembly to our BL602 Simulator in JavaScript

Remember the runScript function we wrote for our uLisp REPL?

Let’s rewrite runScript to fetch the Simulation Events by calling get_simulation_events. From ulisp.html

/// JSON Stream of Simulation Events emitted by uLisp Interpreter. Looks like...
///  [ { "gpio_output_set": { "pin": 11, "value": 1 } }, 
///    { "time_delay": { "ticks": 1000 } }, ... ]
let simulation_events = [];

/// Run the script in the input box
function runScript() {
  //  Get the uLisp script 
  //  var scr = "( list 1 2 3 )";
  const scr = document.getElementById("input").value;

  //  Allocate WebAssembly memory for the script
  const scr_ptr = Module.allocate(intArrayFromString(scr), ALLOC_NORMAL);

  //  Catch any errors so that we can free the allocated memory
  try {
    //  Clear the JSON Stream of Simulation Events in WebAssembly
    Module._clear_simulation_events();

    //  Execute the uLisp script in WebAssembly
    Module.print("\nExecute uLisp: " + scr + "\n");
    Module._execute_ulisp(scr_ptr);

This is similar to the earlier version of runScript except…

  1. We now have a static variable simulation_events that will store the Simulation Events

  2. We use a try...catch...finally block to deallocate the WebAssembly memory.

    (In case we hit errors in the JSON parsing)

  3. We call _clear_simulation_events to erase the buffer of Simulation Events (in WebAssembly).

    (More about this later)

After calling _execute_ulisp to execute the uLisp Script, we fetch the generated Simulation Events by calling _get_simulation_events (which we’ve seen earlier)…

    //  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();

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

_get_simulation_events returns a pointer to a WebAssembly String.

Here we call UTF8ToString (from Emscripten) to convert the pointer to a JavaScript String.

We parse the returned string as a JSON array of Simulation Events…

    //  Parse the JSON Stream of Simulation Events
    simulation_events = JSON.parse(json);
    Module.print("Events: " + JSON.stringify(simulation_events, null, 2) + "\n");

And we store the parsed array of events into the static variable simulation_events

In case the JSON Parsing fails, we have a try...catch...finally block to ensure that the WebAssembly memory is properly deallocated…

  } catch(err) {
    //  Catch and show any errors
    console.error(err);
  } finally {
    //  Free the WebAssembly memory allocated for the script
    Module._free(scr_ptr);
  }

Now we’re ready to run the Simulated BL602 Events and blink the Simulated BL602 LED!

  //  Start a timer to simulate the returned events
  if (simulation_events.length > 0) {
    window.setTimeout("simulateEvents()", 1);
  }
}

We call a JavaScript Timer to trigger the function simulateEvents.

This simulates the events in simulation_events (like flipping the Simulated LED), one event at a time.

GPIO Simulation Events

What’s inside the WebAssembly function clear_simulation_events?

Before running a uLisp Script, our BL602 Simulator calls clear_simulation_events to erase the buffer of Simulation Events: wasm.c

/// Clear the JSON Stream of Simulation Events
void clear_simulation_events(void) {
  strcpy(events, "[]");
}

7 Flip the Simulated LED

simulateEvents is the Event Loop for our BL602 Simulator. It calls itself repeatedly to simulate each event generated by uLisp.

Here’s how it works: ulisp.html

/// 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 (gpio_output_set)
  //  and parameters ({ "pin": 11, "value": 1 })
  const event_type = Object.keys(event)[0];
  const args = event[event_type];

simulateEvents starts by fetching the next event to be simulated (from simulation_events).

It decodes the event into…

  1. Event Type: Like…

    gpio_output_set

  2. Event Parameters: Like…

    { "pin": 11, "value": 1 }

Next it handles each Event Type

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

  //  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;

If we’re simulating a GPIO Output Event, we call the function gpio_output_set and pass the Event Parameters (pin and value).

(We’ll talk about gpio_output_set and the timeout in a while)

    //  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);
  }

This code simulates time delays, which we’ll see later.

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

Finally we simulate the next event (from simulation_events), by triggering simulateEvents with a JavaScript Timer.

And that’s how we simulate every event generated by uLisp!

What’s inside the function gpio_output_set?

gpio_output_set is called by simulateEvents to simulate a GPIO Output Event: ulisp.html

/// 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');

First we fetch the HTML Canvas and its Context.

Then we set the Fill Colour to Blue or Grey, depending on GPIO Output Value…

  //  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);
  }

(Yes we’ve seen this code earlier)

Finally we draw the Simulated LED with the Fill Colour (Blue or Grey)…

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

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

Here’s what we see in the BL602 Simulator when we set the GPIO Output to Low (LED on)…

( digitalwrite 11 :low )

Flip the simulated LED

8 Simulate Delays

Now our BL602 Simulator flips the Simulated LED on and off. We’re ready to blink the Simulated LED right?

Not quite. We need to simulate Time Delays too!

Can’t we implement Time Delays by sleeping inside uLisp?

Not really. From what we’ve seen, uLisp doesn’t run our script in real time.

uLisp merely generates a bunch of Simulation Events. The events need to be simulated in the correct time sequence by our BL602 Simulator.

Hence we also need to simulate Time Delays with a Simulation Event.

How does uLisp generate a Simulation Event for Time Delay?

When we run this uLisp Script…

( delay 1000 )

Our uLisp Intepreter in WebAssembly generates a Time Delay Event like so: wasm.c

/// Add a delay event. 1 tick is 1 millisecond
void time_delay(uint32_t ticks) { 
  //  How many chars in the Simulation Events buffer to keep
  int keep = 
    strlen(events)  //  Keep the existing events
    - 1;            //  Skip the trailing "]"

  //  Append the Time Delay Event to the buffer
  snprintf(
    events + keep,
    sizeof(events) - keep,
    ", { \"time_delay\": { "
      "\"ticks\": %d "
    "} } ]",
    ticks
  );
}

This code adds a Time Delay Event that looks like…

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

(We define 1 tick as 1 millisecond, so this event sleeps for 1 second)

How does our BL602 Simulator handle a Time Delay Event in JavaScript?

Earlier we’ve seen simulateEvents, the Event Loop for our BL602 Simulator: ulisp.html

function simulateEvents() {
  //  Take the first event
  const event = simulation_events.shift();
  ...
  //  Get the event type and parameters
  const event_type = Object.keys(event)[0];
  const args = event[event_type];
  ...
  //  Handle each event type
  switch (event_type) {
    ...
    //  Delay
    //  { "time_delay": { "ticks": 1000 } }
    case "time_delay": 
      timeout += time_delay(args.ticks); 
      break;

simulateEvents handles the Time Delay Event by calling time_delay with the number of ticks (milliseconds) to delay: ulisp.html

/// Simulate a delay for the specified number of ticks (1 tick = 1 millisecond)
/// { "time_delay": { "ticks": 1000 } }
function time_delay(ticks) {
  //  Simulate the next event in "ticks" milliseconds
  return ticks;
}

time_delay doesn’t do much… It returns the number of ticks (milliseconds) to delay.

The magic actually happens in the calling function simulateEvents. From ulisp.html

function simulateEvents() {
  ...
  //  Get the delay in ticks / milliseconds
  timeout += time_delay(args.ticks);
  ...
  //  Simulate the next event
  if (simulation_events.length > 0) {
    //  Timer expires in timeout milliseconds
    window.setTimeout("simulateEvents()", timeout);
  }
}

simulateEvents takes the returned value (number of ticks to wait) and sets the timeout of the JavaScript Timer.

(When the timer expires, it calls simulateEvents to handle the next Simulation Event)

Let’s watch Time Delay Events in action! Guess what happens when we run this uLisp Script with our BL602 Simulator…

( list
  ( digitalwrite 11 :low )
  ( delay 1000 )
  ( digitalwrite 11 :high )
  ( delay 1000 )
)

Simulating delays

9 Simulate Loops

Let’s ponder this uLisp Script that blinks the LED in a loop

( loop
  ( digitalwrite 11 :low )
  ( delay 1000 )
  ( digitalwrite 11 :high )
  ( delay 1000 )
)

Wait a minute… Won’t this uLisp Script generate an Infinite Stream of Simulation Events? And overflow our 1024-byte event buffer?

Righto! We can’t allow uLisp Loops and Recursion to run forever in our simulator. We must stop them! (Eventually)

We stop runaway Loops and Recursion here: wasm.c

/// Preempt the uLisp task and allow background tasks to run.
/// Called by eval() and sp_loop() in src/ulisp.c
void yield_ulisp(void) {
  //  If uLisp is running a loop or recursion,
  //  the Simulation Events buffer may overflow.
  //  We stop before the buffer overflows.
  if (strlen(events) + 100 >= sizeof(events)) {  //  Assume 100 bytes of leeway

    //  Cancel the loop or recursion by jumping to loop_ulisp() in src/ulisp.c
    puts("Too many iterations, stopping the loop");
    extern jmp_buf exception;  //  Defined in src/ulisp.c
    longjmp(exception, 1);
  }
}

uLisp calls yield_ulisp when it iterates through a loop or evaluates a recursive expression.

If yield_ulisp detects that the buffer for Simulation Events is about to overflow, it stops the uLisp Loop / Recursion by jumping out (longjmp) and reporting an exception.

(Which will return a truncated stream of Simulation Events to the BL602 Simulator)

Looks kinda simplistic?

Yes this solution might not work for some kinds of uLisp Loops and Recursion. But it’s sufficient to simulate a blinking LED (for a short while).

How does uLisp call yield_ulisp?

uLisp calls yield_ulisp when iterating through a loop in ulisp.c

///  Execute uLisp Loop
object *sp_loop (object *args, object *env) {
  ...
  for (;;) {
    //  Preempt the uLisp task and allow background tasks to run
    yield_ulisp();

And when it evaluates a (potentially) recursive expression: ulisp.c

///  Main uLisp Evaluator
object *eval (object *form, object *env) {
  ...
  // Preempt the uLisp task and allow background tasks to run
  yield_ulisp();

So now we’re all set to run this uLisp loop?

( loop
  ( digitalwrite 11 :low )
  ( delay 1000 )
  ( digitalwrite 11 :high )
  ( delay 1000 )
)

Yes! Here’s our BL602 Simulator running the LED Blinky Loop. Watch how the Simulated LED stops blinking after a while…

Simulating loops

10 Add Simulator to Blockly

Today we’ve created two things that run in a Web Browser…

  1. uLisp REPL (based on WebAssembly)

  2. BL602 Simulator (based on JavaScript)

In the previous article we’ve created a Blockly Web Editor that lets us drag-and-drop uLisp Programs in a Web Browser (much like Scratch). (See this)

Can we drag-and-drop Blockly Programs in a Web Browser… And run them with uLisp REPL and BL602 Simulator?

Blockly Web Editor for uLisp WebAssembly and BL602 Simulator

Yes we can! Just do this…

  1. Click this link to run the Blockly Web Editor for uLisp WebAssembly and BL602 Simulator

    (This website contains HTML, JavaScript and WebAssembly, no server-side code. We’ll explain blockly-ulisp in a while)

  2. Drag-and-drop this Blockly Program…

    Blockly Web Editor: Blinky

    By snapping these blocks together…

    Make sure they fit snugly. (Not floaty)

    (Stuck? Check the video)

  3. Set the parameters for the blocks as shown above…

  4. Click the Lisp tab at the top.

    We should see this uLisp code generated by Blockly

    Blockly Web Editor: uLisp code for Blinky

  5. Click the Run Button [ ▶ ] at top right.

    The Simulated LED blinks every second!

    (And stops after a while, because we don’t simulate infinite loops)

    Watch the demo on YouTube

Yes indeed we can drag-and-drop Blockly Programs… And run them with the uLisp REPL and BL602 Simulator!

Read on to find out how we connected Blockly to uLisp REPL (in WebAssembly) and BL602 Simulator (in JavaScript).

BL602 Simulator with uLisp and Blockly in WebAssembly

11 Simulate Blockly Programs

Adding the BL602 Simulator to Blockly (from the previous article) was surprisingly painless.

Here’s what we did…

  1. We create a Blockly folder for our new web page (based on the Blockly folder from the previous article)…

    Copy the folder demos/code to demos/simulator

  2. We copy the WebAssembly files to the Blockly folder…

    Copy ulisp.js and ulisp.wasm from ulisp-bl602/docs to demos/simulator

  3. Insert the HTML Canvas and the Output Box (for the uLisp log): index.html

    <!-- Canvas for Simulator -->
    <tr>
      <td colspan=2 align="center">
        <canvas id="canvas" width="400" height="300"></canvas>
        <textarea id="output" style="width: 300px; height: 300px;"></textarea>
        <div class="spinner" id='spinner'></div>
        <div class="emscripten" id="status"></div>    
        <progress value="0" max="100" id="progress" hidden=1></progress>
      </td>
    </tr>
    <!-- End -->
    

    (spinner, status and progress are needed by the Emscripten JavaScript)

  4. Copy the Emscripten JavaScript from ulisp.html to index.html

    <!--  Emscripten Script: From ulisp.html  -->
    <script type='text/javascript'>
      var statusElement   = ...
      var progressElement = ...
      var spinnerElement  = ...
    
      var Module = {
        preRun:  [],
        postRun: [],
        print:     ... ,
        printErr:  ... ,
        canvas:    ... ,
        setStatus: ... ,
        monitorRunDependencies: ...
      };
      Module.setStatus( ... );
      window.onerror = ...
    </script>
    <!--  End of Emscripten Script  -->
    
  5. Copy the BL602 Simulator JavaScript from ulisp.html to index.html

    <!--  Custom Script: TODO Sync with ulisp.html  -->
    <script type="text/javascript">
    
      /// JSON Stream of Simulation Events emitted by uLisp Interpreter
      let simulation_events = [];
    
      /// Wait for emscripten to be initialised
      Module.onRuntimeInitialized = ...
    
      /// Render the simulator pic
      function renderSimulator() { ... }
    
      /// Run the provided script
      function runScript(scr) { /* See changes below */ }
    
      /// Simulate the BL602 Simulation Events recorded in simulate_events
      function simulateEvents() { ... }
    
      /// Simulate setting GPIO pin output to value 0 (Low) or 1 (High)
      function gpio_output_set(pin, value) { ... }
    
      /// Simulate a delay for the specified number of ticks
      function time_delay(ticks) { ... }
    
    </script>    
    <!--  End of Custom Script  -->
    
  6. Modify the runScript function above…

    /// Run the uLisp script from Input Box
    function runScript() {
      //  Get the uLisp script from Input Box
      const scr = document.getElementById("input").value;
    
      //  Allocate WebAssembly memory for the script
      const scr_ptr = Module.allocate(intArrayFromString(scr), ALLOC_NORMAL);
    

    So that it runs the script from the provided parameter (instead of the REPL Input Box): index.html

    /// Run the provided uLisp script
    function runScript(scr) {
      //  Allocate WebAssembly memory for the script
      const scr_ptr = Module.allocate(intArrayFromString(scr), ALLOC_NORMAL);
    
  7. Load our Emscripten WebAssembly into Blockly: index.html

    <!--  Load Emscripten WebAssembly: From ulisp.html  -->
    <script async type="text/javascript" src="ulisp.js"></script>
    <!--  End of Emscripten WebAssembly  -->
    
  8. Previously we ran the uLisp code on a real BL602 with the Web Serial API.

    Now we run the uLisp code on BL602 Simulator: code.js

    ///  Run the uLisp code on BL602 Simulator
    Code.runJS = function() {
      //  Generate the uLisp code by calling the Blockly Code Generator for uLisp
      var code = Blockly.Lisp.workspaceToCode(Code.workspace);
    
      //  Run the uLisp code on BL602 Simulator
      runScript(code);   //  Defined in index.html
    }    
    

And that’s how we added uLisp WebAssembly and BL602 Simulator to Blockly

Dragging-and-dropping uLisp programs for microcontrollers… And running them WITHOUT a microcontroller!

12 Can We Simulate Any BL602 Firmware?

Our BL602 Simulator works OK for simulating a uLisp Program. Will it work for simulating BL602 Firmware coded in C?

Our BL602 Simulator might work for BL602 Firmware coded in C!

Let’s look at this BL602 Blinky Firmware in C: sdk_app_blinky/demo.c

/// Blink the BL602 LED
void blinky(char *buf, int len, int argc, char **argv) {
  //  Configure the LED GPIO for output (instead of input)
  int rc = bl_gpio_enable_output(
    LED_GPIO,  //  GPIO pin number (11)
    0,         //  No GPIO pullup
    0          //  No GPIO pulldown
  );
  assert(rc == 0);  //  Halt on error

  //  Blink the LED 5 times
  for (int i = 0; i < 10; i++) {

    //  Toggle the LED GPIO between 0 (on) and 1 (off)
    rc = bl_gpio_output_set(  //  Set the GPIO output (from BL602 GPIO HAL)
      LED_GPIO,               //  GPIO pin number (11)
      i % 2                   //  0 for low, 1 for high
    );
    assert(rc == 0);  //  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)
    );
  }
}

To get this C code running on our BL602 Simulator, we need to compile the code to WebAssembly.

Will this C code compile to WebAssembly?

Remember earlier we created these C Functions for our WebAssembly Interface

/// Add a GPIO event to set output (0 for low, 1 for high)
int bl_gpio_output_set(uint8_t pin, uint8_t value) { ... }

/// Configure the GPIO pin for output
int bl_gpio_enable_output(uint8_t pin, uint8_t pullup, uint8_t pulldown) { return 0; }

/// Add a delay event. 1 tick is 1 millisecond
void time_delay(uint32_t ticks) { ... }

/// Convert milliseconds to ticks
uint32_t time_ms_to_ticks32(uint32_t millisec) { return millisec; }

These C Function Signatures are 100% identical to the BL602 Functions from the BL602 IoT SDK: bl_gpio_output_set, bl_gpio_enable_output, …

So yep, the above BL602 Firmware Code will compile to WebAssembly!

Similar to a uLisp Program, the BL602 Firmware Code (running in WebAssebly) will generate a JSON Stream of Simulation Events.

(Which we’ll feed to our BL602 Simulator in JavaScript)

But can we simulate ALL functions from the BL602 IoT SDK: GPIO, I2C, SPI, ADC, DAC, LVGL, LoRa, …?

This needs work of course.

To simulate any uLisp Program or BL602 Firmware we need to code the necessary Simulation Functions in JavaScript (like gpio_output_set and time_delay) for GPIO, I2C, SPI, ADC, DAC, LVGL, LoRa, …

(Sounds like an interesting challenge!)

13 Why Simulate A Stream Of Events?

Why did we choose to simulate a JSON Stream of Simulation Events? Is there a simpler way?

Let’s look at this uLisp Program…

( loop
  ( digitalwrite 11 :low )
  ( delay 1000 )
  ( digitalwrite 11 :high )
)

The obvious way to simulate this uLisp Program (with WebAssembly) would be to let uLisp control the simulator directly

  1. uLisp evaluates the first expression

    ( digitalwrite 11 :low )
    
  2. uLisp renders the Simulated LED flipped on.

    (Probably with SDL, which is supported by Emscripten)

  3. uLisp evaluates the next expression

    ( delay 1000 )
    
  4. uLisp pauses for 1 second

  5. uLisp evaluates the next expression

    ( digitalwrite 11 :high )
    
  6. uLisp renders the Simulated LED flipped off

  7. uLisp repeats the above steps forever

This Synchronous Simulation of uLisp Programs is Tightly Coupled.

It’s heavily dependent on the implementation of the uLisp Interpreter. And the way we render the simulator (like SDL).

Which makes this design harder to reuse for other kinds of BL602 Firmware (like our BL602 Blinky Firmware in C) and other ways of rendering the simulator (like JavaScript).

To fix this, we apply Inversion of Control and flip the design, so that uLisp no longer controls the simulator directly

BL602 Simulator Design

And we get the chosen design of our BL602 Simulator.

Some Higher Entity (the Simulator JavaScript) takes the Simulation Events emitted by uLisp, and feeds them to the BL602 Simulator.

With this simulator design we get…

  1. Loose Coupling: We can reuse this design for other kinds of BL602 Firmware, like our BL602 Blinky Firmware in C. (As explained in the previous chapter)

  2. Unit Testing: We might someday feed the JSON Stream of Simulation Events to a Unit Testing Engine, for running Automated Unit Tests on our uLisp Programs and BL602 Firmware. (Without actual BL602 hardware!)

  3. Time Compression: Time Delay Events are encoded inside the stream of Simulation Events. Which means that the simulation runs in Deferred Time, not in Real Time.

    This gets interesting because we no longer need to wait say, 1 hour of real time to simulate a BL602 program… We could speed up the simulator 10 times and see the outcome in 6 minutes!

  4. Time Reversal: With a stream of Simulation Events, we could reverse time too! Rewinding to a specific point in the stream could be really helpful for troubleshooting BL602 Firmware.

14 What’s Next

UPDATE: We’ve created a BL602 Simulator in Rust. Read about it here

Creating a BL602 Simulator for uLisp and Blockly has been a fun experience.

But more work needs to be done… Please lemme know if you’re keen to help!

Could this be the better way to learn Embedded Programming on modern microcontrollers?

Let’s build it and find out! 🙏 👍 😀

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

lupyuen.github.io/src/wasm.md

15 Notes

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