FPGA实现串口与iic控制器总结(3)

      在剖析了《深入浅出玩转FPGA》的串口代码和IIC控制器代码、xilinx官方的xilinx的iic控制器(参见书《FPGACPLD设计工具──Xilinx ISE使用详解》)、《片上系统设计思想与源代码分析》一书中带有wishbone接口的iic控制器后,本文尝试对以上做一些总结,并分析不同的iic控制器的实现区别。

      上一讲,我们分析了特权的iic控制器的实现,这一讲将继续另2种带有总线接口和更充分实现iic协议的例子。下一讲讲谈谈SOC架构中的wishbone总线架构及外设挂载,以及一些软硬件结合的底层的东西,主要参考《自己动手写cpu》一书及相关的网页资料等,敬请期待。

      2、IIC控制器

       2.2 xilinx的iic控制器

       该控制器主要是参考 《FPGACPLD设计工具──Xilinx ISE使用详解》第10章和官方代码,资料链接见文末给出的下载链接地址。

       iic协议的知识同样不再赘述,只是提几点上一讲中没有或无需关注的地方:

       1、I2C总线通信时,起始位后的第一个字节用于寻址,该字节包含7比特的从机地址和1比特的读写指示比特, 一般而言,从机地址由一个固定部分和一个可编程部分构成。

       2、复合格式中,此时主机连续对从机进行多次读写操作,因此在产生起始位、收发数据、产生停止位的整个传输过程中,数据的方向发生多次改变,传输改变方向时,主机会重新发出重复起始位和从机地址(上一讲中为什么读的流程要复杂些的原因)。

       3、I2C总线在一个时刻只能有一个主机,当I2C总线同时有两个或更多的器件想成为主机时,就需要进行仲裁,时钟同步过程的目的是为仲裁提供一个确定的时钟。SCL 线低电平时间取决于低电平时间最长的主机,高电平时间取决于高电平时间最短的主机。主机只会在 I2C 总线空闲时产生起始位,但是在起始位的保持时间 tHD;STA内可能有两个或以上的主机产生起始位,最终总线上的起始位由它们之间的线与运算决定,仲裁在随后 SDA 线上发送的比特中进行。如果一个主机具有从机功能,那么当它失去仲裁时,必须立即切换到从机状态,因为它可能正在被其他主机寻址。

      4、如图

FPGA实现串口与iic控制器总结(3)_第1张图片

      讲完了这些关于iic协议的点继续下面的部分.

       I2C总线控制器的主要作用是提供UC(Microcontroller,微控制器或单片机)和I2C总线之间的接口,为两者之间的通信提供物理层协议的转换。这些I2C协议的器件,就不能直接和单片机外围总线相连,这些器件可以挂在一套I2C总线上,再通过I2C总线控制器和µC连起来,如图所示。在SOC设计中类似的协议转换模块用得非常多。

FPGA实现串口与iic控制器总结(3)_第2张图片

       I2C 总线控制器包含两个主要部分,一是微控制器接口,简称µC接口,二是 I2C Master/Slave 接口,即I2C接口,通过这两个接口,I2C总线控制器实现了微控制器外围总线和 I2C 总线的连接。那么这里的微控制器接口是哪种控制器?是否是哪种总线协议,根据readme文件可以发现原来是 MC68307单片机,是一款集成的多总线处理器。

讲清楚了原理,我们来看看具体设计:

This zip file contains the following folders:
-- Verilog Source Files:
i2c_blk_ver.v           - top level file
i2c_control_blk_ver.v  - control function for the I2C master/slave
shift8_blk_ver.v          - shift register
uc_interface_blk_ver.v   - uC interface function for an 8-bit 68000-like uC
upcnt4_blk_ver.v  - 4-bit up counter

   可以看到5个文件。uc_interface_blk_ver.v 实现了一些寄存器以及对下层模块的控制信号,shift8_blk_ver.v是串行收发,upcnt4_blk_ver.v是4位计数器,i2c_blk_ver.v 是顶层,例化了uc_interface_blk_ver和i2c_control_blk_ver。i2c_control_blk_ver则实现了仲裁,start等收发流程状态机等。整体架构如下图:

FPGA实现串口与iic控制器总结(3)_第3张图片

整个思路还是很清楚的。接口处主要是一些寄存器,寄存器的信号到iic中控制底层的运转。

µC 接口主要包含状态寄存器MBSR、控制寄存器MBCR、地址寄存器MADR、数据寄存器MBDR 和地址译码/总线接口模块。状态寄存器指示I2C总线控制器的当前状态,如传送是否完成、总线是否忙等信息,控制寄存器是µC控制I2C总线控制器的主要途径,通过置0/置1可以完成I2C总线控制器使能、中断使能、Master/Slave模式选择、产生起始位等操作。地址寄存器保存着I2C总线控制器作为Slave时的地址。数据寄存器用于保存接收或是待发送的数据。

下面分析源代码,由于源代码众多,只能摘出部分,梳理主要思路和重要信号线,大家可以下载我注释的代码。

i2c_blk_ver.v 顶层文件:

module i2c_blk (sda, scl, addr_bus, data_bus, as, ds, r_w, dtack, irq, mcf, clk,
                reset);
 
  parameter I2C_ADDRESS = 16'b0000000000000000;
  //   I2C bus signals
  inout sda;
  inout scl;
  //   uC interface signals
  input [23:0] addr_bus;
  inout [7:0] data_bus;
  input as;                //   address strobe, active low
  input ds;                //   data strobe, active low
  input r_w;               //   read/write
  output dtack;            //   data transfer acknowledge 给处理器的,表示数据是否准备好了
  output irq;              //   interrupt request
  inout mcf;               //   temporary output for testing  给处理器,表示传输是否结束
  //   clock and reset
  input clk;
  input reset;
这个是顶层的输入输出,不是什么特殊的总线结构,比较好理解,pdf中有详细的中文解释每个信号的含义。

接下来是一些wire型,实际上就是那些寄存器中有含义的每一位,即各种控制信号线,便于例化的 i2c_control  I2C_CTRL与  uC_interface #(I2C_ADDRESS)  uC_CTRL模块间的接口互联。

接下来从顶层往下,uC_interface模块:

 //   Internal I2C Bus Registers
  //   Address Register (Contains slave address)
  inout [7:0] madr;
  //   Control Register		
  inout men;               //   I2C Enable bit
  inout mien;              //   interrupt enable
  inout msta;              //   Master/Slave bit
  inout mtx;               //   Master read/write
  inout txak;              //   acknowledge bit
  inout rsta;              //   repeated start
  output mbcr_wr;          //   indicates that the control reg has been written
  //   Status Register
  input mcf;               //   end of data transfer
  input maas;              //   addressed as slave
  input mbb;               //   bus busy
  input mal;               //   arbitration lost
  input srw;               //   slave read/write
  input mif;               //   interrupt pending
  input rxak;              //   received acknowledge
  output mal_bit_reset;    //   indicates that the MAL bit should be reset
  output mif_bit_reset;    //   indicates that the MIF bit should be reset
  input msta_rst;          //   resets the MSTA bit if arbitration is lost
  //   Data Register
  inout [7:0] mbdr_micro;
  input [7:0] mbdr_i2c;
  output mbdr_read;
可以看到实际上这个模块的很多输出信号就是这些寄存器的位,控制更底层的如何实现。可以参考pdf查询具体的寄存器(8位)的某一位是什么含义。比如状态寄存器都是input型,因为他主要反馈给uc当前状态,不对底层的状态机控制。数据寄存器中mbdr_micro是inout型,表示是uc总线那一端的,mbdr_i2c是input型,表示是从iic接收到的那一端的。

  //   State Machine Signals
  `define STATE_TYPE_IDLE	 2'd0
  `define STATE_TYPE_ADDR	 2'd1
  `define STATE_TYPE_DATA_TRS	 2'd2
  `define STATE_TYPE_ASSERT_DTACK	 2'd3
  //   Constant Declarations
  parameter RESET_ACTIVE = 1'b0;
  //   Base Address for I2C Module (addr_bus[23:8])
  parameter MBASE = UC_ADDRESS;
  //   Register Addresses (5 Total):
  //   Address Register (MBASE + 8Dh)
  `define MADR_ADDR	 8'b10001101
  //   Control Register (MBASE + 91h)
  `define MBCR_ADDR	 8'b10010001
  //   Status Register (MBASE + 93h)
  `define MBSR_ADDR	 8'b10010011
  //   Data I/O Register (MBASE + 95h)
  `define MBDR_ADDR	 8'b10010101
uc部分的状态机, µC和 I2C总线控制器之间的交互要用到 I2C总线控制器内部的寄存器,寄存器的地址是24位的,其中高16比特为I2C总线控制器的基址,低8位用于区别不同寄存器。接下来定义了10多个wire型的中间变量,这些事状态机中产生的操纵控制状态机的。由于wire型变量不能再always中赋值,所以后面又用这种方式定义一个相应的reg型变量。

//   Address match
  wire address_match;
 reg visual_0_address_match;
  assign address_match = visual_0_address_match;

加上了visual_0的前缀,这样做的好处?我想一是时钟同步,reg信号由clock打一拍同步,也便于去除毛刺,因为assign信号直接相连,稍有抖动就有毛刺,reg信号打拍子可以避免这种问题。二是可以某些信号做类似案件检测一样的消抖,举例:

51    input as; 
221   begin
      visual_0_as_int 						//这里又有一个时钟的延迟一拍
      visual_0_as_int_d1					//经过一个时钟的延迟一拍,为了检测到正确的下降沿而没有抖动的干扰
      visual_0_ds_int <= ds;
      if ((!as && as_int_d1 && addr_bus[23:8] == MBASE))		//低8位是区别那个寄存器
        visual_0_address_match <= 1'b1;
      else
        visual_0_address_match <= 1'b0;
      end
135   reg visual_0_as_int;
      assign as_int = visual_0_as_int;
138   reg visual_0_as_int_d1;
      assign as_int_d1 = visual_0_as_int_d1;

可以看见从221行开始,获取as输入,经过几个中间变量的周转,在if中通过!as && as_int_d1来达到类似案件去抖的效果。另外有些变量是对外输出的,也需变成reg型。
这部分的实际工作流程如图:
FPGA实现串口与iic控制器总结(3)_第4张图片
根据上图的流程:第一个always块,  // Process:SYNCH_INPUTS 判断as是否有有效的下降沿,然后总线的高16位是否是正确的我们这个iic设备在真个cpu外设总线上的地址。为真,则visual_0_address_match信号为1,起始对应为address_match,这个成为后续的控制判断,状态机的重要依据。接下来是uc模块的主状态机:
FPGA实现串口与iic控制器总结(3)_第5张图片

  always @(prs_state or as or as_int_d1 or ds_int or address_match)
  begin
    visual_0_next_state <= prs_state;			//2位的变量,4个状态
    visual_0_dtack_com <= 1'b1;					
    visual_0_dtack_oe <= 1'b0;
    case (prs_state)
      `STATE_TYPE_IDLE :						//引用定义的变量
        //  ----------- IDLE State (00) -------------
        //   Wait for falling edge of as
        if (as_int_d1 && !as)					//as表示输入地址有效信号,低有效,这里的as_int_d1实际上是一个类似按键去抖的效果,延迟了2拍,追踪信号就可以发现
          //   falling edge of AS
          visual_0_next_state <= `STATE_TYPE_ADDR;
 
      `STATE_TYPE_ADDR :
        //  ---------- ADDR State (01) --------------
        //   Check that this module is being address
        if (address_match)						//225行由输入的bus决定,也是打了一拍,做到时钟同步
          //   Wait for ds to be asserted, active low
          if (!ds_int)							//由输入的ds决定
            visual_0_next_state <= `STATE_TYPE_DATA_TRS;
          else
            visual_0_next_state <= `STATE_TYPE_ADDR;
        else
          //   this module is not being addressed
          visual_0_next_state <= `STATE_TYPE_IDLE; 
      `STATE_TYPE_DATA_TRS :
      begin
        //  -------- DATA_TRS State (10) ------------
        //   Read or write from enabled register
        visual_0_next_state <= `STATE_TYPE_ASSERT_DTACK;	//过渡态,为了给出oe的信号,内部控制线
        visual_0_dtack_oe <= 1'b1;							//前面被置为0了的
      end
      `STATE_TYPE_ASSERT_DTACK :
      begin
        //  ------ ASSERT_DTACK State (11) ----------
        //   Assert dtack to uProcessor
        visual_0_dtack_com <= 1'b0;							//前面被置为1了的
        visual_0_dtack_oe <= 1'b1;
        //   Wait for rising edge of as and ds
        if ((!as_int_d1) && (!ds_int))						//锁存操作,直至DTACK有效,甚至可以分析锁存了几个时钟?
          visual_0_next_state <= `STATE_TYPE_ASSERT_DTACK;
        else if ((as_int_d1) && (ds_int))
          visual_0_next_state <= `STATE_TYPE_IDLE;
      end
    endcase
  end
注意在848行处://   set SDA and SCL
  assign sda = (sda_oe == 1'b1 ? 1'b0 : 1'bz);
  assign scl = (scl_out_reg == 1'b0 ? 1'b0 : 1'bz);
  assign scl_not =  (~ (scl)) ;
  //   sda_oe is set when master and arbitration is not lost and data to be output = 0 or
  //   when slave and data to be output is 0
  assign sda_oe = (((master_slave == 1'b1 && arb_lost == 1'b0 && sda_out_reg ==
                  1'b0) || (master_slave == 1'b0 && slave_sda == 1'b0) ||
                  stop_scl_reg == 1'b1) ? 1'b1 : 1'b0);
所以sda实际上经由sda_out_reg,visual_0_sda_out_reg,sda_out,visual_0_sda_out控制,即下图状态机中的visual_0_sda_out实际代表了sda,同样scl也一样。

该状态机还是比较清晰的。

next_state信号通过几个中间变量的周转,还是赋给了prs_state实现状态跳转,292的always块即实现了这种从next_state到prs_state的驱动。dtack_com是一个状态机变化的中间信号,转化成dtack_int变成dtack信号,dtack_oe决定是否输出dtack信号。注意这3条语句每次进来就更新,要注意visual_0_dtack_com与visual_0_dtack_oe这里每次被重置。首先是idle状态,检测到有效的as信号。进入addr状态,监测地址是否匹配,数据是否来了,来了就进入trs,否则继续等。Trs状态是过渡态,为了给出visual_0_dtack_oe信号,进入dtack状态,visual_0_dtack_com与visual_0_dtack_oe被设置表明数据已经放到uc与iic的总线上。等待ds失效,回到idle状态。那么更多的过程实际上是根据状态机的中间变量,在别的always块中实现的,可以分析下两者间的相对时间关系。

接下来的always实现visual_0_prs_state <= next_state;驱动状态更迭。接下来是判断那个24位的地址是映射到那个寄存器,每个寄存器给出一个信号线,若选中则为1,驱动下一个always块中的数据具体传到哪个寄存器中。下个always块中,实现了对不同状态下的一些信号量的设置,而没有在之前的状态机中实现。每根信号线的含义可以查pdf。mbcr_wr代表了mbcr是被读了还是被写了。其实Status Register是只读的,写的话会产生复位。其他几个寄存器类似,比较好理解,理解关于数据寄存器的数据流向。这里面的8位数据都是一个clk完成,注意与上面的dtack的含义在时序上是否冲突,因为dtack是表征数据已经准备好

最后就是几个assign语句,根据上面的信号处理是否有中断,dtack,决定是传输进来某个信号还是给出某个信号。

总的来说结构比较清晰,模块化比较清晰。状态机采用了分段式的写法。

i2c_control_blk_ver.v文件

它的接口主要是uc接口文件中的寄存器的信号线。分别定义了数据流程的状态机和scl信号的状态机。接下来是一堆的中间信号的定义和类似的加上visual_0后的reg处理。

接下来首先例化了upcnt4模块用来统计bit数。这是一个不封顶的4位计数器,但是可以输入一个4位数据来修改计数值。接下来再例化一个用于clk的计数,达到分频的效果。接下来是例化了2个SHIFT8_blk,改模块可以load和输出1个8位数,也可串行接收和发送一个一位数。例化了2个第一个是iic端的,一个是uc端的。接下来是总线仲裁,主要是msta_rst与arb_lost位。当为主机,scl_in为scl的采样。有待重点分析?

接下来是scl信号的一个状态机:

FPGA实现串口与iic控制器总结(3)_第6张图片

I2C 总线控制器复位后处于IDLE 状态,不驱动SCL 和SDA,此时I2C 总线上的其他Master 可以控制SCL 和SDA。如果I2C 总线控制器处于Master 模式,而且I2C 总线处于空闲状态,μC 通过置位MBCR 寄存器的MSTA 比特使GEN_START 信号为高,那么状态机进入START 状态。

在 START 状态,SCL 保持为高电平,同时驱动SDA 信号变低,从而在I2C 总线上产生一个起始位。系统时钟计数器启动计数,直到满足I2C 规范要求的起始条件保持时间(>4ns),状态机进入SCL_LOW_EDGE状态。

在 SCL_LOW_EDGE 状态,状态机使SCL 产生一个下降沿并复位系统时钟计数器,然后在下一个时钟沿到来时进入SCL_LOW 状态。

在SCL_LOW 状态,SCL 保持为低,同时进行计数,直到产生规定的SCL 低电平时间(>4.7ns)。产生规定的 SCL 低电平时间后,如果失去仲裁,那么完成一个字节的传输之后状态机回到IDLE 状态,否则状态机进入SCL_HI_EDGE 状态。

在 SCL_HI_EDGE 状态中,状态机释放SCL 线,希望产生SCL 上升沿,但是SCL 线可能被其他Master 置低,因此状态机并不直接转移到SCL_HI 状态,而是等待SCL 信号变高之后才进入SCL_HI 状态。

进入 SCL_HI 状态后,系统时钟计数器进行计数,以产生I2C规范要求的SCL高电平时间(>4.0ns),如果检测到重复起始条件或停止条件,状态机将在1/2SCL 高电平时间之 后转移到 START 状态重新开始,或转移到 IDLE 状态,否则产生要求的 SCL 高电平时间后状态机进入 SCL_LOW_EDGE 状态,继续产生下一个 SCL 脉冲。

接下来是状态机的驱动visual_0_scl_state <=next_scl_state;接下来是visual_0_sda_in这类信号,这个处理,visual_0_scl_in并不是由scl扇出,仅仅是对这个信号的一个采样。也是上面我们判断scl是否被别人拉高的依据。

后面是start和stop信号产生和检测的一个信号线的处理,也包括主从机的。在下来就是主状态机了:

FPGA实现串口与iic控制器总结(3)_第7张图片

复位后,状态机在 IDLE 状态,当检测到 START 信号时,转移到HEADER 状态。START信号由 I2C 总线上的起始位触发,触发这个起始位的 Master 可以是 I2C 总线控制器本身或其他的 I2C 总线主机。

在 HEADER 状态,如果 I2C 总线控制器处于 Master 模式,它会把 MBDR 中的数据作为HEADER 发送到 I2C 总线上,以寻址特定的 Slave。不管 I2C 总线控制器处于 Master 还是Slave模式,在HEADER 状态时,I2C 总线控制器都会接收总线上的数据,保存到 I2C Header Shift Register 中,收到8 个比特后,状态机转移至 ACK_HEADER 状态。

在 ACK_HEADER 状态,如果 I2C 总线控制器处于 Master 模式,它会采样 SDA 线,以判 断 所 寻 址 的Slave 是 否 响 应 。 如 果 没 有 响 应 , 状 态 机 转 移 到STOP 状 态 , 通 知SCL/START/STOPGenerator 产生STOP 信号,中止传输。如果 Slave 产生了响应比特,状态机根据 Header 的最低位判断发起的是发送操作还是接收操作,然后转移到 RCV_DATA 状态或 XMIT_DATA 状态。如果I2C 控制器处于Slave 模式,电路会不断比较 Header Shift Register 的内容和 I2C 总线控制器地址寄存器 MADR 的内容是否相等,如果相等,说明本I2C总线控制器被其他 Master 寻址,于是 I2C 总线控制器立即转换到 Slave 模式,并把状态寄存器 MBSR 的 MAAS 比特置 1,指示 I2C 总线控制器被其他 Master 寻址。同时MBSR 的SRW 比特记录Header 的最低位,以便µC 判断Master 请求的是读还是写操作。

在 RCV_DATA 状态, I2C 总线控制器处于接收状态(即主机接收状态或从机接收状态),状态机读入 I2C 总线上的数据并保存到移位寄存器中,读完 8 比特的数据后进入ACK_DATA 状态,发出响应比特。响应比特的取值根据 I2C 总线控制器是 Master 还是 Slave有所不同,当I2C 总线控制器是Slave 时,响应比特应为 0,表示正常接收;当 I2C 总线控制器是Master 时,如果已经收到了足够的数据,响应比特要设置为 1,通知 Slave 停止发送,否则响应比特应为 0,通知Slave 继续发送。响应比特的值由 MBCR 的 TXAK 位决定, µC 可以在适当的时候写入。检测到 I2C 总线上的停止位时,状态机转移到 STOP 状态。

在 XMIT_DATA 状态, I2C 总线控制器处于发送状态(即主机发送,或从机发送状态),状态机把数据寄存器 MBDR 的数据移位输出到 SDA 线上,发送 8 比特后进入GET_ACK_DATA 状态,收到响应比特后,状态机回到 XMIT_DATA 状态,继续发送下一个字节。如果没有收到响应比特,说明发送结束或出错,状态机转到 STOP 状态。

在 STOP 状态,如果处于 Master 模式,主状态机通知 SCL/START/STOP Generator 产生停止位。下一个时钟沿到来时,状态机自动转移到 IDLE 状态。

后面就是start stop检测模块,还有一些寄存器信号线的产生。

可以看到这种设计思路是分层次模块的。uc那一端是根据数据交互的流程,读写控制寄存器,然后寄存器的信号输出给底层去实现各种控制。而底层也是分区块,2个状态机,一个实现scl信号的置高和置低,期间有仲裁检测,主从机的切换,结束起始位的判断等等,另一个实现iic与外部设备通信时的交互流程,如先寻址,在等待确认,再续写之类的。中间有很多信号线去同步控制别的always块或者被别的信号线控制。还有起始,结束检测的always块,针对各个寄存器的赋值的专门的always块等等。

2.3  OR1200的iic控制器   

这是一个带wishbone总线结构的iic接口,所以按照顶层框图来画输入输出信号,可以发现输入一段全是wb接口的信号线,输出就是2根scl和sda,但是注意的是这里并不是scl与sda:

	// I2C signals
	// i2c clock line
	input  scl_pad_i;       // SCL-line input			//?不是inout结构?
	output scl_pad_o;       // SCL-line output (always 1'b0)
	output scl_padoen_o;    // SCL-line output enable (active low)

	// i2c data line
	input  sda_pad_i;       // SDA-line input
	output sda_pad_o;       // SDA-line output (always 1'b0)
	output sda_padoen_o;    // SDA-line output enable (active low)
实际上我们需要在这个三态门添加更高的设计层次:

	assign scl = scl_padoen_o ? 1'bz : scl_pad_o;
	assign sda = sda_padoen_o ? 1'bz : sda_pad_o;
	assign scl_pad_i = scl;
	assign sda_pad_i = sda;

定义了不同的寄存器,包括时钟分频的寄存器,支持异步复位。wb_adr_i信号决定具体对哪一个寄存器操作。然后是寄存器的值的修改。这个top模块中没有定义状态机,是由于wb接口的原因,不想2.2的实现方式是有这么一个流程的。而这里因为是有统一的wb接口的原因,所以简单的多。另外wb接口的很多信号线可以根据自己的需求去修改其功能含义的。

但是这个仅仅是主机,并没有实现完整的iic接口,没有仲裁等。

top中例化了i2c_master_byte_ctrl,i2c_master_byte_ctrl中例化了更低层次的i2c_master_bit_ctrl。i2c_master_byte_ctrl中也是一个主状态机维持着start、发、收、ack等交互流程,i2c_master_bit_ctrl将start、stop、rd、wr都分成了好几段,维持着一个庞大的状态机 ,里面是对sda,scl信号线的高低的控制。也是模块化的比较清晰,就不仔细讲了


说几点感受吧:

1、对于这种比较复杂的设计,实际上模块的划分非常重要,理清楚各个模块间的互联,每根信号线的含义。比如这里uc那一端的分段式状态机,与寄存器的设计等。

2、该例程比较复杂,非常繁杂,有很多的信号线。各信号线之间的连接比较繁杂,特别是还有仲裁这一块,不好理解。所有对于这种复杂的设计,没有详细的注释很难理解。另外针对iic的协议,大家的理解和设计思路不一样,可以有很多种实现方式,包括一些个人的写法习惯,如对于状态,对中间信号的定义与处理等,都有差异

3、很多的小技巧,对于信号的命名,打节拍的处理等等



你可能感兴趣的:(FPGA,fpga)