The Phōs project
Phōs is a very simple embedded OS that is used for teaching the first year course on Digital Systems. Unlike most other embedded operating systems, it is based on the single synchronisation primitive of synchronous message passing, with all interrupts converted to messages.
The project repository can be found at https://bitbucket.org/Spivey/phos.
At present, Phōs is specific to the BBC micro:bit, and has drivers for some of the peripheral devices on that board, including the timers and serial port. Drivers for other simple devices, such as the die thermometer and the random number generator, make good exercises for the labs associated with the course. Potential projects include:
- Write device drivers for other devices on the micro:bit.
- The I2C interface [Done], and the accelerometer [Done, both kinds] and magnetometer attached to it.
- An attached display device, driven over I2C or SPI. Perhaps a 16x2 lcd with attached microchip port expander (really needs I2C level shifter), a 128x64 lcd with AVR driver, or a fancy SPI-driven TFT.
- The on-board packet radio interface in Nordic proprietary mode. (Bluetooth Low Energy mode requires a closed-source driver called a 'SoftDevice' that we probably don't want to tangle with. But there may be open-source alternatives?)
- The micro:bit samples contain simple programs for transmitting and receiving packets, a good place to start for incrementally replicating the functions.
- A bit-banging implementation of the protocol for WS2812 'neopixels' [Done, using assembly language with interrupts disabled]. An interrupt-driven version using the SPI hardware would be a demanding test for lightweight high-priority interrupts, but is probably beyond what can be done. On the Adafruit boards, the same effect is achieved with looping DMA from a buffer that can be updated while DMA is active.
- We could see whether there are components of the MyNewt project that could be re-purposed.
- The SAM D21 based Adafruit boards can do hardware-supported USB, and it would be good to make an implementation of a CDC serial port.
- Improve the API for Phōs. Desirable changes include:
- Delegating all management of the NVIC to the operating system and OS-provided subroutines. Note that we don't really need priorities, so can simplify at the same time. [Done]
- Providing for timeouts on hardware operations [Done, via general version of
receivewith timeout]. For example, we might provide a special form of
sendto be used by a clock process that allows
TIMEOUTmessages to be sent as if by hardware, so that
receive(HARWARE, &m)calls would be completed by a timeout. This is an important pre-requisite for reliable implementation of bus protocols like I2C that may suffer from unpredictable lock-ups.
- Implementing checks for stack overflow on each context switch.
- If the number of processes is limited, using a bitmap to represent the set of acceptable processes in a call to
receive[Ben White-Horne worked on this].
- Making as much as possible of the kernel interruptible, and allow special-purpose interrupt handlers with high priority.
- Making interrupt handlers perform only a partial context save, and provide a
PendSVhandler so that they can request a context switch [Done]. A handler can send a message by modifying the ready queue (with interrupts disabled), then requesting a
PendSVinterrupt to activate the scheduler.
- Port Phōs to another board. Top options are boards that are supported by the MBED ecosystem, which provides working code samples (Blinky and Echo) for all supported boards.
- The Freescale FRDM-KL25Z and FRDM-K64F boards.
- ST Microelectronics Nucleo boards, like the Nucleo-F091RC (Cortex-M0, 32kB RAM) and a selection of M4-based boards.
- ARM-based Arduino boards.
- Adafruit's Cortex-M0 boards, including the M0 Trinket and the Metro M0 Express.
- First step: provide a driver for the UART; second step: iumplement serial-over-USB, using code culled from either the AdaFruit driver or (perhaps better) the TinyUSB stack.
- Paul Stoffregen's Teensy boards: a non-MBED platform that also supports drag-and-drop programming over USB.
- It might be best to begin with an MBED board based on a Cortex-M0 processor, and if that goes well, move to an -M3 or -M4 board in the same family. A processor with floating point will require us to save the floating point registers on context switch, so brings with it unattractive complications.
- Provide debugging tools.
- Add whatever is needed to make
gdbaware of the existence of multiple threads.
panica bit so it prints the name of the current thread [Done].
- Add whatever is needed to make
- Vanity projects
- There's a version of the
ppccompiler from the second-year Compilers course that can generate Thumb code. Integrate it with Phōs so client programs can be (partially) written in a Pascal dialect. Provide suitable language support for varying message formats.
- There's a version of the
(In the list above, [Done] means that Mike has worked on the problem, but usually the result still needs packaging and documenting.)
For development, it will be easiest to take the Phōs source and edit it as necessary. In a later stage of the project, we will want to merge the sources for different processors and boards so that the common parts are shared, isolating the parts that are board-specific, and providing multiple implementations of device driver processes so that Phōs applications are portable between boards.
Other boards don't have the switches and lights of the micro:bit but some, including the FRDM boards, support Arduino-compatible add-on 'shield' boards, and we could easily make a shield that replicates the features of the micro:bit.
A dire warning
The micro:bit bootloader expects files in Intel hex format (extension
.hex). The bootloader on the FRDM-KL25Z expects binary images (extension
.bin). If you upload an Intel hex file to the board, it is likely to be interpreted as a binary file and written to the flash directly. Your program will not work! But worse than that, some bits of the image are interpreted as protection bits for the flash memory, and if you are unlucky, you will have bricked the board.
I have succeeded in unbricking at least one board, but after multiple inconclusive experiments, I don't know which method actually did the job. It's worth trying the PyOCD tool with the command
pyocd-flashtool -ce to erase the chip. Another apparently-bricked board was resurrected just by uploading a valid binary image.
The latest (May 2014) firmware for the KL25Z apparently tries to avoid locking the board, so it's worth making sure it is installed. You can do so by opening the file
mbed.html on the board using a text editor. The file starts with a comment that contains the firmware version.
||The initial micro:bit implementation of Phōs.|
||A minimal program for the KL25Z that echoes characters on the UART over USB. Ripped from MBED code, then iteratively simplified.|
||A bit-banging driver for neopixels on the micro:bit.|
API for timeouts
There is a new version of
receive that takes a timeout value in milliseconds:
void receive_t(int src, message *msg, int timeout)
If no other message is received before the timeout happens, then a message is delivered with source
HARDWARE and type
TIMEOUT. The timeout is automatically cancelled on delivery of a genuine message, whether from
HARDWARE (i.e., an interrupt) or from a normal process. The source filter
src may have any value, not just
ANY; any timeout message will come from
HARDWARE, but is delivered even if the
receive call specified a different process – this is more useful than the alternative, because it allows us to wait for a limited time for a message from a specific process, ignoring others.
A timer process can periodically invoke a new system call,
void tick(int ms)
ms is the number of milliseconds since the last invocation of
tick. This increments all active timeout clocks by the specified amount, and delivers
TIMEOUT messages to any that have gone off, making the corresponding processes ready to run. There is no need for an immediate context switch, since responding to a timeout presumably has a low priority.
Any Phōs program that uses timeouts will need a timer process to trigger them, but programs that do not use timeouts can still be written with no need for a timer process. The guarantees are weak: a
receive_t will time out eventually (providing there is a timer process continually calling
tick), and it will do so in a time that is at least the one specified. In order to prevent premature timeouts, it will be best to deliver the message on the tick following the one where the timeout value is reached: that way, the message cannot be delivered early because of an initial short tick. We might allow
timeout=0 as a special case, meaning to deliver a message if one it waiting, otherwise to deliver an immediate timeout.
Ticks can be relatively infrequent, so it is OK to implement
tick() by looping over all processes checking for timeouts. Note that (depending on the device) an interrupt can arrive after the timeout message has been delivered, either before the process has been scheduled, or afterwards while it is dealing with the timeout; in either case, the interrupt will be queued and delivered on the next
receive. So it might be necessary to write the device driver carefully, so that unexpected interrupts are ignored.
- Concise documentation of the Phōs API.
- A page about context switch for the Cortex-M3.
- Datasheet and reference manual for the FRDM-KL25Z.
- An app note about interrupt-driven I2C in Kinetis microcontrollers.
- The truth about timing for neopixels.
- A note explaining that empty I2C writes are not supported on the Nordic chip.
The simulation of a complex protocol by setting the values on GPIO pins under direct program control, with delay loops for timing. For example, serial communications require timings that are more precise that can be achieved by bit-banging, unless the processor is doing nothing else.
A symbolic representation of the machine code for a program.
(Nested Vector Interrupt Controller). An ARM processor component that is able to assign priorities to external interrupts (as opposed to those generated by internally by the processor) and control the servicing of interrupts. As the name indicates, it is able to cope with nested interrupts, where servicing of one interrupt is itself interrupted by another with higher priority. Note that, according to ARM conventions, higher priorities are indicated by lower numbers.
(Universal Asynchronous Receiver/Transmitter). A peripheral interface that is able to send and receive characters serially, commonly used in the past for communication between a computer and an attached terminal. It is commonly used in duplex mode, with the transmitter of one device connected to the receiver of the other with one wire, and the receiver of the one connected to transmitter of the other with a different wire. The asynchronous part of the name refers to the fact that the transmitter and receiver on each wire do not share a common clock, but rely instead on the signalling protocol and precise timing to achieve synchronisation.
An alternative instruction encoding for the ARM in which each instruction is encoded in 16 rather than 32 bits. The advantage is compact code, the disadvantage that only a selection of instructions can be encoded, and only the first 8 registers are easily accessible. In Cortex-M microcontrollers, the Thumb encoding is the only one provided.
see Read-only memory.