The workshop notes

Open the bonnet.

The code is the machine. The paragraph just below is the label on the part. Read a little, understand a little, then put the pieces together.

Before the engine turns

The shopping list: Cargo.toml

Cargo.toml names the libraries the Rust program needs. These are parts on the workbench, not code that runs on the micro:bit by themselves.

Cargo.toml
[dependencies]
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
embedded-hal = "1.0"
microbit-v2 = "0.16"
panic-halt = "1.0"
panic-rtt-target = "0.2"
rtt-target = "0.6"
lsm303agr = "1.1"

microbit-v2 gives us the board and its devices; cortex-m and cortex-m-rt provide the ARM chip support and entry point. embedded-hal gives common interfaces for pins and timers. The panic handlers and RTT crates are used by the examples that stop on a panic or print back to the laptop. lsm303agr talks to the motion sensor.

Set the workbench

Fitter's instructions: .cargo/config.toml

This file tells Cargo which chip target to build for and how to start a flashed program.

.cargo/config.toml
[build]
target = "thumbv7em-none-eabihf"

[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip nRF52833_xxAA"
rustflags = [
  "-C", "linker=rust-lld",
  "-C", "link-arg=-Tlink.x",
]

The target names the ARM Cortex-M4F used by the micro:bit v2. rust-lld is the linker; -Tlink.x tells it to use the linker script supplied by the Cortex-M runtime, which lays out the firmware and its reset and interrupt vectors. The runner asks probe-rs to flash and run the linked firmware on an nRF52833. That is why cargo run is a hardware action here, while cargo check only checks the code and does not perform the final link.

Example 01 ? all-on.rs

Every window lit

The 5-by-5 display is a wall of windows. This program switches every lamp on, gives you time to see the picture, then switches them all off. It is the simplest useful test that the board, display and timer are working together.

Code, then the explanation

Part 01

all-on.rsRead, then look below
#![no_main]

This says the program does not arrive through an operating system. The micro:bit is the machine we are talking to directly; it needs its own starting point.

Part 02

all-on.rsRead, then look below
#![no_std]

There is no desktop standard library on this small chip. We keep the program in the embedded world and bring in only the tools it needs.

Part 03

all-on.rsRead, then look below
use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use microbit::{board::Board, display::blocking::Display, hal::Timer};
use panic_halt as _;

These imports bring the entry-point marker, a timer delay, the board and display types, and a simple panic handler into reach. The delay trait is what lets us ask the timer to wait in milliseconds.

Part 04

all-on.rsRead, then look below
#[entry]
fn main() -> ! {

This marks the function where the chip begins. The exclamation mark in `-> !` tells Rust that the program never returns to a caller; a running micro:bit keeps being our program.

Part 05

all-on.rsRead, then look below
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);

`Board::take()` claims the hardware peripherals once. We give `TIMER0` to a timer, then give the display pins to the display driver. `mut` matters because a timer and display change as they are used.

Part 06

all-on.rsRead, then look below
    // A picture is 5 rows of 5 windows: 1 = lamp on, 0 = lamp off.
    let every_lamp = [
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
    ];

Here is the picture itself: five rows, five windows in every row. Each `1` means light; each `0` would mean dark. We are describing the whole sign in data, instead of asking the program to control 25 separate lamps by hand.

Part 07

all-on.rsRead, then look below
    loop {

The board is not meant to run this once and finish. `loop` makes the small sequence below start again forever.

Part 08

all-on.rsRead, then look below
        // Show the picture for one second...
        display.show(&mut timer, every_lamp, 1000);

`show` hands the picture and timer to the display driver for 1,000 milliseconds. Behind this simple call, the driver keeps scanning the rows fast enough for our eyes to see one steady, bright square.

Part 09

all-on.rsRead, then look below
        // ...then switch every lamp off and wait one second.
        display.clear();
        timer.delay_ms(1000_u32);

Now the display goes dark and the timer waits one second. The suffix `_u32` tells Rust the delay is an unsigned 32-bit number, the type this timer method expects.

Part 10

all-on.rsRead, then look below
    }
}

These braces close the loop and the entry function. When the loop closes, it begins again: all on for one second, all off for one second. That is the whole machine.

See the complete program in one piece
examples/all-on.rsComplete source
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use microbit::{board::Board, display::blocking::Display, hal::Timer};
use panic_halt as _;

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);

    // A picture is 5 rows of 5 windows: 1 = lamp on, 0 = lamp off.
    let every_lamp = [
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
    ];

    loop {
        // Show the picture for one second...
        display.show(&mut timer, every_lamp, 1000);
        // ...then switch every lamp off and wait one second.
        display.clear();
        timer.delay_ms(1000_u32);
    }
}
Back to The wall of windows

Example 02 ? digits.rs

Paint the numbers on the LED grid

The display is a wall of 25 windows, not a tiny printed screen. We choose which windows are lamps and which are dark. This program draws each digit from zero to nine, holds it long enough to recognise, and then paints the next one.

Keep the alphabet in one place `DIGITS` is the sign painter's alphabet: ten pictures, each five rows by five columns. Each number is centred and uses three columns, leaving one dark margin on either side. A one switches on a lamp; a zero leaves it off. ### Set up the two tools that make the picture real The board provides the LED pins, and the timer gives the display driver a steady rhythm. The display driver handles the rapid row scanning needed to make the whole picture appear lit at once. ### Show one sign at a time

The outer loop visits every picture in `DIGITS`, in order. `display.show` presents each for 800 milliseconds. When the tenth picture has had its turn, the loop begins again at zero. No number conversion is needed here: each picture is already drawn.

Open the complete program and read the walkthrough
examples/digits.rsComplete source
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use microbit::{board::Board, display::blocking::Display, hal::Timer};
use panic_halt as _;

// The sign painter's alphabet: ten pictures, one for each digit.
// Each picture is 5 rows of 5 windows. 1 = light on, 0 = light off.
// Row 0 is the top row. Column 0 is the left column.
// Our digits are 3 windows wide and sit in the middle, columns 1 to 3.
const DIGITS: [[[u8; 5]; 5]; 10] = [
    // 0
    [
        [0, 1, 1, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    // 1
    [
        [0, 0, 1, 0, 0],
        [0, 1, 1, 0, 0],
        [0, 0, 1, 0, 0],
        [0, 0, 1, 0, 0],
        [0, 1, 1, 1, 0],
    ],
    // 2
    [
        [0, 1, 1, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 0, 0, 0],
        [0, 1, 1, 1, 0],
    ],
    // 3
    [
        [0, 1, 1, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    // 4
    [
        [0, 1, 0, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 0, 0, 1, 0],
    ],
    // 5
    [
        [0, 1, 1, 1, 0],
        [0, 1, 0, 0, 0],
        [0, 1, 1, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    // 6
    [
        [0, 1, 1, 1, 0],
        [0, 1, 0, 0, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    // 7
    [
        [0, 1, 1, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 0, 1, 0, 0],
        [0, 0, 1, 0, 0],
        [0, 0, 1, 0, 0],
    ],
    // 8
    [
        [0, 1, 1, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 1, 1, 0],
    ],
    // 9
    [
        [0, 1, 1, 1, 0],
        [0, 1, 0, 1, 0],
        [0, 1, 1, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 1, 1, 1, 0],
    ],
];

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);

    loop {
        // Show each picture in turn, 800 milliseconds each, then start again.
        for picture in DIGITS {
            display.show(&mut timer, picture, 800);
        }
    }
}
Back to The wall of windows

Example 03 ? counter.rs

Slide a number through a small window

A two-digit number is wider than the micro:bit display. Think of a long paper strip behind a five-column window: draw the number on the strip, then slide the strip one column at a time. That is what this program does for every number from 00 to 99.

Store each digit as a little sign `FONT` contains ten five-row pictures, each three bits wide. In binary, `0b101` means the left and right lamps are on while the middle one is off. Each digit takes three columns; the fourth column is a blank gap. ### Ask what is printed at one place `strip_pixel` is the sign painter's small rule: for a strip column and row, should that window be lit? The first five columns are blank. After them, division by four tells us which digit we are inside, and the remainder tells us which of its three columns, or its gap, we have reached. The shift picks the relevant bit from the three-bit row. Outside the two digits, the answer is zero. ### Make the strip longer than the screen The strip has five blank columns before the number, eight for two digits and their gaps, and five blank columns after it. The display is only five columns wide, so each picture is a five-column window cut from that longer strip. ### Move the window and start the next number

The number splits into tens and units with division and remainder. The nested loops visit every window offset, every row and every screen column, asking `strip_pixel` what belongs there. Each window stays up for 120 milliseconds. When it has crossed the strip, the next number goes on; after 99, the count starts over at 00.

Open the complete program and read the walkthrough
examples/counter.rsComplete source
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use microbit::{board::Board, display::blocking::Display, hal::Timer};
use panic_halt as _;

// Digits 0 to 9, three windows wide. Each row is 3 bits: 1 = lamp on, left bit first.
const FONT: [[u8; 5]; 10] = [
    [0b111, 0b101, 0b101, 0b101, 0b111], // 0
    [0b010, 0b110, 0b010, 0b010, 0b111], // 1
    [0b111, 0b001, 0b111, 0b100, 0b111], // 2
    [0b111, 0b001, 0b111, 0b001, 0b111], // 3
    [0b101, 0b101, 0b111, 0b001, 0b001], // 4
    [0b111, 0b100, 0b111, 0b001, 0b111], // 5
    [0b111, 0b100, 0b111, 0b101, 0b111], // 6
    [0b111, 0b001, 0b010, 0b010, 0b010], // 7
    [0b111, 0b101, 0b111, 0b101, 0b111], // 8
    [0b111, 0b101, 0b111, 0b001, 0b111], // 9
];

// Picture a long strip of paper behind a window five columns wide.
// On the strip: 5 blank columns, then two digits (3 columns + 1 gap each), then 5 blank.
// Sliding the window along the strip makes the number scroll past.
const STRIP_LEN: usize = 5 + 8 + 5;

/// What is on the strip at column `v`, row `row`? 1 = lamp on.
fn strip_pixel(v: usize, row: usize, digits: [usize; 2]) -> u8 {
    if v < 5 {
        return 0;
    }
    let w = v - 5;
    let which = w / 4; // 0 = tens digit, 1 = units digit, 2 = past the end
    let within = w % 4; // 0, 1, 2 = the digit's columns, 3 = the gap
    if which >= 2 || within == 3 {
        return 0;
    }
    (FONT[digits[which]][row] >> (2 - within)) & 1
}

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);

    loop {
        for number in 0..100usize {
            let digits = [number / 10, number % 10];
            // Slide the window along the strip, one column at a time.
            for offset in 0..=(STRIP_LEN - 5) {
                let mut picture = [[0u8; 5]; 5];
                for row in 0..5 {
                    for col in 0..5 {
                        picture[row][col] = strip_pixel(offset + col, row, digits);
                    }
                }
                display.show(&mut timer, picture, 120);
            }
        }
    }
}
Back to The paper strip

Example 04 ? arrows.rs

Let the buttons point the way

The board is a little sign at a crossroads. Press A and it points left. Press B and it points right. Leave both alone and the single centre lamp says: we are waiting.

Draw the signs before asking anyone to read them Each arrow is a five-row, five-column picture. A one is a lit window; a zero is a dark one. `CENTRE_DOT` is the waiting picture. Keeping these pictures as constants means the button logic can choose a sign instead of redrawing one every time. ### Give the program its board, clock and display `Board::take()` claims the micro:bit hardware. The timer helps the display driver keep its multiplexed lamps looking steady. A and B are inputs. The input-pin trait is brought in because `is_low` is the question this program asks them. ### A pressed button reads low The board gently holds each button pin high. Pressing the button connects that pin to ground, so the reading falls low. That is why the test is `is_low()`. The `unwrap()` says an impossible hardware-read error should stop the program rather than pretend the button was not pressed. ### Choose a sign, then look again

`if` and `else if` make A take priority if both buttons are down: A shows left; otherwise B shows right; otherwise the centre dot appears. `display.show` runs this picture for ten milliseconds, then the loop reads the buttons again. The ten milliseconds is a short viewing slice, not a ten-millisecond pause before the program starts listening.

Open the complete program and read the walkthrough
examples/arrows.rsComplete source
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use embedded_hal::digital::InputPin;
use microbit::{board::Board, display::blocking::Display, hal::Timer};
use panic_halt as _;

const LEFT_ARROW: [[u8; 5]; 5] = [
    [0, 0, 1, 0, 0],
    [0, 1, 0, 0, 0],
    [1, 1, 1, 1, 1],
    [0, 1, 0, 0, 0],
    [0, 0, 1, 0, 0],
];

const RIGHT_ARROW: [[u8; 5]; 5] = [
    [0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0],
    [1, 1, 1, 1, 1],
    [0, 0, 0, 1, 0],
    [0, 0, 1, 0, 0],
];

const CENTRE_DOT: [[u8; 5]; 5] = [
    [0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0],
];

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);
    let mut button_a = board.buttons.button_a;
    let mut button_b = board.buttons.button_b;

    loop {
        // A pressed button reads "low". Ask both buttons, over and over.
        if button_a.is_low().unwrap() {
            display.show(&mut timer, LEFT_ARROW, 10);
        } else if button_b.is_low().unwrap() {
            display.show(&mut timer, RIGHT_ARROW, 10);
        } else {
            display.show(&mut timer, CENTRE_DOT, 10);
        }
    }
}
Back to The doorbell

Example 05 ? indicator.rs

Make the arrows blink like indicators

A car indicator does not leave its lamp on continuously. It flashes, on and off. Here, A selects left, B selects right, and pressing both gives us hazard lights. The display is the lamp; repeated passes through the loop are our clock.

Three pictures, one decision `LEFT`, `RIGHT`, and `HAZARD` are five-by-five pictures; `BLANK` is all dark. The buttons read low when pressed, so each pass records whether A or B is down. ### Count time by counting passes `display.show` spends ten milliseconds showing a picture. We use `ticks` as a rough clock: forty passes are about 400 milliseconds, and dividing into groups of forty lets `% 2` alternate between on and off. It is a simple teaching clock; time spent doing other work makes the real interval approximate. ### Match the buttons to the picture The `match` looks at the pair `(left, right)`. Exactly A shows the left arrow; exactly B shows right; both together show the hazard pattern. When the blinking phase is off, or no button is pressed?the answer is `BLANK`. ### Keep the counter rolling safely

`wrapping_add` lets the unsigned counter roll back to zero at its maximum value instead of overflowing. The phase calculation still works after it wraps, so the indicator can blink for as long as the board is powered.

Open the complete program and read the walkthrough
examples/indicator.rsComplete source
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use embedded_hal::digital::InputPin;
use microbit::{board::Board, display::blocking::Display, hal::Timer};
use panic_halt as _;

const BLANK: [[u8; 5]; 5] = [[0; 5]; 5];

const LEFT: [[u8; 5]; 5] = [
    [0, 0, 1, 0, 0],
    [0, 1, 0, 0, 0],
    [1, 1, 1, 1, 1],
    [0, 1, 0, 0, 0],
    [0, 0, 1, 0, 0],
];

const RIGHT: [[u8; 5]; 5] = [
    [0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0],
    [1, 1, 1, 1, 1],
    [0, 0, 0, 1, 0],
    [0, 0, 1, 0, 0],
];

// Both arrows drawn on top of each other: the hazard lights.
const HAZARD: [[u8; 5]; 5] = [
    [0, 0, 1, 0, 0],
    [0, 1, 0, 1, 0],
    [1, 1, 1, 1, 1],
    [0, 1, 0, 1, 0],
    [0, 0, 1, 0, 0],
];

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);
    let mut button_a = board.buttons.button_a;
    let mut button_b = board.buttons.button_b;

    // Each pass round the loop takes about 10 milliseconds.
    // We count the passes. That count is our clock.
    let mut ticks: u32 = 0;

    loop {
        let left = button_a.is_low().unwrap();
        let right = button_b.is_low().unwrap();

        // 40 passes of 10 ms = 400 ms on, then 400 ms off.
        let lit = (ticks / 40) % 2 == 0;

        let picture = match (left, right) {
            (true, false) if lit => LEFT,
            (false, true) if lit => RIGHT,
            (true, true) if lit => HAZARD,
            _ => BLANK,
        };

        display.show(&mut timer, picture, 10);
        ticks = ticks.wrapping_add(1);
    }
}
Back to The indicator stalk

Example 07 ? tone.rs

Make one note from a pin and a clock

A tone is a repeated movement, not a special sound command. Hold the speaker pin high, then low, at a steady rate and the cone moves back and forth. This example makes an A note at 440 Hz for two seconds.

Work out the length of one movement A 440 Hz note completes 440 full high-low cycles per second. One cycle therefore lasts about 1,000,000 divided by 440 microseconds. The constant `HALF_CYCLE_US` is half of that period, rounded to 1,136 microseconds. ### Count out two seconds of vibration `CYCLES` is 440 times two, or 880 cycles. Each pass drives the speaker high for one half-cycle and low for the other. Together those delays make the square wave that the speaker turns into a tone. ### Play once, then let the board rest

The speaker pin starts low and the timer supplies the short delays. After all 880 cycles, the `wfi` loop waits for an interrupt. Without that final resting loop, the program would reach its end and start playing again when the runtime restarted it.

Open the complete program and read the walkthrough
examples/tone.rsComplete source
#![no_main]
#![no_std]

use cortex_m::asm;
use cortex_m_rt::entry;
use embedded_hal::{delay::DelayNs, digital::OutputPin};
use microbit::{hal::gpio, hal::timer::Timer, Board};
use panic_halt as _;

// A 440 Hz note: 440 pushes out and 440 pulls in every second.
// One whole cycle takes 1,000,000 / 440 = about 2273 microseconds.
// Half of that, pushed out, and half pulled in.
const HALF_CYCLE_US: u32 = 1136;
const CYCLES: u32 = 440 * 2; // two seconds

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut speaker = board.speaker_pin.into_push_pull_output(gpio::Level::Low);
    let mut timer = Timer::new(board.TIMER0);

    for _ in 0..CYCLES {
        speaker.set_high().unwrap();
        timer.delay_us(HALF_CYCLE_US);
        speaker.set_low().unwrap();
        timer.delay_us(HALF_CYCLE_US);
    }

    // The program lives in flash. Without this stop it would play again
    // every time you power the board. Sleep until something wakes the chip.
    loop {
        asm::wfi();
    }
}
Back to The speaker cone

Example 08 ? scale.rs

Play a scale one note at a time

A speaker cone makes a sound when it moves the air. Push it out and pull it back repeatedly; the faster it moves, the higher the note. This program does the pushing and pulling in software, then plays eight frequencies in order.

Write down the notes as frequencies `SCALE` stores eight frequencies in hertz, from 262 to 523 cycles per second. A frequency is how many complete push-pull cycles happen each second. ### Build one note from two halves `play` is generic over any output pin and any timer that can delay. One cycle has two halves: high, then low. Half a cycle lasts 500,000 divided by the frequency in microseconds. The number of cycles is frequency times duration in seconds; here, `ms / 1000` converts milliseconds to seconds. ### Run up the scale and leave the chip resting

The board's speaker pin is configured as an output, and the timer supplies those short waits. Each frequency plays for 400 milliseconds, followed by a 50-millisecond gap so the notes do not run together. At the end, `wfi()` lets the chip sleep rather than starting the scale again.

Open the complete program and read the walkthrough
examples/scale.rsComplete source
#![no_main]
#![no_std]

use cortex_m::asm;
use cortex_m_rt::entry;
use embedded_hal::{delay::DelayNs, digital::OutputPin};
use microbit::{hal::gpio, hal::timer::Timer, Board};
use panic_halt as _;

// Notes, in cycles per second (Hertz): do re mi fa sol la ti do
const SCALE: [u32; 8] = [262, 294, 330, 349, 392, 440, 494, 523];

/// Play one note: push and pull the speaker `hz` times a second, for `ms` milliseconds.
fn play<P: OutputPin, T: DelayNs>(speaker: &mut P, timer: &mut T, hz: u32, ms: u32) {
    let half_us = 500_000 / hz;
    let cycles = hz * ms / 1000;
    for _ in 0..cycles {
        speaker.set_high().unwrap();
        timer.delay_us(half_us);
        speaker.set_low().unwrap();
        timer.delay_us(half_us);
    }
}

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut speaker = board.speaker_pin.into_push_pull_output(gpio::Level::Low);
    let mut timer = Timer::new(board.TIMER0);

    for hz in SCALE {
        play(&mut speaker, &mut timer, hz, 400);
        timer.delay_ms(50_u32); // a short gap so the notes do not run together
    }

    loop {
        asm::wfi();
    }
}
Back to The speaker cone

Example 09 ? hello-rtt.rs

Let the board talk over the debug link

A debug probe is usually introduced as a way to flash a program. It can also carry a small message back from the board. RTT uses memory the debugger can read, so this example prints without using the micro:bit's USB serial connection.

Claim the board before using the runtime

Board::take() claims the micro:bit's peripherals and keeps the device peripheral access crate in this firmware. That crate supplies the interrupt vector table required by the Cortex-M runtime, even though this example does not use a sensor or LED.

Open the RTT channel before you speak

rtt_init_print!() sets up the print channel, and rprintln! sends a line through it. panic_rtt_target chooses the same RTT route if the program hits a panic. This is a debugger conversation, not ordinary text sent to a serial terminal.

Print five counts

The first message says hello. Then count begins at zero, and the while loop increments before printing. That ordering is why the visible count runs from one through five, not zero through four.

Finish the conversation without burning power

After the fifth count, the program has nothing left to print. wfi() tells the processor to wait for an interrupt instead of racing around an empty loop. The program is still loaded in flash; it is simply resting until something wakes the chip.

Open the complete program and read the walkthrough
examples/hello-rtt.rsComplete source
#![no_main]
#![no_std]

use cortex_m::asm::wfi;
use cortex_m_rt::entry;
use microbit::Board;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};

#[entry]
fn main() -> ! {
    let _board = Board::take().unwrap();
    rtt_init_print!();
    rprintln!("Hello from the micro:bit");

    let mut count: u32 = 0;
    while count < 5 {
        count += 1;
        rprintln!("count is {}", count);
    }

    loop {
        wfi();
    }
}
Back to Talking to the laptop

Example 10 ? hello-serial.rs

Send a line to the laptop

Here we take the other road from board to laptop: a UART serial connection. The micro:bit's USB bridge carries those serial bytes to the computer, where a terminal can read them.

Give formatted text somewhere to go `core::fmt::Write` lets `writeln!` write formatted lines into the serial peripheral. We claim the board and create a timer for the one-second pauses. ### Configure the serial road `Uarte::new` takes the serial peripheral and the board's UART pins. The settings say no parity bit and a speed of 115,200 bits per second. The computer's serial terminal must use the matching baud rate to make sense of the bytes. ### Count ten lines, then stop transmitting

`n` starts at zero, so the first line is numbered zero. It increments once per line, pauses for a second, and breaks when it reaches ten. That leaves lines zero through nine. The final `wfi` loop keeps the flashed program parked without printing forever.

Open the complete program and read the walkthrough
examples/hello-serial.rsComplete source
#![no_main]
#![no_std]

use core::fmt::Write;

use cortex_m::asm::wfi;
use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use microbit::{
    hal::{
        uarte,
        uarte::{Baudrate, Parity},
        Timer,
    },
    Board,
};
use panic_halt as _;

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);

    // The UART is the serial port. The second chip carries it to the laptop over USB.
    let mut serial = uarte::Uarte::new(
        board.UARTE0,
        board.uart.into(),
        Parity::EXCLUDED,
        Baudrate::BAUD115200,
    );

    let mut n: u32 = 0;
    loop {
        writeln!(serial, "line {} from the micro:bit", n).unwrap();
        n += 1;
        timer.delay_ms(1000_u32);
        if n == 10 {
            break;
        }
    }

    loop {
        wfi();
    }
}
Back to Talking to the laptop

Example 11 ? tilt.rs

Let the dot behave like a spirit-level bubble

A spirit level has a bubble that drifts as you tilt it and settles near the middle when the level is flat. The micro:bit's accelerometer measures the pull of gravity along three axes. We turn the X and Y readings into one lit window on the 5-by-5 grid.

Turn a measurement into a place on the grid `to_cell` receives acceleration in milli-g. Near zero, the reading belongs in the centre cell, number two. Each 200 mg moves it about one cell. The final checks clamp the answer to zero through four, so a strong tilt cannot send the dot outside the display. ### Use the sensor's internal two-wire connection The accelerometer communicates over I2C. `board.i2c_internal` selects the board's internal bus, and `K100` sets its clock to 100 kHz. The LSM303AGR driver is then given that bus, initialised, and put in high-resolution mode at 50 readings per second. ### Draw only when a fresh reading arrives

The status check avoids drawing the same stale measurement over and over. A new reading gives X, Y and Z in milli-g; this picture uses X for the column and Y for the row, so Z is deliberately ignored. The picture starts dark, one cell is lit, and the display refreshes it for twenty milliseconds.

Open the complete program and read the walkthrough
examples/tilt.rsComplete source
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use microbit::{
    display::blocking::Display,
    hal::{twim, Timer},
    pac::twim0::frequency::FREQUENCY_A,
    Board,
};
use lsm303agr::{AccelMode, AccelOutputDataRate, Lsm303agr};
use panic_halt as _;

/// Turn a tilt reading (in thousandths of g) into a column or row, 0 to 4.
/// Flat means about 0 and lands in the middle, 2.
fn to_cell(mg: i32) -> usize {
    let step = mg / 200; // every 200 mg moves the dot by one window
    let cell = 2 + step;
    if cell < 0 {
        0
    } else if cell > 4 {
        4
    } else {
        cell as usize
    }
}

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);

    // The tilt sensor talks on a two-wire road called I2C, inside the board.
    let i2c = twim::Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100);

    let mut sensor = Lsm303agr::new_with_i2c(i2c);
    sensor.init().unwrap();
    sensor
        .set_accel_mode_and_odr(
            &mut timer,
            AccelMode::HighResolution,
            AccelOutputDataRate::Hz50,
        )
        .unwrap();

    loop {
        if sensor.accel_status().unwrap().xyz_new_data() {
            let (x, y, _z) = sensor.acceleration().unwrap().xyz_mg();

            // One lit window, the bubble in the spirit level.
            let mut picture = [[0u8; 5]; 5];
            let col = to_cell(x);
            let row = to_cell(y);
            picture[row][col] = 1;
            display.show(&mut timer, picture, 20);
        }
    }
}
Back to The spirit level

Example 12 ? temperature.rs

Read the chip's temperature

There is a thermometer inside the micro:bit's main chip. It measures the silicon, not the room directly. When the board has been working hard, the two readings can differ. This example prints the reading for the laptop and shows its whole-number digits on the LED grid.

Turn a digit into a small picture `FONT` stores ten three-column digit shapes. `picture_of` starts with a dark five-by-five picture, then copies the three bits for each row into the middle columns. Shifting the row and masking with one asks for a single lamp at a time. ### Ask the chip, not an imaginary room sensor `Temp::new` connects to the thermometer built into the chip. `measure()` returns a fixed-point reading; `to_num()` converts it to a number we can print. RTT sends that value back to the laptop. ### Fit the reading onto two digits The display can show only one three-column digit comfortably, so the reading is shown as tens and units in turn. The float is cast to an integer, which drops the fractional part. Values below zero are clamped to zero and values above 99 to 99; then division and remainder separate the two digits. ### Give each digit room to be read

The tens digit stays visible for 800 milliseconds. The display clears briefly, then the units digit appears for another 800 milliseconds. After a further pause, the loop takes a fresh reading. The serial RTT line and the LED picture are two views of the same measurement.

Open the complete program and read the walkthrough
examples/temperature.rsComplete source
#![no_main]
#![no_std]

use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use microbit::{
    display::blocking::Display,
    hal::{temp::Temp, Timer},
    Board,
};
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};

// Digits 0 to 9, three windows wide. Each row is 3 bits: 1 = lamp on, left bit first.
const FONT: [[u8; 5]; 10] = [
    [0b111, 0b101, 0b101, 0b101, 0b111], // 0
    [0b010, 0b110, 0b010, 0b010, 0b111], // 1
    [0b111, 0b001, 0b111, 0b100, 0b111], // 2
    [0b111, 0b001, 0b111, 0b001, 0b111], // 3
    [0b101, 0b101, 0b111, 0b001, 0b001], // 4
    [0b111, 0b100, 0b111, 0b001, 0b111], // 5
    [0b111, 0b100, 0b111, 0b101, 0b111], // 6
    [0b111, 0b001, 0b010, 0b010, 0b010], // 7
    [0b111, 0b101, 0b111, 0b101, 0b111], // 8
    [0b111, 0b101, 0b111, 0b001, 0b111], // 9
];

/// Draw one digit in the middle three columns of the grid.
fn picture_of(digit: usize) -> [[u8; 5]; 5] {
    let mut picture = [[0u8; 5]; 5];
    for row in 0..5 {
        for col in 0..3 {
            picture[row][col + 1] = (FONT[digit][row] >> (2 - col)) & 1;
        }
    }
    picture
}

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);
    let mut display = Display::new(board.display_pins);

    // The chip has a thermometer inside it. It reads the chip's own temperature,
    // which is close to the air around it when the board has not been working hard.
    let mut thermometer = Temp::new(board.TEMP);

    loop {
        // The reading comes in steps of a quarter of a degree.
        let celsius: f32 = thermometer.measure().to_num();
        rprintln!("{} degrees C", celsius);

        let whole = celsius as i32;
        let whole = if whole < 0 { 0 } else if whole > 99 { 99 } else { whole };
        let tens = (whole / 10) as usize;
        let units = (whole % 10) as usize;

        display.show(&mut timer, picture_of(tens), 800);
        display.clear();
        timer.delay_ms(200_u32);
        display.show(&mut timer, picture_of(units), 800);
        display.clear();
        timer.delay_ms(800_u32);
    }
}
Back to The thermometer

Example 13 ? edge-pin.rs

Send a heartbeat out of the board

The LED on the board is not the only thing the chip can drive. Pin 0 reaches the gold ring on the edge connector. Put a suitable circuit there and the same high-low rhythm becomes a signal the outside world can use.

Set the pin up as a safe, known output `board.edge.e00` is the micro:bit edge pin labelled P0. `into_push_pull_output` configures it to drive either logic level, and the `Low` argument makes its first output low. A breakout board can bring that gold ring to a more convenient socket. ### Make one slow pulse High is about 3.3 volts on the pin; low is 0 volts. The loop holds each level for one second, so someone measuring P0 sees a repeating one-second-high, one-second-low signal. ### A voltage pin is not a power supply

This example demonstrates a logic signal. Check the micro:bit pin current limits and the requirements of whatever you connect; use a driver circuit for loads such as motors. The GPIO output is for signalling, not for powering a heavy load directly.

Open the complete program and read the walkthrough
examples/edge-pin.rsComplete source
#![no_main]
#![no_std]

use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;

use cortex_m_rt::entry;
use microbit::{hal::gpio, hal::Timer, Board};
use panic_halt as _;

#[entry]
fn main() -> ! {
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);

    // Pin 0 on the edge connector (the big gold ring marked 0).
    // On your breakout board it is the row labelled P0.
    let mut pin0 = board.edge.e00.into_push_pull_output(gpio::Level::Low);

    loop {
        pin0.set_high().unwrap(); // 3.3 volts on the pin
        timer.delay_ms(1000_u32);
        pin0.set_low().unwrap(); // 0 volts on the pin
        timer.delay_ms(1000_u32);
    }
}
Back to The outside world