FPGA-EEPROM读写记录

文章目录

  • 1. 概要
  • 2. AT24C64技术分析
    • 2.1 引脚分析
    • 2.2 首尾操作分析
    • 2.3 寻址分析
    • 2.4 写操作分析
    • 2.5 读操作分析
  • 3. 模块仿真
    • 3.1 模块分析
    • 3.2 仿真分析
    • 小结

1. 概要

本文基于Vivado2020平台,利用正点原子领航者7020开发板控制EEPROM读写。
整篇文章会首先对AT24C64技术文档进行分析,其次分析AT24C64在FPGA上的引脚分配情况,最后逐步分析正点原子给出的Verilog代码,对E2PROM的读写过程进行仿真分析。

2. AT24C64技术分析

EEPROM(Electrically Erasable Promgrammable Read Only Memory,E2PROM)即电可擦除可编程只读存储器。领航者开发板用到的E2PROM为AT24C64,其存储容量为64Kb,内部分为256页,每页32字节,共有8192个字节,读写操作都是以字节为基本单位,共有65536个bit。

2.1 引脚分析

AT24C64的引脚图如图1所示,其引脚的功能如表1所示。

表1 AT24C64引脚功能表
引脚名称 引脚功能
A0-A2 地址输入
SDA 串行数据
SCL 串行时钟输入
WP 写保护

FPGA-EEPROM读写记录_第1张图片

图1 AT24C64引脚图

引脚的具体分析如下。

  • SERIAL CLOCK(SCL):SCL用于提供时钟信号,其上边沿和下边沿在数据写入时有不同的功能。
  • SERIAL DATA(SDA):SDA是双向的数据传输引脚,该引脚是开漏输出,因此需要外接电阻才能够输出高电平。
  • DEVICE/PAGE ADDRESSES(A2-A0):为AT24C64的寻址位,通过修改A2-A0的电平连接情况,可以更改该器件地址。
  • WRITE PROTECT(WP):当该引脚接GND时,允许写操作。当WP接 W c c W_{cc} Wcc 时,所有的写操作将被禁止。如果不连接,WP内部连接GND。

AT24C64家用温度范围为0℃ - 70℃,工作 V c c V_{cc} Vcc 为1.8V - 5.5V。低时钟脉冲最低时间在5V下为1.2 μ s \mu s μs,高时钟脉冲最低时间在5V下为0.6 μ s \mu s μs。写入周期,在5V下最大不超过10 m s ms ms

2.2 首尾操作分析

想要正常使用AT24C64,我们需要知道其是如何开始和结束操作的,两者之间的周期又是如何写入和读出数据的。

  • 时钟和数据传输:SDA在正常情况下用外部设备拉高。在SDA上的数据在SCL为低电平时允许改变,起始或终止的条件由SCL为高电平时SDA的状态决定。
  • 起始条件:当SCL为高电平时,SDA出现下降沿。该条件必须在所有操作的前面。
  • 终止条件:当SCL为高电平时,SDA出现上升沿。
  • 应答:当地址和数据都以字节的形式串口输入到EEPROM中时,EEPROM会在第9个时钟周期输出一个低电平信号表示自己接收到了数据。

上述操作的示例图如下。
FPGA-EEPROM读写记录_第2张图片

图2 起始和终止的定义

FPGA-EEPROM读写记录_第3张图片

图3 从机应答

2.3 寻址分析

EEPROM需要在起始操作后输入一个字节的器件地址,来使能后续的读写操作。该器件地址的高四位是固定的,低四位中的三位由A2-A0决定,最后一位为读/写操作位(0/1)。由于低4位中的三位由引脚决定,因此在一个总线上可以同时存在8个从机设备。器件地址的电平情况如图4所示。

FPGA-EEPROM读写记录_第4张图片

图4 AT24C64器件地址

2.4 写操作分析

由于AT24C64存储器的内存为64Kb,因此当器件地址被从机接收,应答被主机接收完成后。需要输入两个字节的字地址,用来确定写入的位置。每一个字节的字地址被写入后,都要等待从机的应答位,当两个应答位都是低电平代表有效写入。之后,在写入一个字节的数据,从机在收到数据后会输出低电平应答。当所以信息都写入完毕后,从机的微处理器会阻止后续内容的写入。在等待一个从机内部的写入周期后,才能正常写入。这一写入周期最大不超过10 m s ms ms
FPGA-EEPROM读写记录_第5张图片

图5 AT24C64写操作

对于页写入来说,同字节写入相同,只不过在写入一个字节的数据后不发送停止信号,而是继续发送三个字节的数据信息。如果发送的字节数据超过四个字节,则会在当前页覆盖写入,即第五个字节的写入地址重新定位到第一个字节写入的位置。

2.5 读操作分析

读操作分为三种不同的方式,分别是:

  • 读当前地址:AT24C64内部地址指向上一个操作的最后一个地址+1(寄存器内部在操作结束后自动+1),该内部地址在器件断电之前都是有效的,同时如果上一个操作的最后一个地址是页地址的最后一个字节,那么下一个读操作的地址在该页地址的第一个字节。当接收到带有读命令的器件地址时,器件控制器会输出数据,但是不会输出应答位(参考图6)。
  • 随机读:随机读之前需要一个比较笨拙的写操作。先通过写地址的方式将需要读的地址写入,使器件内部指向该地址。在写操作结束之后,主机必须产生一个新的起始条件,在该起始条件之后进行读操作(参考图7)。
  • 连续读:连续读在读当前地址操作和随机读操作的基础上读出额外的字节数据,具体同页写入类似。

FPGA-EEPROM读写记录_第6张图片

图6 AT24C64读当前地址操作

FPGA-EEPROM读写记录_第7张图片

图7 AT24C64随机读操作

3. 模块仿真

在Vivado中构建仿真程序,其中各模块端口及信号连接如图8所示。
FPGA-EEPROM读写记录_第8张图片

图8 顶层模块原理图

基于上述原理图,我们将逐一分析各个模块的主要内容,以及模块参数的意义。

3.1 模块分析

首先,我们先看顶层模块。

module e2prom_top(
	input 		sys_clk,
	input 		sys_rst_n,
	
	output 		iic_scl,					// EEPROM 时钟线SCL
	inout		iic_sda,					// EEPROM 数据线SDA
	output 		led
    
	);
	
// parameter define
parameter	SLAVE_ADDR = 7'b1010_000;		// EEPROM器件地址
parameter 	BIT_CTRL   = 1'b1;				// 字地址位控制参数
parameter   CLK_FREQ   = 26'd50_000_000;	// I2C_DRI模块的驱动时钟频率
parameter	I2C_FREQ   = 18'd250_000;		// I2C的SCL时钟频率
parameter   L_TIME	   = 17'd125_000;		// LED闪烁时间参数
parameter  	MAX_BYTE   = 16'd256;			// 读写测试的字节个数

// wire define
wire 		dri_clk;			// I2C操作时钟 T=1us 在i2c_dri中分频		
wire 		i2c_exec;			// I2C触发控制
wire [15:0] i2c_addr;			// I2C操作地址; AT24C64: 64kB=8KB=8192B=2^13B
wire [ 7:0] i2c_data_w;			// I2C写入数据
wire 		i2c_done;			// I2C操作结束标志
wire 		i2c_ack;			// I2C应答标志	
wire 		i2c_rh_wl;			// I2C读写控制
wire [ 7:0] i2c_data_r;			// I2C读出数据
wire 		rw_done;			// E2PROM读写测试完成
wire		rw_result;			// E2PROM读写测试结果 0:失败  1:成功	
	
// main code

// EEPROM读写测试模块
e2prom_rw #(
	.MAX_BYTE	(MAX_BYTE)
) u_e2prom_rw (
	.dri_clk	(dri_clk),		// 时钟信号
	.rst_n		(sys_rst_n),	// 复位信号	
	// I2C interface
	.i2c_exec	(i2c_exec),		// IC2触发执行信号
	.i2c_rh_wl	(i2c_rh_wl),	// IC2读写控制信号
	.i2c_addr	(i2c_addr),		// IC2器件内地址
	.i2c_data_w	(i2c_data_w),	// IC2写数据	
	.i2c_data_r	(i2c_data_r),	// IC2读数据	
	.i2c_done	(i2c_done),		// IC2一次操作完成
	.i2c_ack	(i2c_ack),		// IC2应答标志
	// user interface
	.rw_done	(rw_done),		// E2PROM读写测试完成
	.rw_result	(rw_result)		// E2PROM读写测试结果 0:失败  1:成功	
	);

// I2C驱动模块
i2c_dri #(
	.SLAVE_ADDR	(SLAVE_ADDR),	// EEPROM芯片从机地址
	.CLK_FREQ	(CLK_FREQ),		// 模块输入时钟频率
	.I2C_FREQ	(I2C_FREQ)		// IIC_SCL的时钟频率
) u_i2c_dri(
	.clk		(sys_clk),	
	.rst_n		(sys_rst_n),
	// I2C interface
	.i2c_exec	(i2c_exec),		// I2C执行触发信号
	.bit_ctrl	(BIT_CTRL),		// 器件地址位(16b\8b)选择
	.i2c_rh_wl	(i2c_rh_wl),	// I2C读写控制
	.i2c_addr	(i2c_addr),		// I2C内地址
	.i2c_data_w	(i2c_data_w),	// I2C写数据
	.i2c_data_r	(i2c_data_r),	// I2C读数据
	.i2c_done	(i2c_done),		// I2C一次操作完成标志
	.i2c_ack	(i2c_ack),		// I2C应答标志
	.scl		(iic_scl),		// I2C的SCL时钟信号
	.sda		(iic_sda),		// I2C的SDA信号
	// user interface
	.dri_clk	(dri_clk)		// I2C操作时钟
	);
	
// led指示模块
rw_result_led #(
	.L_TIME(L_TIME  )   		// 控制led闪烁时间
) u_rw_result_led(
    .clk         (dri_clk   ),  
    .rst_n       (sys_rst_n ), 
    
    .rw_done     (rw_done   ),  
    .rw_result   (rw_result ),
    .led         (led       )    
	);
	
endmodule

在顶层模块中,主要是对其他模块的引用,以及模块与模块之间参数的连接。在这一模块中所有的参数基本都是内部连接线(wire),该定义指的是数值的传递是实时的,在一个模块中内部连接线上数值的变换会立即传递到另一个模块中,不会存在延迟,而寄存器(reg)变量根据条件可能会产生延迟。

在顶层模块中,我们需要着重关注的参数是 i 2 c _ e x e c i2c\_exec i2c_exec i 2 c _ d o n e i2c\_done i2c_done d r i _ c l k dri\_clk dri_clk,第一个参数是读写操作开始的标志,第二个参数是读写操作中传输一字节信息完成的标志,第三个操作是读写操作的执行时钟,这同FPGA自身的时钟是不同的。这三个参数,我们在后续会重新强调。

在读写操作开始前,我们首先看一下驱动模块 i 2 c _ d r i i2c\_dri i2c_dri

module i2c_dri #(
	parameter SLAVE_ADDR = 7'b1010_000,		// 从机地址
	parameter CLK_FREQ   = 26'd50_000_000,	// 输入时钟频率
	parameter I2C_FREQ	 = 18'd250_000		// 时钟频率
)
(
	input 				clk,
	input 				rst_n,
	// I2C interface
	input 				i2c_exec,		// I2C触发执行信号
	input 				bit_ctrl,		// 字地址位控制(16b/8b)
	input 				i2c_rh_wl,		// I2C读写控制信号
	input 		[15:0] 	i2c_addr,		// I2C器件内地址
	input 		[ 7:0] 	i2c_data_w,		// I2C写数据
	output reg  [ 7:0] 	i2c_data_r,		// I2C读数据
	output reg 		  	i2c_done,		// I2C一次操作完成
	output reg 		  	i2c_ack,		// I2C应答标志 0:应答;1:未应答	
	output reg 		  	scl,			// I2C的SCL时钟信号,单方面输出I2C控制时钟
	inout 			  	sda,			// I2C的SDA信号
	// user interface
	output reg 		  	dri_clk			// 驱动I2C的驱动时钟

	);
	


//localparam define
localparam  st_idle     = 8'b0000_0001; // 空闲状态
localparam  st_sladdr   = 8'b0000_0010; // 发送器件地址(slave address)
localparam  st_addr16   = 8'b0000_0100; // 发送16位字地址
localparam  st_addr8    = 8'b0000_1000; // 发送8位字地址
localparam  st_data_wr  = 8'b0001_0000; // 写数据(8 bit)
localparam  st_addr_rd  = 8'b0010_0000; // 发送器件地址读
localparam  st_data_rd  = 8'b0100_0000; // 读数据(8 bit)
localparam  st_stop     = 8'b1000_0000; // 结束I2C操作 

//reg define
reg            sda_dir   ; 				// I2C数据(SDA)方向控制
reg            sda_out   ; 				// SDA输出信号
reg            st_done   ; 				// 状态结束
reg            wr_flag   ; 				// 写标志
reg    [ 6:0]  cnt       ; 				// 计数
reg    [ 7:0]  cur_state ; 				// 状态机当前状态
reg    [ 7:0]  next_state; 				// 状态机下一状态
reg    [15:0]  addr_t    ; 				// 地址
reg    [ 7:0]  data_r    ; 				// 读取的数据
reg    [ 7:0]  data_wr_t ; 				// I2C需写的数据的临时寄存
reg    [ 9:0]  clk_cnt   ; 				// 分频时钟计数

//wire define
wire           sda_in     ; 			// SDA输入信号
wire   [8:0]   clk_divide ; 			// 模块驱动时钟的分频系数
	
//*****************************************************
//**                    main code
//*****************************************************

// SDA控制
assign sda = sda_dir ? sda_out : 1'bz;				// SDA输出高阻
assign sda_in = sda;								// SDA数据输入
assign clk_divide = (CLK_FREQ/I2C_FREQ) >> 2'd2 ;  	// 模块驱动时钟的分频系数 200=1100_1000; >>2=0_0011_0010=50
// clk 50Mhz每计数200可以得到250khz(4us)的信号
// 想要得到1Mhz(1us)的信号频率需要计数50

// 生成I2C的SCL的四倍频率的驱动时钟用于驱动I2C的操作
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        dri_clk <=  1'b0;
        clk_cnt <= 10'd0;
    end
    else if(clk_cnt == (clk_divide[8:1] - 9'd1)) begin
        clk_cnt <= 10'd0;
        dri_clk <= ~dri_clk;
    end
    else
        clk_cnt <= clk_cnt + 10'b1;
end

// (三段式状态机)同步时序描述状态转移
// 1us进入一次
always @(posedge dri_clk or negedge rst_n) begin
    if(!rst_n)
        cur_state <= st_idle;
    else
        cur_state <= next_state;
end

// 组合逻辑判断状态转移条件
// 敏感变量有cur_state、i2c_exec、st_done
// 包括等号右边的量;if()case()内的量
always @(*) begin
    next_state = st_idle;						// 避免干扰
    case(cur_state)
        st_idle: begin                          // 空闲状态
           if(i2c_exec) begin					// 启动工作标志位
               next_state = st_sladdr;			// 等待器件地址写入	
           end
           else
               next_state = st_idle;			// 持续空闲
        end
        st_sladdr: begin
            if(st_done) begin					// 等待器件地址写入完成
                if(bit_ctrl)                    // 判断是16位还是8位字地址
                   next_state = st_addr16;
                else
                   next_state = st_addr8 ;
            end
            else
                next_state = st_sladdr;
        end
        st_addr16: begin                        // 写16位字地址
            if(st_done) begin
                next_state = st_addr8;
            end
            else begin
                next_state = st_addr16;
            end
        end
        st_addr8: begin                         // 8位字地址
            if(st_done) begin
                if(wr_flag==1'b0)               // 读写判断
                    next_state = st_data_wr;
                else
                    next_state = st_addr_rd;
            end
            else begin
                next_state = st_addr8;
            end
        end
        st_data_wr: begin                       // 写数据(8 bit)
            if(st_done)
                next_state = st_stop;
            else
                next_state = st_data_wr;
        end
        st_addr_rd: begin                       // 写地址以进行读数据
            if(st_done) begin
                next_state = st_data_rd;
            end
            else begin
                next_state = st_addr_rd;
            end
        end
        st_data_rd: begin                       // 读取数据(8 bit)
            if(st_done)
                next_state = st_stop;
            else
                next_state = st_data_rd;
        end
        st_stop: begin                          // 结束I2C操作
            if(st_done)
                next_state = st_idle;
            else
                next_state = st_stop ;
        end
        default: next_state = st_idle;			// 零状态改为空闲状态
    endcase
end

// 时序电路描述状态输出
// 1us进入一次
always @(posedge dri_clk or negedge rst_n) begin
    //复位初始化
    if(!rst_n) begin
        scl       <= 1'b1;						// SCL初始为高电平
        sda_out   <= 1'b1;						// SDA初始为高电平	
        sda_dir   <= 1'b1;                      // 表示是否是主机控制,0是从机   
        i2c_done  <= 1'b0;                          
        i2c_ack   <= 1'b0;                          
        cnt       <= 7'b0;                          
        st_done   <= 1'b0;                          
        data_r    <= 8'b0;                          
        i2c_data_r<= 8'b0;                          
        wr_flag   <= 1'b0;                          
        addr_t    <= 16'b0;                          
        data_wr_t <= 8'b0;                          
    end                                              
    else begin                                       
        st_done <= 1'b0 ;                            
        cnt     <= cnt +7'b1 ;                       
        case(cur_state)                              
             st_idle: begin                          // 空闲状态
                scl     <= 1'b1;                     
                sda_out <= 1'b1;                     
                sda_dir <= 1'b1;                     
                i2c_done<= 1'b0;                     
                cnt     <= 7'b0;  
				// i2c_exec 置1后启动读或写操作
				// 该操作在器件判断地址的周期后
                if(i2c_exec) begin                   
                    wr_flag   <= i2c_rh_wl ;         // 读写状态 		
                    addr_t    <= i2c_addr  ;     	 // 器件内地址    
                    data_wr_t <= i2c_data_w;  		 // 写数据
                    i2c_ack   <= 1'b0;               // 应答标志        	
                end                                  
            end                                      
            st_sladdr: begin                         // 写地址(器件地址和字地址)
                case(cnt)                            
                    7'd1 : sda_out <= 1'b0;          // 开始I2C,发送起始信号
                    7'd3 : scl <= 1'b0;              // SCL拉低,SDA改变电平
                    7'd4 : sda_out <= SLAVE_ADDR[6]; // 传送器件地址
                    7'd5 : scl <= 1'b1;        		 // SCL拉高,SDA稳定不变      
                    7'd7 : scl <= 1'b0;              // SCL维持两个周期后拉低
                    7'd8 : sda_out <= SLAVE_ADDR[5]; 
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= SLAVE_ADDR[4]; 
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= SLAVE_ADDR[3]; 
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= SLAVE_ADDR[2]; 
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= SLAVE_ADDR[1]; 
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= SLAVE_ADDR[0]; // 发送完器件地址
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: sda_out <= 1'b0;          // 0:写
                    7'd33: scl <= 1'b1;              
                    7'd35: scl <= 1'b0;              
                    7'd36: begin                     
                        sda_dir <= 1'b0;             // 高阻态,SDA给从机控制
                        sda_out <= 1'b1;             // 未应答情况下为高电平            
                    end                              
                    7'd37: scl  <= 1'b1;             
                    7'd38: begin                     // 从机应答在SCL为高电平
                        st_done <= 1'b1;			 // 操作完成
                        if(sda_in == 1'b1)           // 高电平表示未应答
                            i2c_ack <= 1'b1;         // 拉高应答标志位     
                    end                                          
                    7'd39: begin                     
                        scl <= 1'b0;                 // 应答后拉低等待后续信号
                        cnt <= 7'b0;   				 // 计数值归零              
                    end                              
                    default :  ;                     
                endcase                              
            end                                      
            st_addr16: begin                         // 写入高八位地址
                case(cnt)                            
                    7'd0 : begin                     
                        sda_dir <= 1'b1 ;            // 拉高后重回主机控制
                        sda_out <= addr_t[15];       // 传送字地址
                    end                              
                    7'd1 : scl <= 1'b1;              
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= addr_t[14];    
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= addr_t[13];    
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= addr_t[12];    
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= addr_t[11];    
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= addr_t[10];    
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= addr_t[9];     
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= addr_t[8];     
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: begin                     
                        sda_dir <= 1'b0;             // 从机控制  
                        sda_out <= 1'b1;   			 // 未应答情况下为高电平
                    end                              
                    7'd33: scl  <= 1'b1;             
                    7'd34: begin                     // 从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           // 高电平表示未应答
                            i2c_ack <= 1'b1;         // 拉高应答标志位    
                    end        
                    7'd35: begin                     
                        scl <= 1'b0;                 
                        cnt <= 7'b0;                 
                    end                              
                    default :  ;                     
                endcase                              
            end                                      
            st_addr8: begin                          
                case(cnt)                            
                    7'd0: begin                      
                       sda_dir <= 1'b1 ;       		 // 主机控制      
                       sda_out <= addr_t[7];         // 字地址
                    end                              
                    7'd1 : scl <= 1'b1;              
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= addr_t[6];     
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= addr_t[5];     
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= addr_t[4];     
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= addr_t[3];     
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= addr_t[2];     
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= addr_t[1];     
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= addr_t[0];     
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: begin                     
                        sda_dir <= 1'b0;         
                        sda_out <= 1'b1;                    
                    end                              
                    7'd33: scl     <= 1'b1;          
                    7'd34: begin                     // 从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           // 高电平表示未应答
                            i2c_ack <= 1'b1;         // 拉高应答标志位    
                    end   
                    7'd35: begin                     
                        scl <= 1'b0;                 
                        cnt <= 7'b0;                 
                    end                              
                    default :  ;                     
                endcase                              
            end                                      
            st_data_wr: begin                        // 写数据(8 bit)
                case(cnt)                            
                    7'd0: begin                      
                        sda_dir <= 1'b1;
                        sda_out <= data_wr_t[7];     // I2C写8位数据
                    end                               
                    7'd1 : scl <= 1'b1;              
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= data_wr_t[6];  
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= data_wr_t[5];  
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= data_wr_t[4];  
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= data_wr_t[3];  
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= data_wr_t[2];  
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= data_wr_t[1];  
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= data_wr_t[0];  
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: begin                     
                        sda_dir <= 1'b0;           
                        sda_out <= 1'b1;                              
                    end                              
                    7'd33: scl <= 1'b1;              
                    7'd34: begin                     // 从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           // 高电平表示未应答
                            i2c_ack <= 1'b1;         // 拉高应答标志位    
                    end          
                    7'd35: begin                     
                        scl  <= 1'b0;                
                        cnt  <= 7'b0;                
                    end                              
                    default  :  ;                    
                endcase                              
            end                                      
            st_addr_rd: begin                        // 写地址以进行读数据
                case(cnt)                            
                    7'd0 : begin                     
                        sda_dir <= 1'b1;             
                        sda_out <= 1'b1;             
                    end                              
                    7'd1 : scl <= 1'b1;              
                    7'd2 : sda_out <= 1'b0;          // 重新开始
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= SLAVE_ADDR[6]; // 传送器件地址
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= SLAVE_ADDR[5]; 
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= SLAVE_ADDR[4]; 
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= SLAVE_ADDR[3]; 
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= SLAVE_ADDR[2]; 
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= SLAVE_ADDR[1]; 
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= SLAVE_ADDR[0]; 
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: sda_out <= 1'b1;          // 1:读
                    7'd33: scl <= 1'b1;              
                    7'd35: scl <= 1'b0;              
                    7'd36: begin                     
                        sda_dir <= 1'b0;       		 // 从机控制     
                        sda_out <= 1'b1;             // 默认未应答        	
                    end
                    7'd37: scl     <= 1'b1;
                    7'd38: begin                     // 从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           // 高电平表示未应答
                            i2c_ack <= 1'b1;         // 拉高应答标志位    
                    end   
                    7'd39: begin
                        scl <= 1'b0;
                        cnt <= 7'b0;
                    end
                    default : ;
                endcase
            end
            st_data_rd: begin                        // 读取数据(8 bit)
                case(cnt)
                    7'd0: sda_dir <= 1'b0;
                    7'd1: begin
                        data_r[7] <= sda_in;
                        scl       <= 1'b1;
                    end
                    7'd3: scl  <= 1'b0;
                    7'd5: begin
                        data_r[6] <= sda_in ;
                        scl       <= 1'b1   ;
                    end
                    7'd7: scl  <= 1'b0;
                    7'd9: begin
                        data_r[5] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd11: scl  <= 1'b0;
                    7'd13: begin
                        data_r[4] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd15: scl  <= 1'b0;
                    7'd17: begin
                        data_r[3] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd19: scl  <= 1'b0;
                    7'd21: begin
                        data_r[2] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd23: scl  <= 1'b0;
                    7'd25: begin
                        data_r[1] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd27: scl  <= 1'b0;
                    7'd29: begin
                        data_r[0] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd31: scl  <= 1'b0;
                    7'd32: begin
                        sda_dir <= 1'b1;             
                        sda_out <= 1'b1;
                    end
                    7'd33: scl     <= 1'b1;
                    7'd34: st_done <= 1'b1;          // 非应答
                    7'd35: begin
                        scl <= 1'b0;
                        cnt <= 7'b0;
                        i2c_data_r <= data_r;
                    end
                    default  :  ;
                endcase
            end
            st_stop: begin                           // 结束I2C操作
                case(cnt)
                    7'd0: begin
                        sda_dir <= 1'b1;             // 结束I2C,主机控制
                        sda_out <= 1'b0;			 // 结束前拉低
                    end
                    7'd1 : scl     <= 1'b1;
                    7'd3 : sda_out <= 1'b1;			 // 结束拉高
                    7'd15: st_done <= 1'b1;
                    7'd16: begin
                        cnt      <= 7'b0;
                        i2c_done <= 1'b1;            // 向上层模块传递I2C结束信号
                    end
                    default  : ;
                endcase
            end
        endcase
    end
end
	
endmodule

驱动模块的作用有:

  • 产生驱动时钟:这一时钟的驱动周期为1 u s us us,主要用于AT24C64的读写。
  • 状态转移:在上一个单字节读写操作快要完成时,提前转移到下一个操作的状态中。
  • 状态转移条件:在敏感变量发送变化的时候(例如:操作开始标志位、操作结束标志位等),立刻将下一个状态寄存在 n e x t _ s t a t e next\_state next_state 变量中。
  • 状态输出:根据当前的状态,进行在当前状态下所需要做的操作,并在输出数据的同时置位关键的标志位。

在读写测试模块中,主要进行的操作是控制写入地址及写入数据,并延迟5 m s ms ms,等待写入操作的完成。

module e2prom_rw(
	input 				dri_clk,			// I2C操作时钟信号
	input 				rst_n,				// 复位信号
	// I2C interface
	output reg			i2c_rh_wl,			// I2C读写控制信号
	output reg 			i2c_exec,			// I2C触发执行信号
	output reg [15:0]   i2c_addr,			// I2C内地址
	output reg [ 7:0] 	i2c_data_w,			// I2C写数据
	input	   [ 7:0]	i2c_data_r,			// I2C读数据
	input 				i2c_done,			// I2C一次操作完成
	input 				i2c_ack,			// I2C应答标志
	// user interface
	output reg			rw_done, 			// E2PROM读写测试完成
	output reg 			rw_result			// E2PROM读写测试结果 0:失败  1:成功	
    );

// parameter define
// EEPROM写数据需要间隔时间;读数据不需要
parameter		WR_WAIT_TIME = 14'd5000;	// 写入间隔时间 5ms <= 10ms
parameter		MAX_BYTE	 = 16'd256;		// 读写测试的字节个数
	
// reg define
reg  [1:0]	flow_cnt;						// 状态流控制
reg  [13:0] wait_cnt;						// 延时计数器

//*****************************************************
//**                    main code
//*****************************************************

// EEPROM读写测试,先写后读,并比较结果是否一致
// 1us进入一次
always @ (posedge dri_clk or negedge rst_n) begin
	if(!rst_n) begin
		flow_cnt    <= 2'b0;
		i2c_rh_wl 	<= 1'b0;				// 高电平表示读,低电平表示写
		i2c_exec 	<= 1'b0;
		i2c_addr 	<= 16'b0;
		i2c_data_w 	<= 8'b0;
		wait_cnt 	<= 14'b0;
		rw_done 	<= 1'b0;
		rw_result 	<= 1'b0;
	end
	else begin
		i2c_exec <= 1'b0;
		rw_done	 <= 1'b0;
		// $display("%d", flow_cnt);
		case(flow_cnt)
			2'd0: begin
				wait_cnt <= wait_cnt + 14'b1;	// 延时计数
				// 每5ms进入一次,改变写入地址和写入数据
				// 只对写数据做时间要求,对读数据不做要求
				if(wait_cnt == (WR_WAIT_TIME - 14'b1)) begin
					wait_cnt <= 14'b0;
					// EEPROM写入达到最大位
					if(i2c_addr == MAX_BYTE) begin
						i2c_addr  <= 16'b0;		// 读地址索引归零
						i2c_rh_wl <= 1'b1;		// 开始读
						flow_cnt  <= 2'd2;		// 读模式切换
					end
					// 写操作
					else begin
						flow_cnt <= flow_cnt + 2'b1;	// 写状态切换
						i2c_exec <= 1'b1;				// 拉高点平
					end
				end
			end
			2'd1: begin
				// 单次写入完成
				if(i2c_done == 1'b1) begin
					flow_cnt   <= 2'd0;
					i2c_addr   <= i2c_addr + 16'b1;		// 地址增加
					i2c_data_w <= i2c_data_w + 8'b1;	// 写入数据增加	
				end
			end
			2'd2: begin
				flow_cnt <= flow_cnt + 2'b1;
				i2c_exec <= 1'b1;
			end
			2'd3: begin
				// 单次读出完成
				if(i2c_done == 1'b1) begin
					// 读出值错误或者I2C未应答
					// 地址和读出数据需要相同是因为写入的时候是相同的
					if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
						rw_done   <= 1'b1;
						rw_result <= 1'b0;
					end
					// 读出为最大地址
					else if(i2c_addr == (MAX_BYTE - 16'b1)) begin
						rw_done   <= 1'b1;
						rw_result <= 1'b1;
					end
					// 读出为正常地址,继续读出后续地址
					else begin
						flow_cnt <= 2'd2;
						i2c_addr <= i2c_addr + 16'b1;
					end
				end
			end
			default:;
		endcase
	end
	
end	
	
endmodule

LED模块主要用于判断读写操作能否正确进行。

module rw_result_led (
    input        clk       ,  // 时钟信号
    input        rst_n     ,  // 复位信号
                 
    input        rw_done   ,  // 错误标志
    input        rw_result ,  // E2PROM读写测试完成
    output  reg  led          // E2PROM读写测试结果 0:失败 1:成功
);

// parameter define
parameter L_TIME = 17'd125_000;

//reg define
reg          rw_done_flag;    // 读写测试完成标志
reg  [16:0]  led_cnt     ;    // led计数

//*****************************************************
//**                    main code
//*****************************************************

// 读写测试完成标志
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        rw_done_flag <= 1'b0;
    else if(rw_done)
        rw_done_flag <= 1'b1;
end        

// 错误标志为1时PL_LED0闪烁,否则PL_LED0常亮
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        led_cnt <= 17'd0;
        led <= 1'b0;
    end
    else begin
        if(rw_done_flag) begin
            if(rw_result)                          // 读写测试正确
                led <= 1'b1;                       // led灯常亮
            else begin                             // 读写测试错误
                led_cnt <= led_cnt + 17'd1;
                if(led_cnt == (L_TIME - 17'b1)) begin
                    led_cnt <= 17'd0;
                    led <= ~led;                   // led灯闪烁
                end
                else
                    led <= led;
            end
        end
        else
            led <= 1'b0;                           // 读写测试完成之前,led灯熄灭
    end    
end

endmodule

3.2 仿真分析

我们以写操作为例进行分析,读操作请读者自行分析。

50ms时i2c_exec置1,开始写操作。后续操作中e2prom_rw.v都在2’d1状态中循环等待。1us后i2c_exec在e2prom_rw.v中置0,同时读写状态、内地址、写数据、应答标志被寄存。

FPGA-EEPROM读写记录_第9张图片

1us后i2c_dri.v中的cur_state状态被切换为st_sladdr,在过1us后sda_out信号拉低,向从机发送起始信号,此时SCL的电平为高电平,从机接收到下降沿后判断SCL状态,为高电平则认为是有效的起始信号,并将State的值+1。

FPGA-EEPROM读写记录_第10张图片

之后SCL拉低,使得主机能够改变SDA的状态,并将地址输入,最高位地址为高电平产生上升沿。从机在检验到上升沿后判断SCL的状态,发现是低电平证明主机在向SDA写数据,在判断State的状态,进入read_in任务中,用于后续将主机写在SDA上的数据读入。

FPGA-EEPROM读写记录_第11张图片

主机将数据写入完毕后,SCL的电平拉高,从机在检验到SCL的上升沿后将SDA上的数据读入。依次读入8个数据后,SCL保持在高电平,SDA保持在需要写入的最后一位电平状态。

FPGA-EEPROM读写记录_第12张图片

之后主机将SCL电平拉低,并在1us后释放SDA的占用。从机在检验到SCL的下降沿时,经过1.25us的延时抢占SDA的控制权,并输出有效的从机应答0。
注意:在主机释放SCL时,由于SCL在正常情况下是上拉电阻所以出现0.25us的高电平。

FPGA-EEPROM读写记录_第13张图片

在从机发送完有效应答后,在主机释放SDA的控制权的1us后,主机将SCL置1,要求SDA保持稳定状态,此时SDA的控制权还在从机上,从机发送在SDA上的应答低电平稳定。在经过1us后,主机将st_done即操作完成标志位置1,并判断SDA上的信号是否为应答信号。在经过1us,主机将SCL的电平拉低,此时从机检测到SCL的下降沿,经过1us后释放SDA,此时SDA不受两者控制,即图中对应位置,同时由于上一个1us中st_done的置位所以cur_state的状态转化为写入高8位字地址。也就是说在下一个周期到来时,已经进入了其他的case模块中,之间没有间隔时间。

FPGA-EEPROM读写记录_第14张图片

之后是高八位和低八位字地址的写入,在此不在赘述。
需要注意的是在发送低八位字地址时,由于最后一位是1,因此在第八个上升沿到来之前,SDA就被主机置位为1,在上升沿到来时被传入从机。2us后也就是在该上升沿之后的第一个下降沿,主机在该下降沿的1us后释放SDA的控制,从机在检验到下降沿后经过1.25us的延时控制SDA,从而使得SDA的电平变为有效低电平。

FPGA-EEPROM读写记录_第15张图片
主机释放SDA的1us后将SCL的电平调高,在过1us读取SDA的有效应答状态。在过1us将SCL电平拉低,使其出现下降沿并将状态变量cur_state进行调整,从机检测到后,经过1us的延时退出SDA的控制。

FPGA-EEPROM读写记录_第16张图片

从机释放SDA的控制后,经过器件地址的判断,在将State的值变更为2’b10,并进入write_to_eeprom任务中。主机此时正在控制SDA,并将第一个数据发送到SDA上。在write_to_eeprom中,从机也是先读入1个字节,其中最低位的数值为1。

FPGA-EEPROM读写记录_第17张图片

在最后一个上升沿,经过2us后将SCL拉低。此时从机检测到第一个下降沿后,经过1.25us开始占用SDA发送应答信号,主机则在下降沿后经过1us释放SDA。

FPGA-EEPROM读写记录_第18张图片

在经过1us,主机在将SCL拉高,用于读取SDA上的应答信号。在经过2us后,在将SCL拉低,经过1us后从机释放SDA,退出task。并将数据写入对应地址位后,将State的状态置0。

FPGA-EEPROM读写记录_第19张图片

至此,读操作结束。具体代码请查看正点原子官网关于FPGA的资源包。

小结

本文介绍了AT24C64的引脚功能,并分析了如何对其进行读写操作,需要额外注意的是起始操作和终止操作的条件。最后,我们在Vivado中编写了读写操作的代码,对其进行仿真的同时,分析了仿真波形的结果。

你可能感兴趣的:(FPGA,fpga开发)