目录

1.单次随机读数据

1.1简介

1.2代码

1.3Modelsim仿真

1.4逻辑分析仪上板验证

2.顺序读数据

2.1简介

2.2代码

2.3Modelsim仿真

​编辑2.4逻辑分析仪上板验证 


1.单次随机读数据

1.1简介

在黑金ax301开发板上使用IIC读取EEPROM 24LC04的数据。

fpga型号:EP4CE6F17C8

开发工具:Quartus ll 13.0 + Modelsim 10.1c

系统时钟:50MHZ

IIC时钟:250KHZ

两个模块:IIC驱动模块和IIC顶层模块

使用的ip核:pll

单次随机读时序图如下:

过程如下:

(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;

(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);

(3) 先向从机写入高 8 位地址,且高位在前低位在后;

(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2 字节地址,跳转到步骤(6);

(5) 按高位在前低位在后的顺序写入单字节存储地址;

(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;

(7) 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;

(8) 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;

(9) 数据接收完成后,主机产生一个时钟的高电平无应答信号;

(10) 主机向从机发送停止信号,单字节读操作完成。

1.2代码

IIC驱动模块:该模块的主要功能是通过IIC读取EEPROM中的数据。

EEPROM器件地址为AOH,该芯片的字节地址为8位。

module i2c_driver (
    //系统接口
    input 				rst_n,       //复位信号,低电平有效
    input 				i2c_clk,     //i2c系统时钟,250khz
    //i2c物理接口
    output reg 		    i2c_scl,     //串行时钟信号
    inout 				i2c_sda,     //串行数据信号
    //用户接口
    input [7:0] 		pi_data,     //写入i2c的数据
    input 				i2c_start,   //i2c开始信号
    input [15:0] 		word_addr,   //字节地址
    input 				i2c_num,     //1表示字节地址为16位,0表示字节地址为8位
    output reg 		    i2c_end,     //i2c结束信号
    output reg [7:0]    po_data      //接收的数据
);
    parameter DEVICE_ADDR = 7'b1_010_000;//器件地址
    localparam IDLE = 0,       //空闲状态
               START = 1,      //开始状态
               SEND_ADDR_1 = 2,//发送器件地址+写信号
               ACK_1 = 3,      //从机响应
               SEND_ADDR_H = 4,//发送高八位字节地址
               ACK_2 = 5,      //从机响应
               SEND_ADDR_L = 6,//发送低八位字节地址
               ACK_3 = 7,      //从机响应
               START_2 = 8,    //第二个开始状态
               SEND_ADDR_R = 9,//发送器件地址+读信号
               ACK_4 = 10,     //从机响应
               READ_DATA = 11, //读数据状态
               NACK = 12,      //主机非应答状态
               STOP = 13;      //停止状态

    reg [1:0] i2c_clk_cnt;//分频计数器  
    reg i2c_clk_cnt_en;   //i2c系统时钟分频允许位
    reg [2:0] cnt_data;   //数据位计数
    reg sda_en;           //三态门开关
    reg sda_out;          //sda输出
    wire sda_in;          //sda输入
    reg ack_flag;         //响应标志信号 
    reg [7:0] po_data_re;
	//状态机
    reg [3:0] cur_state;   //当前状态
    reg [3:0] next_state;  //下一个状态

    wire [7:0] addr_w = {DEVICE_ADDR,1'b0}; //7位设备地址+1位写标志位
    wire [7:0] addr_r = {DEVICE_ADDR,1'b1}; //7位设备地址+1位读标志位

    //i2c_clk_cnt
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            i2c_clk_cnt <= 0;
        else if(i2c_clk_cnt_en)  //当i2c系统时钟分频允许位为高电平时,i2c_clk_cnt自加1
            i2c_clk_cnt <= i2c_clk_cnt + 1'b1;   
        else
            i2c_clk_cnt <= 0; 
    end 
    //i2c_clk_cnt_en
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            i2c_clk_cnt_en <= 0;
		  else if(i2c_start)//i2c开始信号来后拉高
            i2c_clk_cnt_en <= 1;	
        else if((cur_state == STOP && cnt_data == 3'd3 && i2c_clk_cnt == 2'd3) || (cur_state == IDLE && !i2c_start))//当STOP状态结束或者空闲状态没有出现i2c开始信号时拉低i2c_clk_cnt_en信号
            i2c_clk_cnt_en <= 0;
        else
            i2c_clk_cnt_en <= i2c_clk_cnt_en;
    end
    //i2c_sda
    assign sda_in = i2c_sda;                                  //i2c_sda作为输入
    assign i2c_sda = sda_en ? (sda_out ? 1'bz : 1'b0) : 1'bz; //i2c_sda作为输出
    
    //三段式状态机第一段同步时序描述状态转移
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            cur_state <= IDLE;
        else
            cur_state <= next_state;   
    end
    
    //三段式状态机第二段组合逻辑判断状态转移条件,描述状态转移规律
    always@(*)begin
        case(cur_state)
            IDLE:
                if(i2c_start)
                    next_state = START;
                else
                    next_state = IDLE;
            START:                                 //开始状态,4个i2c系统时钟周期
                if(i2c_clk_cnt == 2'd3)
                    next_state = SEND_ADDR_1;
                else
                    next_state = START;
            SEND_ADDR_1:                           //发送器件地址+写信号
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
                    next_state = ACK_1;
                else
                    next_state = SEND_ADDR_1;    
            ACK_1:                                 //从机响应
                if(ack_flag && i2c_clk_cnt == 2'd3)begin
                    if(i2c_num)                    //判断字节地址位16位还是8位
                        next_state = SEND_ADDR_H;  //字节地位为16位,先发高8位字节地址
                    else
                        next_state = SEND_ADDR_L;  //字节地址位8位,跳转到发低8位状态
                end
                else if(i2c_clk_cnt == 2'd3)       //从机未响应返回空闲状态
                    next_state <= IDLE;  
                else
                    next_state = ACK_1; 
            SEND_ADDR_H:                           //发送高八位字节地址
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
                    next_state = ACK_2;
                else
                    next_state = SEND_ADDR_H;  
            ACK_2:                                 //从机响应
                if(ack_flag && i2c_clk_cnt == 2'd3)
                    next_state = SEND_ADDR_L;      //从机响应,转移到发送低8位字节地址状态
                else if(i2c_clk_cnt == 2'd3)
                    next_state = IDLE;             //从机未响应返回空闲状态
                else
                    next_state = ACK_2;
            SEND_ADDR_L:                           //发送低八位地址
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
                    next_state = ACK_3;
                else
                    next_state = SEND_ADDR_L;
            ACK_3:
                if(ack_flag && i2c_clk_cnt == 2'd3)
                    next_state = START_2;         //从机响应,转移到开始状态
                else if(i2c_clk_cnt == 2'd3)   
                    next_state = IDLE;            //从机未响应返回空闲状态        
                else
                    next_state = ACK_3;
            START_2:                              //第二个开始状态
                if(i2c_clk_cnt == 2'd3)
                    next_state <= SEND_ADDR_R;
                else
                    next_state <= START_2;
            SEND_ADDR_R:                          //发送器件地址+读信号 
                if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd7)
                    next_state <= ACK_4;
                else
                    next_state <= SEND_ADDR_R;
            ACK_4:                                //从机响应
                if(ack_flag && i2c_clk_cnt == 2'd3)
                    next_state <= READ_DATA;
                else if(i2c_clk_cnt == 2'd3)
                    next_state <= IDLE;
                else
                    next_state <= ACK_4;
            READ_DATA:                            //读数据
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
                    next_state = NACK;
                else
                    next_state = READ_DATA;     
            NACK:
                if(i2c_clk_cnt == 2'd3)
                    next_state = STOP;            //主机非应答状态,转移停止状态
                else
                    next_state = NACK;   
            STOP:                                 //停止状态
					 if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd3)
						  next_state <= IDLE;
					 else
					     next_state = STOP; 
            default:next_state <= IDLE;    
        endcase
    end  
    
    //三段式状态机第三段
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)begin    //初始状态,i2c_sda为输出态,sda_en为高电平,sda_out为高电平
            sda_en <= 1'b1;  
            sda_out <= 1'b1; 
            cnt_data <= 3'd0;
            i2c_end <= 1'b0;
			po_data_re <= 8'd0;
			po_data <= 8'd0;
        end
        else begin
            i2c_end <= 1'b0;
            case(cur_state)
                IDLE:begin                             //空闲状态
                        sda_en <= 1'b1;                //sda位输出状态
                        sda_out <= 1'b1;               //总线拉高
                    end
                START: begin   
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1'b1;
                        sda_out <= addr_w[7];          //此时sda_scl为下降沿,改变sda
                    end
                    else begin
                        sda_en <= 1'b1;
                        sda_out <= 1'b0;              //sda在scl高电平是出现下降沿,i2c开始
                    end
                end
                SEND_ADDR_1:begin                      //发送器件地址
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin      //8位数据发送完毕
                            cnt_data <= 3'd0;
                            sda_en <= 1'b0;            //8位数据发送完毕,拉低sda_en,等待从机响应
                        end
                        else begin                     //发送完一位数据
                            cnt_data <= cnt_data + 1'b1;
                            sda_out <= addr_w[6 - cnt_data];
                            sda_en <= 1'b1;
                        end
                    end					  
                end
                ACK_1:begin
                    if(i2c_clk_cnt == 2'd3)begin
                        if(i2c_num == 1)begin
								sda_en <= 1;               //从机响应完成,拉高sda_en 
								sda_out <= word_addr[15];  //i2c_num为1,字节位为16位
						end
						else begin
							sda_en <= 1;
							sda_out <= word_addr[7];       //i2c_num为0,字节为8位
						end	
					end		
                end
                SEND_ADDR_H:begin                  //发送高8位字节地址
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin  //8位数据发送完毕
                            cnt_data <= 3'd0;
                            sda_en <= 1'b0;        //8位数据发送完毕,等待从机响应
                        end
                        else begin                 //发送完一位数据
                            cnt_data <= cnt_data +1'b1;
                            sda_out <= word_addr[14 - cnt_data];
                            sda_en <= 1'b1;
                        end
                    end
                end
                ACK_2:
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1;          //从机响应完成,拉高sda_en 
                        sda_out <= word_addr[7];         
                end
                SEND_ADDR_L:                  //发送低8位字节地址
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin  //低8位字节地址发送完毕
                            cnt_data <= 1'b0;
                            sda_en <= 1'b0;     //8位数据发送完毕,等待从机响应
                        end
                        else begin              //发送完一位数据
                            cnt_data <= cnt_data + 1'b1;
                            sda_out <= word_addr[6 - cnt_data];
                            sda_en <= 1'b1;
                        end
                    end
                ACK_3:                    
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1;            //从机响应完成,拉高sda_en 
                        sda_out <= 1;
                    end
                START_2:
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1;      
                        sda_out <= addr_r[7];
                    end
                    else if(i2c_clk_cnt == 2'd1)begin   //注意需要在scl为高电平时,sda出现下降沿
                        sda_en <= 1;
                        sda_out <= 0;
                    end
                SEND_ADDR_R:                            
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin    
                            cnt_data <= 0;  
                            sda_en <= 0;       //数据发送完成,拉低en,等待从机响应
                        end
                        else begin
                            cnt_data <= cnt_data + 1'b1;
                            sda_out <= addr_r[6 - cnt_data];
                            sda_en <= 1;
                        end
                    end
                ACK_4:
                        sda_en <= 0;           //下一个状态是读数据状态,因此拉低en
                READ_DATA:begin
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin  //8位数据读取完毕,拉高en
                            cnt_data <= 1'b0;
                            sda_en <= 1;
                            sda_out <= 1;
                            po_data <= po_data_re;
                        end
                        else
                            cnt_data <= cnt_data + 1'b1;
                    end
                    else if(i2c_clk_cnt == 2'd1)
                        po_data_re[7-cnt_data] <= sda_in;
                end
                NACK:begin         //主机非应答状态,sda_out拉低,方便停止状态出现上升沿
						  if(i2c_clk_cnt == 2'd3)begin
						      sda_en <= 1;
                        sda_out <= 0;
						  end
                end
                STOP:begin
                    if(i2c_clk_cnt == 2'd2 && cnt_data == 3'd0)begin//拉高信号作为终止信号
                        sda_en <= 1;
                        sda_out <= 1;
                    end
                    else if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd3)begin    //送完了终止信号且延时一段时间发送I2C结束信号
                            i2c_end <= 1'b1;  
                            cnt_data <= 0;
                        end
                        else
                            cnt_data <= cnt_data + 1'b1;
                    end
                end
                default:;
            endcase
        end
    end

    //生成i2c_scl
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            i2c_scl <= 1;   //空闲状态scl为高电平
        else if(cur_state != STOP)begin
            if(i2c_clk_cnt == 2'd2)
                i2c_scl <= 0;
            else if(i2c_clk_cnt == 2'd0)
                i2c_scl <= 1;
        end
        else
            i2c_scl <= 1;
    end
    //生成从机响应信号
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            ack_flag <= 0;
        else
            case(cur_state)
                ACK_1,ACK_2,ACK_3,ACK_4:
                    if(i2c_clk_cnt == 2'd1 && !sda_in)
						  //if(i2c_clk_cnt == 2'd1)   //仿真时默认从机响应
                        ack_flag <= 1'b1;
                    else if(i2c_clk_cnt == 2'd3)
                        ack_flag <= 1'b0;
                default:ack_flag <= 1'b0;
            endcase
    end

endmodule

IIC顶层模块:该模块功能是生成IIC开始信号和字节地址(0006H)

module i2c_read(
	input     clk,
	input     rst_n,
	output    i2c_scl,   //串行时钟信号
    inout     i2c_sda    //串行数据信号
);

	wire i2c_clk;
	wire i2c_end;
	wire [7:0] po_data;
	pll_250k	pll_250k_inst (
		.inclk0 ( clk ),
		.c0 ( i2c_clk )
		);
		
	reg [15:0] cnt;
	always@(posedge clk or negedge rst_n)
		if(!rst_n)
			cnt <= 0;
		else if (cnt < 1000)
			cnt <= cnt + 1'b1;
		else
			cnt <= cnt;
	wire i2c_start = (cnt >50 && cnt < 300) ? 1'b1 : 1'b0;

	i2c_driver i2c_driver(
		 //系统接口
		  .rst_n(rst_n),          //复位信号,低电平有效
		  .i2c_clk(i2c_clk),        //i2c系统时钟,250khz
		 //i2c物理接口
		  .i2c_scl(i2c_scl),   //串行时钟信号
		  .i2c_sda(i2c_sda),        //串行数据信号
		 //用户接口
		  .pi_data(8'hab), //写入i2c的数据
		  .i2c_start(i2c_start),     //i2c开始信号
		  .word_addr(16'h0006),
		  .i2c_num(0),        //1表示字节地址为16位,0表示字节地址为8位
		  .i2c_end(i2c_end),  //i2c结束信号
		  .po_data(po_data)
	);
	
endmodule

1.3Modelsim仿真

        由于我没有找到IIC的仿真模型,因此在等待从机响应时默认从机响应(在i2c_driver模块的代码中把那个注释去掉),读取的数据是高阻态。

仿真代码:

`timescale 1ns/1ns
module i2c_tb;
	reg clk;
	reg rst_n;
	wire i2c_scl;
	wire i2c_sda;
i2c_read i2c_read(
	 .clk,
	 .rst_n,
	 .i2c_scl,   //串行时钟信号
     .i2c_sda    //串行数据信号
);
	initial clk = 0;
	always #10 clk = !clk;
	
	initial begin
		rst_n = 0;
		#65;
		rst_n = 1;
		#1_000_000;
		$stop;
	end
endmodule

 波形图:

1.4逻辑分析仪上板验证

        EEPROM芯片中已经提前在06H上写入数据56H,我们可以从图中看到读取的数据为56H,因此上板成功。

2.顺序读数据

2.1简介

在黑金ax301开发板上使用IIC顺序读取EEPROM 24LC04的数据。并通过uart232串口把读取的数据发给串口调试助手。(中间将把EEPROM中读取的数据存到fifo中,然后通过处理后从fifo中读取数据传给uart232发送模块)

fpga型号:EP4CE6F17C8

开发工具:Quartus ll 13.0 + Modelsim 10.1c+串口调试助手

系统时钟:50MHZ

IIC时钟:250KHZ

uart232接收波特率:115200

四个模块:IIC驱动模块、uart控制模块、uart发送数据模块和顶层模块。

使用的ip核:pll和fifo

时序图如下:

过程如下:

(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;

(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写 入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);

(3) 先向从机写入高 8 位地址,且高位在前低位在后;

(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2 字节地址,跳转到步骤(6);

(5) 按高位在前低位在后的顺序写入单字节存储地址;

(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;

(7) 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;

(8) 主机接收到从机回传的应答信号后,开始接收从机传回的第一个单字节数据;

(9) 数据接收完成后,主机产生应答信号回传给从机,从机接收到应答信号开始下一字节数据的传输,若数据接收完成,执行下一操作步骤;若数据接收未完成,在此执行步骤(9);

(10) 主机产生一个时钟的高电平无应答信号;

(11) 主机向从机发送停止信号,顺序读操作完成。

2.2代码

IIC驱动模块:该模块使用三段式段式状态机,主要功能是通过i2c顺序从EEPROM 24LC04(器件地址为A0H)中读取数据(数据多少可以指定)。

module i2c_driver (
    //系统接口
    input               rst_n,       //复位信号,低电平有效
    input               i2c_clk,     //i2c系统时钟,250khz
    //i2c物理接口
    output      reg     i2c_scl,     //串行时钟信号
    inout               i2c_sda,     //串行数据信号
    //用户接口
    input               i2c_start,   //i2c开始信号
    input       [15:0]  word_addr,   //字节地址
    input       [7:0]   i2c_num,     //1表示字节地址为16位,0表示字节地址为8位
    output      reg     i2c_end,     //i2c结束信号
    output reg  [7:0]   po_data,     //接收的数据
    output reg          po_data_flag //数据标志位
);
    parameter DEVICE_ADDR = 7'b1_010_000,//器件地址
              DATA_NUM = 8'd10;          //读取的字节数据的个数
    localparam IDLE = 0,       //空闲状态
               START = 1,      //开始状态
               SEND_ADDR_1 = 2,//发送器件地址+写信号
               ACK_1 = 3,      //从机响应
               SEND_ADDR_H = 4,//发送高八位字节地址
               ACK_2 = 5,      //从机响应
               SEND_ADDR_L = 6,//发送低八位字节地址
               ACK_3 = 7,      //从机响应
               START_2 = 8,    //第二个开始状态
               SEND_ADDR_R = 9,//发送器件地址+读信号
               ACK_4 = 10,     //从机响应
               READ_DATA = 11, //读数据状态
               ACK_5 = 12,     //主机响应状态
               NACK = 13,      //主机非应答状态
               STOP = 14;      //停止状态

    reg [1:0] i2c_clk_cnt;//分频计数器  
    reg i2c_clk_cnt_en;   //i2c系统时钟分频允许位
    reg [2:0] cnt_data;   //数据位计数
    reg sda_en;           //三态门开关
    reg sda_out;          //sda输出
    wire sda_in;          //sda输入
    reg ack_flag;         //响应标志信号 
    reg [7:0] po_data_re; //输出数据寄存
    reg [7:0] data_num_cnt;     //读取数据个数计数
	//状态机
    reg [3:0] cur_state;   //当前状态
    reg [3:0] next_state;  //下一个状态
        
    wire [7:0] addr_w = {DEVICE_ADDR,1'b0}; //7位设备地址+1位写标志位
    wire [7:0] addr_r = {DEVICE_ADDR,1'b1}; //7位设备地址+1位读标志位

    //i2c_clk_cnt
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            i2c_clk_cnt <= 0;
        else if(i2c_clk_cnt_en)  //当i2c系统时钟分频允许位为高电平时,i2c_clk_cnt自加1
            i2c_clk_cnt <= i2c_clk_cnt + 1'b1;   
        else
            i2c_clk_cnt <= 0; 
    end 
    //i2c_clk_cnt_en
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            i2c_clk_cnt_en <= 0;
		else if(i2c_start)//i2c开始信号来后拉高
            i2c_clk_cnt_en <= 1;	
        else if((cur_state == STOP && cnt_data == 3'd3 && i2c_clk_cnt == 2'd3) || (cur_state == IDLE && !i2c_start))//当STOP状态结束或者空闲状态没有出现i2c开始信号时拉低i2c_clk_cnt_en信号
            i2c_clk_cnt_en <= 0;
        else
            i2c_clk_cnt_en <= i2c_clk_cnt_en;
    end
    //i2c_sda
    assign sda_in = i2c_sda;                                  //i2c_sda作为输入
    assign i2c_sda = sda_en ? (sda_out ? 1'bz : 1'b0) : 1'bz; //i2c_sda作为输出
    
    //三段式状态机第一段同步时序描述状态转移
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            cur_state <= IDLE;
        else
            cur_state <= next_state;   
    end
    
    //三段式状态机第二段组合逻辑判断状态转移条件,描述状态转移规律
    always@(*)begin
        case(cur_state)
            IDLE:
                if(i2c_start)                      //开始信号来的时候,状态跳到开始状态
                    next_state = START;
                else
                    next_state = IDLE;
            START:                                 //开始状态,4个i2c系统时钟周期
                if(i2c_clk_cnt == 2'd3)
                    next_state = SEND_ADDR_1;
                else
                    next_state = START;
            SEND_ADDR_1:                           //发送器件地址+写信号
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
                    next_state = ACK_1;           
                else
                    next_state = SEND_ADDR_1;    
            ACK_1:                                 //从机响应
                if(ack_flag && i2c_clk_cnt == 2'd3)begin
                    if(i2c_num)                    //判断字节地址位是16位还是8位
                        next_state = SEND_ADDR_H;  //字节地位为16位,跳转到发高8位字节地址状态
                    else
                        next_state = SEND_ADDR_L;  //字节地址位8位,跳转到发低8位字节地址状态
                end
                else if(i2c_clk_cnt == 2'd3)       //从机未响应返回空闲状态
                    next_state <= IDLE;  
                else
                    next_state = ACK_1; 
            SEND_ADDR_H:                           //发送高八位字节地址
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
                    next_state = ACK_2;
                else
                    next_state = SEND_ADDR_H;  
            ACK_2:                                 //从机响应
                if(ack_flag && i2c_clk_cnt == 2'd3)
                    next_state = SEND_ADDR_L;      //从机响应,转移到发送低8位字节地址状态
                else if(i2c_clk_cnt == 2'd3)
                    next_state = IDLE;             //从机未响应返回空闲状态
                else
                    next_state = ACK_2;
            SEND_ADDR_L:                           //发送低八位地址
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
                    next_state = ACK_3;
                else
                    next_state = SEND_ADDR_L;
            ACK_3:
                if(ack_flag && i2c_clk_cnt == 2'd3)
                    next_state = START_2;         //从机响应,转移到开始状态
                else if(i2c_clk_cnt == 2'd3)   
                    next_state = IDLE;            //从机未响应返回空闲状态        
                else
                    next_state = ACK_3;
            START_2:                              //第二个开始状态
                if(i2c_clk_cnt == 2'd3)
                    next_state <= SEND_ADDR_R;
                else
                    next_state <= START_2;
            SEND_ADDR_R:                          //发送器件地址+读信号 
                if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd7)
                    next_state <= ACK_4;
                else
                    next_state <= SEND_ADDR_R;
            ACK_4:                                //从机响应
                if(ack_flag && i2c_clk_cnt == 2'd3)
                    next_state <= READ_DATA;
                else if(i2c_clk_cnt == 2'd3)
                    next_state <= IDLE;
                else
                    next_state <= ACK_4;
            READ_DATA:                            //读数据
                if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)begin
                    if(data_num_cnt == DATA_NUM - 1) 
                        next_state <= NACK;      //数据全部读取完,转移到主机非应答状态
                    else
                        next_state <= ACK_5;     //数据并未全部读取完,转移至主机响应状态
                end
                else
                    next_state = READ_DATA;     
            ACK_5:                               //主句响应状态
				if(i2c_clk_cnt == 2'd3)
					next_state <= READ_DATA;
				else
					next_state <= ACK_5;		
            NACK:
                if(i2c_clk_cnt == 2'd3)
                    next_state = STOP;            //主机非应答状态,转移停止状态
                else
                    next_state = NACK;   
            STOP:                                 //停止状态
					 if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd3)
						  next_state <= IDLE;
					 else
					     next_state = STOP; 
            default:next_state <= IDLE;    
        endcase
    end  
    
    //三段式状态机第三段
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)begin    //初始状态,i2c_sda为输出态,sda_en为高电平,sda_out为高电平
            sda_en <= 1'b1;  
            sda_out <= 1'b1; 
            cnt_data <= 3'd0;
            i2c_end <= 1'b0;
			po_data_re <= 8'd0;
			po_data <= 8'd0;
            data_num_cnt <= 8'd0;
            po_data_flag <= 1'd0;
        end
        else begin
            i2c_end <= 1'b0;
            case(cur_state)
                IDLE:begin                             //空闲状态
                        sda_en <= 1'b1;                //sda位输出状态
                        sda_out <= 1'b1;               //总线拉高
                    end
                START: begin   
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1'b1;
                        sda_out <= addr_w[7];          //此时sda_scl为下降沿,改变sda
                    end
                    else begin
                        sda_en <= 1'b1;
                        sda_out <= 1'b0;              //sda在scl高电平是出现下降沿,i2c开始
                    end
                end
                SEND_ADDR_1:begin                      //发送器件地址
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin      //8位数据发送完毕
                            cnt_data <= 3'd0;
                            sda_en <= 1'b0;            //8位数据发送完毕,拉低sda_en,等待从机响应
                        end
                        else begin                     //发送完一位数据
                            cnt_data <= cnt_data + 1'b1;
                            sda_out <= addr_w[6 - cnt_data];
                            sda_en <= 1'b1;
                        end
                    end					  
                end
                ACK_1:begin
                    if(i2c_clk_cnt == 2'd3)begin
                        if(i2c_num == 1)begin
								sda_en <= 1;               //从机响应完成,拉高sda_en 
								sda_out <= word_addr[15];  //i2c_num为1,字节位为16位
						end
						else begin
							sda_en <= 1;
							sda_out <= word_addr[7];       //i2c_num为0,字节为8位
						end	
					end		
                end
                SEND_ADDR_H:begin                  //发送高8位字节地址
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin  //8位数据发送完毕
                            cnt_data <= 3'd0;
                            sda_en <= 1'b0;        //8位数据发送完毕,等待从机响应
                        end
                        else begin                 //发送完一位数据
                            cnt_data <= cnt_data +1'b1;
                            sda_out <= word_addr[14 - cnt_data];
                            sda_en <= 1'b1;
                        end
                    end
                end
                ACK_2:
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1;          //从机响应完成,拉高sda_en 
                        sda_out <= word_addr[7];         
                end
                SEND_ADDR_L:                  //发送低8位字节地址
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin  //低8位字节地址发送完毕
                            cnt_data <= 1'b0;
                            sda_en <= 1'b0;     //8位数据发送完毕,等待从机响应
                        end
                        else begin              //发送完一位数据
                            cnt_data <= cnt_data + 1'b1;
                            sda_out <= word_addr[6 - cnt_data];
                            sda_en <= 1'b1;
                        end
                    end
                ACK_3:                    
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1;            //从机响应完成,拉高sda_en 
                        sda_out <= 1;
                    end
                START_2:
                    if(i2c_clk_cnt == 2'd3)begin
                        sda_en <= 1;      
                        sda_out <= addr_r[7];
                    end
                    else if(i2c_clk_cnt == 2'd1)begin   //注意需要在scl为高电平时,sda出现下降沿
                        sda_en <= 1;
                        sda_out <= 0;
                    end
                SEND_ADDR_R:                            
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin    
                            cnt_data <= 0;  
                            sda_en <= 0;       //数据发送完成,拉低en,等待从机响应
                        end
                        else begin
                            cnt_data <= cnt_data + 1'b1;
                            sda_out <= addr_r[6 - cnt_data];
                            sda_en <= 1;
                        end
                    end
                ACK_4:
                        sda_en <= 0;           //下一个状态是读数据状态,因此拉低en
                READ_DATA:begin
                    if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd7)begin  //1个8位数据读取完毕
                            if(data_num_cnt < DATA_NUM - 1)begin //字节数据并未全部读取完,跳转至主机应答状态,sda_en拉高,sda_out拉低
								cnt_data <= 1'b0;
								sda_en <= 1;
								sda_out <= 0;
								po_data <= po_data_re;
								po_data_flag <= 1;
							end
							else begin   //字节数据全部读取完成,跳转至主机非应答状态,sda_en拉高,sda_out拉高
								cnt_data <= 1'b0;
								sda_en <= 1;
								sda_out <= 1;
								po_data <= po_data_re;
								po_data_flag <= 1;
							end
                        end
                        else
                            cnt_data <= cnt_data + 1'b1;
                    end
                    else if(i2c_clk_cnt == 2'd1)
                        po_data_re[7-cnt_data] <= sda_in;
                end
                ACK_5:begin          //主机响应状态,sda_out为低电平
                    po_data_flag <= 0;
                    po_data_re <= 8'd0;
                    if(i2c_clk_cnt == 2'd3)begin
                        data_num_cnt <= data_num_cnt + 1'b1; 
						sda_en <= 0;
						sda_out <= 0;
					end
                end
                NACK:begin         //主机非应答状态,sda_out拉低,方便停止状态出现上升沿
                    po_data_flag <= 0;
                    sda_en <= 1;
                    sda_out <= 1;
                end
                STOP:begin
                    if(i2c_clk_cnt == 2'd2 && cnt_data == 3'd0)begin//拉高信号作为终止信号
                        sda_en <= 1;
                        sda_out <= 1;
                    end
                    else if(i2c_clk_cnt == 2'd3)begin
                        if(cnt_data == 3'd3)begin    //送完了终止信号且延时一段时间发送I2C结束信号
                            i2c_end <= 1'b1;  
                            cnt_data <= 0;
                        end
                        else
                            cnt_data <= cnt_data + 1'b1;
                    end
                end
                default:;
            endcase
        end
    end

    //生成i2c_scl
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            i2c_scl <= 1;   //空闲状态scl为高电平
        else if(cur_state != STOP)begin
            if(i2c_clk_cnt == 2'd2)
                i2c_scl <= 0;
            else if(i2c_clk_cnt == 2'd0)
                i2c_scl <= 1;
        end
        else
            i2c_scl <= 1;
    end
    //生成从机响应信号
    always @(posedge i2c_clk or negedge rst_n) begin
        if(!rst_n)
            ack_flag <= 0;
        else
            case(cur_state)
                ACK_1,ACK_2,ACK_3,ACK_4,ACK_5:
                    if(i2c_clk_cnt == 2'd1 && !sda_in)
						  //if(i2c_clk_cnt == 2'd1)   //仿真时默认从机响应
                        ack_flag <= 1'b1;
                    else if(i2c_clk_cnt == 2'd3)
                        ack_flag <= 1'b0;
                default:ack_flag <= 1'b0;
            endcase
    end

endmodule

uart控制模块:该模块主要功能是把从EEPROM中读取的数据存到fifo中,然后在把数据从fifo中读取出来传给uart232发送模块

module uart_ctrl(
    input               clk,               //50mhz
    input               rst_n,             //复位信号
    input               i2c_end,           //i2c结束信号
    input   [7:0]       po_data,           //i2c读取的数据
    input               po_data_flag,      //i2c数据读取标志位
    output  [7:0]       uart_data,         //处理后传入232串口的数据
    output  reg         uart_data_flag     //uart_data数据标志位

);
    localparam cnt_time_max = 8680*10/20-1;//波特率为115200读取1个字节的时间
    reg [7:0] q;            //计数器,po_data_flag来的时候计数
    reg wr_en;              //fifo写允许信号
    reg [12:0] cnt_time;    //fifo读允许信号标志位
    wire rd_en;             //fifo读允许信号
    reg i2c_end_re;         //i2c结束信号寄存
    reg [7:0] cnt;          //fifo中的数据个数计数
    //fifo IP核例化
    fifo	fifo_inst (
	.clock ( clk ),
	.data ( po_data ),
	.rdreq ( rd_en ),
	.wrreq ( wr_en ),
	.q ( uart_data )
    );
    //po_data_flag持续一个i2c周期的高电平(200个系统时钟周期)
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            q <= 0;
        else if(po_data_flag)
            q <= q + 1'b1;
        else
            q <= 0;
    end
    //wr_en
    always@(posedge clk or negedge rst_n)
        if(!rst_n)
            wr_en <= 0;
        else if(q == 8'b1)
            wr_en <= 1;
        else 
            wr_en <= 0;
    //i2c_end_re
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            i2c_end_re <= 0;
        else if(i2c_end)
            i2c_end_re <= 1;
        
    end
     //i2c结束后,每隔(8680*10/20)s,读取fifo中的数据
        always @(posedge clk or negedge rst_n) begin
            if(!rst_n)
                cnt_time <= 0;
            else if(cnt > 0)begin
                if(i2c_end_re)begin
                    if(cnt_time == cnt_time_max)
                        cnt_time <= 0;
                    else
                        cnt_time <= cnt_time + 1'b1;   
                end
            end
        end
    //rd_en
    assign rd_en = (cnt_time == cnt_time_max) ? 1 : 0;
    //寄存rd_en
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            uart_data_flag <= 0;
        else
            uart_data_flag <= rd_en;
    end
    //cnt,wr_en信号来的时候加1,rd_en信号来的时候减1
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            cnt <= 0;
        else if(wr_en)
            cnt <= cnt + 1'b1;
        else if(rd_en)
            cnt <= cnt - 1'b1;
        else
            cnt <= cnt;
    end

endmodule

uart发送模块:波特率为115200

module uart_tx(
    input           clk,
    input           rst_n,
    input   [7:0]   pi_data,
    input           pi_flag,
    output reg      tx,
    output reg      tx_done //结束信号
);
    localparam BAUD_RATE = 115200;
    localparam CNT_BAUD_MAX = 50_000_000 / BAUD_RATE - 1;
    wire done;
    // pi_data寄存,由于pi_data随着tx模块接收数据变动,所以在pi_flag时刻将pi_data复制一份
    reg [7:0] pi_data_r;
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            pi_data_r <= 8'd0;
        else if(pi_flag)
            pi_data_r <= pi_data;
    end
    
    // 计数器使能,在pi_flag来后开始计数,传输完毕结束计数
    reg cnt_ena;
    always @(posedge clk) begin
        if(pi_flag)
            cnt_ena <= 1'b1;
        else if(done)
            cnt_ena <= 1'b0;
    end
    
    // 波特率计数器
    reg [12:0] cnt_baud;
    wire cnt_baud_done = (cnt_baud == CNT_BAUD_MAX);
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            cnt_baud <= 13'd0;
        else if(cnt_ena) begin
            if(cnt_baud_done)
                cnt_baud <= 13'd0;
            else
                cnt_baud <= cnt_baud + 13'd1;
        end
    end
    
    localparam
        IDLE = 0,
        START = 1,
        D0 = 2,
        D1 = 3,
        D2 = 4,
        D3 = 5,
        D4 = 6,
        D5 = 7,
        D6 = 8,
        D7 = 9,
        STOP = 10;
    reg [3:0] state, next;
    
    // stop状态只持续一个时钟周期,因为IDLE状态和STOP状态tx都是高电平,避免错过下一个pi_flag
    always @(*) begin
        case(state)
            IDLE : next = pi_flag ? START : IDLE;
            START : next = cnt_baud_done ? D0 : START;
            D0 : next = cnt_baud_done ? D1 : D0;
            D1 : next = cnt_baud_done ? D2 : D1;
            D2 : next = cnt_baud_done ? D3 : D2;
            D3 : next = cnt_baud_done ? D4 : D3;
            D4 : next = cnt_baud_done ? D5 : D4;
            D5 : next = cnt_baud_done ? D6 : D5;
            D6 : next = cnt_baud_done ? D7 : D6;
            D7 : next = cnt_baud_done ? STOP : D7;
            STOP : next = IDLE;
        endcase
    end
    
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            state <= IDLE;
        else
            state <= next;
    end
        
    // 传输完毕信号
    assign done = (state == STOP);
    
    // tx
    always @(*) begin
        case(state)
            IDLE : tx = 1'b1;
            START : tx = 1'b0;
            D0 : tx = pi_data_r[0];
            D1 : tx = pi_data_r[1];
            D2 : tx = pi_data_r[2];
            D3 : tx = pi_data_r[3];
            D4 : tx = pi_data_r[4];
            D5 : tx = pi_data_r[5];
            D6 : tx = pi_data_r[6];
            D7 : tx = pi_data_r[7];
            STOP : tx = 1'b1;
        endcase
    end

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            tx_done <= 0;
        else if(state == STOP)
            tx_done <= 1;
        else
            tx_done <= 0;
    end
endmodule

顶层模块:默认字节地址为01H,从EEPROM中读取十个数据

module i2c_read(
   input       sys_clk,   //50mhz
   input       sys_n,     //复位
   output      i2c_scl,   //串行时钟信号
   inout       i2c_sda,   //串行数据信号
   output      tx         //232串口发送
);
    wire clk;                       //50mhz
    wire i2c_clk;                   //250khz
    wire locked;                    //pll锁存信号
    wire rst_n = locked && sys_n;   //rst_n
    wire i2c_end;                   //i2c结束信号
    wire [7:0] po_data;             //i2c读取的数据
    wire po_data_flag;              //i2c读取的数据标志位
    wire [7:0] uart_data;           //传入232发送模块的数据
    wire uart_data_flag;            //传入232发送模块的数据标志位
    wire tx_done;                   //串口传输完一个字节数据结束信号

    reg [15:0] cnt;                 //计数器
    //cnt
    always@(posedge clk or negedge rst_n)
        if(!rst_n)
            cnt <= 0;
        else if (cnt < 1000)
            cnt <= cnt + 1'b1;
        else
            cnt <= cnt;
    wire i2c_start = (cnt >50 && cnt < 300) ? 1'b1 : 1'b0;  //上电1000ns后产生i2c开始信号,高电平持续一个i2c时钟周期
    //p;;例化
    pll_250k	pll_250k_inst (
        .inclk0 ( sys_clk ),
        .c0 ( clk ),
        .c1 ( i2c_clk ),
        .locked ( locked )
        );
    //i2c驱动模块例化
    i2c_driver i2c_driver(
        //系统接口
        .rst_n(rst_n),            //复位信号,低电平有效
        .i2c_clk(i2c_clk),          //i2c系统时钟,250khz
        //i2c物理接口
        .i2c_scl(i2c_scl),     //串行时钟信号
        .i2c_sda(i2c_sda),          //串行数据信号
        //用户接口
        .i2c_start(i2c_start),       //i2c开始信号
        .word_addr(16'h0001),//字节地址
        .i2c_num(0),         //1表示字节地址为16位,0表示字节地址为8位
        .i2c_end(i2c_end),    //i2c结束信号
        .po_data(po_data),     //接收的数据
        .po_data_flag(po_data_flag)  //数据标志位
    );
    //232串口控制模块例化
    uart_ctrl uart_ctrl(
        .clk(clk),
        .rst_n(rst_n),
        .i2c_end(i2c_end),       //i2c结束信号
        .po_data(po_data),       //i2c读取的数据
        .po_data_flag(po_data_flag),      //i2c数据读取标志位
        .uart_data(uart_data),  //处理后传入232串口的数据
        .uart_data_flag(uart_data_flag)     //uart_data数据标志位
        );
    //uart_tx例化
    uart_tx uart_tx(
        .clk(clk),
        .rst_n(rst_n),
        .pi_data(uart_data),
        .pi_flag(uart_data_flag),
        .tx(tx),
        .tx_done(tx_done) //结束信号
);
endmodule

2.3Modelsim仿真

仿真代码:

`timescale 1ns/1ns
module i2c_tb;
	reg clk;
	reg rst_n;
	wire i2c_scl;
	wire i2c_sda;
	wire tx;
i2c_read i2c_read(
	 .sys_clk(clk),
	 .sys_n(rst_n),
	 .i2c_scl(i2c_scl),   //串行时钟信号
     .i2c_sda(i2c_sda),   //串行数据信号
	 .tx(tx)
);
	initial clk = 0;
	always #10 clk = !clk;
	
	initial begin
		rst_n = 0;
		#65;
		rst_n = 1;
		#3_000_000;
		$stop;
	end
endmodule

仿真波形:这里就只给i2c_driver模块的波形了,由于没有EEPROM的仿真模型,因此默认从机响应,读取的数据也都是高阻态。

2.4逻辑分析仪上板验证 

当i2c_start出现上升沿时开始抓取,在此之前我们已经通过i2c页写入数据0AH,12H,23H,34H,45H,56H,67H,78H,89H,91H到EEPROM芯片中(起始地址是01H)。这里就只展现i2c驱动模块的图了。

我们也可以从串口调试助手中看到收到了数据,如图:


有任何问题都可以在评论区和我交流。

本文参考了大佬孤独的单刀的博客

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐