Linux Device Driver Tutorial Part 39 – Real I2C Bus Linux Device Driver

This is the Series on Linux Device Driver. The aim of this series is to provide easy and practical examples that anyone can understand. This is the Linux Device Driver Tutorial Part 39 – Real I2C Bus Linux Device Driver example using Raspberry PI.

We are using the Raspberry PI 4 Model B for this demonstration.

Real I2C Bus Linux Device Driver

Prerequisites

Hardware Required

Bring up Raspberry PI

  1. Install Raspberry Pi OS (32-bit) with desktop in the SD card.
  2. Then install the kernel header using sudo apt install raspberrypi-kernel-headers

For your information, In our Raspberry PI 4 board, kernel 5.4.51-v7l+ is installed.

Please enable the I2C in the Raspberry Pi.

Introduction

In our last tutorial, we have seen how to write the dummy I2C bus driver in the Linux kernel. It doesn’t send any data to the Salve device. So in this tutorial, we have come up with the real I2C bus Linux device driver. Using this I2C bus driver, we can send data to the slave device. Let’s start.

We have discussed the APIs too in the last tutorials. In this tutorial, we just used a new API called i2c_add_numbered_adapter(). This API will allocate the bus number that we are asking if that is available. In the last tutorial, we have not used this. Instead, we used i2c_add_adapter(). This API will allocate the bus number dynamically.

We can move to the example straightaway since we don’t have any other thing to discuss.

Example Programming

In this I2C client Driver tutorial, we have just written the I2C Client driver which communicates to the slave device called SSD1306 OLED I2C Display by using its internal I2C bus driver. That example demonstrates it will just fill something into the display while loading and it will clear the display while unloading. The same thing only we are going to try with our own I2C bus driver in this tutorial.

Connection Diagram

  • SCL – GPIO 20
  • SDA – GPIO 21

I2C-bus-driver-ssd1306-interface-with-raspberry-pi

I2C Bus Driver Source Code

[Get the source code form the GitHub]

Note: In the below example I have implemented the I2C communication by using the bit-banging method (Linux Kernel’s  GPIO API has been used). I have not implemented the I2C read part. And also we have not handled arbitration, clock stretching, etc in this tutorial as this is just an example program. This is only for demonstration purposes.

/***************************************************************************//**
*  \file       driver_bus.c
*
*  \details    Simple I2C Bus driver explanation
*
*  \author     EmbeTronicX
*
*  \Tested with Linux raspberrypi 5.4.51-v7l+
*
* *******************************************************************************/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/gpio.h>

#define ADAPTER_NAME     "ETX_I2C_ADAPTER"

/*------------ I2C related functions - Bit banging method - START --------------*/

#define SCL_GPIO  20    //GPIO act as SCL
#define SDA_GPIO  21    //GPIO act as SDA

#define I2C_DELAY usleep_range(5, 10) 

/*
** Function to read the SCL GPIO
*/
static bool ETX_I2C_Read_SCL(void)
{
  gpio_direction_input(SCL_GPIO);
  return gpio_get_value(SCL_GPIO);
}

/*
** Function to read the SDA GPIO
*/
static bool ETX_I2C_Read_SDA(void)
{
  gpio_direction_input(SDA_GPIO);
  return gpio_get_value(SDA_GPIO);
}

/*
** Function to clear the SCL GPIO
*/
static void ETX_I2C_Clear_SCL(void)
{
  gpio_direction_output(SCL_GPIO, 0);
  gpio_set_value(SCL_GPIO, 0);
}

/*
** Function to clear the SDA GPIO
*/
static void ETX_I2C_Clear_SDA(void)
{
  gpio_direction_output(SDA_GPIO, 0);
  gpio_set_value(SDA_GPIO, 0);
}

/*
** Function to set the SCL GPIO
*/
static void ETX_I2C_Set_SCL(void)
{
  gpio_direction_output(SCL_GPIO, 1);
  gpio_set_value(SCL_GPIO, 1);
}

/*
** Function to set the SDA GPIO
*/
static void ETX_I2C_Set_SDA(void)
{
  gpio_direction_output(SDA_GPIO, 1);
  gpio_set_value(SDA_GPIO, 1);
}

/*
** Function to Initialize the GPIOs
*/
static int ETX_I2C_Init( void )
{
  int ret = 0;

  do      //Break if any error
  {
    //Checking the SCL GPIO is valid or not
    if(gpio_is_valid(SCL_GPIO) == false){
          pr_err("SCL GPIO %d is not valid\n", SCL_GPIO);
          ret = -1;
          break;
    }

    //Checking the SDA GPIO is valid or not
    if(gpio_is_valid(SDA_GPIO) == false){
          pr_err("SDA GPIO %d is not valid\n", SDA_GPIO);
          ret = -1;
          break;
    }
    
    //Requesting the SCL GPIO
    if(gpio_request(SCL_GPIO,"SCL_GPIO") < 0){
          pr_err("ERROR: SCL GPIO %d request\n", SCL_GPIO);
          ret = -1;
          break;
    }

    //Requesting the SDA GPIO
    if(gpio_request(SDA_GPIO,"SDA_GPIO") < 0){
          pr_err("ERROR: SDA GPIO %d request\n", SDA_GPIO);
          //free already requested SCL GPIO
          gpio_free(SCL_GPIO);
          ret = -1;
          break;
    }
    
    /*
    ** configure the SCL GPIO as output, We will change the 
    ** direction later as per our need.
    */
    gpio_direction_output(SCL_GPIO, 1);

    /*
    ** configure the SDA GPIO as output, We will change the 
    ** direction later as per our need.
    */
    gpio_direction_output(SDA_GPIO, 1);

  } while(false);

  return ret;  
}

/*
** Function to Deinitialize the GPIOs
*/
static void ETX_I2C_DeInit( void )
{
  //free both the GPIOs
  gpio_free(SCL_GPIO);
  gpio_free(SDA_GPIO);
}

/*
** Function to send the START condition
**      ______  
**            \
**  SDA        \_____
**              :
**              :
**           ___:____
**  SCL     /
**      ___/ 
*/
static void ETX_I2C_Sart( void )
{
  ETX_I2C_Set_SDA();
  ETX_I2C_Set_SCL();
  I2C_DELAY;
  ETX_I2C_Clear_SDA();
  I2C_DELAY;  
  ETX_I2C_Clear_SCL();
  I2C_DELAY;  
}

/*
** Function to send the STOP condition
**                _____
**               /
**  SDA  _______/
**              :
**              :
**            __:______
**  SCL      /
**       ___/ 
*/
static void ETX_I2C_Stop( void )
{
  ETX_I2C_Clear_SDA();
  I2C_DELAY;
  ETX_I2C_Set_SCL();
  I2C_DELAY;
  ETX_I2C_Set_SDA();
  I2C_DELAY;
  ETX_I2C_Clear_SCL();
}

/*
** Function to reads the SDA to get the status
** 
** Retutns 0 for NACK, returns 1 for ACK
*/
static int ETX_I2C_Read_NACK_ACK( void )
{
  int ret = 1;

  //reading ACK/NACK
  I2C_DELAY;
  ETX_I2C_Set_SCL();
  I2C_DELAY;
  if( ETX_I2C_Read_SDA() )      //check for ACK/NACK
  {
    ret = 0;
  }

  ETX_I2C_Clear_SCL();
  return ret;
}

/*
** Function to send the 7-bit address to the slave
** 
** Retutns 0 if success otherwise negative number
*/
static int ETX_I2C_Send_Addr( u8 byte, bool is_read )
{
  int ret   = -1;
  u8  bit;
  u8  i     = 0;
  u8  size  = 7;

  //Writing 7bit slave address
  for(i = 0; i < size ; i++)
  {
    bit = ( ( byte >> ( size - ( i + 1 ) ) ) & 0x01 );  //store MSB value
    (bit) ? ETX_I2C_Set_SDA() : ETX_I2C_Clear_SDA();    //write MSB value     
    I2C_DELAY;
    ETX_I2C_Set_SCL();
    I2C_DELAY;
    ETX_I2C_Clear_SCL();
  }

  // Writing Read/Write bit (8th bit)
  (is_read) ? ETX_I2C_Set_SDA() : ETX_I2C_Clear_SDA();  //read = 1, write = 0
  I2C_DELAY;
  ETX_I2C_Set_SCL();
  I2C_DELAY;
  ETX_I2C_Clear_SCL();
  I2C_DELAY;

  if( ETX_I2C_Read_NACK_ACK() )
  {
    //got ACK
    ret = 0;
  }

  return ret;
}

/*
** Function to send the one byte to the slave
** 
** Retutns 0 if success otherwise negative number
*/
static int ETX_I2C_Send_Byte( u8 byte )
{
  int ret   = -1;
  u8  bit;
  u8  i     = 0;
  u8  size  = 7;

  for(i = 0; i <= size ; i++)
  {
    bit = ( ( byte >> ( size - i ) ) & 0x01 );        //store MSB value
    (bit) ? ETX_I2C_Set_SDA() : ETX_I2C_Clear_SDA();  //write MSB value     
    I2C_DELAY;
    ETX_I2C_Set_SCL();
    I2C_DELAY;
    ETX_I2C_Clear_SCL();
  }
  
  if( ETX_I2C_Read_NACK_ACK() )
  {
    //got ACK
    ret = 0;
  }

  return ret;
}

/*
** Function to read the one byte from the slave
** 
** Retutns 0 if success otherwise negative number
*/
static int ETX_I2C_Read_Byte( u8 *byte )
{
  int ret = 0;
  
  //TODO: Implement this

  return ret;
}

/*
** Function to send the number of bytes to the slave
** 
** Retutns 0 if success otherwise negative number
*/
static int ETX_I2C_Send( u8 slave_addr, u8 *buf, u16 len )
{
  int ret = 0;
  u16 num = 0;

  do      //Break if any error
  {

    if( ETX_I2C_Send_Addr(slave_addr, false) < 0 )   // Send the slave address
    {
      pr_err("ERROR: ETX_I2C_Send_Byte - Slave Addr\n");
      ret = -1;
      break;
    }

    for( num = 0; num < len; num++)
    {
      if( ETX_I2C_Send_Byte(buf[num]) < 0 )   // Send the data bytes
      {
        pr_err("ERROR: ETX_I2C_Send_Byte - [Data = 0x%02x]\n", buf[num]);
        ret = -1;
        break;
      }
    }
  } while(false);

  return ret;  
}

/*
** Function to read the number of bytes from the slave
** 
** Retutns 0 if success otherwise negative number
*/
static int ETX_I2C_Read( u8 slave_addr, u8 *buf, u16 len )
{
  int ret = 0;
  
  //TODO: Implement this

  return ret; 
}

/*------------ I2C related functions - Bit banging method - END ----------------*/

/*
** This function used to get the functionalities that are supported 
** by this bus driver.
*/
static u32 etx_func(struct i2c_adapter *adapter)
{
  return (I2C_FUNC_I2C             |
          I2C_FUNC_SMBUS_QUICK     |
          I2C_FUNC_SMBUS_BYTE      |
          I2C_FUNC_SMBUS_BYTE_DATA |
          I2C_FUNC_SMBUS_WORD_DATA |
          I2C_FUNC_SMBUS_BLOCK_DATA);
}

/*
** This function will be called whenever you call I2C read, wirte APIs like
** i2c_master_send(), i2c_master_recv() etc.
*/
static s32 etx_i2c_xfer( struct i2c_adapter *adap, struct i2c_msg *msgs,int num )
{
  int i;
  s32 ret = 0;
  do      //Break if any error
  {

    if( ETX_I2C_Init() < 0 )                  // Init the GPIOs
    {
      pr_err("ERROR: ETX_I2C_Init\n");
      break;
    }

    ETX_I2C_Sart();                           // Send START conditon
    
    for(i = 0; i < num; i++)
    {
      //int j;
      struct i2c_msg *msg_temp = &msgs[i];

      if( ETX_I2C_Send(msg_temp->addr, msg_temp->buf, msg_temp->len) < 0 )
      {
        ret = 0;
        break;
      }
      ret++;
#if 0
      pr_info("[Count: %d] [%s]: [Addr = 0x%x] [Len = %d] [Data] = ", i, __func__, msg_temp->addr, msg_temp->len);
      
      for( j = 0; j < msg_temp->len; j++ )
      {
          pr_cont("[0x%02x] ", msg_temp->buf[j]);
      }
#endif
    }
  } while(false);

  ETX_I2C_Stop();                             // Send STOP condtion

  ETX_I2C_DeInit();                           // Deinit the GPIOs

  return ret;
}

/*
** This function will be called whenever you call SMBUS read, wirte APIs
*/
static s32 etx_smbus_xfer(  struct i2c_adapter *adap, 
                            u16 addr,
                            unsigned short flags, 
                            char read_write,
                            u8 command, 
                            int size, 
                            union i2c_smbus_data *data
                         )
{
  pr_info("In %s\n", __func__);

  // TODO: Implement this

  return 0;
}

/*
** I2C algorithm Structure
*/
static struct i2c_algorithm etx_i2c_algorithm = {
  .smbus_xfer       = etx_smbus_xfer,
  .master_xfer    = etx_i2c_xfer,
  .functionality  = etx_func,
};

/*
** I2C adapter Structure
*/
static struct i2c_adapter etx_i2c_adapter = {
  .owner  = THIS_MODULE,
  .class  = I2C_CLASS_HWMON,//| I2C_CLASS_SPD,
  .algo   = &etx_i2c_algorithm,
  .name   = ADAPTER_NAME,
  .nr     = 5,
};

/*
** Module Init function
*/
static int __init etx_driver_init(void)
{
  int ret = -1;
  
  ret = i2c_add_numbered_adapter(&etx_i2c_adapter);
  
  pr_info("Bus Driver Added!!!\n");
  return ret;
}

/*
** Module Exit function
*/
static void __exit etx_driver_exit(void)
{
  i2c_del_adapter(&etx_i2c_adapter);
  pr_info("Bus Driver Removed!!!\n");
}

module_init(etx_driver_init);
module_exit(etx_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX <[email protected]>");
MODULE_DESCRIPTION("Simple I2C Bus driver explanation");
MODULE_VERSION("1.37");

Makefile

obj-m += driver_bus.o
 
KDIR = /lib/modules/$(shell uname -r)/build
 
 
all:
    make -C $(KDIR)  M=$(shell pwd) modules
 
clean:
    make -C $(KDIR)  M=$(shell pwd) clean

I2C Client Device Driver Source code

We just took the last tutorial’s I2C Client driver example code. We made the below changes on top of that.

  • Changed the macro I2C_BUS_AVAILABLE value from 1 to 5 as we have asked our i2c bus number as 5.

[Get the source code from the GitHub]

/***************************************************************************//**
*  \file       driver_client.c
*
*  \details    Simple I2C driver explanation (SSD_1306 OLED Display Interface)
*
*  \author     EmbeTronicX
*
*  \Tested with Linux raspberrypi 5.4.51-v7l+
*
* *******************************************************************************/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#include <linux/kernel.h>
 
#define I2C_BUS_AVAILABLE   (          5 )              // I2C Bus available in our Raspberry Pi
#define SLAVE_DEVICE_NAME   ( "ETX_OLED" )              // Device and Driver Name
#define SSD1306_SLAVE_ADDR  (       0x3C )              // SSD1306 OLED Slave Address
 
static struct i2c_adapter *etx_i2c_adapter     = NULL;  // I2C Adapter Structure
static struct i2c_client  *etx_i2c_client_oled = NULL;  // I2C Cient Structure (In our case it is OLED)
 
/*
** This function writes the data into the I2C client
**
**  Arguments:
**      buff -> buffer to be sent
**      len  -> Length of the data
**   
*/
static int I2C_Write(unsigned char *buf, unsigned int len)
{
    /*
    ** Sending Start condition, Slave address with R/W bit, 
    ** ACK/NACK and Stop condtions will be handled internally.
    */ 
    int ret = i2c_master_send(etx_i2c_client_oled, buf, len);
    
    return ret;
}
 
/*
** This function reads one byte of the data from the I2C client
**
**  Arguments:
**      out_buff -> buffer wherer the data to be copied
**      len      -> Length of the data to be read
** 
*/
static int I2C_Read(unsigned char *out_buf, unsigned int len)
{
    /*
    ** Sending Start condition, Slave address with R/W bit, 
    ** ACK/NACK and Stop condtions will be handled internally.
    */ 
    int ret = i2c_master_recv(etx_i2c_client_oled, out_buf, len);
    
    return ret;
}
 
/*
** This function is specific to the SSD_1306 OLED.
** This function sends the command/data to the OLED.
**
**  Arguments:
**      is_cmd -> true = command, flase = data
**      data   -> data to be written
** 
*/
static void SSD1306_Write(bool is_cmd, unsigned char data)
{
    unsigned char buf[2] = {0};
    int ret;
    
    /*
    ** First byte is always control byte. Data is followed after that.
    **
    ** There are two types of data in SSD_1306 OLED.
    ** 1. Command
    ** 2. Data
    **
    ** Control byte decides that the next byte is, command or data.
    **
    ** -------------------------------------------------------                        
    ** |              Control byte's | 6th bit  |   7th bit  |
    ** |-----------------------------|----------|------------|    
    ** |   Command                   |   0      |     0      |
    ** |-----------------------------|----------|------------|
    ** |   data                      |   1      |     0      |
    ** |-----------------------------|----------|------------|
    ** 
    ** Please refer the datasheet for more information. 
    **    
    */ 
    if( is_cmd == true )
    {
        buf[0] = 0x00;
    }
    else
    {
        buf[0] = 0x40;
    }
    
    buf[1] = data;
    
    ret = I2C_Write(buf, 2);
}
 
 
/*
** This function sends the commands that need to used to Initialize the OLED.
**
**  Arguments:
**      none
** 
*/
static int SSD1306_DisplayInit(void)
{
    msleep(100);               // delay
 
    /*
    ** Commands to initialize the SSD_1306 OLED Display
    */
    SSD1306_Write(true, 0xAE); // Entire Display OFF
    SSD1306_Write(true, 0xD5); // Set Display Clock Divide Ratio and Oscillator Frequency
    SSD1306_Write(true, 0x80); // Default Setting for Display Clock Divide Ratio and Oscillator Frequency that is recommended
    SSD1306_Write(true, 0xA8); // Set Multiplex Ratio
    SSD1306_Write(true, 0x3F); // 64 COM lines
    SSD1306_Write(true, 0xD3); // Set display offset
    SSD1306_Write(true, 0x00); // 0 offset
    SSD1306_Write(true, 0x40); // Set first line as the start line of the display
    SSD1306_Write(true, 0x8D); // Charge pump
    SSD1306_Write(true, 0x14); // Enable charge dump during display on
    SSD1306_Write(true, 0x20); // Set memory addressing mode
    SSD1306_Write(true, 0x00); // Horizontal addressing mode
    SSD1306_Write(true, 0xA1); // Set segment remap with column address 127 mapped to segment 0
    SSD1306_Write(true, 0xC8); // Set com output scan direction, scan from com63 to com 0
    SSD1306_Write(true, 0xDA); // Set com pins hardware configuration
    SSD1306_Write(true, 0x12); // Alternative com pin configuration, disable com left/right remap
    SSD1306_Write(true, 0x81); // Set contrast control
    SSD1306_Write(true, 0x80); // Set Contrast to 128
    SSD1306_Write(true, 0xD9); // Set pre-charge period
    SSD1306_Write(true, 0xF1); // Phase 1 period of 15 DCLK, Phase 2 period of 1 DCLK
    SSD1306_Write(true, 0xDB); // Set Vcomh deselect level
    SSD1306_Write(true, 0x20); // Vcomh deselect level ~ 0.77 Vcc
    SSD1306_Write(true, 0xA4); // Entire display ON, resume to RAM content display
    SSD1306_Write(true, 0xA6); // Set Display in Normal Mode, 1 = ON, 0 = OFF
    SSD1306_Write(true, 0x2E); // Deactivate scroll
    SSD1306_Write(true, 0xAF); // Display ON in normal mode
    
    return 0;
}
 
/*
** This function Fills the complete OLED with this data byte.
**
**  Arguments:
**      data  -> Data to be filled in the OLED
** 
*/
static void SSD1306_Fill(unsigned char data)
{
    unsigned int total  = 128 * 8;  // 8 pages x 128 segments x 8 bits of data
    unsigned int i      = 0;
    
    //Fill the Display
    for(i = 0; i < total; i++)
    {
        SSD1306_Write(false, data);
    }
}
 
/*
** This function getting called when the slave has been found
** Note : This will be called only once when we load the driver.
*/
static int etx_oled_probe(struct i2c_client *client,
                         const struct i2c_device_id *id)
{
    SSD1306_DisplayInit();
    
    //fill the OLED with this data
    SSD1306_Fill(0xFF);
 
    pr_info("OLED Probed!!!\n");
    
    return 0;
}
 
/*
** This function getting called when the slave has been removed
** Note : This will be called only once when we unload the driver.
*/
static int etx_oled_remove(struct i2c_client *client)
{   
    //fill the OLED with this data
    SSD1306_Fill(0x00);
    
    pr_info("OLED Removed!!!\n");
    return 0;
}
 
/*
** Structure that has slave device id
*/
static const struct i2c_device_id etx_oled_id[] = {
        { SLAVE_DEVICE_NAME, 0 },
        { }
};
MODULE_DEVICE_TABLE(i2c, etx_oled_id);
 
/*
** I2C driver Structure that has to be added to linux
*/
static struct i2c_driver etx_oled_driver = {
        .driver = {
            .name   = SLAVE_DEVICE_NAME,
            .owner  = THIS_MODULE,
        },
        .probe          = etx_oled_probe,
        .remove         = etx_oled_remove,
        .id_table       = etx_oled_id,
};
 
/*
** I2C Board Info strucutre
*/
static struct i2c_board_info oled_i2c_board_info = {
        I2C_BOARD_INFO(SLAVE_DEVICE_NAME, SSD1306_SLAVE_ADDR)
    };
 
/*
** Module Init function
*/
static int __init etx_driver_init(void)
{
    int ret = -1;
    etx_i2c_adapter     = i2c_get_adapter(I2C_BUS_AVAILABLE);
    
    if( etx_i2c_adapter != NULL )
    {
        etx_i2c_client_oled = i2c_new_device(etx_i2c_adapter, &oled_i2c_board_info);
        
        if( etx_i2c_client_oled != NULL )
        {
            i2c_add_driver(&etx_oled_driver);
            ret = 0;
        }
        
        i2c_put_adapter(etx_i2c_adapter);
    }
    
    pr_info("Driver Added!!!\n");
    return ret;
}
 
/*
** Module Exit function
*/
static void __exit etx_driver_exit(void)
{
    i2c_unregister_device(etx_i2c_client_oled);
    i2c_del_driver(&etx_oled_driver);
    pr_info("Driver Removed!!!\n");
}
 
module_init(etx_driver_init);
module_exit(etx_driver_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX <[email protected]>");
MODULE_DESCRIPTION("Simple I2C driver explanation (SSD_1306 OLED Display Interface)");
MODULE_VERSION("1.36");

Makefile

obj-m += driver_client.o
 
KDIR = /lib/modules/$(shell uname -r)/build
 
 
all:
    make -C $(KDIR)  M=$(shell pwd) modules
 
clean:
    make -C $(KDIR)  M=$(shell pwd) clean

Testing the Device Driver

  • Build the driver by using Makefile (sudo make) in both bus and client driver directories.
  • Load the bus driver first using sudo insmod driver_bus.ko
  • Load the client driver using sudo insmod driver_client.ko
  • See the Display is filled.
  • For your understanding, I have given the I2C’s capture for the first command in SSD1306_DisplayInit() which is 0xAE.

Click here if you don’t see the timing diagram

real-i2c-bus-linux-driver-timing-diagram

  • Unload the driver using sudo rmmod driver_client
  • See the Display has been cleared.
  • Unload the driver using sudo rmmod driver_bus

Output Video

Click here if you don’t see the output gif

In our next tutorial, we will see how to write a complete I2C bus driver using  I2C-GPIO.

0 0 vote
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x