8.23 正点原子领航者V1开发板学习之EEPROM

                                                        一、EEPROM和ICC介绍

1.串口通信中UART 和IIC区别

UART是有两个数据项RX ,TX,属于全双工,无时钟线异步串行通信

IIC 有两根线SCL时钟线和SDA数据线,由于只有一条数据线,是同步半双工通信。

2. AT24C64 芯片手册  器件操作时序

                                                                IIC通信协议

 SDA是在SCL低电平时改变,当SCL为高电平时SDA保持数据稳定。

SCL为高电平时,SDA由高拉低是开始标志,传输完数据后由低拉高是结束标志。

 这个是应答位 输出完8bit数据后,输出一个低电平。当输出一直为高电平有可能是数据出错,SCL 频率太高等。

3.器件地址

        

 前四位是固定值,A2A1A0是可配置地址输入,最多供8个器件EEPROM组合,最后一位是读写控制位。1读0写。本次实验配置A2A1A0为000。

4.写操作

字节写

 这个图略去了SCL时钟波形,根据上述器件操作时序可知,当SCL高电平时SDA拉低则表示数据START,然后在每个SCL低电平期间,数据改变一共输出8位1010A2A1A0+读写位,最后在SCL高电平期间输出一个响应位即SDA拉低。

硬件上就是开始到器件地址位由FPGA来控制,然后由EEPROM响应是否输出低电平响应。

字节地址位,*表示不关心位,+表示32k不关心,芯片是64k的所以要看设置为BIT_CTRL=1。

AT24C64 存储容量 为 64Kbit,内部分成 256 页,每页 32 字节,共有 8192 个字节,且其读写操作都是以字节为基本单位。可 以把 AT24C64 看作一本书,那么这本书有 256 页,每页有 32 行,每行有 8 个字,总共有 256*32*8=65536 个字,对应着 AT24C64 的 64*1024=65536 个 bit。8192=2^13,所以用13位来表示字节地址,当然每8位后还是有一个ACK。识别完器件地址和字节地址信息后,就开始写数据了,随后输出一个ACK和停止位。

页写

 DATA一共写32个字节数据,然后再输出ACK和停止位。

显然如果数据多,用页写操作效率远大于字节写。

5.读操作

EEPROM支持三种读操作,当前地址读,随机地址读和顺序地址读。

顺序读中,读操作和写操作有一个很不同的地方。读到某一页最后字节会跳转到下一页,一直读到最后一页最后一个字节然后跳转到第一页第一个字节。写操作则是写到当前页最后一个字节时跳到当前页第一个字节。

当前地址读好理解,最后不需要ACK信号。 

 任意地址读则需要在都之前进行一次哑写,写入地址,然后读出该地址,同样没有ACK信号。

由于需要哑写,所以耗费时间

 在当前地址读基础上主机不返回STOP

本次实验选择字节写和随机读的方式,也是为了后面实验直接调用本模块。

6.实验IIC读写协议选取

字节写

 随机读

                                                          二、实验

1.实验任务

        先给EEPROM   地址0-255写入数据0-255.之后再读取数据,若读取正确LED常亮否则闪烁。

 模块框架中就两条线SCL和SDA

2.原理图

 图中iic_sda是INOUT信号。

i2c_dri 为 I2C 驱动模块,用来驱动 I2C 的读写操作。当 FPGA 通过 EEPROM 读写模块 e2prom_rw 向 EEPROM 读写数据时,拉高 i2c 触发控制信号 i2c_exec 以使能 I2C 驱动模块,并使用读写控制信号 i2c_rh_wl 控制读写操作,当 i2c_rh_wl 为低电平时,I2C 驱动模块 i2c_dri 执行写操作,当 i2c_rh_wl 为高电平时,I2C 驱动模块 i2c_dri 执行读操作。此外,e2prom_rw 模块通过 i2c_addr 接口向 i2c_dri 模块输入器件字地址,通 过 i2c_data_w接口向 i2c_dri模块输入写的数据,并通过 i2c_data_r 接口读取 i2c_dri模块读到的数据。rw_done 信号是读写测试完成的标志,rw_result 是读写测试的结果。

3.i2c_dri程序设计

sda信号设计:它是一个INOUT信号,EEPROM是半双工,它只有一条数据线。所以用三态门设计读写分开操作

reg            sda_dir   ; //I2C数据(SDA)方向控制
wire          sda_in     ; //SDA输入信号

assign  sda     = sda_dir ?  sda_out : 1'bz;     //SDA数据输出或高阻
assign  sda_in  = sda ;                          //SDA数据输入

dri_clk的设计:

驱动时钟dri要驱动SCL,SDA所以时钟频率要高,毕竟SDA在SCL低电平时候变化,所以dri_clk频率最好是SCL4倍以上。

 先求分频系数,系统时钟50MHZ除以I2C的时钟,可得I2C时钟分频系数,用一个计数器计数到这么多跳变一次的时钟就是I2C所需要的时钟频率的两倍。而dri_clk频率是其四倍,右移两位即除4。(计数为I2C的四分之一就跳变就是频率为其4倍)

assign  clk_divide = (CLK_FREQ/I2C_FREQ) >> 2'd2;//模块驱动时钟的分频系数

计数器模块如下:

//生成I2C的SCL的四倍频率的驱动时钟用于驱动i2c的操作
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        dri_clk <=  1'b0;
        clk_cnt <= 10'd0;
    end
    else if(clk_cnt == clk_divide[8:1] - 1'd1) begin
        clk_cnt <= 10'd0;
        dri_clk <= ~dri_clk;
    end
    else
        clk_cnt <= clk_cnt + 1'b1;
end

 要注意取[8:1]也是除2操作,因为分频系数只是求到那个跳变的计数,即一个周期的一半,故还要再除2,这样才是一个周期。减一1是从0开始计数。(当然这个仅适用于偶分频)

IP核也可以分频但是资源利用率会上升。

状态机的设计:

 初始状态设为idle。

用三段式来设计

//localparam define
localparam  st_idle     = 8'b0000_0001; //空闲状态
localparam  st_sladdr   = 8'b0000_0010; //发送器件地址(slave address)
localparam  st_addr16   = 8'b0000_0100; //发送16位字地址
localparam  st_addr8    = 8'b0000_1000; //发送8位字地址
localparam  st_data_wr  = 8'b0001_0000; //写数据(8 bit)
localparam  st_addr_rd  = 8'b0010_0000; //发送器件地址读
localparam  st_data_rd  = 8'b0100_0000; //读数据(8 bit)
localparam  st_stop     = 8'b1000_0000; //结束I2C操作

reg    [ 7:0]  cur_state ; //状态机当前状态
reg    [ 7:0]  next_state; //状态机下一状态
reg            st_done   ; //状态结束
reg            wr_flag   ; //写标志1读0写

//(三段式状态机)同步时序描述状态转移
always @(posedge dri_clk or negedge rst_n) begin
    if(!rst_n)
        cur_state <= st_idle;
    else
        cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always @(*) begin
    next_state = st_idle;
    case(cur_state)
        st_idle: begin                          //空闲状态
           if(i2c_exec) begin
               next_state = st_sladdr;
           end
           else
               next_state = st_idle;
        end
        st_sladdr: begin
            if(st_done) begin
                if(bit_ctrl)                    //判断是16位还是8位字地址
                   next_state = st_addr16;
                else
                   next_state = st_addr8 ;
            end
            else
                next_state = st_sladdr;
        end
        st_addr16: begin                        //写16位字地址
            if(st_done) begin
                next_state = st_addr8;
            end
            else begin
                next_state = st_addr16;
            end
        end
        st_addr8: begin                         //8位字地址
            if(st_done) begin
                if(wr_flag==1'b0)               //读写判断
                    next_state = st_data_wr;
                else
                    next_state = st_addr_rd;
            end
            else begin
                next_state = st_addr8;
            end
        end
        st_data_wr: begin                       //写数据(8 bit)
            if(st_done)
                next_state = st_stop;
            else
                next_state = st_data_wr;
        end
        st_addr_rd: begin                       //写地址以进行读数据
            if(st_done) begin
                next_state = st_data_rd;
            end
            else begin
                next_state = st_addr_rd;
            end
        end
        st_data_rd: begin                       //读取数据(8 bit)
            if(st_done)
                next_state = st_stop;
            else
                next_state = st_data_rd;
        end
        st_stop: begin                          //结束I2C操作
            if(st_done)
                next_state = st_idle;
            else
                next_state = st_stop ;
        end
        default: next_state= st_idle;
    endcase
end

需要注意的是,写数据不需要发送地址给EEPROM。读数据需要。

时序电路描述状态输出(得到状态转移所要的信号)

初始化

其实ACK信号作为应答,指的就是该EEPROM有没有得到主机的信号,比如器件地址

为1010000,如果主机发送0010000请求则得不到应答。即ACK信号一直为高电平

parameter  SLAVE_ADDR = 7'b1010000 ;    //EEPROM从机地址
/时序电路描述状态输出
always @(posedge dri_clk or negedge rst_n) begin
    //复位初始化
    if(!rst_n) begin
        scl       <= 1'b1;						//初始为高,因为在高电平下才有可能开始数据读写
        sda_out   <= 1'b1;						//假定开始为写数据所以端口设定为输出
        sda_dir   <= 1'b1;                      //方向为写数据    
        i2c_done  <= 1'b0;                          
        i2c_ack   <= 1'b0;                          
        cnt       <= 1'b0;                          
        st_done   <= 1'b0;                          
        data_r    <= 1'b0;                          
        i2c_data_r<= 1'b0;                          
        wr_flag   <= 1'b0;                          
        addr_t    <= 1'b0;                          
        data_wr_t <= 1'b0;                          
    end                   

空闲状态描述

 else begin                                       
        st_done <= 1'b0 ; 						//避免每次操作后还要去拉低,直接写在最上面,使其不操作时一直为0                           
        cnt     <= cnt +1'b1 ;                  //写在这里,如果不操作就会一直累加。    
        case(cur_state)                              
             st_idle: begin                          //空闲状态
                scl     <= 1'b1;                     
                sda_out <= 1'b1;                     
                sda_dir <= 1'b1;                     
                i2c_done<= 1'b0;                     
                cnt     <= 7'b0;      					//计数单独清0         
                if(i2c_exec) begin                   
                    wr_flag   <= i2c_rh_wl ;   			//当驱动使能时,输入读写控制信号       
                    addr_t    <= i2c_addr  ;         	//输入器件内地址
                    data_wr_t <= i2c_data_w;  			//要写的数据
                    i2c_ack <= 1'b0;                    //应答清0   
                end                                  
            end                                                                     

接下来是发送控制命令状态和传送字地址,这一段可以画波形理解。基于IIC协议传送器件地址和数据字地址。还要输出st_done信号供状态跳转。同时注意要拉低最后SCL,让current状态跳转为下一个状态。 

st_sladdr: begin                         //写地址(器件地址和字地址),主要是得到st_done信号给状态跳转
                case(cnt)                            
                    7'd1 : sda_out <= 1'b0;          //开始I2C(这里sda=sda_out,当其为低时,开始iic通信)
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
                    7'd5 : scl <= 1'b1; 			 //画波形一目了然,在SCL低电平中间,SDA赋值。	             
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= SLAVE_ADDR[5]; 
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= SLAVE_ADDR[4]; 
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= SLAVE_ADDR[3]; 
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= SLAVE_ADDR[2]; 
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= SLAVE_ADDR[1]; 
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= SLAVE_ADDR[0]; 
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: sda_out <= 1'b0;          //0:写
                    7'd33: scl <= 1'b1;              
                    7'd35: scl <= 1'b0;              
                    7'd36: begin                     
                        sda_dir <= 1'b0;    		//拉低,变成读操作,sda_out为高阻         
                        sda_out <= 1'b1;             //01都无所谓           
                    end                              
                    7'd37: scl     <= 1'b1;            
                    7'd38: begin                     //从机应答 
                        st_done <= 1'b1;			//由于本实验用64k,需要13位,于是bit_ctrl=1。输出16位地址。
                        if(sda_in == 1'b1)           //高电平表示未应答,读取EEPROM的应答状态
                            i2c_ack <= 1'b1;         //拉高应答标志位 ,未应答    
                    end 
					//不写else是因为初始化i2c_ack为0.也可以补上else啦
                    7'd39: begin                     
                        scl <= 1'b0;      //需要再延迟一拍,当前状态才变为next_state           
                        cnt <= 1'b0;                 
                    end                              
                    default :  ;                     
                endcase                              
            end 
				
            st_addr16: begin                         
                case(cnt)                            
                    7'd0 : begin                     
                        sda_dir <= 1'b1 ;            
                        sda_out <= addr_t[15];       //传送字地址
                    end                              
                    7'd1 : scl <= 1'b1;              
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= addr_t[14];   //卡在低电平中间变化 
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= addr_t[13];    
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= addr_t[12];    
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= addr_t[11];    
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= addr_t[10];    
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= addr_t[9];     
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= addr_t[8];     
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: begin                     
                        sda_dir <= 1'b0;             
                        sda_out <= 1'b1;   
                    end                              
                    7'd33: scl  <= 1'b1;             
                    7'd34: begin                     //从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           //高电平表示未应答
                            i2c_ack <= 1'b1;         //拉高应答标志位    
                    end        
                    7'd35: begin                     
                        scl <= 1'b0;                 
                        cnt <= 1'b0;                 
                    end                              
                    default :  ;                     
                endcase                              
            end                                      
            st_addr8: begin                          
                case(cnt)                            
                    7'd0: begin                      
                       sda_dir <= 1'b1 ;             
                       sda_out <= addr_t[7];         //字地址
                    end                              
                    7'd1 : scl <= 1'b1;              
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= addr_t[6];     
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= addr_t[5];     
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= addr_t[4];     
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= addr_t[3];     
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= addr_t[2];     
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= addr_t[1];     
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= addr_t[0];     
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: begin                     
                        sda_dir <= 1'b0;         
                        sda_out <= 1'b1;                    
                    end                              
                    7'd33: scl     <= 1'b1;          
                    7'd34: begin                     //从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           //高电平表示未应答
                            i2c_ack <= 1'b1;         //拉高应答标志位    
                    end   
                    7'd35: begin                     
                        scl <= 1'b0;                 
                        cnt <= 1'b0;                 
                    end                              
                    default :  ;                     
                endcase                              
            end                   

写数据,主设备是FPGA写8位数据给E2PROM,所以是用sda_out,将要写的数据用同样的IIC协议写入。

            st_data_wr: begin                        //写数据(8 bit)
                case(cnt)                            
                    7'd0: begin                      
                        sda_out <= data_wr_t[7];     //I2C写8位数据
                        sda_dir <= 1'b1;             
                    end                              
                    7'd1 : scl <= 1'b1;              
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= data_wr_t[6];  
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= data_wr_t[5];  
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= data_wr_t[4];  
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= data_wr_t[3];  
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= data_wr_t[2];  
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= data_wr_t[1];  
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= data_wr_t[0];  
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: begin                     
                        sda_dir <= 1'b0;           
                        sda_out <= 1'b1;                              
                    end                              
                    7'd33: scl <= 1'b1;              
                    7'd34: begin                     //从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           //高电平表示未应答
                            i2c_ack <= 1'b1;         //拉高应答标志位    
                    end          
                    7'd35: begin                     
                        scl  <= 1'b0;                
                        cnt  <= 1'b0;                
                    end                              
                    default  :  ;                    
                endcase                              
            end 

写地址进行读数据,共两步,我们选的是任意地址读,首先写入地址数据以便读数据。在SCL高电平期间,sda_out由高变低。重新开始写地址

st_addr_rd: begin                        //写地址以进行读数据
                case(cnt)                            
                    7'd0 : begin                     
                        sda_dir <= 1'b1;   			//dir拉高,方向是输出写数据         
                        sda_out <= 1'b1;             
                    end                              
                    7'd1 : scl <= 1'b1;              
                    7'd2 : sda_out <= 1'b0;          //重新开始
                    7'd3 : scl <= 1'b0;              
                    7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
                    7'd5 : scl <= 1'b1;              
                    7'd7 : scl <= 1'b0;              
                    7'd8 : sda_out <= SLAVE_ADDR[5]; 
                    7'd9 : scl <= 1'b1;              
                    7'd11: scl <= 1'b0;              
                    7'd12: sda_out <= SLAVE_ADDR[4]; 
                    7'd13: scl <= 1'b1;              
                    7'd15: scl <= 1'b0;              
                    7'd16: sda_out <= SLAVE_ADDR[3]; 
                    7'd17: scl <= 1'b1;              
                    7'd19: scl <= 1'b0;              
                    7'd20: sda_out <= SLAVE_ADDR[2]; 
                    7'd21: scl <= 1'b1;              
                    7'd23: scl <= 1'b0;              
                    7'd24: sda_out <= SLAVE_ADDR[1]; 
                    7'd25: scl <= 1'b1;              
                    7'd27: scl <= 1'b0;              
                    7'd28: sda_out <= SLAVE_ADDR[0]; 
                    7'd29: scl <= 1'b1;              
                    7'd31: scl <= 1'b0;              
                    7'd32: sda_out <= 1'b1;          //1:读
                    7'd33: scl <= 1'b1;              
                    7'd35: scl <= 1'b0;              
                    7'd36: begin                     
                        sda_dir <= 1'b0;            
                        sda_out <= 1'b1;                    
                    end
                    7'd37: scl     <= 1'b1;
                    7'd38: begin                     //从机应答
                        st_done <= 1'b1;     
                        if(sda_in == 1'b1)           //高电平表示未应答
                            i2c_ack <= 1'b1;         //拉高应答标志位    
                    end   
                    7'd39: begin
                        scl <= 1'b0;
                        cnt <= 1'b0;
                    end
                    default : ;
                endcase
            end
            st_data_rd: begin                        //读取数据(8 bit)
                case(cnt)
                    7'd0: sda_dir <= 1'b0;			//拉低,变成读操作,sda_out为高阻,sda=sda_in
                    7'd1: begin
                        data_r[7] <= sda_in;
                        scl       <= 1'b1;
                    end
                    7'd3: scl  <= 1'b0;
                    7'd5: begin
                        data_r[6] <= sda_in ;
                        scl       <= 1'b1   ;
                    end
                    7'd7: scl  <= 1'b0;
                    7'd9: begin
                        data_r[5] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd11: scl  <= 1'b0;
                    7'd13: begin
                        data_r[4] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd15: scl  <= 1'b0;
                    7'd17: begin
                        data_r[3] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd19: scl  <= 1'b0;
                    7'd21: begin
                        data_r[2] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd23: scl  <= 1'b0;
                    7'd25: begin
                        data_r[1] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd27: scl  <= 1'b0;
                    7'd29: begin
                        data_r[0] <= sda_in;
                        scl       <= 1'b1  ;
                    end
                    7'd31: scl  <= 1'b0;
                    7'd32: begin
                        sda_dir <= 1'b1;             //拉高就是写
                        sda_out <= 1'b1;			//
                    end
                    7'd33: scl     <= 1'b1;
                    7'd34: st_done <= 1'b1;          //非应答
                    7'd35: begin
                        scl <= 1'b0;
                        cnt <= 1'b0;
                        i2c_data_r <= data_r;		//将读的数据寄存到i2c_data_r
                    end
                    default  :  ;
                endcase
            end

停止

st_stop: begin                           //结束I2C操作
                case(cnt)
                    7'd0: begin
                        sda_dir <= 1'b1;             //结束I2C
                        sda_out <= 1'b0;
                    end
                    7'd1 : scl     <= 1'b1;
                    7'd3 : sda_out <= 1'b1;
                    7'd15: st_done <= 1'b1;        //这里也是I2C结束标志,当SCL为高时,输出SDA_OUT由低变高。
                    7'd16: begin
                        cnt      <= 1'b0;
                        i2c_done <= 1'b1;            //向上层模块传递I2C结束信号
                    end
                    default  : ;
                endcase
            end
        endcase
    end
end

endmodule

驱动模块tb文件:太复杂了。直接调用网上的。以下是仿真VIVADO

 TB设定的地址是0555,写入数据是AA。这里可以注意i2c_rh_wl信号是low。开始结束标志都是在SCL高电平期间由SDA给出。ACK信号也正常。

下图是读操作,由于读写延迟了10MS,仿真时间应该设置为11MS才看的到读操作。

 i2c_rh_wl拉高为1.先进行假写操作。然后再重新开始访问器件地址,最后一位拉高为R读操作。然后是读数据AA,但是之后接着的是NOT ACK,主机非应答。然后拉高SDA结束.

仿真是OK的,可以设定错误的divce地址,会得到NOT ACK,即ACK为高。。。即得不到从机的应答.

接下来是本次实验其他部分。

E2PROM_RW部分

由于我们要检验读出的数据和写入的是否一致。用灯来表示。在RW模块需要完成数据检查,正确输出1,否则为0;同时输出测试完成(不同于IIC驱动的一次操作完成)

这个模块还需要负责读出ICC_dri输入的数据,即EEPROM里的,和输出器件内地址(2^13位,这里用16位)以及要写的数据。TB模块设置的在0555[000001010101]处写aa[10101010]...

module e2prom_rw(
    input                 clk        , //时钟信号  是DRI_CLK
    input                 rst_n      , //复位信号

    //i2c interface
    output   reg          i2c_rh_wl  , //I2C读写控制信号
    output   reg          i2c_exec   , //I2C触发执行信号
    output   reg  [15:0]  i2c_addr   , //I2C器件内地址
    output   reg  [ 7:0]  i2c_data_w , //I2C要写的数据
    input         [ 7:0]  i2c_data_r , //I2C读出的数据
    input                 i2c_done   , //I2C一次操作完成
    input                 i2c_ack    , //I2C应答标志

    //user interface
    output   reg          rw_done    , //E2PROM读写测试完成
    output   reg          rw_result    //E2PROM读写测试结果 0:失败 1:成功
);

//parameter define
//EEPROM写数据需要添加间隔时间,读数据则不需要
parameter      WR_WAIT_TIME = 14'd5000; //写入间隔时间5MS,手册里最大是10MS
parameter      MAX_BYTE     = 16'd256 ; //读写测试的字节个数  8页,一页32字节

//reg define
reg   [1:0]    flow_cnt  ; //状态流控制
reg   [13:0]   wait_cnt  ; //延时计数器

//*****************************************************
//**                    main code
//*****************************************************

//EEPROM读写测试,先写后读,并比较读出的值与写入的值是否一致
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        flow_cnt <= 2'b0;
        i2c_rh_wl <= 1'b0;
        i2c_exec <= 1'b0;
        i2c_addr <= 16'b0;
        i2c_data_w <= 8'b0;
        wait_cnt <= 14'b0;
        rw_done <= 1'b0;
        rw_result <= 1'b0;        
    end
    else begin
        i2c_exec <= 1'b0;
        rw_done <= 1'b0;
        case(flow_cnt)
            2'd0 : begin                                  
                wait_cnt <= wait_cnt + 1'b1;               //延时计数
                if(wait_cnt == WR_WAIT_TIME - 1'b1) begin  //EEPROM写操作延时完成
                    wait_cnt <= 1'b0;
                    if(i2c_addr == MAX_BYTE) begin         //256个字节写入完成
                        i2c_addr <= 1'b0;
                        i2c_rh_wl <= 1'b1;
                        flow_cnt <= 2'd2;
                    end
                    else begin
                        flow_cnt <= flow_cnt + 1'b1;
                        i2c_exec <= 1'b1;
                    end
                end
            end
            2'd1 : begin
                if(i2c_done == 1'b1) begin                 //EEPROM单次写入完成
                    flow_cnt <= 2'd0;
                    i2c_addr <= i2c_addr + 1'b1;           //地址0~255分别写入
                    i2c_data_w <= i2c_data_w + 1'b1;       //数据0~255
                end    
            end
            2'd2 : begin                                   
                flow_cnt <= flow_cnt + 1'b1;
                i2c_exec <= 1'b1;
            end    
            2'd3 : begin
                if(i2c_done == 1'b1) begin                 //EEPROM单次读出完成
                    //读出的值错误或者I2C未应答,读写测试失败
                    if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
                        rw_done <= 1'b1;
                        rw_result <= 1'b0;
                    end
                    else if(i2c_addr == MAX_BYTE - 1'b1) begin //读写测试成功
                        rw_done <= 1'b1;
                        rw_result <= 1'b1;
                    end    
                    else begin
                        flow_cnt <= 2'd2;
                        i2c_addr <= i2c_addr + 1'b1;
                    end
                end                 
            end
            default : ;
        endcase    
    end
end    

endmodule

完成数据检查后将结果给LED模块。我们设置当正确时,rw_result=1时,LED常亮否则闪烁。此模块的时钟同样是dri_clk=500khz.. 我们的试验任务是给地址0-255写入0-255. MAX_BYTE就是256.同时这个模块还要负责输出使能IIC_dri模块,还有控制读写。由于写数据是有延迟的(读没有),我们设置MS延迟,每写一个延迟5ms,下面解释状态流。

FOLW0:写延时,每写一个数据都延时一次。wait_cnt计数直到到WR_WAIT_TIME - 1,然后复位0,如果地址计数没到256,拉高i2c_exec状态流+1;如果地址到了256(地址是到255但是要读完,所以计到256结束)地址清零,由写变成读。状态流变成2.

FOLW1:单个写数据。当写完一个数据,状态回到FLOW0,延时计数且地址加一,由于咱们数据是写入0-255,数据同时+1跟随计数就可以。

FOLW2:状态流+1,拉高i2c_exec。

FOLW3:判断读出数据。这里要考虑EEPROM未应答,ack==1.由于读数据没有延迟,所以读完状态回到FLOW2,而不是FLOW0.我们判断也是跟随地址,i2c_done表示一次操作完成,读写都是这个,所以当i2c_done=1时,判断当前地址是否等于从E2PROM读出的数据(由i2c_dri读取并输入给当前模块)i2c_data_r。

if(i2c_done == 1'b1) begin                 //EEPROM单次读出完成
                    //读出的值错误或者I2C未应答,读写测试失败
                    if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
                        rw_done <= 1'b1;
                        rw_result <= 1'b0;
                    end
                    else 
                        if(i2c_addr == MAX_BYTE - 1'b1) begin //全部读写测试成功
                            rw_done <= 1'b1;
                            rw_result <= 1'b1;
                        end    
                        else begin
                            flow_cnt <= 2'd2;    //对比完一个之后继续比较所以要跳到FLOW2
                            i2c_addr <= i2c_addr + 1'b1;
                        end
                end                

这里是直接一个一个比,读数据读一个比一个。一开始rw_result是0,然后读到错误还是0,读一个对的也是0,直到读到全部都是对才拉高。

关于之前的写操作地址为什么是到256,而这里是到255.:写数据时写完一个地址加一。写完第255个加1,然后判断;读数据则是读完先判断再加地址,所以读完255就已经同时判断好了,所以到255就输出结果。

LED部分,这部分功能就是接收来自RW模块输出的结果rw_result。当读写没完成时LED不亮,读正确常亮,错误闪烁。

module led_alarm 
    #(parameter L_TIME = 25'd25_000_000 
    )
    (
    input        clk       ,  //时钟信号
    input        rst_n     ,  //复位信号
                 
    input        rw_done   ,  //完成标志
    input        rw_result ,  //E2PROM读写测试结果
    output  reg  led          //E2PROM读写测试结果表示,用灯来表示常亮就是正确,否则就是错误
);

//reg define
reg          rw_done_flag;    //读写测试完成标志
reg  [24:0]  led_cnt     ;    //led计数

//*****************************************************
//**                    main code
//*****************************************************

//读写测试完成标志,由于rw_done是一个脉冲信号所以要对他寄存。
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        rw_done_flag <= 1'b0;
    else if(rw_done)
        rw_done_flag <= 1'b1;
end        

//错误标志为1时PL_LED0闪烁,否则PL_LED0常亮
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        led_cnt <= 25'd0;
        led <= 1'b0;
    end
    else begin
        if(rw_done_flag) begin
            if(rw_result)                          //读写测试正确
                led <= 1'b1;                       //led灯常亮
            else begin                             //读写测试错误
                led_cnt <= led_cnt + 25'd1;
                if(led_cnt == L_TIME - 1'b1) begin
                    led_cnt <= 25'd0;
                    led <= ~led;                   //led灯闪烁
                end                    
            end
        end
        else
            led <= 1'b0;                           //读写测试完成之前,led灯熄灭
    end    
end

endmodule

在TOP例化中,来控制L_TIME,由于LED模块用的时钟也是1KHZ,因此闪烁计数值应该以这个来设计。顶层中L_TIME=125000。125ms变一次。(顶层例化的参数才是最终的,所以这里的25000000并不是)

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值