Introduction: BBC Micro:bit C++ Getting Started

Update: 21st December 2106 – Fixing paring with Pixel

If you are having problems paring with Pixel or other Android 7.1 mobiles, implement the 500mS delay shown in
this GitHub pull request #225

Introduction and Setup

There are a number of ways to write and compile programs for the BBC micro:bit. This page is about programming the BBC micro:bit via C++. It presents a number of simple C++ programs to illustrate some less well documented features of Lancaster University's microbit C++ support.

This instuctable covers:-

  • A First Program
  • Debugging using USB Serial
  • Managed Strings and ManagedPrintf
  • Fibers (another sort of Thread)
  • Blocking is Good in micro:bit C++
  • Communicating between fibers
  • Tracing / Debugging Fibers
  • fiber_wait_for_event() – micro:bit's semaphore and monitor
  • How Much Memory (RAM) is Available?
  • Analog to Digital Converter not Accurate when micro:bit is battery powered

These notes and example programs are also available online at www.pfod.com.au

Before you start these examples you should first work through Offline Compiler Setup for BBC micro:bit to install Netbeans and setup the support programs and libraries to allow you to compile C++ programs for the micro:bit. Then read the General and uBit sections of the docs for Lancaster University's microbit C++ support.

For simplicity all these examples will just use one Netbeans project, the pfodtest project set up by Offline Compiler Setup for BBC micro:bit. Just copy and past the various code examples into the main.cpp file you created in Offline Compiler Setup for BBC micro:bit.

Some Background to Lancaster University's micro:bit C++ support

Lancaster University's micro:bit C++ support is heavily dependent on a series of custom C++ libraries. The basic documentation for these libraries is on Lancaster University's microbit C++ support site. Refer to those API docs for the details of the various methods.

Step 1: A First Program

The front page of the Lancaster University's microbit C++ support site contains a simple program.

#include "MicroBit.h"
MicroBit uBit;
int main() { 
  uBit.init(); 
  uBit.display.scroll("HELLO WORLD!");
  release_fiber();
}

Open the main.cpp file you created in Offline Compiler Setup for BBC micro:bit and delete it contents and replace it with the above code.

  • Rebuild the project by clicking on the Netbeans “Hammer” icon
  • Plug in your micro:bit (via USB). It will show up as a new drive.
  • Open a file explore and drag and drop the pfodtest-combined.hex file under the C:/ ../pfodtest/build/bbc-microbit-classic-gcc/source directory to your micro:bit drive.
  • Your micro:bit will restart (press the reset button if it does not) and display “HELLO WORLD!” once only.

Note the ...-combined.hex file is in the /source sub-directory of …./build/bbc-microbit-classic-gcc

If this does not work go back and work through Offline Compiler Setup for BBC micro:bit example again and get it running first. See the Troubleshooting section at the bottom of that page.

The first two lines of the program

#include "MicroBit.h" 
MicroBit uBit;

are common to all your programs using the MicroBit support. You the access the various methods via this uBit object. Netbeans has smart auto complete so typing uBit. (uBit dot) in your code will popup a list of available methods to choose from.

The next line

int main() { 

defines the main() method. This is where your program starts. The first line of main should always be

uBit.init();

This initializes the scheduler, memory allocator, Bluetooth stack etc. ready for your program to use.

Then the code sends the string “HELLO WORD!” to be scroll across the 5x5 micro:bit led display and finally calls

release_fiber()

More on this next.

Step 2: Debugging Using the USB Serial Connection

There are various ways to debug a micro:bit C++ program, but a very simple one is to write out messages to the USB serial connection so you can see where the code is going and what it is doing. Note: the default USB Serial baud rate 115200, 8N1 (8 bits, No parity, 1 stop bit).

The next example, serialDebug_main.cpp, will create a new fiber that repeatedly display “HELLO WORLD!” and uses the USB serial connection to let you know when the call to scrollAsync failed because the display was still busy with the last message.

<br>#include "MicroBit.h"<br>MicroBit uBit;<br><br>void displayHello() {<br>    while (1) { // loop for ever<br>        int rtn = uBit.display.scrollAsync("HELLO WORLD!");<br>        if (rtn == MICROBIT_OK) {<br>            uBit.serial.send("Display OK\n");<br>        } else if (rtn == MICROBIT_BUSY) {<br>            uBit.serial.send("Display Busy\n");<br>        } else { // error<br>            uBit.serial.send("Invalid param\n");<br>        }<br>        uBit.sleep(1000); // wait 1sec and loop to try and display again<br>    }<br>}<br><br>int main() {<br>  uBit.init();<br>  create_fiber(displayHello); // create fiber and schedule it.<br>  release_fiber(); // "release the fibers!!"<br>
}

You may need to close and re-open TeraTerm to re-connect to micro:bit serial after loading the program.

Note carefully uBit.serial.send( ) and, almost all other uBit methods, are designed to be called from fibers only, see below.

A note about Ticker and interrupts in general

Ticker is a another means of repeatedly calling a method. e.g. (serialDebugTicker.cpp)

However be careful to note that Ticker (i.e. timer) calls the method (displayHello) from an interrupt and NOT on a fiber. This means that a call to uBit.serial.send(..) does not work as expected since it expects to be called from a fiber context. The result is your micro:bit may just hang. In general avoid using interrupts, if you can. If you do need to use them, keep the code short and simple. Just set some global (volatile) variables and return.

Limitations of USB debugging

The USB serial TX buffer has a limited length, typically 21 bytes. You can use uBit.serial.getTxBufferSize() to see what the size is. If you are sending a lot of messages quickly or sending longer messages, you may loose some of the messages either because the Tx buffer is full OR because uBit.serial is busy handling the previous message. In the serialDebug_main.cpp all the messages were less than 21 bytes long and there was only one fiber trying to send, but with multiple fibers running you can lose messages altogether if the serial connection is busy.

One way around this loss due to busy limitation might be to use

uBit.serial.printChar(char,SYNC_SPINWAIT);

This is documented to lock up the processor until the char is sent. There are two problems with SYNC_SPINWAIT:-

  1. locking up the processor is generally a bad idea and can interfere with the operation of other micro:bit services, such as bluetooth.
  2. In the current software release, if the serial device is already busy sending some previous data the call just returns without sending the char even if you specify SYNC_SPINWAIT (Also see this bug report)

One solution to this problem is to use this loopUntilSent() method

void loopUntilSent(ManagedString str) {
    int rtn = uBit.serial.send(str);
    while(rtn == MICROBIT_SERIAL_IN_USE) {
       uBit.sleep(0); // let other tasks run
       rtn = uBit.serial.send(str); 
    }
}

and replace uBit.serial.send(“..”); with loopUntilSent(“...”); See the main_monitor.cpp for an example of its use.

Another problem with USB debugging is that adding print statements changes the program operation. Delays are introduced, extra fibers may be created, extra events are sent and processed. These changes can hide or confuse the programming error you are trying to debug.

One approach to avoiding these unwanted effects is to define a char buffer and write to it from various points in the program to record what is happening and in what order. Then at a suitable time send the whole buffer to the USB serial. See Producer_Consumer example code for an example of this apporach.

Step 3: Managed Strings and ManagedPrintf

In addition to fibers (discussed below), micro:bit C++ has ManagedString. Normal Strings (char arrays) in C and C++ are a source of errors and memory leaks.

A memory leak occurs when you allocate (take) some RAM and don't free it (give it back). If this happens over and over again your program eventually runs out of memory and then next allocation fails and the program stops working. ManagedString solves this problem. Its code keeps track of the memory used and makes sure it if freed when string is no longer required.

In the serialDebug_main.cpp, example above,

uBit.display.scrollAsync("HELLO WORLD!") 

call actually turns “HELLO WORLD” into a ManagedString which is then passed off to the display to show.

ManagedPrintf

Debugging using uBit.serial.send("..") or loopUntilSent(“...”) is useful but sometimes you want to debug some data. ManagedString can already automatically convert an int to a string for printing, but does not handle other data types like unsigned ints and floats. This small class ManagedPrintf lets you create a ManagedString using standard printf formats.

NOTE: The underlying mbed compiler does not support printf for doubles so DO NOT use double formats %f %F %e %E %g %G %a %A with ManangedPrintf
If you need to print a double to the USB serial you can use the printFloat() method.

Unzip ManagedPrintf to your pfodtest/source dir and then follow the instructions here to add those files to your Netbeans project and the yotta build files. Then copy and past this managedPrintf_main.cpp into your existing main.cpp, replacing all the existing contents.

#include "MicroBit.h"<br>#include "ManagedPrintf.h"<br>MicroBit uBit;<br>void displayHello() {
    unsigned long counter = 0;
    while (1) { // loop for ever
        counter++;
        ManagedString mStr = ManagedPrintf::printf("loop count:%lu\n", counter);
        uBit.serial.send(mStr);
        // or just uBit.serial.send(ManagedPrintf::printf("loop count:%lu\n", counter));
        int rtn = uBit.display.scrollAsync("HELLO WORLD!");
        if (rtn == MICROBIT_OK) {
            uBit.serial.send("Display OK\n");
        } else if (rtn == MICROBIT_BUSY) {
            uBit.serial.send("Display Busy\n");
        } else { // error
            uBit.serial.send("Invalid param\n");
        }
        uBit.sleep(1000); // wait 1sec and loop to try and display again
    }
}<br><br>int main() {
    uBit.init();
    create_fiber(displayHello); // create fiber and schedule it.
    release_fiber(); // "release the fibers!!"
}

Now when you compile and run this program on your micro:bit, the terminal window will show the loop counter as well as the other messages.

Step 4: Fibers (another Sort of Thread)

Lancaster University's C++ support implements a lightweight thread model called fibers. The multi-tasking is co-operative. That means while one part of your program is running other parts are not. Programs that do more than one thing at a time are called concurrent programs. The micro:bit C++ runtime provides two ways you can achieve concurrency in your programs:

The multi-tasking scheduler is a type of non-preemptive scheduler. This means that the runtime will never take control away from your program - it will wait for it to make a call to a runtime function that is blocking. (All the functions that are blocking are listed as such in their documentation.) This means you do NOT need semaphores to guard lock test and set. If you need to guard a method from being run by more then one fiber you can just use a simple bool value, say for example bool status, and test it on entry and return if some other fiber has set it. See below for how to queue access to a resource.

So back to release_fiber(). This call says we have finished with this processing thread (fiber), that called main(), and should return the the scheduler to see what else, if any thing, needs doing. If nothing needs doing, micro:bit goes into a power efficient sleep.

The call to release_fiber() in main() is very important. If you don't call release_fiber() at the end of main(), the scheduler does not get called and the whole program stops.

So you should think of main() as your setup routine where you initialize your objects, start your background fibers etc and then hand of to the scheduler to keep things running. If you do long (non-blocking) operations in main(), other processes will no be run until that operation is finished.

Functions that may take a very long time to complete (e.g. display.scroll) often have "Async" versions (e.g. display.scrollAsync). These functions do the same job, but don't wait for the effect to finish before allowing the user's program to continue. Instead, as soon as the function is called, the user's program carries on executing (and can go an do something else while the 'Async' task is running in the background).

Blocking is Good in micro:bit C++

In most programming languages, blocking operations are BAD,such as the delay() and read() calls in Arduino sketches. The reason is that in most programming languages, when a thread's code blocks everything stops (except interrupts) and other parts of the code don't get a chance to run. However in micro:bit C++ blocking operations are GOOD, because the fiber model detects a blocking operation and pauses that fiber and lets another fiber run instead.

So, except for the fact that the execution of the code following the blocking call is delayed, there is no problem with calling blocking operations in micro:bit C++. For example in the First Program above, the call to

uBit.display.scroll("HELLO WORLD!");

is a blocking call, but other then delaying the execution of the fiber_release() code below, it is not a problem. In fact for methods that take a long time to execute, blocking calls are an advantage as they gives other code (fibers) a chance to run.

Having said that, often you will prefer to use the ...Async versions of methods, if they are available, because calls to the...Async methods will not hold up the execution of the following code in your method. Many methods also have an ASYNC mode option that achieves the same result

The few (so far) exceptions are uBit.serial.printf(... ) and puts() and putc() which seem to block completely while waiting to send data if the data exceeds the available tx buffer space. Use uBit.serial.send(..) instead.

Step 5: Communicating Between Fibers

In micro:bit C++, the standard method for communicating between fibers is via MicroBitEvents. Read the uBit.messageBus documentation for a good introduction to events and listeners.

There a two ways to communicate between fibers:-

  • Start a new fiber to execute a method, optionally passing an argument to the method.
  • Register a listener which will run a method on it own fiber when that event is seen. Data can be accessed directly from the fiber.

Starting a new fiber

The

create_fiber(displayHello)

in the previous section is an example of creating and starting a new fiber to run a method. The fiber stops when the method exits. It that example there is an infinite while(1) loop to keep the fiber running forever. You can also pass a void * to an argument when you create the fiber as in

create_fiber(debugOut,(void*)&dumpInterval);

Fibers are often created in the main() method but can also be created/started from other fibers.

Registering a listener

Micro:bit C++ events consist of a source id, event value and a method to run when that event is seen. Data can be accessed directly from the fiber method with out any special locks or volatile constructs. Note: If you just want to pass an integer value around you can use the event value itself.

To pass data to a fiber that is responding to an event:-

  1. Define a global variable to hold the data
  2. Set the data in one fiber and then fire off an event to notify one or more other fibers the data is available
  3. Copy the data in the other fiber to take a snapshot of its current state.

Producer_Consumer

Here is a simple Producer_Consumer example. The producer fiber generates the data and the consumer fiber displays it. This example code also illustrates using the debugBuffer to see what is happening at a low level. The produce updates the data every 20mS, much faster then the consumer can display it. So many of the updates are missed. By default the

uBit.messageBus.listen(DATA_ID,NEW_DATA,consumer);

statement will queue events waiting to access fiber method however the queue is only 10 deep so in this case most of the events are lost.

Here is a sample output, the second line is the debug output printed at 5 secs

A0P16R18
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHI
X24C3S

The first and third output line is what the consumer sees. The second line is what the producer generates. The debugBuffer output shows that most of the data updates are being missed by the consumer.

Tracing / Debugging Fibers

Fibers don't have a naming feature, but you can print out the address of the current fiber to get some idea what is happening. The ManagedPrintf class includes a getAddress(void *) method. Here is the statement that prints the current fiber's pointer address using the loopUntilSent() method (from above) to ensure its always reaches the terminal.

 loopUntilSent(“CurrentFiber:”+ManagedPrintf::getAddress(currentFiber)); <br>

See main_semaphore.cpp, for an example of its usage.

Step 6: Fiber_wait_for_event() – Micro:bit's Semaphore and Monitor

micro:bit uses fiber_wait_for_event() and events to implement monitors and semaphores. If you want your fibers to wait until something else happens (monitor), OR if you want you to control mulitple fiber's access to a resource (semaphore), e.g. the display or serial connection, you put your fibers to sleep with fiber_wait_for_event() and send them event when they need to be woken up. Sleeping fibers are kept in a queue and woken on a strict first in first out basis.

micro:bit Monitor Example

The example, main_monitor.cpp, starts an number of fibers and then puts them all to sleep until you press Button A. This code also illustrates the use of the loopUntilSent() method.

fiber_wait_for_event(MY_WAKEUP_EVENT_ID,MY_WAKEUP_EVENT_VALUE); 

is used to put the tasks to sleep and

MicroBitEvent(MY_WAKEUP_EVENT_ID,MY_WAKEUP_EVENT_VALUE); 

is used to wake them all up.

Micro:bit Semaphore Example

The example, main_semaphore.cpp, has limited resource which should only be accessed by one fiber at a time. This resource is protected by a simple lock check and fiber_wait_for_event(). In this simple example the limited resource just sleeps the fiber for 2sec.

A couple of coding points to note:-

  • The check for isLocked() is enclosed in a while (isLocked()) { statement. This is because when one fiber unlocks the resource ALL the waiting fibers are woken up and try to take control. The first one woken up finds isLocked() false and falls through to take the lock. The others then find isLocked() true and are put back to sleep
  • In the unlock() code after sending the event to wake up all the other fibers the current fiber is put to sleep for 0mS. This puts the current fiber that was just using the resource, on the end of sleeping fibers queue so that the other waiting fibers get a change to use the resource.

You can check that all fibers get fair access to the resource by looking at the terminal output of the fiber addresses.

Problems with MICROBIT_ID_NOTIFY_ONE

The Lancaster micro:bit C++ library uses semaphores to control access to the display and the serial connection. However there are problems with this.

The idea is that sending a

MicroBitEvent(MICROBIT_ID_NOTIFY_ONE, value)

will only wake up one of the fibers waiting on (MICROBIT_ID_NOTIFY, value). However if there is any fiber waiting with a catch all (MICROBIT_ID_NOTIFY, MICROBIT_EVT_ANY), it will be woken up instead of the fibers waiting on value. This small program, main_lost_display.cpp, illustrates the problem.

Another problem is that if you use fiber_wait_for_event(MICROBIT_ID_NOTIFY_ONE, value); then MicroBitEvent(MICROBIT_ID_NOTIFY_ONE, value) will wake up ALL the waiting fibers not just one, as you might have expected.

So the guidelines for monitors and semaphores are:-

  1. Use main_semaphore.cpp style code for writing you own semaphores
  2. Never use fiber_wait_for_event(MICROBIT_ID_NOTIFY, MICROBIT_EVT_ANY)
  3. Never use fiber_wait_for_event(MICROBIT_ID_NOTIFY_ONE, ….)

Step 7: How Much Memory (RAM) Is Available?

A simple way to determine how much RAM is available to your program is to run this small method, printMemoryAndStop()

void printMemoryAndStop() {
    int blockSize = 64; int i = 1;
    uBit.serial.printf("Checking memory with blocksize %d char ...\n", blockSize);
    while (true) {
        char *p = (char *) malloc(blockSize);
        if (p == NULL) break; // this actually never happens, get a panic(20) outOfMemory first
        uBit.serial.printf("%d + %d/16 K\n", i/16, i%16);
        ++i;
    }
}
The method just keeps allocation small blocks (64bytes) of memory until it runs out of memory and stops. There is the last few lines of output from the trivial <a href="http://www.forward.com.au/pfod/microbit/hello_memory_test_main.cpp">hello_memory_test_main.cpp</a> example.<br>3 + 15/16 K
4 + 0/16 K
4 + 1/16 K
4 + 2/16 K
4 + 3/16 K

Which indicates there is 4288 bytes of RAM available for use. Not a much as you might think for such a simple program. A lot of the available RAM is taken up by the BLE support. To free up a lot more RAM, edit the config.json file in the root directory of the project (e.g. in /pfodtest dir) to

{<br>    "microbit-dal": {
        "bluetooth": {
            "enabled": 0
        }
    }
}

This disables bluetooth support. Then rebuild the project using the yotta build cmd. A clean build in Netbeans does not work in this case. Now when you re-run the program you get

11 + 7/16 K<br>11 + 8/16 K
11 + 9/16 K
11 + 10/16 K

i.e. 11904 bytest of RAM available. That is bluetooth support uses about 7616 bytes of your RAM.

Some guidelines for RAM usage :-

  • ubit.serial.send(“this is a long string”);
    only uses RAM temporarily to create the string/ManagedString and send it.
  • A global variable (i.e. at the top of the program where MicroBit uBit; is declared) like
    char str_1[] = “this is a global string”;
    reduces the available RAM permanently, i.e. for the whole program.
  • A const global variable like
    const char str_1[] = “this is a global string”;
    is stored in flash memory instead of RAM and does not reduce the available RAM

If you are using printMemoryAndStop() to test this for yourself, make the strings longer, i.e. >64bytes long, so the their effect shows up in the results.

Analog to Digital Converter not Accurate when micro:bit is battery powered

It seems the AtoD reference is Vcc and gain is 1:1, so if you power the micro:bit via USB and connect the 3V pin to the AtoD pin and calibrate the output with a volt meter, then when you switch to battery power the read does not change even though the battery voltage (the voltage on the 3V pin) is 0.5V lower. Not really much you can do about this without rewriting the low level AtoD register configuration to use the 1.2V internal band gap reference.

One use for this inaccurate AtoD converter is as a digital input when battery powered. Normally a digital input with a pullup will draw 0.25mA when low (3V supply and 12k pullup). However if you configure the input as an AtoD then the input impedence is about 130K and using a 470K external pullup resistor will draw about 0.005mA when input is 'high' and about 0.0065mA when the input is grounded. The AtoD will read about 220 counts when high and about 0 counts when the input is grounded. Depending on the noise levels you could also try 1M pullup which would give about 117 counts when 'high' and half the current to 2uA to 3uA.

Conclusion

The multi-tasking fibers provided by micro:bit C++ are an easy and effective way to write programs. USB serial is the primary means of debugging programs. Putting fibers to sleep to be woken by an event (signal) provides simple means of responding to a global signal. Adding a simple boolean lock allows you to control and restrict multi-fiber access to a single resource.

Comments

author
drmpf (author)2016-11-07

Added section on inaccuracy of AtoD when battery powered.

author
ThirdEarthDesign (author)2016-10-12

I've been curious about the BBC Micro:bit so thanks for the Ible :)

author
drmpf (author)2016-10-11

Added section on How Much Memory (RAM) is Available?

author
drmpf (author)2016-10-10

Added note about limitations on printing doubles.

author
drmpf (author)2016-10-10

Added note about SYNC_SPINWAIT problem.