第2-14讲:I2C总线的应用
-
- 学习目的
- 了解I2C总线的特点。
- 掌握I2C地址的定义,对I2C地址要有深刻的了解,之后再看到I2C接口设备中描述的7位地址或8位地址,不会再有疑惑。
- 掌握STC8A8K64D4系列单片机I2C的特点以及编程方法。
- 掌握通过I2C读写eeprom AT24C02。这里的重点是使用按页写入来代替逐个字节写入,从而有效节省写入时间。
- I2C总线概述
- 主要特征
- I2C总线概述
典型的I2C应用原理如下图所示,I2C总线通信仅需两根信号线,可以连接多个设备,从设备都有唯一的地址,主设备通过从设备的地址和不同的从设备通信。
图1:典型的I2C总线应用
- I2C总线硬件结构简单,仅需一根时钟线(SCL)、一根数据线(SDA)和两个上拉电阻即可实现通信。I2C总线的SCL和SDA均为开漏结构,开漏结构的电路只能输出"逻辑0",无法输出"逻辑1",因此SCL和SDA需要连接上拉电阻。上拉电阻的阻值影响传输速率,阻值越大,由于RC影响,会带来上升时间的增大,传输的速率慢,阻值小,传输的速率快,但是会增加电流的消耗,一般情况下,我们会选择4.7K左右的阻值,在从机数量少,信号线短的情况下,可以适当增加阻值,如使用10K的阻值。
- I2C总线中的从设备必须有自己的地址,并且该地址在其所处的I2C总线中唯一,主设备通过此唯一的地址即可和该从设备进行数据传输。
- I2C总线支持多主机,但是同一时刻只允许有一个主机。I2C总线中存在多个主机时,为了避免冲突,I2C总线通过总线仲裁决定由哪一个主机控制总线。
- I2C总线只能传输8位的数据,数据速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
- 同时连接到同一个I2C总线上的设备数量受总线最大电容(400pF)的限制。
- I2C总线电流消耗很低,抗干扰强,适合应用于低功耗的场合。
1.- I2C地址
I2C总线中的设备必须要有唯一的地址,这意味着如果在总线中接入两个相同的设备,该设备必须有配置地址的功能,这也是我们经常用的I2C接口的设备会有几个引脚用来配置地址的原因。
对于I2C地址,我们经常看到有的I2C接口设备在规格书中描述的是7位地址,而有的I2C接口设备在规格书中描述的是8位地址,他们有什么区别?(I2C也有10位地址,但用的较少,这里不做介绍,本章中的内容不涉及到10位地址)。
7位地址和8位地址如下图所示,他们结构上是一样的,都是由7个地址位加一个用来表示读写的位组成,只是描述上有所区别。
- 规格书中描述I2C地址是7位地址的设备:给出的是7个地址位加R/W位,最低位(R/W位)为0时表示为写地址,最低位为1时为读地址。如果把0和1分别带入R/W位,得到的地址就和8位地址一样了。
- 规格书中描述I2C地址是8位地址的设备:直接给出写地址和读地址,也就是最低位(R/W位)为0时的地址和最低位为1时的地址。
图2:I2C地址
由此可见,所谓的7位地址和8位地址实际上都是7位地址加上最低位的读写位,本质上是一样的,只是各个I2C接口设备的描述方式不一样。
I2C保留了如下表所示的两组I2C地址,这些地址用于特殊用途。
表1:保留地址
|----------|--------|--------------|
| 从机地址 | R/ W 位 | 描述 |
| 0000 000 | 0 | 广播呼叫地址。 |
| 0000 000 | 1 | 起始字节。 |
| 0000 001 | X | CBUS 地址。 |
| 0000 010 | X | 保留给不同的总线格式 。 |
| 0000 011 | X | 保留到将来使用。 |
| 0000 1XX | X | Hs 模式主机码。 |
| 1111 1XX | X | 保留到将来使用。 |
| 1111 0XX | X | 10 位从机寻址。 |
-
-
- I2C数据传输
-
- 起始和停止条件( START and STOP conditions )
所有的I2C事务都是以START开始、STOP结束,起始和停止条件总是由主机产生,如下图所示,当SCL为高电平时,SDA从高电平向低电平转换表示起始条件,当SCL是高电平时,SDA由低电平向高电平转换表示停止条件。如果总线中存在多个主机,先将SDA拉低的主机获得总线控制权。
图3:起始和停止条件
- 字节格式( Byte format )
I2C总线发送到SDA上的数据必须为8位,即一次传输一个字节,每次传输可以发送的字节数量不受限制。每个字节后必须跟一个响应位,首先传输的是数据的最高位MSB,如果从机要完成一些其他功能后,例如一个内部中断服务程序才能接收或发送下一个完整的数据字节,那么从机可以将时钟线SCL保持为低电平强制主机进入等待状态,当从机准备好接收下一个字节数据并释放时钟线SCL后数据传输继续。
图4:I2C总线数据传输
-
-
- ACK和NACK
-
每个字节后会跟随一个ACK信号。接收者通过ACK位告知发送者已经成功接收一字节数据并准备好接收下一字节数据。所有的时钟脉冲包括ACK信号的时钟脉冲都是由主机产生的。
- ACK信号:发送者发送完8位数据后,在ACK时钟脉冲期间释放SDA线,接收者可以将SDA拉低并在时钟信号为高时保持低电平,这样就产生了ACK信号,从而使得主机知道从机已成功接收数据并且准备好了接收下一数据。
- NACK信号:当SDA在第9个时钟脉冲的时候保持高电平,定义为NACK信号。这时,主机要么产生STOP条件来放弃这次传输,要么重复START条件来启动一个新的传输。
下面的5种情况会导致产生NACK信号:
- 发送方寻址的接收方在总线上不存在,因此总线上没有设备应答。
- 接收方正在处理一些实时的功能,尚未准备好与主机通信,因此接收方不能执行收发。
- 在传输期间,接收方收到不能识别的数据或者命令。
- 在传输期间,接收方无法接收更多的数据字节。
- 主-接收器要通知从-发送器传输的结束。
1.- 从机地址和R/W位
I2C数据传输如下图所示,在起始条件(S)后,发送从机地址,从机地址是7位,从机地址后紧跟着的第8位是读写位(R/W),读写位为0表示写,读写位为1表示读。数据传输一般由主机产生的停止位P 终止,但是,如果主机仍希望在总线上通信,他可以产生重复起始条件 S和寻址另一个从机而不是首先产生一个停止条件,在这种传输中可能有不同的读写格式结合。
图5:I2C总线传输时序
可能的数据传输格式有:
- 主机发送器发送到从机接收器,传输的方向不会改变,接收器应答每一个字节,如下图所示。
图6:主机发送器7位地址寻址从机接收器(传输方向不改变)
- 在第一个字节后,主机立即读从机,在第一次应答后,主机发送器变成主机接收器,从机接收器变成从机发送器。第一次应答仍由从机生成,主机生成后续应答。之前发送了一个非应答(A)的主机产生STOP条件。
图7:主机发送第一个字节后立即读取从机
- 复合格式,如下图所示。传输改变方向的时侯,起始条件和从机地址都会被重复,但R/W位取反。如果主接收器发送重复START条件,他会在重复START条件之前发送一个非应答(A)。
图8:复合格式
-
- 硬件设计
IK-64D4开发板上设计了I2C接口的EEPROM存储器(AT24C02)和PCF8563时钟日历电路单元,用于我们学习I2C的应用。
-
-
- AT24C02(EEPROM)电路
-
AT24C02 是一款串行CMOS E2PROM,常用来存储一些配置数据,他的存储空间为2K位(256个字节),页面大小为8个字节,共32个页面。
开发板上的AT24C02硬件电路如下图所示。AT24C02的器件地址的低3位可以通过引脚A2 A1 A0配置,本电路中引脚A2 A1 A0均连接到GND,因此,地址的低3位均为0。
AT24C02通过I2C接口和STC8A8K64D4连接,I2C接口为开漏输出,因此SCL和SDA线上需要增加上拉电阻,这里我们使用的上拉电阻阻值为6.8K。
单片机的 P7.7、P7.6通过跳线连接到AT24C02的SCL和SDA引脚,他们和外扩存储器接口J11共用IO,因此,当我们使用AT24C02时,不能使用外扩存储器接口J11。
图9:EEPROM电路
EEPROM存储器(AT24C02)占用的STC8A8K64D4的引脚如下表:
表2:I2C连接AT24C02引脚分配
|--------|--------|---------------|
| 名称 | 引脚 | 说明 |
| SCL | P7.7 | 和外扩存储器接口J11共用 |
| SDA | P7.6 | 和外扩存储器接口J11共用 |
- 注:为了方便读者理解单片机访问AT24C02存储器的编程,AT24C02的I2C通信时序以及读、写、擦除操作将在软件设计部分讲解。
1.- PCF8563时钟/日历电路
PCF8563是一款为低功耗而优化的CMOS实时时钟/日历芯片。他提供了可编程时钟输出、中断输出和低电压检测器,所有地址和数据通过双线双向I2C总线串行传输,最大总线速度为400 kbit/s。
开发板上的PCF8563硬件电路如下图所示。单片机的 P2.5、P2.4通过跳线连接到PCF8563的SCL和SDA引脚,他们和W5500以太网模块接口J7共用IO,因此,当我们使用PCF8563时,不能使用W5500以太网模块接口J7。
图10:PCF8563时钟日历电路
PCF8563占用的STC8A8K64D4的引脚如下表:
表21:I2C连接PCF8563引脚分配
|--------|--------|---------------|
| 名称 | 引脚 | 说明 |
| SCL | P2.5 | 和W5500以太网模块共用 |
| SDA | P2.4 | 和W5500以太网模块共用 |
- 注:为了方便读者理解单片机访问PCF8563存储器的编程,PCF8563的I2C通信时序以及读、写操作将在软件设计部分讲解。
-
- STC8A8K64D4的IIC应用步骤
STC8A8K64D4单片机内部集成了一个 I2C串行总线控制器,该I2C总线具有主机和从机两种操作模式并有多组引脚可供选择,STC8A8K64D4单片机的I2C应用步骤如下图所示。
图11:I2C总线应用步骤
-
-
- 配置I2C功能引脚
-
I2C有多组引脚与之对应(具体几组还取决于芯片封装引脚数),同一时刻,只能通过相关寄存器配置其中的一组使用, STC8A8K64D4单片机I2C的引脚分配如下表。
表3:STC8A8K64D4单片机I2C引脚分配
|---------|-------|-------|
| I2C信号 | 信号编号 | 对应的IO |
| SDA(数据) | SDA | P1.4 |
| SDA(数据) | SDA_2 | P2.4 |
| SDA(数据) | SDA_3 | P7.6 |
| SDA(数据) | SDA_4 | P3.3 |
| SCL(时钟) | SCL | P1.5 |
| SCL(时钟) | SCL_2 | P2.5 |
| SCL(时钟) | SCL_3 | P7.7 |
| SCL(时钟) | SCL_4 | P3.2 |
I2C是通过"外设端口切换控制寄存器2(P_SW2)"中的SPI_S[1:0]配置引脚的,如下图所示。
外设端口切换控制寄存器 2 (P_SW 2 ):
P_SW2寄存器中的I2C_S[1:0]为 I2C功能脚选择位,如下表所示。
表4:I2C功能脚选择位
|--------------|------|------|
| SPI_S[1:0] | SCL | SDA |
| 00 | P1.5 | P1.4 |
| 01 | P2.5 | P2.4 |
| 10 | P7.7 | P7.6 |
| 11 | P3.2 | P3.3 |
-
-
- 配置工作模式和总线速度
-
STC8A8K64D4单片机内的 I2C总线支持主机和从机两种操作模式,因此使用的时候需要根据实际的应用配置I2C工作于主机模式或从机模式(如通过I2C访问AT24C02 EEPROM存储器时,I2C工作于主机模式,因此,应配置I2C为主机)。
I2C常用的总线速度有100Kbit/s和400Kbit/s,在配置I2C速度的时候,要根据实际的需求来配置,并且要确认配置的速度是主机和从机都能支持的。
I2C的工作模式和总线速度都是通过"I2C 配置寄存器( I2CCFG)"配置的,如下图所示。
I2C 配置寄存器( I2CCFG):
- MSSL: I2C 工作模式选择位
- 0: 从机模式
- 1: 主机模式
- MSSPEED[5:0]: I2C 总线速度(等待时钟数)控制。
I2C 总线速度计算公式如下:
其中FOSC是系统时钟,本书配套例子的系统时钟配置的均为24MHz。当我们需要使用的I2C 总线速度为400Kbit/s时,由上面的公式可以计算出MSSPEED应配置为13。
-
-
- 配置中断
-
I2C通信可以使用查询方式或中断方式,如果使用中断方式,则需要开启I2C中断。I2C主机是通过I2C 主机控制寄存器(I2CMSCR)中的"EMSI"位开启中断的,I2C从机是通过I2C 从机控制寄存器( I2CSLCR)中的"ESTAI、ERXI、ETXI"和"ESTOI"位开启中断的,如下图所示。
图12:I2C中断配置
中断产生后,硬件自动置位相应的中断标志,向CPU发出中断请求,CPU处理完中断后需要软件清零中断标志。
- 注意:开启I2C中断的情况下,还需要开启总中断"EA=1",I2C中断才能起作用。
-
-
- 数据传输
-
I2C通信时(以主机为例),通过I2C主机控制寄存器(I2CMSCR)中的"MSCMD[3:0]"发送不同的命令,由多个命令组合实现I2C通信时序。如通过I2C向EEPROM AT24C02写入数据的时序:起始命令→发送数据命令(发送I2C器件地址)→接收ACK→发送数据命令(发送数据写入地址)→接收ACK→发送数据命令(发送写入的数据)→接收ACK→发送停止命令。
I2C主机控制寄存器(I2CMSCR):
MSCMD[3:0] :主机命令
- 0000: 待机,无动作。
- 0001: 起始命令。
- 0010:发送数据命令。
- 0011:接收 ACK 命令。
- 0100: 接收数据命令。
- 0101: 发送 ACK 命令。
- 0110: 停止命令。
- 0111: 保留。
- 1000: 保留。
- 1001:起始命令+发送数据命令+接收 ACK 命令。
- 1010:发送数据命令+接收 ACK 命令。
- 1011:接收数据命令+发送 ACK(0)命令。
- 1100:接收数据命令+发送 NAK(1)命令。
-
注:主机命令的详细解释可阅读《STC8A8K64D4 系列单片机技术参考手册》的21.3.2:I2C 主机控制寄存器( I2CMSCR)。
-
- 软件设计
- I2C读写EEPRON(AT24C02)存储器实验
- 软件设计
-
注:本节的实验是在"实验2-6-1:串口1数据收发实验"的基础上修改,本节对应的实验源码是:"实验2-14-1:硬件I2C读写EEPROM(AT24C02)存储器"。
1.
1.
1. 实验内容
将STC8A8K64D4单片机的I2C配置为主机,速度为400Kbps,通过I2C总线访问AT24C02存储器,完成以下操作。
- 单个字节写入:向指定地址写入1个字节数据。
- 单个字节读出:从指定地址读取1个字节数据。
- 批量写:向指定地址连续写入指定长度数据。为了节省写入时间,写入数据时使用按页写入,因此,批量写入要能处理跨页写入的问题。
- 批量读:从指定地址开始顺序读取指定长度数据,并将读取的数据通过串口输出。
1.
1.
1. 代码编写
- 新建一个名称为"i2c_hw.c"的文件及其头文件"i2c_hw.h"保存到工程的"Source"文件夹,并将"i2c_hw.c"加入到Keil工程中的"SOURCE"组。该文件用于存放I2C硬件操作相关的函数。
- 新建一个名称为"at24c02.c"的文件及其头文件"at24c02.h"保存到工程的"Source"文件夹,并将"at24c02.c"加入到Keil工程中的"SOURCE"组。该文件用于存放EEPROM存储器AT24C02操作相关的函数。
- 引用头文件
因为在"main.c"文件中使用了"at24c02.c"文件中的函数,所以需要引用下面的头文件"at24c02.h"。
**代码清单:**引用头文件
-
//引用AT24C02的头文件
-
#include " at24c02.h"
-
初始化I2C
I2C初始化主要包括配置I2C功能引脚,I2C工作模式和总线速度。本例中使用的是查询模式,因此无需配置中断,代码清单如下。
**代码清单:**I2C初始化
-
/**************************************************************************************
-
* 描 述 : I2C初始化
-
* 参 数 : 无
-
* 返回值 : 无
-
**************************************************************************************/
-
void I2C_init(void )
-
{
-
P7M1 &= 0x3F; P7M0 &= 0x3F; //设置P7.6~P7.7为准双向口
-
P_SW2 |= 0x80; //将EAXFR位置1,允许访问扩展RAM区特殊功能寄存器(XFR)
-
P_SW2 &= 0xCF; //将I2C_S[1:0]置10,以选择I2C硬件功能脚为P7.6 P7.7
-
P_SW2 |= 0x20;
-
I2CCFG=0xED; //配置I2C主机模式、允许I2C功能、I2C总线速度400Kbps
-
I2CMSST=0x00; //清零I2C主机状态寄存器各标志位
-
}
-
AT24C02地址的确定
查询AT24C02数据手册可知其地址高4位固定为"1010",如下图所示,紧跟着的3个位由芯片的引脚A2、A1和A0的电平确定,最低位为读写位。开发板上AT24C02的硬件电路中将引脚A2、A1和A0连接到了GND,因此A2、A1和A0均为0,由此可提取出7位地址为0x50,转换为8位地址:0xA0(写)、0xA1(读),如下图所示。
图13:AT24C02地址的确定
代码中,AT24C02地址定义如下:
**代码清单:**定义AT24C02地址
-
#define AT24C02_ADDR_W 0xA0 //I2C从机写地址
-
#define AT24C02_ADDR_R 0xA1 //I2C从机读地址
-
单字节写入:向指定地址写入单个字节数据
AT24C02将单个字节写入到指定地址的时序如下图所示,首先产生起始条件,紧跟着发送7位地址 + 0(写),之后发送写入数据的地址和数据,最后产生停止条件。
图14:AT24C02写入单个字节数据时序
根据时序,即可写出"写入单个字节数据"的函数,代码清单如下。这里面需要特别注意的是:I2C发送完成后,仅表示AT24C02接收到了数据,并不表示AT24C02已经完成了数据的写入。AT24C02是在停止条件产生后进入写入周期的,AT24C02的写入周期所需的最大时间是5ms,因此I2C传输完成后要延时5ms以确保AT24C02正确写入数据。
**代码清单:**向指定地址写入单个字节数据
-
/**************************************************************************************
-
* 描 述 : 向AT24C02指定地址写入一个字节数据
-
* 参 数 : Addr[in]:写入数据的地址
-
* : dat[in]:待写入的数据
-
* 返回值 : 无
-
**************************************************************************************/
-
void AT24C02_write_byte(u8 Addr,u8 Dat)
-
{
-
I2C_Start(); //发送起始命令,产生起始条件
-
I2C_SendData(AT24C02_ADDR_W); //发送器件地址(写)
-
I2C_RecvACK(); //接收ACK
-
I2C_SendData(Addr); //发送数据写入地址
-
I2C_RecvACK(); //接收ACK
-
I2C_SendData(Dat); //发送写入的数据
-
I2C_RecvACK(); //接收ACK
-
I2C_Stop(); //发送停止命令,产生停止条件
-
delay_ms(5); // AT24C02的写入周期所需的最大时间是5ms,延时5ms确保数据正确写入
-
}
-
单个字节读出:从指定地址读取1个字节数据
AT24C02读取单个字节数据的时序如下图所示,先执行发送操作,将需要读取的数据的地址发送给AT24C02,之后再执行读取操作,即可读出数据。
图15:AT24C02读取单个字节数据时序
读取单个字节数据的代码清单如下。
**代码清单:**从指定地址读出单个字节数据
-
/**************************************************************************************
-
* 描 述 : 从AT24C02指定地址读取一个字节数据
-
* 参 数 : Addr[in]:读出数据的地址
-
* 返回值 : 读取的数据
-
**************************************************************************************/
-
u8 AT24C02_read_byte(u8 Addr)
-
{
-
u8 temp=0;
-
I2C_Start(); //发送起始命令,产生起始条件
-
I2C_SendData(AT24C02_ADDR_W); //发送器件地址(写)
-
I2C_RecvACK(); //接收ACK
-
I2C_SendData(Addr); //发送读取数据的地址
-
I2C_RecvACK(); //接收ACK
-
I2C_Start(); //发送起始命令,产生起始条件
-
I2C_SendData(AT24C02_ADDR_R); //发送器件地址(读)
-
I2C_RecvACK(); //接收ACK
-
temp=I2C_RecvData(); //读一个字节数据
-
I2C_Stop(); //发送停止命令,产生停止条件
-
return temp; //返回读取的数据
-
}
-
按页写入:向指定页面连续写入不大于页面长度的数据
AT24C02按页写入的时序如下图所示,首先产生起始条件,紧跟着发送7位地址 + 0(写),接着发送写入数据的地址,之后连续发送写入的数据,最后产生停止条件。
图16:AT24C02按页写入时序
按页写入要注意AT24C02按页写时的跨页问题,如果地址跨页,则写指针会返回到当前页的起始地址,这一点非常重要,接下来我们来分析按页写的几种情况。
AT24C02的容量为256个字节,页面大小为8个字节,因此AT24C02被分为32个页面,第一个页面地址位0~7,第二个页面地址位8~15,以此类推。如下图所示,按页写时如果地址没有超过当前页面,写入正确。
图17:地址在同一个页面
按页写时如果地址跨页,会出现如下图所示的情形:我们期望从地址0x04开始连续写入"A B C D E F"6个数据,但是实际写时,因为写地址增加到0x07后自动复位到0x00,所以实际写入的地址0x04~0x07写入"A B C D"4个数据,地址0x00和0x01写入"E"和" F"2个数据。
图18:跨页后写指针复位
综上所述,按页写可实现连续写,不需要每个字节都发起一次写流程,这会有效节省操作时间,但每次只能写一个页面,也就是每次最多只能连续写入8个字节。
本例中,我们编写的按页写的函数代码清单如下。这里需要注意一下,按页写的函数本身没有实现跨页的处理,该函数是提供给批量写入函数调用的,批量写入函数中处理了跨页。
**代码清单:**按页写入
-
/*************************************************************************
-
* 功 能 : 按页写入数据。注意:该函数仅提供给批量写入函数(AT24C02_write_buf)
-
* : 和内存填充函数(AT24C02_fill)调用,目的是为了加快写入速度。
-
* : 其他文件调用该函数时,要确保写入的数据处于同一个页面
-
* 参 数 : addr[in]:写入数据的起始地址
-
* : p_data[in]:指向待写入的数据缓存
-
* : len[in]:写入的数据长度,不能超过1个页面的大小8个字节
-
* 返回值 : NRF_SUCCESS:写页面成功
-
*************************************************************************/
-
static u8 AT24C02_write_page(u8 addr,u8 * p_data,u8 len)
-
{
-
//检查写入的数据长度是否合法
-
if (len > AT24C02_PAGESIZE-(addr%AT24C02_PAGESIZE))
-
{
-
return ERROR_INVALID_LENGTH;
-
}
-
I2C_Start(); //发送起始命令,产生起始条件
-
I2C_SendData(AT24C02_ADDR_W); //发送器件地址(写)
-
I2C_RecvACK(); //接收ACK
-
I2C_SendData(addr); //发送写入数据的地址
-
I2C_RecvACK(); //接收ACK
-
while (len--)
-
{
-
I2C_SendData(*(p_data++)); //发送写入的数据
-
I2C_RecvACK(); //接收ACK
-
}
-
I2C_Stop(); //发送停止命令,产生停止条件
-
delay_ms(5); //AT24C02的写入周期所需的最大时间是5ms,延时5ms确保数据正确写入
-
return AT24C02_SUCESS;
-
}
-
批量写入:向指定地址顺序写入指定长度的数据
当写入的数据较多时,可以通过循环执行单个字节写入操作将数据写入到eeprom,这种方法编写程序简单,但缺点是每个字节都需要执行一次写操作流程(起始条件à 7位地址 + 0(写)à写入数据的地址à数据à停止条件à延时等待AT2402写完成),这无疑会花费更多的时间。
更好的方法是使用按页写入的方式将数据写入到eeprom,程序中将处于同一页面的数据通过按页写入的方式一次写入。这种方式的优点是速度快,但编程相对复杂一些,因为要处理地址跨页的问题。
批量写入的代码清单如下,代码中按照写入数据的地址逐个取出待写入的数据,并根据地址判断一个页面的数据是否取完,如果取完,则执行一次按页写入,如此反复,直到数据全部写入。
**代码清单:**批量写入
- /*************************************************************************
- * 功 能 : 向AT24C02指定的起始地址批量顺序写入数据,函数内部实现了跨页写,
- * : 函数会检查AT24C02的空间是否能够容纳写入的数据。
- * 参 数 : WriteAddr[in]:写入数据的起始地址
- * : p_buf[in]:指向待写入的数据缓存
- * : size[in]:写入的数据长度
- * 返回值 : NRF_SUCCESS:写数据成功
- *************************************************************************/
- u8 AT24C02_write_buf(u8 *p_buf,u8 addr,u16 len)
- {
- u8 addr_ptr = addr;
- u8 write_addr;
- u8 sendlen = 0;
- u8 tx_buf[AT24C02_PAGESIZE];
- //AT24C02剩余空间不够存放需要写入的数据,返回:长度无效的错误代码
- if ((AT24C02_SIZE-addr) < len)
- {
- return ERROR_INVALID_LENGTH;
- }
- write_addr = addr_ptr;
- while (len--) //连续写入数据
- {
- if ((addr_ptr%AT24C02_PAGESIZE) == 0) //到达页面的起始地址
- {
- if (sendlen != 0) //到达页面起始地址并且发送长度不等于0,表示即将跨页
- {
- AT24C02_write_page(write_addr,tx_buf, sendlen); //执行一次按页写入操作
- sendlen = 0; //清零发送长度
- write_addr = addr_ptr;
- }
- tx_buf[sendlen++] = *(p_buf++); //数据保存到发送缓存tx_buf
- }
- else
- {
- tx_buf[sendlen++] = *(p_buf++); //数据保存到发送缓存tx_buf
- if (len==0) //写入到最后的页面的数据读取完成
- {
- AT24C02_write_page(write_addr,tx_buf, sendlen); //执行一次按页写入操作
- sendlen = 0; //清零发送长度
- }
- }
- addr_ptr++; //地址加1
- }
- return AT24C02_SUCESS;
- }
- 批量读取:从指定地址顺序读取指定长度数据
AT24C02从指定地址顺序读数据的时序如下图所示,顺序读相对于写要简单,因为不用考虑跨页的问题,顺序读时每读出一个字节数据地址指针会自动加1。
图19:AT24C02连续读时序
批量顺序读取的函数的代码清单如下。
**代码清单:**批量顺序读取
- /*************************************************************************
- * 功 能 : 从指定的起始地址顺序读出指定长度的数据
- * 参 数 : WriteAddr[in]:读出数据的起始地址
- * : p_buf[in]:指向保存读出数据的缓存
- * : size[in]:读出的数据长度
- * 返回值 : NRF_SUCCESS:读数据成功
- *************************************************************************/
- u8 AT24C02_read_buf(u8 *p_buf,u8 ReadAddr,u16 len)
- {
- //读数据的长度已经超出了AT24C02的地址范围,返回:长度无效的错误代码
- if ((AT24C02_SIZE-ReadAddr) < len)
- {
- return ERROR_INVALID_LENGTH;
- }
- I2C_Start(); //发送起始命令,产生起始条件
- I2C_SendData(AT24C02_ADDR_W); //发送器件地址(写)
- I2C_RecvACK(); //接收ACK
- I2C_SendData(ReadAddr); //发送读取数据的地址
- I2C_RecvACK(); //接收ACK
- I2C_Start(); //发送起始命令,产生起始条件
- I2C_SendData(AT24C02_ADDR_R); //发送器件地址(读)
- I2C_RecvACK(); //接收ACK
- //顺序读取数据
- while (len)
- {
- len--;
- *p_buf++ = I2C_RecvData();
- if (len == 0)I2C_Stop(); //如果是最后一个字节,发送停止命令,产生停止条件,否则发送ACK
- else I2C_SendACK();
- }
- return AT24C02_SUCESS;
- }
AT24C02是没有擦除指令的,写入数据前也不需要擦除的,这一点是和Flash不一样的。但在应用中,有时我们希望AT24C02的数据恢复为某个默认值,以便于区分有没有写入数据,鉴于此点,我们编写了一个全片填充函数,用于将全片填充为指定的数据,如全片填充"0xFF",模拟Flash的擦除操作。
**代码清单:**全片填充指定的数据
- /*************************************************************************
- * 功 能 : AT24C02全片填充指定的数据
- * 参 数 : dat[in]:填充的数据
- * 返回值 : 无
- *************************************************************************/
- void AT24C02_fill(u8 dat)
- {
- u8 i;
- u8 tx_buf[AT24C02_PAGESIZE];
- //拷贝填充的数据
- for (i = 0; i < AT24C02_PAGESIZE; i++)tx_buf[i] = dat;
- //全片填充数据
- for (i = 0; i < AT24C02_PAGENUM; i++)
- {
- AT24C02_write_page(i*8,tx_buf,AT24C02_PAGESIZE);
- }
- }
编写好读写函数后,我们就可以通过读写函数对eeprom进行读和写,下面的程序分别使用了批量写入、批量读取和全片填充访问AT24C02,具体功能如下。
- 按下KEY1按键:从AT24C02地址0x00开始连续写入256个字节数据。
- 按下KEY2按键:从AT24C02地址0x00开始顺序读取256个字节数据,读出的数据通过串口输出。
- 按下KEY3按键:全片填充数据"0xFF"。
代码清单:主函数
- /**************************************************************************
- 功能描述:主函数
- 入口参数:无
- 返 回 值:int类型
- **************************************************************************/
- int main(void )
- {
- u8 btn_val;
- u16 i,test_len;
- u8 j;
- P2M1 &= 0x3F; P2M0 &= 0x3F; //设置P2.6~P2.7为准双向口(指示灯D1和D2)
- P7M1 &= 0xF9; P7M0 &= 0xF9; //设置P7.1~P7.2为准双向口(指示灯D4和D3)
- P3M1 &= 0x3F; P3M0 &= 0x3F; //设置P3.6~P3.7为准双向口(按键KEY2和KEY1)
- P0M1 &= 0x5F; P0M0 &= 0x5F; //设置P0.5,P0.7为准双向口(按键KEY4和KEY3)
- P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口(串口1的RxD)
- P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出(串口1的TxD)
- uart1_init(); //串口1初始化
- I2C_init(); //IIC初始化
- EA = 1; //使能总中断
- delay_ms(10); //初始化后延时
- test_len = 256;
- while (1)
- {
- btn_val=buttons_scan(0); //获取开发板用户按键检测值,不支持连按
- //按下KEY1:从地址0x0000开始连续写入256个字节数据
- if (btn_val == BUTTON1_PRESSED)
- {
- j = 0;
- for (i=0;i<test_len;i++)my_tx_buf[i] = j++;
- printf("Write data to AT24C02!\r\n");
- //写入256个字节数据
- AT24C02_write_buf(my_tx_buf,0,test_len);
- //指示灯D1状态翻转,指示操作完成
- led_toggle(LED_1);
- }
- //按下KEY2:从地址0x0000开始连续读出256个字节数据
- else if (btn_val == BUTTON2_PRESSED)
- {
- printf("Read data from fram!\r\n");
- //读取写入的数据
- AT24C02_read_buf(my_rx_buf,0,test_len);
- //串口打印读取的数据
- for (i=00;i<test_len;i++)printf("%02bX ",my_rx_buf[i]);
- printf("\r\n");
- led_toggle(LED_2); //指示灯D2状态翻转,指示操作完成
- }
- //按下KEY3:全片填充0xFF
- else if (btn_val == BUTTON3_PRESSED)
- {
- led_on(LED_3); //点亮指示灯D3
- printf("Clear fram!\r\n");
- AT24C02_fill(0xFF); //AT24C02全片写入0xFF
- led_off(LED_3); //熄灭指示灯D3
- }
- }
- }
1.
1.
1. 硬件连接
本实验需要使用板载的EEPROM AT24C02,按照下图所示短接跳线帽。
图20:跳线帽短接
-
-
-
- 实验步骤
-
-
- 解压"...\第3部分:配套例程源码"目录下的压缩文件"实验2-14-1:硬件I2C读写EEPROM(AT24C02)存储器",将解压后得到的文件夹拷贝到合适的目录,如"D\STC8"(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
- 双击"...\i2c_at24c02\project"目录下的工程文件"i2c_at24c02.uvproj"。
- 点击编译按钮编译工程,编译成功后生成的HEX文件"i2c_at24c02.hex"位于工程的"...\i2c_at24c02\Project\Object"目录下。
- 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz。
- 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps。
- 程序运行后,先按下KEY1按键写入256个字节数据(写入的数据为0x00 0x01...0xFF),之后按下KEY2按键读取数据并通过串口输出,可以观察到读取的数据和写入的数据一致。
- 按下KEY3按键将全片填充数据"0xFF",之后再按下KEY2按键读取数据,可以观察到读取的数据全部为"0xFF"。
- ****说明:****软件I2C只需要修改工程中的I2C相关的底层文件"i2c_hw.c"和"i2c_hw.h"即可,读者如对软件I2C感兴趣,可以尝试将本实验改为软件I2C。
我们也编写好了本实验对应的软件I2C例子,该例子在资料的"...\第3部分:配套例程源码"目录下,实验名称如下,读者在编写的过程中可以参考一下。
- 实验2-14-2:软件I2C读写EEPROM(AT24C02)存储器。
-
-
- PCF8563实时时钟实验
-
- 注:本节的实验是在"实验2-14-1:硬件I2C读写EEPROM(AT24C02)存储器"的基础上修改,本节对应的实验源码是:"实验2-14-3: PCF8563实时时钟(I2C)"。
1.
1.
1. 实验内容
本实验重点在于掌握PCF8563实时时钟的时间读取和时间配置。本实验实现的功能也是基于这两点。
- 时间读取:每秒从PCF8563读取一次时间,为了方便观察,用数码管显示读取的时、分、秒。
- 时间配置:本例中,我们没法获取准确的实时时间,因此,配置的时间是一个固定的时间(2023.3.9 9时30分0秒,第11周)。按下按键KEY1后,将PCF8563的时间配置设置为2023.3.9 9时30分0秒,第11周。
- 说明:实验中会使用定时器和数码管,关于他们的应用读者可参见《第2-10讲:定时器和计数器》和《第2-12讲:数码管显示》,这里不再赘述。
-
-
-
- 代码编写
-
-
- 新建一个名称为"pcf8563.c"的文件及其头文件"pcf8563.h"保存到工程的"Source"文件夹,并将"pcf8563.c"加入到Keil工程中的"SOURCE"组。该文件用于存放I2C硬件操作相关的函数。
- 引用头文件
因为在"main.c"文件中使用了"pcf8563.c"文件中的函数,所以需要引用下面的头文件"pcf8563.h"。
**代码清单:**引用头文件
-
//引用pcf856实时时钟的头文件
-
#include "pcf8563.h"
-
初始化pcf8563实时时钟
使用pcf8563实时时钟前,先要初始化配置I2C通信接口,本例中将STC8A8K64D4单片机的I2C配置为主机,速度为400Kbps,I2C功能引脚使用P2.4(SDA)和P2.5(SCL)。初始化时读取一次时间,并将读取的时间更新到数码管显示数组,代码清单如下。
**代码清单:**I2C初始化
- /**********************************************************************************
- 功能描述:PCF8563时钟初始化
- 参 数:无
- 返 回 值:无
- ***********************************************************************************/
- void PCF8563_Init(void )
- {
- I2C_init(); //IIC初始化
- PCF8563_Read_Time(); //读取时间
- Time_DispUpdata(); //更新数码管显示数组
- }
- AT24C02地址的确定
查询PCF8563实时时钟数据手册可知其地址固定为"1010001",如下图所示,最低位为读写位。由此可提取出7位地址为0x51,转换为8位地址:0xA2(写)、0xA3(读),如下图所示。
图21:PCF8563地址的确定
代码中,PCF8563地址定义如下:
**代码清单:**定义PCF8563地址
- #define PCF8563_ADDR_W 0xA2 //I2C从机写地址
- #define PCF8563_ADDR_R 0xA3 //I2C从机读地址
- 读取时间
PCF8563包含16个8位寄存器,其中时间寄存器的地址是从02H~08H,如下表所示。秒、分、小时、天、周、月、年都采用BCD格式进行编码(高4位表示十位,低4位表示个位)。当一个 RTC 寄存器被写入或读取时,所有时间计数器的内容将被锁存。因此在传送条件下,可以防止对时钟和日历芯片的错读或错写。
表5:时间寄存器
PCF8563的I2C读时序如下图所示,在读访问期间发生的任何被挂起的增加时间寄存器的请求会得到处理(最多可以存储1个请求),因此,必须在1秒之内完成所有访问。
另外,必须一次完成读或写访问,也就是说,在一次访问期间,完成秒到年的设置或读取,否则可能导致时间损坏。
例如,如果在一次访问期间读取时间(秒到年),然后在第二次访问期间读取日期,那么在两次访问期间,时间可能增加,从而导致读取的时间不对。
图22:读时序
推荐的读取时间的方法如下:
- 发送一个起始条件和用于写入的从机地址(A2h)。
- 通过发送 02h,将地址指针设置为 2(VL_seconds)。
- 发送一个 一个起始条件。
- 发送用于读取的从机地址(A3h)。
- 读取 VL_seconds寄存器,获取秒。
- 读取 Minutes寄存器,获取分。
- 读取 Hours寄存器,获取时。
- 读取 Days寄存器,获取日。
- 读取 Weekdays寄存器,获取周。
- 读取 Century_months寄存器,获取月。
- 读取 Years寄存器,获取年。
- 发送一个 停止条件。
编程时,为了方便,我们定义一个名称为"time_pcf_t"的结构体,用来保存时间,代码清单如下。
**代码清单:**定义保存时间的结构体
- //定义一个结构体,用于保存读取的时间
- typedef struct
- {
- u8 year; //年
- u8 mon; //月
- u8 week; //周
- u8 day; //日
- u8 hour; //时
- u8 min; //分
- u8 sec; //秒
- } time_pcf_t;
读取时间的代码清单如下,读取的时间值保存到time_pcf结构体中,提供给其他程序模块使用。
**代码清单:**读取时间
-
/**************************************************************************************
-
* 描 述 : 读取时间,读取的时间值保存到time_pcf结构体
-
* 参 数 : 无
-
* 返回值 : 无
-
**************************************************************************************/
-
void PCF8563_Read_Time(void )
-
{
-
P_SW2 |= 0x80; //将EAXFR位置1,允许访问扩展RAM区特殊功能寄存器(XFR)
-
I2C_Start(); //发送起始命令,产生起始条件
-
I2C_SendData(PCF8563_ADDR_W); //发送I2C器件地址
-
I2C_RecvACK(); //接收ACK
-
I2C_SendData(PCF8563_SECOND_ADDRESS);//发送寄存器地址
-
I2C_RecvACK(); //接收ACK
-
I2C_Start(); //发送起始命令,产生起始条件
-
I2C_SendData(PCF8563_ADDR_R); //发送I2C器件地址
-
I2C_RecvACK(); //接收ACK
-
time_pcf.sec = I2C_RecvData()&0x7F; //读取秒
-
I2C_SendACK(); //发送ACK
-
time_pcf.min = I2C_RecvData()&0x7F; //读取分
-
I2C_SendACK(); //发送ACK
-
time_pcf.hour = I2C_RecvData()&0x3F; //读取时
-
I2C_SendACK(); //发送ACK
-
time_pcf.day = I2C_RecvData()&0x3F; //读取日
-
I2C_SendACK(); //发送ACK
-
time_pcf.week = I2C_RecvData()&0x07; //读取周
-
I2C_SendACK(); //发送ACK
-
time_pcf.mon = I2C_RecvData()&0x1F; //读取月
-
I2C_SendACK(); //发送ACK
-
time_pcf.year = I2C_RecvData(); //读取年
-
I2C_SendACK(); //发送ACK
-
I2C_Stop(); //发送停止命令,产生停止条件
-
P_SW2 &= 0x7F; //将EAXFR位置0,禁止访问XFR
-
}
-
设置时间
设置时间的I2C时序如下图所示,和读取时间一样,设置时间也需要在1秒之内完成所有访问,并且,在一次访问期间,完成秒到年的设置。
图23:设置时间时序
设置时间的代码清单如下,存放待设置时间的数组中时间的顺序为:秒、分、小时、天、周、月、年,他们均采用BCD格式编码。
**代码清单:**设置时间
- /**************************************************************************************
- * 描 述 : 设置时间
- * 参 数 : p_buf[in]:存放待设置时间的数组,BCD格式编码
- * 返回值 : 无
- **************************************************************************************/
- void PCF8563_Config_Time(u8 *p_buf)
- {
- u8 i;
- P_SW2 |= 0x80; //将EAXFR位置1,允许访问扩展RAM区特殊功能寄存器(XFR)
- I2C_Start(); //发送起始命令,产生起始条件
- I2C_SendData(PCF8563_ADDR_W); //发送I2C器件地址
- I2C_RecvACK(); //接收ACK
- I2C_SendData(PCF8563_SECOND_ADDRESS); //发送寄存器地址,从秒寄存器开始写入
- I2C_RecvACK(); //接收ACK
- for (i=0;i<7;i++) //连续写入秒、分、时、日、星期、月、年
- {
- I2C_SendData(p_buf[i]);
- I2C_RecvACK();
- }
- I2C_Stop(); //发送停止命令,产生停止条件
- P_SW2 &= 0x7F; //将EAXFR位置0,禁止访问XFR
- }
- 数码管显示时间
定时器每2ms产生一次中断,定时器中断服务函数中,对中断次数计数,计数500次即1秒后,读取PCF8563实时时钟的时间,读取完成后,更新数码管显示。
**代码清单:**读取时间、更新数码管显示
- /**********************************************************************************
- * 描 述 : 定时器2中断服务函数
- * 入 参 : 无
- * 返回值 : 无
- **********************************************************************************/
- void timer2_isr() interrupt 12
- {
- static u16 cnt = 0;
- cnt++; //2ms进入1次中断
- if (cnt == 500) //每秒读取一次时间,并更新数码管显示
- {
- cnt = 0;
- PCF8563_Read_Time(); //读PCF8563的时间值:秒、分、时、日、月、年
- Time_DispUpdata(); //更新数码管显示,这里只用到了秒、分、时
- }
- LEDseg_write_data(ledseg_nod); //发送段码
- LEDseg_Refresh(); //发送
- ledseg_nod++;
- if (ledseg_nod == 8)ledseg_nod = 0; //8位数码管刷新完成,ledseg_nod复位
- }
更新数码管的时间显示时,只需更新数码管显示数组中的内容即可,注意读取的时间是BCD格式编码,需要计算成十位和个位,代码清单如下。
**代码清单:**更新数码管显示
- /**********************************************************************************
- 功能描述:更新数码管显示时间
- 参 数:p_time[in]:指向保存从PF8563读取的时间的数组,时间为BCD格式编码
- 返 回 值:无
- ***********************************************************************************/
- void LEDseg_TimeDispUpdata(time_pcf_t *p_time)
- {
- SEG8_DispArray[LEDSEG_1] = SEG8_Code[p_time->hour >> 4]; //小时十位
- SEG8_DispArray[LEDSEG_2] = SEG8_Code[p_time->hour & 0x0F]; //小时个位
- SEG8_DispArray[LEDSEG_4] = SEG8_Code[p_time->min >> 4]; //分钟十位
- SEG8_DispArray[LEDSEG_5] = SEG8_Code[p_time->min & 0x0F]; //分钟个位
- SEG8_DispArray[LEDSEG_7] = SEG8_Code[p_time->sec >> 4]; //秒十位
- SEG8_DispArray[LEDSEG_8] = SEG8_Code[p_time->sec & 0x0F]; //秒个位
- }
主函数中完成相关的初始化,之后,在主循环中读取按键的状态,若KEY1按键按下,则将PCF8563实时时钟的时间设置为2023.3.9 9时30分0秒,第11周。
**代码清单:**主函数
- /**************************************************************************
- 功能描述:主函数
- 入口参数:无
- 返 回 值:int类型
- **************************************************************************/
- int main(void )
- {
- u8 btn_val;
- u8 time_buf[7];
- P2M1 &= 0x3F; P2M0 &= 0x3F; //设置P2.6~P2.7为准双向口(指示灯D1和D2)
- P7M1 &= 0xF9; P7M0 &= 0xF9; //设置P7.1~P7.2为准双向口(指示灯D4和D3)
- P3M1 &= 0x3F; P3M0 &= 0x3F; //设置P3.6~P3.7为准双向口(按键KEY2和KEY1)
- P0M1 &= 0x5F; P0M0 &= 0x5F; //设置P0.5,P0.7为准双向口(按键KEY4和KEY3)
- P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口(串口1的RxD)
- P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出(串口1的TxD)
- uart1_init(); //串口1初始化
- PCF8563_Init(); //PCF8563初始化
- LEDseg_init(); //初始化驱动数码管的GPIO
- timer2_init(); //定时器2初始化
- timer2_start(); //启动定时器2
- EA = 1; //使能总中断
- delay_ms(100); //初始化后延时
- while (1)
- {
- btn_val=buttons_scan(0); //获取开发板用户按键检测值,不支持连按
- if (btn_val == BUTTON1_PRESSED) //KEY1按下:设置PCF8563时间为2023.3.9 9时30分0秒,第11周
- {
- time_buf[0] = 0x00;
- time_buf[1] = 0x30;
- time_buf[2] = 0x09;
- time_buf[3] = 0x09;
- time_buf[4] = 0x11;
- time_buf[5] = 0x03;
- time_buf[6] = 0x23;
- EA = 0; //关闭总中断
- PCF8563_Config_Time(time_buf);
- EA = 1; //使能总中断
- led_toggle(LED_1); //指示灯D1状态翻转,指示操作完成
- }
- }
- }
1.
1.
1. 硬件连接
本实验需要使用板载的PCF8563实时时钟,按照下图所示短接跳线帽。另外,本实验也会使用到数码管,数码管的硬件连接参见《第2-12讲:数码管显示》。
图24:跳线帽短接
-
-
-
- 实验步骤
-
-
- 解压"...\第3部分:配套例程源码"目录下的压缩文件"实验2-14-3: PCF8563实时时钟(I2C)",将解压后得到的文件夹拷贝到合适的目录,如"D\STC8"(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
- 双击"...\i2c_pcf8563\project"目录下的工程文件"i2c_pcf8563.uvproj"。
- 点击编译按钮编译工程,编译成功后生成的HEX文件"i2c_pcf8563.hex"位于工程的"...\i2c_pcf8563\Project\Object"目录下。
- 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz。
- 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps。
- 程序运行后,数码管会显示从PCF8563读取一次时间(显示时、分、秒)。按下按键KEY1后,将PCF8563的时间配置设置为2023.3.9 9时30分0秒,第11周。
- ****说明:****软件I2C主要是修改工程中的I2C相关的底层文件"i2c_hw.c"和"i2c_hw.h"即可,读者如对软件I2C感兴趣,可以尝试将本实验改为软件I2C。
我们也编写好了本实验对应的软件I2C例子,该例子在资料的"...\第3部分:配套例程源码"目录下,实验名称如下,读者在编写的过程中可以参考一下。
- 实验2-14-4:PCF8563实时时钟(软件I2C)。