现在,已经有越来越多的外围器件与微控制器的接口形式是二线制的I2C 串行总线,学
会其基本操作已经成为广大电子电气工程师和爱好者的迫切要求。鉴于I2C 总线协议原文内
容十分详尽,对于总线协议的初学者可能不易把握,本文特意非常简要地介绍了I2C 总线的
概念、优点和基础协议,可以帮助您尽快掌握具体器件的用法,也为进一步深入学习掌握
I2C 总线打好基础。如果您希望了解I2C 总线更深层次的问题,请参考:
NXP 半导体(原Philips 半导体)中文网站:http://scn.nxp.com/index.html”
广州周立功单片机发展有限公司网站:http://www.zlgmcu.com
何立民教授编著的《I2C 总线应用系统设计》
1. 什么是I2C 总线
NXP 半导体(原Philips 半导体)于20 多年前发明了一种简单的双向二线制串行通信总
线,这个总线被称为Inter-IC 或者I2C 总线。目前I2C 总线已经成为业界嵌入式应用的标准
解决方案,被广泛地应用在各式各样基于微控器的专业、消费与电信产品中,作为控制、诊
断与电源管理总线。多个符合I2C 总线标准的器件都可以通过同一条I2C 总线进行通信,而
不需要额外的地址译码器。由于I2C 是一种两线式串行总线,因此简单的操作特性成为它快
速崛起成为业界标准的关键因素。
2. I2C 总线的众多优秀特点
总线仅由2 根信号线组成 由此带来的好处有:节省芯片I/O、节省PCB 面积、节
省线材成本,等等。
总线协议简单 I2C 总线的协议原文有好几十页,如果直接让初学者来看确实头大,
但是并不意为着I2C 总线协议本身就复杂。本文撰写的目的就是服务于广大初学者,
仅数页的正式内容,图文并茂,容易入门。相信读者认真看过之后,就能基本上掌
握I2C 总线的要领,为进一步操控具体的器件打下良好的基础。
协议容易实现 得益于简单的协议规范,在芯片内部,以硬件的方法实现I2C 部件
的逻辑是很容易的。对应用工程师来讲,即使MCU 内部没有硬件的I2C 总线接口,
也能够方便地利用开漏的I/O(如果没有,可用准双向I/O 代替)来模拟实现。
支持的器件多 NXP 半导体最早提出I2C 总线协议,目前包括半导体巨头德州仪器
(TI)、美国国家半导体(National Semi)、意法半导体(ST)、美信半导体(Maxim-IC)
等都有大量器件带有I2C 总线接口,这为应用工程师设计产品时选择合适的I2C 器
件提供了广阔的空间。
总线上可同时挂接多个器件 同一条I2C 总线上可以挂接很多个器件,一般可达数
十个以上,甚至更多。器件之间是靠不同的编址来区分的,而不需要附加的I/O 线
或地址译码部件。
总线可裁减性好 在原有总线连接的基础上可以随时新增或者删除器件。用软件可
以很容易实现I2C 总线的自检功能,能够及时发现总线上的变动。
总线电气兼容性好 I2C 总线规定器件之间以开漏I/O 互联,这样,只要选取适当
的上拉电阻就能轻易实现3V/5V 逻辑电平的兼容,而不需要额外的转换。
支持多种通信方式 一主多从是最常见的通信方式。此外还支持双主机通信、多主
机通信以及广播模式等等。
通信速率高 I2C 总线标准传输速率为100kbps(每秒100k 位)。在快速模式下为
400kbps。按照后来修订的版本,位速率可高达3.4Mbps。
兼顾低速通信 I2C 总线的通信速率也可以低至几kbps 以下,用以支持低速器件(比
如软件模拟的实现)或者用来延长通信距离。
有一定的通信距离 一般情况下,I2C 总线通信距离有几米到十几米。通过降低传
输速率等办法,通信距离可延长到数十米乃至数百米以上。
3. I2C 总线的信号线
I2C 总线只需要由两根信号线组成,一根是串行数据线SDA,另一根是串行时钟线SCL。
在系统中,I2C 总线的典型接法如图 1 所示,注意连接时需要共地。
GS1NJ]%7D_5.jpg)
一般具有I2C 总线的器件其SDA 和SCL 管脚都是漏极开路(或集电极开路)输出结构。
因此实际使用时,SDA 和SCL 信号线都必须要加上拉电阻Rp(Pull-Up Resistor)。上拉电
阻一般取值3~10KΩ。开漏结构的好处是:
当总线空闲时,这两条信号线都保持高电平,不会消耗电流。
电气兼容性好。上拉电阻接5V 电源就能与5V 逻辑器件接口,上拉电阻接3V 电源
又能与3V 逻辑器件接口。
因为是开漏结构,所以不同器件的SDA 与SDA 之间、SCL 与SCL 之间可以直接
相连,不需要额外的转换电路。
4. I2C 总线的基本概念
发送器(Transmitter):发送数据到总线的器件
接收器(Receiver):从总线接收数据的器件
主机(Master):初始化发送、产生时钟信号和终止发送的器件
从机(Slave):被主机寻址的器件
I2C 总线是双向传输的总线,因此主机和从机都可能成为发送器和接收器。如果主机向
从机发送数据,则主机是发送器,而从机是接收器;如果主机从从机读取数据,则主机是接
收器,而从机是发送器。不论主机是发送器还是接收器,时钟信号SCL 都要由主机来产生。
5. I2C 总线数据传送速率
I2C 总线的通信速率受主机控制,能快能慢。但是最高速率是有限制的,I2C 总线上数
据的传输速率在标准模式(Standard-mode)下为100kbps(每秒100k 位),在快速模式下为
400kbps。按照后来修订的版本,位速率最高可达3.4Mbps。
6. I2C 总线上数据的有效性(Data validity)
95J6Y%6[2S8@21E2OY%I75.jpg)
数据线SDA 的电平状态必须在时钟线SCL 处于高电平期间保持稳定不变。SDA 的电
平状态只有在SCL 处于低电平期间才允许改变。但是在I2C 总线的起始和结束时例外。
注:某些其它的串行总线协议,如SPI,可能规定数据在时钟信号的边沿(上升沿或下降沿)有效,
而I2C 总线则是电平有效。
7. 起始条件和停止条件(START and STOP conditions)
起始条件 当SCL 处于高电平期间时,SDA 从高电平向低电平跳变时产生起始条件。
总线在起始条件产生后便处于忙的状态。起始条件常常简记为S。
停止条件 当SCL 处于高电平期间时,SDA 从低电平向高电平跳变时产生停止条件。
总线在停止条件产生后处于空闲状态。停止条件简记为P。
84M_)%600_X%60]BJI~L8.jpg)
8. 从机地址(Slave Address)
I2C 总线不需要额外的地址译码器和片选信号。多个具有I2C 总线接口的器件都可以连
接到同一条I2C 总线上,它们之间通过器件地址来区分。主机是主控器件,它不需要器件地
址,其它器件都属于从机,要有器件地址。必须保证同一条I2C 总线上所有从机的地址都是
唯一确定的,不能有重复,否则I2C 总线将不能正常工作。一般从机地址由7 位地址位和一
位读写标志R/W 组成,7 位地址占据高7 位,读写位在最后。读写位是0,表示主机将要向
从机写入数据;读写位是1,则表示主机将要从从机读取数据。
9. 数据是按字节传输的
I2C 总线总是以字节(Byte)为单位收发数据。每次传输的字节数量没有严格限制。首
先传输的是数据的最高位(MSB,第7 位),最后传输的是最低位(LSB,第0 位)。另外,
每个字节之后还要跟一个响应位,称为应答。
10. 应答(Acknowledge)
在I2C 总线传输数据过程中,每传输一个字节,都要跟一个应答状态位。接收器接收数
据的情况可以通过应答位来告知发送器。应答位的时钟脉冲仍由主机产生,而应答位的数据
状态则遵循“谁接收谁产生”的原则,即总是由接收器产生应答位。主机向从机发送数据时,
应答位由从机产生;主机从从机接收数据时,应答位由主机产生。I2C 总线标准规定:应答
位为0 表示接收器应答(ACK),常常简记为A;为1 则表示非应答(NACK),常常简记为
A。发送器发送完LSB 之后,应当释放SDA 线(拉高SDA,输出晶体管截止),以等待接
收器产生应答位。
如果接收器在接收完最后一个字节的数据,或者不能再接收更多的数据时,应当产生非
应答来通知发送器。发送器如果发现接收器产生了非应答状态,则应当终止发送。
11. 基本的数据传输格式


在图 4 和图 5 中,各种符号的意义为:
S:起始位(START)
SA:从机地址(Slave Address),7 位从机地址
W:写标志位(Write),1 位写标志
R:读标志位(Read),1 位读标志
A:应答位(Acknowledge),1 位应答
A:非应答位(Not Acknowledge),1 位非应答
D:数据(Data),每个数据都必须是8 位
P:停止位(STOP)
阴影:主机产生的信号
无阴影:从机产生的信号
应当注意的是,与图 5 中的情况不同的是,在图 4 中,主机向从机发送最后一个字节
的数据时,从机可能应答也可能非应答,但不管怎样主机都可以产生停止条件。如果主机在
向从机发送数据(甚至包括从机地址在内)时检测到从机非应答,则应当及时停止传输。
12. 传输一个字节数据的时序图
为了更清楚地了解I2C 总线的基本数据传输过程,下面画出了只传输1 个字节的时序图,
这是最基本的传输方式。在图 6 和图 7 中,SDA 信号线被画成了两条,一个是主机产生的,
另一个是从机产生的。实际上主机和从机的SDA 信号线总是连接在一起的,是同一根SDA。
画成两个SDA 有助于深刻理解在I2C 总线上主机和从机的不同行为。
[~OO7PSDJ2UA)F.jpg)
13. 传输多个字节数据的时序图
主机连续向从机发送或从从机接收多个字节数据的情况也很容易理解,下面直接给出相关时序图。


14. 重复起始条件(Repeated START condition)
主机与从机进行通信时,有时需要切换数据的收发方向。例如,访问某一具有I2C 总线
接口的E2PROM 存储器时,主机先向存储器输入存储单元的地址信息(发送数据),然后再
读取其中的存储内容(接收数据)。
在切换数据的传输方向时,可以不必先产生停止条件再开始下次传输,而是直接再一次
产生开始条件。I2C 总线在已经处于忙的状态下,再一次直接产生起始条件的情况被称为重
复起始条件。重复起始条件常常简记为Sr。
正常的起始条件和重复起始条件在物理波形上并没有什么不同,区别仅仅是在逻辑方
面。在进行多字节数据传输过程中,只要数据的收发方向发生了切换,就要用到重复起始条
件。
9N%7BVA%7D5.jpg)
图 10 给出了带有重复起始条件的多字节数据传输格式示意图。要特别注意图中重复起
始条件Sr 的用法。如果读者有兴趣的话,可以自行画出其对应的时序图。
15. 子地址
带有I2C 总线的器件除了有从机地址(Slave Address)外,还可能有子地址。从机地址
是指该器件在I2C 总线上被主机寻址的地址,而子地址是指该器件内部不同部件或存储单元
的编址。例如,带I2C 总线接口的E2PROM 就是拥有子地址器件的典型代表。
某些器件(只占少数)内部结构比较简单,可能没有子地址,只有必须的从机地址。
与从机地址一样,子地址实际上也是像普通数据那样进行传输的,传输格式仍然是与数
据相统一的,区分传输的到底是地址还是数据要靠收发双方具体的逻辑约定。子地址的长度
必须由整数个字节组成,可能是单字节(8 位子地址),也可能是双字节(16 位子地址),还
可能是3 字节以上,这要看具体器件的规定。
15. 用GPIO模拟I2C实例程序
下面程序为用GPIO模拟I2C接口功能,接口速率可以通过调节for循环来获得。本程序已经经过验证,
移植时需要根据平台接口进行相应更改即可。此外,此模拟程序只支持单I2C设备。
头文件gpio_i2c.h
#ifndef I2C_H
#define I2C_H
// Proto definition
void i2cStart(void);
void i2cStop (void);
bool i2cGetAck(void);
void i2cPutAck(bool isACK);
void i2cSendByte(u8 data);
u8 i2cGetByte (void);
bool i2c_Open(u8 scl,u8 sda);
void i2c_Close(void);
bool i2c_SendData(u8 Addr, u8 *dataBuffer, u8 Size);
bool i2c_GetData(u8 Addr, u8 *dataBuffer, u8 Size);
#endif // I2C_H
gpio_i2c.c
#include "drv_i2c.h"
#if 0
#include "io.h"
#define i2c_fprintf(x) sxs_fprintf x
#else
#define i2c_fprintf(x)
#endif
// Global variant
u32 I2C_SDA;
u32 I2C_SCL;
u32 I2C_BUS;
// Delay Time (Max I2C = 400 KHz)
// 20 ---> 195 KHz
// 10 ---> 355 KHz
#define DELAY 20
void i2cDelay(u16 cnt)
{
u16 i;
for (i=0;i<cnt;i++);
}
// I2C Extern Interface API
/* -------------------------------------------------------------------------*/
/* Function : i2c_Open */
/* Description: Judge if defines GPIO as I2C bus and if the GPIO used right.*/
/* Parameters : None */
/* Return : TRUE if OK. */
/* FALSE if Wrong. */
/* -------------------------------------------------------------------------*/
bool i2c_Open(u8 scl,u8 sda)
{
i2c_fprintf((TSTDOUT, "Open I2C Bus."));
I2C_SCL = 1 << scl;
I2C_SDA = 1 << sda;
I2C_BUS = I2C_SCL | I2C_SDA;
//if ((I2C_SDA & USED_GPIO)==0 || (I2C_SCL & USED_GPIO)==0)
//{
// i2c_fprintf((TSTDOUT, "I2C GPIO Used Wrong in Board Config."));
// return(FALSE);
//}
//else
{
//i2c_fprintf((TSTDOUT, "I2C GPIO Used Right in Board Config."));
i2c_fprintf((TSTDOUT, "I2C_SDA is GPIO 0x%x", I2C_SDA));
i2c_fprintf((TSTDOUT, "I2C_SCL is GPIO 0x%x", I2C_SCL));
// Set GPIO as output and set output=1
hal_gpio_SetOut(I2C_BUS); // Pay attention that SCL is always output and
hal_gpio_SetBit(I2C_BUS); // SDA can be output or input. So please make sure
// the SDA pin direction first before used.
return(TRUE);
}
}
/* -------------------------------------------------------------------------*/
/* Function : i2c_Close */
/* Description: Close gpio I2C Bus to idle. */
/* Parameters : None */
/* Return : None */
/* -------------------------------------------------------------------------*/
void i2c_Close(void)
{
hal_gpio_SetOut(I2C_BUS);
hal_gpio_SetBit(I2C_BUS);
}
/* -------------------------------------------------------------------------*
* Function : i2c_SendData *
* Description: Send I2C data format. *
* Parameters : Addr -- Slave Address *
* dataBuffer -- Data pointer to be sent *
* Size -- Data size will be sent *
* Return : TURE is sending to slave OK, FALSE is sending fail. *
* -------------------------------------------------------------------------*/
// The Format of I2C Write Telgram
// |Mast | M->S |M->S |S->M | M->S |S->M | M->S |S->M |Mast|
// |Start| 7 bits | R/W | ACK | 8 bits | ACK | 8 bits |N/ACK|STOP|
// |Start|SlaveAddr|WRITE| ACK |DATA - HIGH BYTE| ACK |DATA - LOW BYTE|N/ACK|STOP|
bool i2c_SendData(u8 Addr, u8 *dataBuffer, u8 Size)
{
u8 bufSize = Size;
i2cStart();
i2cSendByte(Addr<<1);
if (!i2cGetAck())
{
i2cStop();
return FALSE;
}
do
{
i2cSendByte(*dataBuffer++);
if (!i2cGetAck())
{
i2cStop();
return FALSE;
}
}
while((--bufSize)!=0);
i2cStop();
return TRUE;
}
/* -------------------------------------------------------------------------*
* Function : i2c_GetData *
* Description: Get I2C data from I2C Slave. *
* Parameters : Addr -- Slave Address *
* dataBuffer -- Data pointer to be stored *
* Size -- Data size will be stored *
* Return : TURE is sending to slave OK, FALSE is sending fail. *
* -------------------------------------------------------------------------*/
// The Format of I2C Write Telgram
// |Mast | M->S |M->S|S->M | S->M |M->S | S->M |M->S|Mast|
// |Start| 7 bits |R/W | ACK | 8 bits | ACK | 8 bits |NACK|STOP|
// |Start|SlaveAddr|READ| ACK |DATA - HIGH BYTE| ACK |DATA - LOW BYTE|NACK|STOP|
bool i2c_GetData(u8 Addr, u8 *dataBuffer, u8 Size)
{
u8 bufSize = Size;
i2cStart();
i2cSendByte((Addr<<1)|0x1);
if (!i2cGetAck())
{
i2cStop();
return FALSE;
}
do
{
*dataBuffer++ = i2cGetByte();
i2cPutAck(TRUE);
}
while((--bufSize)!=1);
*dataBuffer = i2cGetByte();
i2cPutAck(FALSE);
i2cStop();
return (TRUE);
}
// I2C Internal API
/* -------------------------------------------------------------------------*
* Function : i2cStart *
* Description: SDA change from high to low when SCL keep in high. *
* Parameters : None *
* Return : None *
* -------------------------------------------------------------------------*/
void i2cStart(void)
{
hal_gpio_SetOut(I2C_SDA);
hal_gpio_SetBit(I2C_SDA);
hal_gpio_SetBit(I2C_SCL);
i2cDelay(DELAY);
hal_gpio_ClrBit(I2C_SDA);
i2cDelay(DELAY/2);
hal_gpio_ClrBit(I2C_SCL);
i2cDelay(DELAY/2);
}
/* -------------------------------------------------------------------------*
* Function : i2cStop *
* Description: SDA change from low to high when SCL keep in high. *
* Parameters : None *
* Return : None *
* -------------------------------------------------------------------------*/
void i2cStop(void)
{
hal_gpio_SetOut(I2C_SDA);
hal_gpio_ClrBit(I2C_SDA);
i2cDelay(DELAY/2);
hal_gpio_SetBit(I2C_SCL);
i2cDelay(DELAY/2);
hal_gpio_SetBit(I2C_SDA);
// After STOP, I2C_BUS goes into IDLE state.
}
/* -------------------------------------------------------------------------*
* Function : i2cSendByte *
* Description: Send the byte output of gpio, MSB first. *
* Parameters : data to be shifted. *
* Return : None *
* -------------------------------------------------------------------------*/
void i2cSendByte(u8 data)
{
u8 cnt = 8;
hal_gpio_SetOut(I2C_SDA);
do
{
if (data & 0x80)
{
hal_gpio_SetBit(I2C_SDA);
}
else
{
hal_gpio_ClrBit(I2C_SDA);
}
data<<=1;
i2cDelay(DELAY/2);
hal_gpio_SetBit(I2C_SCL);
i2cDelay(DELAY);
hal_gpio_ClrBit(I2C_SCL);
i2cDelay(DELAY/2);
}
while (--cnt != 0);
}
/* -------------------------------------------------------------------------*
* Function : i2cGetByte *
* Description: Get the Data from slave device *
* Parameters : None *
* Return : Received data *
* -------------------------------------------------------------------------*/
u8 i2cGetByte(void)
{
u8 cnt = 8;
u8 data = 0;
// hal_gpio_SetOut(I2C_SDA);
// hal_gpio_SetBit(I2C_SDA); // Should pull up SDA before read.
// i2cDelay(DELAY);
hal_gpio_SetIn(I2C_SDA); // Set GPIO as input
i2cDelay(DELAY/2);
do
{
hal_gpio_SetBit(I2C_SCL);
i2cDelay(DELAY/2);
data<<=1;
if (hal_gpio_GetVal(I2C_SDA)) data++; // Get input data bit
i2cDelay(DELAY/2);
hal_gpio_ClrBit(I2C_SCL);
i2cDelay(DELAY);
}
while (--cnt != 0);
return(data);
}
/* -------------------------------------------------------------------------*
* Function : i2cGetAck *
* Description: During SCL high, when SDA holds low is ACK otherwise NACK. *
* Parameters : No *
* Return : TRUE is ACK, FALSE is NACK. *
* Notes : This function gets the ACK signal from the slave. *
* -------------------------------------------------------------------------*/
bool i2cGetAck(void)
{
bool isACK;
hal_gpio_SetIn(I2C_SDA);
hal_gpio_ClrBit(I2C_SCL);
i2cDelay(DELAY/2);
hal_gpio_SetBit(I2C_SCL);
i2cDelay(DELAY/2);
// Read Line at the middle of input, it is stable.
if (hal_gpio_GetVal(I2C_SDA)) isACK = FALSE;
else isACK = TRUE;
i2cDelay(DELAY/2);
hal_gpio_ClrBit(I2C_SCL);
i2cDelay(DELAY/2);
return isACK;
}
/* -------------------------------------------------------------------------*
* Function : i2cPutAck *
* Description: During SCL high, when SDA holds low is ACK otherwise NACK. *
* Parameters : TRUE is ACK, FALSE is NACK *
* Return : *
* Notes : This function send ACK or NACK to slave device *
* -------------------------------------------------------------------------*/
void i2cPutAck(bool isACK)
{
hal_gpio_SetOut(I2C_SDA);
if (isACK) hal_gpio_ClrBit(I2C_SDA);
else hal_gpio_SetBit(I2C_SDA);
i2cDelay(DELAY/2);
hal_gpio_SetBit(I2C_SCL);
i2cDelay(DELAY);
hal_gpio_ClrBit(I2C_SCL);
i2cDelay(DELAY/2);
}