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 所示,注意连接时需要共地。
一般具有 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)
数据线 SDA 的电平状态必须在时钟线 SCL 处于高电平期间保持稳定不变。SDA 的电平状态只有在 SCL 处于低电平期间才允许改变。但是在 I2C总线的起始和结束时例外。
注:某些其它的串行总线协议,如 SPI,可能规定数据在时钟信号的边沿(上升沿或下降沿)有效,而I2C总线则是低电平有效。
7. 起始条件和停止条件(START and STOP conditions)
起始条件 当 SCL 处于高电平期间时,SDA 从高电平向低电平跳变时产生起始条件。总线在起始条件产生后便处于忙的状态。起始条件常常简记为 S。
停止条件 当 SCL 处于高电平期间时,SDA 从低电平向高电平跳变时产生停止条件。总线在停止条件产生后处于空闲状态。停止条件简记为 P。
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总线上主机和从机的不同行为。
13. 传输多个字节数据的时序图
主机连续向从机发送或从从机接收多个字节数据的情况也很容易理解, 下面直接给出相关时序图。
14. 重复起始条件(Repeated START condition)
主机与从机进行通信时,有时需要切换数据的收发方向。例如,访问某一具有 I2C总线接口的 E2PROM 存储器时,主机先向存储器输入存储单元的地址信息(发送数据) ,然后再读取其中的存储内容(接收数据) 。
在切换数据的传输方向时,可以不必先产生停止条件再开始下次传输,而是直接再一次产生开始条件。I2C 总线在已经处于忙的状态下,再一次直接产生起始条件的情况被称为重复起始条件。重复起始条件常常简记为 Sr。
正常的起始条件和重复起始条件在物理波形上并没有什么不同,区别仅仅是在逻辑方面。在进行多字节数据传输过程中,只要数据的收发方向发生了切换,就要用到重复起始条件。
图 10 给出了带有重复起始条件的多字节数据传输格式示意图。要特别注意图中重复起始条件 Sr 的用法。如果读者有兴趣的话,可以自行画出其对应的时序图。
15. 子地址
带有 I2C 总线的器件除了有从机地址(Slave Address)外,还可能有子地址。从机地址是指该器件在 I2C 总线上被主机寻址的地址, 而子地址是指该器件内部不同部件或存储单元的编址。例如,带 I2C 总线接口的 E2PROM 就是拥有子地址器件的典型代表。
某些器件(只占少数)内部结构比较简单,可能没有子地址,只有必须的从机地址。 与从机地址一样,子地址实际上也是像普通数据那样进行传输的,传输格式仍然是与数据相统一的,区分传输的到底是地址还是数据要靠收发双方具体的逻辑约定。子地址的长度必须由整数个字节组成,可能是单字节(8 位子地址) ,也可能是双字节(16 位子地址) ,还可能是 3 字节以上,这要看具体器件的规定。
_____________________________________________________________________________________________________________________________
I2S(Inter-IC Sound Bus)是飞利浦公司为数字音频设备之间的音频 数据传输而制定的一种总线标准。
I2S有3个主要信号:1.串行时钟SCLK,也叫位时钟(BCLK),即对应数字音频的每一位数据,SCLK都有1个脉冲。SCLK的频率=2×采样频率×采样位数 2.帧时钟LRCK,用于切换左右声道的数据。LRCK为“1”表示正在传输的是左声道的数据,为“0”则表示正在传输的是右声道的数据。LRCK的频率等于采样频率。3.串行数据SDATA,就是用二进制补码表示的音频数据。I2S(Inter-IC Sound Bus)是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准。在飞利浦公司的I2S标准中,既规定了硬件接口规范,也规定了数字音频数据的格式。I2S有3个主要信号:1.串行时钟SCLK,也叫位时钟(BCLK),即对应数字音频的每一位数据,SCLK都有1个脉冲。SCLK的频率=2×采样频率×采样位数 2. 帧时钟LRCK,用于切换左右声道的数据。LRCK为“1”表示正在传输的是左声道的数据,为“0”则表示正在传输的是右声道的数据。LRCK的频率等于采样频率。3.串行数据SDAT就是用二进制补码表示的音频数据。
一个典型的I2S信号见图3。
I2S格式的信号无论有多少位有效数据,数据的最高位总是出现在LRCK变化(也就是一帧开始)后的第2个SCLK脉冲处。这就使得接收端与发送端的有效位数可以不同。如果接收端能处理的有效位数少于发送端,可以放弃数据帧中多余的低位数据;如果接收端能处理的有效位数多于发送端,可以自行补足剩余的位。这种同步机制使得数字音频设备的互连更加方便,而且不会造成数据错位。
______________________________________________________________________________________________________________________________
SPI:串行外围设备接口SPI(serial peripheral interface)总线技术是Motorola公司推出的一种同步串行接口,Motorola公司生产的绝大多数MCU(微控制器)都配有SPI硬件接口,如68系列MCU。SPI用于CPU与各种外围器件进行全双工、同步串行通讯。SPI可以同时发出和接收串行数据。它只需四条线就可以完成MCU与各种外围器件的通讯,这四条线是:串行时钟线(CSK)、主机输入/从机输出数据线(MISO)、主机输出/从机输入数据线(MOSI)、低电平有效从机选择线CS。这些外围器件可以是简单的TTL移位寄存器,复杂的LCD显示驱动器,A/D、D/A转换子系统或其他的MCU。当SPI工作时,在移位寄存器中的数据逐位从输出引脚(MOSI)输出(高位在前),同时从输入引脚(MISO)接收的数据逐位移到移位寄存器(高位在前)。发送一个字节后,从另一个外围器件接收的字节数据进入移位寄存器中。主SPI的时钟信号(SCK)使传输同步。其典型系统框图如下图所示。
图2示出SPI总线工作的四种方式,其中使用的最为广泛的是SPI0和SPI3方式(实线表示):、
SPI模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(CPOL)对传输协议没有重大的影响。如果 CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(CPHA)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI主模块和与之通信的外设音时钟相位和极性应该一致。SPI总线接口时序如图所示。
____________________________________________________________________________________________________________________________
以下为用两个GPIO口模拟I2C总线的程序:
#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 } // 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); } 参考:众发米业www.hubeidami.sinaapp.com