I2C driver implementation

This section describes how to implement an I2C driver for QNX and walks through some of the essential functions to get started. Note that the example functions below are simple examples and shouldn't be copied literally.

Here is the interface to implement an I2C driver on QNX:

typedef struct {
    /* size of this structure */
    size_t size;

    /*
     * Return version information
     * Returns:
     * 0    success
     * -1   failure
     */
    int (*version_info)(i2c_libversion_t *version);

    /*
     * Initialize master interface.
     * Returns a handle that is passed to all other functions.
     * Returns:
     * !NULL    success
     * NULL     failure
     */
    void *(*init)(int argc, char *argv[]);

    /*
     * Clean up driver.
     * Frees memory associated with "hdl".
     */
    void (*fini)(void *hdl);

    /*
     * Master send.
     * Parameters:
     * (in)     hdl         Handle returned from init()
     * (in)     buf         Buffer of data to send
     * (in)     len         Length in bytes of buf
     * (in)     stop        If !0, set stop condition when send completes
     * Returns:
     * bitmask of status bits
     */
    i2c_status_t (*send)(void *hdl, void *buf, unsigned int len,
                         unsigned int stop);
    /*
     * Master receive.
     * Parameters:
     * (in)     hdl         Handle returned from init()
     * (in)     buf         Buffer for received data
     * (in)     len         Length in bytes of buf
     * (in)     stop        If !0, set stop condition when recv completes
     * Returns:
     * bitmask of status bits
     */
    i2c_status_t (*recv)(void *hdl, void *buf, unsigned int len,
                         unsigned int stop);

    /*
     * Force the master to free the bus.
     * Returns when the stop condition has been sent.
     * Returns:
     * 0    success
     * -1   failure
     */
    int (*abort)(void *hdl, int rcvid);

    /*
     * Specify the target slave address.
     * Returns:
     * 0    success
     * -1   failure
     */
    int (*set_slave_addr)(void *hdl, unsigned int addr, i2c_addrfmt_t fmt);

    /*
     * Specify the bus speed.
     * If an invalid bus speed is requested, this function should return
     * failure and leave the bus speed unchanged.
     * Parameters:
     * (in)     speed       Bus speed. Units are implementation-defined.
     * (out)    ospeed      Actual bus speed (if NULL, this is ignored)
     * Returns:
     * 0    success
     * -1   failure
     */
    int (*set_bus_speed)(void *hdl, unsigned int speed, unsigned int *ospeed);

    /*
     * Request info about the driver.
     * Returns:
     * 0    success
     * -1   failure
     */
    int (*driver_info)(void *hdl, i2c_driver_info_t *info);

    /*
     * Handle a driver-specific devctl().
     * Parameters:
     * (in)     cmd         Device command
     * (i/o)    msg         Message buffer
     * (in)     msglen      Length of message buffer in bytes
     * (out)    nbytes      Bytes to return (<= msglen)
     * (out)    info        Extra status information returned by devctl
     * Returns:
     * EOK      success
     * errno    failure
     */
    int (*ctl)(void *hdl, int cmd, void *msg, int msglen,
               int *nbytes, int *info);

    /*
     * Reset i2c bus module.
     * Returns:
     * EOK    success
     * EIO   failure
     */
    int (*bus_reset)(void *hdl);
} i2c_master_funcs_t;

The following sections walk through some of the essential functions and how to integrate them.

Define struct

Before making the functions, define a structure that holds all necessary state information. The struct may look like the following:

typedef struct _your_dev {
    // Buffer management
    uint8_t             *buf;
    unsigned            buflen;

    // Hardware registers
    unsigned            reglen;
    uintptr_t           regbase;
    unsigned            physbase;

    // Interrupt handling
    int                 intr;
    int                 iid;

    // Transfer state
    unsigned            txrx;
    uint32_t            status;
    unsigned            cur_len;
    unsigned            tot_len;

    // I2C configuration
    unsigned            slave_addr;
    i2c_addrfmt_t       slave_addr_fmt;
    unsigned            speed;
} your_dev_t;

Driver initialization

Now, you can make your initialization function to fill the structure with relevant data. This initialization function will then return a handler to the struct and be passed to all other relevant functions. Your initialization function may look like the following:

void *your_init(int argc, char *argv[]) {
your_dev_t *dev;

// 1. Allocate device structure
dev = calloc(1, sizeof(*dev));
if (!dev) return NULL;

// 2. Custom function to parse command line options to fill the device structure with data
if (parse_options(dev, argc, argv) != EOK) {
    goto fail;
}

// 3. Thread control setup
if (ThreadCtl(_NTO_TCTL_IO, 0) == -1) {
    goto fail;
}

// 4. Map hardware registers
dev->regbase = mmap_device_io(dev->reglen, dev->physbase);
if (dev->regbase == MAP_FAILED) {
    goto fail;
}

// 5. Setup interrupt handling
SIGEV_INTR_INIT(&dev->intrevent);
dev->iid = InterruptAttachEvent(dev->intr, &dev->intrevent, 0);

return dev;

fail:
    cleanup(dev);
    return NULL;
}

Set slave address

Implement a function that sets the slave address of your I2C device, which may look like the following:

// Set slave address
int your_set_slave_addr(void *hdl, unsigned int addr, i2c_addrfmt_t fmt) {
    your_dev_t *dev = hdl;
    if ((fmt != I2C_ADDRFMT_7BIT) ) {
        errno = EINVAL;
        return -1;
    }

    dev->slave_addr = addr;
    dev->slave_addr_fmt = fmt;

    return 0;
}

Send Data

Implement a function that sends the data to your I2C device:

i2c_status_t your_send(void *hdl, void *buf, unsigned int len, unsigned int stop) {
    your_dev_t *dev = hdl;

    /* Initialize transfer state */
    dev->tot_len = len;
    dev->cur_len = 0;
    dev->buf = buf;
    dev->txrx = I2C_TX_MODE;
    dev->status = 0;

    /* Configure slave address */
    out32(dev->regbase + YOUR_I2C_SLAVE_ADDR_REG, dev->slave_addr);

    /* Set transfer length */
    out32(dev->regbase + YOUR_I2C_DATA_LEN_REG, dev->tot_len);

    /* Fill buffer initial data */
    while (dev->cur_len < dev->tot_len) {
        dev->status = in32(dev->regbase + YOUR_I2C_STATREG_OFF) & YOUR_STATREG_MASK;

        if (dev->status & YOUR_STATREG_TXD) {
            out32(dev->regbase + YOUR_I2C_FIFOREG_OFF, dev->buf[dev->cur_len++]);
        } else {
            break;
        }
    }

    /* Start transfer */
    out32(dev->regbase + YOUR_I2C_CTRL_REG, YOUR_CTRL_I2C_EN | YOUR_CTRL_START);

    /* Enable interrupts */
    out32(dev->regbase + YOUR_I2C_CTRL_REG,
          in32(dev->regbase + YOUR_I2C_CTRL_REG) | YOUR_CTRL_INT_DONE | YOUR_CTRL_INT_TX);

    /* Wait for completion */
    return wait_for_completion(dev, len);
}
Note:

You can also implement a wait_for_completion() function to wait for the data transfer to be completed before moving on.

Receive data

Implement a function that receives data from your I2C device:

i2c_status_t your_send(void *hdl, void *buf, unsigned int len, unsigned int stop) {
    your_dev_t *dev = hdl;

    /* Initialize receive state */
    dev->tot_len = len;
    dev->cur_len = 0;
    dev->buf = buf;
    dev->txrx = I2C_RX_MODE;
    dev->status = 0;

    /* Configure slave address */
    out32(dev->regbase + I2C_SLAVE_ADDR_REG, dev->slave_addr);

    /* Set receive length */
    out32(dev->regbase + I2C_DATA_LEN_REG, dev->tot_len);

    /* Start read operation - Note READ bit is set */
    out32(dev->regbase + I2C_CTRL_REG,
          I2C_ENABLE | I2C_START | I2C_READ | I2C_CLEAR_FIFO);

    /* Enable receive interrupts */
    out32(dev->regbase + I2C_CTRL_REG,
          in32(dev->regbase + I2C_CTRL_REG) | I2C_INT_DONE | I2C_INT_RX);

    /* Wait for completion */
    return wait_for_completion(dev, len);
}

/*
 * NOTE: The following function must be implemented according to your specific hardware:
 * - wait_for_completion(): Set the SPI clock rate
 */
Note:

You can also implement a wait_for_completion() function to wait for the data to be received before moving on.

Clean up driver

Finally, implement a function to clean up the driver. It may look something like the following:

void your_fini(void *hdl){
    your_dev_t  *dev = hdl;

    if (dev != NULL) {
        if (dev->iid != -1) {
            InterruptDetach(dev->iid);
        }
        munmap_device_io(dev->regbase, dev->reglen);
    }
}

Integrate the implementations

Once you've implemented these functions, you can integrate these implementations. Here is an example on how you can integrate them:

int i2c_master_getfuncs(i2c_master_funcs_t *funcs, int tabsize){
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            init, your_init, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            fini, your_fini, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            send, your_send, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            recv, your_recv, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            set_slave_addr, your_set_slave_addr, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            set_bus_speed, your_set_bus_speed, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            version_info, your_version_info, tabsize);
    I2C_ADD_FUNC(i2c_master_funcs_t, funcs,
            driver_info, your_driver_info, tabsize);
    return 0;
}

Porting Linux I2C driver to QNX

A typical Linux I2C driver may look like the following:

static const struct of_device_id your_i2c_of_match[] = {
    { .compatible = "your-i2c" },
    {},
};
MODULE_DEVICE_TABLE(of, your_i2c_of_match);

static struct platform_driver your_i2c_driver = {
    .probe      = your_i2c_probe,
    .remove     = your_i2c_remove,
    .driver     = {
        .name   = "your-i2c-driver",
        .of_match_table = your_i2c_of_match,
    },
};
module_platform_driver(your_i2c_driver);

In Linux, to initialize the driver and cleanup the driver, you would use the probe() and remove() function. You need to implement these functions on Linux to initialize and cleanup the driver:

static int your_i2c_probe(struct platform_device *pdev) {
    /* Implement the initialization function for your driver here */
    return 0;
}
static void your_i2c_remove(struct platform_device *pdev) {
    /* Implement the clean up function for your driver here */
}

The probe() and remove() function in Linux is equivalent to QNX's init() and fini() functions you have seen before. 

Moreover, the Linux struct to integrate the data transfer function and other functions might look like this:

struct i2c_algorithm {
    /*
     * If an adapter algorithm can't do I2C-level access, set xfer
     * to NULL. If an adapter algorithm can do SMBus access, set
     * smbus_xfer. If set to NULL, the SMBus protocol is simulated
     * using common I2C messages.
     */
    union {
        int (*xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
                int num);
        int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
                   int num);
    };
    union {
        int (*xfer_atomic)(struct i2c_adapter *adap,
                   struct i2c_msg *msgs, int num);
        int (*master_xfer_atomic)(struct i2c_adapter *adap,
                       struct i2c_msg *msgs, int num);
    };
    int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
              unsigned short flags, char read_write,
              u8 command, int size, union i2c_smbus_data *data);
    int (*smbus_xfer_atomic)(struct i2c_adapter *adap, u16 addr,
                 unsigned short flags, char read_write,
                 u8 command, int size, union i2c_smbus_data *data);

    /* To determine what the adapter supports */
    u32 (*functionality)(struct i2c_adapter *adap);

#if IS_ENABLED(CONFIG_I2C_SLAVE)
    union {
        int (*reg_target)(struct i2c_client *client);
        int (*reg_slave)(struct i2c_client *client);
    };
    union {
        int (*unreg_target)(struct i2c_client *client);
        int (*unreg_slave)(struct i2c_client *client);
    };
#endif
};

And the integrations may look like this:

static const struct i2c_algorithm your_i2c_algo = {
    .xfer = your_i2c_xfer,
    .functionality = your_i2c_func,
};

And these functions must be implemented to be integrated into the i2c_algorithm struct and may look like this:

static int your_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg msgs[],
                int num)
{
    /* Implement the transfer function for your driver here */
    return -EIO;
}

static u32 your_i2c_func(struct i2c_adapter *adap)
{
    return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}

The integration of the driver function in Linux happens in the i2c_algorithm struct, while in QNX, to integrate the functions you've created, you have to call the i2c_master_getfuncs() function and the I2C_ADD_FUNC definition just like previously mentioned.

After the Linux driver is developed, the driver is then loaded into the kernel as modules to be used. However, in QNX, you don't have to load it into the kernel. In fact, you can just start the driver just like any other user application.

Page updated: