【接口协议】FPGA实现IIC协议对EEPROM进行数据存储与读取(AT24C64)

0.序言

使用vivado实现IIC协议对EEPROM进行数据存储与读取。本文是基于正点原子的“达芬奇”开发板资料进行学习的笔记,对部分地方进行了修改,并进行了详细的讲解。

1.IIC协议简介

(1)简介

IIC(Inter-Integrated Circuit),即集成电路总线,是一种同步半双工串行总线,用于连接微控制器及外围设备,是用于数据量不大及传输距离不大的场合下的主从通信。IIC是为了与低速设备通信而发明的,所以IIC的传输速率比不上SPI。

(2)物理层接口协议

IIC一共有两根总线:一条是主设备提供给从设备的串行时钟线SCL,一条是双向传输的串行数据线SDA;
SCL:Serial clock line,是时钟线,也就是控制数据发送的时钟;
SDA:Serial data,是数据线,用于数据的传输;

若实现一主多从通信:所有从设备的数据线SDA都接到主设备提供的总线SDA上,所有从设备的时钟线SCL都接到总线SCL上,每一个从器件都有一个唯一的器件地址,主设备每次通信都会发送一个器件地址,与对应器件地址的从器件进行通信。
在这里插入图片描述
三态门:IIC是半双工通信,在数据线SDA上分时进行发送和接收数据,双向数据线SDA要通过三态门实现:
在这里插入图片描述
三态门使用:
定义数据线sdainout类型;
由条件语句控制三态门,控制端sda_dir拉高,三态门导通,数据线sda数据为FPGA输出数据sda_out;控制端sda_dir拉低,三态门关闭,门电路为高阻态;门电路为高阻态,fpga输入数据sda_in为数据线上数据sda在这里插入图片描述

(3)协议层

IIC数据传输包含五部分,开始、发送、接收、应答、结束五个部分构成。SDA和SCL数据线默认为上拉状态(高电平)。
A开始
当SCL为高电平时,SDA由高变为低,为传输开始标志。
B发送
在发送数据前应保证三态门处于输出状态(sda_dir=1’b1)。发送数据时,需在时钟线sclk为低电平时修改数据线sda的数据。
C接收
在接收数据前应保证三态门处于输入状态(sda_dir=1’b0)。接收数据时,外设也是在时钟线sclk为低电平时修改数据线sda的数据,所以接收数据采样是在时钟sclk上升沿。
D应答
从机每接收到主机发送的8bit数据后,从机会通过数据线sda向主机反馈一位应答信号,通常为低电平,表示已经接收到数据,主机可以通过这一位应答信号判断从机状态。应答是从机输入给主机的,此时应保证三态门处于输入状态(sda_dir=1’b0)。
E结束
当SCL为高电平时,SDA由低变为高,为传输结束标志。
在这里插入图片描述

2.EEPROM(使用的EEPROM型号:AT24C64)

(1)EEPROM简介

EEPROM,电可擦除可编程只读存储器,断电后数据不会丢失。

(2)芯片AT24C64硬件接口

在这里插入图片描述
在这里插入图片描述
A0~A2 :器件地址,在实现通信时,主机会发送三位器件地址,当三位器件地址与A2~A0 电平对应时,主从之间实现通信。这也是主从通信时片选的实现。所以在主从通信时,不同的从机,A2~A0接的电平不一样;
WP:写保护,当写保护引脚连接至 GND 时,芯片可以正常写,当写保护引脚连接至 VCC 时,使能写保护功能,此时禁止向芯片写入数据,只能进行读操作;
SCL和SDA即IIC通信的时钟线和数据线。

(2) AT24C64读写时序

A 序言
AT24C64有8k个字节的存储空间,分成了256页,每页32个字节。且有多种读写模式,有字节写、页写、当前地址读、随机地址读、连续读模式。
B 写数据

(a)字节写
时序图:
在这里插入图片描述

说明:一次字节写在开始(START)结束(STOP)标志之间,包含了一个字节的器件地址,两个字节的数据地址,和一个字节的数据,且每个字节传输完后都有一位应答信号ACK(低电平)
器件地址:8位器件地址,高4位默认1010,后接3位从机对应的器件地址A2~A0,最后还有一位读写控制位(R/W)(此时为写数据给0,后面读数据给1);
在这里插入图片描述
数据地址:前面提到,AT24C64,有8k个字节数据,每个字节数据对应一个地址,共13位地址,用两个字节传输数据地址,在第一个数据地址低五位传输数据地址的高五位,在第二个数据地址字节传输数据地址低8位。(图中的*号表示无关位,因为这个图截自数据手册,手册同时包含了AT24C64和AT24C32两个芯片的说明,AT24C32存储空间只有AT24C64的一半,所以会少一位数据地址,所以+号对于AT24C64芯片来说是数据地址最高位,对于AT24C32来说也是无关位);
数据:传给EEPROM的一个字节8位数据。

(b)页写
时序图:
在这里插入图片描述
说明:与字节写不同的是,在给完数据地址后,不仅仅写了一个字节数据,是连续写了多个字节数据;
注意:我们只给了第一个数据的存储地址,后面字节数据的13位存储地址高8位保持不变低5位在给的存储地址的基础上累加,因为前面提到,芯片的存储空间是按页排布(256页x32字节),相当于页选信号(高8位)不变,在当前页内存储地址(低5位)累加,当连续存入的数据超过了当前页的存储空间(32个字节),数据地址又会从当前页第一个数据重新存入。

(c )注意(注意注意注意:强调一下)
完成单次写操作,AT24C64并不是立刻将数据存入,而是需要需要花费一定时间,数据手册上说是最大是10ms,实际上达不到10ms,大家可以试一下,给的代码中是10ms一次写操作。
在这里插入图片描述
C 随机地址读(只介绍这一种,其他两种少见)

(a)时序图
在这里插入图片描述
(b)说明
随机地址读为伪写操作+读操作伪写操作给读操作提供地址,对应字节写的开始标志+器件地址+数据地址,读操作包含一个字节器件地址和一个字节数据,最后一个停止标志;
注意:随机地址读数据后无应答信号

3.实例

(1)功能描述

使用字节写和随机地址读模式,给EEPROM的0到63地址中,存入数据0到63,并从任意地址中取出一个数据进行验证,验证正确点亮led灯。

(2)结构框图

在这里插入图片描述
TOP module:顶层模块,用于例化读写控制模块IIC驱动模块
WR control:读写控制模块,控制IIC驱动模块的读/写操作,产生读写数据有效标志/读写控制信号/读写数据地址/写入的数据,接收读出的数据/单次IIC操作完成标志,并对接收数据进行验证,以led灯进行指示;
IIC_drive:IIC驱动模块,将需要存储的数据转换为IIC数据格式并发送,将读出的IIC数据还原为数据格式;

(3)实现

(A)顶层模块(IIC_EEPROM_top)
功能描述:用于例化读写控制模块IIC驱动模块
(B)读写控制模块(WR_control)
功能描述:控制IIC模块的读写操作,并对写数据进行验证。
首先,产生IIC驱动模块的驱动时钟:
系统时钟是50MHz,AT24C64的对时钟线上时钟的要求如下,有100kHz(1.8V,2.5V,2.7V)和400kH(5V),在正点原子达芬奇开发板中提供的电源为3.3V,250kHz也能正常工作,所以使用的250kHz。为满足开始/结束标志,和数据传输时修改数据线变量在时钟线的低电平时刻,这里的驱动时钟使用的是数据线上时钟的4倍频率,即1MHz。所以分频系数=(系统时钟频率/sclk时钟频率)/4;要实现n分频,计数器记到**(n/2)-1**,时钟状态就要翻转一次(只针对于偶数倍分频器,奇数倍分频器要使用时钟双沿,或者先倍频)。

在这里插入图片描述

其次,实现存入数据读出数据验证数据三个状态:实现每10ms写一个数据,在写数据时需要利用加法器产生数据(data_wr)地址(data_addr)读写控制位(rw_con=1’b0)使能有效标志(data_valid,为iic驱动模块提供的写开始标志),将0到63写入地址0到63的存储空间;然后随机读一个地址中的数(这里读的是十进制数10),读书据时将想验证数据的地址(read_data_add)赋给地址变量(data_addr),同时产生读写控制位(rw_con=1’b1)写使能有效标志(data_valid)。读出后进行验证,一致后点亮led灯。

(C )IIC驱动模块(iic_drive)
有限状态机:
在这里插入图片描述

说明,因为使用的是字节写随机地址读,根据上图中由AT24C64数据手册字节写模式随机地址读模式的时序图可以得出:
字节写:包含写器件地址(开始标志只有一位,将开始标志包含入内)、高位数据地址低位数据地址写入数据结束状态五个状态;
随机地址读:包含写器件地址(开始标志只有一位,将开始标志包含入内)、高位数据地址低位数据地址读操作器件地址读出数据结束状态六个状态。
同时在不传输数据为空闲状态,所以整合后的状态机如上图所示,共8个状态,每个状态时序说明如下:

a 空闲状态(st_idle
对控制信号暂存,使能(data_val)有效时对输入数据进行暂存,包括写的数据(data_wr),地址(data_addr),读写控制信号(rw_con);

b写器件地址(st_sladder_w
在这里插入图片描述
c 地址高八位(st_addr_h
在这里插入图片描述
d 地址低八位(st_addr_l
在这里插入图片描述
e写入数据(st_data_w
在这里插入图片描述

f读器件地址(st_sladder_r
在这里插入图片描述
g读入数据(st_data_r
在这里插入图片描述
h结束状态(st_done_byte
结束标志,产生IIC操作完成标志信号(iic_down)。且当是读数据时,将读到的数据输出。
在这里插入图片描述

(4)代码

A 顶层模块

`timescale 1ns / 1ps
module IIC_EEPROM_top(
    input   clk     ,
    input   rst_n   ,
    //output  led_s   ,
    output  sclk    ,
    inout   sda,
	output  led_s
    );
    wire            dri_clk;    //驱动时钟
    wire            data_val;   //给iic驱动模块数据有效标志
    wire            rw_con;     //读写控制信号
    wire	[15:0]	data_addr;  //数据地址
    wire    [7:0]	data_wr;    //写入EEPROM的数据		
    wire	[7:0]	data_re;    //从EEPROM读出的数据		
	wire			iic_done;   //单次读写操作完成标志	


    WR_control #
	(
		.sys_clk_fre 	(26'd50_000_000),		//系统时钟频率
		.sclk_fre 		(18'd250_000)
	)
	u_WR_control(
	    .clk			(clk),
	    .rst_n		    (rst_n),
	    .dri_clk		(dri_clk),			//iic驱动时钟
	    .data_valid	    (data_val),			//数据有效标志
	    .rw_con		    (rw_con),			//读写控制位
	    .data_addr	    (data_addr),		//读写对应的EEPROM中的地址
	    .data_wr		(data_wr),			//写操作时需要写入的数据
	    .data_re		(data_re),			//读操作时读出的数据
	    .iic_done	    (iic_done),			//一次IIC完成的标志
		.led_s          (led_s)
    );

    iic_drive #
    (
		.SLAVE_ADDR (7'b1010_000)
	)
    u_iic_drive
	(
	    .rst_n		    (rst_n),
	    .dri_clk		(dri_clk),		//iic驱动时钟
	    .data_val	    (data_val),		//数据有效标志
	    .rw_con		    (rw_con),		//读写控制位
	    .data_addr	    (data_addr),
	    .data_wr		(data_wr),	
	    .data_re		(data_re),			//读操作时读出的数据
	    .iic_done	    (iic_done),
	    .sclk	        (sclk),
	    .sda            (sda)
    );
	// EEPROM_AT24C64 u_EEPROM_AT24C64(
    //     .scl	(sclk),
    //     .sda	(sda)
    //    );


endmodule

B 读写控制模块

//读写控制模块
module WR_control
	#(
		parameter sys_clk_fre = 26'd50_000_000,		//系统时钟频率
		parameter sclk_fre = 18'd250_000			//sclk时钟频率
	)
	(
	input 					clk			,
	input 					rst_n		,
	output 	reg 			dri_clk		,			//iic驱动时钟
	output 	reg				data_valid	,			//数据有效标志
	output  reg			    rw_con		,			//读写控制位
	output	reg	[15:0]		data_addr	,			//读写对应的EEPROM中的地址
	output 	reg [7:0]		data_wr		,			//写操作时需要写入的数据
	input   	[7:0]		data_re		,			//读操作时读出的数据
	input 					iic_done	,			//一次IIC完成的标志	
	output reg 				led_s
    );
	

	parameter wr_cnt_max = 10_000;			//写操作时钟计数
	parameter read_data_add = 8'd10;		//验证读出数据的位置
	parameter wr_data_num = 8'd64;			//写入数据个数
	
	//使用4状态状态机
	parameter  	idle 		= 4'b0001;
	parameter  	state_write = 4'b0010;
	parameter 	state_read 	= 4'b0100;
	parameter 	state_vf 	= 4'b1000;
	reg [3:0] current_state;
	reg [3:0] next_state = 4'b0000;

	
	//产生驱动时钟
	wire [8:0] fre_divi;
	reg [9:0] divi_cnt;
	assign fre_divi = (sys_clk_fre/sclk_fre)>>2'd2;
	always@(posedge clk or negedge rst_n)begin
		if(!rst_n)begin
			dri_clk <= 1'b0;
			divi_cnt <= 10'd0;
		end
		else if(divi_cnt == (fre_divi[8:1]-1'b1))begin
			dri_clk <= ~dri_clk;
			divi_cnt <= 10'd0;
		end
		else begin
			dri_clk <= dri_clk;
			divi_cnt <= divi_cnt + 1'b1;
		end
	end
	

	//10ms计数器
	reg [13:0] wr_cnt; 
	always@(posedge dri_clk or negedge rst_n)begin
		if(!rst_n) 
			wr_cnt <= 0;
		else if(current_state == state_write)
			if(wr_cnt == 14'd9_999)
				wr_cnt <= 14'd0;
			else 
				wr_cnt <= wr_cnt+1'b1;
		else
			wr_cnt <= 0;
	end
	//读写数据产生和验证状态机(以后的时钟都在IIC驱动时钟下完成)
	//第一段:时序always过程块,将次态赋给现态
	always@(posedge dri_clk or negedge rst_n)begin
		if(!rst_n)
			current_state <= idle;
		else
			current_state <= next_state;
	end
	//第二段:组合always过程块,状态转移条件判断
	always@(*)begin
		case(current_state)
			idle:begin 
				next_state = state_write;  
			end		
            state_write:begin 
				if(wr_cnt == (wr_cnt_max-1))
					if(data_wr == wr_data_num-1)
						next_state = state_read;
					else
						next_state = state_write;
				else
					next_state = state_write;
			end
			state_read:begin
				if(iic_done == 1'b1)
					next_state = state_vf;
				else
					next_state = state_read;
			end
			default:begin
				next_state = state_vf;
			end
		endcase
	end
	
	//第三段:状态赋值
	reg 	send_valid;
	reg [7:0]	read_data;
	always@(posedge dri_clk or negedge rst_n)begin
		if(!rst_n)begin
			data_addr <= 16'd0;	
			data_wr	  <= 8'd0;
			data_valid <= 1'b0;
			rw_con <= 1'b1;
			send_valid <= 1'b0;
			read_data <= 8'd0;
			led_s <= 1'b0;
		end
		else begin
			case(current_state)
				state_write:begin
					rw_con <= 1'b0;
					if(wr_cnt == 14'd0)begin
						data_valid <= 1'b1;
					end 		
					else if(wr_cnt == 14'd9_999)begin
						data_addr <= data_addr + 1'b1;	
						data_wr	  <= data_wr + 1'b1;
					end
					else begin
						data_valid <= 1'b0;
						data_addr <= data_addr;
						data_wr	  <= data_wr;
					end
				end
				state_read:begin
					rw_con <= 1'b1;
					send_valid <= 1'b1;
					if(!send_valid)begin
						data_valid <= 1'b1;
					end
					else begin
						data_valid <= 1'b0;
					end
					data_addr <= read_data_add;
					if(iic_done)
						send_valid <= 1'b0;
				end
				state_vf:begin
					read_data <= data_re;
					if(read_data == read_data_add)
						led_s <= 1'b1;
				end
				default:;
			endcase
		end
	end	




endmodule


C IIC驱动模块

//iic驱动模块
module 	iic_drive
	#(
		parameter SLAVE_ADDR = 7'b1010_000
	)
	(
	input 					rst_n		,
	input 		 			dri_clk		,		//iic驱动时钟
	input 					data_val	,		//数据有效标志
	input 				    rw_con		,		//读写控制位
	//读写对应的EEPROM中的地址
	input 		[15:0]		data_addr	,
	//写操作时需要写入的数据	
	input 		 [7:0]		data_wr		,	
	//读操作时读出的数据
	output reg   [7:0]		data_re		,			//读操作时读出的数据
	output reg 				iic_done	,
	output reg 				sclk		,
	inout  					sda				
    );
	parameter WRITE = 1'b0;
	parameter READ = 1'b1;
	
	//三态门
	reg sda_dir;		//三态门控制信号
	reg sda_out;		//三态门输出数据
	wire sda_in;		//三态门输入数据
	assign sda = sda_dir?sda_out:1'bz;
	assign sda_in = sda;
	
	
	reg done;	//iic字节操作完成
	//三段式状态机,一位独热编码
	parameter st_idle 		= 8'b0000_0001;
	parameter st_sladder_w 	= 8'b0000_0010;
	parameter st_addr_h 	= 8'b0000_0100;
	parameter st_addr_l		= 8'b0000_1000;
	parameter st_data_w		= 8'b0001_0000;
	parameter st_sladder_r	= 8'b0010_0000;
	parameter st_data_r		= 8'b0100_0000;
	parameter st_done_byte	= 8'b1000_0000;
	reg [7:0] cur_state = 8'b0000_0000;
	reg [7:0] next_state = 8'b0000_0000;
	//时序always过程块
	always@(posedge dri_clk or negedge rst_n)begin
		if(!rst_n)
			cur_state <= st_idle;
		else
			cur_state <= next_state;
	end
	//组合always过程块
	always@(*)begin
		case(cur_state)
			st_idle:begin 
				if(data_val == 1'b1)
					next_state = st_sladder_w;
				else
					next_state = st_idle;
			end
			st_sladder_w:begin
				if(done == 1'b1)
					next_state = st_addr_h;
				else 
					next_state = st_sladder_w;
			end
			st_addr_h:begin
				if(done)
					next_state = st_addr_l;
				else
					next_state = st_addr_h;
			end
			st_addr_l:begin
				if(done)begin
					if(rw_con == 1'b0)
						next_state = st_data_w;
					else
						next_state = st_sladder_r;
				end	
				else
					next_state = st_addr_l;
			end
			st_data_w:begin
				if(done)
					next_state = st_done_byte;
				else	
					next_state = st_data_w;
			end
			st_sladder_r:begin
				if(done)
					next_state = st_data_r;
				else
					next_state = st_sladder_r;
			end
			st_data_r:begin
				if(done)
					next_state = st_done_byte;
				else
					next_state = st_data_r;
			end	
			st_done_byte:begin
				if(done)
					next_state = st_idle;
				else
					next_state = st_done_byte;
			end	
			default:begin 
				next_state = st_idle;
			end
		endcase
	end
	//状态赋值
	reg [5:0] 	cnt;		//状态赋值计数器
	reg 		rw_con_r;	//读写标志位暂存
	reg [7:0]	data_wr_r;	//写入数据暂存
	reg [15:0]	data_addr_r;//写入数据地址
	//reg done;				//字节操作完成标志位(前面已经声明)
	//reg sda_dir;			//三态门控制信号(前面已经声明)
	//reg sclk;				//时钟信号(前面已经声明)
	//reg sda_out;			//输出数据线(前面已经声明)
	reg 		iic_ack;	//iic应答信号
	reg [7:0]	data_re_r;	//iic读入的数据暂存变量
	//reg iic_done;			//iic操作完成信号(前面已经声明)
	//reg [7:0]		data_re;//读入数据(前面已经声明)
	always@(posedge dri_clk or negedge rst_n)begin
		if(!rst_n)begin
			cnt 		<= 1'b0;		
			rw_con_r 	<= 1'b0;
			data_wr_r 	<= 8'd0;
			data_addr_r <= 16'd0;
			done 		<= 1'b0;
			sda_dir		<= 1'b1;
			sclk		<= 1'b1;
			sda_out		<= 1'b1;
			iic_ack		<= 1'b0;
			data_re_r 	<= 8'd0;
			iic_done 	<= 1'b0;
			data_re		<= 8'd0;
		end
		else begin
			case(cur_state)
				st_idle:begin	//信号需要复位
					cnt <= 6'b0;
					sclk <= 1'b1;                     
                	sda_out <= 1'b1;
					if(data_val == 1'b1)begin		//输入数据暂存
						rw_con_r  <= rw_con;
						data_wr_r <= data_wr;
						data_addr_r <= data_addr;
					end
				end
				st_sladder_w:begin 
					cnt <= cnt + 1'b1;
					case(cnt)
						6'd0: sda_out <= 1'b0;
						6'd1: sclk <= 1'b0;
						6'd2: sda_out <= SLAVE_ADDR[6];
						6'd3: sclk <= 1'b1;
						6'd5: sclk <= 1'b0;
						6'd6: sda_out <= SLAVE_ADDR[5];
						6'd7: sclk <= 1'b1;
						6'd9: sclk <= 1'b0;	
						6'd10: sda_out <= SLAVE_ADDR[4];
						6'd11: sclk <= 1'b1;
						6'd13: sclk <= 1'b0;
						6'd14: sda_out <= SLAVE_ADDR[3];		
						6'd15: sclk <= 1'b1;
						6'd17: sclk <= 1'b0;
						6'd18: sda_out <= SLAVE_ADDR[2];		
						6'd19: sclk <= 1'b1;
						6'd21: sclk <= 1'b0;
						6'd22: sda_out <= SLAVE_ADDR[1];		
						6'd23: sclk <= 1'b1;
						6'd25: sclk <= 1'b0;
						6'd26: sda_out <= SLAVE_ADDR[0];		
						6'd27: sclk <= 1'b1;
						6'd29: sclk <= 1'b0;						
						6'd30: sda_out <= 1'b0;		//写数据标志		
						6'd31: sclk <= 1'b1;
						6'd33: sclk <= 1'b0;
						6'd34: begin					//三态门的控制
							sda_dir <= 1'b0;			//将三态门调整为输入状态	
							sda_out <= 1'b1;			//首先将数据线拉高
						end
						6'd35: sclk <= 1'b1;
						6'd36: begin
							done <= 1'b1;				//单byte操作完成标志
							if(sda_in == 1'b1)
								iic_ack <= 1'b1;		//应答信号,拉高表示未应答	
						end
						6'd37: begin
							sclk <= 1'b0;
							done <= 1'b0;
							cnt <= 6'd0;
						end	
						default: ;
					endcase
				end
				st_addr_h:begin
					cnt <= cnt + 1'b1;
					case(cnt)
						6'd0:begin 
							sda_dir <= 1'b1;
							sda_out <= data_addr_r[15];		//无关位3
						end
						6'd1: sclk <= 1'b1;
						6'd3: sclk <= 1'b0;
						6'd4: sda_out <= data_addr_r[14];	//无关位2
						6'd5: sclk <= 1'b1;
						6'd7: sclk <= 1'b0;
						6'd8: sda_out <= data_addr_r[13];	//无关位1
						6'd9: sclk <= 1'b1;
						6'd11: sclk <= 1'b0;	
						6'd12: sda_out <= data_addr_r[12];	//地址最高位
						6'd13: sclk <= 1'b1;
						6'd15: sclk <= 1'b0;	
						6'd16: sda_out <= data_addr_r[11];	
						6'd17: sclk <= 1'b1;
						6'd19: sclk <= 1'b0;	
						6'd20: sda_out <= data_addr_r[10];	
						6'd21: sclk <= 1'b1;
						6'd23: sclk <= 1'b0;	
						6'd24: sda_out <= data_addr_r[9];
						6'd25: sclk <= 1'b1;
						6'd27: sclk <= 1'b0;	
						6'd28: sda_out <= data_addr_r[8];	//地址高八位最后一位
						6'd29: sclk <= 1'b1;
						6'd31: sclk <= 1'b0;	
						6'd32:begin
							sda_dir <= 1'b0;	//将三态门调整为输入状态
							sda_out <= 1'b1;						
						end
						6'd33: sclk <= 1'b1;
						6'd34:begin
							done <= 1'b1;
							if(sda_in == 1'b1)		//单byte操作完成标志
								iic_ack <= 1'b1;	//应答信号,拉高表示未应答	
						end
						6'd35:begin
							sclk <= 1'b0;
							cnt <= 6'd0;
							done <= 1'b0;
						end
						default: ;
					endcase
				end
				st_addr_l:begin
					cnt <= cnt + 1'b1;
					case(cnt)
						6'd0:begin 
							sda_dir <= 1'b1;
							sda_out <= data_addr_r[7];		//无关位3
						end
						6'd1: sclk <= 1'b1;
						6'd3: sclk <= 1'b0;
						6'd4: sda_out <= data_addr_r[6];	//无关位2
						6'd5: sclk <= 1'b1;
						6'd7: sclk <= 1'b0;
						6'd8: sda_out <= data_addr_r[5];	//无关位1
						6'd9: sclk <= 1'b1;
						6'd11: sclk <= 1'b0;	
						6'd12: sda_out <= data_addr_r[4];	//地址最高位
						6'd13: sclk <= 1'b1;
						6'd15: sclk <= 1'b0;	
						6'd16: sda_out <= data_addr_r[3];	
						6'd17: sclk <= 1'b1;
						6'd19: sclk <= 1'b0;	
						6'd20: sda_out <= data_addr_r[2];	
						6'd21: sclk <= 1'b1;
						6'd23: sclk <= 1'b0;	
						6'd24: sda_out <= data_addr_r[1];
						6'd25: sclk <= 1'b1;
						6'd27: sclk <= 1'b0;	
						6'd28: sda_out <= data_addr_r[0];	//地址高八位最后一位
						6'd29: sclk <= 1'b1;
						6'd31: sclk <= 1'b0;	
						6'd32:begin
							sda_dir <= 1'b0;	//将三态门调整为输入状态
							sda_out <= 1'b1;						
						end
						6'd33: sclk <= 1'b1;
						6'd34:begin
							done <= 1'b1;
							if(sda_in == 1'b1)		//单byte操作完成标志
								iic_ack <= 1'b1;	//应答信号,拉高表示未应答	
						end
						6'd35:begin
							sclk <= 1'b0;
							cnt <= 6'd0;
							done <= 1'b0;
						end
						default: ;
					endcase
				end
				st_data_w:begin
					cnt <= cnt + 1'b1;
					case(cnt)
						6'd0:begin 
							sda_dir <= 1'b1;
							sda_out <= data_wr_r[7];		//无关位3
						end
						6'd1: sclk <= 1'b1;
						6'd3: sclk <= 1'b0;
						6'd4: sda_out <= data_wr_r[6];	
						6'd5: sclk <= 1'b1;
						6'd7: sclk <= 1'b0;
						6'd8: sda_out <= data_wr_r[5];	
						6'd9: sclk <= 1'b1;
						6'd11: sclk <= 1'b0;	
						6'd12: sda_out <= data_wr_r[4];	
						6'd13: sclk <= 1'b1;
						6'd15: sclk <= 1'b0;	
						6'd16: sda_out <= data_wr_r[3];	
						6'd17: sclk <= 1'b1;
						6'd19: sclk <= 1'b0;	
						6'd20: sda_out <= data_wr_r[2];	
						6'd21: sclk <= 1'b1;
						6'd23: sclk <= 1'b0;	
						6'd24: sda_out <= data_wr_r[1];
						6'd25: sclk <= 1'b1;
						6'd27: sclk <= 1'b0;	
						6'd28: sda_out <= data_wr_r[0];	//地址最低位
						6'd29: sclk <= 1'b1;
						6'd31: sclk <= 1'b0;	
						6'd32:begin
							sda_dir <= 1'b0;	//将三态门调整为输入状态
							sda_out <= 1'b1;
						end
						6'd33: sclk <= 1'b1;
						6'd34:begin
							done <= 1'b1;
							if(sda_in == 1'b1)		//单byte操作完成标志
								iic_ack <= 1'b1;	//应答信号,拉高表示未应答	
						end
						6'd35:begin
							sclk <= 1'b0;
							cnt <= 6'd0;
							done <= 1'b0;
						end
						default: ;
					endcase
				end
				st_sladder_r:begin
					cnt <= cnt + 1'b1;
					case(cnt)
						6'd0:begin 
							sda_dir <= 1'b1;		//三态门调整为输入
							sda_out <= 1'b1;//数据线拉高,为开始标志做准备		
						end
						6'd1: sclk <= 1'b1;
						6'd2: sda_out <= 1'b0;
						6'd3: sclk <= 1'b0;
						6'd4: sda_out <= SLAVE_ADDR[6];	
						6'd5: sclk <= 1'b1;
						6'd7: sclk <= 1'b0;
						6'd8: sda_out <= SLAVE_ADDR[5];	
						6'd9: sclk <= 1'b1;
						6'd11: sclk <= 1'b0;	
						6'd12: sda_out <= SLAVE_ADDR[4];
						6'd13: sclk <= 1'b1;
						6'd15: sclk <= 1'b0;	
						6'd16: sda_out <= SLAVE_ADDR[3];	
						6'd17: sclk <= 1'b1;
						6'd19: sclk <= 1'b0;	
						6'd20: sda_out <= SLAVE_ADDR[2];	
						6'd21: sclk <= 1'b1;
						6'd23: sclk <= 1'b0;	
						6'd24: sda_out <= SLAVE_ADDR[1];
						6'd25: sclk <= 1'b1;
						6'd27: sclk <= 1'b0;	
						6'd28: sda_out <= SLAVE_ADDR[0];
						6'd29: sclk <= 1'b1;
						6'd31: sclk <= 1'b0;	
						6'd32: sda_out <= 1'b1;		//读
						6'd33: sclk <= 1'b1;
						6'd35: sclk <= 1'b0;
						6'd36:begin
							sda_dir <= 1'b0;	//将三态门调整为输入状态	
							sda_out <= 1'b1;				
						end
						6'd37: sclk <= 1'b1;
						6'd38:begin
							done <= 1'b1;
							if(sda_in == 1'b1)		//单byte操作完成标志
								iic_ack <= 1'b1;	//应答信号,拉高表示未应答	
						end
						6'd39:begin
							sclk <= 1'b0;
							cnt <= 6'd0;
							done <= 1'b0;
						end
						default: ;
					endcase
				end
				st_data_r:begin
					cnt <= cnt + 1'b1;
					case(cnt)
					//*******
						6'd0: sda_dir <= 1'b0;
						6'd1: begin
							sclk <= 1'b1;
							data_re_r[7] <= sda_in;
						end
						6'd3: sclk <= 1'b0;
						6'd5:  begin
							sclk <= 1'b1;
							data_re_r[6] <= sda_in;
						end
						6'd7: sclk <= 1'b0;
						6'd9: begin
							sclk <= 1'b1;
							data_re_r[5] <= sda_in;
						end
						6'd11: sclk <= 1'b0;
						6'd13:begin
							sclk <= 1'b1;
							data_re_r[4] <= sda_in;
						end
						6'd15: sclk <= 1'b0;		
						6'd17: begin
							sclk <= 1'b1;
							data_re_r[3] <= sda_in;
						end
						6'd19: sclk <= 1'b0;		
						6'd21: begin
							sclk <= 1'b1;
							data_re_r[2] <= sda_in;
						end
						6'd23: sclk <= 1'b0;	
						6'd25: begin
							sclk <= 1'b1;
							data_re_r[1] <= sda_in;
						end
						6'd27: sclk <= 1'b0;	
						6'd29: begin
							sclk <= 1'b1;
							data_re_r[0] <= sda_in;
						end
						6'd31: sclk <= 1'b0;
						//******************
						// 7'd32: begin
                        // 	sda_dir <= 1'b1;             
                        // 	sda_out <= 1'b1;
                    	// end
						6'd33: sclk <= 1'b1;
						6'd34: done <= 1'b1;
						6'd35: begin
							sclk <= 1'b0;
							done <= 1'b0;
							cnt <= 6'd0;
						end
						default: ;
					endcase
				end
				st_done_byte:begin
					cnt <= cnt + 1'b1;
					case(cnt)
						//**************时间不够
						6'd0:begin
							sda_dir <= 1'b1;	//将三态门置为输出状态
							sda_out <= 1'b0; //将数据线拉低,为结束标志做准备
						end
						6'd1:sclk <= 1'b1;
						6'd2:sda_out <= 1'b1;//结束标志
						6'd6:begin
							done <= 1'b1;
							iic_done <= 1'b1;
							data_re <= data_re_r;
						end
						6'd7:begin
							done <= 1'b0;
							iic_done <= 1'b0;
							cnt <= 6'd0;
						end
						default: ;	
					endcase
				end
				default: ;	
			endcase
		end
	end

endmodule

(5)上板验证

led_s对应的led灯被点亮,验证成功。
在这里插入图片描述

(6)仿真

如果需要仿真,这里提供了一个AT24C64的仿真模型(用verilog仿真的AT24C64从机),如果需要仿真,将下面模型例化到顶层模块(即将顶层模块中最下面注释的模块取消注释)

`timescale 1ns/1ns
`define timeslice 600			 //在SCL下降沿延时600ns输出应答信号  400KHz
//`define timeslice 300

module EEPROM_AT24C64(
           scl,
           sda
       );
input scl;               //串行时钟线
inout sda;               //串行数据线

reg out_flag;            //SDA数据输出的控制信号

reg[7:0] memory[8191:0]; //数组模拟存储器
reg[12:0]address;        //地址总线
reg[7:0]memory_buf;      //数据输入输出寄存器
reg[7:0]sda_buf;         //SDA数据输出寄存器
reg[7:0]shift;           //SDA数据输入寄存器
reg[7:0]addr_byte_h;     //EEPROM存储单元地址高字节寄存器
reg[7:0]addr_byte_l;     //EEPROM存储单元地址低字节寄存器
reg[7:0]ctrl_byte;       //控制字寄存器
reg[1:0]State;           //状态寄存器

integer i;

//---------------------------
parameter
    r7 = 8'b1010_1111,  w7 = 8'b1010_1110,   //main7
    r6 = 8'b1010_1101,  w6 = 8'b1010_1100,   //main6
    r5 = 8'b1010_1011,  w5 = 8'b1010_1010,   //main5
    r4 = 8'b1010_1001,  w4 = 8'b1010_1000,   //main4
    r3 = 8'b1010_0111,  w3 = 8'b1010_0110,   //main3
    r2 = 8'b1010_0101,  w2 = 8'b1010_0100,   //main2
    r1 = 8'b1010_0011,  w1 = 8'b1010_0010,   //main1
    r0 = 8'b1010_0001,  w0 = 8'b1010_0000;   //main0
//---------------------------

assign sda = (out_flag == 1) ? sda_buf[7] : 1'bz;

//------------寄存器和存储器初始化---------------
initial begin
    addr_byte_h    = 0;
    addr_byte_l    = 0;
    ctrl_byte    = 0;
    out_flag     = 0;
    sda_buf      = 0;
    State        = 2'b00;
    memory_buf   = 0;
    address      = 0;
    shift        = 0;

    for(i=0;i<=8191;i=i+1)
        memory[i] = 0;
end

//启动信号
always@(negedge sda) begin
    if(scl == 1) begin
        State = State + 1;
        if(State == 2'b11)
            disable write_to_eeprom;
    end
end

//主状态机
always@(posedge sda) begin
    if(scl == 1)                //停止操作
        stop_W_R;
    else begin
        casex(State)
            2'b01: begin
                read_in;
                if(ctrl_byte == w7 || ctrl_byte == w6
                   || ctrl_byte == w5  || ctrl_byte == w4
                   || ctrl_byte == w3  || ctrl_byte == w2
                   || ctrl_byte == w1  || ctrl_byte == w0) begin
                    State = 2'b10;
                    write_to_eeprom;    //写操作
                end
                else
                    State = 2'b00;
                //State = State;
            end

            2'b11:
                read_from_eeprom;

            default:
                State = 2'b00;
        endcase
    end
end     //主状态机结束

//操作停止
task stop_W_R;
    begin
        State        = 2'b00;
        addr_byte_h  = 0;
        addr_byte_l  = 0;
        ctrl_byte    = 0;
        out_flag     = 0;
        sda_buf      = 0;
    end
endtask

//读进控制字和存储单元地址
task read_in;
    begin
        shift_in(ctrl_byte);
        shift_in(addr_byte_h);
        shift_in(addr_byte_l);
    end
endtask

//EEPROM的写操作
task write_to_eeprom;
    begin
        shift_in(memory_buf);
        address = {addr_byte_h[4:0], addr_byte_l};
        memory[address] = memory_buf;
        State = 2'b00;
    end
endtask

//EEPROM的读操作
task read_from_eeprom;
    begin
        shift_in(ctrl_byte);
        if(ctrl_byte == r7 || ctrl_byte == w6
           || ctrl_byte == r5  || ctrl_byte == r4
           || ctrl_byte == r3  || ctrl_byte == r2
           || ctrl_byte == r1  || ctrl_byte == r0) begin
            address = {addr_byte_h[4:0], addr_byte_l};
            sda_buf = memory[address];
            shift_out;
            State = 2'b00;
        end
    end
endtask

//SDA数据线上的数据存入寄存器,数据在SCL的高电平有效
task shift_in;
    output[7:0]shift;
    begin
        @(posedge scl) shift[7] = sda;
        @(posedge scl) shift[6] = sda;
        @(posedge scl) shift[5] = sda;
        @(posedge scl) shift[4] = sda;
        @(posedge scl) shift[3] = sda;
        @(posedge scl) shift[2] = sda;
        @(posedge scl) shift[1] = sda;
        @(posedge scl) shift[0] = sda;

        @(negedge scl) begin
             #`timeslice;
             out_flag = 1;     //应答信号输出
             sda_buf = 0;
         end

         @(negedge scl) begin
              #`timeslice;
              out_flag = 0;
          end
      end
  endtask

  //EEPROM存储器中的数据通过SDA数据线输出,数据在SCL低电平时变化
  task shift_out;
      begin
          out_flag = 1;
          for(i=6; i>=0; i=i-1) begin
              @(negedge scl);
              #`timeslice;
              sda_buf = sda_buf << 1;
          end
          @(negedge scl) #`timeslice sda_buf[7] = 1;    //非应答信号输出
          @(negedge scl) #`timeslice out_flag = 0;
      end
  endtask

  endmodule
      //eeprom.v文件结束

vivado联合modelsim仿真结果:
可以看出,经验证,led被点亮。
在这里插入图片描述

  • 9
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
FPGA实现IIC总线驱动来读写EEPROM是一种常见的应用。通过引用和中的内容,我们可以了解到以下步骤和问题解决方案: 1. IIC协议简介:IIC协议是一种串行通信协议,它允许多个设备在同一总线上进行通信。IIC协议在硬件结构上包含了两根线路,即串行数据线(SDA)和串行时钟线(SCL)。 2. 设计思想:在FPGA实现IIC读写EEPROM的设计思路是通过编写Verilog HDL代码来实现IIC总线的驱动功能,并通过IIC总线对EEPROM进行读写测试。 3. RTL代码重要部分:在RTL代码的实现中,需要考虑起始信号、应答信号以及数据的读写时序。根据引用的内容,可能会遇到从机无应答、应答正常但读出数据不对以及时钟问题等问题。对于从机无应答的情况,可以通过添加状态机中的等待应答的机制来解决。对于应答正常但读出数据不对的情况,可能是EEPROM本身的问题,可以尝试重新上电来解决。而时钟问题可以通过添加边沿检测程序来解决。 4. 开发过程遇到的问题:在开发过程中可能会遇到一些问题,如从机无应答、读出数据错误等。通过使用SignalTap抓取波形图可以帮助分析问题所在。 综上所述,实现IIC读写EEPROM的FPGA设计可以根据IIC协议的时序特点编写Verilog HDL代码,并解决可能出现的问题,如从机无应答、读出数据错误等。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [FPGA中 通过IIC读写EEPROM驱动代码示例](https://download.csdn.net/download/qq_20222919/12733052)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [FPGA实现IIC通信(读写EEPROM)](https://blog.csdn.net/xs_sd/article/details/114534036)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值