基于I2C的随机读写EEPROM

1、IIC发送模块的接口定义与整体设计
基于I2C的随机读写EEPROM_第1张图片
24LC04B
I_clk : 系统时钟
I_rst_n : 系统复位
I_i2c_send_en : 发送使能信号,当其为1时,I2C主机才能给从机发送数据
I_dev_addr [6:0]:是I2C从机的设备地址
I_word_addr[7:0] : 字地址,想要操作的I2C设备的内存地址
I_write_data[7:0] : 主机往从机中要写入的数据
O_done_flag : 主机往从机发送一个字节数据结束标志
O-scl : I2C总线的串行时钟线
O_sda : I2C总线的串行数据线。
基于I2C的随机读写EEPROM_第2张图片状态0:空闲状态,用来初始化各个寄存器的值
状态1:加载IIC设备的物理地址
状态2:加载IIC设备的字地址
状态3:加载要发送的数据
状态4:发送起始信号
状态5:发送一个字节,从高位开始发送
状态6:接收应答状态的应答位
状态7:校验应答位
状态8:发送停止信号
状态9:IIC写操作结束

1、由于IIC时序要求数据线SDA在串行时钟线的高电平保持不变,在串行时钟线的低电平才能变化,所以代码里面必须在串行时钟线低电平的正中间产生一个标志位,写代码的时候在这个标志位处改变SDA的值,这样就可以保证SDA在SCL的高电平期间保持稳定了。同理,由于IIC从机(24LC04)在接收到主机(FPGA)发送的有效数据以后会在SCL高电平期间产生一个有效应答信号0,所以为了保证采到的应答信号准确,必须在SCL高电平期间的正中间判断应答信号是否满足条件(0为有效应答,1为无效应答),因此代码里面还必须在串行时钟线高电平的正中间产生一个标志位,在这个标志下接收应答位并进行校验。

2、有了SCL信号低电平正中间标志位和高电平正中间标志位以后最好还产生一个下降沿的标志位。原因是在发送第一个8-bit数据以后,处理这个8-bit数据应答位的位置在SCL信号高电平的正中间,由于要复用发送8-bit数据的那个状态,所以必须在第二次进入发送8-bit数据的状态时必须提前把数据再次加载好,因此可以在这个下降沿的标志来加载第二次要发送的数据,然后在SCL下降沿的正中间把8-bit数据发出去

module I2C_send (
	input I_clk,
	input I_rst_n,
	input I_i2c_send_en,
	
	input [6:0] I_dev_addr,
	input [7:0] I_word_addr,
	input [7:0] I_writ_data,
	output reg O_done_flag,
	
	output O_scl,
	inout IO_sda
);


parameter C_DIV_SELECT = 10'd500;     //分频系数选择

parameter C_DIV_SELECT0 = (C_DIV_SELECT >> 2) - 1;    //用来产生I2C总线SCL低电平最中间的标志位
parameter C_DIV_SELECT1 = (C_DIV_SELECT >> 1) - 1;
parameter C_DIV_SELECT2 = (C_DIV_SELECT0 + C_DIV_SELECT1) + 1;//用来产生I2C总线SCL高电平最中间的标志位
parameter C_DIV_SELECT3 = (C_DIV_SELECT >> 1) + 1;   //用来产生下降沿的标志位


reg [9:0] R_scl_cnt;    //用来产生I2C总线SCL时钟线的计数器
reg R_scl_en;           //I2C总线SCL时钟线的使能信号
reg [3:0] R_state;
reg R_sda_mode;      //设置SDA模式,1为输出,0为输入
reg [7:0] R_load_data;  //发送/接收过程中加载的数据
reg R_sda_reg;     //SDA寄存器
reg [3:0] R_bit_cnt;   //发送字节状态中bit个数计数
reg R_ack_flag;    //应答标志
reg [3:0] R_jump_state;   //跳转状态,传输一个字节成功并应答以后通过这个变量跳转到下一个数据的状态

wire W_scl_low_mid;   //SCL的低电平中间标志位
wire W_scl_high_mid;  //SCL的高电平中间标志位
wire W_scl_neg;     //SCL的下降沿标志位

assign IO_sda = (R_sda_mode == 1'b1) ? R_sda_reg : 1'bz;

always @ (posedge I_clk or negedge I_rst_n) begin
	if(!I_rst_n) begin
		R_scl_cnt <= 'd0;
	end
	else if(R_scl_en) begin
		if(R_scl_cnt == C_DIV_SELECT - 1'b1) begin
			R_scl_cnt <= 'd0;
		end
		else begin
			R_scl_cnt <= R_scl_cnt + 1'b1;
		end
	end
	else begin
		R_scl_cnt <= 'd0;
	end
end

assign O_scl = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0;     //产生串行时钟信号O_scl
assign W_scl_low_mid = (R_scl_cnt==C_DIV_SELECT2) ? 1'b1 : 1'b0;  //产生SCL低电平中间标志信号位
assign W_scl_high_mid = (R_scl_cnt==C_DIV_SELECT0) ? 1'b1 : 1'b0;    //产生SCL高电平中间标志位
assign W_scl_neg = (R_scl_cnt == C_DIV_SELECT3) ? 1'b1 : 1'b0;   //产生SCL下降沿标志位

always @ (posedge I_clk or negedge I_rst_n) begin
	if(!I_rst_n) begin
		R_state <= 4'd0;
		R_sda_mode <= 1'b1;
		R_sda_rag <= 1'b1;
		R_bit_cnt <= 4'd0;
		O_done_flag <= 1'b0;
		R_jump_state <= 4'd0;
		R_ack_flag <= 1'b0;
	end
	else if(I_i2c_send_en) begin  //往I2C设备发送数据
		case(R_state)
			4'd0 : begin    //空闲状态设置SCL和SDA均为高
						R_sda_mode <= 1'b1;    //设置SDA为输出
						R_sda_reg <= 1'b1;      //设置SDA为高电平
						R_scl_en <= 1'b0;  //关闭SCL时钟线
						R_state <= 4'd1;     //下一个状态是加载设备物理地址状态
						R_bit_cnt <= 4'd0;    //发送字节状态中bit个数计数清零
						O_done_flag <= 1'b0;
						R_jump_state <= 4'd0;
				   end
			4'd1 : begin  //加载I2C设备物理地址
						R_load_data <= {I_div_addr,1'b0};
						R_state <= 4'd4;
						R_jump_state <= 4'd2;
				   end
			4'd2 : begin  //加载I2C设备字地址
						R_load_data <= I_word_addr;
						R_state <= 4'd5;
						R_jump_state <= 4'd3;
				   end
			4'd3 : begin  //加载要发送的数据
						R_load_data <= I_write_data;
						R_state <= 4'd5;
						R_jump_state <= 4'd8;
				   end
			4'd4 : begin //发送起始信号
						R_scl_en <= 1'b1;     //打开scl时钟线
						R_sda_mode <= 1'b1;  //设置sda为输出
						if(W_scl_high_mid) begin
							R_sda_reg <= 1'b0;     //在SCL高电平中间把SDA信号拉低,产生起始信号
							R_state <= 4'd5;
						end
						else begin
							R_state <= 4'd4;
						end
				   end
			4'd5 : begin //发送一个字节从高位开始
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_low_mid) begin
							if(R_bit_cnt==4'd8) begin
								R_bit_cnt <= 4'd0;
								R_state <= 4'd6;    //自己发送完进入应答状态
							end
							else begin
								R_sda_reg <= R_load_data[7 - R_bit_cnt];
								R_bit_cnt <= R_bit_cnt + 1'b1;
							end
						end
						else begin
							R_state <= 4'd5;
						end
				   end
			4'd6 : begin  //接收应答状态的应答位
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b0;   //设置SDA为输入
						if(W_scl_high_mid) begin
							R_ack_flag <= IO_sda;
							R_sda_state <= 4'd7;
						end
						else begin
							R_state <= 4'd6;
						end
				   end
			4'd7 : begin  //校验应答位
						R_scl_en <= 1'b1;
						if(R_ack_flag==1'b0) begin
							if(W_scl_neg==1'b1) begin
								R_state <= R_jump_state;
								R_sda_mode <= 1'b1;
								R_sda_reg <= 1'b0;  //读取完应答信号以后要把SDA信号设置后才能输出并拉低,因为
												    //如果这个状态后面是停止状态的话,需要SDA信号的上升沿,所以这里要提前拉低
							end
							else begin
								R_state <= 4'd7
							end
						end
						else begin
							R_state <= 4'd0;
						end
				   end
			4'd8 : begin   //发送停止信号
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_high_mid) begin
							R_sda_reg <= 1'b1;
							R_state <= 4'd9;
						end
				   end
			4'd9 : begin //I2C写操作结束
						R_scl_en <= 1'b0;   //关闭SCL时钟线
						R_sda_mode <= 1'b1;
						R_sda_reg <= 1'b1;
						O_flag_reg <= 1'b1;
						R_state <= 4'd0;
						R_ack_flag <= 1'b0;
				   end
			default : R_state <= 4'd0;
		endcase
	end
	else begin
		R_state <= 4'd0;
		R_sda_mode <= 1'b1;
		R_sda_reg <= 1'b1;
		R_bit_cnt <= 4'd0;
		O_done_flag <= 1'b0;
		R_jump_state <= 4'd0;
		R_ack_flag <= 1'b0;
	end

end

endmodule

基于I2C的随机读写EEPROM_第3张图片
 通过上面的时序图可以清楚的看到:

1号红框是起始信号,在SCL高电平期间SDA有一个下降沿

2~9号红框是发送设备物理地址8’ha0(8’b1010_0000)

10号红框是应答位,在这个期间R_sda_mode保持低电平,SDA为输入

11~18号红框是发送字地址8’h23(8’b0010_0011)

19号红框是应答位,在这个期间R_sda_mode保持低电平,SDA为输入

20~27号红框是发送数据8’h45(8’b0100_0101)

28号红框是应答位,在这个期间R_sda_mode保持低电平,SDA为输入

29号红框是停止信号,在SCL高电平期间SDA有一个上升沿

2、I2C接收模块的接口定义与整体设计

基于I2C的随机读写EEPROM_第4张图片
I_clk 系统时钟
I_rst_n 系统复位
I_i2c_recv_en 接收使能
I_dev_addr[6:0] 从机地址
I_word_addr[7:0] 要读取的I2C内部的存储地址
O_read_data[7:0] 读取到的数据
O_done_flag 接收一个字节完成后产生一个高脉冲
O_scl 串行时钟线
O_sda 串行数据线

基于I2C的随机读写EEPROM_第5张图片
接收模块有以下几个关键点要注意:

1、和发送模块一样,需要产生SCL信号高电平中间标志位,低电平中间标志位以及下降沿标志位

2、由于读数据的过程需要发送第二次起始位,而起始位的条件是在SCL高电平期间SDA有一个下降沿,所以一定要在处理完写设备地址与写字地址的应答位之后,在SCL的下降沿标志处把SDA信号设置成输出并拉高方便产生第二次起始信号。具体细节对照着代码理解。

3、第一次发送的设备物理地址的最低位是0,表示写数据;第二次发送的设备物理地址的最低位是1,表示读数据

4、读完一个字节数据以后,一定要记住是主机(FPGA)给从机(24LC04)发送一个非应答信号1

module i2c_recv (
	input I_clk,
	input I_rst_n,
	input I_i2c_recv_en,
	
	input [6:0] I_dev_addr,
	input [7:0] I_word_addr,
	output reg [7:0] O_read_data,
	output reg O_done_flag,
	
	output O_scl,
	inout IO_sda
);


parameter C_DIV_SELECT = 10'd500;

parameter C_DIV_SELECT0 = (C_DIV_SELECT>>2) - 1;
parameter C_DIV_SELECT1 = (C_DIV_SELECT>>1) - 1;
parameter C_DIV_SELECT2 = (C_DIV_SELECT0 + C_DIV_SELECT1) + 1;
parameter C_DIV_SELECT3 = (C_DIV_SELECT>>1) + 1;


reg [9:0] R_scl_cnt;    //用来产生I2C总线scl时钟线的计数器
reg R_scl_en;     //I2C总线scl时钟线使能信号
reg [3:0] R_state;
reg R_sda_mode;    
reg R_sda_reg;
reg [7:0] R_load_data;
reg [3:0] R_bit_cnt;
reg R_ack_flag;
reg [3:0] R_jump_state;
reg [7:0] R_read_data_reg;

wire W_scl_low_mid;
wire W_scl_high_mid;
wire W_scl_neg;
assign IO_sda = (R_sda_mode==1'b1) ? R_sda_reg : 1'bz;

always @ (posedge I_clk or negedge I_rst_n) begin
	if(!I_rst_n) begin
		R_scl_cnt <= 'd0;
	end
	else if(R_scl_en) begin
		if(R_scl_cnt == C_DIV_SELECT-1'b1) 
			R_scl_cnt <= 'd0;
		else 
			R_scl_cnt <= R_scl_cnt + 1'b1;
	end
	else 
		R_scl_cnt <= 'd0;
end

assign O_scl = (R_scl_cnt<=C_DIV_SELECT1) ? 1'b1 : 1'b0;
assign W_scl_low_mid = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0;
assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0;
assign W_scl_neg = (R_scl_cnt == C_DIV_SELECT3) ? 1'b1 : 1'b0; 

always @ (posedge I_clk or negedge I_rst_n) begin
	if(!I_rst_n) begin
		R_state <= 4'b0;
		R_sda_mode <= 1'b1;
		R_sda_reg <= 1'b1;
		R_bit_cnt <= 4'd0;
		O_done_flag <= 1'b0;
		R_jump_state <= 4'd0;
		R_ack_flag <= 1'b0;
		O_read_data <= 8'd0;
	end
	else if(I_i2c_recv_en) begin
		case(R_state)
			4'd0 : begin
						R_sda_mode <= 1'b1;
						R_sda_reg <= 1'b1;
						R_scl_en <= 1'b0;
						R_state <= 4'd1;
						R_bit_cnt <= 4'd0;
						O_done_flag <= 1'b0;
						R_jump_state <= 4'd0;
						R_read_data_reg <= 8'd0;
				   end
			4'd1 : begin
						R_load_data <= {I_dev_addr,1'b0};
						R_state <= 4'd3;
						R_jump_state <= R_state + 1'b1;
				   end
			4'd2 : begin
						R_load_data <= I_aord_addr;
						R_state <= 4'd4;
						R_jump_state <= R_state + 5'd5;
				   end
			4'd3 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_high_mid) begin
							R_sda_reg <= 1'b0;
							R_state <= 4'd4;
						end
						else 
							R_state <= 4'd3;
				   end
			4'd4 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_low_mid) begin
							if(R_bit_cnt==4'd8) begin
								R_bit_cnt <= 4'd0;
								R_state <= 4'd5;
							end
							else begin
								R_sda_reg <= R_load_data[7-R_bit_cnt];
								R_bit_cnt <= R_bit_vnt + 1'b1;
							end
						end
						else 
							R_state <= 4'd4;
				   end
			4'd5 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b0;
						R_sda_reg <= 1'b0;
						if(W_scl_high_mid) begin
							R_ack_flag <= IO_sda;
							R_state <= 4'd6;
						end
						else 
							R_state <= 4'd5;
				   end
			4'd6 : begin
						R_scl_en <= 1'b1;
						if(R_ack_flag==1'b0) begin
							if(W_scl_neg==1'b1) begin
								R_state <= R_jump_state;
								R_sda_mode <= 1'b1;
								R_Sda_reg <= 1'b1;
							end
							else 
								R_state <= 4'd6;
						end
						else 
							R_state <= 4'd0;
				   end
			4'd7 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_high_mid) begin
							R_sda_reg <= 1'b0;
							R_state <= 4'd8;
						end
						else 
							R_state <= 4'd7;
				   end
			4'd8 : begin
						R_load_data <= {I_dev_addr,1'b1};
						R_state <= 4'd4;
						R_jump_state <= 4'd9;
				   end
			4'd9 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b0;
						if(W_scl_high_mid) begin
							if(R_bit_cnt==4'd7) begin
								R_bit_cnt <= 4'd0;
								R_state <= 4'd10;
								O_read_data <= {R_read_data_reg[6:0],IO_sda};
							end
							else begin
								R_read_data_reg <= {R_read_data_reg[6:0],IO_sda};
								R_bit_cnt <= R_bit_cnt + 1'b1;
							end
						end
						else 
							R_state <= 4'd8;
				   end
			4'd10 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_low_mid) begin
							R_state <= 4'd11;
							R_sda_reg <= 1'b1;
						end
						else 
							R_state <= 4'd10;
					end
			4'd11 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_low_mid) begin
							R_state <= 4'd12;
							R_sda_reg <= 1'b0;
						end
						else 
							R_state <= 4'd11;
					end
			4'd12 : begin
						R_scl_en <= 1'b1;
						R_sda_mode <= 1'b1;
						if(W_scl_high_mid) begin
							R_sda_reg <= 1'b1;
							R_state <= 4'd13;
						end
						else 
							R_state <= 4'd12;
					end
			4'd13 : begin
						R_scl_en <= 1'b0;
						R_sda_mode <= 1'b1;
						R_sda_reg <= 1'b1;
						O_done_flag <= 1'b1;
						R_state <= 4'b0;
						R_read_data_reg <= 8'd0;
					end
			default : R_state <= 4'd0;
		endcase
	end
	else begin
		R_state <= 4'd0;
		R_sda_mode <= 1'b1;
		R_sda_reg <= 1'b1;
		R_bit_cnt <= 4'b0;
		O_done_flag <= 1'b0;
		R_jump_state <= 4'd0;
		R_read_data_reg <= 8'd0;
		R_ack_flag <= 1'b0;
	end
end













endmodule


基于I2C的随机读写EEPROM_第6张图片

1号红框是起始信号,在SCL高电平期间SDA有一个下降沿

2~9号红框是发送设备物理地址8’ha0(8’b1010_0000)

10号红框是应答位,在这个期间R_sda_mode保持低电平,SDA为输入

11~18号红框是发送字地址8’h23(8’b0010_0011)

19号红框是应答位,在这个期间R_sda_mode保持低电平,SDA为输入

20号红框是第二次起始位,在SCL高电平期间SDA有一个下降沿

21~28号红框是发送数据8’ha1(8’b1010_0001)

29号红框是应答位,在这个期间R_sda_mode保持低电平,SDA为输入

30~37号红框是读出的8-bit数据8’h45(8’b0100_0101),在这个期间R_sda_mode保持低电平,SDA为输入

38号红框是非应答位,在这个期间R_sda_mode保持高电平,主机(FPGA)通过SDA输出一个非应答位1

39号红框是停止信号,在SCL高电平期间SDA有一个上升沿

其他变量的时序细节这里不再展开,大家可以自己抓出来。至此,IIC接收模块全部设计完毕。
  
24LC04支持16-Bytes的连续写操作,当超过16-Bytes是后面写入的数据会覆盖先前写入的数据,下面是关于这一段的描述:
The write control byte, word address and the first data byte are transmitted to the 24LC04B/08B in the same way as in a byte write. But instead of generating a stop condition the master transmits up to 16 data bytes to the 24LC04B/08B which are temporarily stored in the on-chip page buffer and will be written into the memory after the master has transmitted a stop condition. After the receipt of each word, the four lower order address pointer bits are internally incremented by one. The higher order seven bits of the word address remains constant. If the master should transmit more than 16 words prior to generating the stop condition, the address counter will roll over and the previously received data will be overwritten. As with the byte write operation, once the stop condition is received an internal write cycle will begin.  基于I2C的随机读写EEPROM_第7张图片

你可能感兴趣的:(基于I2C的随机读写EEPROM)