基于FPGA实现IIC 协议读写 EEPROM

IIC 基本特性

总线信号

SDA:串行数据线
SCL:串行数据时钟

总线空闲状态

SDA:高电平
SCL:高电平

IIC 协议起始位

SCL 为高电平时,SDA 出现下降沿,产生一个起始位。、
IIC 协议起始位.png

IIC 协议结束位

SCL 为高电平时,SDA 出现上升沿,产生一个结束位。
IIC 协议结束位.png

IIC 读写单字节时序

IIC 主机对 IIC 从机写入数据时,SDA 上的每一位数据在 SCL 的高电平期间被写入从机中。对于主机,在 SCL 的低电平期间改变要写入的数据。
IIC 读写单字节时序.png

IIC 主机从 IIC 从机中读出数据时,从机在 SCL 的低电平期间将数据输出到 SDA 总线上,在SCL 的高电平期间保持数据稳定。对于主机,在 SCL 的高电平期间将 SDA 线上的数据读取并存储。

数据接收方对数据发送方的响应

每当一个字节的数据或命令传输完成时,都会有一位的应答位。需要应答位时,数据发出方将 SDA 总线设置为 3 态输入,由于 IIC 总线上都有上拉电阻,因此此时总线默认为高电平,若数据接收方正确接收到数据,则数据接收方将 SDA 总线拉低,以示正确应答。

例如当 IIC 主机对 IIC 从机写入数据或命令时,每个字节都需要从机产生应答信号以告诉主机数据或命令成功被写入。所以,当 IIC 主机将 8 位的数据或命令传出后,会将 SDA 信号设置为输入,等待从机应答(等待 SDA 被从机拉低为低电平),若从机正确应答,表明当前数据或命令传输成功,可以结束或开始下一个命令/数据的传输,否则表明数据/命令写入失败,主机就可以决定是否放弃写入或者重新发起写入。

IIC 器件地址

每个 IIC 器件都有一个器件地址,有的器件地址在出厂时地址就设置好了,用户不可以更改(ov7670:0x42),有的确定了几位,剩下几位由硬件确定(比如有三位由用户确定,就留有 3 个控制地址的引脚,最常见的为 IIC 接口的 EEPROM 存储器),此类较多;还有的有地址寄存器。

严格讲,主机不是向从机发送地址,而是主机往总线上发送地址,所有的从机都能接收到主机发出的地址,然后每个从机都将主机发出的地址与自己的地址比较,如果匹配上了,这个从机就会向主机发出一个响应信号。主机收到响应信号后,开始向总线上发送数据,与这个从机的通讯就建立起来了。如果主机没有收到响应信号,则表示寻址失败。

通常情况下,主从器件的角色是确定的,也就是说从机一直工作在从机模式。不同的器件定义地址的方式是不同的,有的是软件定义,有的是硬件定义。例如某些单片机的 IIC 接口作为从机时,其器件地址是可以通过软件修改从机地址寄存器确定的。而对于一些其他器件,如 CMOS 图像传感器、EEPROM 存储器,其器件地址在出厂时就已经设定好了,具体值可以在对应的数据手册中查到。

对于 AT24C64 这样一颗 EEPROM 器件,其器件地址为 1010 加 3 位的片选信号。3 位片选信号由硬件连接决定。例如 SOIC 封装的该芯片 pin1、pin2、pin3 为片选地址。当硬件电路上分别将这三个 pin 连接到 GND 或 VCC 时,就实现了设置不通的片选地址。IIC 协议在进行数据传输时,主机需要首先向总线上发出控制命令,其中,控制命令就包含了从机地址/片选信号+读写控制。然后等待从机响应。以下为 IIC 控制命令传输的数据格式。
IIC 器件地址.png

IIC 传输时,按照从高到低的位序进行传输。控制字节的最低位为读写控制位,当该位为 0 时表示主机对从机进行写操作,当该位为 1 时表示主机对从机进行读操作。例如,当需要对片选地址为 100 的 AT24LC64 发起写操作,则控制字节应该为 CtrlCode = 1010_100_0。

若要读,则控制字节应该为 CtrlCode = 1010_100_1。

IIC 存储器地址

我们要对一个器件中的存储单元(寄存器和存储器以下简称存储单元)进行读写,就必须要能够指定存储单元的地址。IIC 协议设计了有从机存储单元寻址地址段,该地址段为一个字节或两个字节长度,在主机确认收到从机返回的控制字节响应后,由主机发出。地址段长度视不同的器件类型,长度不同
IIC 存储器地址.png

IIC 读写时序

IIC 单字节写时序

1 字节地址段器件单字节写时序
IIC 单字节写时序.png

2 字节地址段器件单字节写时序
字节地址段器件单字节写时序.png

从主机角度看一次写入过程

a. 主机设置 SDA 为输出
b. 主机发起起始信号
c. 主机传输器件地址字节,其中最低为 0,表明为写操作。
d. 主机设置 SDA 为输入三态,读取从机应答信号。
e. 读取应答信号成功,传输 1 字节地址数据
f. 主机设置 SDA 为输入三态,读取从机应答信号。
g. 对于两字节地址段器件,传输地址数据低字节,对于 1 字节地址段器件,传输待写入的数据
h. 设置 SDA 为输入三态,读取从机应答信号。
i. 对于两字节地址段器件,传输待写入的数据(2 字节地址段器件可选)
j. 设置 SDA 为输入三态,读取从机应答信号(2 字节地址段器件可选)。
k. 主机产生 STOP 位,终止传输

IIC 连续写时序(页写时序)

  1. 字节地址段器件多字节写时序
    字节地址段器件多字节写时序.png

  2. 字节地址段器件多字节写时序
    字节地址段器件多字节写时序2.png

从主机角度看一次写入过程

  1. 主机设置 SDA 为输出
  2. 主机发起起始信号
  3. 主机传输器件地址字节,其中最低为 0,表明为写操作。
  4. 主机设置 SDA 为输入三态,读取从机应答信号。
  5. 读取应答信号成功,传输 1 字节地址数据
  6. 主机设置 SDA 为输入三态,读取从机应答信号。
  7. 对于两字节地址段器件,传输低字节地址数据,对于 1 字节地址段器件,传输待写入的第一个数据
  8. 设置 SDA 为输入三态,读取从机应答信号。
  9. 写入待写入的第 2 至第 n 个数据并读取应答信号。对于 AT24Cxx,一次可写入的最
    大长度为 32 字节。
  10. 主机产生 STOP 位,终止传输。

IIC 单字节读时序

  1. 字节地址段器件单节读时序
    字节地址段器件单节读时序.png

  2. 字节地址段器件单节读时序
    字节地址段器件单节读时序2.png

从主机角度看一次读取过程

  1. 主机设置 SDA 为输出
  2. 主机发起起始信号
  3. 主机传输器件地址字节,其中最低为 0,表明为写操作。
  4. 主机设置 SDA 为输入三态,读取从机应答信号。
  5. 读取应答信号成功,传输 1 字节地址数据
  6. 主机设置 SDA 为输入三态,读取从机应答信号。
  7. 对于两字节地址段器件,传输低字节地址数据,对于 1 字节地址段器件,无此段数据传输。
  8. 主机发起起始信号
  9. 主机传输器件地址字节,其中最低为 1,表明为写操作。
  10. 设置 SDA 为输入三态,读取从机应答信号。
  11. 读取 SDA 总线上的一个字节的数据
  12. 产生无应答信号(高电平)(无需设置为输出高点片,因为总线会被自动拉高)
  13. 主机产生 STOP 位,终止传输。

IIC 多字节连续读时序(页读取)

  1. 字节地址段器件多字节读时序
    字节地址段器件多字节读时序.png

  2. 字节地址段器件多字节读时序
    字节地址段器件多字节读时序2.png

从主机角度看一次读取过程

  1. 主机设置 SDA 为输出
  2. 主机发起起始信号
  3. 主机传输器件地址字节,其中最低为 0,表明为写操作。
  4. 主机设置 SDA 为输入三态,读取从机应答信号。
  5. 读取应答信号成功,传输 1 字节地址数据
  6. 主机设置 SDA 为输入三态,读取从机应答信号。
  7. 对于两字节地址段器件,传输低字节地址数据,对于 1 字节地址段器件,无此段数据传输。
  8. 主机发起起始信号
  9. 主机传输器件地址字节,其中最低为 1,表明为写操作。
  10. 设置 SDA 为输入三态,读取从机应答信号。
  11. 读取 SDA 总线上的 n 个字节的数据(对于 AT24Cxx,一次读取长度最大为 32 字节)
  12. 产生无应答信号(高电平)(无需设置为输出高点片,因为总线会被自动拉高)主机产生 STOP 位,终止传输

EEPROM 读写控制程序设计

EEPROM 存储器芯片的型号为 AT24C64,其存储器容量为 64kbit,器件
片选地址有 3 位,A2、A1、A0。数据存储地址是 13 位,属于 2 字节地址段器件。
根据上面 IIC 的基本概念中有关读写时 SDA 与 SCL 时序,不管对于从机还是主机 SDA上的每一位数据在 SCL 的高电平期间为有效数据,在 SCL 的低电平期间是要改变的数据。
根据这个用 2 个标志位对时钟 SCL 的高电平和低电平进行标记,如下图所示:scl_high 对SCL 高电平中间进行标志,scl_low 对 SCL 低电平中间进行标志。这个在具体的实现中也不难实现。
EEPROM 读写控制程序设计.png

IIC 读写状态机设计

SCL 时钟总线以及其高低电平中间标志位产生完成后其后就是 SDA 数据线的产生,这个需要根据具体的读写操作完成。这里主要采用状态机实现
IIC FPGA 读写状态机设计.png


module IIC_24LC64(
     clk50M,
     reset,
     iic_en,
     cs_bit,
     address,
     write,
     write_data,
     read,
     read_data,
     scl,
     sda,
     done
);
 input clk50M; //系统时钟 50MHz
 input reset; //异步复位信号
 input iic_en; //使能信号
 input [2:0]cs_bit; //器件选择地址
input [12:0]address; //13 位数据读写地址,24LC64 有 13 位数据存储
地址
 input write; //写数据信号
 input [7:0]write_data; //写数据
 input read; //读数据信号
 output reg[7:0]read_data; //读数据

 output reg scl; //IIC 时钟信号
 inout sda; //IIC 数据总线

 output reg done; //一次 IIC 读写完成

 parameter SYS_CLOCK = 50_000_000; //系统时钟采用 50MHz
 parameter SCL_CLOCK = 200_000; //scl 总线时钟采用 200kHz

 //状态
 parameter
      Idle = 16'b0000_0000_0000_0001,
      Wr_start = 16'b0000_0000_0000_0010,
      Wr_ctrl = 16'b0000_0000_0000_0100,
      Ack1 = 16'b0000_0000_0000_1000,
      Wr_addr1 = 16'b0000_0000_0001_0000,
      Ack2 = 16'b0000_0000_0010_0000,
      Wr_addr2 = 16'b0000_0000_0100_0000,
      Ack3 = 16'b0000_0000_1000_0000,
      Wr_data = 16'b0000_0001_0000_0000,
      Ack4 = 16'b0000_0010_0000_0000,
      Rd_start = 16'b0000_0100_0000_0000,
      Rd_ctrl = 16'b0000_1000_0000_0000,
      Ack5 = 16'b0001_0000_0000_0000,
      Rd_data = 16'b0010_0000_0000_0000,
      Nack = 16'b0100_0000_0000_0000,
      Stop = 16'b1000_0000_0000_0000;

 //sda 数据总线控制位
 reg sda_en;

 //sda 数据输出寄存器
 reg sda_reg;

 assign sda = sda_en ? sda_reg : 1'bz;

 //状态寄存器
 reg [15:0]state;
//读写数据标志位
 reg W_flag;
 reg R_flag;

 //写数据到 sda 总线缓存器
 reg [7:0]sda_data_out;
 reg [7:0]sda_data_in;
 reg [3:0]bit_cnt;


 reg [7:0]scl_cnt;
 parameter SCL_CNT_M = SYS_CLOCK/SCL_CLOCK; //计数最大值
 reg scl_cnt_state;

 //产生 SCL 时钟状态标志 scl_cnt_state,为 1 表示 IIC 总线忙,为 0 表示总线闲
 always@(posedge clk50M or negedge reset)
     begin
     if(!reset)
         scl_cnt_state <= 1'b0;
     else if(iic_en)
         scl_cnt_state <= 1'b1;
     else if(done)
         scl_cnt_state <= 1'b0;
     else
         scl_cnt_state <= scl_cnt_state;
     end

 //scl 时钟总线产生计数器
 always@(posedge clk50M or negedge reset)
       begin
          if(!reset)
              scl_cnt <= 8'b0;
          else if(scl_cnt_state)
             begin
                 if(scl_cnt == SCL_CNT_M - 1)
                     scl_cnt <= 8'b0;
                 else
                     scl_cnt <= scl_cnt + 8'b1;
             end
          else
                 scl_cnt <= 8'b0;
          end
//scl 时钟总线产生
 always@(posedge clk50M or negedge reset)
         begin
             if(!reset)
                 scl <= 1'b1;
             else if(scl_cnt == (SCL_CNT_M>>1)-1)
                 scl <= 1'b0;
             else if(scl_cnt == SCL_CNT_M - 1)
                 scl <= 1'b1;
             else
                 scl <= scl;
          end

 //scl 时钟电平中部标志位
 reg scl_high;
 reg scl_low;

 always@(posedge clk50M or negedge reset)
        begin
             if(!reset)
             begin
                 scl_high <= 1'b0;
                 scl_low <= 1'b0;
             end 
             else if(scl_cnt == (SCL_CNT_M>>2))
                 scl_high <= 1'b1;
             else if(scl_cnt == (SCL_CNT_M>>1)+(SCL_CNT_M>>2))
                 scl_low <= 1'b1;
             else
             begin
                 scl_high <= 1'b0;
                 scl_low <= 1'b0; 
 end
 end

 //状态机
 always@(posedge clk50M or negedge reset)
 begin
 if(!reset)
 begin
 state <= Idle;
 sda_en <= 1'b0;
 sda_reg <= 1'b1;
 W_flag <= 1'b0;
R_flag <= 1'b0; 
 done <= 1'b0;
 end
 else 
 case(state)
 Idle:
 begin 
 done <= 1'b0;
 W_flag <= 1'b0; 
 R_flag <= 1'b0;
 sda_en <= 1'b0; 
 sda_reg <= 1'b1;
 if(iic_en && write) //使能 IIC 并且为写操作
 begin
 W_flag <= 1'b1; //写标志位置 1 
 sda_en <= 1'b1; //设置 SDA 为输出模式
 sda_reg <= 1'b1; //SDA 输出高电平
 state <= Wr_start; //跳转到起始状态 
 end
 else if(iic_en && read) //使能 IIC 并且为读操作
 begin
 R_flag <= 1'b1; //读标志位置 1 
 sda_en <= 1'b1; //设置 SDA 为输出模式
 sda_reg <= 1'b1; //SDA 输出高电平
 state <= Wr_start; //跳转到起始状态
 end
 else
 state <= Idle; 
 end 

 Wr_start:
 begin
 if(scl_high)
 begin
 sda_reg <= 1'b0;
 state <= Wr_ctrl;
 sda_data_out <= {4'b1010, cs_bit,1'b0}; 
 bit_cnt <= 4'd8;
 end
 else
 begin
 sda_reg <= 1'b1;
 state <= Wr_start;
 end
end

 Wr_ctrl: //写控制字节 4'b1010+3 位片选地址+1 位写控制
 begin
 if(scl_low)
 begin
 bit_cnt <= bit_cnt -4'b1;
 sda_reg <= sda_data_out[7];
 sda_data_out <= {sda_data_out[6:0],1'b0};
 if(bit_cnt == 0)
 begin
 state <= Ack1;
 sda_en <= 1'b0;
 end
 else
 state <= Wr_ctrl; 
 end
 else
 state <= Wr_ctrl; 
 end

 Ack1: //通过判断 SDA 是否拉低来判断是否有从机响应
 begin 
 if(scl_high)
 if(sda == 1'b0)
 begin
 state <= Wr_addr1; 
 sda_data_out <= {3'bxxx,address[12:8]};
 bit_cnt <= 4'd8;
 end
 else
 state <= Idle;
 else
 state <= Ack1; 
 end

 Wr_addr1: //写 2 字节地址中的高地址字节中的低五位
 begin
 if(scl_low)
 begin
 sda_en <= 1'b1;
 bit_cnt <= bit_cnt -4'b1;
 sda_reg <= sda_data_out[7];
 sda_data_out <= {sda_data_out[6:0],1'b0};
if(bit_cnt == 0)
 begin
 state <= Ack2; 
 sda_en <= 1'b0; 
 end
 else
 state <= Wr_addr1; 
 end
 else
 state <= Wr_addr1;
 end

 Ack2: //通过判断 SDA 是否拉低来判断是否有从机响应
 begin 
 if(scl_high)
 if(sda == 1'b0)
 begin
 state <= Wr_addr2; 
 sda_data_out <= address[7:0];
 bit_cnt <= 4'd8;
 end
 else
 state <= Idle;
 else
 state <= Ack2; 
 end

 Wr_addr2: //写 2 字节地址中的低地址字节
 begin
 if(scl_low)
 begin
 sda_en <= 1'b1;
 bit_cnt <= bit_cnt -4'b1;
 sda_reg <= sda_data_out[7];
 sda_data_out <= {sda_data_out[6:0],1'b0};
 if(bit_cnt == 0)
 begin
 state <= Ack3; 
 sda_en <= 1'b0; 
 end
 else
 state <= Wr_addr2; 
 end
 else
state <= Wr_addr2;
 end

 Ack3: //通过判断 SDA 是否拉低来判断是否有从机响应
 begin 
 if(scl_high)
 if(sda == 1'b0) //有响应就判断是读还是写操作
 begin 
 if(W_flag) //如果是写数据操作,进入写数据状态
 begin 
 sda_data_out <= write_data;
 bit_cnt <= 4'd8;
 state <= Wr_data;
 end
 else if(R_flag) //如果是读数据操作,进入读数据开始状
态
 begin
 state <= Rd_start;
 sda_reg <= 1'b1;
 end
 end
 else
 state <= Idle;
 else
 state <= Ack3; 
 end

 Wr_data: //写数据状态,向 EEPROM 写入数据
 begin 
 if(scl_low)
 begin
 sda_en <= 1'b1;
 bit_cnt <= bit_cnt -4'b1;
 sda_reg <= sda_data_out[7];
 sda_data_out <= {sda_data_out[6:0],1'b0};
 if(bit_cnt == 0)
 begin
 state <= Ack4;
 sda_en <= 1'b0;
 end
 else
 state <= Wr_data; 
 end
 else
state <= Wr_data;
 end 

 Ack4: //通过判断 SDA 是否拉低来判断是否有从机响应
 begin
 if(scl_high)
 if(sda == 1'b0) //有响应就进入停止状态
 begin
 sda_reg <= 1'b0;
 state <= Stop; 
 end
 else
 state <= Idle;
 else
 state <= Ack4;
 end

 Rd_start: //读数据的开始操作 
 begin
 if(scl_low)
 begin
 sda_en <= 1'b1;
 end
 else if(scl_high)
 begin
 sda_reg <= 1'b0;
 state <= Rd_ctrl;
 sda_data_out <= {4'b1010, cs_bit,1'b1};
 bit_cnt <= 4'd8;
 end
 else
 begin
 sda_reg <= 1'b1;
 state <= Rd_start;
 end
 end


 Rd_ctrl: //写控制字节 4'b1010+3 位片选地址+1 位读控制 
 begin
 if(scl_low)
 begin
 bit_cnt <= bit_cnt -4'b1;
 sda_reg <= sda_data_out[7];
sda_data_out <= {sda_data_out[6:0],1'b0};
 if(bit_cnt == 0)
 begin
 state <= Ack5;
 sda_en <= 1'b0;
 end
 else
 state <= Rd_ctrl; 
 end
 else
 state <= Rd_ctrl; 
 end 

 Ack5: //通过判断 SDA 是否拉低来判断是否有从机响应 
 begin 
 if(scl_high)
 if(sda == 1'b0) //有响应就进入读数据状态
 begin
 state <= Rd_data;
 sda_en <= 1'b0; //SDA 总线设置为 3 态输入
 bit_cnt <= 4'd8;
 end
 else
 state <= Idle;
 else
 state <= Ack5; 
 end 

 Rd_data: //读数据状态
 begin
 if(scl_high) //在时钟高电平读取数据
 begin
 sda_data_in <= {sda_data_in[6:0],sda};
 bit_cnt <= bit_cnt - 4'd1;
 state <= Rd_data;
 end
 else if(scl_low && bit_cnt == 0) //数据接收完成进入无应答
响应状态
 begin
 state <= Nack; 
 end
 else
 state <= Rd_data; 
 end
Nack: //不做应答响应
 begin
 read_data <= sda_data_in;
 if(scl_high)
 begin
 state <= Stop; 
 sda_reg <= 1'b0;
 end
 else
 state <= Nack; 
 end

 Stop: //停止操作,在时钟高电平,SDA 上升沿
 begin
 if(scl_low)
 begin
 sda_en <= 1'b1; 
 end 
 else if(scl_high)
 begin
 sda_en <= 1'b1;
 sda_reg <= 1'b1; 
 state <= Idle;
 done <= 1'b1;
 end 
 else
 state <= Stop;
 end

 default:
 begin
 state <= Idle;
 sda_en <= 1'b0;
 sda_reg <= 1'b1;
 W_flag <= 1'b0;
 R_flag <= 1'b0;
 done <= 1'b0;
 end 
 endcase 
 end
endmodule
编辑 重设标签(回车键确认) 标为违禁 关闭 合并 删除

提问于 2019-04-28 16:18:44 +0800

这个帖子被标记为一个社区wiki

这个帖子是一个wiki(维基). 任何一个积分 >500的人都可以完善它