FPGA对EEPROM驱动控制(I2C协议)

本文摘要:本文首先对I2C协议的通信模式和AT24C16-EEPROM芯片时序控制进行分析和理解,设计了一个i2c通信方案。人为按下写操作按键后,FPGA(Altera EP4CE10)对EEPROM指定地址写入字节数据,并接后按下读操作按键,读取该地址上的一个字节数据在数码管低两位显示出来。其中包括了对此方案的Modelsim仿真测试,并且接续完成板级验证。(过程笔记)

关键词:EEPROM、I2C协议、Verilog HDL、FPGA

框图设计:

输入端口包括系统时钟、复位信号、写操作和读操作按键,输出端口包括IIC通信接口、数码管段选和位选信号。

共计用到了四种子模块,分别是按键消抖、i2c通信数据处理、i2c通信控制、2khz时钟分配。按逻辑链接成体,完成本文设计的测试方案。在调试过程中,为方便观察操作反应,另外加入了两个LED灯接口,具体框图如下。


【I2C协议通信模式】

I2C协议(通常称为IIC协议)是一种串行、同步、半双工通信协议。I2C协议支持多主机功能,允许多个具备主控能力的设备在同一总线上竞争控制权,并通过硬件仲裁机制避免冲突。其使用串行数据线(SDA)和串行时钟线(SCL)进行通信,标准模式的传输速率为100 kbit/s(Standard-mode, Sm),快速模式下的I2C传输速率提高至400 kbit/s(Fast-mode,Fm),在高速模式下可达3.4Mbit/s

发送到SDA线上的每个字节必须为8位,且每次传输可以发送的字节数量不受限制,每个字节后必须跟一个ACK响应,位首先传输的是数据的最高位MSB,SDA线上的数据必须在SCL时钟的高电平周期保持稳定,数据线的高或低电平状态只有在SCL时钟是低电平时才能改变。

传输信号类型:(见上图)

  • 启动信号(START)(S条件):在SCL线处于高电平,SDA上的数据由高向低转换,则表示启动IIC总线;
  • 应答信号(ACK):在接收到了8bit的信息后, 接收数据一方需要向发送信息的另一方传递默认的低电平脉冲作为信号,表明已经获取了数据;
  • 结束信号(STOP)(P条件):在SCL线处于高电平,SDA上的数据由低向高转换,则表示停止IIC总线。

起始(S)和停止(P)条件一般由主机产生,总线在起始条件后被认为处于忙的状态,在停止条件的某段时间后总线被认为再次处于空闲状态 。

由于I2C 总线没有中央控制,其控制只能由地址或主机码以及竞争主机发送的数据决定,总线也没有任何定制的优先权。所以,当发生多主机通信时,需要一个控制仲裁机制来决定通信优先。

仲裁过程中的时钟同步:

在I2C通信中,所有主机在SCL线上生成自己的时钟信号以传输数据。数据仅在时钟高电平时有效,因此需要同步时钟以实现逐位仲裁。时钟同步通过线与连接实现,即SCL线状态由所有设备共同决定。

①见上图中,当SCL线从高变低时,所有设备开始计数其低电平周期。若某设备时钟先变低,则它会保持SCL线在低电平直到其时钟再次变高。若此时有其他设备仍处于低电平周期,它们的时钟变化不会改变SCL线状态,直至最长低电平周期的设备完成计数。

②一旦所有设备完成低电平周期,SCL线释放并变为高电平,随后所有设备同步开始计数高电平周期。

③首先完成高电平周期的设备将再次拉低SCL线。因此,SCL时钟的低电平周期由最长低电平周期的设备决定,而高电平周期由最短高电平周期的设备决定。(巧记:保证最长低电平周期)

仲裁判定优先:

主机只能在总线空闲时启动传输,两个或多个主机可能在起始条件的最小持续时间内产生一个起始条件,结果在总线上产生一个规范的起始条件(如下框S条件内)。当SCL线是高电平时,仲裁在SDA线发生,在其他主机保持低电平状态时,首先拉高电平的主机将断开数据输出级,如下图DATA1失去了通信的优先权。

仲裁可以持续多位,第一个阶段是比较地址位。 如果各主机都尝试寻址同一器件,仲裁机制会持续到数据位。在仲裁过程中不会丢失信息(仲裁区间有效信息一致),丢失仲裁的主机可以产生时钟脉冲直到丢失仲裁的该字节末尾 。

补充:如果主机也结合了从机功能,并且在寻址阶段丢失仲裁,由于它存在是赢得仲裁的主机所寻址的器件。因此,丢失仲裁的主机必须立即切换到从机模式。

【AT24C16时序分析】

AT24C16 EEPROM存储芯片的相关信息:存储容量为16Kbit,即2048字节。芯片内部分成128页,每页16字节,读写操作都是以字节为基本单位。AT24C16具有低电压工作、高速串行通信和硬件写保护等特点。(下图为芯片引脚说明)

  • 地址组成:AT24C16的器件地址包括高4位固定地址(1010)和用户需设置的低3位地址(A0、A1、A2);
  • 地址设置:通过连接芯片的A0、A1、A2这3个引脚到VCC或GND来实现地址的低3位设置。例如这3个引脚均连接到VCC,则器件地址为1010_111。由于该 3 位只能组合出 8 种情况,因此,一个主机最多可以连接8个AT24C16存储芯片。

1、EEPROM驱动写时序进行分析

字节写入时序 (Byte Write):通信开始,由主机发送一个起始条件。紧接着,主机发送EEPROM的设备地址,选择目标EEPROM芯片。如果EEPROM支持字地址寻址,主机将接续发送一个或多个字节的字地址来指定要写入数据的内存位置。然后,主机写入数据字节,最高有效位(MSB)首先发送。 EEPROM在接收到每个字节后,返回一个应答信号(ACK),应答信号是低电平脉冲,表示已成功接收字节并准备接收下一个字节或停止信号。待所有数据字节都发送完毕后,主机发送停止信号(STOP)来结束目前的通信。

页写入时序 (Page Write):与字节写入时序相比,页写入时序类似,但数据被分组为多个字节,作为一个连续的数据块发送。 发送起始设备地址后,接续发送一系列的数据字节(DATA (n), DATA (n + 1),......, DATA (n + x))单个字节发送完成,EEPROM都会返回一个应答信号(ACK)。

补充:所有I2C设备均支持单字节数据写入操作,但只有部分I2C设备支持页写操作;且支持页写操作的设备,一次页写操作写入的字节数不能超过设备单页包含的存储单元数。

2、读时序进行分析

IIC协议支持三种EEPROM读时序。首先是指定地址单字节读取方式:操作时序和写时序类似,不同的是,在写入目标地址后,主机的操作方式换为读取。

顺序读取时序:主机发送一个起始地址后,EEPROM开始连续发送数据(DATA n, DATA n+1, DATA n+2, ... DATA n+X)。在每个数据字节的末尾,EEPROM同样等待主机的应答信号(ACK)。主机在接收到每个数据字节后发送ACK信号,直到所有数据都被接收。当所有数据发送完毕或主机决定停止时,它会发送一个停止信号(STOP),结束顺序读取操作。

随机读取时序:起始条件后,发送设备地址和读写位(R/W=1),接着发送随机地址。EEPROM发送数据,主机接收后发送ACK。所有数据发送完毕后,发送停止信号。EEPROM在接收到有效地址后,通过SDA线连续发送数据(DATA n, DATA n+1, ...)。在每个数据字节的末尾,EEPROM等待主机的应答信号(ACK)。主机待接收到数据字节后,通过发送ACK信号来确认接收,并告知EEPROM可以继续发送下一个数据字节。

【时序逻辑设计方案】

1、时钟处理与i2c通信启动

系统时钟频率为50Mhz,频率很高,这里首先需要从系统时钟分频提供一个1Mhz的i2c_clk时钟用于i2c通信处理。下图中cnt_clk为一个时钟分频计数器。

以写操作为例,待写触发信号write发生,拉高写有效信号write_valid,并且为使i2c_clk时钟信号要在上升沿检测到其高电平,write触发要保持≥2个时钟周期,对应100个系统时钟周期。这里设定有效计数器cnt_wr,计数150个系统周期,即300ns,满足要求。计数完成,拉低写有效信号write_valid完成触发操作,具体时序见下图,当然,写有效时序也是如此。

写有效信号write_valid触发,在其拉高的下一个i2c_clk时钟上升沿,触发写使能wr_en信号,并且,启动cnt_start计数器,计数值为5000(1Mhz ->1us)≈ 5ms。这是因为AT64C16单次操作间隔周期需要保持5ms空闲状态。完成计数后,启动i2c_start起始信号触发,后面就是i2c通信的具体流程。当完成操作后,i2c_end结束信号标志通信完成,从而拉低写使能wr_en信号。过程中,设定了写操作的目标地址为16'H00_4D,写入数据为8'H8A。

注意,大部分信号时钟触发源都是i2c_clk,而不是系统时钟。当i2c_start起始信号触发,i2c_clk启动,作为i2c_scl通信时钟的触发源。同时,根据下图逻辑,状态机状态由IDLE转到START1。起始信号仅占一个i2c_clk周期。cnt_i2c_clki2c_scl通信时钟线的计数器,计数值范围0-3,使得i2c_scl周期为250khz。

完成后,进入SEND_ADDR状态,向IIC总线发生器件地址1010011+控制位0,表示写入。比特位计数器cnt_bit用于比特位0-7的计数。在ACK1状态,等待总线回应。ACK是从机,即EEPROM,传回来的低电平信号。对于主机来说SDA线此时是高阻态,总线的上拉电阻将电位钳位在高电位(sda为inout类型)。所以在后面,可以看到这里是处于高阻态的。通信后面的流程根据状态机的跳转,在时序图表现得明显。在STOP停止状态,FPGA向EEPROM发送停止信号,一次单字节数据写操作完成,拉低使能信号i2c_clk_enwe_en,并且及时触发i2c_end,随后状态机跳回IDLE初始状态。

读控制时序处理与写时序处理流程类似,具体见上图,不同在ACK3状态的跳转,接收一个周期回应信号后,进入的是起始状态START2,从而触发第二次起始信号,再次写入器件地址,读取一字节数据。rd_data_reg作为一个暂存器,存储读取到的字节数据,待完成后,转录到rd_data。在N_ACK等待回应一个高电平回应信号,将i2c_scl由地拉高产生一个停止信号,同时触发i2c_end。两个操作过程具体状态判断条件,需具体分析,但都类似。

2、STATE状态机转换逻辑

i2c整个通信的过程可以分为很多阶段,将它们细分开:空闲、起始、器件地址写入、字/字节地址写入、各级响应、字节数据写入、读取字节数据、停止。主机视角对从设备操作过程,具体可见如下。

设备启动,主机处于空闲状态IDLE,待检测到写/读操作信号后,程序转入到第一个起始状态START_1,表现为SDA线产生一个由高到低的电平转换信号。接续进入器件地址写入状态SEND_ADDR,根据硬件图,这里预先设定器件是1010_011。器件确认后,向主机发生一个低电平回应ACK_1。寄存器存储地址类型分为字地址和字节地址,分别对应不同的状态切换。完成后ACK_3响应后,写操作,进入字节数据写入状态WR_DATA,写入一个预先数据,并回应ACK_4进入结束停止状态STOP。而ACK_3响应后,进入读操作流程,需再次发生起始START_2,接后写入读取目标地址,等待从机完成数据发送后,进入结束停止状态STOP。最终保持一定周期,再次转换至空闲状态IDLE

根据时序图逻辑,确定状态机状态转换条件:

当前状态 目标跳转状态 跳转条件 操作类型
IDLE START1 i2c_start拉高 读/写
START1 SEND_ADDR cnt_i2c_clk == 2'd3(一个i2c_clk周期 ) 读/写
SEND_ADDR ACK1 (cnt_i2c_clk == 2'd3)&&(!ack) 读/写
ACK1 SEND_BH (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) 读/写
SEND_BH ACK2 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) 读/写
ACK2 SEND_BL (cnt_i2c_clk == 2'd3)&&(!ack) 读/写
SEND_BL ACK3 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3) 读/写
ACK3 WR_DATA (cnt_i2c_clk == 2'd3)&&(!ack) 读/写
WR_DATA ACK4 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3)
ACK4 STOP (cnt_i2c_clk == 2'd3)&&(!ack)
ACK3 START2 rd_en拉高
START2 SEND_RA cnt_i2c_clk == 2'd3
SEND_RA ACK5 (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3)
ACK5 RD_DATA (cnt_i2c_clk == 2'd3)&&(!ack)
RD_DATA N_ACK (cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3)
N_ACK STOP cnt_i2c_clk == 2'd3
STOP IDLE (cnt_bit == 3'd3)&&(cnt_i2c_clk == 2'd3) 读/写

对上面的表格可以总结,ACK1 - ACK5条件都是相同的,只是跳转目标不同,单字节读/写结束后状态跳转判断条件相同,两个起始状态跳转判断条件也是相同的 ,N_ACKSTOP的跳转注意区别。跳转机Verilog HDL具体程序如下:

//dispose state condition
always @(posedge i2c_clk or negedge sys_rst)begin
    if(!sys_rst) state <= IDLE;
    else case(state)
        IDLE:   if(i2c_start) state <= START1;else state<= state;
        START1:  if(cnt_i2c_clk == 2'd3) state <= SEND_ADDR;
                else state <= state;
        SEND_ADDR:  
                if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                        state <= ACK1;
                else state <= state;
        ACK1:   if((cnt_i2c_clk == 2'd3)&&(!ack))begin
                    if(addr_num)state <= SEND_BH;
                    else state<= SEND_BL;
                end else state <= state;
        SEND_BH:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK2;
                else state<= state;
        ACK2:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= SEND_BL;
                else state <= state;
        SEND_BL:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK3;
                else state <= state;
        ACK3:   if((cnt_i2c_clk == 2'd3)&&(!ack))begin
                    if(wr_en) state <= WR_DATA;
                    else if(rd_en) state <= START2;
                end
                else state <= state;
        WR_DATA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK4;
                else state <= state;
        ACK4:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= STOP;
                else state <= state;
        START2: if(cnt_i2c_clk == 2'd3) state <= SEND_RA;
                else state <= state;
        SEND_RA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= ACK5;
                else state <= state;
        ACK5:   if((cnt_i2c_clk == 2'd3)&&(!ack))
                    state <= RD_DATA;
                else state <= state;
        RD_DATA:if((cnt_bit == 3'd7)&&(cnt_i2c_clk == 2'd3))
                    state <= N_ACK;
                else state <= state;
        N_ACK:  if(cnt_i2c_clk == 2'd3)
                    state <= STOP;
                else state <= state;
        STOP:   if((cnt_bit == 3'd3)&&(cnt_i2c_clk == 2'd3))
                    state <= IDLE;
                else state <= state;
    endcase
end

STATE状态机在整个程序过程中十分重要,i2c_scl时钟电平和i2c_sda输出电平情况,也要根据其状态的不同做出具体分析。

//dispose i2c_scl sequence
always@(*)begin
    case(state)
        IDLE: i2c_scl <= 1'b1;
        START1:
            if(cnt_i2c_clk == 2'd3) i2c_scl <= 1'b0;
            else i2c_scl <= 1'b1;
        SEND_ADDR,ACK1,SEND_BH,ACK2,SEND_BL,
        ACK3,WR_DATA,ACK4,START2,SEND_RA,ACK5,RD_DATA,N_ACK:
            if((cnt_i2c_clk == 2'd1) || (cnt_i2c_clk == 2'd2)) i2c_scl <= 1'b1;
            else i2c_scl <= 1'b0;
        STOP:
            if((cnt_bit == 3'd0) &&(cnt_i2c_clk == 2'd0)) i2c_scl <=  1'b0;
            else i2c_scl <=  1'b1;
        default:    i2c_scl <=  1'b1;
    endcase
end

//dispose i2c_sda_reg & rd_data_reg sequence
always @(*)begin
    case(state)
        IDLE:       begin
                        i2c_sda_reg <= 1'b1;
                        rd_data_reg  <= 8'd0;
                    end
        START1:     if(cnt_i2c_clk == 2'd0) i2c_sda_reg <= 1'b1;
                    else i2c_sda_reg <= 1'b0;
        SEND_ADDR:  if(cnt_bit <= 3'd6) i2c_sda_reg<= DEVICE_ADDR[6-cnt_bit];
                    else i2c_sda_reg<= 1'b0;
        ACK1:       i2c_sda_reg<= 1'b1;
        SEND_BH:    i2c_sda_reg<= byte_addr[15-cnt_bit];
        ACK2:       i2c_sda_reg<= 1'b1;
        SEND_BL:    i2c_sda_reg<= byte_addr[7-cnt_bit];
        ACK3:       i2c_sda_reg<= 1'b1;
        WR_DATA:    i2c_sda_reg<= wr_data[7-cnt_bit];
        ACK4:       i2c_sda_reg<= 1'b1;
        START2:     if(cnt_i2c_clk <= 2'd1)i2c_sda_reg <= 1'b1;
                    else i2c_sda_reg <= 1'b0;
        SEND_RA:    if(cnt_bit <= 3'd6)i2c_sda_reg<= DEVICE_ADDR[6-cnt_bit];
                    else i2c_sda_reg<= 1'b1;
        ACK5:       i2c_sda_reg<= 1'b1;
        RD_DATA:    if(cnt_i2c_clk  == 2'd2) rd_data_reg[7-cnt_bit] <= sda_in;
                    else rd_data_reg <= rd_data_reg;
        N_ACK:      i2c_sda_reg<= 1'b1;
        STOP:       if((cnt_bit==3'd0)&&(cnt_i2c_clk<2'd3))i2c_sda_reg <= 1'b0;
                    else i2c_sda_reg<= 1'b1;
        default:    begin
                        i2c_sda_reg<= 1'b1;
                        rd_data_reg <=  rd_data_reg;
                    end
    endcase
end

其他信号在此不再列举,它们的判断条件相对状态机来说处理起来比较简单,逻辑也很清晰。

【阶段仿真验证】

下面看下程序的仿真结果,查看四个:读/写操作启动时刻、写操作整体时序、读操作整体时序、读/写操作结束时序。

操作启动:write信号发生,write_valid拉高,并在下一i2c_clk上升沿拉高wr_en使能信号,等待cnt_wr完成计数150后,放低write_validcnt_startwr_en拉高后,开始计数。

写操作(这里器件地址设定1010_011):cnt_start计数到4900处,发起i2c_start触发,state值变为4'h01,表示IDLE进入START1状态,依次完成后续的状态切换。STOP状态下,待cnt_bit保持到3‘h3,结束停止状态返回IDLEwr_en拉低,i2c_end拉高一个周期后放下。从下面的仿真图可以看到,整个写操作的时序表现正常。

读操作及读操作结束仿真图如下:

【最终上机验证】

Quartus II生成框图如下,两个按键做为输入触发,输出包括IIC通信总线SDA和SCL,数码管段选SEG_SEL和位选SEG_LED,以及外部挂载的两个LED。程序烧录后,设备低二位数码管显示00,LED均处于熄灭状态,按下key_wr后,led_wr点亮,表示写操作启动;接续,按下key_rd后,led_rd点亮,表示读操作启动,并且数码管显示读取数据8A。最终得到的现象与预期一致。

文献参考:

[1] I2C总线规范 (https://sumcu.suda.edu.cn/_upload/article/files/74/e5/d4eb93de45808d71ad8aad542ede/a3cb5873-aaf4-4af0-9e5f-521793fbba46.pdf);

[2] 基于I2C协议的EEPROM驱动控制(https://doc.embedfire.com/fpga/altera/ep4ce10_mini/zh/latest/fpga/I2C.html);

[3] 王荣华. 可配置的IIC协议控制器IP核的设计[D]. 黑龙江:哈尔滨理工大学,2011.( DOI:10.7666/d.y2012472);


本篇文章中使用的Verilog程序模块,若有需见网页左栏Gitee仓库链接:https://gitee.com/silly-big-head/little-mouse-funnyhouse/tree/FPGA-Verilog/

热门相关:傲娇小萌妃:殿下太腹黑   娇女种田,掌家娘子俏夫郎   这个赘婿有点强   火力法则   我能改变东西颜色