2. I2C Adapters Concept

This section explains the key features of I2C peripheral adapters as well as the procedure to enable and correctly configure the peripheral adapters for I2C functionality. The procedure is a four-step process which can be applied to almost every type of adapter including serial peripheral adapters (I2C, SPI, UART) and GPADC adapters.

'The Four-Step Process for Setting an Adapter Mechanism'

Fig. 2 The Four-Step Process for Setting an Adapter Mechanism

2.1. Header Files

The header files related to adapter functionality can be found in /sdk/adapters/include. These files contain the APIs and macros for configuring the majority of the available hardware blocks. In particular, this tutorial focuses on the adapters that are responsible for the I2C peripheral hardware block. Table 1 briefly explains the header files related to I2C adapters (red indicates the path under which the files are stored while green indicates which ones are used for I2C operations).

'Headers for I2C Adapters'

Fig. 3 Headers for I2C Adapters.

Table 1 Header Files used by I2C Adapters
Filename Description
ad_i2c.h This file contains the recommended APIs and macros for performing I2C operations. Use these APIs when accessing the I2C peripheral bus.
platform_devices.h This file contains macros for declaring virtual devices. These devices may be connected to the Dialog family of devices via a peripheral bus (for example, SPI, I2C, UART) or a peripheral hardware block (for example, GPADC).

2.2. Preparing the I2C Adapter

  1. As illustrated in Fig. 4, the first step for configuring the I2C adapter mechanism is to enable it by defining the following macros in /config/custom_config_qspi.h:
/*
 * Macros for enabling I2C operations using Adapters
 */
#define dg_configUSE_HW_I2C                     (1)
#define dg_configI2C_ADAPTER                    (1)

'First Step for Configuring the I*\ :sup:`2`\ *C Adapter Mechanism'

Fig. 4 First Step for Configuring the I2C Adapter Mechanism

From this point onwards, the overall adapter implementation with all its integrated functions is available.
  1. The second step is to declare all the devices externally connected on the I2C bus. A device can be considered as a set of settings describing the complete I2C interface. These settings are applied every time the device is selected and used. To do this, the SDK exhibits two macros, named I2C_SLAVE_DEVICE_DMA and I2C_SLAVE_DEVICE respectively. The first one is used when a DMA mechanism is used during a transaction.
/*
 * Macro for setting I2C bus parameters
 */
I2C_SLAVE_DEVICE_DMA(bus, name, _address_, _addr_mode, _speed,
                                                      _dma_channel)
'Second Step for Configuring the I*\ :sup:`2`\ *C Adapter Mechanism'

Fig. 5 Second Step for Configuring the I2C Adapter Mechanism

Table 2 Description of the Macro Fields, Used for Declaring an I2C Device
Argument Name Description
bus The DA1468x family of devices features two distinct I2C hardware blocks. Valid values are I2C1 and I2C2.
name Declare an arbitrary alias for the I2C interface (for instance, My_slave_device). This name should be used for opening that specific device.
_address_ The address to which the externally connected slave device listens. The value is device-specific information and is usually found in the manufacturer’s datasheet.
_addr_mode The I2C controller supports both the 7-bit and 10-bit addressing modes. Valid values are those from HW_I2C_ADDRESSING enum in /sdk/peripherals/include/hw_i2c.h.
_speed The I2C controller supports two different speed modes: 100 kHz and 400 kHz respectively. Valid values are those from HW_I2C_SPEED enum in /sdk/peripherals/include/hw_i2c.h.
_dma_channel The DA1468x family of devices features eight general-purpose DMA channels that can be used for various transactions. This field defines the DMA number for the RX channel. TX will have the next number and it is automatically assigned by the adapter mechanism.

Note

The I2C_SLAVE_DEVICE() macro has the same syntax as I2C_SLAVE_DEVICE_DMA except for the last parameter, that is _dma_channel. Also note that DMA RX/TX channels must be used in pairs, that is, 0/1, 2/3, 4/5, and 6/7. Thus, the RX channel must always be set to an even number (0, 2, 4, 6).

The DA1468x family of devices features two distinct I2C blocks namely I2C1 and I2C2. Depending on the I2C interface used, device configurations must be placed between the correct macro indicators:
/* Declare I2C bus configurations for devices connected to I2C1 hardware block */
I2C_BUS(I2C1)

/*
 * Use I2C_SLAVE_DEVICE() and/or I2C_SLAVE_DEVICE_DMA() for each
 * device declaration.
 */

I2C_BUS_END


/* Declare I2C bus configurations for devices connected to I2C2 hardware block */
I2C_BUS(I2C2)

/*
 * Use I2C_SLAVE_DEVICE() and/or I2C_SLAVE_DEVICE_DMA() for each
 * device declaration.
 */

I2C_BUS_END
  1. As illustrated in Fig. 6, the third step is the declaration of the I2C signals. The user can multiplex and expose I2C signals on any available pin on DA1468x SoC.
static void prvSetupHardware( void )
{

   /* Init hardware */
   pm_system_init(periph_init)

}
'Third Step for Configuring the I*\ :sup:`2`\ *C Adapter Mechanism'

Fig. 6 Third Step for Configuring the I2C Adapter Mechanism

Note

When the system enters sleep it loses its pin configurations. Thus, it is essential for the pins to be reconfigured to their last state as soon as the system wakes up. To do this, all pin configurations must be declared in periph_init() which is supervised by the Power Manager of the system.

  1. Once the I2C adapter mechanism is enabled, the developer can use all the available APIs for performing I2C transactions. The following steps describe the required sequence of APIs in an application to successfully execute an I2C write/read operation.
'Fourth Step for Configuring the I2C Adapter Mechanism.'

Fig. 7 Fourth Step for Configuring the I2C Adapter Mechanism

  1. ad_i2c_init()

    This must be called once at either platform start (for instance, in system_init()) or task initialization to perform all the necessary initialization routines.

  2. ad_i2c_open()

    Before using the I2C interface, the application task must open the device that will access the bus. Opening a device involves enabling the I2C controller. If the device is the only connected device on the I2C bus, configuration of the I2C controller also takes place. This function returns a handler to the main flow for use in subsequent adapter functions. Subsequent calls from other tasks simply return the already existing handler.

  3. ad_i2c_bus_acquire()

    This API is optional since it is automatically called upon a write/read transaction and is used for locking the I2C bus for the given opened device. This function should be called when the application task wants to communicate to the I2C bus directly using low level drivers.

Note

The function can be called several times. However, it is essential that the number of calls must match the number of calls to ad_i2c_bus_release().

  1. Perform a write/read transaction either synchronously or asynchronously.

    After opening a device, the application task(s) can perform any read/write I2C transaction either synchronously or asynchronously. Please note that all the available APIs for writing/reading over an I2C bus, nest the corresponding APIs for acquiring and releasing a device.

  2. ad_i2c_bus_release()

    This function must be called for each call to ad_i2c_bus_acquire().

  3. ad_i2c_close()

    After all user operations are done and the device is no longer needed, it should be closed by the task that has currently acquired it. The application can then switch to other devices connected on the same I2C bus. Remember that the I2C adapter implementation follows a single device scheme, that is only one device can be opened at a time.

2.3. I2C Transactions

Write and read functions can be divided into two distinct categories:

  • Synchronous Mode
  • Asynchronous Mode

2.3.1. Synchronous Mode

In synchronous mode, the calling task is blocked for the duration of the write/read access but other tasks are not. Code initially waits for the I2C bus to become available and then blocks the calling task until a transaction is completed. Once a write/read process is finished, the I2C bus is freed and further write/read transactions over the I2C bus can take place.

Code snippet of a typical write followed by a read synchronous I2C transaction:

// Open the device that will utilize the I2C bus
i2c_device dev = ad_i2c_open(My_Slave_Device);

// Perform I2C transactions to the already opened device
ad_i2c_transact(dev, command, sizeof(command), response, sizeof(response));

// Close the already opened device
ad_i2c_close(dev);

The above code performs a write transaction followed by a read transfer, an operation which is typical when reading data from I2C peripherals. In such cases, an address needs to be specified through a write before reading data. The function first waits for both the device and bus resources to become available, before proceeding with the write without waiting for the STOP condition. If no error occurs by the time the last byte is placed in the transmit FIFO of the DA146x SoC, the function continues with the read operation and waits until it is completed.

Note

The aforementioned API can also be used for write only or read only transactions by providing a NULL pointer in the corresponding input parameter. For example, to perform a write only operation: ad_i2c_transact(dev, command, sizeof(command), NULL, 0);

2.3.2. Asynchronous Mode

In asynchronous mode, the calling task is not blocked by the write or read operation. It can continue with other operations while waiting for a dedicated callback function to be called, signaling the completion of the read or write transaction. I2C adapters allow a developer to perform I2C transactions that consist of a number of reads, writes, and callback calls. This provides a time-efficient way to manage all I2C related actions. Most of the actions are executed within ISR context. There are a number of arguments-actions that should be used to perform various I2C transaction schemes. Table 3 explains all the available arguments that can be used to configure an I2C transaction scheme.

Table 3 Available Arguments for Configuring I2C Asynchronous Transactions
Argument Name Description
I2C_SND() Use this argument to send data over the I2C bus, without waiting for a STOP condition to be issued by the master device.
I2C_SND_ST() Use this argument to send data over the I2C bus and wait for a STOP condition to be detected.
I2C_RCV() Use this argument to read data over the I2C bus. A STOP condition is generated after receiving the last byte.
I2C_RCV_NS() Use this argument to read data over the I2C bus. A STOP condition is not generated after receiving the last byte.
I2C_CB() Declare a callback function that should be called when finishing with all defined I2C actions. The developer cannot pass data in the callback function.
I2C_CB1() Declare a callback function that should be called when finishing with all defined I2C actions. The developer can pass data in the callback function.
I2C_END Use this argument to mark the end of an I2C transaction scheme. This argument should be the last argument passed.

Code Snippet of a typical write followed by a read asynchronous I2C transaction:

// Open the device that will utilize the I2C bus
i2c_device dev = ad_i2c_open(My_Slave_Device);

// Perform I2C transactions to the already opened device
ad_i2c_async_transact(dev, I2C_SND(command,  sizeof(command)) , // Initiate an I2C write operation
                           I2C_RCV(response, sizeof(response)), // Initiate an I2C read  operation
                           I2C_CB (final_callback)            , // Function to be called upon finishing with all the above I2C operations
                           I2C_END);                            // Indicate the end of I2C operations

// Close the already opened device
ad_i2c_close(dev);

When using I2C operations in asynchronous mode, the following should be considered:

  • Callback functions are called from within Interrupt Service Routine (ISR) context. Therefore, callback’s execution time should be as short as possible and not contain complex calculations. Please note that for as long as a system interrupt is serviced, the main application is halted.
  • If the callback function is the last action to be performed, then resources (I2C device and bus) are released before the callback is called.
  • Do not call asynchronous related APIs consecutively without guaranteeing that the previous asynchronous transaction is finished.
  • After the callback function is called, it is not guaranteed that the scheduler will give control to the freeRTOS task waiting for that transaction to complete. This is important to consider if several tasks are using this API.

Note

All the write/read I2C related APIs return a code which can be used to indicate whether an I2C operation has been successfully executed or not. All the possible values are declared in HW_I2C_ABORT_SOURCE enum located in /sdk/peripherals/include/hw_i2c.h.