网站网站开发的公司电话,制作公司网站设,花木公司网站源码,太原网站定制第2-14讲#xff1a;I2C总线的应用 学习目的了解I2C总线的特点。掌握I2C地址的定义#xff0c;对I2C地址要有深刻的了解#xff0c;之后再看到I2C接口设备中描述的7位地址或8位地址#xff0c;不会再有疑惑。掌握STC8A8K64D4系列单片机I2C的特点以及编程方法。掌握通过I2C读…第2-14讲I2C总线的应用 学习目的了解I2C总线的特点。掌握I2C地址的定义对I2C地址要有深刻的了解之后再看到I2C接口设备中描述的7位地址或8位地址不会再有疑惑。掌握STC8A8K64D4系列单片机I2C的特点以及编程方法。掌握通过I2C读写eeprom AT24C02。这里的重点是使用按页写入来代替逐个字节写入从而有效节省写入时间。 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总线电流消耗很低抗干扰强适合应用于低功耗的场合。 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时的地址。 图2I2C地址
由此可见所谓的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后数据传输继续。 图4I2C总线数据传输 ACK和NACK
每个字节后会跟随一个ACK信号。接收者通过ACK位告知发送者已经成功接收一字节数据并准备好接收下一字节数据。所有的时钟脉冲包括ACK信号的时钟脉冲都是由主机产生的。
ACK信号发送者发送完8位数据后在ACK时钟脉冲期间释放SDA线接收者可以将SDA拉低并在时钟信号为高时保持低电平这样就产生了ACK信号从而使得主机知道从机已成功接收数据并且准备好了接收下一数据。NACK信号当SDA在第9个时钟脉冲的时候保持高电平定义为NACK信号。这时主机要么产生STOP条件来放弃这次传输要么重复START条件来启动一个新的传输。
下面的5种情况会导致产生NACK信号
发送方寻址的接收方在总线上不存在因此总线上没有设备应答。接收方正在处理一些实时的功能尚未准备好与主机通信因此接收方不能执行收发。在传输期间接收方收到不能识别的数据或者命令。在传输期间接收方无法接收更多的数据字节。主-接收器要通知从-发送器传输的结束。 从机地址和R/W位
I2C数据传输如下图所示在起始条件(S)后发送从机地址从机地址是7位从机地址后紧跟着的第8位是读写位R/W读写位为0表示写读写位为1表示读。数据传输一般由主机产生的停止位P 终止但是如果主机仍希望在总线上通信他可以产生重复起始条件 S和寻址另一个从机而不是首先产生一个停止条件在这种传输中可能有不同的读写格式结合。 图5I2C总线传输时序
可能的数据传输格式有
主机发送器发送到从机接收器传输的方向不会改变接收器应答每一个字节如下图所示。 图6主机发送器7位地址寻址从机接收器传输方向不改变
在第一个字节后主机立即读从机在第一次应答后主机发送器变成主机接收器从机接收器变成从机发送器。第一次应答仍由从机生成主机生成后续应答。之前发送了一个非应答A的主机产生STOP条件。 图7主机发送第一个字节后立即读取从机
复合格式如下图所示。传输改变方向的时侯起始条件和从机地址都会被重复但R/W位取反。如果主接收器发送重复START条件他会在重复START条件之前发送一个非应答A。 图8复合格式 硬件设计
IK-64D4开发板上设计了I2C接口的EEPROM存储器AT24C02和PCF8563时钟日历电路单元用于我们学习I2C的应用。 AT24C02EEPROM电路
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。 图9EEPROM电路
EEPROM存储器AT24C02占用的STC8A8K64D4的引脚如下表
表2I2C连接AT24C02引脚分配 名称 引脚 说明 SCL P7.7 和外扩存储器接口J11共用 SDA P7.6 和外扩存储器接口J11共用 注为了方便读者理解单片机访问AT24C02存储器的编程AT24C02的I2C通信时序以及读、写、擦除操作将在软件设计部分讲解。 PCF8563时钟/日历电路
PCF8563是一款为低功耗而优化的CMOS实时时钟/日历芯片。他提供了可编程时钟输出、中断输出和低电压检测器所有地址和数据通过双线双向I2C总线串行传输最大总线速度为400 kbit/s。
开发板上的PCF8563硬件电路如下图所示。单片机的 P2.5、P2.4通过跳线连接到PCF8563的SCL和SDA引脚他们和W5500以太网模块接口J7共用IO因此当我们使用PCF8563时不能使用W5500以太网模块接口J7。 图10PCF8563时钟日历电路
PCF8563占用的STC8A8K64D4的引脚如下表
表21I2C连接PCF8563引脚分配 名称 引脚 说明 SCL P2.5 和W5500以太网模块共用 SDA P2.4 和W5500以太网模块共用 注为了方便读者理解单片机访问PCF8563存储器的编程PCF8563的I2C通信时序以及读、写操作将在软件设计部分讲解。 STC8A8K64D4的IIC应用步骤
STC8A8K64D4单片机内部集成了一个 I2C串行总线控制器该I2C总线具有主机和从机两种操作模式并有多组引脚可供选择STC8A8K64D4单片机的I2C应用步骤如下图所示。 图11I2C总线应用步骤 配置I2C功能引脚
I2C有多组引脚与之对应具体几组还取决于芯片封装引脚数同一时刻只能通过相关寄存器配置其中的一组使用 STC8A8K64D4单片机I2C的引脚分配如下表。
表3STC8A8K64D4单片机I2C引脚分配 I2C信号 信号编号 对应的IO SDA数据 SDA P1.4 SDA_2 P2.4 SDA_3 P7.6 SDA_4 P3.3 SCL时钟 SCL P1.5 SCL_2 P2.5 SCL_3 P7.7 SCL_4 P3.2 I2C是通过“外设端口切换控制寄存器2P_SW2”中的SPI_S[1:0]配置引脚的如下图所示。
外设端口切换控制寄存器2P_SW2 P_SW2寄存器中的I2C_S[1:0]为 I2C功能脚选择位如下表所示。
表4I2C功能脚选择位 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”位开启中断的如下图所示。 图12I2C中断配置
中断产生后硬件自动置位相应的中断标志向CPU发出中断请求CPU处理完中断后需要软件清零中断标志。 注意开启I2C中断的情况下还需要开启总中断“EA1”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.2I2C 主机控制寄存器 I2CMSCR。 软件设计 I2C读写EEPRON(AT24C02)存储器实验注本节的实验是在“实验2-6-1串口1数据收发实验”的基础上修改本节对应的实验源码是“实验2-14-1硬件I2C读写EEPROM(AT24C02)存储器”。 实验内容
将STC8A8K64D4单片机的I2C配置为主机速度为400Kbps通过I2C总线访问AT24C02存储器完成以下操作。
单个字节写入向指定地址写入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; I2CCFG0xED; //配置I2C主机模式、允许I2C功能、I2C总线速度400Kbps I2CMSST0x00; //清零I2C主机状态寄存器各标志位 } AT24C02地址的确定
查询AT24C02数据手册可知其地址高4位固定为“1010”如下图所示紧跟着的3个位由芯片的引脚A2、A1和A0的电平确定最低位为读写位。开发板上AT24C02的硬件电路中将引脚A2、A1和A0连接到了GND因此A2、A1和A0均为0由此可提取出7位地址为0x50转换为8位地址0xA0写、0xA1读如下图所示。 图13AT24C02地址的确定
代码中AT24C02地址定义如下
代码清单定义AT24C02地址
#define AT24C02_ADDR_W 0xA0 //I2C从机写地址 #define AT24C02_ADDR_R 0xA1 //I2C从机读地址 单字节写入向指定地址写入单个字节数据
AT24C02将单个字节写入到指定地址的时序如下图所示首先产生起始条件紧跟着发送7位地址 0写之后发送写入数据的地址和数据最后产生停止条件。 图14AT24C02写入单个字节数据时序
根据时序即可写出“写入单个字节数据”的函数代码清单如下。这里面需要特别注意的是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之后再执行读取操作即可读出数据。 图15AT24C02读取单个字节数据时序
读取单个字节数据的代码清单如下。
代码清单从指定地址读出单个字节数据
/************************************************************************************** * 描 述 : 从AT24C02指定地址读取一个字节数据 * 参 数 : Addr[in]读出数据的地址 * 返回值 : 读取的数据 **************************************************************************************/ u8 AT24C02_read_byte(u8 Addr) { u8 temp0; 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 tempI2C_RecvData(); //读一个字节数据 I2C_Stop(); //发送停止命令产生停止条件 return temp; //返回读取的数据 } 按页写入向指定页面连续写入不大于页面长度的数据
AT24C02按页写入的时序如下图所示首先产生起始条件紧跟着发送7位地址 0写接着发送写入数据的地址之后连续发送写入的数据最后产生停止条件。 图16AT24C02按页写入时序
按页写入要注意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(len0) //写入到最后的页面的数据读取完成 { AT24C02_write_page(write_addr,tx_buf, sendlen); //执行一次按页写入操作 sendlen 0; //清零发送长度 } } addr_ptr; //地址加1 } return AT24C02_SUCESS; }
批量读取从指定地址顺序读取指定长度数据
AT24C02从指定地址顺序读数据的时序如下图所示顺序读相对于写要简单因为不用考虑跨页的问题顺序读时每读出一个字节数据地址指针会自动加1。 图19AT24C02连续读时序
批量顺序读取的函数的代码清单如下。
代码清单批量顺序读取
/************************************************************************* * 功 能 : 从指定的起始地址顺序读出指定长度的数据 * 参 数 : 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.5P0.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_valbuttons_scan(0); //获取开发板用户按键检测值不支持连按 //按下KEY1从地址0x0000开始连续写入256个字节数据 if(btn_val BUTTON1_PRESSED) { j 0; for(i0;itest_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(i00;itest_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 } } } 硬件连接
本实验需要使用板载的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”。 实验内容
本实验重点在于掌握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配置为主机速度为400KbpsI2C功能引脚使用P2.4SDA和P2.5SCL。初始化时读取一次时间并将读取的时间更新到数码管显示数组代码清单如下。
代码清单I2C初始化
/********************************************************************************** 功能描述PCF8563时钟初始化 参 数无 返 回 值无 ***********************************************************************************/ void PCF8563_Init(void) { I2C_init(); //IIC初始化 PCF8563_Read_Time(); //读取时间 Time_DispUpdata(); //更新数码管显示数组 }
AT24C02地址的确定
查询PCF8563实时时钟数据手册可知其地址固定为“1010001”如下图所示最低位为读写位。由此可提取出7位地址为0x51转换为8位地址0xA2写、0xA3读如下图所示。 图21PCF8563地址的确定
代码中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(i0;i7;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.5P0.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_valbuttons_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状态翻转指示操作完成 } } } 硬件连接
本实验需要使用板载的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-4PCF8563实时时钟软件I2C。