知识点
- 掌握I2C总线
- 如何看时序图
- 如何使用I2C接口的器件,例如AT24C02
原理
I2C/IIC(集成电路总线)是philips推出的一种串行总线。
主要特性
- 只有两根线,串行数据线SDA,串行时钟线SCL
- 总线上的所有器件必须都有唯一的地址
- 多主机总线,可同时支持多个slave和多个master,即支持冲突检测和仲裁
- 8位双向数据传输,速率标准模式下最高100kbit/s,快速模式下最高400kbit/s,高速模式下最高3.4Mbit/s
硬件电路要求
如图,
- 由于设备之间是线与到一起的,所以设备的GPIO必须是开漏输出,不能是推挽输出
- 由于GPIO是开漏输出,所以必须接上拉电阻,因此SDA和SCL默认是高电平
协议要求
可以这么理解,
- I2C上的设备要进行通信,必然需要收发两方,即发送器和接收器,
- 总线需要初始化,需要有设备产生时钟信号,总线上的设备总要知道是谁正在访问谁,即主机和从机,主机完成总线初始化,产生时钟信号,并向从机发起寻址访问
- 既然各方角色已经明确了,那么
- 当主机要访问从机时,总要告诉从机要开始了,即起始调节
- 当从机收到后,
- 可以告诉主机自己收到了,让主机继续发生后面的数据,即应答信号
- 也可以告诉主机自己不想搭理主机了,让主机停止发生,即非应答信号
- 当主机判断需要停止该次传输时,应该告诉从机通信结束,即停止调节
具体解释如下:
1. 数据格式
如图,
- SDA上的数据每个字节必须是8位,且是按bit传输的,高位先传,低位后传
- 前8个bit是数据bit位,最后一个bit是应答或非应答信号
2. 有效数据位识别
如图,
1. 当SCL为低电平时,SDA的状态是允许切换的,即发送的数据要在时钟是低电平时输出
2. 当SCL是高电平时,SDA的状态是不能变化的,即需要读取的数据要在时钟是高电平时读走
3. 起始条件
如图,当SCL是高电平的时候,SDA由高电平变化到低电平
4. 应答信号
如图,
5. 非应答信号
如图,
6. 停止条件
如图,当SCL是高电平时,SDA由低电平变化到高电平
通过I2C总线读写EEPROM
硬件设计
如图,我们通过I2C总线完成对EEPROM AT24C02的读写操作,A0,A1,A2都接地,即该芯片物理地址是0
AT24C02C手册导读
要实现对AT24C02C读写操作,我们需要了解AT24C02C的基本功能和要求,这些可以从Datasheet中找到,例如
1. 写保护和地址要求
2. 上电复位要求
如下图,芯片复位时长在130到270ms之间,稳妥起见,上电后至少应该在270ms后在对该芯片进行操作
3. I2C总线时序要求
如下图,SCL和SDA的每个电平的时间都做了详细的说明,编码时需要严格按照该时间要求编码。
4. 读操作的基本时序
- 下面的图中,都只画了SDA的变化,没画SCL,原因是从I2C协议规定了SDA和SCL的关系,因此只要知道SDA怎么变化的自然就知道SCL应该如何变化,所以只画SDA足够了
- 度操作支持下面三种,在手册里都有详细的介绍
5. 写操作的基本时序
写操作支持两种,手册里也有详细介绍
软件设计
-
为了更深入的理解I2C总线协议,此例中没用硬件I2C控制器,而是用GPIO口模拟的,原理明白了,硬件方式就更加简单了
-
从前面的介绍可以看出,I2C总线是通用的,不仅AT24C02C可以使用,其它的芯片也可以使用,且协议规范是一样的,因此分成三部分,这样三部分的代码在后续的编码中可以做到通用
-
下面三部分的编码与上面原理和手册中的时序是完全匹配的,可以对照阅读
1. I2C总线驱动
- 我把电平切换和延时封装成一个宏,这样函数中只要把精力都投入到电平变化就可以了
- 延时时间固定5us,这样代码会简单很多,原因如“I2C总线时序要求”一节中,时序基本都要求了最小时间间隔5us足以,有两项有最大时间要求,却不影响我们编码。
#define I2C_Set1(i2c) GPIO_SetBits(i2c);I2C_Delay(5);
#define I2C_Set0(i2c) GPIO_ResetBits(i2c);I2C_Delay(5);
#define I2C_Get(i2c) GPIO_ReadInputBit(i2c);
VOID DRV_I2C_Start(VOID)
{
I2C_SetOutput(I2C_SDA);
I2C_Set1(I2C_SDA);
I2C_Set1(I2C_SCL);
I2C_Set0(I2C_SDA);
I2C_Set0(I2C_SCL);
}
VOID DRV_I2C_Stop(VOID)
{
I2C_SetOutput(I2C_SDA);
I2C_Set0(I2C_SDA);
I2C_Set1(I2C_SCL);
I2C_Set1(I2C_SDA);
}
U32 DRV_I2C_WriteByte(IN U8 data)
{
U8 i = 0;
U8 byte = data;
U8 sda = 0;
I2C_SetOutput(I2C_SDA);
for (i = 0; i < 8; i++)
{
I2C_Set0(I2C_SCL);
if (byte & 0x80)
{
I2C_Set1(I2C_SDA);
}
else
{
I2C_Set0(I2C_SDA);
}
I2C_Set1(I2C_SCL);
byte <<= 1;
}
I2C_Set0(I2C_SCL);
I2C_SetInput(I2C_SDA);
I2C_Set1(I2C_SCL);
sda = I2C_Get(I2C_SDA);
if (sda)
{
I2C_Set0(I2C_SCL);
I2C_SetOutput(I2C_SDA);
return OS_ERROR;
}
I2C_Set0(I2C_SCL);
I2C_SetOutput(I2C_SDA);
I2C_Set1(I2C_SDA);
return OS_OK;
}
U32 DRV_I2C_ReadByte(OUT U8 *byte)
{
U8 i = 0;
U8 bit = 0;
U8 sda = 0;
I2C_SetInput(I2C_SDA);
for (i = 0; i < 8; i++)
{
I2C_Set1(I2C_SCL);
sda = I2C_Get(I2C_SDA);
if (sda)
{
bit |= 0x1;
}
I2C_Set0(I2C_SCL);
if (i != 7)
{
bit <<= 1;
}
}
*byte = bit;
return OS_OK;
}
VOID DRV_I2C_NoAck(VOID)
{
I2C_Set0(I2C_SCL);
I2C_SetOutput(I2C_SDA);
I2C_Set1(I2C_SDA);
I2C_Set1(I2C_SCL);
I2C_Set0(I2C_SCL);
}
VOID DRV_I2C_Ack(VOID)
{
I2C_Set0(I2C_SCL);
I2C_SetOutput(I2C_SDA);
I2C_Set0(I2C_SDA);
I2C_Set1(I2C_SCL);
I2C_Set0(I2C_SCL);
}
VOID DRV_I2C_Init(VOID)
{
GPIO_InitPara GPIO_InitStructure;
RCC_APB2PeriphClock_Enable(RCC_APB2PERIPH_GPIOB,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStructure.GPIO_Mode = GPIO_MODE_OUT_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_SPEED_50MHZ;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_PIN_6);
GPIO_SetBits(GPIOB, GPIO_PIN_7);
}
2. AT24C02C驱动
下面的函数接口与手册中的读写接口一一对应,其中立即地址读没有做,因为不常用。
/* 字节写 */
VOID DRV_AT24C02C_WriteByte(IN U8 slaveAddr, IN U8 byteAddr, IN U8 data)
{
DRV_I2C_Start();
DRV_I2C_WriteByte(slaveAddr);
DRV_I2C_WriteByte(byteAddr);
DRV_I2C_WriteByte(data);
DRV_I2C_Stop();
}
/* 页写 */
VOID DRV_AT24C02C_WritePage(IN U8 slaveAddr, IN U8 byteAddr, IN U8 data[], IN U8 len)
{
U8 i = 0;
DRV_I2C_Start();
DRV_I2C_WriteByte(slaveAddr);
DRV_I2C_WriteByte(byteAddr);
for (i = 0; i < len; i++)
{
DRV_I2C_WriteByte(data[i]);
}
DRV_I2C_Stop();
}
/* 选择地址读 */
VOID DRV_AT24C02C_ReadByte(IN U8 slaveAddr, IN U8 byteAddr, OUT U8 *data)
{
U8 tmp = 0;
DRV_I2C_Start();
DRV_I2C_WriteByte(slaveAddr);
DRV_I2C_WriteByte(byteAddr);
DRV_I2C_Start();
DRV_I2C_WriteByte(slaveAddr+1);
DRV_I2C_ReadByte(&tmp);
DRV_I2C_NoAck();
DRV_I2C_Stop();
*data = tmp;
}
/* 连续读 */
VOID DRV_AT24C02C_ReadPage(IN U8 slaveAddr, IN U8 byteAddr, OUT U8 data[], IN U8 len)
{
U8 tmp = 0;
U8 i = 0;
DRV_I2C_Start();
DRV_I2C_WriteByte(slaveAddr);
DRV_I2C_WriteByte(byteAddr);
DRV_I2C_Start();
DRV_I2C_WriteByte(slaveAddr+1);
for (i = 0; i < len-1; i++)
{
DRV_I2C_ReadByte(&tmp);
DRV_I2C_Ack();
data[i] = tmp;
}
DRV_I2C_ReadByte(&tmp);
DRV_I2C_NoAck();
data[i] = tmp;
DRV_I2C_Stop();
}
VOID DRV_AT24C02C_Init(VOID)
{
DRV_I2C_Init();
}
3. 功能测试举例
下面的代码,只是为了举例说明如何使用上述接口而已,其中0xA0的含义如下图,其中R/W位接口内部有处理,此处统一填了0。
#define I2C_AT24C02C_ADDR 0xA0
VOID APP_I2C_Test(VOID)
{
U8 len = 255;
U8 databufIn[5] = {0};
U8 databufOut[255] = {0};
U8 dataOut = 0;
U8 byteAddr = 0x00;
DRV_AT24C02C_Init();
dataOut = 0;
DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
APP_DEBUG("read=0x%x", dataOut);
DRV_AT24C02C_WriteByte(I2C_AT24C02C_ADDR, byteAddr, 0x11);
APP_Delay(10);
DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
APP_DEBUG("read=0x%x", dataOut);
DRV_AT24C02C_WriteByte(I2C_AT24C02C_ADDR, byteAddr, 0x2);
APP_Delay(10);
DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
APP_DEBUG("read=0x%x", dataOut);
DRV_AT24C02C_WriteByte(I2C_AT24C02C_ADDR, byteAddr, 0xff);
APP_Delay(10);
DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
APP_DEBUG("read=0x%x", dataOut);
DRV_AT24C02C_ReadPage(I2C_AT24C02C_ADDR, byteAddr, databufOut, len);
I2C_Dump(databufOut, len);
DRV_AT24C02C_WritePage(I2C_AT24C02C_ADDR, byteAddr, databufIn, 5);
APP_Delay(10);
DRV_AT24C02C_ReadPage(I2C_AT24C02C_ADDR, byteAddr, databufOut, len);
I2C_Dump(databufOut, len);
while(1);
}
代码路径
https://github.com/YaFood/GD32F103/tree/master/TestI2C
https://gitee.com/YaFOOD/GD32F103/tree/master/TestI2C