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)必须保持稳定,在时钟(SCL)为低电平的时候,数据总线(SDA)可以改变。
    在这里插入图片描述
  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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值