18 EEPROM读写

EEPROM 简介

EEPROM (Electrically Erasable Progammable Read Only Memory,E2PROM)即电可擦除可编程只读存储器,是一种常用的非易失性存储器(掉电数据不丢失),EEPROM 有多种类型的产品,此次实验使用的是ATMEL 公司生产的 AT24C 系列的 AT24C64 这一型号。AT24C64 具有高可靠性,可对所存数据保存 100 年,并可多次擦写,擦写次数达一百万次。
AT24C64 采用两线串行接口的双向数据传输协议——I2C 协议实现读写操作,其存储容量为 64Kbit,内部分成 256 页,每页 32 字节,共有 8192 个字节,且其读写操作都是以字节为基本单位,写入的数据是先写入到缓存中,需要等一段时间才会同步到ROM中(可以通过发设备地址来确认同步是否完成,完成后会响应ACK,否则响应NAK),对于 AT24C64 其缓存大小为 32B ,因此一次可写入的最大长度为 32 字节。

I2C 协议解析

I2C 即 Inter-Integrated Circuit(集成电路总线),是由 Philips 半导体公司(现在的 NXP 半导体公司)在八十年代初设计出来的一种简单、双向、二线制总线标准,多用于主机和从机在数据量不大且传输距离短的场合下的主从通信。
在物理层上,I2C 接口只需要两条总线线路,即 SCL(串行时钟线)、SDA(串行数据线),I2C 总线是半双工的,所以任意时刻只能有一个主机。每个连接在总线上的 I2C 器件都有一个唯一的器件地址,在通信的时候就是靠这个地址来通信的。I2C 传输速率标准模式下可以达到 100kb/s,快速模式下可以达到 400kb/s,高速模式下可达 3.4Mbit/s。总线上的主设备与从设备之间以字节(8 位)为单位进行双向的数据传输。
I2C 总线上的每一个设备都可以作为主设备或者从设备,但同一时刻最多只能有一个主设备,且每一个从设备都会对应一个唯一的地址(可以从 I2C 器件数据手册得知),主从设备之间就是通过这个地址来确定与哪个器件进行通信。通常情况下,主从器件的角色是确定的,也就是说从机一直工作在从机模式,如下是常见的 I2C总线物理拓扑结构图。
在这里插入图片描述

I2C 协议规定

  1. 在时钟(SCL)为高电平的时候,数据总线(SDA)必须保持稳定,所以数据总线(SDA)在时钟(SCL)为低电平的时候才能改变。
    在这里插入图片描述
  2. 在时钟(SCL)为高电平的时候,数据总线(SDA)由高到低的跳变为总线起始信号
  3. 在时钟(SCL)为高电平的时候,数据总线(SDA)由低到高的跳变为总线停止信号
    在这里插入图片描述
  4. 当传输器件将 8 位数据输出完后,会将数据总线(SDA)释放(即设置为输入),然后等待接收器件应答(低电平 0 表示应答,1 表示非应答),此时的时钟仍然是主机提供的。
    在这里插入图片描述

I2C 器件地址

每个 I2C 器件都有一个器件地址,有些 I2C 器件的器件地址是固定的,而有些 I2C 器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到 I2C 总线上,例如 EEPROM 器件,为了增加系统的 EEPROM 容量,可能需要多个 EEPROM。器件可编程地址位一般由它管脚决定,比如 EEPROM 器件一般会留下 3 个管脚用于设置可编程地址位。但有些 I2C 器件在出厂时器件地址就设置好了,用户不可以更改(如实时时钟 PCF8563 的器件地址为固定的 7’h51)。
对于 AT24C64 而言,其器件地址为 1010 加 3 位的可编程地址,3 位可编程地址由器件上的 3 个管脚A2、A1、A0(见图 31.2.2)的硬件连接决定。当硬件电路上分别将这三个管脚连接到 GND 或 VCC 时,就可以设置不同的可编程地址。
进行数据传输时,主机首先向总线上发出开始信号,对应开始位 S,然后按照从高到低的位序发送器件地址,一般为 7bit,第 8bit 位为读写控制位 R/W,该位为 0 时表示主机对从机进行写操作,当该位为 1时表示主机对从机进行读操作,然后接收从机响应。对于 AT24C64 来说,其传输器件地址格式如下图所示。
在这里插入图片描述

I2C 存储器地址

一般而言,每个兼容 I2C 协议的器件,内部总会有可供读写的寄存器或存储器,例如,EEPROM 存储器,内部就是顺序编址的一系列存储单元;型号为 OV7670 的 CMOS 摄像头(OV7670 的该接口叫 SCCB 接口,其实质也是一种特殊的 I2C 协议,可以直接兼容 I2C 协议),其内部就是一系列编址的可供读写的寄存器。因此,要对一个 I2C 器件中的存储单元(包括寄存器和存储器)进行读写时,必须要先指定存储单元的地址。该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定,当存储单元数量不超过一个字节所能表示的最大数量(256)时,用一个字节表示,超过一个字节所能表示的最大数量时,就需要用两个字节来表示,例如同是 E2PROM 存储器,AT24C02 的存储单元容量为 2Kbit=256Byte,用一个字节地址即可寻址所有的存储单元,而 AT24C64 的存储单元容量为64Kb=8KB,需要 13 位的地址位,而 I2C 又是以字节为单位进行传输的,所以需要用两个字节地址来寻址整个存储单元。

I2C 单字节写时序

下图分别是 1 字节存储器地址器件和 2 字节存储器地址器件单字节写时序图
在这里插入图片描述
单字节存储器地址写单字节数据过程:

  1. 主机设置 SDA 为输出;
  2. 主机发起起始信号;
  3. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  4. 主机设置 SDA 为三态门输入,读取从机应答信号;
  5. 读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
  6. 主机设置 SDA 为三态门输入,读取从机应答信号;
  7. 读取应答信号成功,主机设置 SDA 为输出,传输待写入的数据;
  8. 设置 SDA 为三态门输入,读取从机应答信号;
  9. 读取应答信号成功,主机产生 STOP 位,终止传输。
    双字节存储器地址写单字节数据过程:
  10. 主机设置 SDA 为输出;
  11. 主机发起起始信号;
  12. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  13. 主机设置 SDA 为三态门输入,读取从机应答信号;
  14. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据高字节;
  15. 主机设置 SDA 为三态门输入,读取从机应答信号;
  16. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据低字节;
  17. 设置 SDA 为三态门输入,读取从机应答信号;
  18. 读取应答信号成功,主机设置 SDA 为输出,传输待写入的数据;
  19. 设置 SDA 为三态门输入,读取从机应答信号;
  20. 读取应答信号成功,主机产生 STOP 位,终止传输。

I2C 连续写时序

注意:
I2C 连续写时序仅部分器件支持

连续写是主机连续写多个字节数据到从机,这个和单字节写操作类似,连续多字节写操作也是分为 1 字节存储器地址器件和 2 字节存储器地址器件的写操作,下图分别是 1 字节存储器地址器件和 2 字节存储器地址器件的连续写时序图
在这里插入图片描述
单字节存储器地址器件连续多字节写数据过程:

  1. 主机设置 SDA 为输出;
  2. 主机发起起始信号;
  3. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  4. 主机设置 SDA 为三态门输入,读取从机应答信号;
  5. 读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
  6. 主机设置 SDA 为三态门输入,读取从机应答信号;
  7. 读取应答信号成功,主机设置 SDA 为输出,传输待写入的第 1 个数据;
  8. 设置 SDA 为三态门输入,读取从机应答信号;
  9. 读取应答信号成功后,主机设置 SDA 为输出,传输待写入的下一个数据;
  10. 设置 SDA 为三态门输入,读取从机应答信号;n 个数据被写完,转到步骤 11,若数据未被写完,转到步骤 9;
  11. 读取应答信号成功,主机产生 STOP 位,终止传输。
    双字节存储器地址器件连续多字节写数据过程:
  12. 主机设置 SDA 为输出;
  13. 主机发起起始信号;
  14. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  15. 主机设置 SDA 为三态门输入,读取从机应答信号;
  16. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据高字节;
  17. 主机设置 SDA 为三态门输入,读取从机应答信号;
  18. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据低字节;
  19. 设置 SDA 为三态门输入,读取从机应答信号;
  20. 读取应答信号成功,主机设置 SDA 为输出,传输待写入的第 1 个数据;
  21. 设置 SDA 为三态门输入,读取从机应答信号;
  22. 读取应答信号成功后,主机设置 SDA 为输出,传输待写入的下一个数据;
  23. 设置 SDA 为三态门输入,读取从机应答信号;n 个数据被写完,转到步骤 13,若数据未被写完,转到步骤 11;
  24. 读取应答信号成功,主机产生 STOP 位,终止传输。

I2C 单字节读时序

单字节读操作分为 1 字节存储器地址器件单字节数据读操作和 2 字节存储器地址器件单字节数据读操作。下图分别为不同情况的时序图
在这里插入图片描述
单字节存储器地址读取单字节数据过程:

  1. 主机设置 SDA 为输出;
  2. 主机发起起始信号;
  3. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  4. 主机设置 SDA 为三态门输入,读取从机应答信号;
  5. 读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
  6. 主机设置 SDA 为三态门输入,读取从机应答信号;
  7. 读取应答信号成功,主机发起起始信号;
  8. 主机传输器件地址字节,其中最低位为 1,表明为读操作;
  9. 设置 SDA 为三态门输入,读取从机应答信号;
  10. 读取应答信号成功,主机设置 SDA 为三态门输入,读取 SDA 总线上的一个字节的数据;
  11. 产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);
  12. 主机产生 STOP 位,终止传输。
    双字节存储器地址读取单字节数据过程:
  13. 主机设置 SDA 为输出;
  14. 主机发起起始信号;
  15. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  16. 主机设置 SDA 为三态门输入,读取从机应答信号;
  17. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据高字节;
  18. 主机设置 SDA 为三态门输入,读取从机应答信号;
  19. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据低字节;
  20. 设置 SDA 为三态门输入,读取从机应答信号;
  21. 读取应答信号成功,主机发起起始信号;
  22. 主机传输器件地址字节,其中最低位为 1,表明为读操作;
  23. 设置 SDA 为三态门输入,读取从机应答信号;
  24. 读取应答信号成功,主机设置 SDA 为三态门输入,读取 SDA 总线上的一个字节的数据;
  25. 主机设置 SDA 输出,产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);
  26. 主机产生 STOP 位,终止传输

I2C 连续读时序

连续读是主机连续从从机读取多个字节数据,这个和单字节读操作类似,连续多字节读操作也是分为 1 字节存储器地址器件和 2 字节存储器地址器件的读操作,下图分别为字节存储器地址器件和 2 字节存储器地址器件连续多字节读的时序图。
在这里插入图片描述
单字节存储器地址器件连续多字节读取数据过程:

  1. 主机设置 SDA 为输出;
  2. 主机发起起始信号;
  3. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  4. 主机设置 SDA 为三态门输入,读取从机应答信号;
  5. 读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
  6. 主机设置 SDA 为三态门输入,读取从机应答信号;
  7. 读取应答信号成功,主机发起起始信号;
  8. 主机传输器件地址字节,其中最低位为 1,表明为读操作;
  9. 设置 SDA 为三态门输入,读取从机应答信号;
  10. 读取应答信号成功,主机设置 SDA 为三态门输入,读取 SDA 总线上的第 1 个字节的数据;
  11. 主机设置 SDA 输出,发送一位应答信号;
  12. 设置 SDA 为三态门输入,读取 SDA 总线上的下一个字节的数据;若 n 个字节数据读完成,跳转到步骤 13,若数据未读完,跳转到步骤 11;
  13. 主机设置 SDA 输出,产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);
  14. 主机产生 STOP 位,终止传输。
    双字节存储器地址器件连续多字节读取数据过程:
  15. 主机设置 SDA 为输出;
  16. 主机发起起始信号;
  17. 主机传输器件地址字节,其中最低位为 0,表明为写操作;
  18. 主机设置 SDA 为三态门输入,读取从机应答信号;
  19. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据高字节;
  20. 主机设置 SDA 为三态门输入,读取从机应答信号;
  21. 读取应答信号成功,主机设置 SDA 为输出,传输地址数据低字节;
  22. 设置 SDA 为三态门输入,读取从机应答信号;
  23. 读取应答信号成功,主机发起起始信号;
  24. 主机传输器件地址字节,其中最低位为 1,表明为读操作;
  25. 设置 SDA 为三态门输入,读取从机应答信号;
  26. 读取应答信号成功,主机设置 SDA 为三态门输入,读取 SDA 总线上的第 1 个字节的数据;
  27. 主机设置 SDA 输出,发送一位应答信号;
  28. 设置 SDA 为三态门输入,读取 SDA 总线上的下一个字节的数据;若 n 个字节数据读完成,跳转到步骤 15,若数据未读完,跳转到步骤 13;
  29. 主机设置 SDA 输出,产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);
  30. 主机产生 STOP 位,终止传输。

I2C 控制器功能拆分

通过前面的读写时序可以发现,每次读写过程都可以由开始、发送(包括读取对方应答)、接收(包括回复对方应答)、停止这 4 过个操作组合而来,因此 I2C 控制器只需要实现这 4 个操作即可,三层模块提供 I2C 控制其提供的这 4 中操作即可完成 I2C 从设备的读写。

EEPROM 原理图

在这里插入图片描述
在这里插入图片描述

代码编写

代码一共分为3个模块,分别是 I2C 驱动模块、EEPROM 驱动模块、EEPROM 读写测试模块,其功能如下:
I2C 驱动模块;提供 I2C 总线收发数据的功能,生成模块可以提供此模块在总线上测试起始和结束信号,并进行数据收发。
EEPROM 驱动模块;基于 I2C驱动模块实现 EEPROM 的一些基本操作,如读、写等。
EEPROM 读写测试模块;利用 EEPROM 驱动模块提供的 EEPROM 基本操作进行读、写测试,测试过程中状态 LED 常灭,测试出错状态 LED闪烁,测试完成状态 LED 常亮。

I2C 驱动模块

module i2c_driver #(
	parameter I2C_CLK_PERIOD = 125				//I2C时钟周期,以系统时钟为参考
)
(
	input sys_rst_n,			//系统复位
	input sys_clk,				//系统时钟

	input [7:0] ctrl_cmd,		//i2c控制命令
	input ctrl_start,			//i2c控制器启动信号
	output ctrl_idle,			//i2c控制器空闲信号
	output reg ctrl_done,		//i2c控制器完成信号

	input [7:0] tx_data,		//需要发送的数据
	output reg tx_ack,			//数据发送完成后从设备返回的应答

	output reg [7:0] rx_data,	//接收到的数据
	input rx_ack,				//数据接收完成后响应给从机的应答

	output reg i2c_scl,			//i2c时钟输出
	output reg i2c_sda_o,		//i2c数据输出
	input i2c_sda_i,			//i2c数据输入
	output reg i2c_sda_t		//i2c数据方向控制,1输入,0输出
);

//时钟周期的一半
localparam I2C_CLK_PERIOD_HALF = I2C_CLK_PERIOD / 2;

//状态机的状态
localparam IDLE_STATE = 8'b0000001;			//空闲状态
localparam GEN_STA_STATE = 8'b0000010;		//产生起始信号
localparam WR_DATA_STATE = 8'b0000100;		//写数据状态,写完8bit后还要读ACK
localparam RD_DATA_STATE = 8'b0001000;		//读数据状态,读完8bit后还要生成ACK
localparam GEN_STO_STATE = 8'b0010000;		//产生停止信号

//指令集
localparam GEN_STA_CMD = 7'b0000010;		//产生起始信号
localparam WR_DATA_CMD = 7'b0000100;		//写数据状态,写完8bit后还要读ACK
localparam RD_DATA_CMD = 7'b0001000;		//读数据状态,读完8bit后还要生成ACK
localparam GEN_STO_CMD = 7'b0010000;		//产生停止信号

//I2C时钟周期计数器
reg [32:0] clk_period_count;

//传输bit计数
reg [7:0] bit_cnt;

//发送移位寄存器
reg [7:0] tx_data_reg;
//从机响应的ACK
reg tx_ack_reg;

//接收移位寄存器
reg [7:0] rx_data_reg;
//发送给从机的ACK
reg rx_ack_reg;

//上一刻的状态
reg [7:0] prev_state;
//当前状态
reg [7:0] current_state;
//下一刻的状态
reg [7:0] next_state;
//当前状态结束标志,切换到下一个状态
reg state_done;

//I2C总线空闲标志
reg bus_idle_flag;

//记录上一时刻的状态
//状态跳转
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		prev_state <= IDLE_STATE;
		current_state <= IDLE_STATE;
	end
	else begin
		prev_state <= current_state;
		current_state <= next_state;
	end
end

//根据当前状态确定下一刻状态
always @(*) begin
	case(current_state)
		IDLE_STATE: begin
			if((state_done == 1'b0) && (ctrl_start == 1'b1) && (ctrl_cmd == GEN_STA_CMD))
				next_state = GEN_STA_STATE;
			else if((state_done == 1'b0) && (ctrl_start == 1'b1) && (ctrl_cmd == WR_DATA_CMD))
				next_state = WR_DATA_STATE;
			else if((state_done == 1'b0) && (ctrl_start == 1'b1) && (ctrl_cmd == RD_DATA_CMD))
				next_state = RD_DATA_STATE;
			else if((state_done == 1'b0) && (ctrl_start == 1'b1) && (ctrl_cmd == GEN_STO_CMD))
				next_state = GEN_STO_STATE;
			else
				next_state = IDLE_STATE;
		end
		GEN_STA_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = GEN_STA_STATE;
		end
		WR_DATA_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = WR_DATA_STATE;
		end
		RD_DATA_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = RD_DATA_STATE;
		end
		GEN_STO_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = GEN_STO_STATE;
		end
		default: begin
			next_state = IDLE_STATE;
		end
	endcase
end

//进行时钟周期计数
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		clk_period_count <= 32'b0;
	else begin
		case(current_state)
			IDLE_STATE: begin
				clk_period_count <= 32'b0;
			end
			GEN_STA_STATE, WR_DATA_STATE, RD_DATA_STATE, GEN_STO_STATE: begin
				//产生启动信号,按I2C_CLK_PERIOD进行计数
				//发送数据,发送完成后读取ACK,按I2C_CLK_PERIOD进行计数
				//接收数据,接收完成后生成ACK,按I2C_CLK_PERIOD进行计数
				//产生停止信号,按I2C_CLK_PERIOD进行计数
				if(state_done == 1'b0) begin
					if(clk_period_count < (I2C_CLK_PERIOD - 1))
						clk_period_count <= clk_period_count + 32'b1;
					else
						clk_period_count <= 32'b0;
				end
			end
			default: begin
				clk_period_count <= 32'b0;
			end
		endcase
	end
end

//进行传输bit计数
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		bit_cnt <= 8'b0;
	else begin
		case(current_state)
			IDLE_STATE: begin
				bit_cnt <= 8'b0;
			end
			GEN_STA_STATE, GEN_STO_STATE: begin
				//产生开始信号和停止信号时第一个时钟周期用于将IO设置为初始状态,低二个时钟周期输出开始或结束信号
				if(clk_period_count == (I2C_CLK_PERIOD - 1)) begin
					if(bit_cnt < (2 - 1))
						bit_cnt <= bit_cnt + 8'b1;
				end
			end
			WR_DATA_STATE, RD_DATA_STATE: begin
				//发送数据,8bit数据加1bit应答
				//接收数据,8bit数据加1bit应答
				if(clk_period_count == (I2C_CLK_PERIOD - 1)) begin
					if(bit_cnt < (9 - 1))
						bit_cnt <= bit_cnt + 8'b1;
				end
			end
			default: begin
				bit_cnt <= 8'b0;
			end
		endcase
	end
end

//锁存需要发送的数据
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		tx_data_reg <= 8'b0;
		rx_ack_reg <= 1'b0;
	end
	else if(current_state == IDLE_STATE) begin
		//暂存需要发送的数据
		if(next_state == WR_DATA_STATE)
			tx_data_reg <= tx_data;
		//暂存需要发送的ACK
		if(next_state == RD_DATA_STATE)
			rx_ack_reg <= rx_ack;
	end
end

//控制数据传输
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		bus_idle_flag <= 1'b1;
		i2c_scl <= 1'b1;
		i2c_sda_o <= 1'b1;
		i2c_sda_t <= 1'b1;
		tx_ack_reg <= 1'b0;
		rx_data_reg <= 8'b0;
		state_done <= 1'b0;
	end
	else begin
		case(current_state)
			IDLE_STATE: begin
				if(bus_idle_flag == 1'b1) begin
					i2c_scl <= 1'b1;
					i2c_sda_o <= 1'b1;
					i2c_sda_t <= 1'b1;
					tx_ack_reg <= 1'b0;
					rx_data_reg <= 8'b0;
				end
				state_done <= 1'b0;
			end
			GEN_STA_STATE: begin
				if(state_done == 1'b0) begin
					//设置总线为忙
					if((bit_cnt == 0) && (clk_period_count == 0))
						bus_idle_flag <= 1'b0;

					//第一个时钟周期SCL和SDA均输出高,进入准备状态
					//第二个时钟周期SCL保持高,SDA前半个周期为高后半个周期为低,产生开始信号
					if(bit_cnt <= (2 - 2)) begin
						//SCL为高
						i2c_scl <= 1'b1;
						//SDA为输出
						i2c_sda_t <= 1'b0;
						//SDA为输出高
						i2c_sda_o <= 1'b1;
					end
					else begin
						//SCL为高
						i2c_scl <= 1'b1;
						//SDA为输出
						i2c_sda_t <= 1'b0;
						//前半个周期SDA为高,后半个周期SDA为低
						if(clk_period_count <= (I2C_CLK_PERIOD_HALF - 1)) 
							i2c_sda_o <= 1'b1;
						else
							i2c_sda_o <= 1'b0;
					end

					//启动信号结束
					if((bit_cnt == (2 - 1)) && (clk_period_count == (I2C_CLK_PERIOD - 1)))
						state_done <= 1'b1;
				end
			end
			WR_DATA_STATE: begin
				if(state_done == 1'b0) begin
					if(bus_idle_flag == 1'b0) begin
						if(bit_cnt <= (9 - 2)) begin
							//写8bit数据
							//SDA为输出
							i2c_sda_t <= 1'b0;
							//SDA输出数据寄存器中的值
							i2c_sda_o <= tx_data_reg[7 - bit_cnt];

							//前半个周期SCL为低,后半个周期SCL为高
							if(clk_period_count <= (I2C_CLK_PERIOD_HALF - 1)) 
								i2c_scl <= 1'b0;
							else
								i2c_scl <= 1'b1;
						end
						else begin
							//读从机应答
							//SDA为输入
							i2c_sda_t <= 1'b1;

							//前半个周期SCL为低,后半个周期SCL为高
							if(clk_period_count <= (I2C_CLK_PERIOD_HALF - 1))
								i2c_scl <= 1'b0;
							else
								i2c_scl <= 1'b1;

							//在时钟周期3/4处采样SDA输入
							if(clk_period_count == (I2C_CLK_PERIOD - I2C_CLK_PERIOD / 4 - 1))
								tx_ack_reg <= i2c_sda_i;
						end

						//写操作结束
						if((bit_cnt == (9 - 1)) && (clk_period_count == (I2C_CLK_PERIOD - 1)))
							state_done <= 1'b1;
					end
					else
						state_done <= 1'b1;
				end
			end
			RD_DATA_STATE: begin
				if(state_done == 1'b0) begin
					if(bus_idle_flag == 1'b0) begin
						if(bit_cnt <= (9 - 2)) begin
							//读8bit数据
							//SDA为输入
							i2c_sda_t <= 1'b1;

							//前半个周期SCL为低,后半个周期SCL为高
							if(clk_period_count <= (I2C_CLK_PERIOD_HALF - 1))
								i2c_scl <= 1'b0;
							else
								i2c_scl <= 1'b1;

							//在时钟周期3/4处采样SDA输入
							if(clk_period_count == (I2C_CLK_PERIOD - I2C_CLK_PERIOD / 4 - 1))
								rx_data_reg[7 - bit_cnt] <= i2c_sda_i;
						end
						else begin
							//写从机应答
							//SDA为输出
							i2c_sda_t <= 1'b0;
							//SDA输出应答
							i2c_sda_o <= rx_ack_reg;

							//前半个周期SCL为低,后半个周期SCL为高
							if(clk_period_count <= (I2C_CLK_PERIOD_HALF - 1)) 
								i2c_scl <= 1'b0;
							else
								i2c_scl <= 1'b1;
						end

						//读操作结束
						if((bit_cnt == (9 - 1)) && (clk_period_count == (I2C_CLK_PERIOD - 1))) 
							state_done <= 1'b1;
					end
					else
						state_done <= 1'b1;
				end
			end
			GEN_STO_STATE: begin
				if(state_done == 1'b0) begin
					//第一个时钟周期SCL和SDA均输出低,进入准备状态
					//第二个时钟周期SCL保持高,SDA前半个周期为低后半个周期为高,产生停止信号
					if(bit_cnt <= (2 - 2)) begin
						//SCL为低
						i2c_scl <= 1'b0;
						//SDA为输出
						i2c_sda_t <= 1'b0;
						//SDA为输出低
						i2c_sda_o <= 1'b0;
					end
					else begin
						//SCL为高
						i2c_scl <= 1'b1;
						//SDA为输出
						i2c_sda_t <= 1'b0;
						//前半个周期SDA为低,后半个周期SDA为高
						if(clk_period_count <= (I2C_CLK_PERIOD_HALF - 1)) 
							i2c_sda_o <= 1'b0;
						else
							i2c_sda_o <= 1'b1;
					end

					//停止信号结束
					if((bit_cnt == (2 - 1)) && (clk_period_count == (I2C_CLK_PERIOD - 1)))
						state_done <= 1'b1;

					//设置总线为空闲
					if((bit_cnt == (2 - 1)) && (clk_period_count == (I2C_CLK_PERIOD - 1)))
						bus_idle_flag <= 1'b1;
				end
			end
			default: begin
				if(bus_idle_flag == 1'b1) begin
					i2c_scl <= 1'b1;
					i2c_sda_o <= 1'b1;
					i2c_sda_t <= 1'b1;
					tx_ack_reg <= 1'b0;
					rx_data_reg <= 8'b0;
				end
				state_done <= 1'b0;
			end
		endcase
	end
end

//输出完成信号和数据
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		ctrl_done <= 1'b0;
		tx_ack <= 1'b0;
		rx_data <= 8'b0;
	end
	else begin
		case(current_state)
			IDLE_STATE: begin
				if(prev_state != IDLE_STATE) begin
					ctrl_done <= 1'b1;
					tx_ack <= tx_ack_reg;
					rx_data <= rx_data_reg;
				end
				else
					ctrl_done <= 1'b0;
			end
			default: begin
				ctrl_done <= 1'b0;
			end
		endcase
	end
end

//空闲标志输出
assign ctrl_idle = ((current_state == IDLE_STATE) && (state_done == 1'b0) && (sys_rst_n == 1'b1)) ? 1'b1 : 1'b0;

endmodule

EEPROM 读写控制模块

module eeprom_driver #(
	parameter I2C_CLK_PERIOD = 125,				//I2C时钟周期,以系统时钟为参考
	parameter EEPROM_MEM_ADDR_BYTES = 2				//内存地址字节数
)
(
	input sys_rst_n,				//系统复位
	input sys_clk,					//系统时钟

	input eeprom_start,				//启动信号
	input [7:0] eeprom_cmd,			//操作命令
	input [6:0] slave_addr,			//从机地址
	input [15:0] mem_addr,			//操作的存储器地址

	input [7:0] wr_data_len,		//写入长度
	input [7:0] wr_data,			//需要写入的数据
	output reg wr_data_req,			//写入数据请求

	input [7:0] rd_data_len,		//读取长度
	output [7:0] rd_data,			//读取到的数据
	output rd_data_flag,			//读取输出标志

	output eeprom_idle,				//空闲标志
	output reg eeprom_error_flag,	//EEPROM错误标志

	output i2c_scl,					//i2c时钟输出
	output i2c_sda_o,				//i2c数据输出
	input i2c_sda_i,				//i2c数据输入
	output i2c_sda_t				//i2c数据方向控制,1输入,0输出
);

//状态机的状态
localparam IDLE_STATE = 8'h01;			//空闲状态
localparam READ_STATE = 8'h02;			//读状态
localparam WRITE_STATE = 8'h04;			//写状态

//EEPROM操作命令
localparam READ_CMD = 8'h02;			//读命令
localparam WRITE_CMD = 8'h04;			//写命令

//i2c指令集
localparam GEN_STA_CMD = 7'b0000010;		//产生起始信号
localparam WR_DATA_CMD = 7'b0000100;		//写数据状态,写完8bit后还要读ACK
localparam RD_DATA_CMD = 7'b0001000;		//读数据状态,读完8bit后还要生成ACK
localparam GEN_STO_CMD = 7'b0010000;		//产生停止信号

//当前状态
reg [7:0] current_state;
//下一刻的状态
reg [7:0] next_state;
//当前状态结束标志,切换到下一个状态
reg state_done;

//I2C操作启动计数
reg [7:0] i2c_opt_begin_count;
//I2C操作完成计数
reg [7:0] i2c_opt_end_count;
//I2C错误处理启动计数
reg [7:0] i2c_error_begin_count;
//I2C错误处理完成计数
reg [7:0] i2c_error_end_count;

//i2c控制命令
reg [7:0] i2c_ctrl_cmd;
//i2c控制器启动信号
reg i2c_ctrl_start;
//i2c控制器空闲信号
wire i2c_ctrl_idle;
//i2c控制器完成信号
wire i2c_ctrl_done;

//I2C总线发送的数据
reg [7:0] i2c_tx_data;
//数据发送完成后从设备返回的应答
wire i2c_tx_ack;

//I2C总线接收到的数据
wire [7:0] i2c_rx_data;
//数据接收完成后发送给从设备的应答
reg i2c_rx_ack;


//状态跳转
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		current_state <= IDLE_STATE;
	else
		current_state <= next_state;
end

//根据当前状态确定下一刻状态
always @(*) begin
	case(current_state)
		IDLE_STATE: begin
			if((state_done == 1'b0) && (eeprom_start == 1'b1) && (eeprom_cmd == READ_CMD))
				next_state = READ_STATE;
			else if((state_done == 1'b0) && (eeprom_start == 1'b1) && (eeprom_cmd == WRITE_CMD))
				next_state = WRITE_STATE;
			else
				next_state = IDLE_STATE;
		end
		READ_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = READ_STATE;
		end
		WRITE_STATE: begin
			if(state_done == 1'b1)
				next_state = IDLE_STATE;
			else
				next_state = WRITE_STATE;
		end
		default:
			next_state = IDLE_STATE;
	endcase
end

//空闲标志输出
assign eeprom_idle = ((current_state == IDLE_STATE) && (state_done == 1'b0)) ? 1'b1 : 1'b0;

//控制I2C传输
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		i2c_ctrl_cmd <= 8'b0;
		i2c_ctrl_start <= 1'b0;
		i2c_tx_data <= 8'b0;
		i2c_rx_ack <= 1'b0;
	end
	else begin
		case(current_state)
			IDLE_STATE: begin
				i2c_ctrl_cmd <= 8'b0;
				i2c_ctrl_start <= 1'b0;
				i2c_tx_data <= 8'b0;
				i2c_rx_ack <= 1'b0;
			end
			READ_STATE: begin
				if(state_done == 1'b0) begin
					if((i2c_tx_ack == 1'b0) && (eeprom_error_flag == 1'b0)) begin
						//发送状态回复ACK
						if((i2c_ctrl_idle == 1'b1) && (i2c_ctrl_start == 1'b0)) begin
							//产生开始信号
							if(i2c_opt_begin_count == 0) begin
								i2c_ctrl_cmd <= GEN_STA_CMD;
								i2c_ctrl_start <= 1'b1;
							end
							//发送设备地址+写标志
							if(i2c_opt_begin_count == 1) begin
								i2c_ctrl_cmd <= WR_DATA_CMD;
								i2c_tx_data <= {slave_addr, 1'b0};
								i2c_ctrl_start <= 1'b1;
							end
							//发送内存地址
							if(EEPROM_MEM_ADDR_BYTES == 2) begin
								//发送内存地址高字节
								if(i2c_opt_begin_count == 2) begin
									i2c_ctrl_cmd <= WR_DATA_CMD;
									i2c_tx_data <= mem_addr[15:8];
									i2c_ctrl_start <= 1'b1;
								end
								//发送内存地址低字节
								if(i2c_opt_begin_count == 3) begin
									i2c_ctrl_cmd <= WR_DATA_CMD;
									i2c_tx_data <= mem_addr[7:0];
									i2c_ctrl_start <= 1'b1;
								end
							end
							else begin
								//发送内存地址
								if(i2c_opt_begin_count == 2) begin
									i2c_ctrl_cmd <= WR_DATA_CMD;
									i2c_tx_data <= mem_addr[7:0];
									i2c_ctrl_start <= 1'b1;
								end
							end
							//产生停止信号
							if(i2c_opt_begin_count == (EEPROM_MEM_ADDR_BYTES + 2)) begin
								i2c_ctrl_cmd <= GEN_STO_CMD;
								i2c_ctrl_start <= 1'b1;
							end
							//再次产生开始信号
							if(i2c_opt_begin_count == (EEPROM_MEM_ADDR_BYTES + 3)) begin
								i2c_ctrl_cmd <= GEN_STA_CMD;
								i2c_ctrl_start <= 1'b1;
							end
							//发送设备地址+读标志
							if(i2c_opt_begin_count == (EEPROM_MEM_ADDR_BYTES + 4)) begin
								i2c_ctrl_cmd <= WR_DATA_CMD;
								i2c_tx_data <= {slave_addr, 1'b1};
								i2c_ctrl_start <= 1'b1;
							end
							//读数据
							if((i2c_opt_begin_count >= (EEPROM_MEM_ADDR_BYTES + 5)) && (i2c_opt_begin_count < (EEPROM_MEM_ADDR_BYTES + 5 + rd_data_len))) begin
								i2c_ctrl_cmd <= RD_DATA_CMD;
								//控制输出给从机的应答信号,读取最后byte时输出NAK
								if(i2c_opt_begin_count < (EEPROM_MEM_ADDR_BYTES + 5 + rd_data_len - 1))
									i2c_rx_ack <= 1'b0;
								else
									i2c_rx_ack <= 1'b1;
								i2c_ctrl_start <= 1'b1;
							end
							//产生停止信号
							if(i2c_opt_begin_count == (EEPROM_MEM_ADDR_BYTES + 5 + rd_data_len)) begin
								i2c_ctrl_cmd <= GEN_STO_CMD;
								i2c_ctrl_start <= 1'b1;
							end
						end
						else if(i2c_ctrl_idle == 1'b0)
							i2c_ctrl_start <= 1'b0;
					end
					else begin
						//发送状态回复NCK
						if((i2c_ctrl_idle == 1'b1) && (i2c_ctrl_start == 1'b0)) begin
							//产生停止信号
							if(i2c_error_begin_count == 0) begin
								i2c_ctrl_cmd <= GEN_STO_CMD;
								i2c_ctrl_start <= 1'b1;
							end
						end
						else if(i2c_ctrl_idle == 1'b0)
							i2c_ctrl_start <= 1'b0;
					end
				end
			end
			WRITE_STATE: begin
				if(state_done == 1'b0) begin
					if((i2c_tx_ack == 1'b0) && (eeprom_error_flag == 1'b0)) begin
						//发送状态回复ACK
						if((i2c_ctrl_idle == 1'b1) && (i2c_ctrl_start == 1'b0)) begin
							//产生开始信号
							if(i2c_opt_begin_count == 0) begin
								i2c_ctrl_cmd <= GEN_STA_CMD;
								i2c_ctrl_start <= 1'b1;
							end
							//发送设备地址+写标志
							if(i2c_opt_begin_count == 1) begin
								i2c_ctrl_cmd <= WR_DATA_CMD;
								i2c_tx_data <= {slave_addr, 1'b0};
								i2c_ctrl_start <= 1'b1;
							end
							//发送内存地址
							if(EEPROM_MEM_ADDR_BYTES == 2) begin
								//发送内存地址高字节
								if(i2c_opt_begin_count == 2) begin
									i2c_ctrl_cmd <= WR_DATA_CMD;
									i2c_tx_data <= mem_addr[15:8];
									i2c_ctrl_start <= 1'b1;
								end
								//发送内存地址低字节
								if(i2c_opt_begin_count == 3) begin
									i2c_ctrl_cmd <= WR_DATA_CMD;
									i2c_tx_data <= mem_addr[7:0];
									i2c_ctrl_start <= 1'b1;
								end
							end
							else begin
								//发送内存地址
								if(i2c_opt_begin_count == 2) begin
									i2c_ctrl_cmd <= WR_DATA_CMD;
									i2c_tx_data <= mem_addr[7:0];
									i2c_ctrl_start <= 1'b1;
								end
							end
							//写数据
							if((i2c_opt_begin_count >= (EEPROM_MEM_ADDR_BYTES + 2)) && (i2c_opt_begin_count < (EEPROM_MEM_ADDR_BYTES + 2 + wr_data_len))) begin
								i2c_ctrl_cmd <= WR_DATA_CMD;
								i2c_tx_data <= wr_data;
								i2c_ctrl_start <= 1'b1;
							end
							//产生停止信号
							if(i2c_opt_begin_count == (EEPROM_MEM_ADDR_BYTES + 2 + wr_data_len)) begin
								i2c_ctrl_cmd <= GEN_STO_CMD;
								i2c_ctrl_start <= 1'b1;
							end
						end
						else if(i2c_ctrl_idle == 1'b0)
							i2c_ctrl_start <= 1'b0;
					end
					else begin
						//发送状态回复NCK
						if((i2c_ctrl_idle == 1'b1) && (i2c_ctrl_start == 1'b0)) begin
							//产生停止信号
							if(i2c_error_begin_count == 0) begin
								i2c_ctrl_cmd <= GEN_STO_CMD;
								i2c_ctrl_start <= 1'b1;
							end
						end
						else if(i2c_ctrl_idle == 1'b0)
							i2c_ctrl_start <= 1'b0;
					end
				end
			end
			default: begin
				i2c_ctrl_cmd <= 8'b0;
				i2c_ctrl_start <= 1'b0;
				i2c_tx_data <= 8'b0;
				i2c_rx_ack <= 1'b0;
			end
		endcase
	end
end

//数据请求输出
always @(posedge sys_clk)begin
	if(!sys_rst_n)
		wr_data_req <= 1'b0;
	else if((current_state == WRITE_STATE) && (i2c_tx_ack == 1'b0) && (i2c_opt_begin_count >= (EEPROM_MEM_ADDR_BYTES + 1)) && (i2c_opt_begin_count < (EEPROM_MEM_ADDR_BYTES + 1 + wr_data_len))) begin
		if((i2c_ctrl_idle == 1'b0) && (i2c_ctrl_start == 1'b1))
			wr_data_req <= 1'b1;
		else
			wr_data_req <= 1'b0;
	end
	else
		wr_data_req <= 1'b0;
end

//输出读取到的数据
assign rd_data = i2c_rx_data;
assign rd_data_flag = ((current_state == READ_STATE) && (i2c_opt_end_count >= (EEPROM_MEM_ADDR_BYTES + 5)) && (i2c_opt_end_count < (EEPROM_MEM_ADDR_BYTES + 5 + rd_data_len))) ? i2c_ctrl_done : 1'b0;

//错误检测
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		eeprom_error_flag <= 1'b0;
	end
	else if(current_state != IDLE_STATE) begin
			//从机响应NAK
			if(i2c_tx_ack == 1'b1)
				eeprom_error_flag <= 1'b1;
		end
	else
		eeprom_error_flag <= 1'b0;
end

//I2C操作启动计数
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		i2c_opt_begin_count <= 0;
		i2c_error_begin_count <= 0;
	end
	else if(current_state != IDLE_STATE) begin
		if(eeprom_error_flag == 1'b0) begin
			if((i2c_ctrl_idle == 1'b0) && (i2c_ctrl_start == 1'b1))
				i2c_opt_begin_count <= i2c_opt_begin_count + 1;
		end
		else begin
			if((i2c_ctrl_idle == 1'b0) && (i2c_ctrl_start == 1'b1))
				i2c_error_begin_count <= i2c_error_begin_count + 1;
		end
	end
	else if(current_state == IDLE_STATE) begin
		i2c_opt_begin_count <= 0;
		i2c_error_begin_count <= 0;
	end
end

//I2C操作完成计数
//I2C错误处理完成计数
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		i2c_opt_end_count <= 0;
		i2c_error_end_count <= 0;
	end
	else if(current_state != IDLE_STATE) begin
		if(eeprom_error_flag == 1'b0) begin
			if(i2c_ctrl_done == 1'b1)
				i2c_opt_end_count <= i2c_opt_end_count + 1;
		end
		else begin
			if(i2c_ctrl_done == 1'b1)
				i2c_error_end_count <= i2c_error_end_count + 1;
		end
	end
	else if(current_state == IDLE_STATE) begin
		i2c_opt_end_count <= 0;
		i2c_error_end_count <= 0;
	end
end

//状态结束检测
always @(posedge sys_clk) begin
	if(!sys_rst_n)
		state_done <= 1'b0;
	else begin
		case(current_state)
			IDLE_STATE: begin
				state_done <= 1'b0;
			end
			READ_STATE: begin
				//读存储器操作完成或出错
				if(i2c_opt_end_count == (EEPROM_MEM_ADDR_BYTES + 6 + rd_data_len) || (i2c_error_end_count == 1))
					state_done <= 1'b1;
			end
			WRITE_STATE: begin
				//写存储器操作完成或出错
				if(i2c_opt_end_count == (EEPROM_MEM_ADDR_BYTES + 3 + wr_data_len) || (i2c_error_end_count == 1))
					state_done <= 1'b1;
			end
			default: begin
				state_done <= 1'b0;
			end
		endcase
	end
end

//例化i2c_driver模块
i2c_driver #(
	.I2C_CLK_PERIOD(I2C_CLK_PERIOD)
)
tb_i2c_driver_inst0 (
	.sys_rst_n(sys_rst_n),
	.sys_clk(sys_clk),

	.ctrl_cmd(i2c_ctrl_cmd),
	.ctrl_start(i2c_ctrl_start),
	.ctrl_idle(i2c_ctrl_idle),
	.ctrl_done(i2c_ctrl_done),

	.tx_data(i2c_tx_data),
	.tx_ack(i2c_tx_ack),

	.rx_data(i2c_rx_data),
	.rx_ack(i2c_rx_ack),

	.i2c_scl(i2c_scl),
	.i2c_sda_o(i2c_sda_o),
	.i2c_sda_i(i2c_sda_i),
	.i2c_sda_t(i2c_sda_t)
);

endmodule

EEPROM 读写测试模块

module eeprom_rw_test #(
	parameter I2C_CLK_PERIOD = 1250,				//I2C时钟周期,以系统时钟为参考
	parameter ALARM_LED_PERIOD = 25'd25_000_000,
	parameter WAIT_TIME = 250000
)
(
	input sys_rst_n,			//系统复位
	input sys_clk,				//系统时钟

	output i2c_scl,				//i2c时钟输出
	inout i2c_sda,				//i2c数据

	output reg alarm_led		//状态指示灯
);

//EEPROM操作命令
localparam READ_CMD = 8'h02;			//读命令
localparam WRITE_CMD = 8'h04;			//写命令

//状态机的状态
localparam IDLE_STATE = 8'h01;			//空闲状态
localparam READ_STATE = 8'h02;			//读状态
localparam WAIT_STATE = 8'h04;			//等待状态
localparam WRITE_STATE = 8'h08;			//写状态

//错误指示
reg error_flag;
//警示LED闪烁计数器
reg [31:0] led_count;

//写入计数
reg [7:0] write_count;
//读取计数
reg [7:0] read_count;

//等待写入计时计数器
reg [31:0] wait_count;

//当前状态
reg [7:0] current_state;
//下一刻的状态
reg [7:0] next_state;
//对应状态结束标志,应切换到下一个状态,一个bit对应一个状态
reg [7:0] state_done;
//状态启动标志
reg [7:0] state_start;

//启动信号
reg eeprom_start;
//操作命令
reg [7:0] eeprom_cmd;
//操作的存储器地址
reg [15:0] mem_addr;

//写入长度
reg [7:0] wr_data_len;
//需要写入的数据
reg [7:0] wr_data;
//写入数据请求
wire wr_data_req;

//读取长度
reg [7:0] rd_data_len;
//读取到的数据
wire [7:0] rd_data;
//读取输出标志
wire rd_data_flag;

//空闲标志
wire eeprom_idle;
//EEPROM错误标志
wire eeprom_error_flag;

//根据错误标志控制led闪烁或常亮
//操作未完成熄灭
//操作完成常亮
//发生错误时闪烁
always @(posedge sys_clk) begin
	if(!sys_rst_n) begin
		alarm_led <= 1'b0;
		led_count <= 32'b0;
	end
	else if(error_flag == 1'b1) begin
		if(led_count >= (ALARM_LED_PERIOD - 1)) begin
			alarm_led <= ~alarm_led;
			led_count <= 32'b0;
		end
		else
			led_count <= led_count + 32'b1;
	end
	else if((current_state == IDLE_STATE) && (state_done != 8'h0)) begin
		alarm_led <= 1'b1;
		led_count <= 32'b0;
	end
	else begin 
		alarm_led <= 1'b0;
		led_count <= 32'b0;
	end
end

//状态跳转
always @(posedge sys_clk)begin
	if(!sys_rst_n)
		current_state <= IDLE_STATE;
	else
		current_state <= next_state;
end

//根据当前状态确定下一刻状态
always @(*)begin
	case(current_state)
		IDLE_STATE: begin
			if((state_done == 16'h0) && (error_flag == 1'b0))
				next_state = WRITE_STATE;
			else
				next_state = IDLE_STATE;
		end
		WRITE_STATE: begin
			if(error_flag == 1'b1)
				next_state = IDLE_STATE;
			else if(state_done & WRITE_STATE)
				next_state = WAIT_STATE;
			else
				next_state = WRITE_STATE;
		end
		WAIT_STATE: begin
			if(error_flag == 1'b1)
				next_state = IDLE_STATE;
			else if(state_done & WAIT_STATE)
				next_state = READ_STATE;
			else
				next_state = WAIT_STATE;
		end
		READ_STATE: begin
			if(error_flag == 1'b1)
				next_state = IDLE_STATE;
			else if(state_done & READ_STATE)
				next_state = IDLE_STATE;
			else
				next_state = READ_STATE;
		end
		default:
			next_state = IDLE_STATE;
	endcase
end

//写入等待过程计时
always @(posedge sys_clk)begin
	if(!sys_rst_n) 
		wait_count <= 0;
	else begin
		if(current_state == WAIT_STATE) begin
			if(wait_count < (WAIT_TIME - 1))
				wait_count <= wait_count + 1;
		end
		else
			wait_count <= 0;
	end
end

//控制EEPROM读写
always @(posedge sys_clk)begin
	if(!sys_rst_n) begin
		eeprom_start <= 1'b0;
		eeprom_cmd <= 8'b0;
		mem_addr <= 16'b0;
		wr_data_len <= 8'b0;
		rd_data_len <= 8'b0;
		state_done <= 8'h0;
		state_start <= 8'h0;
	end
	else begin
		case(current_state)
			WRITE_STATE: begin
				//启动写操作
				if((eeprom_idle == 1'b1) && (!(state_start & WRITE_STATE)) && (eeprom_start == 1'b0)) begin
					eeprom_start <= 1'b1;
					eeprom_cmd <= WRITE_CMD;
					mem_addr <= 16'h0000;
					wr_data_len <= 8'd8;
					state_start <= state_start | WRITE_STATE;
				end
				//写操作完成
				if((eeprom_idle == 1'b1) && (state_start & WRITE_STATE) && (eeprom_start == 1'b0)) begin
					if(!(state_done & WRITE_STATE))
						state_done <= state_done | WRITE_STATE;
				end
				//操作已经启动,复位启动标志
				if((eeprom_idle == 1'b0) && (eeprom_start == 1'b1))
					eeprom_start <= 1'b0;
			end
			WAIT_STATE: begin
				//等待EEPROM写入结束
				if(wait_count >= (WAIT_TIME - 1))
					state_done <= state_done | WAIT_STATE;
			end
			READ_STATE: begin
				//启动读flash操作
				if((eeprom_idle == 1'b1) && (!(state_start & READ_STATE)) && (eeprom_start == 1'b0)) begin
					eeprom_start <= 1'b1;
					eeprom_cmd <= READ_CMD;
					mem_addr <= 24'h0000;
					rd_data_len <= 8'd8;
					state_start <= state_start | READ_STATE;
				end
				//读操作完成
				if((eeprom_idle == 1'b1) && (state_start & READ_STATE) && (eeprom_start == 1'b0)) begin
					if(!(state_done & READ_STATE))
						state_done <= state_done | READ_STATE;
				end
				//操作已经启动,复位启动标志
				if((eeprom_idle == 1'b0) && (eeprom_start == 1'b1))
					eeprom_start <= 1'b0;
			end
			default: begin
				eeprom_start <= 1'b0;
				eeprom_cmd <= 8'b0;
				mem_addr <= 16'b0;
			end
		endcase
	end
end

//生成写入的数据
always @(posedge sys_clk)begin
	if(!sys_rst_n) begin
		wr_data <= 8'h0;
		write_count <= 8'h0;
	end
	else if(current_state == WRITE_STATE) begin
		if(wr_data_req == 1'b1) begin
			//生成写入数据
			wr_data <= write_count[7:0] + 1;
			//写入计数
			write_count <= write_count + 8'h1;
		end
	end
	else begin
		wr_data <= 0;
		write_count <= 0;
	end
end

//处理读取的数据
//处理EEPROM错误
always @(posedge sys_clk)begin
	if(!sys_rst_n) begin
		error_flag <= 1'b0;
		read_count <= 8'h0;
	end
	else begin
		//处理EEPROM错误
		if(eeprom_error_flag == 1'b1)
			error_flag <= 1'b1;
		//处理读取的数据
		if(current_state == READ_STATE) begin
			if(rd_data_flag == 1'b1) begin
				//检查接收的数据
				if(rd_data != (read_count[7:0] + 1))
					error_flag <= 1'b1;
				//接收计数
				read_count <= read_count + 8'h1;
			end
		end
	end
end

//用IOBUF将SDA合并
//对于inout类型的端口最好显示的调用IOBUF硬核
IOBUF u_IOBUF_inst0(
	.IO(i2c_sda),		//连接到外部IO引脚

	.T(i2c_sda_t),		//输入输出控制,0输出(将I输出到IO),1输入(将IO设置为高阻态,此时O为IO输入电平)
	.I(i2c_sda_o),		//IOBUF输入,应接用户逻辑输出
	.O(i2c_sda_i)		//IOBUF输出,应接用户逻辑输入
);

//例化eeprom_driver
eeprom_driver #(
	.I2C_CLK_PERIOD(1250),
	.EEPROM_MEM_ADDR_BYTES(2)
)
tb_eeprom_driver_inst0(
	.sys_rst_n(sys_rst_n),
	.sys_clk(sys_clk),

	.eeprom_start(eeprom_start),
	.eeprom_cmd(eeprom_cmd),
	.slave_addr(7'b1010000),
	.mem_addr(mem_addr),

	.wr_data_len(wr_data_len),
	.wr_data(wr_data),
	.wr_data_req(wr_data_req),

	.rd_data_len(rd_data_len),
	.rd_data(rd_data),
	.rd_data_flag(rd_data_flag),

	.eeprom_idle(eeprom_idle),
	.eeprom_error_flag(eeprom_error_flag),

	.i2c_scl(i2c_scl),
	.i2c_sda_o(i2c_sda_o),
	.i2c_sda_i(i2c_sda_i),
	.i2c_sda_t(i2c_sda_t)
);

endmodule

仿真激励文件

在仿真过程中需要用到AT24C64的仿真激励模型,可以通过AT24C64仿真激励模型下载地址进行下载,完成的仿真激励文件如下:

`timescale 1ns / 1ps

module tb_eeprom_rw_test( );

reg sys_rst_n;		//系统复位
reg sys_clk;		//系统时钟

wire i2c_scl;		//i2c时钟输出
wire i2c_sda;		//i2c数据

wire alarm_led;		//状态指示灯

//产生仿真信号序列
initial begin
	sys_rst_n = 1'b0;
	sys_clk = 1'b0;
	#2000
	sys_rst_n = 1'b1;
end

//产生时钟
always #10 sys_clk = ~sys_clk;

//将SDA数据线上拉 
pullup(i2c_sda);

//例化eeprom_rw_test
eeprom_rw_test #(
	.I2C_CLK_PERIOD(1250),
	.ALARM_LED_PERIOD(25'd25_000_000)
)
tb_eeprom_rw_test_inst0(
	.sys_rst_n(sys_rst_n),
	.sys_clk(sys_clk),

	.i2c_scl(i2c_scl),
	.i2c_sda(i2c_sda),

	.alarm_led(alarm_led)
);

//例化e2prom仿真模型
M24LC64 u_M24LC64_inst0(
	.A0(0),
	.A1(0),
	.A2(0),
	.WP(0),
	.SDA(i2c_sda),
	.SCL(i2c_scl),
	.RESET(~sys_rst_n)
);

endmodule

引脚约束

#时序约束
create_clock -period 20.000 -name sys_clk [get_ports sys_clk]

#IO 管脚约束
set_property -dict {PACKAGE_PIN R4 IOSTANDARD LVCMOS15} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U7 IOSTANDARD LVCMOS15} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN F13 IOSTANDARD LVCMOS33} [get_ports i2c_scl]
set_property -dict {PACKAGE_PIN A19 IOSTANDARD LVCMOS33} [get_ports i2c_sda]
set_property -dict {PACKAGE_PIN V9 IOSTANDARD LVCMOS15} [get_ports alarm_led]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/672108.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

车载软件架构 - AUTOSAR 的信息安全框架

车载软件架构 - AUTOSAR 的信息安全架构 我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗…

处理一对多的映射关系

一对多关系&#xff0c;比如说根据id查询一个部门的部门信息及部门下的员工信息 在Dept类中先添加List emps属性 1、collection DeptMapper.xml文件中 <resultMap id"deptAndEmpResultMap" type"Dept"><id property"did" column&qu…

国内外主流大模型语言技术大比拼

国内外主流大模型语言技术对比 2024 自2017年起&#xff0c;美国深度布局人工智能&#xff0c;全面融入经济、文化与社会。至2023年&#xff0c;中国凭借自研技术平台崭露头角&#xff0c;ChatGPT及其技术成国家战略焦点&#xff0c;引领未来科技浪潮。中美竞逐&#xff0c;人工…

crossover软件是干什么的 crossover软件安装使用教程 crossover软件如何使用

CrossOver 以其出色的跨平台兼容性&#xff0c;让用户在Mac设备上轻松运行各种Windows软件&#xff0c;无需复杂的设置或额外的配置&#xff0c;支持多种语言&#xff0c;满足不同国家和地区用户的需求。 CrossOver 软件是干嘛的 使用CrossOver 不必购买Windows 授权&#xf…

【JAVA WEB实用与优化技巧】Maven自动化构建与Maven 打包技巧

文章目录 一、MavenMaven生命周期介绍maven生命周期命令解析 二、如何编写maven打包脚本maven 配置详解setting.xml主要配置元素setting.xml 详细配置 使用maven 打包springboot项目maven 引入使用package命令来打包idea打包 三、使用shell脚本自动发布四、使用maven不同环境配…

蓝桥杯练习系统(算法训练)ALGO-935 互质数个数

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 互质数个数 问题描述 已知正整数x&#xff0c;求1~x-1中&#xff0c;有多少与x互质的数。&#xff08;互质是指两个数最大公约数为1&…

6月2(信息差)

&#x1f30d;特斯拉&#xff1a;Model3高性能版预计6月中旬开启首批交付 &#x1f384;微软对开源字体 Cascadia Code 进行重大更新 ✨天猫618加码引爆消费热潮 截至晚9点185个品牌成交破亿 1.瑞士清洁科技公司Librec开发废旧锂离子电池回收技术&#xff0c;可回收电池90%的…

day3 数1 函数

基础概念 单值函数&#xff1a; 每一个x&#xff0c;有运算法则f&#xff0c;则会有一个y与之对应 多值函数&#xff1a;一个x对应一个y1又对应另一个y2 反函数&#xff1a; 原来的函数陈为直接函数&#xff0c;每一个y&#xff0c;都有唯一x与之对应。 严格单调函数必有反…

【视频创作思维流程】教你从0培养视频创作思维

【视频创作思维流程】教你从0培养视频创作思维 1.创作认知2.培养自己的想象力2.1通过音乐辅助闭上眼睛想象2.2多看多见多模仿 3 视频脚本3.1简单的脚本3.2复杂脚本 4.拍摄预见能力4.1拍摄预见力思维用于转场4.2拍摄预见力思维给特效制作留住空间4.2拍摄预见力思维给字幕制作留住…

eNSP学习——VRRP基础配置

目录 主要命令 原理概述 实验目的 实验内容 实验拓扑 实验编址 实验步骤 1、基本配置 2、部署OSPF网络 3、配置VRRP协议 4、验证VRRP主备切换 主要命令 //创建备份组 [R2]int g0/0/1 [R2-GigabitEthernet0/0/1]vrrp vrid 1 virtual-ip 192.168.1.254 //修改优先级 …

一天挣几十元的网上兼职副业有哪些?推荐几个适合普通人做的兼职副业,有线上的也有线下的,建议收藏哦~

一天几十的兼职&#xff0c;不是几百的&#xff0c;这个会更容易实现。 相比网络上充斥着各种五花八门的兼职&#xff0c;教你轻松月入过万&#xff0c;一年几十万的...... 对于绝大多数没有一技之长的普通人&#xff0c;网络小白的话刚开始会很难的&#xff0c;慢慢来就可以…

linux文件共享之samba

1.介绍 Samba是一个开源文件共享服务&#xff0c;可以使linux与windows之间进行文件共享&#xff0c;可以根据不同人员调整共享设置以及权限管理。 2.安装 一个命令就OK了&#xff1a;yum install -y samba [rootansible01 ~]# yum install -y samba 已加载插件&#xff1a;l…

springboot 实现kafka多源配置

文章目录 背景核心配置自动化配置类注册生产者、消费者核心bean到spring配置spring.factoriesyml配置使用 源码仓库 背景 实际开发中&#xff0c;不同的topic可能来自不同的集群&#xff0c;所以就需要配置不同的kafka数据源&#xff0c;基于springboot自动配置的思想&#xf…

建筑企业有闲置资质怎么办?

如果建筑企业拥有闲置资质&#xff0c;可以考虑以下几种方式来充分利用这些资质&#xff1a; 1. 租赁或转让资质&#xff1a; 将闲置的建筑资质租赁给其他企业或个人使用&#xff0c;或者通过转让的方式将资质出售给有需要的企业或个人。 2. 提供咨询服务&#xff1a; 利用建…

导线防碰撞警示灯:高压线路安全保障

导线防碰撞警示灯&#xff1a;高压线路安全保障 在广袤的大地上&#xff0c;高压线路如同血脉般纵横交错&#xff0c;然而&#xff0c;在这看似平静的电力输送背后&#xff0c;却隐藏着不容忽视的安全隐患。特别是在那些输电线路跨越道路、施工等区域的路段&#xff0c;线下超…

哈夫曼树的构造,哈夫曼树的存在意义--求哈夫曼编码

一:哈夫曼树的构造 ①权值,带权路径长度。 ②一组确定权值的叶子节点可以构造多个不同的二叉树,但是带权路径长度min的是哈夫曼树 ③算法基本思想及其实操图片演示 注:存储结构和伪代码 1 初始化: 构造2n-1棵只有一个根节点的二叉树,parent=rchild=lchild=-1; 其中…

编程环境资源汇总

目录 前言 正文 虚拟机模块 常用软件模块&#xff08;同时包含各别好用的小软件&#xff09; 语言模块 尾声 &#x1f52d; Hi,I’m Pleasure1234&#x1f331; I’m currently learning Vue.js,SpringBoot,Computer Security and so on.&#x1f46f; I’m studying in Univer…

人工智能在消化道肿瘤中的最新研究【24年五月|顶刊速递·05-31】

小罗碎碎念 2024-05-31|医学AI顶刊速递 今天分享的六篇文章,主题是AI+结肠癌。但是,并非所有的文章都是直接与结直肠癌相关,比如第一篇研究的就是肝癌。 我其实想关注的是消化道肿瘤的医学AI研究——消化道由口腔、食管、胃、小肠、大肠和直肠组成,而肝脏虽然不直接参与食…

免费企业域名备案手把手教程

走的阿里云的备案服务&#xff0c;全程免费 前提 主办者&#xff1a;你的企业主办者负责人&#xff1a;当前登录的阿里云账户的人&#xff0c;不是企业法人的话&#xff0c;得准备委托书&#xff0c;会有地方提供模板&#xff0c;打印一下&#xff0c;签字扫描上传就行域名的…

牛客网题目--哈夫曼树

关于哈夫曼编码与哈夫曼树的介绍,可以看这个视频 题目链接 以3,4,5,6为例构造哈夫曼树 import java.util.*;public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);int n in.nextInt();PriorityQueue<Long> heap new Pr…