GalagoAPI Reference - OutbreakInc/Galago GitHub Wiki
GalagoAPI.h is the primary programming interface (library) for Galago and contains both software and hardware device abstractions. To use:
#include <GalagoAPI.h>
using namespace Galago;
For the most up-to-date reference, please see GalagoAPI.h
Concepts
GalagoAPI is built on a few concepts that may be familiar to you as a developer on other APIs or in other programming languages.
API Objects
GalagoAPI uses two main API singletons (objects of one global instance)
system
and io
. system
is in charge of memory, time, clocks and
Tasks. io
handles all IO peripherals, like the UART, I2C, SPI,
ADC and GPIO.
Several other objects, like Tasks and Buffers, are used throughout the API and are implemented as reference objects, so they may be passed by value without concern for memory management. This is particularly useful for Buffer objects.
Tasks
A fundamental concept in GalagoAPI is the Task, which is a type of
promise, a value that represents ongoing work until it is completed.
When completed, a task becomes either successful or failed. Many
asynchonous methods return Tasks, which indicate that they're waiting
for hardware features in the background. For a full description of
Tasks, see the system
API below.
Tasks represent asynchronous work that has either not yet completed,
has succeeded or has failed. Tasks are a type of promise that make
writing asynchronous code simple while being immune to race conditions,
deadlocks and other nuisances when used correctly.
Classes
Galago Task object
Tasks represent asynchronous work that has either not yet completed, has succeeded or has failed. Tasks are a type of promise that make writing asynchronous code simple while being immune to race conditions, deadlocks and other nuisances when used correctly.
Creating and passing Tasks
Tasks are reference objects so you don't need to worry about how they
refer to state. To create tasks, use the System::createTask()
method (see System API documentation) and pass and assign them as
normal value objects:
Task t; // null reference
t = system.createTask();
Task t2 = t;
someMethod(t2);
t = doAsyncAction(500);
Tasks may be compared with the == and != operators. This compares references and, by extension, value:
if(t == Task()) // is t a null Task reference?
You may combine Tasks to create a new Task that's only complete when all its constituent Tasks are complete, or is considered failed when the first of its constituent Tasks fails:
Task t3 = t + t2;
system.when(t3, &someAction);
Please find the main Task documentation in the System API.
Galago CircularBuffer object
CircularBuffer is a utility object that implements a simple circular buffer - that is, a section of memory into which bytes can be written and read in a first-in-first-out (FIFO) fashion.
Creating and referencing CircularBuffers
CircularBuffers are regular objects, so creation and ownershop follows normal C++ rules:
CircularBuffer* b = new CircularBuffer(24);
CircularBuffer* b2 = b;
delete b2; // caution, leaves b reference dangling
Reading and writing bytes
To read and write bytes, use the .read()
and .write()
methods. For
example, with single bytes a bool is returned indicating success:
b.write(1); // returns true if the CircularBuffer is not full
b.write(2); // "
b.write(3); // "
byte a;
b.read(&a); // reads 1 into a, returns true
b.read(&a); // reads 2 into a, returns true
b.read(&a); // reads 3 into a, returns true
b.read(&a); // b is empty, returns false. 'a' is unchanged
For multiple bytes, the number of bytes read or written is returned.
If the entire byte string cannot be written, as much of it as possible
is written and that length is returned. By extension, if a
CircularBuffer is full, 0 is returned. For reads, if the requested
read length isn't available, as much as possible is read and that
length is returned. To determine how many bytes are available or
occupied, use the .bytesFree()
and .bytesUsed()
methods,
respectively.
Galago Buffer object
Buffers are reference-counted blocks of memory that support many high- level operations common on String objects in other languages.
Creating and passing Buffers
To create a buffer, call the static method Buffer::New()
:
Buffer b; // a null reference
Buffer b = Buffer::New("a string"); // null-terminated char string
Buffer b = Buffer::New(24); // a size
Buffer b = Buffer::New(data, length); // arbitrary bytes
References can be copied - both b
and b2
point at the same buffer:
Buffer b = Buffer::New("example");
Buffer b2 = b;
A Buffer's memory is freed when no references point at it anymore:
b2 = b = Buffer(); // set b and b2 to null references
You may treat a Buffer as a bool to see if it has a non-null reference:
if(b) doSomethingWith(b);
Buffer contents
Buffers can be compared with == and != operators. Equivalent references and equal contents are both considered equal.
if(b == b2) theyAreEqual();
Contents can be further compared with .equals()
and .startsWith()
methods:
if(b.equals("hello", 5)) wasHello();
if(b.startsWith("../", 3)) handleThisCase();
The contents and length are accessible with the .bytes()
and the
.length()
methods respectively:
io.serial.write(b.bytes(), b.length());
b.bytes()[4] = 'a';
Concatentation is possible with + and += operators:
b += Buffer::New(" more string");
Buffer b3 = b2 + b;
A substring can be extracted with .slice()
:
Buffer b4 = b.slice(0, 7);
A single character or a substring can be found in a Buffer with the
.indexOf()
method:
ssize_t position = b.indexOf('a');
position = b.indexOf(b3, 6); // optional start offset passed
The .parseInt()
and .parseUint()
methods attempt to interpret the
Buffer's contents as an integer or unsigned integer, respectively:
int value = Buffer::New("50").parseInt(); // value would be 50
value = Buffer::New("a").parseInt(); // value would be 0
Galago::io
Galago IO API: IO is responsible for all IO peripherals, like the UART, I2C, SPI, ADC and GPIO. Four IO Classes make up the IO API. They are presented here in summary and below in full detail.
Pin (GPIO and mode control)
Central to the concepts in the IO class is the Pin object. A Pin represents a single i/o pin and reflects its capabilities. Pins can be used directly for general purpose IO and the ADC, but also control which other features, such as I2C, UART and SPI, are mapped to the pin.
Please see the full documentation below.
SPI (3-wire/4-wire synchronous serial interface)
SPI is a three-wire bus with duplex serial lanes and out-of-band device selection. Unlike I2C, it has no standard data size, clock polarity, arbitration, acknowledgement or other high-level features. What it lacks in features it makes up for in speed, simplicity and flexibility.
Please see the full documentation below.
I2C bus (Inter-IC bus)
I2C is a two-wire bus that supports multiple masters and slaves with addressing, collision avoidance, acknowledgement and slave bit delays. It's widely used for a large range of peripherals including sensors, memory devices, human interfaces and displays.
Please see the full documentation below.
UART (Asynchronous serial port)
The UART is a very common serial interface traditionally used to connect computers, modems, dissimilar microcontrollers and industrial equipment, communications devices and countless other applications. Unlike SPI and I2C it's asynchronous and therefore relies on accurate absolute time references on both the sending and receiving ends.
Please see the full documentation below.
Pin
Central to the concepts in the IO class is the Pin object. A Pin
represents a single i/o pin and reflects its capabilities. Logically,
Pins behave like constant references to real pins, so you can use =
to set the state of a Pin rather than change its reference. To set
the reference, use Pin.bind()
:
Pin doorSolenoid;
doorSolenoid.bind(io.p5); // bind to io.p5
Pin modes
Pins have a setMode()
method, which allows configuration of the mode
and features of the pin, plus shorthand methods setOutput()
,
setInput()
, setAnalog()
and setPWM()
. The supported modes vary
per pin, so check the hardware documentation for what each pin can do.
For the full list of supported modes, see the inline documentation for
the IO::Pin::Mode
enum below.
Pins as GPIO
To use Pins as general-purpose i/o (GPIO), call .setOutput()
,
.setInput()
or .setMode()
with the appropriate options and
.read()
, .write()
to read or write a digital value (respectively.)
For example:
io.p2.write(1); // set pin p2 to digital high
io.p2 = 1; // equivalent
io.p2 = true; // equivalent
io.p2 = io.p4; // set pin p2 to the value read from p4
int value = io.p2.read(); // read the digital value from pin p2
bool buttonPressed = io.p2; // digital values are bools too
if(io.p2) // you can read pins and use them as bool expressions
{
...
}
Like other microcontroller platforms, you may read a gpio value while the Pin is set for output mode, and if you write a value while it's an input, no level will be driven on the pin.
For analog values, ensure the Pin is in analog mode and then read it:
io.a3.setAnalog();
unsigned int value = io.readAnalog();
As a convenience, you may access the last analog value with the
.analogValue()
method.
SPI (3-wire/4-wire synchronous serial interface)
SPI is a three-wire bus with duplex serial lanes and out-of-band device selection. Unlike I2C, it has no standard data size, clock polarity, arbitration, acknowledgement or other high-level features. However this simplicity also gives SPI great flexibility. Read more here: http://en.wikipedia.org/wiki/Serial_Peripheral_Interface
The SPI class gives you access to this interface. To enable it, call
.start()
with the bitrate and options you desire. With no arguments
specified, the class picks reasonable defaults:
io.spi.start(); // uses reasonable defaults: 2MHz, 8-bit, mode 0
// perhaps you have an unusual application that needs 12-bit mode 2:
io.spi.start(10000000UL, IO::SPI::Mode2 | IO::SPI::CharsAre12Bit);
To stop the interface, call .stop()
. Existing queued reads and
and writes will be dropped, and their tasks will complete as failed.
Reading and writing data on an SPI interface is simultaneous, so many methods are offered for reads and writes of different data types to be used according to your input and output needs:
.read()
methods to read while writing repetitive or dummy data.write()
methods to write dummy or repetitive data while reading.readAndWrite()
methods to simultaneously read and write data.write()
methods to write data with optional reading
There's intentionally a high degree of overlap between the methods to facilitate writing expressive code, and all methods default to the most obvious behaviour if parameters are omitted.
The SPI subsystem also has .lock()
and .unlock()
methods comprising
a mutex interface, which you can use for strict multiplexing. At the
present time this interface is considered experimental, please don't
rely on it yet.
I2C bus (Inter-IC bus)
I2C is a two-wire bus that supports multiple masters and slaves with addressing, collision avoidance, acknowledgement and slave bit delays. It's widely used for a large range of peripherals including sensors, memory devices, human interfaces and displays. Read more here: http://en.wikipedia.org/wiki/I2C
I2C is implemented with an elegant interface consisting of just four
methods: .start()
to start the interface, .stop()
to stop it,
.write()
to request a write packet and .read()
to request a read.
To start, call .start()
and pass a bitrate. If not specified, that
defaults to the I2C standard of 100KHz. 400KHz and 1MHz are supported
on certain slave devices, and slower speeds are always possible. Call
.stop()
to stop the interface, which will drop pending I2C tasks and
complete their corresponding tasks as failed.
To read data from a slave, use the .read()
method. Pass the slave
address and a Buffer object. The length of the read is implicit from
the length of the Buffer. For example:
struct Context
{
Buffer i2cRead;
};
...
// create a context object and a 10-byte buffer to read into
Context* c = new Context();
c->i2cRead = Buffer::New(10);
// read 10 bytes from address 0x99
Task t = io.i2c.read(0x99, c->i2cRead);
system.when(t, &onComplete, c);
...
void onComplete(void* c, Task t, bool success)
{
Context* context = (Context*)c;
// parameter 'success' indicates if the operation succeeded
// context->i2cRead contains the read bytes
}
Exactly the same approach can be used for I2C write operations.
UART (Asynchronous serial port)
UARTs are very common serial interfaces traditionally used to connect connect computers, modems, communications devices and countless other applications. Unlike SPI and I2C it's asynchronous and therefore relies on accurate absolute time references on both the sending and receiving ends. Various baud-rates and encodings are common. Read more here: http://en.wikipedia.org/wiki/UART
The UART is accessed through the io.serial object, and manages queues
and buffers for sending and receiving data. To enable it, use the
.start()
method:
io.serial.start(); // starts with default settings, 9600 8-n-1
// 38400 baud, 7-bit, no parity, one stop bit
io.serial.start(38400, IO::UART::CharsAre7Bit);
The UART is a fully asynchronous device, so data can be sent and received in an independent fashion. Building on GalagoAPI constructs, the UART exposes a Task that allows you to respond to data when it has been received without having to poll (constantly check for it):
system.when(io.serial.bytesReceived(), &onSerialData);
Task semantics are maintained so when data is available, the task
completes. A new, unresolved task is created the following time
.bytesReceived()
is called, which is ready for the next time data
comes in.
Reading from the UART is done with .read()
methods, which read from
the internal CircularBuffer inside the UART instance. The number of
available bytes can be determined with the .bytesAvailable()
method.
Writing to the serial port is done with the .write()
methods, which
are offered in different versions for different data types. The ones
that take a single datum as their first argument also take a formatting
option from the IO::UART::Format
enum to control how it's rendered
to human-readable characters (e.g. 97 => "97" or 97 => "a").
Galago::system
Galago System API: System is responsible for the CPU, memory, clocks, time and Tasks.
Tasks
A fundamental concept in GalagoAPI is the Task, which is a type of promise, a value that represents ongoing work until it is completed. When completed, a task becomes either successful or failed. Many asynchonous methods return Tasks, which indicate that they're waiting for hardware features in the background. This is not the same as threads or processes in high-level operating systems, but more like a signal that something has completed on an independent timeline.
Properties of Tasks (important!)
- They are created initially in an unresolved state
- They can only be resolved (to true or false) once.
- When Tasks are resolved, any completion callbacks are called
- If completions are added after resolution, they are still called
- All completions are only called once per Task they're bound to
These properties make Tasks impervious to race conditions, which enables asynchronous programming without many normal frustrations.
Completions
If you need to do something when a Task has completed, there are two methods, responding and blocking. Responding is done as follows:
system.when(aTask, &aCompletionFunction, optionalContext);
...
void aCompletionFunction(void* context, Task t, bool success)
{
// t is the Task we're responding to
// success is the reslution of the Task, true or false
// context is the optionalContext from above (defaults to null)
}
You may call system.when()
multiple times to assign multiple
completions to a Task.
In some cases, you way wish to simply wait for a task to finish. This stops the flow of your program, and is thus known as blocking:
system.wait(aTask);
When waiting for a task, the completion callbacks for other concurrent tasks are still called, which prevents deadlock conditions.
Summation
Because Tasks are objects, you pass them by value. Moreover, you may add them to create Tasks that are only complete when all constituent Tasks are complete, or fail when the first constituent fails:
Task allDone = shutdownWirelessTask + closeFileTask;
system.when(allDone, &powerDown);
Of course, the properties of Tasks allow you to add them both before and after they're resolved.
Your own Tasks
To create a Task, use the following:
Task myTask = system.createTask();
To complete it, for example as a result of other asynchronous actions:
system.completeTask(myTask);
// or:
system.completeTask(myTask, false); // if it failed
You may also abort a task by calling:
system.completeTask(taskToAbort, false);
Be aware, however, that this will call completions with a failed task but it doesn't guarantee that the underlying operations, such as I/O, will stop. In most practical cases, however, it doesn't matter.
Time
Time intervals in GalagoAPI are requested using system.delay
and
system.delayMicro
and return Tasks:
Task oneSecond = system.delay(1000); // time in milliseconds
With a Task, you may respond when the time is up:
system.when(oneSecond, &turnOnLamp); // turnOnLamp() after 1 second
Time Tasks can also be used in concert with other asynchronous actions like I/O or user input:
// this combined task will be done when "hello!" is sent, but no
// faster than twice per second (once per 500ms.)
Task t = io.serial.write("hello!") + system.delay(500);
Lots of sophisticated behaviours are possible with time and Tasks, so be creative!
Clocks
System is also in control of core clocks. The core CPU frequency and
the clock output frequency may be adjusted. In both cases, the
nearest possible frequency is chosen - it may be slightly higher or
lower than desired, but it will be the closest of the two. To find
the actual frequency, call the getCoreFrequency()
or
getClockOutputFrequency()
method. To stop a clock, set the frequency
to 0.
Memory
System exposes alloc()
and free()
methods that work like their C
standard library counterparts. Their current implementation is
optimized for low code and memory overhead, not speed. However,
remember that Cortex-M chips usually have disproportionally high CPU
speeds compared to memory and code size, so it's appropriate.
Most memory allocation can be done with C++ operator new / delete, but these methods exist for flexibility.
Failure to allocate memory currently causes a debugger exception, which is a crash when no debugger is attached. Overriding the HardFault exception handler will let you catch it.