请选择 进入手机版 | 继续访问电脑版
[X]关闭
0

S02-CH11 I2C读写EEPROM实验

摘要: 软件版本:VIVADO2017.4操作系统:WIN10 64bit硬件平台:适用米联客 ZYNQ系列开发板米联客(MSXBO)论坛:www.osrc.cn答疑解惑专栏开通,欢迎大家给我提问!!11.1 概述 我们知道I2C总线具备广泛的用途,比如寄存器 ...

软件版本:VIVADO2017.4

操作系统:WIN10 64bit

硬件平台:适用米联客 ZYNQ系列开发板

米联客(MSXBO)论坛:www.osrc.cn答疑解惑专栏开通,欢迎大家给我提问!!

11.1 概述

       我们知道I2C总线具备广泛的用途,比如寄存器的配置,EEPROM的使用,更重要的是I2C总线上可以挂载非常多的外设。 对于一些低速器件的访问非常节省IO资源,由于是标准的总线接口,使用起来非常方便。I2C总线是OC开路,支持双向传输,所以总线上需要上拉电阻,如下图。

11.2 I2C总线协议

       由于节课讲解的I2C是基于ZYNQ的I2C控制器,实际上可以不需要非常清楚I2C的详细时序,但是作为初学者,如果第一次学习I2C总线的,还是有必要学习下。

I2C协议把传输的消息分为两种类型的帧:

一个地址帧 :用于master指明消息发往哪个slave;

一个或多个数据帧: 由master发往slave的数据(或由slave发往master),每一帧是8-bit的数据。

注:协议要求每次放到SDA上的字节长度必须为8位,并且每个字节后须跟一个ACK位,在下面会讲到。

数据在SCL处于低电平时放到SDA上,并在SCL变为高电平后进行采样。读写数据和SCL上升沿之间的时间间隔是由总线上的设备自己定义的,不同芯片可能有差异。

I2C数据传输的时序图如下:

开始条件(start condition):

为了标识传输正式启动,master设备会将SCL置为高电平(当总线空闲时,SDA和SCL都处于高电平状态),然后将SDA拉低,这样,所有slave设备就会知道传输即将开始。如果两个master设备在同一时刻都希望获得总线的所有权,那么谁先将SDA拉低,谁就赢得了总线的控制权。在整个通信期间,可以存在多个start来开启每一次新的通信序列(communication sequence),而无需先放弃总线的控制权,后面会讲到这种机制。


地址帧(address frame):

地址帧总是在一次通信的最开始出现。一个7-bit的地址是从最高位(MSB)开始发送的,这个地址后面会紧跟1-bit的操作符,1表示读操作,0表示写操作。

接下来的一个bit是NACK/ACK,当这个帧中前面8bits发送完后,接收端的设备获得SDA控制权,此时接收设备应该在第9个时钟脉冲之前回复一个ACK(将SDA拉低)以表示接收正常,如果接收设备没有将SDA拉低,则说明接收设备可能没有收到数据(如寻址的设备不存在或设备忙)或无法解析收到的消息,如果是这样,则由master来决定如何处理(stop或repeated start condition)。


数据帧(data frames):

在地址帧发送之后,就可以开始传输数据了。Master继续产生时钟脉冲,而数据则由master(写操作)或slave(读操作)放到SDA上。每个数据帧8bits,数据帧的数量可以是任意的,直到产生停止条件。每一帧数据传输(即每8-bit)之后,接收方就需要回复一个ACK或NACK(写数据时由slave发送ACK,读数据时由master发送ACK。当master知道自己读完最后一个byte数据时,可发送NACK然后接stop condition)。


停止条件(stop condition):

当所有数据都发送完成时,master将产生一个停止条件。停止条件定义为:在SDA置于低电平时,将SCL拉高并保持高电平,然后将SDA拉高。

注意,在正常传输数据过程中,当SCL处于高电平时,SDA上的值不应该变化,防止意外产生一个停止条件。


重复开始条件(repeated start condition):

有时master需要在一次通信中进行多次消息交换(例如与不同的slave传输消息,或切换读写操作),并且期间不希望被其他master干扰,这时可以使用“重复开始条件” —— 在一次通信中,master可以产生多次start condition,来完成多次消息交换,最后再产生一个stop condition结束整个通信过程。由于期间没有stop condition,因此master一直占用总线,其他master无法切入。

为了产生一个重复的开始条件,SDA在SCL低电平时拉高,然后SCL拉高。接着master就可以产生一个开始条件继续新的消息传输(按照正常的7-bit/10-bit地址传输时序)。重复开始条件的传输时序如下图所示:

时钟拉伸(clock stretching):

有时候,低速slave可能由于上一个请求还没处理完,尚无法继续接收master的后续请求,即master的数据传输速率超过了slave的处理能力。这种情况下,slave可以进行时钟拉伸来要求master暂停传输数据 —— 通常时钟都是由master提供的,slave只是在SDA上放数据或读数据。而时钟拉伸则是slave在master释放SCL后,将SCL主动拉低并保持,此时要求master停止在SCL上产生脉冲以及在SDA上发送数据,直到slave释放SCL(SCL为高电平)。之后,master便可以继续正常的数据传输了。可见时钟拉伸实际上是利用了时钟同步的机制(见下文),只是时钟由slave产生。

如果系统中存在这种低速slave并且slave实现了clock stretching,则master必须实现为能够处理这种情况,实际上大部分slave设备中不包含SCL驱动器的,因此无法拉伸时钟。

所以更完整的I2C数据传输时序图为:

10-bit地址空间:

上面讲到I2C支持10-bit的设备地址,此时的时序如下图所示:

在10-bit地址的I2C系统中,需要两个帧来传输slave的地址。第一个帧的前5个bit固定为b11110,后接slave地址的高2位,第8位仍然是R/W位,接着是一个ACK位,由于系统中可能有多个10-bit slave设备地址的高2bit相同,因此这个ACK可能由多有slave设备设置。第二个帧紧接着第一帧发送,包含slave地址的低8位(7:0),接着该地址的slave回复一个ACK(或NACK)。

注意,10-bit地址的设备和7-bit地址的设备在一个系统中是可以并存的,因为7-bit地址的高5位不可能是b11110。实际上对于7-bit的从设备地址,合法范围为b0001XXX-b1110XXX,’X’表示任意值,因此该类型地址最多有112个(其他为保留地址[1])。

两个地址帧传输完成后,就开始数据帧的传输了,这和7-bit地址中的数据帧传输过程相同。


时钟同步和仲裁:

如果两个master都想在同一条空闲总线上传输,此时必须能够使用某种机制来选择将总线控制权交给哪个master,这是通过时钟同步和仲裁来完成的,而被迫让出控制权的master则需要等待总线空闲后再继续传输。在单一master的系统上无需实现时钟同步和仲裁。


时钟同步:

时钟同步是通过I2C接口和SCL之间的线“与”(wired-AND)来完成的,即如果有多个master同时产生时钟,那么只有所有master都发送高电平时,SCL上才表现为高电平,否则SCL都表现为低电平。


总线仲裁:

总线仲裁和时钟同步类似,当所有master在SDA上都写1时,SDA的数据才是1,只要有一个master写0,那此时SDA上的数据就是0。一个master每发送一个bit数据,在SCL处于高电平时,就检查看SDA的电平是否和发送的数据一致,如果不一致,这个master便知道自己输掉仲裁,然后停止向SDA写数据。也就是说,如果master一直检查到总线上数据和自己发送的数据一致,则继续传输,这样在仲裁过程中就保证了赢得仲裁的master不会丢失数据。

输掉仲裁的master在检测到自己输了之后也不再产生时钟脉冲,并且要在总线空闲时才能重新传输。

仲裁的过程可能要经过多个bit的发送和检查,实际上两个master如果发送的时序和数据完全一样,则两个master都能正常完成整个的数据传输。

11.3 EEPROM 24C02介绍

经过上面对I2C总线的介绍,下面我们介绍EEPROM 24C02的I2C总线读写控制。

如下图,A0-A2是EEPROM I2C器件地址,SDA和SCL是EEPROM I2C总线SLAVE接口,WP是保护脚,一般接VCC。

24LXX 器件地址如下图

       我们看下24C02的写时序,可以看到,支持单个字节的写,以及多个字节的写。首先发送器件的地址,然后发送需要写EEPROM存储空间的地址,之后就是数据,对于读操作一次可以写1个字节或者多个字节。

       我们看下24C02的读时序,可以看到,支持单个字节的读,以及多个字节的读。以下支持3种读的方式:

第一种:CURRENT ADDRESS READ 只要发送器件地址就能读数据

第二种:RANDOM READ 需要发送器件地址,然后发送内存地址,之后再发送器件地址并且读取到数据,支持连续读取。

第三种:SEQUENTIAL CURRENT READ 只要发送器件地址,就能连续读取当前地址的数据,支持连续读取。

教程代码中采用的是第二种方法。

11.4 FPGA BD工程

      为了提高学习效率,从本课开始只讲解搭建BD工程以及IP的关键参数配置,搭建FPGA BD工程的重复步骤不再详细讲解,如果有不清楚的,请认真学习本章节前面的课程内容。

      做这个实验必须勾选支持I2C控制器,通过EMIO的方式引出I2C总线。EEPROM模块连线就可以完成实验。对于初学者需要注意,EMIO是FPGA的PIN脚因此需要添加XDC文件约束FPGA PIN脚。

 

      另外为了完本课程实验,需要选择购买EEPROM模块。对于MZ7XA-7010(mini)/MZ7XA-7020/MZ7XB-7020开发板具有板载的IO扩展,只要正确和EEPROM模块对接就能完成此实验。

11.5 I2C Polled方式读写EEPROM

11.5.1 I2c 控制器

PS支持两个具有以下主要功能的I2C设备:

I2C总线规范版本2

支持16字节FIFO

可编程的正常和快速总线数据速率

主模式

 -写转移

 -读取转移

 -扩展地址支持

-支持缓慢处理器服务的HOLD

-支持中断

从模式

11.5.2 I2cPs_Polled.c

我们米联客(MSXBO)编写了两种读写EEPROM的方式,并且封装成子函数方便用户调用。我们先看代码。

include "I2cPs_Polled.h"

#include "sleep.h"

int I2cPs_init(XIicPs *I2C_Ptr,u16 DeviceId)

{

int Status;

XIicPs_Config *Config;


/*

 * Initialize the IIC driver so that it's ready to use

 * Look up the configuration in the config table, then initialize it.

 */

Config = XIicPs_LookupConfig(DeviceId);

if (NULL == Config) {

return XST_FAILURE;

}


Status = XIicPs_CfgInitialize(I2C_Ptr, Config, Config->BaseAddress);

if (Status != XST_SUCCESS) {

return XST_FAILURE;

}


/*

 * Perform a self-test to ensure that the hardware was built correctly.

 */

Status = XIicPs_SelfTest(I2C_Ptr);

if (Status != XST_SUCCESS) {

return XST_FAILURE;

}


/*

 * Set the IIC serial clock rate.

 */

XIicPs_SetSClk(I2C_Ptr, IIC_SCLK_RATE);


return XST_SUCCESS;

}



void I2cPs_write(XIicPs *I2C_Ptr, u8 *MsgPtr, s32 ByteCount, u16 SlaveAddr)

{


XIicPs_MasterSendPolled(I2C_Ptr, MsgPtr, ByteCount, SlaveAddr);


while (XIicPs_BusIsBusy(I2C_Ptr)) {

}

usleep(2000);


}



void I2cPs_read(XIicPs *I2C_Ptr, u8 *MsgPtr, s32 ByteCount, u16 SlaveAddr)

{

XIicPs_MasterRecvPolled(I2C_Ptr, MsgPtr, ByteCount, SlaveAddr);

while (XIicPs_BusIsBusy(I2C_Ptr)) {

}

usleep(2000);


}


I2cPs_init

函数负责初始化I2C控制器,关键是初始化了I2C控制的速度XIicPs_SetSClk(I2C_Ptr, IIC_SCLK_RATE)。

I2cPs_write 

函数顾名思义是实现I2C的写数据

I2cPs_read  

函数顾名思义是实现I2C的读数据

11.5.3 eeprom.c

/*

 * si570.c

 *

 */


#include "eeprom.h"


#include "sleep.h"


void eeprom_test()

{

    int i;

for(i=1;i<9;i++)

eeprom_wbuf[i]=i;

xil_printf("write eeprom 24lc02 I2C polled\r\n");

write_eeprom(0,8);


xil_printf("read eeprom 24lc02 I2C polled\r\n");

read_eeprom(0,8);


for(i=0;i<8;i++)

xil_printf("%d\r\n",eeprom_rbuf[i]);


}


void read_eeprom(u8 addr,u8 len)

{

eeprom_wbuf[0]=addr;

I2cPs_write(&Iic,eeprom_wbuf, 1, EEPROM_ADDR);

I2cPs_read (&Iic,eeprom_rbuf, len, EEPROM_ADDR);

}



void write_eeprom(u8 addr,u8 len)

{

eeprom_wbuf[0]=addr;

I2cPs_write(&Iic,eeprom_wbuf,len+1,EEPROM_ADDR);

}



      这个函数中,实现对EEPROM 24C02的读写操作,并且打印读出的结果。这里需要注意的是掌握24CXX EEPROM的读写控制。

      如下图所示,程序中写操作是Page Write连续写入1个器件地址,1个寄存器首地址,8个有效数据。其中器件地址就是EEPROM_ADDR。而寄存地址首就是eeprom_wbuf[0],有效数据是eeprom_wbuf[1]~eeprom_wbuf[8]。

      如下图所示,再看读操作,读操作的时候写寄存器地址,eeprom_wbuf[0]就是寄存器地址,然后再启动读函数读数据,读数据是保存在eeprom_rbuf中 ,在eeprom_rbuf中,一次可以读到8个字节数据。

11.5.4 main.c


/*

 *

 * www.osrc.cn

 * www.milinker.com

 *

*/


#include "I2cPs_Polled.h"


#include "eeprom.h"



int main(void)

{


I2cPs_init(&Iic,IIC_DEVICE_ID);

eeprom_test();


    return 0;

}

在main.c文件里面实现了 I2C接口的初始化,之后调用eeprom_test函数对eeprom测试。

11.6 I2C中断方式读写EEPROM

11.6.1 I2cPs_intr.c

/*

 * i2c_intr.c

 *

 */


#include "I2cPs_intr.h"


int I2cPs_init(XIicPs *I2C_Ptr,u16 DeviceId)

{

int Status;

XIicPs_Config *Config;


/*

 * Initialize the IIC driver so that it's ready to use

 * Look up the configuration in the config table, then initialize it.

 */

Config = XIicPs_LookupConfig(DeviceId);

if (NULL == Config) {

return XST_FAILURE;

}


Status = XIicPs_CfgInitialize(I2C_Ptr, Config, Config->BaseAddress);

if (Status != XST_SUCCESS) {

return XST_FAILURE;

}


/*

 * Perform a self-test to ensure that the hardware was built correctly.

 */

Status = XIicPs_SelfTest(I2C_Ptr);

if (Status != XST_SUCCESS) {

return XST_FAILURE;

}


/*

 * Setup the handlers for the IIC that will be called from the

 * interrupt context when data has been sent and received, specify a

 * pointer to the IIC driver instance as the callback reference so

 * the handlers are able to access the instance data.

 */

XIicPs_SetStatusHandler(I2C_Ptr, (void *)I2C_Ptr, I2cPs_Handler);


/*

 * Set the IIC serial clock rate.

 */

XIicPs_SetSClk(I2C_Ptr, IIC_SCLK_RATE);


return XST_SUCCESS;

}



void I2cPs_write(XIicPs *I2C_Ptr, u8 *MsgPtr, s32 ByteCount, u16 SlaveAddr)

{


while (XIicPs_BusIsBusy(I2C_Ptr)) {

/* NOP */

}


SendComplete = FALSE;


/*

 * Send the buffer, errors are reported by TotalErrorCount.

 */

XIicPs_MasterSend(I2C_Ptr, MsgPtr, ByteCount, SlaveAddr);


/*

 * Wait for the entire buffer to be sent, letting the interrupt

 * processing work in the background, this function may get

 * locked up in this loop if the interrupts are not working

 * correctly.

 */

while (!SendComplete) {

if (0 != TotalErrorCount) {

xil_printf("I2C write error! %d\r\n", TotalErrorCount);

return;

}

}


}



void I2cPs_read(XIicPs *I2C_Ptr, u8 *MsgPtr, s32 ByteCount, u16 SlaveAddr)

{


while (XIicPs_BusIsBusy(I2C_Ptr)) {

/* NOP */

}


/*

 * Receive data from slave, errors are reported through

 * TotalErrorCount.

 */

RecvComplete = FALSE;


XIicPs_MasterRecv(I2C_Ptr, MsgPtr, ByteCount, SlaveAddr);


while (!RecvComplete) {

if (0 != TotalErrorCount) {

xil_printf("I2C read error! %d\r\n", TotalErrorCount);

return;

}

}


}


int I2cPs_Setup_IntrSystem(XScuGic * GicInstancePtr , XIicPs *I2C_Ptr ,u16 I2cIntrId)

{

int Status;


Status = XScuGic_Connect(GicInstancePtr, I2cIntrId,

(Xil_InterruptHandler)XIicPs_MasterInterruptHandler,

(void *)I2C_Ptr);

if (Status != XST_SUCCESS) {

return Status;

}


XScuGic_Enable(GicInstancePtr, I2cIntrId);

return XST_SUCCESS;

}



/*****************************************************************************/

/**

*

* This function is the handler which performs processing to handle data events

* from the IIC.  It is called from an interrupt context such that the amount

* of processing performed should be minimized.

*

* This handler provides an example of how to handle data for the IIC and

* is application specific.

*

* @param CallBackRef contains a callback reference from the driver, in

* this case it is the instance pointer for the IIC driver.

* @param Event contains the specific kind of event that has occurred.

*

* @return None.

*

* @note None.

*

*******************************************************************************/

void I2cPs_Handler(void *CallBackRef, u32 Event)

{

/*

 * All of the data transfer has been finished.

 */

//xil_printf("Event %d \r\n", Event);


if (0 != (Event & XIICPS_EVENT_COMPLETE_RECV)){

RecvComplete = TRUE;

} else if (0 != (Event & XIICPS_EVENT_COMPLETE_SEND)) {

SendComplete = TRUE;

} else if (0 == (Event & XIICPS_EVENT_SLAVE_RDY)){

/*

 * If it is other interrupt but not slave ready interrupt, it is

 * an error.

 * Data was received with an error.

 */

TotalErrorCount++;

}

}

I2cPs_init

函数负责初始化I2C控制器,以及设置了中断回调函数XIicPs_SetStatusHandler(I2C_Ptr, (void *)I2C_Ptr, I2cPs_Handler),初始化了I2C控制的速度XIicPs_SetSClk(I2C_Ptr, IIC_SCLK_RATE)。

I2cPs_write 

函数顾名思义是实现I2C的写数据

I2cPs_read  

函数顾名思义是实现I2C的读数据

I2cPs_Setup_IntrSystem

函数负责将I2C中断连接到全局中断服务,并且启动I2C中断

I2cPs_Handler

中断产生后会调用此函数。

11.6.2 eeprom.c

/*

 * eeprom.c

 *msxbo-www.osrc.cn

 */


#include "eeprom.h"


#include "sleep.h"


void eeprom_test()

{

    int i;

for(i=1;i<9;i++)

eeprom_wbuf[i]=i;

xil_printf("write eeprom 24lc02 I2C intr\r\n");

write_eeprom(0,8);

usleep(2000);

xil_printf("read eeprom 24lc02  I2C intr\r\n");

read_eeprom(0,8);


for(i=0;i<8;i++)

xil_printf("%d\r\n",eeprom_rbuf[i]);


}


void read_eeprom(u8 addr,u8 len)

{

eeprom_wbuf[0]=addr;

I2cPs_write(&Iic,eeprom_wbuf, 1, EEPROM_ADDR);

I2cPs_read (&Iic,eeprom_rbuf, len, EEPROM_ADDR);

}


void write_eeprom(u8 addr,u8 len)

{

eeprom_wbuf[0]=addr;

I2cPs_write(&Iic,eeprom_wbuf,len+1,EEPROM_ADDR);

}


eeprom.c文件中通过的调用I2C读写接口函数实现对EEPROM的读写,详细的介绍在11.4.3 eeprom.c程序分析中已经介绍。

11.6.3 main.c

/*

* www.osrc.cn

 * www.milinker.com

 *

*/


#include "sys_intr.h"

#include "sleep.h"


#include "eeprom.h"

#include "I2cPs_intr.h"


void init_intr_sys(void)

{

I2cPs_init(&Iic ,IIC_DEVICE_ID);

Init_Intr_System(&Intc);

Setup_Intr_Exception(&Intc);

I2cPs_Setup_IntrSystem(&Intc, &Iic,IIC_INT_VEC_ID);

}


int main(void)

{


init_intr_sys();

eeprom_test();


    return 0;

}

在main.c文件中实现对I2C中断的初始化,之后调用eeprom_test函数对eeprom测试。

11.7 硬件连线

通过外扩的FPGA GPIO 连接 24LCX模块,MZ7XA、MZ7XB自带此IO

对于没有这组IO的开发板通过FEP转NEP转接卡实现,此转接卡需要单独购买

11.8测试结果


路过

雷人

握手

鲜花

鸡蛋

最新评论

本文作者
2019-9-6 19:22
  • 7
    粉丝
  • 5040
    阅读
  • 0
    回复

关注米联客

扫描关注,了解最新资讯

联系人:汤经理
电话:0519-80699907
EMAIL:270682667@qq.com
地址:常州溧阳市天目云谷3号楼北楼201B
热门评论
排行榜