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.
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).
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¶
- 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)
From this point onwards, the overall adapter implementation with all its integrated functions is available.
- 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
andI2C_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)
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
- 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)
}
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.
- 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.
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.
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.
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()
.
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.
ad_i2c_bus_release()
This function must be called for each call to
ad_i2c_bus_acquire()
.
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.
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
.