micro:bian devices (Digital Systems)

Copyright © 2024 J. M. Spivey
Jump to navigation Jump to search

The core of micro:bian is a process scheduler and message-passing mechanism that supports device drivers for peripherals. Outside the core, drivers for various peripheral devices on the micro:bit are provided, as listed on this page. Most devices have their own driver process that can be started from the init function of an application, though simple devices attached to the I2C bus share a single driver that looks after the bus itself.

Each device supports both a message interface (in which requests are sent as messages to the driver process, and it sends back replies), and a function interface for simple clients, where the message and reply are bundled together in a simple function that the client can call.


Message interface:

  • The serial device driver SERIAL accepts messages of type PUTC with a field m_i1 containing a character to be printed; no reply is sent. The device driver maintains an internal buffer of output characters so as to allow overlap between output and computation. It translates line feed characters in the output into carriage return / line feed sequences.
  • The device driver also accepts messages of type GETC, and responds to each request with a message of type OK with a field m_i1 that contains an input character. The driver itself performs echoing and line editing in response to backspace characters typed on the keyboard; it makes input available to clients only when return is pressed.
  • For use by our implementation of printf, the device driver also accepts PUTBUF messages that contain a pointer to a string buffer and a character count. All the characters in the string are transferred at once to the driver's internal buffer.

As an implementation restriction, at most one GETC request may be outstanding at once: there is no queue for processes waiting to input. While one process is waiting to input, however, other processes may still send PUTC messages to print output.

Function interface: There is a function

void serial_putc(char ch);

that prints a single character by sending a PUTC message to SERIAL, and a function

char serial_getc(void);

that requests and returns an input character. Also, the function printf defined in lib.c prints its output by calling the function

void print_buf(char *buf, int n);

that is defined here.

Programs that use the serial interface should start the driver process by calling

void serial_init(void);

from the init function.


Clients of the timer driver TIMER may either request a single message sent after (at least) a specified delay has elapsed, or request a repetitive message sent at regular intervals. The timer has a resolution of 5 milliseconds.

Message interface: The driver process accepts messages of type REGISTER with a field m_i1 containing a delay in milliseconds, and a field m_i2 that contains a boolean, indicating whether the timer is repetitive. The driver sends a reply of type PING after the delay has ellapsed, and if the timer is repetitive, it continues to send PING messages at regular intervals thereafter.

Function interface: There is a function

void timer_delay(int ms);

that returns after a one-shot delay of the specified length in milliseconds. A different function

void timer_pulse(int ms);

requests that regular PING messages should be sent from the timer to the calling process at an interval specified in milliseconds.

Clients of the timer must be ready to receive PING messages when they are sent, or other clients of the timer will be delayed. This is automatic when the function timer_delay is called, because the calling process is suspended as it waits for the PING message to be sent in reply.

Programs that use the timer should start the driver process by calling

void timer_init(void);

from the init function.


The i2c module provides both a process that can carry out transactions on the I2C bus, and also functions that use the bus to communicate with the accelerometer on the micro:bit board.

I2C devices often have multiple registers that can be read by a transaction that involves writing the register address, then reading its value, using an I2C feature called a repeated start condition. The function

int i2c_read_reg(int addr, int cmd);

performs such a transaction. The parameter addr is the I2C address of the device, and cmd is the register address within the device: both single bytes, as is the result. Such devices often support writes to registers, mediated by the function

void i2c_write_reg(int addr, int cmd, int val);

that carries out a single write transaction.

Both the functions above are defined in terms of a function that performs a general I2C transaction:

int i2c_xfer(int kind, int addr, byte *buf1, int n1, byte *buf2, int n2);

The parameters are as follows:

  • kind is either READ or WRITE.
    • if kind = READ and n1 = 0, a read transaction is performed.
    • if kind = READ and n1 > 0, a write transaction is followed, via a repeated start, by a read transaction for the same device.
    • if kind = WRITE, a write transaction is performed, with n1 >= 0 bytes from buf1 followed by n2 > 0 bytes from buf2.
  • addr is the 7-bit address of an I2C device.
  • buf1 is a buffer containing n1 >= 0 bytes that are written to the device.
  • buf2 is a buffer that either (if kind = WRITE) contains n2 > 0 bytes that are written after the bytes from buf1, or (if kind = READ) receives n2 > 0 bytes read from the device following a repeated start condition.

The value returned is OK in the case of a successful transaction, and ERROR in case of an I2C error.

Message interface: the driver process I2C accepts messages of type READ or WRITE, with fields m_b1, m_b2 and m_b3 corresponding to the parameters addr, n1 and n2 of i2c_xfer, plus fields m_p1 and m_p2 corresponding to buf1 and buf2. The reply is a message of type OK or ERROR; if there is an error, the field m_i1 additionally contains the value found in the I2C_ERRORSRC register, documented in the nRF51822 hardware manual.

Programs that use i2c should start the driver process by calling

void i2c_init(void);

from the init function.


The micro:bit has a 3-axis accelerometer accessible over I2C: either a separate chip, or a single chip containing both the accelerometer and a magnetometer (see the micro:bit page). The i2c module provides functions for accessing the accelerometer: the function

void accel_start(void)

initialises the accelerometer, and starts it taking readings 50 times per second. It detects automatically which sensor chip is installed on the board. Subsequently, the function

void accel_read(int *x, int *y, int *z)

fetches the most recent accelerometer reading, storing the X, Y and Z values in variables x, y and z as 8-bit samples in the range −127..127. The values are scaled so that an acceleration of 1g is shown as 64, and the signs are such that, with the LED matrix uppermost and the USB connector away from the viewer, the Z value is positive, and the X and Y values become positive if it is tilted to the left and tilted away from the viewer, respectively.

Both functions should be called from a process in the micro:bian program, not from init. This is necessary because even accel_start must use the driver process to communicate over the I2C bus.


The micro:bit also has a 3-axis magnetometer on the I2C bus, either as a separate chip or integerated with the accelerometer. At the moment, micro:bian provides no driver for it, but writing one should be straightforward. It could be implemeted entirely with subroutines, using the I2C process to enforce mutual exclusion between processes wishing to read the magnetometer, and between those processes and others wishing to use the I2C bus.


The radio module uses the 2.4GHz packet radio on the NRF51822 to communicate with other micro:bits in the vicinity. The packet format that is used is compatible with the datagrams supported by the official micro:bit runtime. The programming interface supports arbitrary broadcast packets of up to 32 bytes, and the driver process augments these with the same headers as are used by the official runtime before transmission.

Message interface: The RADIO process accepts messages of type SEND or RECEIVE, each with a field m_p1 containing the address of a buffer containing a payload. A SEND message may have a payload of up to 32 bytes, and a second field m_i2 gives its length. A RECEIVE message should specify a buffer of 32 bytes, big enough to store the payload from any message that might be received. The RADIO process replies to these messages once the packet has been sent or received; the reply for a SEND message is OK, and for a RECEIVE message is a message of type PACKET with a field m_i1 that gives the size of the payload. Only one client may be waiting to receive at a time, and in both cases the buffer is owned by the RADIO process until it sends the reply.

Function interface: Two functions

void radio_send(void *buf, int n);
int radio_receive(void *buf);

provide a simple equivalent to the message-based interface. The function radio_receive blocks until a packet is received, stores the payload in the 32-byte buffer buf, and returns the size of the payload in bytes.

Programs using the radio interface should call

void radio_init(void);

from the init function to start the driver process.

Analog-to-digital converter

The nRF51822 has a multi-channel ADC, capable of digitising a voltage taken from one of a several sources. The driver process ADC accepts messages of type REQUEST with a field m_i1 that selects a channel. Pin 2 on the micro:bit edge connector corresponds to channel 2 of the ADC. The driver process responds with a message of type OK, with a field m_i1 containing the reading, an integer between 0 and 1023.

The driver sets up the ADC so that it uses the positive supply rail Vdd as a reference and produces a 10-bit result. Using Vdd as a reference works well for measuring the position of a potentiometer wired across the rails. The driver process takes care to delect the input channel after obtaining a reading so as not to interfere with possible other uses of the same pin.

A function

int adc_reading(int chan);

is provided for use by client processes: it takes a channel number as parameter and returns a reading taken from that channel. Programs using the ADC service should call

void adc_init(void);

from the init function to start the driver process.

Other devices

The nRF51822 provides a hardware random number generator, and also a thermometer that measures the temperature of the microcontroller die. It is not hard to write device driver processes for these, and this is suggested as an exercise as part of Lab 4 in the Digital Systems course.