在剖析了《深入浅出玩转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、如图
讲完了这些关于iic协议的点继续下面的部分.
I2C总线控制器的主要作用是提供UC(Microcontroller,微控制器或单片机)和I2C总线之间的接口,为两者之间的通信提供物理层协议的转换。这些I2C协议的器件,就不能直接和单片机外围总线相连,这些器件可以挂在一套I2C总线上,再通过I2C总线控制器和µC连起来,如图所示。在SOC设计中类似的协议转换模块用得非常多。
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等收发流程状态机等。整体架构如下图:
整个思路还是很清楚的。接口处主要是一些寄存器,寄存器的信号到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;
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信号的一个状态机: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信号产生和检测的一个信号线的处理,也包括主从机的。在下来就是主状态机了:
复位后,状态机在 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、很多的小技巧,对于信号的命名,打节拍的处理等等