S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。

这篇文档算上期末复习这段时间其实拖了有好久了,因为从一开始接触linux的i2c驱动体系我就各种凌乱,因为起初脑海中既没有整体框架也不熟悉相关体系下的结构,所以四处乱看,经常性的在看内核代码时看着看着就把自己看飞了。结果就总是无功而返。现在我决定当前阶段把结构熟悉,知道大致的驱动体系框架。日后真正做到此类项目时再来深入理解、巩固、总结。

因为我所要操作的EEPROM使用的是I2C接口。那么自然要先了解一下I2C总线协议。对于I2C总线协议我的理解:两条线“SCL和SDA”,一个数据信号一个时钟脉冲信号。俩线都是高电平时,SDA从高到低发出一个跳变,便是开始信号;然后就是发送数据。若SDA从低到高再跳变一次就是结束信号。这期间,SCL为高时SDA必须保持稳定无变化,因为SCL为高时会采样数据。若此时SDA变化,可能导致误发结束信号而产生终止。也就是说SCL高,SDA保持稳定,则数据有效,SDA的改变只能发生在SCL的低电平期间。当然接收器每接收一个字节数据都会产生一个ASK回应信号,表示已经收到数据。
下面是I2C总线的传输时序图。
S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第1张图片
再具体到Linux下驱动子系统I2C驱动体系结构(I2C总线驱动;I2c核心;I2C从设备驱动):
首先我们要知道,I2c驱动体系中一般来说硬件上可含有一个或多个从设备,一个或多个适配器,这俩之间的关系:一个适配器可以控制多个从设备;且我们既可以通过直接编写从设备驱动来操控他,也可以通过操作适配器来操控从设备。

现在这样来理解:linux下的I2C驱动子系统,相对硬件来说肯定必须得先有驱动。有了驱动从设备才能有效工作,才能软性的帮助适配器操控从设备工作。所以对于S3C2440开发板我们要知道:

 (1)2440中的I2C控制器(i2c-s3c2410)有一个驱动(s3c2440中的I2C适配器驱动基于platform实现)。这个用来操作控制器来产生特定的I2C的时序信号,来发送数据和接收数据。也就是让适配器工作。

(2)挂接在I2C总线上的从设备AT24C02(e2prom)为例,它也有一个驱动,这个用来操作读写我们的芯片,读取和存放具体获得的数据。

在Linux系统中,上述的两个驱动,第一个属于I2C总线驱动,第二个属于I2C设备驱动。

======================================================================================================

上面说到了I2C总线驱动I2C设备驱动,对于I2C驱动体系结构还有个最重要的就是I2C核心(出厂时内核已经自带)。既然是核心,那自然是通过一系列的通信方法(algorithm)把总线驱动,设备驱动与用户层串起来,其中还包括有总线驱动和设备驱动的注册,注销方法。

======================================================================================================

现在我们知道:I2C驱动体系结构分为三大部分:I2C总线驱动; I2C 核心; I2C设备驱动。 

=====================================================================================================
注意:一般来说,如果CPU中集成了I2C控制器并且Linux内核支持这个CPU,那么总线驱动方面就不用我们操心了,内核已经做好了。但如果CPU中没有I2C控制器,而是外接的话,那么就要我们自己实现总线驱动了。对于设备驱动来说,一般常用的驱动也都包含在内核中了,如果我们用了一个内核中没有的芯片,那么就要自己来写了。
=====================================================================================================

第一部分:

Linux中的I2C总线驱动(s3c2440的总线驱动i2c-s3c2410.c)

定义描述具体I2C总线适配器的i2c_adapter数据结构、实现在具体I2C适配器上的I2C总线通信方法,并由i2c_algorithm数据结构进行描述。 经过I2C总线驱动的的代码,可以为我们控制I2C产生开始位、停止位、读写周期以及从设备的读写、产生ACK等。
I2C总线驱动具体实现在/drivers/i2c目录下busses文件夹。例如:Linux I2C GPIO总线驱动为i2c_gpio.c. I2C总线算法在/drivers/i2c目录下algos文件夹。例如:Linux I2C GPIO总线驱动算法实现在i2c_algo_bit.c.

T:I2C总线适配器由i2c_adapter结构体描述;I2C适配器上的I2C总线通信方法由i2c_algorithm结构体描述。一个总线驱动用于支持一条特定的I2C总线的读写。一个总线驱动通常需要两个模块,struct i2c_adapter和struct i2c_algorithm来描述

I2C_adapter:构造一个对I2C core层接口的数据结构,并通过接口函数向I2C core注册一个控制器adapter。

i2c_algorithm主要实现对I2C总线访问通信的具体算法。

第二部分:

Linux中的I2C设备驱动(at24.c属于s3c2440的设备AT24C02的驱动代码)

是对具体I2C硬件驱动的实现。I2C 设备驱动通过I2C适配器与CPU通信。其中主要包含i2c_driveri2c_client数据结构,i2c_driver结构对应一套具体的驱动方法,例如:probe、remove、suspend等,需要自己申明。i2c_client数据结构由内核根据具体的设备注册信息自动生成,设备驱动根据硬件具体情况填充。具体使用下面介绍。

I2C 设备驱动具体实现放在在/drivers/i2c目录下chips文件夹。

i2c-dev.c:提供了用户层对I2C设备的访问,包括open,read,write,ioctl,release等常规文件操作,我们可以通过open函数打开 I2C的设备文件,通过ioctl函数设定要访问从设备的地址,然后可以通过 read和write或者ioctl函数完成对I2C设备的读写操作。

I2C_client:对应于真实的物理设备,结构体中包含了芯片地址,设备名称,设备使用的中断号,设备所依附的控制器,设备所依附的驱动等内容。

第三部分:

I2C core核心(s3c2440的代码i2c-core.c)

提供了核心数据结构的定义和相关接口函数,用来实现I2C适配器驱动和设备驱动的注册、注销管理,以及I2C通信方法上层的、与具体适配器无关的代码以及探测设备,检测设备地址的上层代码,为系统中每个I2C总线增加相应的读写方法。

增加/删除i2c_adapter
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_del_adapter(struct i2c_adapter *adap)
//int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)

i2c_client依附/脱离
int i2c_attach_client(struct i2c_client *client)
int i2c_detach_client(struct i2c_client *client)

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)

以上是I2C驱动的大致的框架。有了大概的了解以后,就涉及到我们具体的设备使用了。毕竟I2C驱动也是为了我们能够更好的使用设备。

1.首先对于我们的开发板来说我们需要进入到内核中去对其进行配置:

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第2张图片S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第3张图片

发现网上的这个图更方便,两张就概括了。所以直接截图了,另外我是linux3.0内核。注:配置图片来自:http://blog.csdn.net/yj4231/article/details/18145973

2.修改文件: linux/arch/arm/mach-s3c2440/mach-smdk2440.c
增加如下代码片段:

#include 
static struct at24_platform_data at24c02 = {
	.byte_len	= SZ_2K / 8,
	.page_size	= 8,
	.flags		= 0,
};

static struct i2c_board_info __initdata smdk_i2c_devices[] = {
	/* more devices can be added using expansion connectors */
	{
		I2C_BOARD_INFO("24c02", 0x50),
		.platform_data = &at24c02,
	},
};
在smdk2440_machine_init函数中增加如下:

i2c_register_board_info(0, smdk_i2c_devices, ARRAY_SIZE(smdk_i2c_devices));

=====================================================================================================

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第4张图片

==================================================================================================

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第5张图片

==================================================================================================

由datasheet中的A0,A1,A2以及百度百科AT24C02的芯片地址介绍可知设备地址为0x 10100000,这里确实与我们平常代码中的0x50不一致。我找寻了许多资料,只发现下面这种较为合理且清晰的解释:手册中at24c02的设备地址是0b 1 0 1 0 0 0 0 R/W, 其最低位是读写标志位,但是在Linux中,I2C设备地址的最高位为0,而低七位地址就是手册中去掉R/W的剩余7位。因此,地址为0b 01010000(0x50)

==================================================================================================

3.然后重新编译进内核,启动开发板后进入目录/sys/devices/platform/s3c2440-i2c/i2c-0/0-0050/可以查看到name与eeprom显示如下:

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第6张图片

现在我们来谈谈具体编程操作,网上说编写I2C设备驱动有两种方法:

一种是利用系统给我们提供的i2c-dev.c来实现一个i2c适配器的设备文件。然后通过在应用层操作I2C适配器来控制i2c设备。另一种是为i2c设备,独立编写一个设备驱动。

我这里实现的都是利用i2c-dev.c提供的函数接口在应用层通过适配器来控制I2c设备。

code 1:

/*********************************************************************************
 *      Copyright:  (C) 2015 songyong
 *                  All rights reserved.
 *
 *       Filename:  i2c.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2015年05月23日)
 *         Author:  sky 
 *      ChangeLog:  1, Release initial version on "2015年05月23日 16时34分51秒"
 *                 
 ********************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include


#define LEN 256
#define PRINT_ERROR printf("error: %d:%s",errno,strerror(errno))


/********************************************************************************
 *  Description:
 *   Input Args:
 *  Output Args:
 * Return Value:
 ********************************************************************************/
int main (int argc, char **argv)
{
   int ret,fd,i,j;
   char read_data[LEN];
   char write_data[LEN] ="\n";
   char offset[LEN];

   memset(offset,0,sizeof(offset));
   fd = open("/sys/devices/platform/s3c2440-i2c/i2c-0/0-0050/eeprom",O_RDWR);
   if(fd < 0)
   {
       printf("Open AT24C02 fail.\n"); 
        return -1;
   }
   /*  
   ret = read(fd, offset,256);
   if(ret < 0)
   {
       printf("Read AT24C02 fail.\n"); 
        return -1;
   }
    strncpy(read_data, offset,sizeof(offset));
    for(i = 0 ; i

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第7张图片

上面是通过at24.c的read和write以及lseek直接对I2C设备(EEPROM)进行读写操作,其实依然是利用i2c-dev.c,最终调用的还是其中的read,write;在open打开设备节点后能我们虽然能直接对从设备EEPROM进行读写操作,但不方便的是不能随意指定地址插入数据,只能通过偏移量来改变或者插入数据。(此类方法是把I2C设备当作一个普通的字符设备来处理

code2:

/*********************************************************************************
 *      Copyright:  (C) 2015 songyong
 *                  All rights reserved.
 *
 *       Filename:  i2c_master.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2015年05月28日)
 *         Author:  sky 
 *      ChangeLog:  1, Release initial version on "2015年05月28日 21时06分40秒"
 *                 
 ********************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
/********************************************************************************
 *  Description:
 *   Input Args:
 *  Output Args:
 * Return Value:
 ********************************************************************************/
int main (int argc, char **argv)
{
    int i, fd, ret, length;
    unsigned char rdwr_addr = 0x00;  //eeprom 读写地址
    unsigned char device_addr = 0x50; //eeprom 设备地址
    unsigned char data[] = "test1234\n";
    struct i2c_rdwr_ioctl_data e2prom_data;

    printf("open i2c device...\n");
    fd = open("/dev/i2c-0",O_RDWR);
    if(fd < 0)
            {
                        printf("open faild");
                                return -1;
                
            }
     e2prom_data.msgs =(struct i2c_msg *)malloc(e2prom_data.nmsgs * sizeof(struct i2c_msg));
     if(e2prom_data.msgs == NULL)
     {
                printf("malloc error");
                        return -1;
                            
     }
    
    ioctl(fd, I2C_TIMEOUT, 1);// 设置超时
    ioctl(fd, I2C_RETRIES, 2);// 设置重试次数
            
     /*  向e2prom中写入数据 */
     length = sizeof(data);
     e2prom_data.nmsgs = 1;
     e2prom_data.msgs[0].len = length;
     e2prom_data.msgs[0].addr = device_addr;
                          
     e2prom_data.msgs[0].buf =(unsigned char *)malloc(length);
     e2prom_data.msgs[0].buf[0] = rdwr_addr;
     for(i = 0;i

同样是操作i2c设备eeprom,这份代码却和上面把设备当作普通文件来进行读写操作的代码不太相同,下面我们通过代码的具体实例来了解一下I2C驱动体系中的相关结构:

在linux下的I2C驱动体系中,我们往往会通过I2C控制器来操控I2C从设备,而其在内核中消息的传递又涉及到内核中相关的结构体并且必须要遵循如下读写时序:

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第8张图片S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第9张图片

上述代码我们是用cpu内部的I2C控制器通过i2c-dev.c提供的文件接口来操控从设备。其中我们首先要接触的就是i2c-dev.c里面的i2c_rdwr_ioctl_data结构体。这里要提一下,i2c-dev.c中的read、write函数不适用于多个开始信号的数据传送。只适合发了开始信号之后的一次性数据传输。一句话就是ioctl可替代read,write,且比它们都好用。(i2c_msg与i2c_rdwr_ioctl_data结构体都是定义在i2c-dev.h头文件中)

struct i2c_rdwr_ioctl_data {  
         struct i2c_msg __user *msgs;         /* pointersto i2c_msgs */  
         __u32 nmsgs;                    /* number ofi2c_msgs */  
};  //Msgs     表示单个开始信号传递的数据;
	//Nmsgs     表示有多少个msgs
从上面的结构体我们再来看看里面的i2c_msg这个结构体
struct i2c_msg {  
         __u16 addr;     /* slave address                         */  
         __u16 flags;  /* 默认为写入 0 */  
         __u16 len;                  /* msg length                              */  
         __u8 *buf;                 /* pointer to msgdata                       */  
};  

我们在ioctl调用之前必须先填充好i2c_msg消息结构体以及nmsgs成员。我们可以看到消息结构体里面有从设备地址,读写标志,数据长度以及存储数据buf。这些成员我们看完之后会发现它大致符合先给设备地址,然后给写信号以及数据的时序。其实但我们写代码的时候并不一定是addr非得定义在flags前面,因为内核会自动帮助我们完成这些具体的时序操作。但有一点,我们必须要填充好nmsgs以及i2c_msg中的成员

那么我们具体的i2c下的 ioctl 函数是怎么样的呢?我们暂且把i2c-dev.c看作一个设备驱动。里面的fops结构体显示

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第10张图片

我们继续追踪看看i2cdev_ioctl这个函数

 static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct i2c_client *client = file->private_data;
    unsigned long funcs;

    dev_dbg(&client->adapter->dev, "ioctl, cmd=0x%02x, arg=0x%02lx\n",
        cmd, arg);

    switch (cmd) {
    case I2C_SLAVE:
    case I2C_SLAVE_FORCE:
        /* NOTE:  devices set up to work with "new style" drivers
         * can't use I2C_SLAVE, even when the device node is not
         * bound to a driver.  Only I2C_SLAVE_FORCE will work.
         *
         * Setting the PEC flag here won't affect kernel drivers,
         * which will be using the i2c_client node registered with
         * the driver model core.  Likewise, when that client has
         * the PEC flag already set, the i2c-dev driver won't see
         * (or use) this setting.
         */
        if ((arg > 0x3ff) ||
            (((client->flags & I2C_M_TEN) == 0) && arg > 0x7f))
            return -EINVAL;
        if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg))
			            return -EBUSY;
        /* REVISIT: address could become busy later */
        client->addr = arg; //设置addr
        return 0;
    case I2C_TENBIT://设置10 bit地址模式
        if (arg)
            client->flags |= I2C_M_TEN;
        else
            client->flags &= ~I2C_M_TEN;
        return 0;
    case I2C_PEC://设置传输后增加PEC标志
        if (arg)
            client->flags |= I2C_CLIENT_PEC;
        else
            client->flags &= ~I2C_CLIENT_PEC;
        return 0;
    case I2C_FUNCS://获取函数支持
        funcs = i2c_get_functionality(client->adapter);
        return put_user(funcs, (unsigned long __user *)arg);

    case I2C_RDWR://读取和发送数据
        return i2cdev_ioctl_rdrw(client, arg);

    case I2C_SMBUS: //SMBUS协议数据传输
        return i2cdev_ioctl_smbus(client, arg);
		
    case I2C_RETRIES://设置重试次数
        client->adapter->retries = arg;
        break;
    case I2C_TIMEOUT://设置超时时间
        /* For historical reasons, user-space sets the timeout
         * value in units of 10 ms.
         */
        client->adapter->timeout = msecs_to_jiffies(arg * 10);
        break;
    default:
        /* NOTE:  returning a fault code here could cause trouble
         * in buggy userspace code.  Some old kernel bugs returned
         * zero in this case, and userspace code might accidentally
         * have depended on that bug.
         */
        return -ENOTTY;
    }
    return 0;
}
对于简单使用来说,我现在并没有全深入整明白,所以暂且知道:

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。
在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}的结构,每一个case对应一个cmd操作命令,并有相对应的操作。

我这里cmd参数使用的是I2C_RDWR这个命令码,根据i2c-dev.c里的源码看i2cdev_ioctl_rdrw结构体可知

I2C 设备的写操作经历了如下几个步骤。
(1) 从用户空间到字符设备驱动写函数接口,写函数构造 I2C 消息数组。
(2) 写函数把构造的 I2C 消息数组传递给 I2C 核心的传输函数 i2c_transfer()。
(3) I2C 核心的传输函数 i2c_transfer()找到对应适配器 algorithm 的通信方法函数 master_xfer()去最终完成 I2C 消息的处理。

PS:I2c_transfer这个函数实现了core与adapter的联系。想更深入的探究,可以自己去看看I2C总线驱动中的I2C_algorithm结构以及其中的s3c24xx_i2c_xfer(),s3c24xx_i2c_doxfer()和s3c24xx_i2c_message_start()函数和i2c_transfer()函数。

下面我们看看代码在开发板中运行的现象:

1.在把可执行文件放入开发板启动之前我们先检查下I2C控制器s3c2410-i2c节点是否配置好。

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第11张图片

上面的i2c_dev/i2c-0是在注册i2c-dev.c后产生的,代表一个可操作的适配器。如果不使用i2c-dev.c的方式,就没有,也不需要这个节点。

2.确保出现I2C控制器s3c2410-i2c后即可通过交叉编译器将编译后可执行文件放入开发板中执行。

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第12张图片

仔细看上面的结果。可以看到我们原本的数据test1234\n在上面的显示中出了点问题。后来我又测试一次test123\n与test12345678

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第13张图片

我第一次是通过at24.c的read和write以及lseek直接对eeprom读写,所以可以一次写入超过8个字节,第二次通过内部i2c控制器的ioctl来间接读写eeprom时则遭遇了阻击,一次时序,若超过8个字节,超过的字节数自动将前面的数据覆盖掉。然后我想到at24c02是32个页,一页8个字节。通过网上查阅知:

===================================================================================================

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第14张图片

================================================================================================================================

由于E2PROM的半导体工艺特性,对E2PROM的写入时间要5~10ms,但AT24CXX系列串行E2PROM芯片内部设置了一个具有SRAM性质的输入缓冲器,称为页写缓冲器。CPU对该芯片写操作时,AT24CXX系列芯片先将CPU输入的数据暂存页写缓冲器内,然后,慢慢写入E2PROM中。因此,CPU对AT24CXX系列E2PROM一次写入的数据,受到该芯片页写缓冲器容量的限制。页写缓冲器的容量:AT24C01A/02为8B,AT24C04/08/16为16B,AT24C32/64为32B。

====================================================================================================
注意:
写AT24CXX应用时,若CPU需写入超过芯片页写缓冲器容量的字节数据,应在一页写完后,隔5~10ms重新启动一次写操作。其次,若不是从页写缓冲器零地址(指AT24CXX片内末位地址0或8)写起,一次写入不能超出页内地址111,若超出页写缓冲器最大地址时,也应将超出部分,隔5~10ms重新启动一次写操作。

最后通我们回顾AT24C02的官方datasheet来看看本质:

Byte write:

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第15张图片S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第16张图片

==================================================================================================================

Byte write的操作时序如上图所示。主机在发送device address,并且接受到确定回应Ask后再接着发送需要写的地址(把这个数据写到芯片的哪个地址上),然后收到确定回应ask后再发送数据。当AT24C02接受完毕这个数据时会输出一个确认回应Ack,此时主机发送一个停止信号Stop,然后AT240C2进入写时序,将刚才接受到的数据从缓冲器写到存储单元中,并在此期间不响应任何输入,直到操作完成。

==================================================================================================================

Pagewrite:

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第17张图片S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第18张图片

==================================================================================================================

Page write前面几步的操作和Bytewrite操作类似,只是在成功发送第一个数据之后,主机在收到AT24C02的确认回应Ask之后不会发送停止信号Stop而是继续发送剩余的7个字节数据。直到一个page的8字节数据发送完毕之后才发送停止信号Stop。在页操作的时候word address用与表示业内的低地址的低3bit会每收到一个数据就自动增长,页地址维持不变。所以,当业内地址到顶端时,此时假如还有数据,则数据将会被放到页的起始地址处,页起始地址中之前存放的数据也将会被覆盖。即AT24C02页操作时,写入的数据大于8byte,则大于8byte的数据将重新从此页起始处存放,并覆盖掉之前写入的的数据

==================================================================================================================

另外我们再说一下随机读取:

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第19张图片S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第20张图片

==================================================================================================================

随机读写的操作就是主机先用一个写操作来骗过AT24C02器件,使AT24C02内部的data word address中的地址值修改,然后再通过current address read操作来读取所需地址上的数据。平时我们所说的读操作之前往往要先写之后才能读。这里也是如此,我们主机先发送一个写操作,但是发送完毕之后并不发送数据,而是发送一个Start开始信号,此时AT24C02中的data wordaddress中的地址值已经修改了,然后通过current address read去读取当前地址上的数据。

==================================================================================================================

最后贴上Linux下I2C驱动体系容易理解的图解:

S3C2440 Linux下的I2C驱动以及I2C体系下对EEPROM进行读写操作。_第21张图片


Reference:

http://www.360doc.com/content/13/1207/14/3038654_335207399.shtml#

http://blog.csdn.net/paul_liao/article/details/6982883

http://blog.csdn.net/ypoflyer/article/details/6376545

你可能感兴趣的:(Linux下的I2C驱动体系)