You have a micro:bit, a laptop, and a first program that already ran on the board.
You will be able to
Know how each chapter works, and what to trust and what to check.
This book teaches you to drive every part of the micro:bit from Rust, until you can do it from memory and teach it to someone else.
Picture this
A good mechanic does not learn a vehicle from the manual. He learns it by driving it. He turns the key, sees what happens, then opens the bonnet to find out why. After a week he can find any part with his eyes closed.
This book works the same way. Every chapter starts with a picture from the workshop. Then you run a program and something real happens on the board. Only then do we open the bonnet and explain why. The questions come last, and they only ask about things the chapter has already told you.
How a chapter runs
A picture. A workshop story, so you can see the idea before any jargon.
A program. A short Rust file, ready in your project folder. You run it and watch the board.
The explanation. What each part of the program does, in the order you read it.
From memory. A small job to rebuild the program with the book closed. This is how it sticks.
Check yourself. Questions you can answer from the chapter, and no trick questions.
What you can trust, and what you should check
Every program in this book was run through the Rust compiler's type checker, against the real libraries for the micro:bit, before it was put on the page. That catches mistakes in the way the code is written, such as a wrong name, a missing import or a mismatched type.
It does not prove the board will behave as described. The compiler that builds the final file for the chip, the programmer and the board were not in the room. So each program is honestly labelled, and if something does not behave as described, that is information. Tell me what you saw and we fix it.
Some programs are adapted from the examples in the micro:bit v2 Embedded Discovery Book, a free book by the Rust Embedded project. Those programs are marked. The explanations, pictures and questions are written for this book.
The chapters
#
Picture
What it teaches
1
The engine on the stand
The board, the chip and its memory
2
Your workbench
The project folder, and how a program gets onto the board
3
The wall of windows
Writing on the 25 LEDs
4
The paper strip
Scrolling numbers
5
The doorbell
Reading the two buttons
6
The indicator stalk
A car's turn signal and hazard lights
7
The stopwatch
Time, delays and a steady blink
8
The speaker cone
Notes and a scale
9
Talking to the laptop
Printing with RTT and the serial port
10
The spirit level
The tilt sensor
11
The thermometer
The chip's own temperature
12
The outside world
The pins on the edge
13
What is not here yet
The honest list
Say it back
Picture, program, explanation, from memory, then the questions.
A program that passes the type-check is written correctly. Only the board tells you what it does.
A micro:bit is a small board with lights and buttons on it.
You will be able to
Point at the engine, the second chip and the speaker, and say what memory the engine has.
Picture this
Bolt a small engine to a stand in the corner of the workshop. It has no seats and no steering wheel. It has one job, and it does that job the moment power arrives, every time, and it never asks a question.
That engine is your micro:bit. The thinking part is one small chip called a microcontroller. Everything else on the board is there to help that chip see, press, light up and make noise.
The front of the micro:bit v2. Everything you touch and see is here.
On the front you can see the touch logo, the grid of 25 lamps, the two buttons A and B, and the gold rings along the bottom edge. The rings are where wires can be clipped on.
The back. The chips live here. There are three black squares: the speaker, the engine, and the second chip.
On the back are three black squares. The biggest one is the speaker. The next is the engine, the chip called the nRF52833, which runs the programs you write. The smallest, the nRF52820, is a second chip that looks after power and reset, carries the serial text to your laptop, and acts as the programmer that puts your program into the engine.
Reading the number plate
Every engine has a plate. Your chip has one too, and it is tiny: N52833 QIAAA0 2024AL. If you cannot read it with your eyes, do not worry. The maker's data sheet explains the code, and the Discovery book passes it on:
The part number is a code. Each piece answers one question.
Piece
What it tells you
N52
Made by Nordic Semiconductor, in their nRF52 series
833
The part code, so this is the nRF52833
QI
The package: a small square with 73 tiny pins (called aQFN73)
AA
The variant: 512 KB of flash and 128 KB of RAM
A0
The hardware version and configuration
2024AL
A tracking code, which may differ on your chip
Flash and RAM
The chip keeps two kinds of memory, and the difference matters. Flash is the workshop manual on the shelf. It keeps your program when the power is off. RAM is the whiteboard on the wall. It is quick to write on, and it is wiped when the power goes.
The chip has far more manual than whiteboard.
So your program lives in flash, and the numbers it works on while it runs live in RAM. When you press reset, the whiteboard is wiped and the chip starts reading the manual again from the first page.
Who designed the engine?
The chip says Nordic, but the core inside it that does the calculating is called a Cortex-M4, and that name belongs to a company called Arm. Arm does not make chips. It designs processor cores and licenses the designs to makers like Nordic, who build them into their own chips. That is why the same Rust tools work on chips from many different makers.
Key idea
The micro:bit is one small chip, the engine, with helpers around it. Your program lives in the chip's flash memory, and its numbers live in RAM.
Say it back
The nRF52833 is the engine and the nRF52820 is the helper that talks to the laptop.
Flash keeps the program, RAM holds working numbers and is wiped when power goes.
You have installed the tools and run a first program from a project folder.
You will be able to
Set up one project folder for the whole book and run any program in it with one command.
Picture this
A good workshop has one bench with every tool within reach. You do not walk to another building to fetch a spanner. This book gives you one bench: one project folder with every library the micro:bit needs already listed, and one command that builds a program and puts it into the chip.
The folder
Unzip microbit-properly somewhere simple, like D:\Projects\, and open the folder in VS Code. Inside you will see:
File or folder
What it is
Cargo.toml
The shopping list. Which libraries this project uses.
.cargo\config.toml
The fitter's instructions. Which chip to build for, and which tool to use to put the program in it.
examples\
Every program in this book. One file for each.
src\main.rs
A tiny empty program, so the folder is a complete project.
Each line is a library. microbit-v2 is the dashboard for this one board. embedded-hal is the standard shape for pedals and levers. cortex-m-rt starts the program properly when the chip wakes. panic-halt says what to do if the program breaks: stop. rtt-target prints messages to your laptop. lsm303agr talks to the tilt sensor.
The first two lines say the program must be built for the engine's kind of processor, not for your laptop. The runner line is the fitter: after building, run probe-rs to put the file in the chip. This is exactly the setup you already used.
Running a program
Open the terminal inside the project folder and type one command, with the program's name at the end:
terminal
cargo run --example all-on
The very first run downloads the libraries and builds them, and it takes about a minute. Later runs reuse that work and take a couple of seconds. The program then runs on the board. To stop watching, press Ctrl+C in the terminal. The program keeps running on the chip.
If a build fails, read the first error line and the file name beside it. Copy that block to me. A compiler message is a fault code, and the first line is the one that matters.
Everything in one place
From here on, every chapter names a program and a command. The command is always the same shape, cargo run --example and then the name. Learn that shape once.
Key idea
One folder, one shopping list, one fitter. cargo run --example NAME builds the program and puts it into the chip.
Say it back
Cargo.toml lists the libraries and config.toml says how to build and fit.
cargo run --example NAME builds a program and puts it into the chip.
You can run any program in the folder with one command.
You will be able to
Draw a picture on the 25 LEDs, and draw any digit from memory.
Picture this
Imagine a wall of 25 windows, five rows of five, with a lamp behind each. A sign painter is told to write the number 7 on the wall. He does not paint anything. He decides which lamps to switch on, and the rest stay dark.
A picture on the micro:bit is exactly that: a list of which lamps are on.
The number 7 as a list of rows. A 1 is a lamp on, a 0 is a lamp off. Row 0 is the top, column 0 is the left.
First, light every lamp
Run the first program and watch the grid:
terminal
cargo run --example all-on
Program adapted from the Rust Embedded Discovery book examples.
examples/all-on.rs
#![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();
letmut timer = Timer::new(board.TIMER0);
letmut 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);
}
}
What you should see
All 25 lamps light for one second, go dark for one second, and repeat.
Reading the program, in order
Board::take() asks for the board: every pin, every part. You can take it only once, so two parts of a program cannot fight over the same pin.
Timer::new(board.TIMER0) gets a stopwatch. The display library needs one to know how long a picture should stay up.
Display::new(board.display_pins) hands the 25 lamps to the display library. It switches rows on and off so fast that your eye sees the whole picture.
every_lamp is the picture: five rows of five numbers.
display.show(&mut timer, every_lamp, 1000) shows the picture for 1000 milliseconds, which is one second.
display.clear() switches every lamp off.
Now write with it
The next program is an alphabet: ten pictures, one for each digit. It shows them one after another, 0 to 9.
The digits count 0, 1, 2 up to 9 and start again, each one for under a second. Each digit is three windows wide and sits in the middle three columns.
The alphabet is a table of ten pictures, DIGITS. Each entry is a picture exactly like the 7 above. To show digit 4, you hand the library the fifth entry, because counting starts at zero. The loop for picture in DIGITS walks down the table, one picture at a time.
That is the heart of every display you will ever build: a table of pictures, and a rule for picking which one to show.
Key idea
A picture is five rows of five numbers. Writing on the board means choosing which picture to show.
Say it back
A picture is five rows of five numbers: 1 is on, 0 is off.
display.show takes the stopwatch, a picture, and how long to show it.
Scroll a number across the grid, and say why two digits do not fit at once.
Picture this
A shop has a narrow window and a long paper banner on a roller behind it. Only a short piece of the banner can be seen at one time. To read the whole sign, the shopkeeper winds the roller a little at a time and the words slide past.
The micro:bit's grid is that window. It is five columns wide. A two-digit number is wider than that, so we put the number on a long strip and slide the window along it.
A long strip with five blank columns, the digits, and five more blank columns. The window shows five columns at a time.
A smaller alphabet
In the last chapter each digit was five rows of five numbers. That is a lot of typing, so the next program shortens each row to three bits. A row such as 0b101 means lamp on, off, on. The whole digit is five of those. The program works out the picture from it.
terminal
cargo run --example counter
Program written for this book.
examples/counter.rs
#![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.constFONT: [[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.constSTRIP_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 {
return0;
}
let w = v - 5;
let which = w / 4; // 0 = tens digit, 1 = units digit, 2 = past the endlet within = w % 4; // 0, 1, 2 = the digit's columns, 3 = the gapif which >= 2 || within == 3 {
return0;
}
(FONT[digits[which]][row] >> (2 - within)) & 1
}
#[entry]
fn main() -> ! {
let board = Board::take().unwrap();
letmut timer = Timer::new(board.TIMER0);
letmut display = Display::new(board.display_pins);
loop {
for number in0..100usize {
let digits = [number / 10, number % 10];
// Slide the window along the strip, one column at a time.for offset in0..=(STRIP_LEN - 5) {
letmut picture = [[0u8; 5]; 5];
for row in0..5 {
for col in0..5 {
picture[row][col] = strip_pixel(offset + col, row, digits);
}
}
display.show(&mut timer, picture, 120);
}
}
}
}
What you should see
The numbers 00 up to 99 scroll across the grid from right to left, one after another, and then it starts again.
Reading the program
FONT is the alphabet: ten digits, each five rows of three bits.
strip_pixel(v, row, digits) answers one question: on the long strip, in column v, is the lamp at this row on? The first five columns are blank. Then each digit takes three columns plus one blank gap. Anything past the end is blank.
The inner loop builds a picture of five by five by asking that question 25 times, with the window starting at column offset.
The loop over offset slides the window along by one column each time, and display.show(..., 120) holds each frame for 120 milliseconds.
Slide the window by a different amount and you get a different speed. Try a shorter frame time and the number rushes past.
Key idea
To show something wider than the grid, put it on a long strip and slide a five-column window along it.
Say it back
The grid is five columns wide, so a number scrolls by sliding a window along a strip.
The frame time in display.show sets the scroll speed.
Read button A and button B, and make the display answer them.
Picture this
A doorbell has two wires. While nobody presses it, the wires are apart and nothing happens. When a finger presses it, the wires touch and the circuit completes. The bell does not know who pressed it. It only knows the wires touch.
A button on a chip works the same way, with one twist. The chip pin is gently held up at high voltage. Pressing the button joins the pin to ground, which pulls it low. So on this board, pressed means low.
Not pressed, the pin sits high. Pressed, the button joins it to ground and it reads low.
The program cannot feel a finger. All it can do is ask the pin, "are you high or low?" Asking over and over, many times a second, is called polling. It is like a guard walking back and forth past a door to check whether it is open.
Turn signals on the grid
The next program does a small, useful job. Press A and a left arrow appears. Press B and a right arrow appears. Press neither and a single dot sits in the middle.
terminal
cargo run --example arrows
Program adapted from the Rust Embedded Discovery book examples.
A dot in the middle. Hold A and it becomes a left arrow. Hold B and it becomes a right arrow.
Reading the program
board.buttons.button_a and button_b are the two buttons. You take them from the board the same way as the display.
button_a.is_low().unwrap() asks "is button A pressed?" and answers true or false. The unwrap() handles a rare error by stopping, which is fine for a small program.
The loop checks A first, then B. If neither is pressed, it shows the dot.
Each pass shows a picture for 10 milliseconds, so the loop goes round about a hundred times a second. That is quick enough that the board feels instant.
Key idea
A pressed button reads low. Polling means asking the pin again and again.
Say it back
On this board a pressed button reads low.
Polling means asking a pin its state over and over.
Make arrows blink like a car's indicators, and say why counting passes of the loop works as a clock.
Picture this
On a car, the lever beside the steering wheel flicks up for the right indicator and down for the left. The lamp does not stay on. It blinks: on for about half a second, off for about half a second. Press the hazard button and both sides blink together.
We have two buttons and a grid. That is enough to make the same thing.
The problem: blinking while watching
To blink a lamp, you switch it on, wait, switch it off, wait. But if the program is busy waiting, it is not looking at the buttons. If you let go of the button in the middle of a wait, the program will not notice until the wait is over.
There is a neat trick for small programs. Do not wait for a long time. Take one short step, about ten milliseconds, and each time round the loop, count one tick. The count is your clock. When the count says it is time to switch, switch. Because each step is short, the buttons are checked a hundred times a second.
terminal
cargo run --example indicator
Program written for this book.
examples/indicator.rs
#![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 _;
constBLANK: [[u8; 5]; 5] = [[0; 5]; 5];
constLEFT: [[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],
];
constRIGHT: [[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.constHAZARD: [[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();
letmut timer = Timer::new(board.TIMER0);
letmut display = Display::new(board.display_pins);
letmut button_a = board.buttons.button_a;
letmut button_b = board.buttons.button_b;
// Each pass round the loop takes about 10 milliseconds.// We count the passes. That count is our clock.letmut 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);
}
}
What you should see
Hold A and a left arrow blinks, on for about 0.4 of a second and off for the same. Hold B and a right arrow blinks. Hold both and the two arrows blink together, like hazard lights.
Reading the program
ticks counts passes round the loop. Each pass takes about 10 milliseconds.
(ticks / 40) % 2 == 0 is true for 40 passes, then false for 40 passes, then true again. That is 400 milliseconds on and 400 off.
The match looks at both buttons at once. Left alone and lit gives the left arrow. Right alone and lit gives the right arrow. Both and lit gives the hazard picture. Anything else gives a blank grid.
wrapping_add(1) adds one, and if the count ever reaches its maximum, it quietly starts again at zero instead of crashing.
Where this method runs out
This works for small jobs. As soon as the program has many things to do, each with its own timing, counting passes by hand becomes tangled. Later you will meet a better way, where the chip itself taps the program on the shoulder when something happens. That is called an interrupt. For now, notice what the trick teaches: a program has no clock of its own, so it makes one by counting.
Key idea
Take short steps and count them. The count is your clock, and the buttons stay watched the whole time.
Say it back
Counting short steps gives the program a clock while it keeps watching the buttons.
Use the timer to wait exactly, and blink a single lamp at a steady rate.
Picture this
A mechanic who checks a tightening sequence uses a stopwatch, not a guess. "Hold for ten seconds." He does not count in his head. He starts the watch, glances at it and stops when it says so.
The chip has stopwatches built into it, called timers. They count steadily whatever the program is doing. A wait that uses a timer is exact. A wait that uses a loop which counts is only as exact as how fast the loop happens to run.
The lamp at the crossing
The simplest light on the grid is the top-left one. It sits where row 1 crosses column 1, like a lamp at a crossing of two streets. A lamp lights when its row is driven high and its column is driven low. This program drives the column low and then switches the row on and off every half second.
terminal
cargo run --example blink
Program adapted from the Rust Embedded Discovery book examples.
examples/blink.rs
#![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();
letmut timer = Timer::new(board.TIMER0);
// The top-left LED sits where row 1 meets column 1.// A lamp lights when its row is high and its column is low.letmut row1 = board
.display_pins
.row1
.into_push_pull_output(gpio::Level::Low);
let _col1 = board
.display_pins
.col1
.into_push_pull_output(gpio::Level::Low);
loop {
row1.set_high().unwrap();
timer.delay_ms(500_u32);
row1.set_low().unwrap();
timer.delay_ms(500_u32);
}
}
What you should see
The top-left lamp is on for half a second, off for half a second, and repeats.
Reading the program
board.display_pins.row1 and col1 are the two wires that meet at the top-left lamp.
into_push_pull_output(gpio::Level::Low) turns a pin into an output, a switch your program controls, and sets its starting level.
row1.set_high() puts the row at full voltage and the lamp lights. set_low() puts it at zero and the lamp goes out.
timer.delay_ms(500_u32) waits exactly 500 milliseconds on the timer.
Notice that this program does nothing else while it waits. It cannot look at the buttons during a wait. That is why the last chapter used the counting trick. Which method to use depends on the job: a pure blink can just wait, and a program that must stay watchful cannot.
Key idea
A timer gives exact waits. A pin set as an output is a switch your program controls.
Say it back
A timer gives exact waits.
An output pin is a switch your program controls, high or low.
Hold a tin lid against your chest and tap it quickly with a finger. The lid moves in and out. The air moves with it. If you tap fast enough, your ear hears a note.
The micro:bit's speaker is a small cone that moves in and out. When the pin is high, the cone is pushed out. When the pin is low, the cone is pulled in. Do that a few hundred times a second and you get a note.
The pin goes high and low. The cone follows it, and the air moves.
The number of pushes and pulls per second is the pitch, measured in hertz. The ordinary A above middle C is 440 hertz: 440 pushes and 440 pulls every second.
One note
terminal
cargo run --example tone
Program adapted from the Rust Embedded Discovery book examples.
examples/tone.rs
#![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.constHALF_CYCLE_US: u32 = 1136;
constCYCLES: u32 = 440 * 2; // two seconds
#[entry]
fn main() -> ! {
let board = Board::take().unwrap();
letmut speaker = board.speaker_pin.into_push_pull_output(gpio::Level::Low);
letmut timer = Timer::new(board.TIMER0);
for _ in0..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();
}
}
What you should see
A steady note, the A above middle C, for two seconds, and then silence.
Reading the program
One cycle of 440 hertz lasts about 2273 microseconds. A microsecond is a millionth of a second. Half of that is 1136 microseconds, which is the time for each push and each pull.
The loop pushes the cone out, waits half a cycle, pulls it in, waits half a cycle, 880 times. That is two seconds of note.
The last loop with asm::wfi() parks the chip asleep once the note is done. A program stored in flash starts again from the top every time the board is powered or reset, so this keeps it from running off the end of main.
A whole scale
A function lets you write the "play one note" job once and use it for every note. The scale program plays do, re, mi, fa, sol, la, ti, do.
terminal
cargo run --example scale
Program written for this book.
examples/scale.rs
#![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 doconstSCALE: [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: &mutP, timer: &mutT, hz: u32, ms: u32) {
let half_us = 500_000 / hz;
let cycles = hz * ms / 1000;
for _ in0..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();
letmut speaker = board.speaker_pin.into_push_pull_output(gpio::Level::Low);
letmut timer = Timer::new(board.TIMER0);
for hz inSCALE {
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();
}
}
What you should see
Eight rising notes, each for 400 milliseconds with a short gap, and then silence.
The function play takes the speaker, the timer, the pitch in hertz, and how long to play for. It works out the half cycle for itself, 500_000 / hz microseconds. Change the numbers in SCALE and you change the tune.
The speaker on this board is a simple on-off kind: the cone is either fully out or fully in. That is why the sound is bright and buzzy and cannot reproduce speech or music with soft tones. That is the speaker, not your program.
Key idea
Pitch is how many pushes and pulls per second. Half a cycle is the wait between a push and a pull.
Say it back
Pitch is the number of pushes and pulls per second, in hertz.
The half cycle is 500,000 divided by the pitch, in microseconds.
You can make the board act, but you cannot yet hear it say anything.
You will be able to
Print messages from the board to your laptop in two ways.
Picture this
A mechanic listens to an engine, but a good mechanic also has a scan tool that tells him what the engine thinks. The engine cannot talk, so the scan tool reads its numbers and writes them on a screen.
Your board can write numbers and words to your laptop in the same way. There are two routes, and each is useful for a different reason.
Route one: RTT, the quick line
RTT is a way for the program to drop short messages into a small memory buffer, and for the programmer tool to read them out through the same two wires used to load the program. There is no extra cable and no extra setup. When you start the program with cargo run, the messages show up in your terminal.
terminal
cargo run --example hello-rtt
Program written for this book.
examples/hello-rtt.rs
#![no_main]
#![no_std]
use cortex_m::asm::wfi;
use cortex_m_rt::entry;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
#[entry]
fn main() -> ! {
rtt_init_print!();
rprintln!("Hello from the micro:bit");
letmut count: u32 = 0;
while count < 5 {
count += 1;
rprintln!("count is {}", count);
}
loop {
wfi();
}
}
What you should see
In the terminal: the line "Hello from the micro:bit", then "count is 1" up to "count is 5".
rtt_init_print!() sets the line up. rprintln! works like the println! you know from ordinary Rust, with curly brackets for the values. The messages are for you while you work. Nobody sees them once the board runs alone.
Route two: the serial port
The serial port is a true line from the engine to your laptop, through the second chip and the USB cable. A program on your laptop, such as PuTTY, shows what arrives. Unlike RTT, serial is the same line a finished product might use to talk to a computer.
Text goes from the engine, through the second chip, over USB, and into a terminal program.
On Windows, open a terminal and type mode before you plug the board in, and again after. The new COM port that appears, such as COM3, is the board. Open PuTTY, choose Serial, type that COM name, and set the speed to 115200, with 8 data bits, 1 stop bit, no parity and no flow control. Click Open.
terminal
cargo run --example hello-serial
Program adapted from the Rust Embedded Discovery book examples.
examples/hello-serial.rs
#![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();
letmut timer = Timer::new(board.TIMER0);
// The UART is the serial port. The second chip carries it to the laptop over USB.letmut serial = uarte::Uarte::new(
board.UARTE0,
board.uart.into(),
Parity::EXCLUDED,
Baudrate::BAUD115200,
);
letmut 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();
}
}
What you should see
In the PuTTY window, a new line every second: "line 0 from the micro:bit", "line 1 ...", up to line 9, then no more. Press the reset button on the back to run it again.
Uarte::new builds the serial port. The speed, 115200, is called the baud rate: the number of signal changes per second. Both ends must agree on it, or you get nonsense. writeln! writes a line of text to the port, with values in curly brackets like println!.
Start PuTTY before you flash the program. The serial line does not keep old messages. If PuTTY is not listening when the board speaks, the words are gone.
Key idea
RTT is quick and shows up in your terminal while you work. Serial is a real line to the laptop and needs a program such as PuTTY to read it.
Say it back
RTT prints to the terminal while you work. Serial goes through the second chip to PuTTY.
You can print numbers to your laptop and show a picture on the grid.
You will be able to
Read the tilt sensor and make a lamp slide like the bubble in a spirit level.
Picture this
A builder's spirit level has a small bubble in a tube of liquid. Tilt the level and the bubble drifts to the high end. When the surface is flat, the bubble sits in the middle.
The micro:bit has a sensor inside it that feels which way is down. We will use it to make one lamp on the grid behave like that bubble.
The sensor and the two wires
The sensor is a small chip called the LSM303AGR. It can feel tilt and shake, and it can feel the direction of north. It is a separate chip from the engine, so the engine has to ask it questions. They talk over a pair of wires called I2C. One wire, the clock, ticks like a metronome. The other wire carries the data, one bit on each tick.
Two wires carry the whole conversation between the engine and the sensor.
You do not have to build the conversation by hand. A ready-made driver library, lsm303agr, knows the sensor's language. Your program only says: start the sensor, then give me the acceleration.
terminal
cargo run --example tilt
Program adapted from the Rust Embedded Discovery book examples.
Adapted from the Discovery book's show-accel example, with a spirit-level display written for this book.
examples/tilt.rs
#![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 windowlet cell = 2 + step;
if cell < 0 {
0
} elseif cell > 4 {
4
} else {
cell asusize
}
}
#[entry]
fn main() -> ! {
let board = Board::take().unwrap();
letmut timer = Timer::new(board.TIMER0);
letmut 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);
letmut 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.letmut picture = [[0u8; 5]; 5];
let col = to_cell(x);
let row = to_cell(y);
picture[row][col] = 1;
display.show(&mut timer, picture, 20);
}
}
}
What you should see
One lit lamp. Flat on the table, it sits in the middle. Tilt the board and the lamp slides toward the low side.
Reading the program
Twim::new(board.TWIM0, board.i2c_internal.into(), K100) builds the two-wire link to the sensor, running at 100,000 bits per second.
Lsm303agr::new_with_i2c(i2c) and sensor.init() start up the driver and the sensor. set_accel_mode_and_odr chooses how precise and how often to measure: here, high resolution, 50 times a second.
sensor.acceleration().unwrap().xyz_mg() gives three numbers: how hard the sensor is being pulled in the x, y and z directions, in thousandths of the pull of gravity. On a flat table, x and y are near zero and z is near 1000, because gravity pulls straight down through the board.
to_cell turns a tilt number into a grid position from 0 to 4. Every 200 units of tilt moves the lamp one window, and the result is held between 0 and 4.
I do not know from here which way each axis points on your board. If the lamp slides the wrong way when you tilt, put a minus sign in front of x or y. Finding and fixing that is a good first experiment.
Key idea
The sensor is a separate chip. The engine asks it questions over two wires, and a driver library does the talking.
Say it back
I2C uses a clock wire and a data wire.
Acceleration comes in thousandths of gravity: flat means x and y near zero.
You can show digits on the grid and read a number from a part of the chip.
You will be able to
Read the chip's temperature and show it on the grid.
Picture this
Many engines have a temperature sender screwed into the block. It does not measure the air in the workshop. It measures the engine itself. If the engine has been working hard, the reading is high.
The nRF52833 has a thermometer built into the silicon. It measures the chip's own temperature. On a board that has been sitting idle, that is close to the air around it. On a board that has been working hard, it is warmer.
terminal
cargo run --example temperature
Program written for this book.
examples/temperature.rs
#![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.constFONT: [[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] {
letmut picture = [[0u8; 5]; 5];
for row in0..5 {
for col in0..3 {
picture[row][col + 1] = (FONT[digit][row] >> (2 - col)) & 1;
}
}
picture
}
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = Board::take().unwrap();
letmut timer = Timer::new(board.TIMER0);
letmut 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.letmut 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 asi32;
let whole = if whole < 0 { 0 } elseif whole > 99 { 99 } else { whole };
let tens = (whole / 10) asusize;
let units = (whole % 10) asusize;
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);
}
}
What you should see
In the terminal, a line such as "24.5 degrees C" every few seconds. On the grid, the tens digit, a pause, then the units digit, then a longer pause.
Reading the program
Temp::new(board.TEMP) takes the thermometer. thermometer.measure() asks it for a reading.
The reading comes in steps of a quarter of a degree, and .to_num() turns it into an ordinary number with a decimal point.
rprintln! prints it for you to read in the terminal.
The digits are split into tens and units, and each is drawn from the same alphabet idea as chapter 4: a table of pictures and a rule for picking one. picture_of builds a picture from three-bit rows.
This is the temperature of the chip and not a laboratory instrument. Do not use it to set a medical or safety limit. It is good for watching how a board warms and cools.
Key idea
The chip senses its own temperature, in quarter-degree steps. The table-of-pictures idea shows any number on the grid.
Say it back
The thermometer measures the chip itself, in quarter-degree steps.
You can switch a pin high and low, and you have a breakout board with labelled pins.
You will be able to
Drive a pin on the edge connector, and say what a pin can and cannot carry.
Picture this
Every machine eventually has to touch something that is not itself. A lamp, a relay, a sensor, a motor. The edge of the micro:bit, the gold rings and the row of tiny connections beside them, is where the engine reaches out.
Your breakout board takes each of those connections and brings it out to a row of sockets in groups of three, one for the signal, one for power and one for ground. The labels printed on the back, such as P0, P1, P2 and so on, are the names the program uses.
terminal
cargo run --example edge-pin
Program written for this book.
examples/edge-pin.rs
#![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();
letmut 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.letmut 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);
}
}
What you should see
Pin 0 goes to 3.3 volts for one second, then to 0 volts for one second, repeating. You can see it with a small lamp and a resistor between the P0 signal socket and ground, or with a multimeter on the pin.
Reading the program
board.edge.e00 is edge pin 0, the big gold ring marked 0. Your breakout labels it P0.
into_push_pull_output turns it into a switch your program controls, just like the lamp pin in chapter 7.
set_high() puts 3.3 volts on it and set_low() puts 0 volts on it.
What a pin can and cannot do
A pin is a whisper, not a shout. It can switch a small lamp with a resistor, or tell something bigger to switch on. It cannot carry the current of a motor, a relay coil or a horn. If you connect one of those directly, you can damage the chip.
So the pin whispers to a driver, and the driver, with its own power supply, does the heavy work. That is the way every big machine in this book will be moved. Three rules go with it:
The heavy load gets its own supply, and never draws from the micro:bit.
The driver and the micro:bit must share a ground, or the whisper cannot be understood.
Do not put 5 volts on any signal pin. The micro:bit's pins work at 3.3 volts.
Key idea
A pin carries a signal, not a load. Drivers do the heavy work, and every circuit shares a ground.
Say it back
A pin carries a signal. A driver with its own power carries the load.
You have driven the lights, buttons, timer, speaker, serial port, tilt sensor, thermometer and edge pins.
You will be able to
Know what is missing from this book, and why.
An honest book lists what it has not covered. Here is the list.
Picture this
A new vehicle comes with a list of features. A careful dealer shows you what he has driven and tested, and tells you plainly which ones he has not.
Part of the board
Why it is not here yet
The microphone
It needs the chip's analog input and a power pin for the microphone. I could not find a tested example to adapt, and I will not guess.
The touch logo
The gold logo senses a finger with a touch circuit. I could not find a tested example for it in these libraries.
The compass
It uses the same sensor and the same two wires as the tilt sensor, but it needs calibration to give a true heading. It comes next.
The radio
One micro:bit talking to another. A chapter of its own, after the others are solid.
Interrupts
The chip tapping the program on the shoulder. This is the better answer to the problem in chapter 6, and it is the next big idea.
Motors
Needs a driver board with its own power. We start with the signals in chapter 12 and build on them.
How to know you are ready
The test is simple. For each chapter, can you rebuild its program from a blank file without opening the book? Can you draw its picture for a technician? Can you break it on purpose and say why it broke? If you can, you can teach it.
Key idea
This book covers what it has tested. The rest is on the list, and it gets done properly.
Say it back
Microphone, touch logo, compass, radio, interrupts and motors come next.