BASEplatform in Depth: The I/O API

In the last article about our recently launched BASEplatform product, we gave an overview of the various modules that can be part of the BASEplatform. In this article we go in a little more detail about an important aspect of any embedded software product, the API. In this case, the I/O API since it’s one of the primary reasons to use the BASEplatform is to interface with on-board peripherals.

This article will focus on the overall features while the fine details of the API will be the subject of yet another article. For now, here’s a few generalities valid for most of the BASEplatform API design.

  • Nearly all functions, apart from a few special exceptions, returns a status code indicating various success or error conditions.
  • When a status code is returned, it’s always as a plain C integer (i.e. “int”) as the function return value.
  • Each potentially blocking function has an optional timeout value without exceptions.
  • API functions are designed to be concise and specialized.
  • Most functions attempt to limit the number of arguments to 4 or less. Structures passed by reference are used when appropriate to prevent unusually long function signatures.

All the low-speed peripherals, UART, SPI and I2C share a similar API with some variations to account for the particularities of each protocol. In addition to the portable main API, individual drivers can expose additional hardware specific functions such as advanced modes and configurations. For this article we’ll use the I2C module as an example.

Below is a list of the standard API functions available for every platform. Other API functions may exist to access driver specific features.

int bp_i2c_open(const bp_i2c_board_def_t *p_def, const bp_i2c_cfg_t *p_cfg, bp_i2c_hndl_t *p_hndl);
int bp_i2c_hndl_get(const char *p_if_name, bp_i2c_hndl_t *p_hndl);
int bp_i2c_enable(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_disable(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_is_enabled(bp_i2c_hndl_t hndl, bool *p_is_enabled);
int bp_i2c_reset(bp_i2c_hndl_t hndl, uint32_t timeout_ms);

int bp_i2c_cfg_set(bp_i2c_hndl_t hndl, const bp_i2c_cfg_t *p_cfg, uint32_t timeout_ms);
int bp_i2c_cfg_get(bp_i2c_hndl_t hndl, bp_i2c_cfg_t *p_cfg, uint32_t timeout_ms);

int bp_i2c_xfer(bp_i2c_hndl_t hndl, bp_i2c_tf_t *p_tf, size_t *p_recv_len, uint32_t timeout_ms);
int bp_i2c_xfer_async(bp_i2c_hndl_t hndl, bp_i2c_tf_t *p_tf, uint32_t timeout_ms);
int bp_i2c_xfer_async_abort(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_idle_wait(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_last_err_get(bp_i2c_hndl_t hndl, int *p_last_err);

The API can be categorized in three groups, life-cycle management, configuration and I/O operations.

Peripheral life-cycle management

The life-cycle functions allow to open, enable, disable, and reset a peripheral and its associated software driver.

int bp_i2c_open(const bp_i2c_board_def_t *p_def, const bp_i2c_cfg_t *p_cfg, bp_i2c_hndl_t *p_hndl);
int bp_i2c_hndl_get(const char *p_if_name, bp_i2c_hndl_t *p_hndl);
int bp_i2c_enable(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_disable(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_is_enabled(bp_i2c_hndl_t hndl, bool *p_is_enabled);
int bp_i2c_reset(bp_i2c_hndl_t hndl, uint32_t timeout_ms);

The life of a peripheral instance starts with the open function, in the case of I2c, bp_i2c_open().

int bp_i2c_open(const bp_i2c_board_def_t *p_def, const bp_i2c_cfg_t *p_cfg, bp_i2c_hndl_t *p_hndl);

The open function takes as its first argument a pointer to a board definition structure. The entire definition of a peripheral instance is composed of a set of hierarchical structures describing the peripherals. Those structures are provided with the BASEplatform and are customized for a specific combination of MCU and board.

For example, a hardware instance structure for the first I2C peripheral on the chosen MCU could be called g_i2c0_def. Passing it by reference to bp_i2c_open() will instantiate a driver according to the description structures. Open also requires an initial configuration which will be discussed below along with the configuration set and get functions. If successful, a handle is written to the pointee of the p_hndl argument. Finally, the result code is returned as the function return value.

The handle created by the open function must subsequently be used to perform any other operation on the peripheral instance. An open handle can be queried by its name using the bp_i2c_hndl_get() function.

Once open the peripheral is in the enabled state and can be used for I/O operations. A peripheral can be temporarily disabled using the appropriate disable function.

int bp_i2c_enable(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_disable(bp_i2c_hndl_t hndl, uint32_t timeout_ms);

Disabling a peripheral will put it in an idle low-power state and, if possible, disable its clock. This is especially useful for low power applications that need to control peripherals power usage.

Finally, a reset function is provided to reset both the hardware and software state of a peripheral to the same state as it was just after the open operation.

int bp_i2c_reset(bp_i2c_hndl_t hndl, uint32_t timeout_ms);

Configuration

The configuration API is used to update or query the configuration of a peripheral at runtime.

int bp_i2c_cfg_set(bp_i2c_hndl_t hndl, const bp_i2c_cfg_t *p_cfg, uint32_t timeout_ms);
int bp_i2c_cfg_get(bp_i2c_hndl_t hndl, bp_i2c_cfg_t *p_cfg, uint32_t timeout_ms);

Configuration includes, when relevant, clock rate, mode such as master or slave and other parameters specific to every protocol. Of interest the query feature retrieves the configuration from the hardware registers, this includes the actual clock rate configured which can be useful for testing and validation.

I/O operations

Last but not least. the I/O operations proper.

int bp_i2c_xfer(bp_i2c_hndl_t hndl, bp_i2c_tf_t *p_tf, size_t *p_recv_len, uint32_t timeout_ms);
int bp_i2c_xfer_async(bp_i2c_hndl_t hndl, bp_i2c_tf_t *p_tf, uint32_t timeout_ms);
int bp_i2c_xfer_async_abort(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_idle_wait(bp_i2c_hndl_t hndl, uint32_t timeout_ms);
int bp_i2c_last_err_get(bp_i2c_hndl_t hndl, int *p_last_err);

The BASEplatform offers two flavours of I/O API, a synchronous API which can be blocking or non-blocking and an asynchronous callback-driven API. The same API functions are used for both master and slave in the case of SPI & I2C, while UART has two sets of API call for transmit and receive.

Like all the other possibly blocking API calls the I/O operation functions each has optional timeout to protect against deadlock or bus errors. It does not matter if the function blocks on a mutex or waits for an interrupt the timeout always applies and is tracked across application layers to ensure the timeout is honoured as strictly as possible. Additionally, the asynchronous transfer operation can be aborted asynchronously as well.

Overall

All in all the I/O API of the BASEplatform is feature-rich, providing all the necessary functions for most applications without being overly bloated or difficult to learn and use. Coupled with a centralized description for SoC and board specific configurations means that supported peripherals can be used immediately without needing endless and complicated configuration from the application developers. Portability is also enhanced since the top level API does not change from one MCU to the next or from one RTOS to another.

Read more about the BASEplatform: www.jblopen.com/baseplatform/


See all articles