I2C总线简介

I2C(集成电路总线),由Philips公司(2006年迁移到NXP)在1980年代初开发的一种简单、双线双向的同步串行总线,它利用一根时钟线和一根数据线在连接总线的两个器件之间进行信息的传递,为设备之间数据交换提供了一种简单高效的方法。每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。(引用文章《一文搞懂I2C通信总线》

由于I2C总线只需要一根数据线SDA用于收发信息,一根时钟线SCL用于同步双方时钟,两根信号线(不包括电源线)即可满足多设备通讯,因此在嵌入式领域得到了广泛的应用。具体的简介可查看前文中引用的CSDN文章或在网上查询,有很多优秀的博主对I2C有着更深入的讲解,所以本文不再赘述。

I2C的地址

I2C的传输为8为一帧(一个字节),其最低为表示后续字节的传输方向,’0‘表示主机发送数据给从机,’1‘表示从机发送数据给主机

故表示I2C设备的地址为7位,所以I2C理论上最多能承载的从机设备为2^7=128个(但其实I2C会有16个保留地址,但一般不会使用到)

所以在表达I2C设备的地址会有两种方法,既根据七位地址表示和七位地址+一位读写位的八位地址表示

以MPU6050为例,在不使能INT的情况下, 其七位地址为0x68=110 1000b,但如果加上最低位的读写位,则有0xD0 = 1101 0000b表示写地址,0xD1 = 1101 0001b表示读地址,由于在linux中采用的是7位地址表示法,故本站中所有对于I2C地址的表示都默认采用7位地址表示法,既认为MPU6050的I2C地址位0x68

本文将介绍Arduino平台的I2C库的使用方法,并跟大家分享一下笔者自己实现的STM32模拟I2C的驱动库,由于个人水平有限,如有错误请联系笔者(QQ:244251560)并注明来意。

Arduino

在Arduino中有一个用于处理I2C的内置库Wire Library,使用这个库可以很轻松的使用I2C,并将Arduino配置为主机或从机。

Wire Library内置了很多十分好用的函数可以轻松配置I2C,只需要在文件中包含#include 便可使用这些函数,下面挑选其中比较常用的函数进行部分功能讲解

begin函数

函数原型及用法如下

Wire.begin();
Wire.begin(address);
函数说明 此函数初始化 Wire 库并将 I2C 总线作为控制器或外设加入。此函数通常只应调用一次。
参数1 address:7位地址(可选);如果未指定,则代表将Arduino设定为主机设备
返回值

beginTransmission函数

函数原型及用法如下

Wire.beginTransmission(address);
函数说明 启动总线,并往总线上写Address的地址,等待ACK
参数1 address:要传输的设备的 7 位地址。
返回值

endTransmission函数

函数原型及用法如下

Wire.endTransmission() ;
Wire.endTransmission(stop) ;
函数说明 结束主机发送
参数1 state:如果为true,则在传输后发送停止消息,释放I2C总线;
如果为false,则在传输后发送重启消息,总线不会被释放;
默认值为true
返回值 0:成功。
1:数据太长,无法放入传输缓冲区。
2:在发送地址时收到 NACK。
3:在传输数据时接收到 NACK。
4:其他错误。
5:超时

write函数

函数原型及用法如下

Wire.write(value);
Wire.write(string);
Wire.write(data, length);
函数说明 响应控制器设备的请求,从外围设备写入数据,或者将字节队列从控制器传输到外围设备
value 作为单个字节要发送的值
string 要作为一系列字节发送的字符串
data 要作为字节发送的数据数组
length 要传输的字节数
返回值 写入的字节数(读取此数字是可选的)

read函数

函数原型及用法如下

Wire.read()
函数说明 读取一个字节,该字节是在调用外围设备后从外围设备传输到控制器设备或从控制器设备传输到外围设备后从外围设备传输到控制器设备的
返回值 收到的下一个字节

单寄存器设备读写操作

由于这种设备内部只有一个寄存器,使用不需要提前写寄存器地址,直接写数据就可以了

写数据:

Wire.beginTransmission(Address);    /* 启动总线,并往总线上写Address的地址,等待ACK */
Wire.printf(byte);              /* 往总线上写入为byte的字节数据 */
Wire.endTransmission();         /* 结束总线传输 */

读数据:

uint8_t data;
uint8_t databuffer = Wire.requestFrom(Address, 1);  //从地址为Address的设备读取一个字节到master的数据缓冲区
Wire.readBytes(data, databuffer);//把数据从缓冲区读到变量里来

多寄存器设备读写操作

在多寄存器设备中,设备除了本身的I2C地址外,内部还有多个寄存器,这些寄存器同样通过寻址的方式进行读写操作

假设某个设备的I2C地址为Address,其内部有一个寄存器A的地址为RegA_addr

写数据

Wire.beginTransmission(Address);    /* 启动总线,并往总线上写设备的I2C地址为Address,等待ACK */
Wire.write(RegA_addr);              /* 写入寄存器地址 */
Wire.write(0x13);                   /* 向寄存器写入数据0x13 */

读数据

uint8_t data[2];
uint32_t ack;
Wire.beginTransmission(Address);    /* 启动总线,并往总线上写设备的I2C地址为Address,等待ACK */
Wire.write(RegA_addr);              /* 写入寄存器地址 */
ack = Wire.endTransmission();           /* 结束总线传输 */

Wire.requestFrom(Address, 2);       /* 从地址Address请求两个字节的数据 */
if (Wire.available()) { /* 如果有数据 */
    data[0] = Wire.read();
    data[1] = Wire.read();
}

STM32

STM32的代码为复用GPIO模拟I2C,为软件层级的I2C驱动,由于STM32HAL库的I2C有一些玄学的问题,再加上硬件I2C只能固定在几个引脚上,故跟大家分享笔者自己实现的软件模拟I2C库,在后续本站中读写I2C设备也会使用本文的代码
源文件可以点击源码下载

I2C.c

#include "I2C.h"
/***************************************************************
Copyright © LiJie Co., Ltd. 2024-2029. All rights reserved.
文件名     : I2C.h
作者      : karlren
版本      : V1.0
描述      : I2C总线源文件
其他      : 无
网站      : www.enjoylearn.top
日志      : 初版V1.0 2024/09/14 karlren创建
***************************************************************/

/* I2C总线私有数据结构体 */
struct I2C_Data {
    GPIO_TypeDef *SCL_Port;
    uint16_t SCL_Pin;
    GPIO_TypeDef *SDA_Port;
    uint16_t SDA_Pin;
};

/* I2C GPIO输出状态 */
typedef enum I2C_GPIO_State {
    LOW = 0,
    HIGH = 1
}I2C_GPIO_State;

/* 全局变量:I2C句柄数组,创建的I2C总线都会添加到此变量中,Get_I2C_Bus函数会遍历这个总线找到对应名字的I2C句柄 */
struct I2C_Bus **gp_I2C_Buses;
/* 已经注册的I2C总线数量 */
static int g_I2C_Bus_Count = 0;

/* 函数说明 : 初始化I2C的GPIO
 * 输入参数 : p_I2CBus:I2C的句柄
 * 返回参数 : 无
 * 函数备注 : 无
 */
static void I2C_GPIO_Init(struct I2C_Bus *p_I2CBus)
{
    struct I2C_Data *p_I2CData = p_I2CBus->private_data;
    /* 开启GPIOA~GPIOC时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = p_I2CData->SCL_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(p_I2CData->SCL_Port, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = p_I2CData->SDA_Pin;
    HAL_GPIO_Init(p_I2CData->SDA_Port, &GPIO_InitStruct);

}

/* 函数说明 : I2C的SCL写命令
 * 输入参数 : p_I2CBus:I2C的句柄
 * 输入参数 : BitValue:写入的电平
 * 返回参数 : 无
 * 函数备注 : 无
 */
static void I2C_W_SCL(struct I2C_Bus *p_I2CBus, I2C_GPIO_State BitValue)
{
    struct I2C_Data *p_I2CData = p_I2CBus->private_data;
    /*根据BitValue的值,将SCL置高电平或者低电平*/
    if (BitValue == LOW) {
        HAL_GPIO_WritePin(p_I2CData->SCL_Port, p_I2CData->SCL_Pin, GPIO_PIN_RESET);
    } else {
        HAL_GPIO_WritePin(p_I2CData->SCL_Port, p_I2CData->SCL_Pin, GPIO_PIN_SET);
    }
    /*如果单片机速度过快,可在此添加适量延时,以避免超出I2C通信的最大速度*/
    /* 后续再考虑I2C速度问题 */
}

/* 函数说明 : I2C的SDA写命令
 * 输入参数 : p_I2CBus:I2C的句柄
 * 输入参数 : BitValue:写入的电平
 * 返回参数 : 无
 * 函数备注 : 无
 */
static void I2C_W_SDA(struct I2C_Bus *p_I2CBus, I2C_GPIO_State BitValue)
{
    struct I2C_Data *p_I2CData = p_I2CBus->private_data;
    /*根据BitValue的值,将SDA置高电平或者低电平*/
    if (BitValue == LOW) {
        HAL_GPIO_WritePin(p_I2CData->SDA_Port, p_I2CData->SDA_Pin, GPIO_PIN_RESET);
    } else {
        HAL_GPIO_WritePin(p_I2CData->SDA_Port, p_I2CData->SDA_Pin, GPIO_PIN_SET);
    }
    /*如果单片机速度过快,可在此添加适量延时,以避免超出I2C通信的最大速度*/
    /* 后续再考虑I2C速度问题 */
}

/* 函数说明 : I2C的SDA读命令
 * 输入参数 : p_I2CBus:I2C的句柄
 * 返回参数 : 读取到的SDA电平状态
 * 函数备注 : 无
 */
static uint8_t I2C_R_SDA(struct I2C_Bus *p_I2CBus)
{   
    struct I2C_Data *p_I2CData = p_I2CBus->private_data;
    return HAL_GPIO_ReadPin(p_I2CData->SDA_Port, p_I2CData->SDA_Pin);/* 返回SDA电平 */
}

/* 函数说明 : 开启I2C
 * 输入参数 : p_I2CBus:I2C的句柄
 * 返回参数 : 无
 * 函数备注 : 无
 */
void I2C_Start(struct I2C_Bus *p_I2CBus)
{
    I2C_W_SDA(p_I2CBus, HIGH);                  /* 释放SDA,确保SDA为高电平 */
    I2C_W_SCL(p_I2CBus, HIGH);                  /* 释放SCL,确保SCL为高电平 */
    I2C_W_SDA(p_I2CBus, LOW);                   /* 在SCL高电平期间,拉低SDA,产生起始信号 */
    I2C_W_SCL(p_I2CBus, LOW);                   /* 起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接 */
}

/* 函数说明 : 停止I2C
 * 输入参数 : p_I2CBus:I2C的句柄
 * 返回参数 : 无
 * 函数备注 : 无
 */
void I2C_Stop(struct I2C_Bus *p_I2CBus)
{
    I2C_W_SDA(p_I2CBus, LOW);                   /* 拉低SDA,确保SDA为低电平 */
    I2C_W_SCL(p_I2CBus, HIGH);                  /* 释放SCL,使SCL呈现高电平 */
    I2C_W_SDA(p_I2CBus, HIGH);                  /* 在SCL高电平期间,释放SDA,产生终止信号 */
}

/* 函数说明 : 向I2C写入字节
 * 输入参数 : p_I2CBus:I2C的句柄
 * 输入参数 : Byte:要写入的字节数据
 * 返回参数 : 无
 * 函数备注 : 无
 */
void I2C_SendByte(struct I2C_Bus *p_I2CBus, uint8_t Byte)
{
    uint8_t i;

    /*循环8次,主机依次发送数据的每一位*/
    for (i = 0; i < 8; i++) {
        /* 使用掩码的方式取出Byte的指定一位数据并写入到SDA线 */
        /* 两个!的作用是,让所有非零的值变为1 */
        I2C_W_SDA(p_I2CBus, (I2C_GPIO_State)!!(Byte & (0x80 >> i)));
        I2C_W_SCL(p_I2CBus, HIGH);              /* 释放SCL,从机在SCL高电平期间读取SDA */
        I2C_W_SCL(p_I2CBus, LOW);               /* 拉低SCL,主机开始发送下一位数据 */
    }
}

/* 函数说明 : 从I2C读取字节
 * 输入参数 : p_I2CBus:I2C的句柄
 * 返回参数 : 读取到的字节数据
 * 函数备注 : 无
 */
uint8_t I2C_ReceiveByte(struct I2C_Bus *p_I2CBus)
{
    uint8_t i, Byte = 0x00;                     /* 定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到 */
    I2C_W_SDA(p_I2CBus, HIGH);                  /* 接收前,主机先确保释放SDA,避免干扰从机的数据发送 */
    for (i = 0; i < 8; i ++) {                   /* 循环8次,主机依次接收数据的每一位 */ 
        I2C_W_SCL(p_I2CBus, HIGH);              /* 释放SCL,主机机在SCL高电平期间读取SDA */
        if (I2C_R_SDA(p_I2CBus) == 1) {         /* 读取SDA数据,并存储到Byte变量 */
            Byte |= (0x80 >> i);                /* 当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0 */
        }                                   
        I2C_W_SCL(p_I2CBus, LOW);               /* 拉低SCL,从机在SCL低电平期间写入SDA */
    }
    return Byte;                                /* 返回接收到的一个字节数据 */
}

/* 函数说明 : 向I2C写入应答
 * 输入参数 : p_I2CBus:I2C的句柄
 * 输入参数 : AckBit:应答数据(0为应答,1为非应答)
 * 返回参数 : 无
 * 函数备注 : 无
 */
void I2C_SendAck(struct I2C_Bus *p_I2CBus, I2C_Ack_State AckBit)
{
    I2C_W_SDA(p_I2CBus, (I2C_GPIO_State)AckBit);/* 主机把应答位数据放到SDA线 */
    I2C_W_SCL(p_I2CBus, HIGH);                  /* 释放SCL,从机在SCL高电平期间,读取应答位 */
    I2C_W_SCL(p_I2CBus, LOW);                   /* 拉低SCL,开始下一个时序模块 */
}

/* 函数说明 : 从I2C读取应答
 * 输入参数 : p_I2CBus:I2C的句柄
 * 返回参数 : 读取到的应答数据(0为应答,1为非应答)
 * 函数备注 : 无
 */
uint8_t I2C_ReceiveAck(struct I2C_Bus *p_I2CBus)
{
    uint8_t AckBit;                             /* 定义应答位变量 */
    I2C_W_SDA(p_I2CBus, HIGH);                  /* 接收前,主机先确保释放SDA,避免干扰从机的数据发送 */
    I2C_W_SCL(p_I2CBus, HIGH);                  /* 释放SCL,主机机在SCL高电平期间读取SDA */
    AckBit = I2C_R_SDA(p_I2CBus);               /* 将应答位存储到变量里 */ 
    I2C_W_SCL(p_I2CBus, LOW);                   /* 拉低SCL,开始下一个时序模块 */ 
    return AckBit;                              /* 返回定义应答位变量 */
}

/* 函数说明 : 创建I2C总线
 * 输入参数 : name:I2C总线名称(一般为i2c1、i2c2)
 * 输入参数 : scl_port:SCL的GPIO的端口(例如GPIOA、GPIOB)
 * 输入参数 : scl_pin :SCL的引脚号
 * 输入参数 : sda_port:SDA的GPIO的端口(例如GPIOA、GPIOB)
 * 输入参数 : sda_pin :SDA的引脚号
 * 返回参数 : I2C总线的句柄
 * 函数备注 : 无
 */
struct I2C_Bus *Create_I2C_Bus(char *name, GPIO_TypeDef *scl_port, uint16_t scl_pin, GPIO_TypeDef *sda_port, uint16_t sda_pin)
{
    struct I2C_Bus *p_I2CBus = malloc(sizeof(struct I2C_Bus));
    if (p_I2CBus == NULL) {
        return NULL;
    }

    struct I2C_Data *p_I2CData = malloc(sizeof(struct I2C_Data));
    if (p_I2CData == NULL) {
        return NULL;
    }
    /* 注册的I2C总线数量+=1 */
    int i = g_I2C_Bus_Count += 1;

    if (gp_I2C_Buses == NULL) {  /* 如果是首次注册I2C总线 */
        gp_I2C_Buses = (struct I2C_Bus **)malloc(sizeof(struct I2C_Bus));
    } else {                    /* 如果已经注册过I2C总线 */
        gp_I2C_Buses = realloc(gp_I2C_Buses, sizeof(struct I2C_Bus *) * g_I2C_Bus_Count);
    }
    /* 将新注册的I2C总线句柄传入全局变量 */
    gp_I2C_Buses[i - 1] = p_I2CBus;

    /* I2C总线GPIO初始化 */
    p_I2CData->SCL_Port = scl_port;
    p_I2CData->SCL_Pin = scl_pin;
    p_I2CData->SDA_Port = sda_port;
    p_I2CData->SDA_Pin = sda_pin;
    p_I2CBus->private_data = p_I2CData;
    I2C_GPIO_Init(p_I2CBus);

    /* 初始化I2C句柄的内部成员 */
    p_I2CBus->Start = I2C_Start;
    p_I2CBus->Stop = I2C_Stop;
    p_I2CBus->SendByte = I2C_SendByte;
    p_I2CBus->ReceiveByte = I2C_ReceiveByte;
    p_I2CBus->SendAck = I2C_SendAck;
    p_I2CBus->ReceiveAck = I2C_ReceiveAck;

    /* 将SCL和SDA拉高,设置I2C初始状态 */
    I2C_W_SCL(p_I2CBus, HIGH);
    I2C_W_SDA(p_I2CBus, HIGH);

    return p_I2CBus;
}

/* 函数说明 : 根据总线名获取I2C总线的句柄
 * 输入参数 : name:总线名
 * 返回参数 : I2C总线的句柄
 * 函数备注 : 无
 */
struct I2C_Bus *Get_I2C_Bus(char *name)
{
    int i = 0;
    for (i = 0; i < g_I2C_Bus_Count; i++) {
        if (strcmp(name, gp_I2C_Buses[i]->name) == 0) {
            return gp_I2C_Buses[i];
        }
    }
    return NULL;
}

I2C.h

#ifndef __I2C_H__
#define __I2C_H__
/***************************************************************
Copyright © LiJie Co., Ltd. 2024-2029. All rights reserved.
文件名     : I2C.h
作者      : karlren
版本      : V1.0
描述      : I2C总线头文件
其他      : 无
网站      : www.enjoylearn.top
日志      : 初版V1.0 2024/09/14 karlren创建
***************************************************************/

#include "main.h"
#include <string.h>
#include <stdlib.h>

#ifndef uint8_t
#define uint8_t unsigned char
#endif

#ifndef uint16_t
#define uint16_t unsigned short int
#endif

/* SendAck应答位,0表示应答,1表示非应答 */
typedef enum I2C_Ack_State {
    Ack = 0,
    NAck = 1
}I2C_Ack_State;
/* 总线驱动结构体 */
struct I2C_Bus {
    char *name; /* 名字:一般为i2c1、i2c2等 */
    void (*Start)(struct I2C_Bus *p_I2CBus);
    void (*Stop)(struct I2C_Bus *p_I2CBus);
    void (*SendByte)(struct I2C_Bus *p_I2CBus, uint8_t Byte);
    uint8_t (*ReceiveByte)(struct I2C_Bus *p_I2CBus);
    void (*SendAck)(struct I2C_Bus *p_I2CBus, I2C_Ack_State AckBit);
    uint8_t (*ReceiveAck)(struct I2C_Bus *p_I2CBus); 
    void *private_data;     /* 私有数据 */
};
/* 创建总线,返回总线驱动结构体指针 */
struct I2C_Bus *Create_I2C_Bus(char *name, GPIO_TypeDef *scl_port, uint16_t scl_pin, GPIO_TypeDef *sda_port, uint16_t sda_pin);
/* 根据名字返回总线驱动结构体指针 */
struct I2C_Bus *Get_I2C_Bus(char *name);

#endif // !__I2C_H__

By karlren

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注