i.MX283开发板I2C驱动——DS2460

i.MX283开发板有两个I2C接口,其中I2C0接了一个DS2460加密芯片,本文介绍Linux下如何编写I2C驱动程序读写DS2460。

Linux上I2C架构可以分为I2C核心I2C总线驱动I2C设备驱动三个部分:

I2C核心:主要为总线驱动和设备驱动提供各种API,比如设备探测、注册、注销,设备和驱动匹配等函数。它在I2C架构中处于中间的位置。

I2C总线驱动:I2C总线驱动维护了I2C适配器数据结构(i2c_adapter)和适配器的通信方法数据结构(i2c_algorithm)。所以I2C总线驱动可控制I2C适配器产生start、stop、ACK等。I2C总线驱动在整个架构中是处于最底层的位置,它直接和真实的物理设备相连,同时它也是受CPU控制。

I2C设备驱动:I2C设备驱动主要负责和用户层交互,此处的设备是一个抽象的概念,并非真实的物理设备,它是挂在I2C适配器上,通过I2C适配器与真实的物理设备通信。

下图是整个I2C驱动框架:

实际上,Linux经过这么多年的发展,已经形成了一套完善的I2C驱动框架,现在编写I2C驱动,我们只需要完成上面所说的I2C设备驱动部分就可以,其他的芯片厂商已经为我们做好了。

根据bus-dev-drv框架模型,我们的主要工作是实现设备文件驱动文件,也就是上图中的i2c_client和i2c_driver,i2c_client作用是完成设备和适配器的绑定 ,以确定设备驱动需要和哪个适配器下面的真实物理设备通信,i2c_driver的作用就是实现用户层的open、write、read等调用。

下面将详细说明整个过程:

注意:i2c适配器就是cpu中的i2c接口,cpu有几个i2c接口,就代表有几个适配器,又称i2c主机。 

由于i.mx283开发板有两个i2c接口,所以这里就有两个适配器。首先假设有4个E2PROM挂在两个适配器下面,现在用户想要调

用设备驱动2来读写E2PROM3,根据上面提到的设备驱动模型,设备驱动2分为i2c_client2和i2c_driver2,首先client要为自己取

一个名字,假设叫做“E2PROM3”,然后它需要把自己和适配器2绑定(因为E2PROM3是挂在适配器2下面的),最后向内核注

册自己,I2c总线就知道自己下面多了一个设备——“E2PROM3”,i2c_client2部分的工作就做完了。

接着,需要实现i2c_driver2部分的功能,首先,i2c_driver2也需要为自己取一个名字,也必须叫“E2PROM3”,然后它需要实现

open、close、write、read等这些文件接口,对于write和read,需要使用I2C核心层提供的I2C读写数据API接口。接着,需要 

实现probe和remove函数接口,probe函数里面实现就是字符设备驱动注册的那一套流程,remove函数正好相反,最后,它也

需要向内核注册自己,也就是告诉I2C总线,有一个新的驱动需要添加——“E2PROM3”,i2c_driver2部分的工作就做完了。

i2c总线:当向内核注册i2c驱动时,会将i2c驱动添加到总线的链表中,遍历总线上所有设备,通i2c_client>namei2c_driver-

>i2c_device_id->name进行字符串匹配,如果匹配,就调用驱动程序的probe函数。上面已经将client和driver的名字都设置为

"E2PROM3",所以它们是匹配的,然后,I2C总线会调用驱动的probe函数,并把client结构体通过形参传给driver,然后执行

probe函数,注册字符设备驱动,client结构体里主要保存了适配器的信息,这个非常重要,当用户APP进行read和write调用

时,首先会进入driver里面的write和read函数,刚刚提到driver的write和read,需要使用I2C核心层提供的I2C读写数据API接

口,这些接口是用来和适配器通信的,所以需要指定哪个适配器,适配器信息就在刚刚保存的client结构体里面,这样,用户层

和适配器就是通了,最后,适配器再和挂在它下面的真实物理设备通信,这个部分是不需要我们操心的,芯片厂家已经做好了这

部分的工作,至此,整个I2C通信流程就走完了。

Linux编写I2C驱动程序的一般流程为:

  1. 创建i2c_client,并向内核注册这个client,注册的方法有图一中提到的4种方法。
  2. 创建i2c_driver,并向内核注册driver,一般使用i2c_add_driver注册driver。
  3. 注册driver时,总线会自动调用match函数,匹配client和driver,如果匹配成功,会调用driver里面的probe函数。
  4. probe函数里面完成字符设备驱动那些工作,一个I2C驱动程序基本完成。

一、设备注册

图一中提到4种注册方法,我们这里仅介绍第一种,利用i2c_new_probed_device或者i2c_new_device注册,这两个函数的区别是后者必须指定真实设备的从机地址,前者是指定一个地址范围,内核会一个个探测(发送起始信号,看是否有ACK)地址是否有效,若探测成功,则内核记录这个地址,再调用i2c_new_device注册设备。

这里会使用一个重要的结构体i2c_board_info

struct i2c_board_info {
	char		type[I2C_NAME_SIZE];
	unsigned short	flags;
	unsigned short	addr;
	void		*platform_data;
	struct dev_archdata	*archdata;
#ifdef CONFIG_OF
	struct device_node *of_node;
#endif
	int		irq;
};

它的作用是描述物理设备信息,主要是nameaddr,但内核不会探测这个addr的真实性,这个结构体是你已知真实物理设备的从机地址的情况下,可以直接指定设备信息,然后调用i2c_new_device注册设备。

我们今天使用的是i2c_new_probed_device注册设备,所以还需要给定一个地址范围。

static const unsigned short addr_list[] = 
{
    0x30,0x35,0x40,0x50,I2C_CLIENT_END,
};

addr_list数组里面就定义了地址范围,这里的0x40是ds2460的真实地址,内核会从0x30一直探测到0x50,若某个地址探测成功,它会把这个地址保存到i2c_board_info.addr成员中,然后调用i2c_new_device注册设备。

注意:无论使用哪种方式,都需指定设备的名称,即i2c_board_info.type成员。使用i2c_new_probed_device注册的好处是当

设备地址不正确时,设备是无法注册成功的!

下面是ds2460_dev.c:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/slab.h>


static struct i2c_board_info ds2460_info=
{
	I2C_BOARD_INFO("ds2460",0x40),//设备名称+设备地址7bit
};

static const unsigned short addr_list[] = 
{
    0x30,0x35,0x40,0x50,I2C_CLIENT_END,
};

struct i2c_client *ds2460_client = NULL;//定义一个client

static int ds2460_dev_init(void)
{
  struct i2c_adapter *adapter=NULL;

  /*获取i2c适配器0 */
  adapter = i2c_get_adapter(0);

  /*创建一个client 并绑定适配器*/
  ds2460_client = i2c_new_probed_device(adapter,&ds2460_info,addr_list);
  //ds2460_client = i2c_new_device(adapter,&ds2460_info);
  if(ds2460_client != NULL)
  {
	  i2c_put_adapter(adapter);
	  printk("module init ok\n");
	  return 0;
  }
  else
  {
     printk("device not exist\n");
	 return -EPERM;
  }
  
}


static void ds2460_dev_exit(void)
{
  i2c_unregister_device(ds2460_client);
  
  printk("module exit ok\n");
}


module_init(ds2460_dev_init);
module_exit(ds2460_dev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzx2020");

二、驱动注册

驱动实际就是字符设备驱动那一套流程,open、read、write等文件接口以及设备号申请、自动创建设备节点等操作。

只不过设备号申请、自动创建设备节点等操作需要放到probe函数里面去,注销操作则需要放到remove函数里面去。

这里主要讲讲read和write调用的实现:

驱动是和适配器通信的,而I2C核心为我们提供了很多和适配器通信的接口函数,具体可见/linux-2.6.35.3/Documentation/i2c

里面i2c-protocolsmbus-protocol两则文档。

下面是标准I2C协议的通信函数:

int i2c_master_send(struct i2c_client *client,const char *buf ,int count)  
int i2c_master_recv(struct i2c_client *client, char *buf ,int count)  
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) 

除此之外,还有SMBUS协议的通信函数,SMBUS是I2C协议的子集:

SMBus Receive Byte:  i2c_smbus_read_byte()
SMBus Send Byte:  i2c_smbus_write_byte()
SMBus Read Byte:  i2c_smbus_read_byte_data()
SMBus Read Word:  i2c_smbus_read_word_data()

实际上很多I2C器件用的协议都是SMBus协议,它们的时序和SMBus完全一样,所以这里我们选择SMBus的通信函数与DS2460通信。这里主要用到i2c_smbus_read_byte_data()和i2c_smbus_read_word_data()两个函数。

s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value)

/*写一个字节数据到指定的地址,地址通过command字节传送*/

/*    S Addr Wr [A] Comm [A] Data [A] P         */

/*===========================================================================*/

s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command)

/*从指定的地址读取一个字节,地址通过command字节传送,返回值是读到的字节*/

/* S Addr Wr [A] Comm [A] S Addr Rd [A] [Data] NA P  */

i2c_driver驱动还有个非常重要的结构体i2c_driver :

struct i2c_driver {
	unsigned int class;

	/* Notifies the driver that a new bus has appeared or is about to be
	 * removed. You should avoid using this if you can, it will probably
	 * be removed in a near future.
	 */
	int (*attach_adapter)(struct i2c_adapter *);
	int (*detach_adapter)(struct i2c_adapter *);

	/* Standard driver model interfaces */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);

	/* driver model interfaces that don't relate to enumeration  */
	void (*shutdown)(struct i2c_client *);
	int (*suspend)(struct i2c_client *, pm_message_t mesg);
	int (*resume)(struct i2c_client *);

	/* Alert callback, for example for the SMBus alert protocol.
	 * The format and meaning of the data value depends on the protocol.
	 * For the SMBus alert protocol, there is a single bit of data passed
	 * as the alert response's low bit ("event flag").
	 */
	void (*alert)(struct i2c_client *, unsigned int data);

	/* a ioctl like command that can be used to perform specific functions
	 * with the device.
	 */
	int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

	struct device_driver driver;
	const struct i2c_device_id *id_table;

	/* Device detection callback for automatic device creation */
	int (*detect)(struct i2c_client *, struct i2c_board_info *);
	const unsigned short *address_list;
	struct list_head clients;
};

其中,成员const struct i2c_device_id *id_table记录了驱动的名字和私有数据,驱动的名字必须和设备的名字一致,否则内核会匹配失败。

struct i2c_device_id {
	char name[I2C_NAME_SIZE];
	kernel_ulong_t driver_data	/* Data private to the driver */
			__attribute__((aligned(sizeof(kernel_ulong_t))));
};

i2c_driver 结构体填充如下:

/*配置驱动的名称和私有数据*/
static const struct i2c_device_id ds2460_id_table=
{
    "ds2460",0//名称为“ds2460”需要和设备保持一致内核才会调用驱动的probe函数
              //0 表示没有私有数据
};


/*创建i2c_driver结构体*/
static struct i2c_driver ds2460_driver =
{
  .driver={
  	   .name ="ds2460_driver",//这个名字无所谓
	   .owner=THIS_MODULE,
     },
  .probe = ds2460_probe,
  .remove= __devexit_p(ds2460_remove),
  .id_table = &ds2460_id_table,//这里的名字才是和设备进行匹配的

};

最后,在probe函数里注册常规字符设备驱动,在remove函数里注销字符设备驱动,i2c_driver工作就基本结束了。

ds2460_drv.c:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/slab.h>
#include<linux/fs.h>
#include<asm/uaccess.h> 
#include <linux/cdev.h>
#include <linux/delay.h>

#define SEQUENT_RW    0  //读写模式 0:读写不连续           1:连续读写
#define DEVICE_NAME	"ds2460_drv"//驱动名称
static struct i2c_client *ds2460_client =NULL;
static struct cdev *ds2460_cdev=NULL;
static struct class *ds2460_class = NULL;
static struct device *ds2460_device = NULL;
static dev_t device_id;


static int ds2460_open(struct inode *inode, struct file *fp)
{
  return 0;
} 


static int ds2460_release(struct inode * inode, struct file * file)
{
  return 0;
}


static int ds2460_read(struct file *filp,  char __user *buf, size_t count,
                loff_t *f_pos)
{
    unsigned char address;//读取的地址
	unsigned char *data = NULL;//需要读取的数据
	unsigned char i;
    int ret;	
#if (SEQUENT_RW == 1)

	else if(count == 1)
	{
	    ret = copy_from_user(&address, buf, 1);
	    if(ret < 0)
	    { 
	       printk("read param num error ret = %d ,buf = %d\n",ret,(int)buf[0]);
		   return -EINVAL;     
		}
	    ret = i2c_smbus_read_byte_data(ds2460_client,address);
		if(ret < 0)
		{
	      printk("i2c read error %d\n",ret);
		  return ret;
		}
		copy_to_user(buf, &ret, 1);
	}
	else
	{
	   data = kmalloc(count, GFP_KERNEL);//申请count字节内存 
       ret = copy_from_user(data, buf, 1);//第1个字节是地址
	   if(ret < 0)
	   {
	       printk("write param num error \n");
		   kfree(data);
		   return -EINVAL;     
	   }
       address = data[0];	   
       ret = i2c_smbus_read_i2c_block_data(ds2460_client,address,count,data);
       if(ret < 0)
	   {
	      printk("i2c_smbus_read_i2c_block_data error %d\n",ret);
		  kfree(data);
		  return ret;
	   }	   
       copy_to_user(buf, data, count);  
	   kfree(data);

	}
#else
	 /*申请内存*/
     data = kmalloc(count,GFP_KERNEL);  
	 ret = copy_from_user(data, buf, 1);
	 if(ret < 0)
	 { 
       printk("read param num error ret = %d ,buf = %d\n",ret,(int)buf[0]);
	   return -EINVAL;     
     } 
	 
	 /*用户buf第一个字节是需要读取的地址*/
     address = data[0];

	 /*依次读取数据*/
	 for(i=0;i<count;i++,address++)
	 {
	    /*非连续读取 每次读取产生一次完整的I2
C通信*/
	    data[i] = i2c_smbus_read_byte_data(ds2460_client,address);
		if(data[i] < 0)
		{
	      printk("i2c read error %d\n",data[i]);
		  return data[i];
		}  
	 }
	 
	 /*将数据拷贝到用户层buf*/
     copy_to_user(buf, data, count); 
	 
	 /*释放内存*/
	 kfree(data);
#endif
	 return 0;
   
}

ssize_t ds2460_write(struct file *filp, const char __user *buf, size_t count,
								loff_t *f_pos)
{

   unsigned char address;//需要写入的地址
   unsigned char *data = NULL;//需要写入的数据
   unsigned char i;
   int ret;
   
   /*至少写入1个数据 即count至少等于2*/
   if(count < 2)
   {
       printk("write param num error count = %d\n",count);
	   return -EINVAL;     
   }
#if (SEQUENT_RW == 1)
   else if(count == 2)
   {
       data = kmalloc(2,GFP_KERNEL);  
	   ret = copy_from_user(data, buf, 2);
	   if(ret < 0)
	   {
	       printk("write param num error \n");
		   kfree(data);
		   return -EINVAL;     
	   }
	   address = data[0];
	   ret=i2c_smbus_write_byte_data(ds2460_client,address,data[1]);
	   if(ret < 0)
	   {
	      printk("i2c_smbus_write_byte_data error %d\n",ret);
		  kfree(data);
		  return ret;
	   }
	   kfree(data);
   }
   else 
   {
       data = kmalloc(count,GFP_KERNEL);  
	   ret = copy_from_user(data, buf, count);
	   if(ret < 0)
	   {
	       printk("write param num error %d\n",ret);
		   kfree(data);
		   return -EINVAL;     
	   }
	   
	   address = data[0];
	   
	   ret=i2c_smbus_write_i2c_block_data(ds2460_client,address,count-1,&data[1]);
	   if(ret < 0)
	   {
	      printk("i2c_smbus_write_i2c_block_data error %d\n",ret);
		  kfree(data);
		  return ret;
	   }
	   kfree(data);
   }
#else
       /*申请内存*/
       data = kmalloc(count,GFP_KERNEL); 

       /*拷贝count个字节到刚刚申请的内存中*/
	   ret = copy_from_user(data, buf, count);
	   if(ret < 0)
	   {
	       printk("write param num error %d\n",ret);
		   kfree(data);
		   return -EINVAL;     
	   }

	   /*用户buf第一个字节是需要写入的地址*/
       address = data[0];

	   /*依次写入数据*/
	   for(i=1;i<=count-1;i++,address++)
	   {
	       /*非连续读写      每写入1个字节产生一次完整的I2C通信*/
           ret=i2c_smbus_write_byte_data(ds2460_client,address,data[i]);
		   if(ret < 0)
		   {
		      printk("i2c_smbus_write_byte_data error %d\n",ret);
			  kfree(data);
			  return ret;
		   }
		   /*E2PROM写入需要延时10ms*/
           mdelay(10);
	   }
	   
	   /*释放内存*/
	   kfree(data);
#endif
       return 0;

}

static struct file_operations ds2460_fops = 
{

  .owner   = THIS_MODULE,
  .open    = ds2460_open,
  .release = ds2460_release,
  .read    = ds2460_read,
  .write   = ds2460_write,
};


static int __devinit ds2460_probe(struct i2c_client *client,
				        const struct i2c_device_id *id)
{
     int ret;

	 /*获取当前操作的设备    */
     ds2460_client = client;
   
	 /*申请设备号*/
     ret = alloc_chrdev_region(&device_id, 0, 1, DEVICE_NAME);
	 if(ret < 0)
	 {
		 printk(KERN_ERR "alloc dev_id error %d \n", ret);
		 return ret;
	 }
	 
     /*分配一个cdev结构体*/
	 ds2460_cdev = cdev_alloc(); 
	 if(ds2460_cdev != NULL)
	 {
	     /*初始化cdev结构体*/
         cdev_init(ds2460_cdev, &ds2460_fops);

		 /*向内核添加该cdev结构体*/
		 ret = cdev_add(ds2460_cdev, device_id, 1);
		 if(ret != 0)
		 {
	       printk("cdev add error %d \n",ret);
		   goto error;
		 }
	 }
	 else
    {    
	     printk("cdev_alloc error \n");
	     return -1;
    }
	 
	 /*创建一个class*/
	ds2460_class = class_create(THIS_MODULE, "ds2460_class"); 
	if(ds2460_class != NULL)
	{
        ds2460_device = device_create(ds2460_class, NULL, device_id, NULL, DEVICE_NAME);  
	}
    else
    {
       	 printk("class_create error\n");
	     return -1;
	}
	
	return 0;
error:
	cdev_del(ds2460_cdev);
	unregister_chrdev_region(device_id, 1);
	return -1;
}


static int __devexit ds2460_remove(struct i2c_client *client)
{
    /*删除cdev结构体*/
    cdev_del(ds2460_cdev);

	/*释放设备号*/
	unregister_chrdev_region(device_id, 1);

	/*删除设备*/
	device_del(ds2460_device);

	/*删除类*/
	class_destroy(ds2460_class);
	return 0;
}

/*配置驱动的名称和私有数据*/
static const struct i2c_device_id ds2460_id_table=
{
    "ds2460",0//名称为“ds2460”需要和设备保持一致内核才会调用驱动的probe函数
              //0 表示没有私有数据
};


/*创建i2c_driver结构体*/
static struct i2c_driver ds2460_driver =
{
  .driver={
  	   .name ="ds2460_driver",//这个名字无所谓
	   .owner=THIS_MODULE,
     },
  .probe = ds2460_probe,
  .remove= __devexit_p(ds2460_remove),
  .id_table = &ds2460_id_table,//这里的名字才是和设备进行匹配的

};
	

static int ds2460_drv_init(void)
{
  /*向内核注册驱动 如果和设备匹配 会执行probe函数*/
  i2c_add_driver(&ds2460_driver);
  
  printk("module init ok \n");
  return 0;
}


static void ds2460_drv_exit(void)
{  
   /*向内核注销驱动 如果和设备匹配 会执行remove函数*/
   i2c_del_driver(&ds2460_driver);
   
   printk("module exit ok \n");
}


module_init(ds2460_drv_init);
module_exit(ds2460_drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzx2020");

最后,编写测试函数:

DS2460是一个SHA加密芯片,但是它内部包含有一个112字节的E2PROM区域,其E2PROM区域的首地址是0x80,我们首先向这个位置写入50个数据,再读取50个数据,对比看下是否正确,最后再读下芯片ID。

ds2460_test.c:

#include<stdio.h>	/* using printf()        */
#include<stdlib.h>      /* using sleep()         */
#include<fcntl.h>       /* using file operation  */
#include<sys/ioctl.h>   /* using ioctl()         */
#include <asm/ioctls.h>
#include <unistd.h> //sleep  write read close


int main(int argc, const char * argv [ ])
{
  int fd,i;
  int value = 0;
  unsigned char txbuf[51],rxbuf[50];
  fd = open("/dev/ds2460_drv",O_RDWR);
  if(fd < 0)
  {
     printf("open ds2460_drv error %d\n",fd);
	 return 0;
  }

  txbuf[0] = 0x80;//需要写入的地址
  
  for(i = 1;i<=50;i++)
  {
    txbuf[i] = i;//填充写入的数据
  }

  printf("write:\n");
  for(i=0;i<=50;i++)
  {
	 printf("%d ",txbuf[i]);
  }
  printf("\n");//打印要写入的数据
  
  write(fd,txbuf,51);//写入芯片0x80的位置
 
  rxbuf[0] = 0x80;//读取的地址
  
  read(fd,rxbuf,50);//从0x80读取50个字节

  printf("read ds2460:\n");
  
  for(i = 0; i < 50;i++)
  {
    printf("%d ",rxbuf[i]);//打印读到的数据
  }
  printf("\n");


  rxbuf[0] = 0xF0;//读芯片ID
  read(fd,rxbuf,8);  
  printf("ID:\n"); 
  for(i = 0; i < 8;i++)
  {
    printf("%x ",rxbuf[i]);
  }
  printf("\n");
  
  
  return 0;
}

编译ds2460_dev.c,ds2460_drv.c,ds2460_test,得到三个文件:ds2460_dev.ko /ds2460_drv.ko /ds2460_test.

在开发板上加载前面两个驱动模块,再执行最后一个测试程序:

可以看到,驱动加载成功,写入和读取的数据也是一致的,芯片ID(低字节在前)为:3C 53 7F 3e 0 0 0 39

我们编写的I2C驱动没有问题。

后记:

1.在linux系统下编写I2C驱动,目前主要有两种方法,一种是把I2C设备当作一个普通的字符设备来处理,另一种是利用linux下I2C驱动体系结构来完成。下面比较下这两种方法:
第一种方法:
优点:思路比较直接,不需要花很多时间去了解linux中复杂的I2C子系统的操作方法。
缺点: 要求工程师不仅要对I2C设备的操作熟悉,而且要熟悉I2C的适配器(I2C控制器)操作。
       要求工程师对I2C的设备器及I2C的设备操作方法都比较熟悉,最重要的是写出的程序可以移植性差。
       对内核的资源无法直接使用,因为内核提供的所有I2C设备器以及设备驱动都是基于I2C子系统的格式。

第一种方法的优点就是第二种方法的缺点,
第一种方法的缺点就是第二种方法的优点。

2.E2PROM支持连续取,对于DS2460,发送一次起始信号最多可连续读取8字节,但是本文没有采用这一方式(ds2460_drv.c文件 中有宏定义开关可以打开),本文采用的是最原始的方式,即每读写一个字节产生一次完整的I2C通信,这种方式会大大影响速度。

3.linux内核源码/linux-2.6.35.3/drivers/i2c中有I2C相关代码

busses/i2c-mxs.c:总线驱动文件即I2C适配器的驱动文件,包含I2C基本通信函数。

i2c-core.c:I2C核心文件,主要提供API,与硬件无关。

i2c-dev.c:通用设备驱动文件,它不针对某一款I2C芯片,它提供通用的方式让用户操作I2C设备,同时也是一个字符设备驱动。
用户直接open这个设备驱动就是上面讲的把I2C设备当作一个普通的字符设备来处理,I2C所有通信细节都需要用户自己完成。

下面推荐几篇写的比较好的Linux I2C驱动框架文章:

Linux I2C驱动框架(超详细)

Linux3.5下I2C设备驱动程序

linux下I2C驱动架构全面分析

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值