Note: The mercurial server is disabled at the moment while I investigate whether it can run with an acceptably low CPU load – Mike.
micro:bian devices (Digital Systems)
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.
Serial
Message interface:
- The serial device driver
SERIAL
accepts messages of typePUTC
with a fieldm_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 typeOK
with a fieldm_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 acceptsPUTBUF
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.
Timer
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.
I2C
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 eitherREAD
orWRITE
.- if
kind = READ
andn1 = 0
, a read transaction is performed. - if
kind = READ
andn1 > 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, withn1 >= 0
bytes frombuf1
followed byn2 > 0
bytes frombuf2
.
- if
addr
is the 7-bit address of an I2C device.buf1
is a buffer containingn1 >= 0
bytes that are written to the device.buf2
is a buffer that either (ifkind = WRITE
) containsn2 > 0
bytes that are written after the bytes frombuf1
, or (ifkind = READ
) receivesn2 > 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.
Accelerometer
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.
Magnetometer
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.
Radio
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.