前言

本文摘自《FPGA之道》,接口是FPGA最重要的内容之一,一起来看看作者对于外界接口的总结。

关于外界接口的编程思路

不和外界打交道的FPGA设计是一个没有任何意义的设计,因为除了发热外别无它用。那么要想和外界进行通信,就必须具有外界接口,而FPGA设计可以利用FPGA芯片的管脚(PINs/PADs)来创建和外界交互的接口。但是对于FPGA设计来说,仅仅具有外界接口不还行,如果不能保证信息传递的正确性、准时性等等要求,那么外界接口也就成了聋子的耳朵——摆设。因此,本章节就来集中讨论一下关于外界接口的编程思路。当然了,外界接口的成功离不开正确的管脚约束和时序约束,不过这些内部不属于本章节介绍的范畴,在后续的【时序分析篇】中,将会进一步讨论。

按传递方向分类

按照信号传递的方向来分,外界接口可分为输入接口、输出接口和双向接口三种,分别介绍如下。

输入接口

输入接口的作用将外部信息导入至FPGA内部,当然了,这种信息的导入必须是正确的、适时的,否则无论FPGA内部的逻辑功能设计得多么巧妙、完美,若无法准确接收信息,将会导致整个FPGA设计的行为异常。
以下即为在HDL代码中定义输入接口的一些简单示例:

-- VHDL example
clk in : std_logic; 
rst in : std_logic;
dataIn in: std_logic_vector(7 downto 0);

// Verilog example
input clk; 
input rst;
input [7:0] dataIn;

输出接口

输出接口的作用将FPGA内部的信息导出至外界,当然了,这种信息的导出也必须是正确的、适时的,否则无论FPGA内部的逻辑功能设计得多么巧妙、完美,若表达不准确,那么将会导致整个外围接收设备行为异常。
以下即为在HDL代码中定义输出接口的一些简单示例:

-- VHDL example
dClk out : std_logic;
dataOut out : std_logic_vector(7 downto 0);
flag buffer : std_logic;

// Verilog example
output dClk; 
output [7:0] dataOut;
output flag;

双向接口

原理简介

双向接口的作用是使用一个接口便既可以将外部信息导入至FPGA内部,也可以将FPGA内部的信息导出至外界。由此可见,双向接口兼具输入接口和输出接口的功能,事实上,双向接口的确是同时具有输入接口和输出接口两种实现电路,只不过让它们复用了同一个IO管脚、同一个数据线罢了,所以基于双向接口的信息交互为半双工通信(所谓半双工,即通信的双方可以互相发送信息,但同一时刻仅能允许一方发送;而所谓全双工,即指通信的双方可以同时进行信息的发送工作;而对于之前提到的输入接口或输出接口来说,单独来看,它们都属于单工通信模式,即信息传递方向固定。通常来说,全双工通信接口都是用一个输入接口加一个输出接口的组合来实现)。当然了,为了能够让双向接口中的输出电路部分和输入电路部分都能正常工作,通常需要引入三态门,这也是为什么大部分FPGA芯片仅在接口资源中加入了三态门的电路,如下即为双向接口的连接原理图:
FPGA之道(58)关于外界接口的编程思路_接收端
如图所示,芯片A和芯片B利用一个双向接口进行通信,对于通信的任何一方,连接到双向接口上的信号都有四个——输入信号、输出信号、控制信号、总线信号。对于数字电路来说,一个输出可以对应多个输入,因为输入端口对外的电阻特性是高阻,所以它们之间的并联只要不是太多,基本不会对输出电路的电压值产生影响,因此上图中的输入信号是直接连接到双向总线上的。但是多个输出端口却不能并联,因为输出端口对外呈现低阻特性,如果并联的输出端口中,有些输出高电平、有些输出低电平,那么总线将会出现竞争现象,而无法判断其上的信号到底是什么状态,所以输出信号不能直接连接到双向总线上,这也是为什么双向接口都是半双工的原因。因此,为了保证双向接口的正常工作,通信的双方必须通过恰当的三态门控制逻辑来保证通信时不会出现两个输出接口同时和双向总线连通的情况出现。此外,为了保证在通信双方均关闭三态门时,总线能够进入一个确定的状态,通常需要为双向总线配置一个上拉电阻,当然,有些时候,FPGA的管脚内部已经嵌入了上拉电阻,这时就不需要在芯片外部为总线再做上拉操作。(注意,对于使用三极管实现的集电极开路形式的单向总线复用情况,也即OC门,必须加入上拉电阻才能保证线与逻辑输出高电平。)
那么,当芯片A向芯片B发送信息时,芯片A内部的控制信号使得其三态门导通,而芯片B处的控制信号使得其三态门关闭(即输出高阻),那么此时等效的电路图如下:
FPGA之道(58)关于外界接口的编程思路_数据_02
反之,当芯片B向芯片A发送信息时,等效电路图则如下:
FPGA之道(58)关于外界接口的编程思路_数据_03
如果同时使能通信双方的三态门,则双向总线因具有多个驱动源而呈现出不稳定状态,此时任何一方的输入电路得无法得到确定的输入信号,因此,在使用中应该避免这一情况的发生。
接下来,就介绍一下常见的双向端口的工作模式:

工作模式

根据使用总线的各个设备之间的等级关系,可以将双向接口的工作模式分为主从模式和对等模式两种,分别简介如下,供大家设计时参考。

主从模式

主从模式是指通信的多个设备之间有主从关系,即,有一个设备为主设备,主导每次通信工作;而其他设备均为从设备,根据需求响应主设备发起的每次通信。
鉴于主从模式下的设备关系,系统初始化或者空闲时,所有的从设备均必须关闭三态门,输出高阻,将双向总线的使用权留给主设备,并同时利用输入功能电路监听双向总线上的消息。而主设备则可以在系统初始化或空闲时工作在输出状态或高阻状态。
当需要通信时,主设备打开三态门,首先输出想要建立连接的从设备地址(对于只有一主一从的端对端通信可以省略这一步,而对于多点通信则必须经历这一步)。然后再输出通信命令字,例如写入、读出或其它控制命令。如果该命令仅为控制命令,无需配合数据发送也不需等待反馈,则通信到此结束;如果该命令需要配合数据一起发给从设备,例如写入命令,则主设备接下来会发送具体的数据(通常还需要配合数据长度字或特殊的结束符),数据发送完后结束通信;如果该命令需要等待从设备的反馈,则主设备发送完命令后便关闭三态门,将总线的控制权交出,自己则进入监听状态,等待从设备的应答。
整个通信过程中,从设备一开始均是处于监听状态,当监听到的设备地址和自己的地址匹配时(端对端通信可省略这一步),则会根据接下来监听到的命令字做出相应的动作。如果主设备需要自己的应答,则应答完后迅速关闭自己的三态门,将总线使用权立即交还给主设备。
以上便是主从模式的原理简介,在实际的使用中往往需要根据实际需求向其中注入更多的设计元素,例如,命令字格式、数据字格式、帧格式、通信协议等等。
对于主从模式来说,如果两个从设备之间想交换数据,则必须以主设备为媒介,即主设备先发起一次通信,从一个从设备中读取相应的数据,然后再发起一次通信,将之前读取的数据写入到另一个从设备中。由此可见,如果从设备之间经常会有信息交换需求,则按照主从模式来工作其效率是比较低的,而接下来介绍的对等模式则可以解决这个问题。

对等模式

对等模式是指通信的多个设备之间,平时是没有主从关系的,即,大家是平等的,每个设备均可发起或响应一次通信。这样一来,任意两个设备之间便可建立连接,从而直接完成通信。一旦通信连接建立后,发起通信的设备便成为临时的主设备,而响应通信的设备便成为临时的从设备,其他设备此刻便成为冻结设备。当然了,可以通过广播地址等技术,让系统中出现多个临时从设备。
由于对等模式下大家是平等的,所以同一时刻便可能会有多个设备同时想要发起通信,这便导致总线出现冲突现象。因此,实现对等模式的关键,便是需要引入能够成功避免总线出现冲突的技术。
可以想象,对等模式下,所有设备在系统初始化或者空闲时,均应该关闭三态门,释放总线,并同时进入监听状态。那么,当某个设备想要成为临时主设备,并发起一次通信时,它首先要做的不是打开三态门,而是监听总线。由于总线通常被上拉,因此,如果在一定时间段内监听的结果始终为逻辑1,则说明此时没人在使用总线。完成这一步之后,才能打开三态门,输出一个约定好的测试字段(每个设备均不同,通常可以选择发送自己的设备地址)。这么做的原因在于,可能有多个设备同时发现总线空闲并想要发起通信,因此仍可能出现总线冲突。为了避免这种情况,在发送测试字段时,该设备还必须同时监听总线,看能否正确从总线上读回刚刚发送的测试字段。如果回读成功,则说明此时总线上只有自己在发送数据,根据总线监听机制,可以证明总线的控制权此刻已被自己掌握,可以放心向任一设备发起通信;如果回读失败,则说明此时总线上至少还有另一个设备打算发送数据,此时必须停止通信,并等待上一段时间后重新开始上述环节。为了减小再次通信时仍发生冲突,每个设备的等待时间一般都是随机的,或者设计好不同的等待时间以让各个设备具有一定的优先级关系。

简单示例

本小节,给出一个简单的双向端口的定义和使用示例,实际使用中,只要参考【工作模式】小节中的内容,让通信的多个模块控制好自己的三态门通断,即可实现可靠的双向通信。

-- VHDL example
entity ExampleOfInOut is
port (
	-- inner interface with FPGA fabric
	ctrl : in  std_logic;
	dataOut : in  std_logic;
	dataIn : out std_logic;

	-- outer interface with other chip
	bus : inout  std_logic
);
end ExampleOfInOut;

architecture Behavioral of ExampleOfInOut is	
begin

	dataIn <= bus;
	bus <= dataOut when ctrl = '1' else
		'Z';

end Behavioral;

// Verilog example
module ExampleOfInOut(
	// inner interface with FPGA fabric
	input ctrl,
	input dataOut,
	output dataIn,

	// outer interface with other chip
	inout bus
);

	assign dataIn = bus;
	assign bus = (ctrl == 1'b1) ?  dataOut : 1'bz;

endmodule

按电气特性分类

按照电信号的载体来分,外界接口又主要分为单端接口和差分接口两大部分,分别介绍入下。

单端接口

单端接口,即传输电信号的物理载体为1根电线,其上传递的电压值通过与一个阀值或两个阀值进行比较,就可以得出其对应的数字电信号——逻辑1或逻辑0。按照电压值、阀值的选取不同,单端接口又分为TTL、LVCMOS等等,详见【共同语言篇->模拟与数字->模拟信号处理电路系统与数字信号处理电路系统->数字系统之间的接口】章节里的介绍。
例如,在【按传递方向分类】章节中的那些例子里,接口全为单端接口。

差分接口

差分接口,即传输电信号的物理载体为2根电线,通过这两根线之间的电压差值来判断其上传递的数字电信号是逻辑1还是逻辑0。按照电压值、阀值的不同,差分接口又分为LVDS、RS485等等,详见【共同语言篇->模拟与数字->模拟信号处理电路系统与数字信号处理电路系统->数字系统之间的接口】章节里的介绍。
下面就以Xilinx的Virtex5系列FPGA器件为例,给出HDL代码中定义和使用差分接口的一些简单示例:

-- VHDL example
Library UNISIM;
use UNISIM.vcomponents.all;
-- ......
-- outer interface with other chip
clk_p : in std_logic;
clk_n : in std_logic;
dIn_p : in std_logic;
dIn_n : in std_logic;
dOut_p : out std_logic;
dOut_n : out std_logic;
-- inner interface with FPGA fabric
clkIn : out std_logic;
dIn : out std_logic;
dOut : in std_logic;
-- ......
IBUFGDS_inst : IBUFGDS
generic map (
	DIFF_TERM => FALSE, -- Differential Termination 
	IOSTANDARD => "DEFAULT")
port map (
	O => O,  -- Clock buffer output
	I => clk_p,  -- Diff_p clock buffer input (connect directly to top-level port)
	IB => clk_n -- Diff_n clock buffer input (connect directly to top-level port)
);
	
IBUFDS_inst : IBUFDS
generic map (
	DIFF_TERM => FALSE, -- Differential Termination 
	IOSTANDARD => "DEFAULT")
port map (
	O => dIn,  -- Buffer output
	I => dIn_p,  -- Diff_p buffer input (connect directly to top-level port)
	IB => dIn_n -- Diff_n buffer input (connect directly to top-level port)
);

OBUFDS_inst : OBUFDS
generic map (
	IOSTANDARD => "DEFAULT")
port map (
	O => dOut_p,     -- Diff_p output (connect directly to top-level port)
	OB => dOut_n,   -- Diff_n output (connect directly to top-level port)
	I => dOut      -- Buffer input 
);

// Verilog example
// ......
// outer interface with other chip
input clk_p;
input clk_n;
input dIn_p;
input dIn_n;
output dOut_p;
output dOut_n;
// inner interface with FPGA fabric
output clkIn;
output dIn;
input dOut;
// ......
IBUFGDS #(
	.DIFF_TERM("FALSE"),    // Differential Termination
	.IOSTANDARD("DEFAULT")  // Specify the input I/O standard
) IBUFGDS_inst (
	.O(clkIn),  // Clock buffer output
	.I(clk_p),  // Diff_p clock buffer input (connect directly to top-level port)
	.IB(clk_n) // Diff_n clock buffer input (connect directly to top-level port)
);

IBUFDS #(
	.DIFF_TERM("FALSE"),       // Differential Termination (Virtex-5, Spartan-3E/3A)
	.IOSTANDARD("DEFAULT")     // Specify the input I/O standard
) IBUFDS_inst (
	.O(dIn),  // Buffer output
	.I(dIn_p),  // Diff_p buffer input (connect directly to top-level port)
	.IB(dIn_n) // Diff_n buffer input (connect directly to top-level port)
);

OBUFDS #(
	.IOSTANDARD("DEFAULT") // Specify the output I/O standard
) OBUFDS_inst (
	.O(dOut_p),     // Diff_p output (connect directly to top-level port)
	.OB(dOut_n),   // Diff_n output (connect directly to top-level port)
	.I(dOut)      // Buffer input 
);

注意,差分接口不能直接在HDL中使用,必须通过恰当的原语转换,才可用于功能描述。当然了,实际使用中,还必须要配合正确的硬件连接和管脚约束信息。

无线接口

无论是之前介绍的单端接口还是差分接口,都是有线接口,因为信息的传输都是利用看得见、摸得着的硬件连线。而无线接口与有线接口恰恰相反,因为信息的传输都是利用空间电磁波的形式。不过,目前FPGA芯片的直接接口都是有线接口,因此只能通过利用有线接口的方式与相关的无线传输设备连接,这样就能对有线信息进行转换,进而实现无线通信。
不过由于FPGA芯片没有直接的无线接口,所以这里就不进行具体介绍。

按功能特性分类

按照传递信号的功能特性来分,外界接口又可分为时钟接口、控制接口和数据接口三大部分,分别介绍入下。

时钟接口

时钟接口,顾名思义,是传递时钟信号的接口,它是为FPGA设计提供时钟驱动信号或者FPGA芯片为外围硬件提供时钟信号的接口,除非整个FPGA芯片实现的是一个纯组合逻辑功能,否则时钟接口信号必不可少。
时钟信号从电信号波形上来看,通常都具有周期性,受其驱动的电路通常会在其上升沿或下降沿做出响应动作。在后续的章节中,我们会根据时钟信号的特性不同来进一步分析外界接口。

控制接口

控制接口,是传递控制信号的接口,例如复位信号、各类使能信号、标志信号等等。
控制信号从电信号波形上来看,通常传递的都是电平信息,受其驱动的电路通常会根据其当前处于高电平或低电平来做出不同的响应动作。

数据接口

数据接口,是传递数据信号的接口,例如视频数据、音频数据、坐标数据等等。
数据信号从电信号波形上来看,通常传递的都是脉冲序列,序列中高、低脉冲的排列情况就是数据信息。因此,为了能够不遗漏的、正确的传递和接收数据信息,数据接口通常都需要配合时钟接口一起工作。

按时钟特性分类

要完成一定的接口功能,往往需要组合使用多个不同类型的基本接口,这其中,时钟接口是绝大部分功能接口中都需要使用到的,例如,前面介绍过的数据接口通常都离不开时钟接口的配合。那么根据时钟信号与要传递信息之间的对应关系,又可以将外部接口分为异步接口和同步接口两个大类,分别介绍如下。

异步接口

异步接口,即不含有时钟信息的接口。一般控制信号中较容易碰到异步接口的情况,例如异步复位接口等等。对于数据信号来说,同步接口的情况较多,但也有异步接口的情况,例如异步串口。本小节主要讨论一下异步接口情况下是如何保证正确的数据传递的。

异步串口原理简介

数据信号是脉冲序列,那么在没有匹配的时钟信号的情况下,该如何不遗漏且不重复的采集到所有的数据信息呢?
首先,通信的双方必须约定好数据的传递速率,例如,1MSPS(1兆个采样点每秒)。接下来,发送方将会采用这个速率发送数据,而数据的发送显然是需要一个1MHz的时钟配合生成,只不过这个时钟信号并不会通过接口传递到接收端。
那么,由于接收端没有这样一个时钟信号,但却要正确接收数据,因此异步数据接口的实现难度主要在数据的接收端。既然发送端没有传过来跟数据匹配的时钟信号,那么在接收端的硬件电路上利用晶振等方法生成一个同频的时钟信号是不是就可以进行数据采集呢?答案是不行。因为就算能够得到一个频率一模一样的时钟信号,但由于相位无法保证,因此在进行数据采样时就难以保证各项时序指标的正确性。因此为了确保一定能够正确采样信号,在接收端会以一个频率远高于数据信号采样率的时钟来对异步数据信号进行采样,令其倍率为N(N通常为3倍以上,有时候甚至为几十倍)。这样一来,我们便可以根据冗余采样的结果,随便找到一个不接近数据变化沿的高频采样时钟边沿(越居中越好,因为这样建立和保持时间余量都比较大),然后每隔N个高频时钟周期采集一次数据信号即可顺利从异步接口中提取出所有的数据信息。
以异步串口数据通信为例,如果发送端的数据率为1MSPS,那么在接收端采用一个5MHz的采样时钟,其数据接收原理分析如下:
由于接收端的高频采样时钟和产生数据的时钟之间没有固定相位关系,因此接收端的两种极限采样情况如下图所示:
FPGA之道(58)关于外界接口的编程思路_其他_04
上图中,以采样一个单位逻辑0脉冲为例。
采样1为最好极限,即每个采样时钟都能采样到稳定的信号,因此采样结果为
……11100000111……;
采样2为最差极限,即数据的变化沿与高频时钟采样沿对齐,因此采样结果为
……11X0000X111……;(X表示不确定数值,可能判断为1也可能判断为0)
由于异步串口通常会以一个符号位的低脉冲表示通信的起始数据位,因此当高频采样时钟监测到至少4个连续的0时,就表示通信开始。且此时,以第三个0的采样时刻为基准,每隔5个时钟周期采集一次数据,便可得到所有的异步数据。如下图所示:
FPGA之道(58)关于外界接口的编程思路_其他_05
对于极限情况采样1,第三个0正好位于数据符号的中央,因此今后的每次采样都在数据位的中央;而对于极限情况采样2,由于数据从1至0的变化沿对准了高频时钟的采样沿,所以这一个采样可能为0也可能为1,从而导致第三个0的判断出现两种情况,分别如图中椭圆和长方形所示,不过无论是那种情况,今后的每次采样也都是比较靠近数据位中央的。综上所述,选取起始位的第三个0采样为采样基准,可以确保今后的采样点介于极限情况采样2的两种基准情况之间,如图中“采样范围”所示,从而确保异步采样的正确性。

异步串口HDL范例

在这一小节,给出一个具体的异步串口接收部分的HDL范例。其中高频采样时钟为数据率的5倍,数据帧的格式为1bit起始位(低电平)、8bit数据位(高位在前)、1bit停止位(高电平),空闲状态时,数据线保持在高电平。

-- VHDL example
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity UARTRXDemo is
	port (
		clk : in  std_logic;
		rst : in  std_logic;
		
		RX : in  std_logic;
		
		en : out std_logic;		
		data : out std_logic_vector(7 downto 0)
	);
end UARTRXDemo;

architecture Behavioral of UARTRXDemo is
	type stateType is (s_sniffer, s_receive);
	signal state, nextState : stateType;
	signal receiving, finishing : std_logic;
	signal highSpeedShifter : std_logic_vector(3 downto 0);
	signal lowSpeedShifter : std_logic_vector(6 downto 0);
	signal sampleCounter, nextSampleCounter : std_logic_vector(2 downto 0);
	signal bitCounter, nextBitCounter : std_logic_vector(2 downto 0);			
begin
	process(clk)
	begin
		if(clk'event and clk = '1')then
			if(rst = '1')then
				state <= s_sniffer;
				
				sampleCounter <= (others => '0');
				bitCounter <= (others => '0');
				
				highSpeedShifter <= (others => '1');
				lowSpeedShifter <= (others => '0');
				
				en <= '0';
				data <= (others => '0');
			else	
				state <= nextState;
		
				sampleCounter <= nextSampleCounter;
				bitCounter <= nextBitCounter;
				
				highSpeedShifter <= highSpeedShifter(2 downto 0) & RX;
				
				if(receiving = '1' and sampleCounter = "100")then
					lowSpeedShifter <= lowSpeedShifter(5 downto 0) & highSpeedShifter(1); 
				end if;
				
				en <= finishing;
				if(finishing = '1')then
					data <= lowSpeedShifter & highSpeedShifter(1);
				end if;		
			end if;
		end if;
	end process;
	
	process(state, highSpeedShifter, sampleCounter, bitCounter)
	begin
		nextSampleCounter <= (others => '0');
		nextBitCounter <= (others => '0');
		
		receiving <= '0';
		finishing <= '0';
		
		case (state) is 			
		when s_sniffer =>
			if(highSpeedShifter = "0000")then
				nextState <= s_receive;
			else
				nextState <= s_sniffer;
			end if;
			
		-- working on dividing
		when s_receive =>				
			receiving <= '1';	

			if(sampleCounter = "100")then
				nextSampleCounter <= (others => '0');
				nextBitCounter <= bitCounter + 1;
			else
				nextSampleCounter <= sampleCounter + 1;
				nextBitCounter <= bitCounter;
			end if;
						
			if(bitCounter = "111" and sampleCounter = "100")then
				finishing <= '1';
				nextState <= s_sniffer;
			else
				nextState <= s_receive;
			end if;
						
		when others =>
			nextState <= s_sniffer;
		end case;
	end process;
end Behavioral;

// Verilog example
module UARTRXDemo(
input clk5x,
input rst,

input RX,

output reg en,
output reg [7:0] data
);

parameter s_sniffer = 0, s_receive = 1;
reg state, nextState;
reg receiving, finishing;
reg [3:0] highSpeedShifter;
reg [6:0] lowSpeedShifter;
reg [2:0] sampleCounter, nextSampleCounter;
reg [2:0] bitCounter, nextBitCounter;

always@(posedge clk5x)
begin
	if(rst == 1'b1)
	begin
		state <= s_sniffer;
		
		sampleCounter <= 3'b0;
		bitCounter <= 3'b0;
		
		highSpeedShifter <= 4'b1111;
		lowSpeedShifter <= 7'b0;
		
		en <= 1'b0;
		data <= 8'b0;
	end
	else
	begin
		state <= nextState;
		
		sampleCounter <= nextSampleCounter;
		bitCounter <= nextBitCounter;
		
		highSpeedShifter <= {highSpeedShifter[2:0], RX};
		
		if(receiving == 1'b1 && sampleCounter == 3'd4)
		begin
			lowSpeedShifter <= {lowSpeedShifter[5:0], highSpeedShifter[1]}; 
		end
		
		en <= finishing;
		if(finishing == 1'b1)
		begin
			data <= {lowSpeedShifter, highSpeedShifter[1]};
		end			
	end
end

always@(state, highSpeedShifter, sampleCounter, bitCounter)
begin
	nextSampleCounter = 3'b0;
	nextBitCounter = 3'b0;
	
	receiving = 1'b0;
	finishing = 1'b0;
	
	case(state)
	s_sniffer : 
	begin
		if(highSpeedShifter == 4'b0)
		begin
			nextState = s_receive;	
		end
		else
		begin
			nextState = s_sniffer;
		end
	end
	
	s_receive : 
	begin
		receiving = 1'b1;

		if(sampleCounter == 3'd4)
		begin
			nextSampleCounter = 3'd0;
			nextBitCounter = bitCounter + 1'b1;
		end
		else
		begin
			nextSampleCounter = sampleCounter + 1'b1;
			nextBitCounter = bitCounter;
		end
					
		if(bitCounter == 3'd7 && sampleCounter == 3'd4)
		begin
			finishing = 1'b1;
			nextState = s_sniffer;
		end
		else
		begin
			nextState = s_receive;
		end			
	end
	
	default :
	begin
		nextState = s_sniffer;
	end
	endcase
end
endmodule

异步串口注意事项

疑问一:使用异步串口进行通信的时候,有一个非常重要的注意事项,那就是每次通信所传输的数据量不要太大。
举个例子来说,如果有100个字节的数据要发送,不要采用
“1bit起始位+数据长度(字节数)+800bits数据+1bit停止位”
这样的格式,而应该尽量分散每次传输的数据量,例如,采用
“1bit起始位+ 8bits数据+1bit停止位”
这样的格式,重复100次来完成所有数据的传输。
为什么要这样呢?

疑问二:在【异步串口HDL范例】小节中,我们可以看到,N = 5时已经能够很好的完成异步串口通信工作了,那为什么在【异步串口原理简介】小节中,提到接收端的高频采样时钟相比于数据率的倍率N有时候可能达到几十倍之高呢?

要解释以上两个疑问,得从异步串口的实际应用状况着手:
首先:既然是异步的,那么高频采样时钟就不可能精确的是数据率的某个整数倍。所以,自从识别出起始位后,每采样一次数据位,所间隔的时间都和实际的数据变化周期存在着误差。这是一种具有积累效应的误差,它会让接下来的数据采样点相对于数据的中心位置发生定向移动,并很快呈现出逐渐远离数据中心位置的趋势。因此,如果每次连续传递的数据位数过多,就有可能出现采样点进入数据变化区、同一个数据位被采样2次或者某个数据位被漏采的情况。
其次:异步串口应该具有通用性,否则一旦下次换另外一台不同数据速率的设备接入,就无法正常通信。因此,为了使得异步串口能够适应多种速率的通信,通常为其提供一个频率较高的时钟信号,倍率可能是该串口所支持的通信数据速率最大值的几十倍以上。这样一来,由于该时钟的周期较小,所以用来发送或接收某一指定速率的数据时,所引入的误差就小,从而能够保证更长的连续数据通信。例如,异步串口的高频时钟为1MHz,如果需要发送30KSPS的数据,则可以近似用高频时钟的33分频——30.3030……KHz去发送;如果需要接收30KSPS的数据,则同样可以在找到起始位后近似用30.3030……KHz去采样数据。

综上所述,一个通用的串口描述,其内部的高频时钟的频率应该选择在最大支持时钟频率的几十倍以上(不一定要是整数倍),并且每次通信所传递或接收的连续数据位数应该严格受控,在不影响通信的前提下越短越好。

同步接口

同步接口,即含有时钟信号的接口,大部分数据信号、控制信号的传递均是采用同步接口的形式。由于数据或控制信息与和它们匹配的时钟信号一起传递,这样在接收端便可以直接利用该时钟信号完成信息的采集,因此基于同步接口的通信稳定、可靠。由于同步接口主要是针对数据通信而言,因此本章节将根据时钟和数据的不同对应关系,将同步接口细分为SDR、DDR、MDR、CDR四种基本类型,分别介绍如下:

SDR

SDR,英文全称:Single Data Rate,翻译成中文即单倍数据率。它是指在同步接口传递数据时,时钟信号的频率与数据信号的采样率是相等的,即,在每个时钟周期内仅蕴含有一个数据采样,因此无论在数据的发送端或接收端,都仅需要使用时钟信号的一类有效边沿(上升沿或下降沿)驱动逻辑电路即可。例如,下图即为一种SDR接口的时序图例:
FPGA之道(58)关于外界接口的编程思路_串口_06
由于时钟频率与采样率相等,因此在数据发送时,采用时钟的上升沿或下降沿将数据逐个发送,并将该时钟信号或调整相位后的该时钟信号也一并发送出去。如下为SDR的发送端HDL示例:

-- VHDL example
	-- interface with FPGA fabric
	clk : in  std_logic;
	-- interface with other chip
	sClk : out  std_logic;
	sData : out  std_logic_vector(7 downto 0);
	......
	-- updated by other logic
	signal buffer : std_logic_vector(7 downto 0);	
	......
	process(clk)
	begin
		if(clk'event and clk = '1')then
			sData <= buffer;
		end if;
	end process;

	sClk <= clk;
	-- sClk <= not clk;

	// Verilog example
	// interface with FPGA fabric
	input clk,
	// interface with other chip
	output sClk, 
	output reg [7:0] sData,
	......	
	// updated by other logic
	reg [7:0] buffer;
	......	
	always@(posedge clk)
	begin
		sData <= buffer;
	end

	assign sClk = clk;
	// assign sClk = ~ clk;

而在接收端,则直接使用输入时钟信号的上升沿或下降沿完成数据接收即可。如下为SDR的接收端HDL示例:

-- VHDL example
-- interface with other chip
sClk : in std_logic;
sData : in std_logic_vector(7 downto 0);
......
-- used inside FPGA
signal buffer : std_logic_vector(7 downto 0);	
......
process(sClk)
begin
	if(sClk'event and sClk = '1')then
		buffer <= sData;
	end if;
end process;

// Verilog example
// interface with other chip
input sClk, 
input [7:0] sData,
......	
// used inside FPGA
reg [7:0] buffer;
......	
always@(posedge clk)
begin
	buffer <= sData;
end

DDR

DDR,英文全称:Double Data Rate,翻译成中文即双倍数据率。它是指在同步接口传递数据时,数据信号的采样率为时钟信号频率的两倍,即,在每个时钟周期内蕴含有两个数据采样。例如,下图即为一种DDR接口的时序图例:
FPGA之道(58)关于外界接口的编程思路_接收端_07
在【共同语言篇->数字逻辑电路基础知识->数字逻辑功能单元->时序逻辑基本单元简介】章节中,我们介绍了一些用于数字逻辑的时序单元,没有介绍的其实也都大同小异,万变不离其宗。因此我们可以发现,没有能够同时敏感时钟上升沿和下降沿的时序单元,那么,在数据的发送端或接收端,是怎样实现这样一个双沿驱动电路的呢?
使用锁相环进行时钟倍频,并以此时钟作为接收或发送端的基本采样时钟当然是可以的,不过这需要为每一个DDR数据接口都配备一个锁相环资源,由于DDR接口是一个被广泛、大量使用的接口,因此这样的做法显然有点“杀鸡用了宰牛刀”之嫌。显然,实现DDR接口应该存在着更为简便的方法,事实上,DDR就是一种不需要提高时钟频率就能够加倍提高数据传输速率的技术,如下即为实现ODDR(发送端DDR接口)、IDDR(接收端DDR接口)的一种原理图例:
FPGA之道(58)关于外界接口的编程思路_其他_08
通过上图可以看出,实现ODDR接口,其实就是利用两个触发器,一个敏感时钟信号上升沿、另一个敏感时钟下降沿,这样便可得到相位相差180度的两个数据流,然后通过复用器,配合恰当的选择逻辑,即可实现两倍速的数据输出。可参考下图理解:
FPGA之道(58)关于外界接口的编程思路_接收端_09
而对于IDDR接口,同样也需要两个触发器,一个利用时钟上升沿采样数据、另一个利用时钟下降沿采样另一半数据,如果内部想统一使用一个时钟边沿传递数据,那么可以再利用时钟的上升沿或下降沿同时采样这两个触发器的输出,进而可以在一个时钟周期内得到两个有效数据。可参考下图理解:
FPGA之道(58)关于外界接口的编程思路_其他_10
当然了,由于DDR接口更多时候是为了完成较高速的数据通信任务的(否则使用SDR接口即可),因此DDR接口的实现主要也都是利用FPGA芯片接口资源中的寄存器等逻辑资源来实现,具有专门的硬件结构,因此线延迟、门延迟等时序参数也都精确可控。因此,在HDL程序实现时,我们可以直接调用编译器提供的ODDR、IDDR的相关原语(原语的概念请参考【本篇->编程思路->原语的使用】章节),并根据实际需要为其配置正确的属性(主要是输入、输出数据相对于时钟边沿的关系)即可。例如,对于Xilinx公司Virtex5系列的FPGA芯片来说,其DDR相关原语的使用示例如下:

-- VHDL example
Library UNISIM;
use UNISIM.vcomponents.all;

IDDR_inst : IDDR 
generic map (
	DDR_CLK_EDGE => "OPPOSITE_EDGE", -- "OPPOSITE_EDGE", "SAME_EDGE" 
												-- or "SAME_EDGE_PIPELINED" 
	INIT_Q1 => '0', -- Initial value of Q1: '0' or '1'
	INIT_Q2 => '0', -- Initial value of Q2: '0' or '1'
	SRTYPE => "SYNC") -- Set/Reset type: "SYNC" or "ASYNC" 
port map (
	Q1 => Q1, -- 1-bit output for positive edge of clock 
	Q2 => Q2, -- 1-bit output for negative edge of clock
	C => C,   -- 1-bit clock input
	CE => CE, -- 1-bit clock enable input
	D => D,   -- 1-bit DDR data input
	R => R,   -- 1-bit reset
	S => S    -- 1-bit set
	);
	
ODDR_inst : ODDR
generic map(
	DDR_CLK_EDGE => "OPPOSITE_EDGE", -- "OPPOSITE_EDGE" or "SAME_EDGE" 
	INIT => '0',   -- Initial value for Q port ('1' or '0')
	SRTYPE => "SYNC") -- Reset Type ("ASYNC" or "SYNC")
port map (
	Q => Q,   -- 1-bit DDR output
	C => C,    -- 1-bit clock input
	CE => CE,  -- 1-bit clock enable input
	D1 => D1,  -- 1-bit data input (positive edge)
	D2 => D2,  -- 1-bit data input (negative edge)
	R => R,    -- 1-bit reset input
	S => S     -- 1-bit set input
);

// Verilog example
IDDR #(
	.DDR_CLK_EDGE("OPPOSITE_EDGE"), // "OPPOSITE_EDGE", "SAME_EDGE" 
											  //    or "SAME_EDGE_PIPELINED" 
	.INIT_Q1(1'b0), // Initial value of Q1: 1'b0 or 1'b1
	.INIT_Q2(1'b0), // Initial value of Q2: 1'b0 or 1'b1
	.SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
) IDDR_inst (
	.Q1(Q1), // 1-bit output for positive edge of clock 
	.Q2(Q2), // 1-bit output for negative edge of clock
	.C(C),   // 1-bit clock input
	.CE(CE), // 1-bit clock enable input
	.D(D),   // 1-bit DDR data input
	.R(R),   // 1-bit reset
	.S(S)    // 1-bit set
);

ODDR #(
	.DDR_CLK_EDGE("OPPOSITE_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE" 
	.INIT(1'b0),    // Initial value of Q: 1'b0 or 1'b1
	.SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
) ODDR_inst (
	.Q(Q),   // 1-bit DDR output
	.C(C),   // 1-bit clock input
	.CE(CE), // 1-bit clock enable input
	.D1(D1), // 1-bit data input (positive edge)
	.D2(D2), // 1-bit data input (negative edge)
	.R(R),   // 1-bit reset
	.S(S)    // 1-bit set
);

如果想要通过自己编写HDL代码的形式来实现DDR接口,则除了要从逻辑上搭好DDR接口数字电路,还需要按照添加综合约束的方法确保资源的使用情况,以及添加恰当的时序约束。

MDR

MDR,英文全称:Multiple Data Rate,翻译成中文即多倍数据率。它是指在同步接口传递数据时,数据信号的采样率为时钟信号频率的好几倍(大于两倍),即,在每个时钟周期内蕴含有多个数据采样。例如,下图即为一种3倍数据率MDR接口的时序图例:
FPGA之道(58)关于外界接口的编程思路_其他_11
对于MDR接口来说,要想实现它的收、发功能,只能采用时钟倍频、分频的处理了,也就是需要利用锁相环资源。MDR接口通常也适用于传输速率较高的情况,但是它并不会比DDR接口更快,也就是说,MDR接口并没有提高DDR接口的传输速率,反而却多消耗了锁相环资源,那么MDR存在的意义何在呢?
MDR存在的主要意义就是使得边界检测变得非常简单,这是由于串行通信时,需要将数据按位发出,而在接收端则需要确定数据流中何处为某一数据的开始位,何处为某以数据的结束位。而利用MDR,可以让串行传输的某一数据的所有位均被一个时钟周期包含,这样便可以简单的在接收端确定串行传输数据的边界。例如,目前大部分视频传输都是基于低压差分信号(LVDS)的MDR接口,如下即为一个颜色深度为8bits的彩色图像通道时序图示例:
FPGA之道(58)关于外界接口的编程思路_数据_12
通过上图我们可以看出,该通道包含1对时钟差分线和4对数据差分线(串行)。从时序图可以看出,该MDR接口的数据率与时钟频率比值为7,也即一个时钟周期内部包含7个数据采样。时钟信号的占空比为4:3,并以1100011作为一个时钟周期,其时间范围内包含的4对数据线上的共28个采样值(每根线上7个采样值),共同属于一个视频数据元素,分别为R、G、B分量各8bits,行、场同步各1bit,数据使能1bit,保留位1bit。
由此可见,MDR接口中的时钟信号,兼具传递时钟信息和边界信息两种作用,所以在进行HDL代码设计时,对于发送端,为了保证时钟与数据的良好对应关系,建议低频时钟信号也以数据的形式发送出去,而不要用分频、调相的方式发送;而对于接收端,低频时钟信号除了被倍频产生高频采样时钟外,在进行数据采集的同时还需要采集低频时钟信号,从而才能进行边界的判断。如下为一个7倍MDR接口的示意性HDL例子:

-- VHDL example
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.STD_LOGIC_ARITH.ALL;
use IEEE.STD_LOGIC_UNSIGNED.ALL;

entity MDRInterface is
port (
	-- inner interface with FPGA fabric
	clk7x : in std_logic;
	loadSendData : in std_logic;
	sendData : in std_logic_vector(6 downto 0);
	-- outer interface with other chip
	MDRClkOut : out std_logic;
	MDRDataOut : out std_logic;

	-- inner interface with FPGA fabric
	enReceive : out std_logic;
	receiveData : out std_logic_vector(6 downto 0);
	-- outer interface with other chip
	MDRClkIn : in std_logic;
	MDRDataIn : in std_logic
);
end MDRInterface;

architecture Behavioral of MDRInterface is		
	signal sendDataShift : std_logic_vector(6 downto 0);
	signal sendClkShift : std_logic_vector(6 downto 0);
	
	signal clkFx : std_logic;		
	signal receiveDataShift : std_logic_vector(6 downto 0);
	signal receiveClkShift : std_logic_vector(6 downto 0);
begin	

-- ------------------------------------------------
-- MDR sender
process(clk7x)
begin
	if(clk7x'event and clk7x = '1')then
		if(loadSendData = '1')then
			sendClkShift <= "1100011";
			sendDataShift <= sendData;
		else
			sendClkShift <= sendClkShift(5 downto 0) & '0';
			sendDataShift <= sendDataShift(5 downto 0) & '0';
		end if;
	end if;
end process;

-- ------------------------------------------------
-- MDR receiver
PLL_inst : PLL
port map (
	CLKOUT => clkFx,  -- x7, may need some phase shift
	CLKIN => MDRClkIn -- MDRClkIn must use the global clk pin
);

process(clkFx)
begin
	if(clkFx'event and clkFx = '1')then
		receiveClkShift <= receiveClkShift(5 downto 0) & '0';
		receiveDataShift <= receiveDataShift(5 downto 0) & '0';

		if(receiveClkShift = "1100011")then
			enReceive <= '1';
			receiveData <= receiveDataShift;
		else
			enReceive <= '0';
		end if;
	end if;
end process;
end Behavioral;

// Verilog example
module MDRInterface(
// inner interface with FPGA fabric
input clk7x,
input loadSendData,
input [6:0] sendData,
// outer interface with other chip
output MDRClkOut,
output MDRDataOut,

// inner interface with FPGA fabric
output enReceive,
output [6:0] receiveData,
// outer interface with other chip
input MDRClkIn,
input MDRDataIn
);

reg [6:0] sendDataShift;
reg [6:0] sendClkShift;

wire clkFx;
reg [6:0] receiveDataShift;
reg [6:0] receiveClkShift;

//------------------------------------------------
// MDR sender
assign MDRClkOut = sendClkShift[6];
assign MDRDataOut = sendDataShift[6];

always@(posedge clk7x)
begin
	if(loadSendData == 1'b1)
	begin
		sendClkShift <= 7'b1100011;
		sendDataShift <= sendData;
	end
	else
	begin
		sendClkShift <= {sendClkShift[5:0], 1'b0};
		sendDataShift <= {sendDataShift[5:0], 1'b0};
	end
end

//------------------------------------------------
// MDR receiver
PLL PLL_inst (
	.CLKOUT(clkFx), // x7, may need some phase shift        
	.CLKIN(MDRClkIn) // MDRClkIn must use the global clk pin
);

always@(posedge clkFx)
begin
	receiveClkShift <= {receiveClkShift[5:0], 1'b0};
	receiveDataShift <= {receiveDataShift[5:0], 1'b0};
	
	if(receiveClkShift == 7'b1100011)
	begin
		enReceive <= 1'b1;
		receiveData <= receiveDataShift;
	end
	else
	begin
		enReceive <= 1'b0;
	end
end
endmodule

注意,由于MDR通常也是处理高速率数据传输的,因此上述示例通常需要配合恰当的管脚约束、综合约束、时序约束才能正常工作,例如,连接时钟的FPGA芯片管脚可能必须为全局时钟管脚,因为PLL、DCM等不能估计从其它管脚连接到其输入端的线延迟。

CDR

CDR,英文全称:Clock/Data Recovery,翻译成中文即时钟、数据恢复。使用CDR接口进行通信时,也是同时传递时钟信息和数据信息的,因此CDR属于同步接口。但是与之前介绍的SDR、DDR、MDR等接口不同的地方是,CDR接口中没有使用专门的硬件连线来传递时钟信息或数据信息,而是将时钟信息和数据信息融合到一起,用一根信号线完成所有信息的传输。因此,如果通俗一点来讲,复用接口是长着异步接口的外表,但却具有同步接口的心。就目前来说,CDR接口在极速数据通信中有着非常广泛的应用,例如USB3.0、Serial ATA、PCI Express等等,而使CDR接口具有如此极速的通信能力,并且又如此节省硬件连线资源的原因,就是8B\10B编、解码技术。
在CDR发送端,运用8B\10B编码技术,将每8个bits的数据信息编码为10个bits的编码,这就意味着可以从1024个编码符号中选出256个来与实际数据符号相对应,因此,可以保证每个10bits编码中的0、1数量相等或仅有2位偏差,同时还可以保证发送过程中的连1或连0不会超过5个。这样一来,将使得整个串行数据传输达到直流平衡,而这将有利于使用一个具有高通特性的通道来进行数据通信,例如以太网使用的双绞线、光纤等。
在CDR接收端,由于8B\10B编码技术赋予了串行数据足够次数的状态变迁,因此便可以利用数据的这种特性通过相关时钟恢复算法恢复出匹配的高频时钟信号,并利用该高频时钟信号完成信号采样。然后再运用8B\10B解码技术,对采集的信息进行边界确定、解码,并最终恢复出原始信息。
以上便是CDR接口的基本工作原理,目前有很多ASIC芯片都可以完成这样的接口功能,对于一些高级的FPGA芯片来说,里面也集成了具有这种功能的硬件资源,因此在进行FPGA设计时,可以通过调用相关IP核,或者编写相应ASIC芯片的控制逻辑,来实现CDR接口。

按数据位宽分类

在以传递数据信息为主的接口中,按照数据单元在数据线上的分布情况,数据接口又可主要分为串行接口、并行接口两大类,分别介绍如下:

普通串行接口

普通串行接口,简称串口,是采用串行传输方式来传输数据的接口标准。串口的数据传递方式是数据一位位地顺序传送,因此无论待传递的数据采样是什么样的位宽形式(字节、字、双字等等),都能简单的通过单根数据线完成信息传递,所以串口通信的最大特点就是通信线路非常简单,一根传输线完成单工通信、一对传输线就可以实现双向通信。
例如,异步串口UART、同步串口SSI、接口、SPI接口等等,均是普通串行接口。

并行接口

并行接口,简称并口,是指采用并行传输方式来传输数据的接口标准。并口的数据传递方式是数据采样的所有位一并通过数据线传送,因此随着数据采样本身的位宽形式不同,并口的传输线根数也会有所不同。不过相比于普通串口,由于并口一次性发送数据采样的所有数据位,因此数据信息传递的速度等级往往要远高于普通的串口。
例如,并口硬盘的IDE接口、PC主板上的PCI接口、各类存储芯片的数据存取接口等等,均是并行接口。

高速串行接口

随着数据通信速率的不断提升,并行接口也有其自身的问题,那就是并行通信的多根数据线之间,会存在着相互干扰和串扰,并且随着数据传输速率的上升,这种干扰、串扰的影响也就越明显,所以这就限制了并口的通信速度。因此,随着人们对通信速率的进一步渴望,便又将目光投向了串行接口。
通过使用【按时钟特性分类->同步接口->CDR】小节中介绍的8B\10B编、解码技术,可以将整个数据通信融汇为只需一根传输线,从而避免了传输线之间的干扰和串扰;通过使用【按电气特性分类->差分接口】小节中介绍的差分编码技术,可以通过单线变双线的方法提高冗余,从而避免了外界对传输线之间的干扰;再加上类似光纤、万兆网线等可作为高速传输屏蔽线的载体出现,使得高速串口在高速、极速通信领域得到了广泛的应用。
例如,串口硬盘的SATA接口、PC主板上的PCIe接口等等,均是高速串行接口。

半串半并接口

有时候,为了提高串口的速度,可以同时使用多个串口进行数据通信;有时候,为了适应固定位宽总线的并口通信,需要将一个数据采样分段传输。因此,类似这样的通信接口兼具串口、并口的特性,所以这里将它们统称为半串半并接口。
例如【按时钟特性分类->同步接口->MDR】小节中介绍的视频LVDS接口就属于这种半串半并接口。

串口中的对齐方式

串口通信中,由于数据是一位位地顺序传送的,这样便破坏了原始数据采样的完整性,因此,如果不采取措施,在数据的接收端将因为无法确定数据采样的边界而无法恢复出正确的数据信息。例如,要发送字节数据类型,0x47(对应二进制为01000111),如果从高位开始发送,那么数据总线上的情况应该是:
……01000111……
试问,传输数据时如果没有恰当的措施,将如何才能将01000111这8个连续的bits认为是属于一个数据采样的呢?
那么,接下来就介绍一些串口通信中常用的边界对齐方式:

起始位、停止位

起始位、停止位,是指通信双方事先约定好一个固定长度的电平信号、或者一个确定的跳变沿。平时数据线保持在另一个电平状态,当监测到约定好的电平信号或者跳变沿,则表明通信正式开始,此后接收到的第一个串行数据即为正式通信时传输第一个数据采样的第一个bit。
例如,在【按时钟特性分类->异步接口->异步串口原理简介】中介绍的异步串口采用的就是起始位、停止位的边界对齐方式。

字对齐

字对齐的方式,是在串行传输的数据中,插入一些特殊的码字,用于帮助字符边界的识别。
例如,这里介绍一个简单的方法。指定0x47作为通信开始的字符识别,指定0x56为无效占位符号,并且数据采样中不会有0x47、0x56,这样一来,便保证了特殊字符的唯一性。现在来解释一下为什么需要一个无效占位符0x56。尽管在通信过程中,预先已经保证了数据采样中不存在0x47这样的字节数据,但是,如果连续发送0x14、0x73这样的两个字节,那么也会在串行数据中检测到0x47,这样便发生了误检。因此,为了避免这种情况发生,在发送端就需要预检测,一旦可能出现这种情况,就在这连续两个字节的中间插入一个无效占位符,变成类似0x14、0x56、0x73这样的序列。对于0x47(二进制为01000111)来说,由于0x56(二进制为01010110),即不可能成为它的前缀、也不可能成为它的后缀,因此无论0x56前面或后面是什么字节,都不可能凑出0x47的位流关系,所以采用这种方法可以确保0x47特殊码字在串行数据流中的唯一性,从而避免误检发生。
又或者只使用0x47作为起始识别字符,并且也不限制通信过程中的数据形式,只不过将数据划分成为包的形式,例如每个数据包100个字节,并以0x47作为包头,这样一来,如果连续3次以上,每隔99个字节就能检测到一个0x47码流,则说明数据包已经同步上,以此作为基础便能正确找到边界。并在今后的通信中继续检测0x47,若隔了99个字节没有检测到,则说明数据通信失锁,需要再次进行锁定后才能开始数据接收。
当然了,字对齐的方式有很多,以上只是举了两个很简单的例子,在实际的使用中,可以设计符合当前通信特性、合情合理的字对齐原则。

时钟对齐

时钟对齐,是指利用时钟信号完成串行数据的边界对齐工作。通常来说,时钟对齐的做法,是指在1个时钟的周期内包含有若干个bits,而这些bits都是属于一个原始数据采样字符的。例如与时钟具有特定关系,例如【按时钟特性分类->同步接口->MDR】小节中介绍的MDR接口,采用的就是时钟对齐的方式。

串并、并串转换接口

串行通信能够得到广泛应用的重要前提之一,就是各类高效的串并和并串接口,它们以低位优先或者高位优先的方式发送或者接收串行数据,简介如下:

并串转换接口

先看并串接口,原始数据采样往往都不是单比特的,因为数据采样需要一定的位宽来实现足够的精度或分辨率,因此在使用串口发送数据的时候,绝大部分时候都需要使用并串接口。对于速度不是很高的串行通信来说,我们可以使用HDL来实现并串接口的功能,例如:

-- VHDL example
sDOut <= shift(7);

process(clk8x)
begin
	if(clk8x'event and clk8x = '1')then
		if(en = '1')then -- 1 pulse every 8 clk8x period
			shift <= pDout; -- MSB first
		else
			shift <= shift(6 downto 0) & '0';
		end if;
	end if;
end process;

// Verilog example
assign sDOut = shift[7];

always@(posedge clk8x)
begin
	if(en == 1'b1) // 1 pulse every 8 clk8x period
	begin
		shift <= pDout; // MSB first
	end
	else
	begin
		shift <= {shift[6:0], 1'b0};
	end
end

但对于速度较高的一些的串行通信,最好或者只能采用FPGA芯片中的一些专有逻辑资源和IP核来实现,因为这样会更容易、更可靠,详见后续【按速度分类】章节中的介绍。

串并转换接口

再看串并接口,在数据接收端,经过边界对齐算法找到串行传输数据符号的边界后,必须通过串并转换接口将串行数据流转换为有意义的并行数据符号序列。对于速度不是很高的串行通信来说,我们可以使用HDL来实现串并接口的功能,例如:

-- VHDL example
process(clk8x)
begin
	if(clk8x'event and clk8x = '1')then			
		dataShift <= dataShift(6 downto 0) & sDIn; -- MSB first

		if(dataShift = X"47")then
			lock <= '1';
		else
			lock <= '0';
		end if;

		if(lock = '1')then
			enShift <= "00000001";
		else
			enShift <= enShift(6 downto 0) & '0';
		end if;

		enPDout <= enShift(7);
		if(enShift(7) = '1')then
		begin
			pDout <= dataShift;
		end
	end if;
end process;

// Verilog example
always@(posedge clk8x)
begin
	dataShift <= {dataShift[6:0], sDIn}; // MSB first
	
	if(dataShift == 8'b47)
	begin
		lock <= 1'b1;
	end
	else
	begin
		lock <= 1'b0;
	end
	
	if(lock == 1'b1)
	begin
		enShift <= 8'b00000001;			
	end
	else
	begin
		enShift <= {enShift[6:0], 1'b0};
	end
	
	enPDout <= enShift[7];
	if(enShift[7] == 1'b1)
	begin
		pDout <= dataShift;
	end
end

但对于速度较高的一些的串行通信,最好或者只能采用FPGA芯片中的一些专有逻辑资源和IP核来实现,因为这样会更容易、更可靠,详见后续【按速度分类】章节中的介绍。

按速度分类

按照传递信号的速度情况来分,外界接口又可分为低速接口、中低速接口、中高速接口、高速接口和极速接口五大类别,分别介绍入下。

低速接口

低速接口是信息传递速率低的接口,如果非要给低速接口定一个量的话,姑且就认为通信信息率小于10MSPS的都属于低速接口。
由于通信速度较低,FPGA芯片内部的各类线延迟、门延迟等等参数相比于数据更新周期(大于100ns)都可忽略不计,因此只要逻辑功能把握正确,符合接口要求的时序关系,便能完成正确的数据发送与接收工作。
对于低速接口来说,由于时间裕度较大,所以,在接收端,可以采用直接将FPGA芯片输入pin脚的信息引入至FPGA内部的可配置逻辑资源块中去处理的方法;而在发送端,也可以采用直接从FPGA内部的可配置逻辑单元产生输出信号并直接连接至FPGA芯片的输出pin脚。因此,对于低速接口,直接用HDL进行相关的功能描述就可以正确实现。

中低速接口

中低速接口是信息传递速率较低的接口,如果非要给中低速接口定一个量的话,姑且就认为通信信息率介于10MSPS ~ 50MSPS范围之间的都属于中低速接口。
由于通信速率不算高,所以FPGA芯片内部的各类线延迟、门延迟等等参数通常也都小于数据更新周期(介于100ns ~ 20ns),因此只要逻辑功能实现的较为简洁(避免引入较复杂的组合逻辑),符合接口要求的时序关系,也能完成正确的数据发送与接收工作。
从实现上来说,中低速接口与低俗接口类似,只是需要注意一点,就是用HDL描述的逻辑要尽量简洁,否则积累下来的线延迟或门延迟可能就可以和数据更新周期相比拟了。

中高速接口

中高速接口是信息传递速率较高的接口,如果非要给中高速接口定一个量的话,姑且就认为通信信息率介于50MSPS ~ 150MSPS范围之间的都属于中高速接口。
由于通信速率较高,所以FPGA芯片内部的各类线延迟、门延迟等等参数已经可以和数据更新周期(介于20ns ~ 6.6ns)相比拟了,因此仅仅逻辑功能实现的正确,符合接口要求的时序关系,也未必能完成正确的数据发送与接收工作。
对于中高速接口来说,由于时间裕度较小,所以,在接收端,如果采用直接将FPGA芯片输入pin脚的信息引入至FPGA内部的可配置逻辑资源块中去处理的方法,则在简化逻辑的同时还必须配合使用严格的时序约束,这样编译器才会通过努力的布局布线来减少、控制门延迟和线延迟,从而使得接收能够成功;而在发送端,如果采用直接从FPGA内部的可配置逻辑单元产生输出信号并直接连接至FPGA芯片的输出pin脚,则也必须配合使用正确的时序约束,虽然目前来说,针对输出端的时序约束,并不会左右编译器的布局布线结果,但是至少在后续的时序分析中过程,编译器可以告知我们该发送接口是否满足后续接收芯片的时序要求,并以此作为后续修改的依据。因此,对于中高速接口,必须确保逻辑HDL正确且简洁,同时必须配合正确且严格的时序约束,才可以确保接口的正确实现。

高速接口

高速接口是信息传递速率高的接口,如果非要给高速接口定一个量的话,姑且就认为通信信息率介于150MSPS ~ 1000MSPS的都属于高速接口。
由于通信速度高,所以FPGA芯片内部的各类线延迟、门延迟等等参数很可能比数据更新周期(介于6.6ns ~ 1ns)要大,因此仅靠逻辑简洁、正确加上时序约束,往往也不可能完成正确的数据发送与接收工作。
对于高速接口来说,由于时间裕度很小,所以,在接收端,必须采用直接将FPGA芯片输入pin脚的信息引入至离FPGA芯片pin脚最近的接口资源块中去处理的方法;而在发送端,也最好将最后一部分发送逻辑置入离FPGA芯片pin脚最近的接口资源块中。由于接口资源中具有少量触发器,且具有固定且精确的布局布线特性,所以线延迟、门延迟相对于可编程逻辑块资源都更小且可控,因此,对于高速接口,光有正确且简洁的HDL描述、正确且严格的时序约束还不够,还需要配合正确的综合约束或者使用特定的原语将特定的逻辑置于接口资源中,并且配合一些IP核(高速接口常常会用到PLL、DCM等等资源)的正确使用,方可确保接口的正确实现。
最后,需要注意一点,对于高速接口来说,使用时序约束往往也无法左右编译器的映射和布局布线行为,因为接口资源内部的布局布线相对来说比较固定,但是可以在后续的时序分析环节让我们知道该接口是否满足时序要求。

极速接口

极速接口是信息传递速率非常高的接口,如果非要给极速接口定一个量的话,姑且就认为通信信息率大于1GSPS的都属于极速接口。
由于通信速度非常高,FPGA芯片内部的各类线延迟、门延迟等等参数可能会远大于数据更新周期(小于1ns),因此采用HDL描述的形式往往不能实现极速接口的正确收发。
对于极速接口来说,由于时间裕度非常小,所以在接收端和发送端,往往都需要专有的硬件资源,对于某些高级一些的FPGA芯片,其内部往往集成了被称为收发器这样的资源,可以用来实现大于1GSPS的通信要求,而对于那些没有集成这类资源的FPGA芯片,则可以通过使用现成的高速并串、串并转换芯片,来实现极速接口。