关于I2C的那点事:i2c_master_send 和 i2c_master_recv i2c_transfer

做linux 嵌入式驱动,离不开调试i2c 外设,这里对i2c做一下3点总结:

1.先要知道i2c的4个信号;

a) 起始信号:当 SCL 线是高电平时 SDA 线从高电平向低电平切换。 

b) 停止信号:当 SCL 线是高电平时 SDA 线由低电平向高电平切换。

c)   ACk信号: 当scl线由低变高时候,SDA线保持低电平;

d)  NACK信号:当scl线由低变高时候,SDA线保持高电平;

2、通信接口
i2c发送或者接收一次数据都以数据包 struct i2c_msg 封装
[cpp] 
struct i2c_msg { 
    __u16 addr;     // 从机地址 
    __u16 flags;    // 标志 
#define I2C_M_TEN   0x0010  // 十位地址标志 
#define I2C_M_RD    0x0001  // 接收数据标志 
    __u16 len;      // 数据长度 
    __u8 *buf;      // 数据指针 
}; 
其中addr为从机地址;flags则是这次通信的标志,发送数据为0,接收数据则为 I2C_M_RD;len为此次通信的数据字节数;buf 为发送或接收数据的指针。在设备驱动中我们通常调用 i2c-core 定义的接口 i2c_master_send 和 i2c_master_recv 来发送或接收一次数据。
[cpp]
int i2c_master_send(struct i2c_client *client,const char *buf ,int count) 

    int ret; 
    struct i2c_adapter *adap=client->adapter;  // 获取adapter信息 
    struct i2c_msg msg;                        // 定义一个临时的数据包 
 
    msg.addr = client->addr;                   // 将从机地址写入数据包 
    msg.flags = client->flags & I2C_M_TEN;     // 将从机标志并入数据包 
    msg.len = count;                           // 将此次发送的数据字节数写入数据包 
    msg.buf = (char *)buf;                     // 将发送数据指针写入数据包 
 
    ret = i2c_transfer(adap, &msg, 1);         // 调用平台接口发送数据 
 
     
    return (ret == 1) ? count : ret;           // 如果发送成功就返回字节数 

EXPORT_SYMBOL(i2c_master_send); 
i2c_master_send 接口的三个参数:client 为此次与主机通信的从机,buf 为发送的数据指针,count 为发送数据的字节数。
[cpp] 
int i2c_master_recv(struct i2c_client *client, char *buf ,int count) 

    struct i2c_adapter *adap=client->adapter;  // 获取adapter信息 
    struct i2c_msg msg;                        // 定义一个临时的数据包 
    int ret; 
 
    msg.addr = client->addr;                   // 将从机地址写入数据包 
    msg.flags = client->flags & I2C_M_TEN;     // 将从机标志并入数据包 
    msg.flags |= I2C_M_RD;                     // 将此次通信的标志并入数据包 
    msg.len = count;                           // 将此次接收的数据字节数写入数据包 
    msg.buf = buf; 
 
    ret = i2c_transfer(adap, &msg, 1);         // 调用平台接口接收数据 
 
     
    return (ret == 1) ? count : ret;           // 如果接收成功就返回字节数 

EXPORT_SYMBOL(i2c_master_recv); 
i2c_master_recv 接口的三个参数:client 为此次与主机通信的从机,buf 为接收的数据指针,count 为接收数据的字节数。我们看一下 i2c_transfer 接口的参数说明:
[cpp
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); 

其中 adap 为此次主机与从机通信的适配器;msgs 为通信的数据包,这里可以是单个或多个数据包;num 用于指定数据包的个数,如果大于1则表明将进行不止一次的通信。通信一次就需要寻址一次,如果需要多次通信就需要多次寻址,前面2个接口都是进行一次通 信,所以 num 为1;有的情况下我们要读一个寄存器的值,就需要先向从机发送一个寄存器地址然后再接收数据,这样如果想自己封装一个接口就需要将 num 设置为2。接口的返回值如果失败则为负数,如果成功则返回传输的数据包个数。

首先我们先来看一下写数据的时序图:进行了1次寻址操作 


结合I2C总线协议的知识,我们可以知道OZ9350的I2C写数据由一下10个步骤组成。 
  第一步,发送一个起始信号。 
  第二步,发送7bit从机地址,即OZ9350的地址。此处需要注意,发送数据时,无法发送7bit数据,此处发送了7bit地址+1bit读写选择位,即发送7bit+r/w。最低位为1表示读,为0表示写。 
  第三步,产生一个ACK应答信号,此应答信号为从机器件产生的应答。 
  第四步,发送寄存器地址,8bit数据。 
  第五步,产生一个ACK应答信号,此应答信号为从机器件产生的应答。 
  第六步,发送一个数据,8bit数据。 
  第七步,产生一个ACK应答信号,此应答信号为从机器件产生的应答信号。 
  第八步,发送一个CRC校验码,此CRC校验值为2、4、6步数据产生的校验码。 
  第九步,既可以发送一个应答信号,也可以发送一个无应答信号,均有从机器件产生。 

  第十步,发送一个停止信号。

读数据的时序图如下图所示:进行了2次寻址操作

 

通过分解后的时序图,可以看到OZ9350的读数据由以下13个步骤组成。 
  第一步,发送一个起始信号。 
  第二步,发送7bit从机地址,即OZ9350的地址。此处需要注意,发送数据时,无法发送7bit数据,此处发送了7bit地址+1bit读写选择位,即发送7bit+r/w。最低位为1表示读,为0表示写。 
  第三步,产生一个ACK应答信号,此应答信号为从机器件产生的应答。 
  第四步,发送寄存器地址。 
  第五步,产生一个ACK应答信号,此应答信号为从机器件产生的应答。 
  第六步,再次发送一个骑士信号。 
  第七步,发送7bit从机地址,即OZ9350的地址。此处需要注意,发送数据时,无法发送7bit数据,此处发送了7bit地址+1bit读写选择位,即发送7bit+r/w。最低位为1表示读,为0表示写。 
  第八步,产生一个ACK应答信号,此应答信号为从机器件产生的应答。 
  第九步,读取一个字节(8bit)的数据。 
  第十步,产生一个ACK应答信号,此应答信号为CPU产生。 
  第十一步,读取一个CRC校验码。 
  第十二步,产生一个NACK信号。此无应答信号由CPU产生。 

  第十三步,产生一个停止信号。

3.I2C读写设备寄存器封装例子(短包:向8bit地址,读写8bit位的数据,读写函数原型返回值都是int):

rt5651_i2c_write(0x02, 0x08);

int cydat1[1] = {0};

cydat1[0] = rt5651_i2c_read(0xFF);

static int rt5651_i2c_write(u8 reg, u8 writedata)   //由上知识可知,要进行1次寻址操作

{
u8 databuf[2] = {0};
int ret = 0;
databuf[0] = reg;
databuf[1] = writedata;
ret = i2c_master_send(rt5651_i2c_client, databuf, 2);  //第一次寻址
printk(" codec  --- rt5651_i2c_write ret=%d, databuf[0]=%0x, databuf[1]=%0x\n", ret, databuf[0], databuf[1]);
if(ret < 0)
{
printk(" codec  --- rt5651_i2c_write send data failed !\n");
return -1;
}
return ret;
}
 
 static int rt5651_i2c_read(u8 reg)  //由上知识可知,要进行2次寻址操作
{
unsigned char val[1] = {0}; 
//其实这里就相当于u8 val[1] = {0};
int ret = 0;
val[0] = reg;
ret = i2c_master_send(rt5651_i2c_client, val, 1); //第一次寻址
if (ret < 0) 
{
printk(" codec  --- rt5651_i2c_read I2C i/o error ret = %d\n", ret);
return ret;
}
mdelay(10);
ret = i2c_master_recv(rt5651_i2c_client, val, 1); //第二次寻址
if (ret < 0) 
{
printk(" codec  --- rt5651_i2c_read I2C read error ret = %d\n", ret);
return ret;
}
return val[0];
}

4.I2C读写设备寄存器封装例子(长包:向8bit地址,读写16bit位的数据,读写函数原型返回值都是指针型*int):

//向0xff寄存器里面写0x1f2c

        u8 Wdata[3];
         Wdata[0]=0x1f;
Wdata[1]=0x2c;

ALC5623_WriteReg(0xff, Wdata);


u8 * rt5651id;
rt5651id=ALC5623_ReadReg(0xFF);  
printk("rt5651_i2c_probe rt5651id[0]=%x,rt5651id[1]=%x\n",rt5651id[0],rt5651id[1]); //这里把指针里面的值打印出来了。


下面一共给了2组例子,一个用了指针,一个用数组,很是经典:首先下面是别人的例子:

static void ALC5623_WriteReg( uint8 Reg, uint8 *data)
{
uint16 Ret=0;
uint8 *Wdata = NULL;                                //1.申请一个指针
Wdata=(uint8 *)PanicUnlessMalloc(4);     //2.给指针开辟一个4个u8的空间,其实这里相当于申请一个有4个u8数据的数组
Wdata[0]=Reg;
memcpy(&Wdata[1], data, 2);                  //这里非常巧妙,很有意思,指针这么好用,跟数据一样;
Ret = I2cTransfer( ALC5623_addr |Codec_Write, Wdata,3, NULL,0);
free(Wdata);                                            //3.之前有申请了空间,所以这里要释放
}
static uint8* ALC5623_ReadReg( uint8 Reg)
{
uint8 * ReadREG= malloc(16);
uint8 REGADDR[1];
memset(ReadREG,0,16);
REGADDR[0]=Reg;
I2cTransfer( ALC5623_addr |Codec_Write, REGADDR,1, NULL,0);
I2cTransfer( ALC5623_addr |Codec_Read, NULL,0, ReadREG,2);
return ReadREG;
}

它山之石可以攻玉:本人参照上面的例子写的,已在mt6737 andorid_m0上验证通过:

static int ALC5623_WriteReg( u8 Reg, u8 *data)

{
int ret = 0;
u8 databuf[4] = {0};

databuf[0] = Reg;

memcpy(&databuf[1], data, 2);

ret = i2c_master_send(rt5651_i2c_client, databuf, 3);
printk(" codec  --- rt5651_i2c_write ret=%d, databuf[0]=0x%x, databuf[1]=0x%x,databuf[1]=0x%x\n", ret, databuf[0], databuf[1],databuf[2]);
if(ret < 0)
{
printk(" codec  --- rt5651_i2c_write send data failed !\n");
return -1;
}
return ret;
}

static u8* ALC5623_ReadReg( u8 Reg)
{       u8 ret;
u8 ReadREG[2]= {0};
        u8 REGADDR[1]= {0};
REGADDR[0]=Reg;
ret = i2c_master_send(rt5651_i2c_client, REGADDR, 1);
if (ret < 0) 
{
printk(" codec  --- rt5651_i2c_read I2C i/o error ret = %d\n", ret);
return ret;
}
mdelay(10);
ret = i2c_master_recv(rt5651_i2c_client, ReadREG, 2);
if (ret < 0) 
{printk(" codec  --- rt5651_i2c_read I2C read error ret = %d\n", ret);
return ret;
}
printk(" codec  --- rt5651_i2c_read I2C read ReadREG[0]= %x ,ReadREG[1]= %x\n", ReadREG[0],ReadREG[1]);
return ReadREG;
}



<think>我们正在讨论Linux内核中的I2C驱动函数。用户特别询问了`i2c_master_send``i2c_master_recv`的使用方法及示例。根据引用[1][2],我们有这两个函数的原型。同时,引用[3]提供了一个使用类似函数(RT-Thread系统)进行拼接发送的示例,这可以给我们一些思路。 首先,我们回顾一下这两个函数: 1. `i2c_master_send`:用于I2C主设备向从设备发送数据。 ```c int i2c_master_send(const struct i2c_client *client, const char *buf, int count); ``` - `client`:指向I2C设备的`i2c_client`结构,其中包含了设备地址等信息。 - `buf`:要发送的数据缓冲区。 - `count`:要发送的字节数。 - 返回值:成功时返回发送的字节数,失败时返回负的错误码。 2. `i2c_master_recv`:用于I2C主设备从从设备接收数据。 ```c int i2c_master_recv(const struct i2c_client *client, char *buf, int count); ``` - `client`:同上。 - `buf`:用于存放接收数据的缓冲区。 - `count`:期望接收的字节数。 - 返回值:成功时返回接收的字节数,失败时返回负的错误码。 注意:这两个函数都是同步的,会阻塞直到传输完成或出错。 使用场景: - `i2c_master_send`:通常用于向设备写入命令或数据。注意,对于需要先写寄存器地址再读数据的操作,这两个函数不能直接完成,因为每次调用都会产生起始停止条件。如果需要复合操作(先写后读),应使用`i2c_transfer`。 - `i2c_master_recv`:用于从设备读取数据,但前提是设备已经准备好发送数据(即不需要先写入寄存器地址)。如果需要先写入寄存器地址再读取数据,则不能单独使用`i2c_master_recv`,因为这样会在读取之前产生一个停止条件,从而中断了传输。 引用[3]中提到了一个技巧:使用`RT_I2C_NO_STOP``RT_I2C_NO_START`标志来实现多次调用组合成一个完整的传输(先发送寄存器地址,然后读取数据)。但在Linux内核中,`i2c_master_send``i2c_master_recv`函数并不支持这样的标志。因此,在Linux中,我们通常使用`i2c_transfer`来实现复合操作。 不过,如果设备支持,我们可以用以下方式: 1. 对于只写操作,直接使用`i2c_master_send`。 2. 对于只读操作(设备上电后就会自动发送数据,或者已经设置好要读取的寄存器地址),可以直接使用`i2c_master_recv`。 但是,大多数I2C设备都需要先写入一个寄存器地址,然后再读取数据。这时,我们需要两个`i2c_msg`组合在一起,并使用`i2c_transfer`。然而,用户明确要求使用`i2c_master_send``i2c_master_recv`,所以我们需要说明它们的限制并给出可行的示例。 示例1:单独使用`i2c_master_send`发送命令 假设我们有一个设备,我们只需要向它发送一个命令(比如重置命令),不需要发送数据,那么我们可以这样: ```c char buf[1] = {0x01}; // 假设0x01是重置命令 int ret = i2c_master_send(client, buf, 1); if (ret < 0) { // 处理错误 } ``` 示例2:单独使用`i2c_master_recv`读取数据 假设设备在接收到读取请求时,会直接返回数据(比如设备只有一个数据寄存器,且地址在之前已经设置好),那么我们可以这样: ```c char buf[4]; // 假设我们要读取4个字节 int ret = i2c_master_recv(client, buf, 4); if (ret < 0) { // 处理错误 } ``` 但是,注意:上述示例2的情况很少见,因为通常我们需要先写入寄存器地址。所以,如果我们想要读取特定寄存器的值,我们需要先写入寄存器地址,然后读取。这需要两个独立的传输(中间有停止条件),但有些设备可能不允许这样,因为停止条件会中断设备的操作。因此,正确的做法是使用复合传输(无停止条件的重复开始)。 然而,如果我们必须使用这两个函数,并且设备允许在写读之间有一个停止条件,那么我们可以: ```c // 先发送要读取的寄存器地址 char reg_addr[1] = {0x00}; // 假设要读取0x00寄存器的值 int ret = i2c_master_send(client, reg_addr, 1); if (ret < 0) { // 处理错误 } // 然后读取数据 char data[2]; ret = i2c_master_recv(client, data, 2); if (ret < 0) { // 处理错误 } ``` 注意:这里两次调用分别产生了起始停止条件。在第一次发送后,有一个停止条件;然后第二次读取前,又有一个起始条件。这符合I2C协议,但并不是所有设备都支持。有些设备要求复合传输(即重复起始条件,没有中间的停止条件)。在这种情况下,我们必须使用`i2c_transfer`。 因此,在大多数情况下,我们不推荐使用`i2c_master_send``i2c_master_recv`来实现先写后读的操作,而应该使用`i2c_transfer`或者SMBus函数(如`i2c_smbus_read_byte_data`等)。 总结: - 对于简单的写操作,使用`i2c_master_send`。 - 对于简单的读操作(设备已经处于待读取状态),使用`i2c_master_recv`。 - 对于需要先写后读的操作,如果设备允许中间有停止条件,可以分两次调用;否则,使用`i2c_transfer`。 最后,我们给出一个使用`i2c_master_send``i2c_master_recv`的完整驱动函数示例(假设设备允许中间停止条件): ```c // 读取设备寄存器的值,假设设备允许先发送寄存器地址(写操作),然后停止,再启动读取 static int read_reg(struct i2c_client *client, u8 reg, u8 *val, int len) { int ret; // 先发送要读取的寄存器地址 ret = i2c_master_send(client, &reg, 1); if (ret < 0) { dev_err(&client->dev, "发送寄存器地址失败: %d\n", ret); return ret; } // 然后读取数据 ret = i2c_master_recv(client, val, len); if (ret < 0) { dev_err(&client->dev, "读取数据失败: %d\n", ret); return ret; } return 0; } ``` 注意:这个函数假设设备在收到寄存器地址后,会在下一次读取操作中返回该寄存器的值。但是,由于两次传输之间有停止条件,所以设备必须能够在这种操作下正常工作。 如果设备要求复合传输(无中间停止条件),则应该使用`i2c_transfer`: ```c static int read_reg(struct i2c_client *client, u8 reg, u8 *val, int len) { struct i2c_msg msgs[2] = { { .addr = client->addr, .flags = 0, // 写 .len = 1, .buf = &reg, }, { .addr = client->addr, .flags = I2C_M_RD, // 读 .len = len, .buf = val, } }; int ret = i2c_transfer(client->adapter, msgs, 2); if (ret < 0) { dev_err(&client->dev, "传输失败: %d\n", ret); return ret; } else if (ret != 2) { dev_err(&client->dev, "传输消息数量不足: %d\n", ret); return -EIO; } return 0; } ``` 因此,用户需要根据设备的数据手册决定使用哪种方式。 最后,我们生成相关问题:</think>### Linux I2C驱动:`i2c_master_send`与`i2c_master_recv`使用详解 #### 1. 函数原型与参数说明 ```c // 发送数据函数 int i2c_master_send(const struct i2c_client *client, const char *buf, int count); ``` - `client`:I2C设备客户端结构体指针 - `buf`:待发送数据缓冲区 - `count`:发送字节数 - **返回值**:成功返回发送字节数,失败返回负错误码[^1] ```c // 接收数据函数 int i2c_master_recv(const struct i2c_client *client, char *buf, int count); ``` - `client`:I2C设备客户端结构体指针 - `buf`:接收数据缓冲区 - `count`:期望接收字节数 - **返回值**:成功返回接收字节数,失败返回负错误码[^2] #### 2. 关键特性 1. **单次传输**:每次调用产生完整的I2C传输序列(起始位 + 数据 + 停止位) 2. **同步阻塞**:函数执行期间会阻塞当前线程 3. **寄存器访问限制**:需预先设置目标寄存器地址(需额外操作) 4. **不适用场景**:复合操作(如先写地址后读数据)需使用`i2c_transfer()` #### 3. 使用示例 ##### 示例1:基础数据发送 ```c // 发送温度设置命令(2字节) u8 config_data[2] = {0x01, 0x25}; // 寄存器地址 + 值 int ret = i2c_master_send(client, config_data, sizeof(config_data)); if (ret < 0) { dev_err(&client->dev, "发送失败: %d\n", ret); } else { dev_info(&client->dev, "发送%d字节成功\n", ret); } ``` ##### 示例2:基础数据接收 ```c // 读取传感器状态(3字节) u8 status_buf[3]; int ret = i2c_master_recv(client, status_buf, sizeof(status_buf)); if (ret < 0) { dev_err(&client->dev, "读取失败: %d\n", ret); } else { dev_info(&client->dev, "状态: 0x%02X 0x%02X 0x%02X\n", status_buf[0], status_buf[1], status_buf[2]); } ``` ##### 示例3:复合操作实现(先写后读) ```c // 步骤1:发送寄存器地址(无停止位) u8 reg_addr = 0x05; int ret = i2c_master_send(client, &reg_addr, 1); if (ret != 1) { dev_err(&client->dev, "地址发送失败"); return ret; } // 步骤2:读取寄存器数据 u8 reg_value; ret = i2c_master_recv(client, &reg_value, 1); if (ret != 1) { dev_err(&client->dev, "数据读取失败"); } ``` #### 4. 使用注意项 1. **错误处理**:必须检查返回值,常见错误码: - `-EIO`:总线通信错误 - `-ENXIO`:地址无响应 - `-ETIMEDOUT`:操作超时 2. **缓冲区管理**: ```c char *buf = kmalloc(BUF_SIZE, GFP_KERNEL); // 分配缓冲区 // ... 使用缓冲区 ... kfree(buf); // 释放内存 ``` 3. **时钟频率设置**: ```c // 在probe函数中设置总线频率 client->adapter->timeout = HZ; // 1秒超时 client->adapter->retries = 3; // 重试次数 ``` 4. **替代方案建议**: - 优先使用SMBus函数(如`i2c_smbus_read_byte_data()`) - 复杂操作使用`i2c_transfer()`+`i2c_msg` #### 5. 典型工作流程 ```mermaid graph TD A[probe函数] --> B[初始化I2C客户端] B --> C{操作类型} C -->|写操作| D[i2c_master_send] C -->|读操作| E[i2c_master_recv] D --> F[检查返回值] E --> F F -->|成功| G[处理数据] F -->|失败| H[错误处理] ``` > **最佳实践**:在`probe()`函数中通过`i2c_master_send()`发送设备ID读取命令,验证设备是否存在[^3]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值