I2C协议的使用与实践
1. I2C通信协议概述
I2C(Inter-Integrated Circuit)是由飞利浦在1982年设计的一种同步串行通信协议,它仅使用两条线:SDA(数据线)和SCL(时钟线),因此有时也被称为TWI(Two-Wire Interface)。I2C总线可以有多个主设备和多个从设备,其简单架构如下图所示:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
Master(主设备):::process -->|SDA| Slave1(从设备1):::process
Master -->|SCL| Slave1
Master -->|SDA| Slave2(从设备2):::process
Master -->|SCL| Slave2
Master -->|SDA| Slave3(从设备3):::process
Master -->|SCL| Slave3
与SPI通信系统相比,I2C仅需两条线是其优势,但该协议也有更严格的要求。
2. EEPROM介绍
EEPROM(Electrically Erasable Programmable Read-Only Memory)用于永久存储数据。它最初源于ROM(只读存储器),用于存储系统的BIOS程序,采用非易失性存储器,断电后数据不会丢失。后来发展到PROM(可编程ROM),用户和制造商都可以对其进行编程并永久存储数据。接着是EPROM(可擦除可编程ROM),可以擦除程序或数据以便重新编程,但需要使用紫外线进行擦除。最终发展到EEPROM,可通过发送适当的电信号来擦除数据,既可以永久存储重要数据,又能方便地更改内容,非常实用。
3. 24LC256 EEPROM
3.1 基本信息
24LC256是Microchip生产的一款32k×8位串行EEPROM,符合I2C协议,有八引脚双列直插封装(PDIP)等多种封装形式。
3.2 引脚使用
其引脚使用情况如下表所示:
|引脚编号|用途|
| ---- | ---- |
|1|A0:三个地址线中的第一个|
|2|A1:三个地址线中的第二个|
|3|A2:三个地址线中的第三个|
|4|VSS:接地端|
|5|SDA:数据端,可作为数据输入和输出|
|6|SCL:时钟输入端|
|7|WP:写保护端|
|8|VCC:正电源端|
3.3 通信连接
24LC256与主设备通信仅需两个连接:SCL提供主设备到从设备的时钟信号;SDA在主设备写入时作为数据输入,从设备发送数据时作为数据输出。
3.4 地址设置
它使用A0、A1和A2三条地址线,最多可使八个EEPROM从同一主设备进行单独寻址。WP引脚可防止意外写入,该引脚高电平有效,即逻辑‘1’时,EEPROM不可写入。
3.5 写入操作
向24LC256写入数据的步骤如下:
1. 主设备发送起始位,即在时钟信号为高电平时,SDA线从高到低的转变。
2. 发送控制字节,前四位为控制码1010,接下来三位是EEPROM的地址位A0、A1和A2,必须与实际EEPROM上的三个地址引脚连接的逻辑匹配,最后一位用于确定主设备是写入还是读取操作,写入时该位为逻辑‘0’。
3. 从设备生成确认位,在主设备生成的第九个时钟脉冲为高电平时,将SDA线拉低,每个字节传输后都会产生确认位。
4. 主设备发送要写入或读取的16位地址的高字节,由于是32KB设备,实际上只需要15条地址线,高字节的最高有效位不重要。从设备再次确认该字节。
5. 主设备发送地址的低字节,从设备确认。
6. 主设备发送要写入指定地址的数据字节,从设备确认。
7. 主设备发送停止位,即在SCL线为高电平时,SDA线从低到高的转变。
写入操作流程如下:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(发送起始位):::process --> B(发送控制字节):::process
B --> C(等待从设备确认):::process
C --> D(发送地址高字节):::process
D --> E(等待从设备确认):::process
E --> F(发送地址低字节):::process
F --> G(等待从设备确认):::process
G --> H(发送数据字节):::process
H --> I(等待从设备确认):::process
I --> J(发送停止位):::process
如果要连续写入多个地址,可在第一个数据确认后、主设备发送停止位前发送更多数据,EEPROM会自动打开下一个存储位置。
3.6 读取操作
从24LC256读取数据的步骤如下:
1. 主设备发送起始位,接着发送包含从设备地址的控制字节,最后一位设置为逻辑‘0’,这仍是写入操作,因为主设备需要先写入要开始读取的地址。
2. 从设备确认控制字节后,主设备发送要开始读取的地址的高字节和低字节,从设备发送确认位。
3. 主设备发送新的起始位,接着发送相同的控制和从设备地址信息,但新控制字节的最后一位设置为逻辑‘1’,表示主设备要从刚发送的地址读取数据。
4. 从设备确认新控制字节后,将请求的数据放在SDA总线上,随后是一个非确认位,主设备发送停止位。
读取操作流程如下:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(发送起始位):::process --> B(发送控制字节):::process
B --> C(等待从设备确认):::process
C --> D(发送地址高字节):::process
D --> E(等待从设备确认):::process
E --> F(发送地址低字节):::process
F --> G(等待从设备确认):::process
G --> H(发送新起始位):::process
H --> I(发送控制字节,最后一位为1):::process
I --> J(等待从设备确认):::process
J --> K(从设备发送数据):::process
K --> L(主设备发送非确认位):::process
L --> M(主设备发送停止位):::process
同样,如果要连续读取多个地址,可在发送非确认位和停止位前请求更多数据,EEPROM会自动打开下一个存储位置。
4. TC74温度传感器
4.1 基本信息
TC74是Microchip生产的温度传感器,有五引脚TO22封装和五引脚SOT - 23A封装两种形式。
4.2 引脚使用
其引脚使用情况如下表所示:
|引脚编号|用途|
| ---- | ---- |
|1|NC:无连接|
|2|SDA:数据输入和输出|
|3|GND:接地(TO - 220封装的外标签也接地)|
|4|SCL:时钟输入|
|5|VCC:正电源|
4.3 通信方式
TC74的通信与EEPROM类似,通常为读取操作,但有时也需要写入。
4.4 读取温度操作
读取TC74当前获取的温度的步骤如下:
1. 主设备向从设备发送起始位。
2. 主设备发送TC74的七位地址,默认地址为1001 101,还有其他七个地址需由Microchip设置。
3. 主设备将地址字节的第八位设置为逻辑‘0’,表示正在向TC74写入。
4. 主设备等待从设备的确认位。
5. 主设备发送命令字节0X00。
6. 主设备等待从设备的确认位。
7. 主设备发送另一个起始位。
8. 接着发送地址字节1001 101,但这次第八位设置为逻辑‘1’,表示主设备要从TC74读取数据。
9. 主设备等待确认位。
10. TC74发送表示温度读数的八位数据。
11. 主设备用非确认位和停止位响应。
读取温度操作流程如下:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(发送起始位):::process --> B(发送七位地址):::process
B --> C(设置第八位为0):::process
C --> D(等待确认位):::process
D --> E(发送命令字节0X00):::process
E --> F(等待确认位):::process
F --> G(发送新起始位):::process
G --> H(发送地址字节,第八位为1):::process
H --> I(等待确认位):::process
I --> J(TC74发送温度数据):::process
J --> K(主设备发送非确认位):::process
K --> L(主设备发送停止位):::process
5. I2C协议代码实现
以下是使用I2C协议与24LC256 EEPROM和TC74温度传感器通信的代码:
/*
* File: 12cEEPROMPIC18F4525Prog.c
Author: Mr H. H. Ward
*
Created on 25 July 2018, 13:19
*/
#include <xc.h>
#include <stdio.h>
#include <conFigInternalOscNoWDTNoLVP.h>
#include <4BitLCDDemoBoard.h>
// declare any variables
unsigned char a,b, i, disData, temp, tempr, thigh, tlow, adhigh, adlow;
float sysTemperature;
unsigned char rxData[12];
unsigned char dataout [8] =
{
0x41,
0x6E,
0x6E,
0x2C,
0x57,
0x61,
0x72,
0x64,
};
//declare any subroutines
void ms13delay (unsigned char (t))
{
for (n = 0; n <t; n++)
{
TMR0 = 0;
while (TMR0 < 255);
}
}
void MSSP2CInit()
{
SSPCON1 = 0x28;
SSPCON2 = 0x00;
SSPSTAT = 0x00;
SSPADD = 0x13;
TRISCbits.RC3 = 1;
TRISCbits.RC4 = 1;
}
void MSSP2Cidle()
{
while ((SSPSTAT & 0x04) || (SSPCON2 & 0x1F));
}
void MSSP2Cwait()
{
while(!PIR1bits.SSPIF);
PIR1bits.SSPIF=0;
}
void MSSP2CStart()
{
MSSP2Cidle();
SSPCON2bits.SEN = 1;
}
void MSSP2CStop()
{
MSSP2Cidle();
SSPCON2bits.PEN = 1;
}
void MSSP2CRestart()
{
MSSP2Cidle();
SSPCON2bits.RSEN = 1;
}
void MSSP2CNACK()
{
MSSP2Cidle();
SSPCON2bits.ACKDT = 1;
SSPCON2bits.ACKEN = 1;
}
void MSSP2CWrite(unsigned char data)
{
MSSP2Cidle();
SEND: SSPBUF=data;
MSSP2Cidle();
while(SSPCON2bits.ACKSTAT)
{
SSPCON2bits.RSEN=1;
MSSP2Cwait();
goto SEND;
}
}
unsigned char MSSP2Cread()
{
SSPCON2bits.RCEN=1;
while(!BF);
SSPCON2bits.RCEN=0;
return SSPBUF;
}
unsigned char MSSP2CreadNAck()
{
SSPCON2bits.RCEN=1;
while(!BF);
SSPCON2bits.RCEN=0;
SSPCON2bits.ACKDT=1;
SSPCON2bits.ACKEN=1;
MSSP2Cwait();
return SSPBUF;
}
void writeToEEPROM(unsigned char adhigh, unsigned char adlow, unsigned char edata)
{
MSSP2Cidle();
MSSP2CStart();
MSSP2CWrite(0xA0);
MSSP2CWrite(adhigh);
MSSP2CWrite(adlow);
MSSP2CWrite(edata);
MSSP2CStop();
//ms13delay (2);
}
unsigned char readFromEEPROM(unsigned char adhigh, unsigned char adlow)
{
MSSP2Cidle();
MSSP2CStart();
MSSP2CWrite(0XA0);
MSSP2CWrite(adhigh);
MSSP2CWrite(adlow);
MSSP2CRestart();
MSSP2CWrite(0xA1);
temp = MSSP2CreadNAck ();
MSSP2CStop();
return temp;
}
void displayTemp(float dp)
{
sprintf(str, "%.1f OC ", dp);
writeString(str);
}
void displayTempNeg(float dp)
{
sprintf(str, "-%.1f OC ", dp);
writeString(str);
}
void main()
{
PORTA = 0;
PORTB = 0;
PORTC = 0;
PORTD = 0;
TRISA = 0XFF;
TRISB = 0;
TRISC = 0;
TRISD = 0;
ADCON0 = 0;
ADCON1 = 0X0F;
OSCCON = 0X74;
T0CON = 0XC7;
setUpTheLCD ();
MSSP2CInit();
writeString ("Hello EEPROM");
line2 ();
adhigh = 0x01;
for(adlow=0,b=0;adlow<8;adlow++)
{
a = dataout [b];
writeToEEPROM(adhigh,adlow, a);
ms13delay (3);
b++;
}
for(adlow=0, b=0;adlow<8;adlow++)
{
disData = readFromEEPROM(adhigh,adlow);
rxData [b] = disData;
ms13delay (10);
lcdData = rxData[b];
lcdOut ();
}
ms13delay(30);
while (1)
{
clearTheScreen ();
writeString ("Temp is");
line2 ();
MSSP2Cidle ();
MSSP2CStart ();
MSSP2CWrite (0x9A);
MSSP2CWrite (0x00);
MSSP2CRestart();
MSSP2CWrite(0x9B);
thigh = MSSP2CreadNAck ();
MSSP2CStop();
if (thigh & 0b10000000)
{
thigh = (~thigh + 1);
sysTemperature = thigh;
displayTempNeg(sysTemperature);
}
else
{ sysTemperature = thigh;
displayTemp(sysTemperature);
}
ms13delay(61);
}
}
6. 代码分析
6.1 代码结构
- 注释部分(第1 - 6行) :为标准注释,包含文件信息和创建时间。
-
头文件包含(第7 - 10行)
:包含了常用的头文件,其中
stdio.h用于sprintf函数,用于在LCD上显示温度。 -
变量声明(第12 - 15行)
:设置了一些
unsigned char类型的变量和一个float类型的变量sysTemperature,用于存储温度值。还设置了一个空数组rxData[12]用于存储从EEPROM读取的数据,以及一个包含八个ASCII值的数组dataout [8],用于存储要写入EEPROM的数据。 -
子函数声明与实现
:
-
ms13delay函数(第27 - 34行) :创建一个可变延迟的子函数,最小延迟时间为13ms。 -
MSSP2CInit函数(第35 - 43行) :初始化MSSP(主同步串行端口)用于I2C操作。 -
MSSP2Cidle函数(第44 - 47行) :检查I2C模块是否空闲,避免总线冲突。 -
MSSP2Cwait函数(第48 - 52行) :使PIC等待直到MSSP2模块完成操作。 -
MSSP2CStart函数(第53 - 57行) :生成发送数据到从设备所需的起始位。 -
MSSP2CStop函数(第58 - 62行) :生成停止位。 -
MSSP2CRestart函数(第63 - 67行) :生成重启位。 -
MSSP2CNACK函数(第68 - 73行) :生成主设备在传输结束时发送给从设备的非确认信号。 -
MSSP2CWrite函数(第74 - 85行) :向从设备写入一个字节的数据。 -
MSSP2Cread函数(第87 - 93行) :读取I2C总线上的数据。 -
MSSP2CreadNAck函数(第94 - 103行) :读取从设备的数据并发送非确认信号。 -
writeToEEPROM函数(第104 - 114行) :向EEPROM写入数据。 -
readFromEEPROM函数(第115 - 127行) :从EEPROM读取数据。 -
displayTemp函数(第128 - 132行) :将float类型的温度值显示在LCD上。 -
displayTempNeg函数(第133 - 137行) :显示负温度值。
-
6.2 主函数分析
- 初始化部分(第139 - 153行) :对端口、寄存器进行初始化,设置LCD和MSSP2C模块。
-
写入EEPROM部分(第156 - 163行)
:将数组
dataout中的数据写入EEPROM的指定地址。 - 读取EEPROM部分(第164 - 171行) :从EEPROM读取数据并显示在LCD上。
- 温度监测部分(第173 - 198行) :进入无限循环,不断监测TC74温度传感器的温度,并根据温度正负情况显示在LCD上。
通过以上代码和分析,我们可以实现使用I2C协议与24LC256 EEPROM和TC74温度传感器进行通信,完成数据的读写和温度的监测。
7. 关键子函数详细解析
7.1
MSSP2CInit
函数
void MSSP2CInit()
{
SSPCON1 = 0x28;
SSPCON2 = 0x00;
SSPSTAT = 0x00;
SSPADD = 0x13;
TRISCbits.RC3 = 1;
TRISCbits.RC4 = 1;
}
- 功能 :初始化MSSP(主同步串行端口)用于I2C操作。
-
具体操作
:
-
SSPCON1 = 0x28:将SSPCON1寄存器加载为0b00101000,使SSPEN位为逻辑‘1’,开启MSSP模块;CKP位在I2C模式中不使用;第3位为逻辑‘1’,第2、1、0位为逻辑‘0’,将MSSP设置为I2C模式,时钟频率为振荡器除以(4 × (SSPADD + 1))。 -
SSPCON2 = 0x00:将SSPCON2寄存器的所有位加载为逻辑‘0’,关闭相关功能。 -
SSPSTAT = 0x00:将SSPSTAT寄存器的所有位加载为逻辑‘0’,关闭相关功能。 -
SSPADD = 0x13:对于24LC256 EEPROM,时钟信号最大频率为100kHz或400kHz,这里使用100kHz。振荡器频率设置为8Mhz,要将其分频到100kHz,需除以80,即4 × (SSPADD + 1)=80,所以SSPADD寄存器的值为19(十六进制为0x13)。 -
TRISCbits.RC3 = 1和TRISCbits.RC4 = 1:将相应位设置为输入。
-
7.2
MSSP2Cidle
函数
void MSSP2Cidle()
{
while ((SSPSTAT & 0x04) || (SSPCON2 & 0x1F));
}
- 功能 :检查I2C模块是否空闲,避免总线冲突。
-
具体操作
:
-
(SSPSTAT & 0x04):检查主设备是否正在传输数据。 -
(SSPCON2 & 0x1F):检查确认、接收使能、停止、重复起始或起始是否激活。如果任何一个测试结果为真,则PIC必须等待,直到这些条件都不满足(即I2C模块空闲)。
-
7.3
MSSP2CWrite
函数
void MSSP2CWrite(unsigned char data)
{
MSSP2Cidle();
SEND: SSPBUF=data;
MSSP2Cidle();
while(SSPCON2bits.ACKSTAT)
{
SSPCON2bits.RSEN=1;
MSSP2Cwait();
goto SEND;
}
}
- 功能 :向从设备写入一个字节的数据。
-
具体操作
:
-
MSSP2Cidle():确保I2C模块空闲。 -
SSPBUF=data:将数据加载到SSPBUF寄存器,准备发送给从设备。 -
MSSP2Cidle():再次确保I2C模块空闲。 -
while(SSPCON2bits.ACKSTAT):检查从设备是否发送了确认信号。如果ACKSTAT位为逻辑‘1’,表示未收到确认信号,执行以下操作:-
SSPCON2bits.RSEN = 1:使能PIC重新发送相同的数据。 -
MSSP2Cwait():调用MSSP2Cwait子函数,使PIC等待操作完成。 -
goto SEND:跳转到SEND标签,再次尝试发送数据。
-
-
7.4
MSSP2Cread
函数
unsigned char MSSP2Cread()
{
SSPCON2bits.RCEN=1;
while(!BF);
SSPCON2bits.RCEN=0;
return SSPBUF;
}
- 功能 :读取I2C总线上的数据。
-
具体操作
:
-
SSPCON2bits.RCEN = 1:使能MSSP接收I2C总线上的数据。 -
while(!BF):等待SSPSTAT寄存器的BF(缓冲区满)标志变为逻辑‘1’,表示SSPBUF寄存器已充满从设备发送的数据。 -
SSPCON2bits.RCEN = 0:关闭MSSP的接收功能。 -
return SSPBUF:返回SSPBUF寄存器中的数据。
-
8. 实际应用流程
8.1 写入数据到EEPROM
-
初始化
:调用
MSSP2CInit函数初始化MSSP模块。 -
设置地址
:设置要写入的地址的高字节和低字节,例如
adhigh = 0x01,adlow通过循环从0递增到7。 -
写入数据
:调用
writeToEEPROM函数,将数据写入指定地址。具体步骤如下:-
调用
MSSP2Cidle函数确保I2C模块空闲。 -
调用
MSSP2CStart函数发送起始位。 -
调用
MSSP2CWrite(0xA0)发送控制字节。 -
调用
MSSP2CWrite(adhigh)和MSSP2CWrite(adlow)发送地址的高字节和低字节。 -
调用
MSSP2CWrite(edata)发送要写入的数据。 -
调用
MSSP2CStop函数发送停止位。
-
调用
8.2 从EEPROM读取数据
-
初始化
:调用
MSSP2CInit函数初始化MSSP模块。 - 设置地址 :设置要读取的地址的高字节和低字节。
-
读取数据
:调用
readFromEEPROM函数,从指定地址读取数据。具体步骤如下:-
调用
MSSP2Cidle函数确保I2C模块空闲。 -
调用
MSSP2CStart函数发送起始位。 -
调用
MSSP2CWrite(0XA0)发送控制字节。 -
调用
MSSP2CWrite(adhigh)和MSSP2CWrite(adlow)发送地址的高字节和低字节。 -
调用
MSSP2CRestart函数发送重启位。 -
调用
MSSP2CWrite(0xA1)发送控制字节,指示要读取数据。 -
调用
MSSP2CreadNAck函数读取数据并发送非确认信号。 -
调用
MSSP2CStop函数发送停止位。
-
调用
8.3 读取TC74温度传感器的温度
-
初始化
:调用
MSSP2CInit函数初始化MSSP模块。 -
发送命令
:按照以下步骤发送命令读取温度:
-
调用
MSSP2Cidle函数确保I2C模块空闲。 -
调用
MSSP2CStart函数发送起始位。 -
调用
MSSP2CWrite(0x9A)发送TC74的地址,指示要写入数据。 -
调用
MSSP2CWrite(0x00)发送命令字节。 -
调用
MSSP2CRestart函数发送重启位。 -
调用
MSSP2CWrite(0x9B)发送地址,指示要读取数据。 -
调用
MSSP2CreadNAck函数读取温度数据。 -
调用
MSSP2CStop函数发送停止位。
-
调用
-
处理温度数据
:根据温度数据的正负情况,调用
displayTemp或displayTempNeg函数在LCD上显示温度。
9. 总结
通过上述内容,我们详细介绍了I2C通信协议的原理、24LC256 EEPROM和TC74温度传感器的使用方法,以及如何使用代码实现与它们的通信。在实际应用中,我们可以按照以下步骤进行操作:
1. 了解I2C协议的基本原理和特点,掌握其通信方式和数据传输规则。
2. 熟悉24LC256 EEPROM和TC74温度传感器的引脚使用和通信流程。
3. 根据实际需求编写代码,实现数据的读写和温度的监测。
4. 对代码进行详细分析和调试,确保程序的正确性和稳定性。
通过合理运用I2C协议和相关设备,我们可以实现高效、稳定的数据通信和设备控制。以下是整个操作流程的mermaid流程图:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(初始化系统):::process --> B(初始化MSSP模块):::process
B --> C{选择操作}:::process
C -->|写入EEPROM| D(设置地址和数据):::process
D --> E(写入数据到EEPROM):::process
C -->|读取EEPROM| F(设置地址):::process
F --> G(从EEPROM读取数据):::process
C -->|读取TC74温度| H(发送读取温度命令):::process
H --> I(读取温度数据):::process
I --> J(处理并显示温度):::process
E --> K(结束操作):::process
G --> K
J --> K
在实际开发中,我们可以根据具体需求对代码进行扩展和优化,例如增加错误处理机制、提高数据传输的效率等。同时,要注意I2C总线的电气特性和通信规范,避免出现通信故障。
超级会员免费看

被折叠的 条评论
为什么被折叠?



