IIC读写EEPROM

1、IIC协议
1.1、什么是iic

    IC即为Inter-Inegrated Circuit(集成电路总线),在上世纪八十年代左右由Philips公司(即现做的NXP半导体公司)设计出来的一种简单、双向、二进制总线标准。其只需要两根线(SDA数据线和SCL时钟线)即可实现总线连接的器件间的通信,但同一时间只能有一个主机,即一主多从模式,通信方式为半双工通信。SDA和SCL都是双向I/O线,所以主要适用于数据量小,距离短的主从通信。
    数据传输模式分为三种:

  • 标准模式:100kbit/s
  • 快速模式:400kbit/s
  • 高速模式:3.4Mbit/s
    在这里插入图片描述
1.2、EEPROM

    1、EEPROM,全称电可擦除可编程只读存储器 (Electrically-Erasable Programmable Read-Only Memory),是一种可以通过电子方式多次复写的半导体存储设备。相比EPROM,EEPROM不需要用紫外线照射,也不需取下,就可以用特定的电压,来抹除芯片上的信息,以便写入新的数据。
    2、我们使用的是C4开发板,型号为24LC04B的EEPROM存储芯片。24LC04B的存储容量为512Bytes/4Kbits,其内部有两个Block,每个Block中有256个字节(一个字节为8bit)。其读写操作都是以字节(8bit)为基本单位。
    3、24LC04B EEPROM 存储芯片的器件地址包括:

  • 厂商设置的高4位1010,这里表设备代码
  • 用户需自主设置的低3位 x、x、B0 来选择块地址
  • 字节存储地址,一共8bit

    4、在IIC主从设备通讯时,主机在发送了起始信号后,接着会向从机发送控制命令。控制命令长度为1个字节,它的高7位为上文讲解的 IIC设备的器件地址,最低位为读写控制位。EEPROM储存芯片控制命令格式示意图,具体见下图。
在这里插入图片描述

  • 读写控制位为 0 时,表示主机(FPGA)要对从机(EEPROM)进行数据写入操作:{7’b1010xxx,1’b0}
  • 读写控制位为 1 时,表示主机(FPGA)要对从机(EEPROM)进行数据读出操作:{7’b1010xxx,1’b1}
1.3、iic传输特点

    由于IIC是由数据线(SDA)和时钟线(SCL)所组成的,因此二者相互配合完成数据的传输。当时钟线(SCL)为高电平时要求数据线(SDA)的数据稳定,此时从机开始接收主机数据或者主机接收从机的应答;当时钟线(SCL)为低电平时数据线(SDA)的数据可变化,即更新数据以备下一次的接收。
在这里插入图片描述

1.4、时序特点
1.4.1、起始信号

    1、IIC传输主要由开始信号、停止信号、应答/非应答、读、写信号五部分组成。
    2、起始信号:
    当时钟线(SCL)为高电平期间,数据线(SDA)由高电平变为低电平即为起始信号,时序图如下图所示:
在这里插入图片描述

1.4.2、停止信号

    停止信号:
    当时钟线(SCL)为高电平期间数据线(SDA)由低电平恢复到高电平即为终止信号,时序图如下图所示:
在这里插入图片描述

1.4.3、读操作

    1、读数据:
    读数据的时序与应答/非应答的相同,都是在时钟线(SCL)为高电平期间读取数据线(SDA)的值,数据线(SDA)的高低电平即代表“1”或“0”,分为当前地址读、随机读以及顺序读
    2、当前地址读:当前一次读操作或者写操作完毕后,24XX04内部有一个地址计数器,会自增一,所以当前地址读是对下一个地址去读(当前地址读先起始位——然后写控制字节,从机接收到应答信号,然后读数据,无应答信号——最后结束位):
在这里插入图片描述

    3、随机读(起始位——写控制字节,从机接收到应答信号——然后dummuy write 虚写,写数据地址,从机接收到应答信号——再次读起始位——读控制字节,从机接收到应答信号——读数据——停止位)
在这里插入图片描述

    4、顺序读:
    顺序读就在随机读之后
在这里插入图片描述

1.4.4、写操作

    1、从机在时钟线(SCL)高电平期间读取数据线(SDA)的数据,因此写数据时需要在时钟线(SCL)低电平期间改变数据线(SDA)的值来传输数据,分为字节写和页写。
    2、字节写,一次只能写入一字节(写起始位——写控制字节,从机接收到发应答信号——写数据地址,从机接收到发应答信号,如果数据地址是2位的话,就继续写数据地址,从机接收到发应答信号——写数据,从机接收到发应答信号——停止位)
在这里插入图片描述

    3、页写,一次最多写入16个字节(每字节为8bit)数据,(写起始位——写控制字节,从机接收到应答信号——写数据地址,从机接收到应答信号,如果数据地址是2位的话,就继续写数据地址,从机接收到应答信号——写数据,从机接收到应答信号——继续写数据,直到写完全部的数据——停止位)
在这里插入图片描述

2、模块设计
2.1、总体设计思想

    1、使用芯片外设按键模拟读请求,收到读写请求信号时,FPGA通过I2C协议向EEPROM芯片写入单字节数据或从E2PROM芯片读出单字节数据,再使用串口来接收需要写入的数据和显示被读出的数据。需要一个读写控制模块与I2C接口模块:

  • 读写控制模块负责发送读写命令和地址,以及使用两个fifo分别寄存uart写入数据和EEPROM读出数据。
  • 2C接口模块负责将这些指令翻译为I2C协议允许的格式再传输EEPROM中,并将接收到的信息翻译再反馈给FPGA(串并转换)。
  • 串口收发模块以及收发分别对应的2个fifo。

    2、读:

  • 按下key,经过按键消抖模块后输出稳定的key_out信号到eeprom_rw模块中
  • eeprom_rw模块接收到读请求信号后向i2c_interface模块发送读请求
  • i2c_interface模块接收到读请求后发送读控制字和读地址,然后将接收到的SDA信号进行一个串并转换发送到eeprom_rw模块中
  • eeprom_rw模块接收到的rd_data[7:0]缓存到rdfifo中,然后以先进先出的原则依次发送给uart_tx模块
  • uart_tx模块将接收到的每一个rd_data[7:0]进行并串转换,然后发送到上位机中,在发送途中拉高busy信号,表示tx线已被占用,等发完这个字节再继续传下一个进来

    3、写:

  • 由上位机串行发送 x byte的数据到串口接收模块uart_rx中
  • uart_rx模块将接收到的串行信号进行串并转换然后发送到eeprom_rw模块中
  • eeprom_rw模块,每接收到一次din_vld(数据接收有效信号)后,开启wrfifo的写请求,写入一个字节,直到接收完为止
  • wrfifo缓存完成后,eeprom_rw模块向i2c_interface模块发送写请求和需要写入的数据,每次发送1个字节的数据,在收到应答信号后再次发送1个字节
  • i2c_interface模块接收到写命令后便开始发送写控制字和写地址,然后将接收到的并行数据wr_data[7:0]进行一个并串转换来写入到eeprom中
2.2、 模块状态图

    1、iic模块状态图
    WRITE:写完一个字节
    READ:读完一个字节继续写数据发一个字节
    START:发起始位
    STOP:发停止位
    SACK:发送应答,分为有效应答和无效应答
    RACK:接收应答,分为有效应答和无效应答
在这里插入图片描述

    2、EEPROM读写模块状态图
在这里插入图片描述

    3、总体框架图
在这里插入图片描述

2.3、部分代码

    1、顶层模块代码

`include "param.v"

module i2c_top (
    input   clk_50  ,                                        //时钟信号
    input   rst_n   ,                                        //复位信号
    input   key     ,
    input   rx      ,                                        //串口数据接收总线
    inout   i2c_sda ,                                        //i2c的数据线
                              
    output  tx      ,                                        //串口数据发送总线
    output  i2c_scl                                          //i2c的时钟线
    );

//信号定义
    wire  [1:0]     baud_sel    ;                            //波特率选择
    wire            key_out     ;                            //按键消抖输出
    wire  [7:0]     rx_byte     ;                            //串口接收到的值经过串并转换eeprom_control模块  
    wire            rx_byte_vld ; 
    wire            tx_busy     ;                            //串口忙状态标志
    wire  [7:0]     tx_data     ;                            //EEPROM的数据输出到串口发送模块
    wire            tx_data_vld ;

    assign  baud_sel = 3;                                    //选择波特率为115200

//例化按键消抖模块
    key_debounce #(.KEY_W(1))             u_key(
	/*input          */.clk_50            (clk_50    ),                              
	/*input          */.rst_n             (rst_n     ),                            
	/*input          */.key_in            (key       ),                              
	                               
	/*output         */.key_out           (key_out   )                   
    );

//例化串口发送模块
    uart_tx            u_tx(
    /*input          */.clk_50            (clk_50      ),
	/*input          */.rst_n             (rst_n       ),  
    /*input  [1:0]   */.baud_sel          (baud_sel    ),    //选择波特率                 
	/*input  [7:0]   */.din               (tx_data     ),
	/*input          */.din_vld           (tx_data_vld ),

	/*output         */.busy              (tx_busy     ),    //忙状态指示
	/*output         */.tx                (tx          )     //数据发送总线
	);	

//例化串口接收模块
    uart_rx            u_rx(
    /*input          */.clk_50            (clk_50      ),
	/*input          */.rst_n             (rst_n       ),	                    
	/*input          */.rx                (rx          ),    //数据接收总线
    /*input  [1:0]   */.baud_sel          (baud_sel    ),    //选择波特率  
    
	/*output  [7:0]  */.rx_byte           (rx_byte     ),
	/*output         */.rx_byte_vld       (rx_byte_vld )
	);	

//例化EEPROM控制模块
    eeprom_control#(.WR_LEN(`WR_BYTE), .RD_LEN(`RD_BYTE))  u_eeprom(
    /*input               */.clk_50       (clk_50      ),
    /*input               */.rst_n        (rst_n       ),
    /*input       [7:0]   */.din          (rx_byte     ),
    /*input               */.din_vld      (rx_byte_vld ),
    /*input               */.rd_en        (key_out     ),
    /*input               */.tx_busy      (tx_busy     ),   //串口发送忙标志
    /*inout               */.i2c_sda      (i2c_sda     ),

    /*output      [7:0]   */.dout         (tx_data     ),
    /*output              */.dout_vld     (tx_data_vld ),
    /*output              */.i2c_scl      (i2c_scl     )
    
	);	


endmodule //i2c_top

    3、IIC接口模块

`include "param.v"

module iic_interface (
    input           clk        ,                          //时钟信号 
    input           rst_n      ,                          //复位信号
    input           req        ,                          //输入请求信号
    input   [3:0]   cmd        ,                          //输入命令
    input   [7:0]   din        ,                          //数据
    input           iic_sda_i  ,                          //输入串行数据

    output          iic_scl    ,                          //iic的时钟信号(200kbit/s)速率                                      
    output          iic_sda_o  ,                          //输出IIC串行数据总线
    output          iic_sda_oe ,                          //输出使能信号                                                
    output          done       ,                          //传输完成信号
    output  [7:0]   dout                                  //输出数据
);

//状态机传输定义
    parameter   IDLE    = 7'b000_0001 ,                   //空闲状态
                START   = 7'b000_0010 ,                   //发起始位状态
                WRITE   = 7'b000_0100 ,                   //写状态:FPGA向EEPROM写入数据
                RACK    = 7'b000_1000 ,                   //接收应答状态:FPGA接收EEPROM发送的应答
                READ    = 7'b001_0000 ,                   //读状态:FPGA向EEPROM读出数据
                SACK    = 7'b010_0000 ,                   //发送应答状态:FPGA发送应答给EEPROM(接收方发应答)
                STOP    = 7'b100_0000 ;                   //发停止位状态

//信号定义
    reg   [6:0]      cstate           ;
    reg   [6:0]      nstate           ;

    reg              iic_scl_r        ;
    reg              iic_sda_o_r      ;
    reg              iic_sda_oe_r     ;
    reg   [7:0]      dout_r           ;
    
    reg   [7:0]      cnt_scl          ;                   //iic的时钟频率计数寄存器
    wire             add_cnt_scl      ;   
    wire             end_cnt_scl      ;

    reg   [3:0]      cnt_bit          ;                   //比特计数寄存器
    wire             add_cnt_bit      ;
    wire             end_cnt_bit      ;    

    reg   [7:0]      tx_data          ;                   //寄存数据输入(din)
    reg   [7:0]      rx_data          ;                   //寄存iic的数据总线数据
    reg   [3:0]      bit_num          ;

    wire             idle_to_start    ;
    wire             idle_to_write    ;
    wire             idle_to_read     ;
    wire             start_to_write   ;
    wire             start_to_read    ;
    wire             write_to_rack    ;
    wire             read_to_sack     ;
    wire             rack_to_idle     ;
    wire             rack_to_stop     ;
    wire             sack_to_idle     ;
    wire             sack_to_stop     ;
    wire             stop_to_idle     ;

//iic的时钟频率计数实现
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            cnt_scl <= 0;
        else if (add_cnt_scl)
        begin
            if (end_cnt_scl) 
                cnt_scl <= 0;
            else
                cnt_scl <= cnt_scl + 1'b1;
        end
    end
    assign  add_cnt_scl = cstate != IDLE;
    assign  end_cnt_scl = add_cnt_scl && cnt_scl == `SCL_PERIOD - 1;

//比特计数实现
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            cnt_bit <= 0;
        else if (add_cnt_bit)
        begin
            if (end_cnt_bit) 
                cnt_bit <= 0;
            else
                cnt_bit <= cnt_bit + 1'b1;
        end
    end
    assign  add_cnt_bit = end_cnt_scl;
    assign  end_cnt_bit = add_cnt_bit && cnt_bit == bit_num - 1;

//bit_num
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            bit_num <= 0;
        else if (cstate == READ | cstate == WRITE)
            bit_num <= 8;
        else
            bit_num <= 1;
    end

//状态机跳转
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            cstate <= IDLE;
        else 
            cstate <= nstate;
    end

    always @(*) 
    begin
        case (cstate)
            IDLE:  
            begin
                if (idle_to_start) 
                    nstate = START;
                else if (idle_to_write)
                    nstate = WRITE;
                else if (idle_to_read)
                    nstate = READ;
                else
                    nstate = cstate;
            end
            START:  
            begin
                if (start_to_read)
                    nstate = READ;
                else if (start_to_write)
                    nstate = WRITE;
                else 
                    nstate = cstate;
            end
            WRITE:  
            begin
                if (write_to_rack)
                    nstate = RACK;
                else 
                    nstate = cstate;
            end
            READ:  
            begin
                if (read_to_sack)
                    nstate = SACK;
                else    
                    nstate = cstate;
            end
            RACK:  
            begin
                if (rack_to_stop)
                    nstate = STOP;
                else if (rack_to_idle)
                    nstate = IDLE;
                else
                    nstate = cstate;
            end
            SACK:  
            begin
                if (sack_to_stop)
                    nstate = STOP;
                else if (sack_to_idle)
                    nstate = IDLE;
                else
                    nstate = cstate;
            end
            STOP:  
            begin
                if (stop_to_idle)
                    nstate = IDLE;
                else 
                    nstate = cstate;
            end
            default:  ;
        endcase
    end

//状态机跳转条件判断
    assign  idle_to_start  = nstate == IDLE  & (req & (cmd&`CMD_START))         ;
    assign  idle_to_write  = nstate == IDLE  & (req & (cmd&`CMD_WRITE))         ;
    assign  idle_to_read   = nstate == IDLE  & (req & (cmd&`CMD_READ ))         ;
    assign  start_to_write = nstate == START & (end_cnt_bit & (cmd&`CMD_WRITE)) ;
    assign  start_to_read  = nstate == START & (end_cnt_bit & (cmd&`CMD_READ))  ;
    assign  write_to_rack  = nstate == WRITE & (end_cnt_bit)                    ;
    assign  read_to_sack   = nstate == READ  & (end_cnt_bit)                    ;
    assign  rack_to_idle   = nstate == RACK  & (end_cnt_bit & ~(cmd&`CMD_STOP)) ;
    assign  rack_to_stop   = nstate == RACK  & (end_cnt_bit & (cmd&`CMD_STOP))  ;
    assign  sack_to_idle   = nstate == SACK  & (end_cnt_bit & ~(cmd&`CMD_STOP)) ;
    assign  sack_to_stop   = nstate == SACK  & (end_cnt_bit & (cmd&`CMD_STOP))  ;
    assign  stop_to_idle   = nstate == STOP  & end_cnt_bit                      ; 

//tx_data
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            tx_data <= 0;
        else if (req)
            tx_data <= din;
    end

//iic_scl_r, iic时钟总线数据寄存待输出
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            iic_scl_r <= 1'b1;
        else if (idle_to_start | idle_to_write | idle_to_read)                //发送数据时拉低时钟总线
            iic_scl_r <= 1'b0;
        else if (add_cnt_scl & cnt_scl == `SCL_HALF)
            iic_scl_r <= 1'b1;
        else if (add_cnt_scl && ~stop_to_idle)                                 //停止信号是scl保持高电平时sda恢复到高电平
            iic_scl_r <= 1'b0;
    end

//iic_sda_o_r, iic数据总线数据寄存待输出    
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            iic_sda_o_r <= 1'b1; 
        else if (cstate == START)                                              //发送起始位
        begin
            if (add_cnt_scl & cnt_scl == `LOW_HLAF)                            //先在时钟低电平时拉高sda数据线,保证能够检测到起始位
                iic_sda_o_r <= 1'b1;
            else if (add_cnt_scl & cnt_scl == `HIGH_HALF)                      //检测到起始位
                iic_sda_o_r <= 1'b0;
        end
        else if (cstate == WRITE  & cnt_scl == `LOW_HLAF)                      //时钟低电平时进行数据发送
            iic_sda_o_r <= tx_data[7-cnt_bit];
        else if (cstate == RACK & cnt_scl == `LOW_HLAF)
            iic_sda_o_r <= (cmd&`CMD_STOP) ? 1'b1:1'b0;                        //是否发送应答
        else if (cstate == STOP)                                               //发送停止位
        begin
            if (add_cnt_scl & cnt_scl == `LOW_HLAF)                            //先在时钟低电平时拉低sda数据线,保证能够检测到停止位
                iic_sda_o_r <= 1'b0;
            else if (add_cnt_scl & cnt_scl == `HIGH_HALF)                      //检测到停止位
                iic_sda_o_r <= 1'b1;
        end
    end

//iic_sda_oe_r
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            iic_sda_oe_r <= 0;
        else if (idle_to_start | idle_to_write | rack_to_stop | read_to_sack) //发送数据时使能信号拉高
            iic_sda_oe_r <= 1'b1;
        else if (idle_to_read | start_to_read | write_to_rack | stop_to_idle)
            iic_sda_oe_r <= 1'b0;
    end

//iic_sda_i  读入数据寄存
    always @(posedge clk or negedge rst_n) 
    begin
        if (!rst_n)
            rx_data <= 0;
        else if (cstate == READ & cnt_scl == `HIGH_HALF)
            rx_data[7-cnt_bit] <= iic_sda_i;
    end

//输出
    assign  iic_scl    = iic_scl_r    ;
    assign  iic_sda_o  = iic_sda_o_r  ;
    assign  iic_sda_oe = iic_sda_oe_r ;
    assign  dout       = rx_data      ;
    assign  done       = rack_to_idle | sack_to_idle | stop_to_idle ;


endmodule //iic_interface

    
    
    

参考:
    https://blog.csdn.net/xs_sd/article/details/114534036
    https://blog.csdn.net/weixin_45888898/article/details/122889135

  • 5
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值