1.I2C介绍
I2C(Inter-Integrated Circuit)总线是由 PHILIPS 公司开发的两线式 串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域广泛采用的 一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单, 器件封装形式小,通信速率较高等优点。I2C 总线只有两根双向信号线。一根是 数据线 SDA,另一根是时钟线 SCL。由于其管脚少,硬件实现简单,可扩展性强 等特点,因此被广泛的使用在各大集成芯片内。下面我们就从 I2C 的物理层与 协议层来了解 I2C。
1.1 I2C物理层
I2C 通信设备常用的连接方式如下图所示:
它的物理层有如下特点:
- 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在 一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通 讯从机。
- 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA),一 条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
- 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址 进行不同设备之间的访问。
- 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而 当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
- 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定 由哪个设备占用总线。
- 具有三种传输模式:标准模式传输速率为 100kbit/s,快速模式为 400kbit/s,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模 式。
- 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
下面我们来了解下 I2C 总线常用的一些术语:
主机:启动数据传送并产生时钟信号的设备;
从机:被主机寻址的器件;
多主机:同时有多于一个主机尝试控制总线但不破坏传输;
主模式:用 I2CNDAT 支持自动字节计数的模式; 位 I2CRM,I2CSTT,I2CSTP 控制数据的接收和发送;
从模式:发送和接收操作都是由 I2C 模块自动控制的;
仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并 使传输不被破坏的过程;
同步:两个或多个器件同步时钟信号的过程;
发送器:发送数据到总线的器件;
接收器:从总线接收数据的器件。
1.2 I2C协议层
I2C 的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟 同步和地址广播等环节。下面我们就来简单介绍下。
(1)数据有效性规定
I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保 持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态 才允许变化。如下图:
每次数据传输都以字节为单位,每次传输的字节数不受限制。
(2)起始和停止信号
SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;SCL 线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。如下图:
起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用 的状态;在终止信号产生后,总线就处于空闲状态。
(3)应答响应
每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,这个校 验位是接收端通过控制 SDA(数据线)来实现的,以提醒发送端数据我这边已经 接收完成,数据传送可以继续进行。这个校验位其实就是数据或地址传输过程中 的响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收 端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望 对方继续发送数据,则需要向对方发送“应答(ACK)”信号即特定的低电平脉冲, 发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非 应答(NACK)”信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止 信号,结束信号传输。应答响应时序图如下:
每一个字节必须保证是 8 位长度。数据传送时,先传送最高位(MSB),每 一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)。
由于某种原因从机不对主机寻址信号应答时(如从机正在进行实时性的处理 工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。
如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的 数据时,从机可以通过对无法接收的第一个数据字节的“非应答” 通知主机, 主机则应发出终止信号以结束数据的继续传送。
当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束 传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放 SDA 线,以允许主机产生终止信号。
这些信号中,起始信号是必需的,结束信号和应答信号都可以不要。
(4)总线的寻址方式 I2C 总线寻址按照从机地址位数可分为两种,一种是 7 位,另一种是 10 位。采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如 下:
D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“ 0”时表示主机 向从机写数据,为“1”时表示主机由从机读数据。
10 位寻址和 7 位寻址兼容,而且可以结合使用。10 位寻址不会影响已有 的 7 位寻址,有 7 位和 10 位地址的器件可以连接到相同的 I2C 总线。我们 就以 7 位寻址为例进行介绍。
当主机发送了一个地址后,总线上的每个器件都将头 7 位与它自己的地址 比较,如果一样,器件会判定它被主机寻址,其他地址不同的器件将被忽略后面 的数据信号。至于是从机接收器还是从机发送器,都由 R/W 位决定的。从机的 地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从 机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机 的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址 8 个同样的 器件,即可以有 8 个同样的器件接入到该 I2C 总线系统中。
(5)数据传输
I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据 信号。在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/W),用“ 0”表示主机发送(写)数据(W),“ 1”表示主机接收 数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望 继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信 号对另一从机进行寻址。
在总线的一次数据传送过程中,可以有以下几种组合方式:
a.主机向从机发送数据,数据传送方向在整个传送过程中不变
注意:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从 机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表 示终止信号。
b.主机在第一个字节后,立即从从机读数据
c.在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复 产生一次,但两次读/写方向位正好相反
到这里我们就介绍完了 I2C 总线,由于 51 单片机没有硬件 IIC 接口,即使 有硬件接口我们通常还是采用软件模拟 I2C。主要原因是硬件 IIC 设计的比较 复杂,而且稳定性不怎么好,程序移植比较麻烦,而用软件模拟 IIC,最大的好 处就是移植方便,同一个代码兼容所有单片机,任何一个单片机只要有 IO 口(不 需要特定 IO),都可以很快的移植过去。 下面我们再来了解下开发板上的 AT24C02 芯片。
2. AT24C02芯片介绍
AT24C01/02/04/08/16...是一个 1K/2K/4K/8K/16K 位串行 CMOS,内部含有 128/256/512/1024/2048 个 8 位字节,AT24C01 有一个 8 字节页写缓冲器, AT24C02/04/08/16 有一个 16 字节页写缓冲器。该器件通过 I2C 总线接口进行 操作,它有一个专门的写保护功能。我们开发板上使用的是 AT24C02(EEPROM)芯片,此芯片具有 I2C 通信接口,芯片内保存的数据在掉电情况下都不丢失, 所以通常用于存放一些比较重要的数据等。AT24C02 芯片管脚及外观图如下图所 示:
芯片管脚说明如下图所示:
AT24C02 器件地址为 7 位,高 4 位固定为 1010,低 3 位由 A0/A1/A2 信 号线的电平决定。 因为传输地址或数据是以字节为单位传送的,当传送地址时, 器件地址占 7 位,还有最后一位(最低位 R/W)用来选择读写方向,它与地址 无关。其格式如下:
我们开发板已经将芯片的 A0/A1/A2 连接到 GND,所以器件地址为 1010000,即 0x50(未计算最低位)。如果要对芯片进行写操作时,R/W 即为 0, 写器件地址即为 0XA0;如果要对芯片进行读操作时,R/W 即为 1,此时读器件 地址为 0XA1。开发板上我们也将 WP 引脚直接接在 GND 上,此时芯片允许数据 正常读写。
I2C 总线时序如下图所示:
3.硬件设计
本实验使用到硬件资源如下:
(4)软件设计
本章所要实现的功能是:首先检测 AT24C02 芯片是否存在,如果存在则通 过串口输出提示信息,然后通过按键 K1、K2 和 K3 控制 AT24C02 数据读写及清 除,并通过数码管显示写入和读取的数据信息,最后让 D1 指示灯闪烁提示系统 正常运行。程序框架如下: (1)使能所用 GPIO 端口时钟,初始化 GPIO
/*******************************************************************************
* 函 数 名 : IIC_Init
* 函数功能 : IIC初始化
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IICA_Init(void)
{
EALLOW;
SysCtrlRegs.PCLKCR3.bit.GPIOINENCLK = 1;// 开启GPIO时钟
GpioCtrlRegs.GPBPUD.bit.GPIO32 = 0; //上拉
GpioCtrlRegs.GPBDIR.bit.GPIO32 = 1; // 输出端口
GpioCtrlRegs.GPBMUX1.bit.GPIO32 = 0; // IO口
GpioCtrlRegs.GPBQSEL1.bit.GPIO32 = 3; // 不同步
GpioCtrlRegs.GPBPUD.bit.GPIO33 = 0; //上拉
GpioCtrlRegs.GPBDIR.bit.GPIO33 = 1; // 输出端口
GpioCtrlRegs.GPBMUX1.bit.GPIO33 = 0; // IO口
GpioCtrlRegs.GPBQSEL1.bit.GPIO33 = 3; // 不同步
EDIS;
}
/*******************************************************************************
* 函 数 名 : SDA_OUT
* 函数功能 : SDA输出配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void SDA_OUT(void)
{
EALLOW;
GpioCtrlRegs.GPBDIR.bit.GPIO32=1; //Output. SDA
EDIS;
}
/*******************************************************************************
* 函 数 名 : SDA_IN
* 函数功能 : SDA输入配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void SDA_IN(void)
{
EALLOW;
GpioCtrlRegs.GPBDIR.bit.GPIO32=0; //Input, SDA
EDIS;
}
/*******************************************************************************
* 函 数 名 : IIC_Start
* 函数功能 : 产生IIC起始信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_Start(void)
{
SDA_OUT(); //sda线输出
IIC_SDA_SETH;
IIC_SCL_SETH;
DELAY_US(5);
IIC_SDA_SETL;//START:when CLK is high,DATA change form high to low
DELAY_US(6);
IIC_SCL_SETL;//钳住I2C总线,准备发送或接收数据
}
/*******************************************************************************
* 函 数 名 : IIC_Stop
* 函数功能 : 产生IIC停止信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_Stop(void)
{
SDA_OUT();//sda线输出
IIC_SCL_SETL;
IIC_SDA_SETL;//STOP:when CLK is high DATA change form low to high
IIC_SCL_SETH;
DELAY_US(6);
IIC_SDA_SETH;//发送I2C总线结束信号
DELAY_US(6);
}
/*******************************************************************************
* 函 数 名 : IIC_Wait_Ack
* 函数功能 : 等待应答信号到来
* 输 入 : 无
* 输 出 : 1,接收应答失败
0,接收应答成功
*******************************************************************************/
unsigned char IIC_Wait_Ack(void)
{
unsigned char tempTime=0;
IIC_SDA_SETH;
DELAY_US(1);
SDA_IN(); //SDA设置为输入
IIC_SCL_SETH;
DELAY_US(1);
while(READ_SDA)
{
tempTime++;
if(tempTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL_SETL;//时钟输出0
return 0;
}
/*******************************************************************************
* 函 数 名 : IIC_Ack
* 函数功能 : 产生ACK应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_Ack(void)
{
IIC_SCL_SETL;
SDA_OUT();
IIC_SDA_SETL;
DELAY_US(2);
IIC_SCL_SETH;
DELAY_US(5);
IIC_SCL_SETL;
}
/*******************************************************************************
* 函 数 名 : IIC_NAck
* 函数功能 : 产生NACK非应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void IIC_NAck(void)
{
IIC_SCL_SETL;
SDA_OUT();
IIC_SDA_SETH;
DELAY_US(2);
IIC_SCL_SETH;
DELAY_US(5);
IIC_SCL_SETL;
}
/*******************************************************************************
* 函 数 名 : IIC_Send_Byte
* 函数功能 : IIC发送一个字节
* 输 入 : txd:发送一个字节
* 输 出 : 无
*******************************************************************************/
void IIC_Send_Byte(unsigned char txd)
{
unsigned char t;
SDA_OUT();
IIC_SCL_SETL;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
if((txd&0x80)>0) //0x80 1000 0000
IIC_SDA_SETH;
else
IIC_SDA_SETL;
txd<<=1;
DELAY_US(2); //对TEA5767这三个延时都是必须的
IIC_SCL_SETH;
DELAY_US(2);
IIC_SCL_SETL;
DELAY_US(2);
}
}
/*******************************************************************************
* 函 数 名 : IIC_Read_Byte
* 函数功能 : IIC读一个字节
* 输 入 : ack=1时,发送ACK,ack=0,发送nACK
* 输 出 : 应答或非应答
*******************************************************************************/
unsigned char IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL_SETL;
DELAY_US(2);
IIC_SCL_SETH;
receive<<=1;
if(READ_SDA)receive++;
DELAY_US(1);
}
if (!ack)
IIC_NAck();//发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
#define AT24C01 127
#define AT24C02 255
#define AT24C04 511
#define AT24C08 1023
#define AT24C16 2047
#define AT24C32 4095
#define AT24C64 8191
#define AT24C128 16383
#define AT24C256 32767
//开发板使用的是24c02,所以定义EE_TYPE为AT24C02
#define EE_TYPE AT24C02
/*******************************************************************************
* 函 数 名 : AT24CXX_Init
* 函数功能 : AT24CXX初始化
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Init(void)
{
IICA_Init();//IIC初始化
}
/*******************************************************************************
* 函 数 名 : AT24CXX_ReadOneByte
* 函数功能 : 在AT24CXX指定地址读出一个数据
* 输 入 : ReadAddr:开始读数的地址
* 输 出 : 读到的数据
*******************************************************************************/
unsigned char AT24CXX_ReadOneByte(Uint16 ReadAddr)
{
unsigned char temp=0;
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr>>8);//发送高地址
}
else
{
IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //进入接收模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop();//产生一个停止条件
return temp;
}
/*******************************************************************************
* 函 数 名 : AT24CXX_WriteOneByte
* 函数功能 : 在AT24CXX指定地址写入一个数据
* 输 入 : WriteAddr :写入数据的目的地址
DataToWrite:要写入的数据
* 输 出 : 无
*******************************************************************************/
void AT24CXX_WriteOneByte(Uint16 WriteAddr,unsigned char DataToWrite)
{
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8);//发送高地址
}
else
{
IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop();//产生一个停止条件
DELAY_US(10*1000);
}
/*******************************************************************************
* 函 数 名 : AT24CXX_WriteLenByte
* 函数功能 : 在AT24CXX里面的指定地址开始写入长度为Len的数据
用于写入16bit或者32bit的数据
* 输 入 : WriteAddr :写入数据的目的地址
DataToWrite:要写入的数据
Len :要写入数据的长度2,4
* 输 出 : 无
*******************************************************************************/
void AT24CXX_WriteLenByte(Uint16 WriteAddr,Uint32 DataToWrite,unsigned char Len)
{
unsigned char t;
for(t=0;t<Len;t++)
{
AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
}
}
/*******************************************************************************
* 函 数 名 : AT24CXX_ReadLenByte
* 函数功能 : 在AT24CXX里面的指定地址开始读出长度为Len的数据
用于读出16bit或者32bit的数据
* 输 入 : ReadAddr :开始读出的地址
Len :要读出数据的长度2,4
* 输 出 : 读取的数据
*******************************************************************************/
Uint32 AT24CXX_ReadLenByte(Uint16 ReadAddr,unsigned char Len)
{
unsigned char t;
Uint32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1);
}
return temp;
}
/*******************************************************************************
* 函 数 名 : AT24CXX_Check
* 函数功能 : 检查AT24CXX是否正常
* 输 入 : 无
* 输 出 : 1:检测失败,0:检测成功
*******************************************************************************/
unsigned char AT24CXX_Check(void)
{
unsigned char temp;
temp=AT24CXX_ReadOneByte(255);//避免每次开机都写AT24CXX
if(temp==0x36)return 0;
else//排除第一次初始化的情况
{
AT24CXX_WriteOneByte(255,0X36);
temp=AT24CXX_ReadOneByte(255);
if(temp==0X36)return 0;
}
return 1;
}
/*******************************************************************************
* 函 数 名 : AT24CXX_Read
* 函数功能 : 在AT24CXX里面的指定地址开始读出指定个数的数据
* 输 入 : ReadAddr :开始读出的地址 对24c02为0~255
pBuffer :数据数组首地址
NumToRead:要读出数据的个数
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Read(Uint16 ReadAddr,unsigned char *pBuffer,Uint16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
/*******************************************************************************
* 函 数 名 : AT24CXX_Write
* 函数功能 : 在AT24CXX里面的指定地址开始写入指定个数的数据
* 输 入 : WriteAddr :开始写入的地址 对24c02为0~255
pBuffer :数据数组首地址
NumToRead:要读出数据的个数
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Write(Uint16 WriteAddr,unsigned char *pBuffer,Uint16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}
void main()
{
int i=0;
unsigned char key=0,k=0;
InitSysCtrl();
InitPieCtrl();
IER = 0x0000;
IFR = 0x0000;
InitPieVectTable();
LED_Init();
TIM0_Init(150,200000);//200ms
UARTa_Init(4800);
SMG_Init();
KEY_Init();
AT24CXX_Init();
while(AT24CXX_Check()) //检测AT24C02是否正常
{
UARTa_SendString("AT24C02检测不正常!\r\n");
DELAY_US(500*1000);
}
UARTa_SendString("AT24C02检测正常!\r\n");
while(1)
{
key=KEY_Scan(0);
if(key==KEY1_PRESS)
{
k++;
if(k>255)
{
k=255;
}
AT24CXX_WriteOneByte(0,k);
}
else if(key==KEY2_PRESS)
{
k=AT24CXX_ReadOneByte(0);
}
else if(key==KEY3_PRESS)
{
k=0;
}
SMG_DisplayInt(k);
}
}
版权声明:本文为CSDN博主「时光话」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012616827/article/details/122162536
暂无评论