Silicon Labs


Silicon Labs a leading provider of microcontroller, sensing and wireless connectivity solutions for the Internet of Things (IoT).

Using the mbed asynchronous APIs

Introduction

A lot of mbed libraries rely on I/O transactions of some kind. Whether it's a toggling pin, an I2C, SPI or Serial transfer, or even something else, these transactions have one thing in common: they tend to take time. Since the bus speed is commonly much lower then the frequency at which the core operates, these libraries tend to introduce a lot of while(transmitting)-like code. Since this just burns CPU cycles without doing anything useful, it's not a recommended design practice, but up until recently mbed had no support for doing something else.

Enter the asynchronous APIs. They allow you to set up a transfer, send it to the peripheral for execution, and get a callback when the transfer is finished. This way, you can do something else, such as more number crunching, or even go to sleep using the improved sleep API. Going to sleep while waiting for an I/O transaction to finish can save you a lot of power, as demonstrated in this article: Reducing Power Consumption with the ARM mbed Low Power APIs and EFM32 MCUs

Event callbacks

The first thing you realize is different about asynchronous calls is that they can optionally tell your program that the transfer has completed (or something bad has happened). This is called a callback, and happens through executing a function you define with some status flags. Beware though, this can happen anytime, so you should make sure your code is robust enough to handle that!

In order to deal with this callback system in C++, mbed introduced a new object type called 'event_callback_t'. It declares a FunctionPointer object, and initializes it so it can point to a function taking an int as argument, and returning void. The benefit of doing it this way, instead of passing around regular C-like function pointers, is that you can also have member functions as a callback. This is particularly handy when you have multiple instances of an object doing I/O!

Import library

Public Member Functions

  FunctionPointerArg1 (R(*function)(A1)=0)
  Create a FunctionPointer, attaching a static function.
template<typename T >
  FunctionPointerArg1 (T *object, R(T::*member)(A1))
  Create a FunctionPointer, attaching a member function.
void  attach (R(*function)(A1))
  Attach a static function.
template<typename T >
void  attach (T *object, R(T::*member)(A1))
  Attach a member function.
call (A1 a)
  Call the attached static or member function.

Data Fields

R(*)()  get_function (A1)
  Get registered static function.

Setting up an event_callback_t object

event_callback_t functionpointer; //event_callback_t can only point to a void (int) style function

void handler(int events) {
    // Handle events here
    // The events argument holds the event flags
}

void init_fp() {
    // Set up the functionpointer object to point to the 'handler' method on this object
    functionpointer.attach(handler);
}

From a back-end perspective, calling the call(arg) method on the event_callback_t object, will call the attached function with the passed argument.

Asynchronous APIs

Now that we know how to set up a callback mechanism, let's look at how the different communication peripherals now do asynchronous operations.

SPI

Until now, SPI really only consisted of one function, write, which would write the configured amount of bits to the bus, and return the response. With the asynchronous API, we are introducing the ‘transfer’ function, much like the I2C equivalent. The difference here is that if the amount of bytes to receive is greater then the amount of writes, the transaction will automatically write fill words to receive the given number of bytes. The other way around also works, the transaction will only fill the receive buffer with the specified amount of data, even if the write buffer is larger.

The transfer function also takes an event argument, which is the logical OR of all events you want to get a callback for (defined in spi_api.h), and a pointer to an event_callback_t for your callback handler.

Import library

Public Member Functions

  SPI (PinName mosi, PinName miso, PinName sclk, PinName ssel=NC)
  Create a SPI master connected to the specified pins.
void  format (int bits, int mode=0)
  Configure the data transmission format.
void  frequency (int hz=1000000)
  Set the spi bus clock frequency.
virtual int  write (int value)
  Write to the SPI Slave and return the response.
template<typename Type >
int  transfer (const Type *tx_buffer, int tx_length, Type *rx_buffer, int rx_length, const event_callback_t &callback, int event=SPI_EVENT_COMPLETE)
  Start non-blocking SPI transfer using 8bit buffers.
void  abort_transfer ()
  Abort the on-going SPI transfer, and continue with transfer's in the queue if any.
void  clear_transfer_buffer ()
  Clear the transaction buffer.
void  abort_all_transfers ()
  Clear the transaction buffer and abort on-going transfer.
int  set_dma_usage (DMAUsage usage)
  Configure DMA usage suggestion for non-blocking transfers.

Protected Member Functions

void  irq_handler_asynch (void)
  SPI IRQ handler.
int  transfer (const void *tx_buffer, int tx_length, void *rx_buffer, int rx_length, unsigned char bit_width, const event_callback_t &callback, int event)
  Common transfer method.
int  queue_transfer (const void *tx_buffer, int tx_length, void *rx_buffer, int rx_length, unsigned char bit_width, const event_callback_t &callback, int event)
void  start_transfer (const void *tx_buffer, int tx_length, void *rx_buffer, int rx_length, unsigned char bit_width, const event_callback_t &callback, int event)
  Configures a callback, spi peripheral and initiate a new transfer.
void  start_transaction ( transaction_t *data)
  Start a new transaction.
void  dequeue_transaction ()
  Dequeue a transaction.

You can see the new APIs being used in Silicon Labs’ mbed library for the Sharp LS013B7DH03 memory LCD.

Import libraryMemoryLCD

Display driver for Sharp's range of SPI-driven memory LCD's, leveraging the power of asynchronous transfers. Currently supports LS013B7DH03, but easily extendable to other models as well.

Serial

The serial bus is the odd one out of the three. Because it is asynchronous by nature, the functions had to be split up in two: one for sending, and one for receiving data.

The transmit function is called ‘write’, and takes a buffer, its length, a logical OR of events to listen for, and a pointer to the relevant event_callback_t object. The event flags are defined in serial_api.h. The transmit function will return immediately, and the callback will get called on completion of the transmission, or on an error condition. When there is already a transmission in progress, the transmit function will return an error code, and the callback will not get called.

Likewise, the receive function, called ‘read’, takes a buffer, its length, a collection of event flags, and a pointer to the relevant event_callback_t object. However, it can also take a fifth argument, which is a character to listen for. If the SERIAL_EVENT_RX_CHARACTER_MATCH flag is enabled, reception will cease upon seeing this character, and the callback will get called with this flag.

Import library

Public Member Functions

  Serial (PinName tx, PinName rx, const char *name=NULL)
  Create a Serial port, connected to the specified transmit and receive pins.
void  baud (int baudrate)
  Set the baud rate of the serial port.
void  format (int bits=8, Parity parity=SerialBase::None, int stop_bits=1)
  Set the transmission format used by the serial port.
int  readable ()
  Determine if there is a character available to read.
int  writeable ()
  Determine if there is space available to write a character.
void  attach (void(*fptr)(void), IrqType type=RxIrq)
  Attach a function to call whenever a serial interrupt is generated.
template<typename T >
void  attach (T *tptr, void(T::*mptr)(void), IrqType type=RxIrq)
  Attach a member function to call whenever a serial interrupt is generated.
void  send_break ()
  Generate a break condition on the serial line.
void  set_flow_control (Flow type, PinName flow1=NC, PinName flow2=NC)
  Set the flow control type on the serial port.
int  write (const uint8_t *buffer, int length, const event_callback_t &callback, int event=SERIAL_EVENT_TX_COMPLETE)
  Begin asynchronous write using 8bit buffer.
int  write (const uint16_t *buffer, int length, const event_callback_t &callback, int event=SERIAL_EVENT_TX_COMPLETE)
  Begin asynchronous write using 16bit buffer.
void  abort_write ()
  Abort the on-going write transfer.
int  read (uint8_t *buffer, int length, const event_callback_t &callback, int event=SERIAL_EVENT_RX_COMPLETE, unsigned char char_match=SERIAL_RESERVED_CHAR_MATCH)
  Begin asynchronous reading using 8bit buffer.
int  read (uint16_t *buffer, int length, const event_callback_t &callback, int event=SERIAL_EVENT_RX_COMPLETE, unsigned char char_match=SERIAL_RESERVED_CHAR_MATCH)
  Begin asynchronous reading using 16bit buffer.
void  abort_read ()
  Abort the on-going read transfer.
int  set_dma_usage_tx (DMAUsage usage)
  Configure DMA usage suggestion for non-blocking TX transfers.
int  set_dma_usage_rx (DMAUsage usage)
  Configure DMA usage suggestion for non-blocking RX transfers.

You can see the new API in action in Silicon Labs’ low power serial demo program.

Import programSerial-LowPower-Demo

A code sample showing the use of the asynchronous Serial APIs.

I2C

For I2C, the traditional start, stop, read and write functions have been superseded by a single ‘transfer’ function. It takes both a write (tx) and read (rx) length in bytes, and char pointers to the respective write and read buffers. It will automatically add start and stop conditions before and after the transaction, and if both write and read lengths are greater then zero, a restart condition will be generated between the write and the read. Additionally, it takes an event argument, which is the logical OR of all events you want to get a callback for (defined in i2c_api.h), and a pointer to an event_callback_t for your callback handler.

When the transfer has started, the function will return 0. If not, there is an error or there is another I2C transfer already in progress. The abort_transfer function will abort the current transfer.

Import library

Public Member Functions

  I2C (PinName sda, PinName scl)
  Create an I2C Master interface, connected to the specified pins.
void  frequency (int hz)
  Set the frequency of the I2C interface.
int  read (int address, char *data, int length, bool repeated=false)
  Read from an I2C slave.
int  read (int ack)
  Read a single byte from the I2C bus.
int  write (int address, const char *data, int length, bool repeated=false)
  Write to an I2C slave.
int  write (int data)
  Write single byte out on the I2C bus.
void  start (void)
  Creates a start condition on the I2C bus.
void  stop (void)
  Creates a stop condition on the I2C bus.
int  transfer (int address, const char *tx_buffer, int tx_length, char *rx_buffer, int rx_length, const event_callback_t &callback, int event=I2C_EVENT_TRANSFER_COMPLETE, bool repeated=false)
  Start non-blocking I2C transfer.
void  abort_transfer ()
  Abort the on-going I2C transfer.

You can see the new APIs being used in a real-world scenario in Silicon Labs’ mbed library for the si70xx series of relative temperature and humidity sensors.

Import librarySILABS_RHT

This is a library to read out sensor values from Silicon Labs' si70xx-range of relative humidity and temperature sensors.

(Low Power) Ticker/Timer/Timeout

The API to Ticker, Timer and Timeout functionality did not change. However, since they are also asynchronous in nature, they are included here for reference. Please note that starting with mbed release 99, on supported platforms, there are also Low Power versions of these APIs. The time resolution for these is quite granular (on Silicon Labs about a quarter millisecond), but they are much more power friendly.

The one thing that sets their API apart from the transactional model of Serial/SPI/I2C, is that the callbacks you get from a time-based event have no event flags. Therefore, they do not use the event_callback_t model. Instead, the API has an attach() method that allows you to attach either a static or member function by passing the relevant pointer(s). The callback function signature has to be of the type void functionName(void);

Using a timeout with a member function as callback

//In SimpleObject.h:
public:
    SimpleObject(mbed::DigitalOut *led);

private:
    mbed::Timeout blinker;
    mbed::DigitalOut *ledReference;

    void TimeOutHandler(void);

//in SimpleObject.cpp:
SimpleObject::SimpleObject(DigitalOut* led) {
    ledReference = led;
    blinker.attach(this, &SimpleObject::TimeOutHandler, 0.5f);
}

void SimpleObject::TimeOutHandler(void) {
    led = !led;
    blinker.attach(this, &SimpleObject::TimeOutHandler, 0.5f);
}

Import library

Public Member Functions

void  attach (void(*fptr)(void), float t)
  Attach a function to be called by the Ticker , specifiying the interval in seconds.
template<typename T >
void  attach (T *tptr, void(T::*mptr)(void), float t)
  Attach a member function to be called by the Ticker , specifiying the interval in seconds.
void  attach_us (void(*fptr)(void), timestamp_t t)
  Attach a function to be called by the Ticker , specifiying the interval in micro-seconds.
template<typename T >
void  attach_us (T *tptr, void(T::*mptr)(void), timestamp_t t)
  Attach a member function to be called by the Ticker , specifiying the interval in micro-seconds.
void  detach ()
  Detach the function.

Static Public Member Functions

static void  irq (uint32_t id)
  The handler registered with the underlying timer interrupt.

Protected Attributes

timestamp_t  _delay
  Time delay (in microseconds) for re-setting the multi-shot callback.
FunctionPointer   _function
  Callback.

All wikipages