I2C (Inter-Integrated Circuit) Framework

This note includes:

Overview

I2C (Inter-Integrated Circuit) is a simple serial protocol that connects multiple devices in a master-slave relationship. Multiple master devices may share a single bus. The same device may function as both a master and a slave in different transactions. The I2C specification defines these transfer speed ranges:

The I2C framework is intended to facilitate consistent implementation of I2C interfaces. The framework consists of the following parts:

hardware/i2c/*
The hardware interface.
lib/i2c
The resource manager layer.
<hw/i2c.h>
A public header file that defines the hardware and application interfaces.

The most common application of the I2C bus is low-bandwidth access to a slave device's registers, such as:

Typically, only a few bytes are exchanged on the bus.

You can implement the I2C master as a single- or multithreaded resource manager, or as a dedicated application. The primary advantages of a resource manager interface are:

For a dedicated I2C-bus application, a hardware access library is more efficient. The hardware interface, which defines the interface to this library, is useful as a starting point for developers and facilitates maintenance and code portability.

Hardware interface

This is the interface to the code that implements the hardware-specific functionality of an I2C master. It's defined in <hw/i2c.h>.

Function table

The i2c_master_funcs_t structure is a table of pointers to functions that you can provide for your hardware. The higher-level code calls these functions.

typedef struct {
    size_t size;    /* size of this structure */
    int (*version_info)(i2c_libversion_t *version);
    void *(*init)(int argc, char *argv[]);
    void (*fini)(void *hdl);
    i2c_status_t (*send)(void *hdl, void *buf, unsigned int len, 
                         unsigned int stop);
    i2c_status_t (*recv)(void *hdl, void *buf, unsigned int len, 
                         unsigned int stop);
    int (*abort)(void *hdl, int rcvid);
    int (*set_slave_addr)(void *hdl, unsigned int addr, i2c_addrfmt_t fmt);
    int (*set_bus_speed)(void *hdl, unsigned int speed, unsigned int *ospeed);
    int (*driver_info)(void *hdl, i2c_driver_info_t *info);
    int (*ctl)(void *hdl, int cmd, void *msg, int msglen, 
               int *nbytes, int *info);
} i2c_master_funcs_t;

Note:
  • In a multimaster system, the send and recv functions should handle bus arbitration.
  • If the I2C interface supports DMA, you can implement it in the send and recv functions.

The functions are described in the sections that follow:

version_info function

The higher-level code calls the version_info function to get information about the version of the library. The prototype for this function is:

int version_info( i2c_libversion_t *version );

The version argument is a pointer to a i2c_libversion_t structure that this function must fill in. This structure is defined as follows:

typedef struct {
    unsigned char   major;
    unsigned char   minor;
    unsigned char   revision;
} i2c_libversion_t;

This function should set the members of this structure as follows:

version->major = I2CLIB_VERSION_MAJOR;
version->minor = I2CLIB_VERSION_MINOR;
version->revision = I2CLIB_REVISION;

The function must return:

0
Success.
-1
Failure.

init function

The init function initializes the master interface. The prototype for this function is:

void *init( int argc, char *argv[]);

The arguments are those passed on the command line.

The function returns a handle that's passed to all other functions, or NULL if an error occurred.

fini function

The fini function cleans up the driver and frees any memory associated with the given handle. The prototype for this function is:

void fini( void *hdl);

The argument is the handle that the init function returned.

send function

The send function initiates a master send. An optional event is sent when the transaction is complete (and the data buffer can be released). If this function fails, no transaction has been initiated.

The prototype for this function is:

i2c_status_t send(
               void *hdl,
               void *buf,
               unsigned int len,
               unsigned int stop );

The arguments are:

hdl
The handle returned by the init function.
buf
A pointer to the buffer of data to send.
len
The length, in bytes, of the data to send.
stop
If this is nonzero, the function sets the stop condition when the send completes.

The function returns one of the following:

I2C_STATUS_DONE
The transaction completed (with or without an error).
I2C_STATUS_ERROR
An unknown error occurred.
I2C_STATUS_NACK
Slave no-acknowledgement.
I2C_STATUS_ARBL
Lost arbitration.
I2C_STATUS_BUSY
The transaction timed out.
I2C_STATUS_ABORT
The transaction was aborted.

recv function

The recv function initiates a master receive. An optional event is sent when the transaction is complete (and the data buffer can be used). If this function fails, no transaction has been initiated.

The prototype for this function is:

i2c_status_t recv(
               void *hdl,
               void *buf,
               unsigned int len,
               unsigned int stop );

The arguments are:

hdl
The handle returned by the init function.
buf
A pointer to the buffer in which to put the received data.
len
The length, in bytes, of the buffer.
stop
If this is nonzero, the function sets the stop condition when the receive completes.

The function returns one of the following:

I2C_STATUS_DONE
The transaction completed (with or without an error).
I2C_STATUS_ERROR
An unknown error occurred.
I2C_STATUS_NACK
Slave no-acknowledgement.
I2C_STATUS_ARBL
Lost arbitration.
I2C_STATUS_BUSY
The transaction timed out.
I2C_STATUS_ABORT
The transaction was aborted.

abort function

The abort function forces the master to free the bus. It returns when the stop condition has been sent. The prototype for this function is:

int abort(
      void *hdl,
      int rcvid );

The arguments are:

hdl
The handle returned by the init function.
rcvid
The receive ID of the client.

The function must return:

0
Success.
-1
Failure.

set_slave_addr function

The set_slave_addr function specifies the target slave address. The prototype for this function is:

int set_slave_addr(
      void *hdl,
      unsigned int addr,
      i2c_addrfmt_t fmt );

The arguments are:

hdl
The handle returned by the init function.
addr
The target slave address.
fmt
The format of the address; one of:

The function must return:

0
Success.
-1
Failure.

set_bus_speed function

The set_bus_speed function specifies the bus speed. If an invalid bus speed is requested, this function should return a failure and leave the bus speed unchanged. The prototype for this function is:

int set_bus_speed(
      void *hdl,
      unsigned int speed,
      unsigned int *ospeed );

The arguments are:

hdl
The handle returned by the init function.
speed
The bus speed. The units are implementation-defined.
ospeed
NULL, or a pointer to a location where the function should store the actual bus speed.

The function must return:

0
Success.
-1
Failure.

driver_info function

The driver_info function returns information about the driver. The prototype for this function is:

int driver_info(
      void *hdl,
      i2c_driver_info_t *info );

The arguments are:

hdl
The handle returned by the init function.
info
A pointer to a i2c_driver_info_t structure where the function should store the information:
typedef struct {
    _Uint32t    speed_mode;
    _Uint32t    addr_mode;
    _Uing32t    reserved[2];
} i2c_driver_info_t;
  

For the speed_mode member, OR together the appropriate values from the following list to indicate the supported speeds:

I2C_SPEED_STANDARD
Up to 100 Kbit/s.
I2C_SPEED_FAST
Up to 400 Kbit/s.
I2C_SPEED_HIGH
Up to 3.4 Mbit/s.

Set the addr_mode to one of the following to indicate the supported address format:

The function must return:

0
Success.
-1
Failure.

ctl function

The ctl function handles a driver-specific devctl() command. The prototype for this function is:

int ctl(
      void *hdl,
      int cmd,
      void *msg,
      int msglen,
      int *nbytes,
      int *info );

The arguments are:

hdl
The handle returned by the init function.
cmd
The device command.
msg
A pointer to the message buffer. The function can change the contents of the buffer.
msglen
The length of the message buffer, in bytes.
nbytes
The number of bytes being returned. This must not be greater than msglen.
info
A pointer to a location where the function can store extra status information returned by devctl().

The function must return:

EOK
Success.
Any other errno value
Failure.

Access function

This function is used by higher-level code (such as the resource manager interface) to access the hardware-specific functions. It must be implemented.

int i2c_master_getfuncs(i2c_master_funcs_t *funcs, int tabsize);

This function must fill in the given table with the hardware-specific functions.

The arguments are:

funcs
The function table to fill in. The library initializes this table before calling this function. If you haven't implemented a function in the table, leave its entry unchanged; don't set it to NULL.
tabsize
The size of the structure that funcs points to, in bytes.

Note: Don't change the size member of the structure.

To set an entry in the table, use the I2C_ADD_FUNC() macro:

#define I2C_ADD_FUNC(tabletype, table, entry, func, tabsize) ...

For example:

I2C_ADD_FUNC( i2c_master_funcs_t, funcs, init, my_init, tabsize);

The function must return:

0
Success.
-1
Failure.

Sample calls

A typical sequence of hardware library calls is as follows:

    #include <hw/i2c.h>

    i2c_master_funcs_t  masterf;
    i2c_libversion_t    version;
    i2c_status_t        status;
    void *hdl;

    i2c_master_getfuncs(&masterf, sizeof(masterf));

    masterf.version_info(&version);
    if ((version.major != I2CLIB_VERSION_MAJOR) ||
        (version.minor > I2CLIB_VERSION_MINOR))
    {
        /* error */
        ...
    }

    hdl = masterf.init(...);

    masterf.set_bus_speed(hdl, ...);
    masterf.set_slave_addr(hdl, ...);

    status = masterf.send(hdl, ...);
    if (status != I2C_STATUS_DONE) {
        /* error */
        if (!(status & I2C_STATUS_DONE))
            masterf.abort(hdl);
    }

    status = masterf.recv(hdl, ...);
    if (status != I2C_STATUS_DONE) {
        /* error */
        ...
    }

    masterf.fini(hdl);

Application interfaces

Shared-library interface

When an I2C master device is dedicated for use by a single application, you can compile the hardware interface code as a library and link it to the application.

To increase code portability, the application should call i2c_master_getfuncs() to access the hardware-specific functions. Accessing the hardware library through the function table also makes it easier for an application to load and manage more than one hardware library.

The resource manager interface uses the hardware library in a similar way.

Resource manager interface

The resource manager interface is designed to mediate accesses to a single master. In a system with multiple masters, a separate instance of the resource manager should be run for each master.

The resource manager layer registers a device name (usually /dev/i2c0). Applications access the I2C master by issuing devctl() commands to the device name.

The supported devctl() commands and their formats are defined in <hw/i2c.h>. The commands include:

These commands are supported only by the multithreaded resource manager library:

The following commands are deprecated:

Supporting data types

Many of the devctl() commands use the i2c_addr_t structure, which is defined as:

typedef struct {
    _Uint32t addr;   /* I2C address */
    _Uint32t fmt;    /* I2C_ADDRFMT_7BIT or I2C_ADDRFMT_10BIT */
} i2c_addr_t;

DCMD_I2C_DRIVER_INFO

The DCMD_I2C_DRIVER_INFO command returns information about the hardware library.

Input
None.
Output
An i2c_driver_info_t structure that contains driver information; see the description of the driver_info function, earlier in this technote.

If an error occurs, the command returns:

EIO
The driver query failed.

DCMD_I2C_SEND

The DCMD_I2C_SEND command executes a master send transaction. It returns when the transaction is complete.

Input
Output
None.

If an error occurs, the command returns:

EIO
The master send failed. The causes include: bad slave address, bad bus speed, bus is busy.
EFAULT
An error occurred while accessing the data buffer.
EINVAL
Bad message format.
ENOMEM
Insufficient memory.
EPERM
The master is locked by another connection.

DCMD_I2C_RECV

The DCMD_I2C_RECV command executes a master receive transaction. It returns when the transaction is complete.

Input
Output

The i2c_recv_t structure is defined as:

typedef struct {
    i2c_addr_t slave;  /* slave address */
    _Uint32t   len;    /* length of receive data in bytes */
    _Uint32t   stop;   /* send stop when complete? (0=no, 1=yes) */
} i2c_recv_t;

If an error occurs, the command returns:

EIO
The master send failed. Causes include: bad slave address, bad bus speed, bus is busy.
EINVAL
Bad message format.
ENOMEM
Insufficient memory.
EPERM
The master is locked by another connection.

DCMD_I2C_SENDRECV

The DCMD_I2C_SENDRECV command executes a send followed by a receive. This sequence is typically used to read a slave device's register value. When multiple applications access the same slave device, it is necessary to execute this sequence atomically to prevent register reads from being interrupted. Although this functionality is also provided by DCMD_I2C_LOCK and DCMD_I2C_UNLOCK, the implementation of this functionality is much simpler.

Input
Output

The i2c_sendrecv_t structure is defined as:

typedef struct {
    i2c_addr_t slave;      /* slave address */
    _Uint32t   send_len;   /* length of send data in bytes */
    _Uint32t   recv_len;   /* length of receive data in bytes */
    _Uint32t   stop;       /* set stop when complete? */
} i2c_sendrecv_t;

If an error occurs, the command returns:

EIO
The master send failed. Causes include: bad slave address, bad bus speed, bus is busy.
EFAULT
An error occurred while accessing the data buffer.
EINVAL
Bad message format.
ENOMEM
Insufficient memory.
EPERM
The master is locked by another connection.

DCMD_I2C_SET_BUS_SPEED

The DCMD_I2C_SET_BUS_SPEED command sets the bus speed for the current connection. You should set the bus speed before attempting a data-transfer operation.

Input
_Uint32t -- the bus speed
Output
None.

If an error occurs, the command returns:

EINVAL
Bad mesage format.

DCMD_I2C_LOCK

The DCMD_I2C_LOCK command reserves the master interface for exclusive use by the current connection. This function blocks until the lock is acquired. If the lock is already owned by the current connection, there's no effect. Lock counts aren't maintained.


Note: This command is supported only by the multithreaded resource manager library.

Input
None.
Output
None.

If an error occurs, the command returns:

EBUSY
Unable to acquire lock.
ENOSYS
The operation is unsupported.

DCMD_I2C_UNLOCK

The DCMD_I2C_UNLOCK command release the lock.


Note: This command is supported only by the multithreaded resource manager library.

Input
None.
Output
None.

If an error occurs, the command returns:

EPERM
The lock is owned by another connection.
EBUSY
The device is already unlocked.
ENOSYS
The operation is unsupported.

DCMD_I2C_SET_SLAVE_ADDR (deprecated)

The DCMD_I2C_SET_SLAVE_ADDR command sets the slave device address for the current connection. You should set the slave device address before attempting a master send or receive transaction.

Input
i2c_addr_t -- the slave device address and the address format
Output
None.

This command doesn't return any error codes.

DCMD_I2C_MASTER_SEND (deprecated)

The DCMD_I2C_MASTER_SEND command execute a master send transaction, using the slave device address and bus speed specified for the current connection.

Input
Output
None.

If an error occurs, the command returns:

EIO
The master send failed. Causes include: bad slave address, bad bus speed, bus is busy.
EFAULT
An error occurred while accessing the data buffer.
EINVAL
Bad message format.
ENOMEM
Insufficient memory.
EPERM
The master is locked by another connection.

DCMD_I2C_MASTER_RECV (deprecated)

The DCMD_I2C_MASTER_RECV command executes a master receive transaction, using the slave device address and bus speed specified for the current connection.

Input
i2c_messagehdr_t -- the message header
Output
_Uint8t[] -- the receive data

Note: The message header is overwritten.

If an error occurs, the command returns:

EIO
The master receive failed. Causes include: bad slave address, bad bus speed, bus is busy.
EINVAL
Bad message format.
ENOMEM
Insufficient memory.
EPERM
The master is locked by another connection.

Resource manager design

The resource manager layer is implemented as a library that's statically linked with the hardware library.

We provide these implementations:

libi2c-master
A single-threaded manager.
libi2c-master_mt
A multithreaded manager.

Locking is supported only by the multithreaded manager because it's cumbersome to implement in a single-threaded model; the single-threaded manager would need to maintain a priority queue of the clients blocked on the lock. Locking isn't very useful when there are only a few client applications sharing the interface.

Unblocking by aborting is also only fully supported by the multithreaded manager because unblock pulses cannot be properly handled in the single-threaded manager.

On startup, the resmgr layer does the following:

i2c_master_getfuncs(&masterf)
masterf.init()
masterf.set_bus_speed()

The resource manager then makes itself run in the background.

Here's how the resource manager handles these devctl() commands:

The resource manager thread remains occupied until the transaction is complete and replies to the client.

In the multithreaded case, the hardware library calls -- except masterf.driver_info() -- are mutexed by the resmgr library.

Locking (DCMD_I2C_LOCK and DCMD_I2C_UNLOCK) is managed by a sleepon and an (atomically accessed) lock ID. A resource manager thread handles the DCMD_I2C_LOCK request by waiting on the lock ID. The lock ID is verified before processing all devctl() commands except DCMD_I2C_SET_BUS_SPEED, DCMD_I2C_SET_SLAVE_ADDRESS, and DCMD_I2C_DRIVER_INFO. Functions block until the lock is freed.

You can terminate the resource manager by sending a SIGTERM to it.