Skip to content

Foregoing the uBit object

Recommended only for experienced users

You should only concern yourself with this page if you know what you're doing.

We purposefully use uBit as our exposed API. We can guarantee that programs built with the uBit object will work in the future, but we cannot guarantee the same for individualised components!

Under the surface, the micro:bit runtime is a highly configurable, modular and component based piece of software.

The uBit object is provided as a collection of the commonly used components, all gathered together in one place to make it easier for novice users to access the functionality of the device. However, there is no obligation to use the uBit abstraction. More advanced users may prefer to create and use only the parts of the runtime they need.

This provides more control and often frees up more memory resource for the application program - but does so at the expense of the user taking more responsibility and additional complexity in their programs.

Using components directly

Taking advantage of the modular structure of the micro:bit runtime is fairly straightforward.

  • Firstly, create a program that does not create or initialise a uBit object.
  • Include MicroBit.h (or if you prefer, just the header files of the components you want to use). Including MicroBit.h is however, simpler.
  • Instead, create C++ object instances of the classes that you want to use as global variables in your program. Create as many components as you need. You are free to use any of the constructors in this documentation.
  • Call functions on those instances to elicit the behaviour you need, using the name of your object instances instead of uBit.*

For example, if you wanted to create a program that just used the LED matrix display driver, you might write a program like this:

Example

1
2
3
4
5
6
7
8
9
#include "MicroBit.h"

MicroBitDisplay display;

int main()
{
    while(1)
        display.scroll(":)");
}

If you need other components, add them to your program in the same way.

If a component has a dependency on another component (e.g. in the example below, the accelerometer is dependent on an I2C bus), then this will be requested as a mandatory parameter in the constructor.

See the 'Component Constructor' section of each component's API documentation for details and examples.

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include "MicroBit.h"

MicroBitI2C i2c = MicroBitI2C(I2C_SDA0, I2C_SCL0);
MicroBitAccelerometer accelerometer = MicroBitAccelerometer(i2c);
MicroBitDisplay display;

int main()
{
    while(1)
        display.scroll(accelerometer.getX());
}

Warning

micro:bit runtime components should always be brought up as global variables. They should not be created as local variables - either in your main method or anywhere else. The reason for this is the the runtime is a multi-threaded environment, and any variables created in stack memory (like local variables) may be paged out by the scheduler, and result in instability if they utilise interrupts or are accessed by other threads. So... don't do it!

System components

There are also system components that provide background services. Without the uBit object, these will not be created by default. Examples include the fiber scheduler, message bus and heap allocator.

You are not required to initialise these components, but you should do so if you want to benefit from the functionality they provide. The following section describe how to do this.

Initialising the message bus

The MicroBitMessageBus allows events to be created and delivered to applications. So if a MicroBitMessageBus is not created, then all events in the micro:bit runtime will be quietly ignored.

To enable this functionality, simply create an instance of the MicroBitMessageBus class. From that point onward in your program, you can raise and listen for events as described in the MicroBitMessageBus documentation.

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include "MicroBit.h"

MicroBitMessageBus bus;
MicroBitButton buttonA(DEVICE_PIN_BUTTON_A, DEVICE_ID_BUTTON_A);
MicroBitDisplay display;

void onPressed(Event e)
{
    display.print("S");    
}

int main()
{
    bus.listen(DEVICE_ID_BUTTON_A, DEVICE_BUTTON_EVT_CLICK, onPressed);

    while(1)
    {
    }
}

Warning

Running a MessageBus without the Fiber Scheduler will result in all event handlers being registered as MESSAGE_BUS_LISTENER_IMMEDIATE (see MicroBitMessageBus for details). This means that your event handler will be executed in the context of the code that raised the event. This may include interrupt context, which may not be safe for all operations. It is recommend that you always run the MessageBus with the Fiber Scheduler in order to allow the event to be decoupled from interrupt context.

Initialising the fiber scheduler

Often when using asynchronous events, it is also useful to run the fiber scheduler. Without a scheduler in operation, all event handlers (such as the one above) will be executed with the threading mode MESSAGE_BUS_LISTENER_IMMEDIATE, as described on the MicroBitMessageBus documentation.

Also, it is not really possible to transparently enter a power efficient sleep - as illustrated in the busy loop in the above example.

Initialising the fiber scheduler is simple, and is demonstrated in the following example.

From the moment the fiber scheduler is initialised, it is then possible to block the processor in a power efficient way and to operate threaded event handlers:

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include "MicroBit.h"

MicroBitMessageBus bus;
MicroBitButton buttonA(MICROBIT_PIN_BUTTON_A, MICROBIT_ID_BUTTON_A);
MicroBitDisplay display;

void onPressed(MicroBitEvent e)
{
    display.print("S");    
}

int main()
{
    scheduler_init(bus);

    bus.listen(MICROBIT_ID_BUTTON_A, MICROBIT_BUTTON_EVT_CLICK, onPressed);

    while(1)
        fiber_sleep(1000);
}

Note

Function calls to uBit.sleep() must be replaced with the direct, equivalent calls to the scheduler using fiber_sleep().

Initialising the heap allocator

The micro:bit runtime provides an optional, heap memory allocator. This is primarily to permit the use of multiple regions of memory to be used as heap memory space for your variables.

The uBit initialisation function will automatically release any memory unused by the Bluetooth stack for general purpose use in this fashion (this typically provides an additional 1K of SRAM under Bluetooth enabled builds, and another 8K if Bluetooth is disabled).

Should you wish to also reclaim memory in this way, you can do so as follows:

1
2
3
4
5
6
#include "MicroBit.h"

int main()
{
    device_create_heap(MICROBIT_SD_GATT_TABLE_START + MICROBIT_SD_GATT_TABLE_SIZE, MICROBIT_SD_LIMIT);
}