用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号转成串行输出信号的芯片,UART通常被集成于其他通讯接口的连结上
1.1.1串口信号定义和接线方法-5针串口-9针串口-全功能串口
并行口与串行口的区别是交换信息的方式不同,并行口能同时通过8条数据线传输信息,一次传输一个字节;而串行口只能用1条线传输一位数据,每次传输一个字节的一位。并行口由于同时传输更多的信息,速度明显高于串行口,但串行口可以用于比并行口更远距离的数据传输。
3.串行口的典型代表是RS-232C及其兼容插口,有9针和25针两类。25针串行口具有20mA电流环接口功能,用9、11、18、25针来实现。其针脚功能如下:
目前较为常用的串口有9针串口(DB9)和25针串口(DB25),通信距离较近时(<12m),可以用电缆线直接连接标准rs232端口(rs422< span="">,RS485较远),若距离较远,需附加调制解调器(MODEM)。最为简单且常用的是三线制接法,即地、接收数据和发送数据三脚相连,本文只涉及到最为基本的接法,且直接用RS232相连。表2‑6中为DB9和DB25的常用信号脚说明。
RS232C串口通信接线方法(三线制),串口传输数据只要有接收数据针脚和发送针脚就能实现:同一个串口的接收脚和发送脚直接用线相连,两个串口相连或一个串口和多个串口相连:同一个串口的接收脚和发送脚直接用线相连 对9针串口和25针串口,均是2与3直接相连;两个不同串口(不论是同一台计算机的两个串口或分别是不同计算机的串口)。上面分析是对微机标准串行口而言的,还有许多非标准设备,如接收GPS数据或电子罗盘数据,只要记住一个原则:接收数据针脚(或线)与发送数据针脚(或线)相连,彼此交叉,信号地对应相接,就能百战百胜。串口调试时,准备一个好用的调试工具,如串口调试助手、串口精灵等,有事半功倍之效果; 强烈建议不要带电插拨串口,插拨时至少有一端是断电的,否则串口易损坏。如果在通信过程的任意时刻,信息只能由一方A传到另一方B,则称为单工。如果在任意时刻,信息既可由A传到B,又能由B传A,但只能由一个方向上的传输存在,称为半双工传输。如果在任意时刻,线路上存在A到B和B到A的双向信号传输,则称为全双工。电话线就是二线全双工信道。由于采用了回波抵消技术,双向的传输信号不致混淆不清。双工信道有时也将收、发信道分开,采用分离的线路或频带传输相反方向的信号,如回线传输。串行数据在传输过程中,由于干扰可能引起信息的出错,例如,传输字符‘E’,其各位为:
由于干扰,可能使位变为1,这种情况,我们称为出现了“误码”。我们把如何发现传输中的错误,叫“检错”。发现错误后,如何消除错误,叫“纠错”。最简单的检错方法是“奇偶校验”,即在传送字符的各位之外,再传送1位奇/偶校验位。可采用奇校验或偶校验。奇校验:所有传送的数位(含字符的各数位和校验位)中,“1”的个数为奇数,如:
偶校验:所有传送的数位(含字符的各数位和校验位)中,“1”的个数为偶数,如:
奇偶校验能够检测出信息传输过程中的部分误码(1位误码能检出,2位及2位以上误码不能检出),同时,它不能纠错。在发现错误后,只能要求重发。但由于其实现简单,仍得到了广泛使用。有些检错方法,具有自动纠错能力。如循环冗余码(CRC)检错等。在串行通讯处理中,常常看到RTS/CTS和XON/XOFF这两个选项(尤其在上位机中),这就是两个流控制的选项,目前流控制主要应用于调制解调器的数据通讯中,但对普通RS232编程,了解一点这方面的知识是有好处的。那么,流控制在串行通讯中有何作用,在编制串行通讯程序怎样应用呢?这里我们就来谈谈这个问题。这里讲到的“流”,当然指的是数据流。数据在两个串口之间传输时,常常会出现丢失数据的现象,或者两台计算机的处理速度不同,如台式机与单片机之间的通讯,接收端数据缓冲区已满,则此时继续发送来的数据就会丢失。现在我们在网络上通过MODEM进行数据传输,这个问题就尤为突出。流控制能解决这个问题,当接收端数据处理不过来时,就发出“不再接收”的信号,发送端就停止发送,直到收到“可以继续发送”的信号再发送数据。因此流控制可以控制数据传输的进程,防止数据的丢失。PC机中常用的两种流控制是硬件流控制(包括RTS/CTS、DTR/CTS等)和软件流控制XON/XOFF(继续/停止),下面分别说明。硬件流控制常用的有RTS/CTS流控制和DTR/DSR(数据终端就绪/数据设置就绪)流控制。硬件流控制必须将相应的电缆线连上,用RTS/CTS(请求发送/清除发送)流控制时,应将通讯两端的RTS、CTS线对应相连,数据终端设备(如计算机)使用RTS来起始调制解调器或其它数据通讯设备的数据流,而数据通讯设备(如调制解调器)则用CTS来起动和暂停来自计算机的数据流。这种硬件握手方式的过程为:我们在编程时根据接收端缓冲区大小设置一个高位标志(可为缓冲区大小的75%)和一个低位标志(可为缓冲区大小的25%),当缓冲区内数据量达到高位时,我们在接收端将CTS线置低电平(送逻辑0),当发送端的程序检测到CTS为低后,就停止发送数据,直到接收端缓冲区的数据量低于低位而将CTS置高电平。RTS则用来标明接收设备有没有准备好接收数据。常用的流控制还有还有DTR/DSR(数据终端就绪/数据设置就绪)。我们在此不再详述。由于流控制的多样性,我个人认为,当软件里用了流控制时,应做详细的说明,如何接线,如何应用。由于电缆线的限制,我们在普通的控制通讯中一般不用硬件流控制,而用软件流控制。一般通过XON/XOFF来实现软件流控制。常用方法是:当接收端的输入缓冲区内数据量超过设定的高位时,就向数据发送端发出XOFF字符(十进制的19或Control-S,设备编程说明书应该有详细阐述),发送端收到XOFF字符后就立即停止发送数据;当接收端的输入缓冲区内数据量低于设定的低位时,就向数据发送端发出XON字符(十进制的17或Control-Q),发送端收到XON字符后就立即开始发送数据。一般可以从设备配套源程序中找到发送的是什么字符。应该注意,若传输的是二进制数据,标志字符也有可能在数据流中出现而引起误操作,这是软件流控制的缺陷,而硬件流控制不会有。随着计算机的日益普及,很多非RS232的串口也要接入PC机,如果为每一种新出现的串口都增加一个新的I/O口显然不现实,因为PC后面板位置有限,因此,将RS232串口和非RS232串口都通过RS232口接入是最佳方案。UART的U(通用)指的就是这个意思。早期ROM BIOS和DOS里的通信软件都是为RS232设计的,在没有检测到DCD有效前不会发送数据,因此,就连发送一个字符这样朴素的应用也要给出DCD、DTR、DSR等控制信号。因此,串口接头上要将一些控制线短接,或者干脆绕过系统软件自己写通信程序。到此,UART的涵义就总结为:通用的 异步 (串行) I/O口。就在UART冠以通用二字,准备一统江湖的时候,制造商们不满于它的速度、体积和灵活性(软件可配置),推出了USB和1394串口。目前,笔记本上的UART串口有被取消的趋势,因而有网友发出了“没有串口,吾谁与归”的慨叹,古今多少事,都付笑谈中,USB取代UART是后话,暂且不表。话说自从贺氏(Hayes)公司推出了聪明猫(SmartModem),他们制定的MODEM接口就成了业界标准,自此以后,所有公司制造的兼容猫都符合贺氏标准(连AT指令也兼容)。细观贺氏制定的MODEM串口,与RS232标准大不相同。DTR在整个通信过程中一直保持有效,DSR在MODEM上电后/可以拨号前有效(取决于软件对DSR的理解),在通信过程的任意时刻,只要DTR/DSR无效,通信过程立即终止。在某种意义上,这也可以算是流控,但肯定不是RS232所指的那种主流控。如果拘泥于RS232,你是不会理解DTR和DSR的用途的。贺氏不但改了DTR和DSR,竟然连RTS和CTS的涵义也重新定义了。因此,RTS和CTS已经不具有最开始的意义了。从字面理解RTS和CTS,是用于半双工通信的,当DTE想从收模式改为发模式时,就有效RTS请求发送,DCE收到RTS请求后不能立即完成转换,需要一段时间,然后有效CTS通知DTE:DCE已经转到发模式,DTE可以开始发送了。在全双工时,RTS和CTS都缺省置为有效即可。然而,在贺氏的MODEM串口定义中,RTS和CTS用于硬件流控,和什么全双工/半双工一点关系也没有。注意,硬件流控是靠软件实现的,之所以强调“硬件”二字,仅仅是因为硬件流控提供了用于流量情况指示的硬件连线,并不是说,你只要把线连上,硬件就能自己流控。如果软件不支持,光连上RTS和CTS是没有用的。RTS和CTS硬件流控的软件算法如下:(RTS有效表示PC机可以收,CTS有效表示MODEM可以收,这两个信号互相独立,分别指示一个方向的流量情况) 。发:当发现(不一定及时发现) CTS (-3v to -15v)无效时,停止发送, 当发现(不一定及时发现) CTS (3v to 15v)有效时,恢复发送;收:当接收buffers中的bytes当接收buffers中的bytes>N 时,给 RTS 无效信号(-3v to -15v);在RS232中本来CTS 与RTS 有明确的意义,但自从贺氏(HAYES ) 推出了聪明猫(SmartModem)后就有点混淆了。在RS232中RTS 与CTS 是用来半双工模式下的方向切换;HAYES Modem中的RTS ,CTS 是用来进 行硬件流控的。通常UART的RTC、CTS 的含义指后者,即用来做硬流控的。如果UART只有RX、TX两个信号,要流控的话只能是软流控;如果有RX,TX,CTS ,RTS 四个信号,则多半是支持硬流控的UART;如果有 RX,TX,CTS ,RTS ,DTR,DSR 六个信号的话,RS232标准的可能性比较大。硬流控的RTS 、CTS :RTS (Require To Send,发送请求)为输出信号,用于指示本设备准备好可接收;CTS(Clear To Send,发送清除)为输入信号,有效时停止发送。假定A、B两设备通信,A设备的RTS 连接B设备的CTS ;A设备的CTS 连接B设备 的RTS 。前一路信号控制B设备的发送,后一路信号控制A设备的发送。对B设备的发送(A设备接收)来说,如果A设备接收缓冲快满的时发出RTS 信号(意思 通知B设备停止发送),B设备通过CTS 检测到该信号,停止发送;一段时间后A设备接收缓冲有了空余,发出RTS 信号,指示B设备开始发送数据。A设备发(B设备接收) 类似。上述功能也能在数据流中插入Xoff(特殊字符)和Xon(另一个特殊字符)信号来实现。A设备一旦接收到B设备发送过来的Xoff,立刻停止发 送;反之,如接收到B设备发送过来的Xon,则恢复发送数据给B设备。同理,B设备也类似,从而实现收发双方的速度匹配。半双工的方向切换:RS232中使用DTR(Date Terminal Ready,数据终端准备)与DSR(Data Set Ready ,数据设备准备好)进行主流控,类似上述的RTS 与CTS 。对半双工的通信的DTE(Date Terminal Equipment,数据终端设备)与DCE(Data circuit Equipment )来说,默认的方向是DTE接收,DCE发送。如果DTE要发送数据,必须发出RTS 信号,请求发送数据。DCE收到后如果 空闲则发出CTS 回 应RTS 信 号,表示响应请求,这样通信方向就变为DTE->TCE,同时RTS 与CTS 信号必须一直保持。从这里可以看出,CTS ,TRS虽 然也有点流控的意思(如CTS 没有发出,DTE也不能发送数据),但主要是用来进行方向切换的。
1.1.2UART传输时序分析
串口传输数据都是一帧数据 11 位,参考图2‑33中的串口时序。
在串口的总线上“高电平”是默认的状态,当一帧数据的开始传输必须先拉低电平,这就是第 0 位的作用。第 0 位过后就是 8 个数据位,这八个数据位才是一帧数据中最有意义的东西。最后的两位是校验位和停止位,作用如同命名般一样。串口传输还有另一个重要参数就是“波特率”。“波特率”在宏观上理解就是串口传输的传输速度;在微观上“波特率”就是串口传输中“一个位的周期”,换句话说亦是“一个位所逗留的时间”。这个概念在后期编写程序是很重要的。常用的波特率有 9600 bps 和 115200 bps ( bit per second )。“9600 bps” 表示每秒可以传输 9600 位。但是经过公式计算“一个位的周期”就会暴露出来。从上述的公式,我们明白一个事实 9600 bps ,传输一位数据占用 0.000104166666666667s 时间。如果是一帧 11 位的数据,就需要0.000104166666666667 x 11 = 0.001145833333333341 / 0.00114583333333334 = 872.727272727268当然这只是在数字上计算出来而已,但是实际上还有许多看不见的延迟因数。= 0.000086805555555555555555555555555556传输一位数据占用 0.000086805555555555555555555555555556s 时间。如果是一帧 11 位的数据,就需要0.000086805555555555556 x 11 = 0.000954861111111111111111111 / 0.00095486111111111111111111 = 1047.27272727272727272727394591741047.2727272727272727272 个帧数据。( 1/115200 ) / (1/50E+6) = 8.68E-6 / 20E-9
1.1.3简单UART传输FPGA实现
图2‑34 FPGA发送一帧串口数据(考虑波特率)如果图2‑34考虑 115200 的波特率,结果如图2‑34所示,每一位数据都保持 434 个时钟,为此 Verilog 可以这样表示,如代码2‑11所示:
3.always @( posedge CLOCK)5. 0,1,2,3,4,5,6,7,8,9,10:6. if( C1 == 9’ d434 -1 ) begin C1 <= 9’ d0; i <= i + 1’ b1; endelse begin TXD <= D1[i]; C1 <= C1 + 1'b1; end
|
如代码2‑11所示,步骤 1~8 不再保持一个时钟,换之每个步骤都保持 434 个时钟,因此每位 TXD 的发送数据也保持 8.68us。除此此外,串口传输协议不仅可以自定义波特率,串口传输协议也可以自定义一帧数据的位宽,自定义内容如表2‑8所示:
如表2‑8所示,可以自定义的数据其中便包含数据位,默认下为 1 字节,自定义内容则是 5~9 位,校验位也可以设置为有或者无( 默认下是有),停止位也可以增至 2 位(默认下是 1 位)。如图2‑35所示,该模块的左方有问答信号,还有 8 位的 iData,至于右方则是 TXD 顶层信号。此外,一帧数据的波特率为 115200 bps。
1.//****************************************************************************//3.//# @Date: 2019-04-21 21:14:514.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-08-04 02:48:178.//# @Modification History: 2014-05-10 13:42:279.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2014-05-10 13:42:2712.//# ========================================================================= #15.//****************************************************************************//24. parameter B115K2 = 9'd434; // formula : ( 1/115200 )/( 1/50E+6 )32. always @( posedge CLOCK or negedge RESET )45. begin D1 <= { 2'b11 , iData , 1'b0 }; i <= i + 1'b1; end47. 1,2,3,4,5,6,7,8,9,10,11:48. if( C1 == B115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end49. else begin rTXD <= D1[i - 1]; C1 <= C1 + 1'b1; end52. begin isDone <= 1'b1; i <= i + 1'b1; end55. begin isDone <= 1'b0; i <= 4'd0; end60. assign oDone = isDone;
|
第16~24行为相关的出入端声明,第 9 行则是波特率为 115200 的常量声明。第41~62行为部分核心操作。第 41 行的 if( iCall ) 表示该模块不使能就不工作。步骤 0 用来准备发送数据,其中 2’b11 是停止位与校验位(随便填),1’b0 则是起始位。步骤 1~11用来发送一帧数据。步骤 12~13 用来反馈完成信号并返回步骤。由上图可知,串口发送模块是“定时发送”的过程。波特率模块产生的时间间隔是通过计数器实现的,由图2‑36可知,每隔一定时间波特率模块就会产生一个高脉冲给TX_Pin_Out引脚。对于FPGA实现UART的RX模块功能主要就是电平采集。那么它到底是如何实现采集的呢?说到底,在发送模块中一位数据发送的时间间隔是通过波特率进行控制的,同理,在接受模块中采集一位数据的间隔同样也要通过波特率进行控制,具体如上图所示。上图中,数据采集都是在“每位数据的中间”进行着。RX_Pin_In 输入一帧数据,当检测到低电平(起始位), 在第 0 位数据,采取忽略的态度,然后接下来的 8 位数据位都被采集,最后校验位和停止位,却是采取了忽略的操作。有一点必须好好注意,串口传输数据“从最低位开始,到最高位结束”。因为 Verilog 无法描述理想以外的时序, 对此所有时序活动都必须看成理想时序。图2‑39 FPGA接收一帧波特率为115200的数据当FPGA接收一帧数据为波特率115200之际,情况差不多如图2‑39所示。50Mhz是FPGA的时钟源,也是一帧数据的采集时钟, RXD 则是一帧数据的输入端。波特率为 115200的一位数据经过 50Mhz 的时钟量化以后,每一位数据大约保持 8.68us,即 434 个时钟。串口传输没有自己的时钟信号,所以我们必须利用 FPGA 的时钟源“跟踪”每一位数据。对此, FPGA 只能借用计数器“同步跟踪”而已,至于 Verilog 则可以这样描述,结果如代码2‑13所示:
1.0,1,2,3,4,5,6,7,8,9,10: //同步跟踪中 ...2. if( C1 == 434 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end
|
如代码2‑13所示,所谓同步跟踪,就是利用计数器估计每一位数据 ... 期间,步骤 0~10表示每一位数据,至于 C1 计数 434 个时钟则是同步跟踪中。其中-1考虑了步骤之间的跳转所耗掉的时钟。知道串口的一帧数据都是从拉低的起始位开始,然而为了完美尾行,亦即实现精密控时,起始位的读取往往都是关键。如图2‑40所示,当我们在第一个时钟读取(采集)起始位的时候,由于 Verilog 的读取只能经过读取过去值而已,余下起始位还有 433 个时钟需要我们跟踪,为此 Verilog 可以这样描述,结果如代码2‑14所示:
2. if( RXD == 1'b0 ) begin i <= i + 1'b1; C1 <= C1 + 4'd1; end4. if( C1 == BPS115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end
|
如代码2‑14所示,步骤 0 用来检测起始位,如果 RXD 的电平为拉低状态, C1 立即递增以示同步跟踪已经用掉一个时钟,同样也可以看成 i 进入下一个步骤用掉一个时钟。然而步骤 1 是用来跟踪余下的 433 个时钟,但是计数器 C1 不是从 0 开始计数,而是从 1开始计算,因为 C1 在步骤已经递增的缘故。一帧数据的跟踪结果与读取结果如图2‑41所示 ... 除了起始位,我们使用了两个步骤采集并跟踪之余,接下来便用 8 个步骤数据一边跟踪一边采集所有数据位,然而采集的时候则是 1/4 周期,即每位数据的第 108 个时钟。最后的校验位及结束位则是跟踪而已。对此, Verilog 可以这样表示,结果如代码2‑15所示:
2.if( RXD == 1'b0 ) begin i <= i + 1'b1; C1 <= C1 + 4'd1; end5.if( C1 == BPS115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end8.2,3,4,5,6,7,8,9: //stalk and count 1~8 data's bit , sample data at 1/2 for bps10.if( C1 == SAMPLE ) D1[i-2] <= RXD;11. if( C1 == BPS115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end12. else C1 <= C1 + 1'b1;15.10,11: // parity bit & stop bit16.if( C1 == BPS115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end20.begin isDone <= 1'b1; i <= i + 1'b1; end23.begin isDone <= 1'b0; i <= 4'd0; end
|
如代码2‑15所示,步骤 0~1 用来采集与跟踪起始位,步骤 2~9 则用来跟踪数据位,并且采集为 1/4 周期。步骤 10~11 则用来跟踪校验位于结束位。图2‑42是 RX 功能模块的建模图,左方链接至顶层信号 RXD,右方则是问答信号还有 8位的 oData。
1.//****************************************************************************//3.//# @Date: 2019-04-21 22:46:154.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-08-04 03:21:388.//# @Modification History: 2014-05-10 13:44:289.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2014-05-10 13:44:2812.//# ========================================================================= #15.//****************************************************************************//24. parameter BPS115K2 = 9'd434, SAMPLE = 9'd108;31. always @ ( posedge CLOCK or negedge RESET )43. if( RXD == 1'b0 ) begin i <= i + 1'b1; C1 <= C1 + 4'd1; end46. if( C1 == BPS115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end47. else C1 <= C1 + 1'b1;49. 2,3,4,5,6,7,8,9: //stalk and count 1~8 data's bit , sample data at 1/2 for bps51. if( C1 == SAMPLE ) D1[i-2] <= RXD;52. if( C1 == BPS115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end53. else C1 <= C1 + 1'b1;56. 10,11: // parity bit & stop bit57. if( C1 == BPS115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end58. else C1 <= C1 + 1'b1;61. begin isDone <= 1'b1; i <= i + 1'b1; end64. begin isDone <= 1'b0; i <= 4'd0; end68. assign oDone = isDone;
|
第 16~24 行是相关的出入端声明,第 24 行则是波特率为 115200 的常量声明,其外还有采集的周期。第 26~38 行是相关的寄存器声明,第 32~38 行则是这些寄存器的复位操作。第 39~46 行是核心操作。第 39 行的 if( iCall ) 表示该模块不使能便不工作。步骤 0~1 用来判断与跟踪起始位;步骤 2~9 用来跟踪并且读取当中的数据位;步骤 10 至 11 则是用来跟踪校验位与停止位而已。步骤 12~13 则用来反馈完成信号,以示一次性的接收工作已经完成。
1.1.4实用UART传输FPGA实现
上一节设计实现的UART只是简易的实现,没考虑诸如抖动(起始位抖动会导致数据位传输或接收错误)等问题,但是对于理解UART传输协议却很有帮助。在单片机中使用时一般串口都可以进行大量数据的传输,这得益于单片机在串口传输时会有“缓存”空间用于数据的存储,下面来看看单片机内部串口结构,如图2‑43所示:所以本节设计串口时会参考单片机内部串口结构,主要是将上一节的UART功能拆解并完善部分功能,其中缓冲部分会在后面章节介绍完FIFO(First Input First Output)后,在将最终版本的UART进行封装,但是本节的串口设计也可以单独使用,也具有实用性。在进行实用型UART设计之前,需要先了解在FPGA中怎么进行电平检测。在FPGA中最常见的就是利用“阻塞”“非阻塞”赋值语句进行电平检测,在一些特定场合还需要利用延迟达到精确检测的目的。
为了更好地理解“阻塞”“非阻塞”赋值要点,我们需要对Verilog语言中的阻塞赋值和非阻塞赋值的功能和执行时间上的差别有深入的理解。我们定义下面的两个关键字:RHS——方程式右手方向的表达式或变量可分别缩写成 RHS表达式或RHS变量;LHS ——方程式左手方向的表达式或变量可分别缩写成LHS 表达式或LHS变量。IEEE Verilog标准定义了有些语句有确定的执行时间,有些语句没有确定的执行时间。若有两条或两条以上的语句准备在同一时间执行,但由于语句的排列顺序不同,却产生了不同的输出结果。这就是造成Verilog模块冒险和竞争的原因。为了避免产生竞争,理解阻塞和非阻塞赋值在执行时间上的差别是至关重要的。阻塞赋值用等号(=)表示。为什么称这种赋值为阻塞赋值呢?因为在赋值时先计算RHS部分的值,这是赋值语句不允许任何别的Verilog语言的干扰,直到现行的赋值完成时刻,即把RHS赋值给LHS的时刻,它才允许别的赋值语句的执行。一般可综合的赋值操作在RHS不能设定延时(即使是0延时也不允许)。从理论上讲,它与后面的赋值语句只有概念上的先后,而无实质的延迟。若在RHS上加延迟,则在延迟时间会阻止赋值语句的执行,延迟后才进行赋值,这种赋值语句是不可综合的,在需要综合的模块设计中不可使用这种风格的代码。所谓阻塞的概念是指在同一个always块中,其后面的赋值语句从概念上是在前一句赋值语句结束之后再开始赋值的。非阻塞赋值用小于等于号(<=)表示。为什么称这种赋值为非阻塞赋值呢?因为在赋值开始时计算RHS表达式,赋值操作时刻结束时更新LHS。在计算非阻塞赋值的RHS表达式和更新LHS期间,其他的Verilog语句,包括其他的非阻塞赋值语句都可能计算RHS表达式和更新LHS。非阻塞赋值允许其他的Verilog语句同时进行操作。非阻塞赋值可以看作两个步骤的过程:非阻塞赋值操作只能用于对寄存器类型变量进行赋值,因此只能用在“initial”块和“always”块等过程块中,而非阻塞赋值不允许用于连续赋值。3)用always块建立组合逻辑模型时,用阻塞赋值;4)在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值;5)在同一个alway块中,不要即用非阻塞又用阻塞赋值;6)不要在一个以上的always块中为同一个变量赋值;7)用$strobe系统任务来显示用非阻塞赋值的变量值;9)在描述组合逻辑的always块中用阻塞赋值,则综合成组合逻辑的电路结构;10)在描述时序逻辑的always块中用非阻塞赋值,则综合成时序逻辑的电路结构。
|
图2‑44中是最基本的门电路设计的上升沿、下降沿捕获电路。由图可知,Trigger作为外部触发信号的输入,通过FPGA内部的clk与rst_n全局时钟信号,同步寄存输出。上升沿与下降沿的边沿捕获信号与当前的输入信号(Trigger)、上一时刻的寄存信号(Trigger_r)有关,主要关系如表2‑9所示。
边沿检测的实现很好理解,当上一时刻为低电平,而当前时刻为高电平时,显而易见这是外部信号的上升沿;反之,当上一时刻为高电平,而当前时刻为低电平时,为外部信号的下降沿。与上述原理图中使用了2输入与门,通过反相器的辅助,作为2个时钟信号的对比。可以采用逆向思维进行推断;当pos_edge为高,即捕获到上升沿时,2输入与门输入了2个“1”,由反相器可知D触发器寄存输出后的信号为0,而当前信号为1。结果分析与设计的一样。neg_edge的设计也是如此。外部输入的信号有效时,会保持一段时间的高电平,但FPGA不能通过判断高电平使能信号去进行逻辑分析。由于在FPGA中不便于处理类似的触发信号(除非外部输入信号作为全局时钟使用),所以通过边沿采样技术实现上升沿时刻的使能信号的捕捉,进而实现外部信号的上升沿触发功能。一级的D触发器寄存在比较时,前一时刻的信号已经同步到同一时钟域,而当前时刻直接从外部输入,与FPGA整体逻辑电路不再同一时钟域。但希望我们的设计能够全部通过同步电路设计,以提高系统的可靠性,因此可以采用2级D触发器作为信号的寄存,同时比较第一级与第二级D触发器输出的信号,来检测外部输入信号的上升沿或下降沿。根据原理图设计的电路如图2‑45所示。图2‑45 二级寄存器实现的上升沿、下降沿捕获电路
3.CLK, RSTn, Pin_In, H2L_Sig, L2H_Sig11./**********************************/12.// 开发板使用的晶振为 50MHz, 50M*0.0001-1=4_99913.parameter T100US = 11'd4999;14./**********************************/17.always @ ( posedge CLK or negedge RSTn )23.else if( Count1 == T100US )26. Count1 <= Count1 + 1'b1;27./********************************************/32.always @ ( posedge CLK or negedge RSTn )47./***********************************/48.assign H2L_Sig = isEn ? ( H2L_F2 & !H2L_F1 ) : 1'b0;49.assign L2H_Sig = isEn ? ( !L2H_F2 & L2H_F1 ) : 1'b0;50./***********************************/
|
第 13 行定义了 100us 的常量,而第 17~26 行是用于延迟 100us。因为电平检测模块是非常敏感,在复位的一瞬间,电平容易处于不稳定的状态,我们需要延迟 100us。isEn = 1 寄存器是表示 100us 的延迟已经完成( 26 行)。第 28~31 行,声明了四个寄存器。H2L_F1, H2L_F2,是针对检测电平由高变低。相反的 L2H_F1, L2H_F2,则是针对检测电平由低变高。在 35~38 行,对各个寄存器初始化了,由于 H2L_Fx 是为了检测由高变低的电平,所以初始化为逻辑 1。L2H_Fx 是为了检测由低变高的电平,初值被设置为逻辑 0。
8.Pin_Out = H2L_F2 & !H2L_F1
|
上面代码是用来检测电平由高变低。 H2L_F1 和 H2L_F2 的初值都是逻辑 1。假设第一个时间 Pin_In 为低电平, H2L_F1 就会被赋值位逻辑 0,而 H2L_F1 则是被赋值为 H2L_F1上一个时间的值(也就是 H2L_F1 的初值)。我们知道在第一个时间, H2L_F1 为逻辑 0, H2L_F2 位逻辑 1。由于对 H2L_F1 的取反操作, H2L_F1 与 H2L_F2“求与”运算,所以这个表达式的输出是逻辑 1。再假设第二个时间 Pin_In 保持为低电平, H2L_F1 同样会被赋值为逻辑 0, 而 H2L_F2则是被赋值为 H2L_F1 上一个时间的值,亦即逻辑 0 (第一个时间的值)。再经过布尔表达式的运算, 在第二个时间, H2L_F1 是逻辑 0, H2L_F2 是逻辑 0, 所以输出的结果是逻辑 0.
|
|
|
Pin_Out = ( !H2L_F1 ) & H2L_F2
|
|
|
|
|
|
|
|
|
|
|
|
|
第 41~56 行,正是执行如上的操作,无论是检测电平由高变低或者由低变高,思路基本都是一样。而最后的 48~49 行,是关于“电平检测”的布尔表达式。但是有一点不同的是,Pin_Out 的输出,是发生在 100us 之后,因为 100us 之前被 isEn 寄存器所限制(原因之前已经说了)。换一句话说,电平检测模块的有效是发生在 100us 的延迟之后。发送模块的时序及相关介绍前面都已经写的很清楚了,就是将波特率模块分开,这样方便后期维护及修改,同时也为了下一节接收模块的设计做统一处理。图2‑47 发送模块(tx_module.v)整体结构图首先,有一个顶层模块(tx_module.v),这个模块会包含(例化)成下面的两个子模块(bps_module.v和tx_control_module.v)。bps_module.v波特率产生模块,简单说就是分频模块。tx_control_module.v发送控制模块,就是利用状态机(简化状态机)进行发送开始位、数据位和停止位。表2‑11 发送模块(tx_module.v)模块间信号定义
|
|
|
|
|
tx_bps_module.v模块传输给tx_control_module.v
|
由波特率计算得出的时钟传输给发送控制模块,用于传输数据位。
|
|
与TX_En_Sig引脚连接,送给tx_bps_module.v模块
|
TX_En_Sig 拉低的时候,tx_bps_module.v是处于随眠的状态。一旦 TX_En_Sig 拉高电平,那么 tx_bps_module.v 就开始计数
|
|
|
|
|
|
表2‑12 发送模块(tx_module.v)顶层模块信号定义
|
|
|
|
|
|
当 TX_En_Sig 拉高电平,同时tx_bps_module.v 也 会 开 始 计 数,tx_control_module.v就会按照tx_bps_module.v给的时钟往TX_Pin_Out传输数据(TX_Data)。
|
|
|
|
|
|
|
|
|
|
|
|
|
从根本上说,tx_module.v 是实现“定时发送”的功能。假设我配置的波特率为9600 bps,那么每隔0.000104166666666667s bps_module.v 这个模块就会产生一个高脉冲给tx_control_module.v,该控制模块会按这个节拍将数据一位一位的数据发送出去。假设配 置的波特率为115200 bps,那么每隔0.000086805555555555555555555555555556s tx_bps_module.v 这个模块就会产生一个高脉冲给tx_control_module.v,该控制模块会按这个节拍将数据一位一位的数据发送出去。一帧数据有 11 位,那么 bps_module.v 需要产生 12 次定时。首先看下bps_module.v模块,波特率产生模块设计和之前任意波形发生器一样。bps_module.v 是作为“定时”的功能。当 TX_En_Sig/En_Sig 拉低的时候,它是处于随眠的状态。一旦 TX_En_Sig/En_Sig 拉高电平,那么 bps_module.v 就开始计数。然后定时产生一个高脉冲经 BPS_CLK 给 tx_control_module.v。整体结构如下:图2‑48 波特率产生模块(bps_module)整体结构图表2‑13 波特率产生模块(bps_module)引脚说明
按照1.5.5任意分频器设计进行bps_module模块设计。第一步,相位计数器-32位K步计数器,如代码2‑19所示。
1.//------------------------------------------------------2.//RTL1: Precise fractional frequency for uart bps clock4.always@(posedge CLOCK or negedge RST_n)12.//------------------------------------------------------
|
代码2‑19中,在全局时钟CLOCK驱动下,进行K步计数,cnt为0~232-1的地址。BPS_CNT是一个16倍波特率(16*115200)的宏定义的分频参数,方便例化或者维护。在第一步完成了0~232-1寻址后,需要对地址进行比较、划分,得到一个方波信号。如代码2‑20所示。
1.//RTL2: Equal division of the Frequency division clock3.always@(posedge CLOCK or negedge RST_n)9. if(cnt < 32'h7FFF_FFFF)16.//------------------------------------------------------
|
代码2‑20中,第9行,32‘h7FFF_FFFF为232-1的中点,因此它可以作为一种“门限电压”来实现合成频率的方波输出。主要利用“边沿检测技术”生成使能时钟信号,如代码2‑21所示。
1.//RTL3: Generate enable clock for clock3.always@(posedge CLOCK or negedge RST_n)8. cnt_equal_r <= cnt_equal;11.assign BPS_CLKen = (~cnt_equal_r & cnt_equal) ? 1'b1 : 1'b0;12.assign BPS_CLK = cnt_equal_r;
|
通过边沿检测技术,捕获了合成时钟的上升沿作为时钟使能信号,同时输出cut_equel_r信号作为分频时钟BPS_CLK。代码2‑22 波特率产生模块(bps_module)完整代码
1.//****************************************************************************//3.//# @Date: 2019-05-03 01:32:404.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-06 21:22:188.//# @Modification History: 2019-05-06 21:22:189.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-06 21:22:1812.//# ========================================================================= #15.//****************************************************************************//16./***********************************************************************17. fc : Refrence clock = 50MHz = 50*10^621.100MHz K = fo*(2^32)/fc = fo*(2^32)/(100*10^6) = 42.94967296 * fo22.50MHz K = fo*(2^32)/fc = fo*(2^32)/(50*10^6) = 85.89934592 * fo23.***********************************************************************/27. //BPS_CNT = 85.89934592 * fo for 50Mhz28.// parameter BPS_CNT = 32'd21990233 //256000bps 2199023329.// parameter BPS_CNT = 32'd10995116 //128000bps 1099511630.// parameter BPS_CNT = 32'd9895605 //115200bps 989560531. parameter BPS_CNT = 32'd824634//9600bps 82463432. //BPS_CNT = 42.949 67296 * fo for 100Mhz33.// parameter BPS_CNT = 32'd175921860 //256000bps34.// parameter BPS_CNT = 32'd87960930 //128000bps35.// parameter BPS_CNT = 32'd79164837 //115200bps36.// parameter BPS_CNT = 32'd412317 //9600bps50./********************************/52.//------------------------------------------------------53.//RTL1: Precise fractional frequency for uart bps clock55.always@(posedge CLOCK or negedge RST_n)60. cnt <= cnt + BPS_CNT;63.//------------------------------------------------------64.//RTL2: Equal division of the Frequency division clock66.always@(posedge CLOCK or negedge RST_n)72. if(cnt < 32'h7FFF_FFFF)79.//------------------------------------------------------80.//RTL3: Generate enable clock for clock82.always@(posedge CLOCK or negedge RST_n)87. cnt_equal_r <= cnt_equal;90.assign BPS_CLKen = (~cnt_equal_r & cnt_equal) ? 1'b1 : 1'b0;91.assign BPS_CLK = cnt_equal_r;
|
波特率产生后会将时钟信号送给tx_control_module模块,模块的整体结构如下:图2‑49 发送控制模块(tx_control_module)整体结构图表2‑14 发送控制模块(tx_control_module)引脚说明
tx_control_module.v 控制模块是最为中心的一部分,当 TX_En_Sig 拉高电平,同时间bps_module.v 也会开始计数。tx_control_module.v 将 TX_Data 的值,按bps_module.v 产生的定时,有节奏的往 TXD 发送。当一帧数据发送完毕后,就反馈一个 TX_Done_Sig 的高脉冲。代码2‑23 发送控制模块(tx_control_module)完整代码
1.//****************************************************************************//3.//# @Date: 2019-05-04 00:47:084.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-05 22:30:218.//# @Modification History: 2019-05-05 22:30:219.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-05 22:30:2112.//# ========================================================================= #15.//****************************************************************************//16./**************************************************************************17...IDLE...Start...............UART DATA........................End...IDLE...18.________ ______________19. |____< D0 >< D1 >< D2 >< D3 >< D4 >< D5 >< D6 >< D7 >20. Bit0 Bit1 Bit2 Bit3 Bit4 Bit5 Bit6 Bit7 Bit8 Bit921.**************************************************************************/22.module tx_control_module43. /********************************************************/49. always @ ( posedge CLOCK or negedge RST_n )60. if( BPS_CLK ) begin i <= i + 1'b1; rTX <= 1'b1; end62. if( BPS_CLK ) begin i <= i + 1'b1; rTX <= 1'b0; end64. 4'd2, 4'd3, 4'd4, 4'd5, 4'd6, 4'd7, 4'd8, 4'd9 :65. if( BPS_CLK ) begin i <= i + 1'b1; rTX <= TX_Data[ i - 2 ]; end68. if( BPS_CLK ) begin i <= i + 1'b1; rTX <= 1'b1; end71. if( BPS_CLK ) begin i <= i + 1'b1; rTX <= 1'b1; end74. if( BPS_CLK ) begin i <= i + 1'b1; isDone <= 1'b1; end77. begin i <= 4'd0; isDone <= 1'b0; end83. /********************************************************/86. assign TX_Done_Sig = isDone;88. /*********************************************************/
|
代码2‑23中的设计的核心思想是状态机,状态机部分在1.6节已经介绍过了,只不过在写代码的时候将状态机精简。当 TX_En_Sig 被拉高(36行)该控制模块就会开始工作了(同时间bps_module.v 也会开始计数)。每当bps_module.v 产生一个定时的高脉冲 BPS_CLKen, tx_control_module.v 都会发送一位数据。第 0 位数据是起始位,所以 rTX 寄存器被赋值与逻辑 0(40 行)。接下来的八位都是数据位, tx_control_module.v 按 TX_Data 的值从最低位到最高位,将数据赋值给 rTX 寄存器(43 行)。最后两位数据则是校验位和停止位,如果没有什么特别需求,就随便填上逻辑 1 就行了(41~45 行)。最后产生一个 TX_Done_Sig 的高脉冲(51~55 行)。在 62 行 TX_Done_Sig 的输出是被 isDone 这个标志寄存器驱动着,然而 TXD的输出是由 rTX 这个寄存器驱动。接下来是将上两个模块进行例化,完成整个发送模块的设计。电路的整体结构如图2‑49所示。顶层模块只需要按照整体结构图进行“连线”就可以了。代码如下:代码2‑24 发送顶层模块(tx_module.v)完整代码
1.//****************************************************************************//3.//# @Date: 2019-05-03 02:18:374.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-06 22:13:078.//# @Modification History: 2019-05-06 22:13:079.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-06 22:13:0712.//# ========================================================================= #15.//****************************************************************************//32./********************************/39. //BPS_CNT = 85.89934592 * fo for 50Mhz40.// .BPS_CNT(32'd21990233) //256000bps41.// .BPS_CNT(32'd10995116) //128000bps42.// .BPS_CNT(32'd9895605) //115200bps43. .BPS_CNT(32'd824634) //9600bps * 16 82463444. //BPS_CNT = 42.949 67296 * fo for 100Mhz45.// .BPS_CNT(32'd10995116) //256000bps 1099511646.// .BPS_CNT(32'd5497558) //128000bps 549755847.// .BPS_CNT(32'd4947802) //115200bps 494780248.// .BPS_CNT(32'd412317) //9600bps 41231754. .En_Sig( TX_En_Sig ), // input - from U255. .BPS_CLK( BPS_CLOCK ), // output - to X56. .BPS_CLKen( BPS_CLOCKen ) // output - to U258./*********************************/60.tx_control_module U2_tx_control_module64. .TX_En_Sig( TX_En_Sig ), // input - from top65. .TX_Data( TX_Data ), // input - from top66. .BPS_CLK( BPS_CLOCKen ), // input - from U267. .TX_Done_Sig( TX_Done_Sig ), // output - to top68. .TXD( TXD ) // output - to top70./***********************************/
|
上述代码的ModelSim仿真和2.3.3节仿真代码一样,结果如下:为了测试代码的实用性及使用方法,编写代码2‑25。
1.//****************************************************************************//3.//# @Date: 2019-05-03 02:18:374.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-06 21:38:048.//# @Modification History: 2019-05-06 21:38:049.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-06 21:38:0412.//# ========================================================================= #15.//****************************************************************************//26./***************************/30./****************************/40. .TX_Done_Sig( DoneU1 ),44./*******************************/49./**************************/51. parameter T1S = 26'd49_999_999;53./***************************57.always @ ( posedge CLOCK or negedge RST_n )60. else if( Count_Sec == T1S )63. Count_Sec <= Count_Sec + 1'b1;65./********************************68.always @ ( posedge CLOCK or negedge RST_n )79. else if( Count_Sec == T1S )81./******************************/84./******************************/86.always @ ( posedge CLOCK or negedge RST_n )97. if( DoneU1 ) begin isTX <= 1'b0; i <= i + 1'b1; end98. else begin isTX <= 1'b1; rData <= 8'hAB; end101. if( DoneU1 ) begin isTX <= 1'b0; i <= i + 1'b1; end102. else begin isTX <= 1'b1; rData <= 8'hCD; end105. if( DoneU1 ) begin isTX <= 1'b0; i <= i + 1'b1; end106. else begin isTX <= 1'b1; rData <= 8'hEF; end112./******************************/
|
串口发送模块是“定时发送”的功能,那么串口接收模块就是“定时采集”的功能,这里的“定时”都是指波特率,“发送”就是按照“定时”的时间将数据一位一位的送到TXD引脚,那么“采集”呢?所谓的采集就是采集RXD引脚上的电平。表2‑15 接收模块(rx_module.v)模块间信号定义
|
|
|
|
|
|
由电平检测模块检测到输入电平由高到低输出标志传输给接收控制模块,用于后续的数据处理。
|
|
|
|
|
|
当rx_control_module.v拉高Count_Sig, bps_module.v经BPS_CLK对 rx_control_module.v
|
|
|
|
|
|
|
表2‑16 接收模块(rx_module.v)顶层模块信号定义
|
|
|
|
|
|
当 RX_En_Sig 拉高,整个模块就开始工作,它将采集来自 RX_Pin_In 的数据,当完成一帧数据接收的时候,就会产生一个高
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
数据采集都是在“每位数据的中间”进行着(这个时候数据是最稳定的)。在上图中 RXD 输入一帧数据,当 detect_module.v 检测到低电平(起始位), rx_control_module.v 和 rx_bps_module.v 就产生定时(与 RX_Pin_In的波特率是一致)。然而 rx_bps_module.v 产生的定时是在每个位时间的中间。在第 0 位数据,采取忽略的态度,然后接下来的 8 位数据位都被采集,最后校验位和停止位,却是采取了忽略的操作。有一点你必须好好注意,串口传输数据“从最低位开始,到最高位结束”。
|
|
|
|
|
detect_module.v模块输入是连结物理引脚 RXD,主要实现异步数据的同步逻辑及检测一帧数据的第 0 位,也就是起始位,然后产生一个高脉冲经 H2L_Sig 给 rx_control_module.v ,以表示一帧数据接收工作已经开始。由于PC与FPGA端数据不同步,为了保证数据的同步,数据进入FPGA之后,首先需要将异步数据进行同步逻辑处理,使用的方式就是简单的D触发器时序电路,如下:
1./******************************/2.//sync the rxd data: rxd_sync4.always@(posedge CLOCK or negedge RST_n)12./******************************/
|
代码2‑27 detect_module.v模块代码
1.//****************************************************************************//3.//# @Date: 2019-05-12 16:32:524.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-12 20:00:338.//# @Modification History: 2019-05-12 20:00:339.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-12 20:00:3312.//# ========================================================================= #15.//****************************************************************************//28. /******************************/29. //sync the rxd data: rxd_sync31. always@(posedge CLOCK or negedge RST_n)39. /******************************/44. always @ ( posedge CLOCK or negedge RST_n )56. /***************************************/58. assign H2L_Sig = H2L_F2 & !H2L_F1;60. /***************************************/
|
代码2‑27,和2.3.4.2节非常相似,针对性的修改了几个参数,理解起来应该不难。三、rx_control_module.v模块设计rx_control_module.v 是核心控制模块。针对串口的配置主要是 1 帧 11 位的数据,重视八位数据位,无视起始位,校验位和结束位。当 RX_En_Sig 拉高,这个模块就开始工作,它将采集来自 RXD 的数据,当完成一帧数据接收的时候,就会产生一个高脉冲给 RX_Done_Sig。代码前端也需要加入异步逻辑数据的同步逻辑。代码2‑28 rx_control_module.v模块代码
1.//****************************************************************************//3.//# @Date: 2019-05-12 16:32:524.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-12 20:00:328.//# @Modification History: 2019-05-12 20:00:329.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-12 20:00:3212.//# ========================================================================= #15.//****************************************************************************//16.module rx_control_module42. /********************************************************/43. //sync the rxd data: rxd_sync45. always@(posedge CLOCK or negedge RST_n)54. /********************************************************/61. always @ ( posedge CLOCK or negedge RST_n )73. if( H2L_Sig ) begin i <= i + 1'b1; isCount <= 1'b1; end76. if( BPS_CLK ) begin i <= i + 1'b1; end78. 4'd2, 4'd3, 4'd4, 4'd5, 4'd6, 4'd7, 4'd8, 4'd9 :79. if( BPS_CLK ) begin i <= i + 1'b1; rData[ i - 2 ] <= rxd_sync; end82. if( BPS_CLK ) begin i <= i + 1'b1; end85. if( BPS_CLK ) begin i <= i + 1'b1; end88. begin i <= i + 1'b1; isDone <= 1'b1; isCount <= 1'b0; end91. begin i <= 4'd0; isDone <= 1'b0; end95. /********************************************************/97. assign BPS_En_Sig = isCount;98. assign RX_Data = rData;99. assign RX_Done_Sig = isDone;102. /*********************************************************/
|
在69~91 行,是 rx_control_module.v 的核心控制功能。在 69 行,表示了如果 RX_En_Sig如果没有拉高,这个模块是不会工作。(在 58 行定义了 isCount 标志寄存器,为了使能bps_module.v 输出采集定时信号)当 rx_control_module.v 模块被使能,该模块就会处于就绪状态,一旦 detect_module.v 检查到由高变低的电平变化(73 行),会使步骤 i 进入第 0 位采集,然而 isCount 标志寄存器同时也会被设置为逻辑 1, bps_module.v便会开始产生波特率的定时。bps_module.v 产生的定时是在“每位数据的中间”。在 75~76 行,第一次的定时采集时第 0 位数据(起始位),保持忽略态度。在 78~79 行,定时采集的是八位数据位,每一位数据位会依低位到最高位储存入 rData 寄存器。81~85 行,是最后两位的定时采集(校验位,停留位),同时采取忽略的态度。当进入87~91 行,这表示一帧数据的采集工作已经结束。最后会产生一个完成信号的高脉冲,同时间 isCount 会被设置为逻辑 0,亦即停止bps_module.v 的操作。至于 74~76 行, isCount 标志寄存器是 BPS_En_Sig 输出的驱动器, rData 寄存器是RX_Data 输出的驱动器,最后 isDone 标志寄存器是 RX_Done_Sig 输出的驱动寄存器。接下来是将上几个模块进行例化,完成整个接收模块的设计。电路的整体结构如图2‑52所示。顶层模块只需要按照整体结构图进行“连线”就可以了。代码如下:
1.//****************************************************************************//3.//# @Date: 2019-05-12 16:32:524.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-12 20:22:338.//# @Modification History: 2019-05-12 20:22:339.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-12 20:22:3312.//# ========================================================================= #15.//****************************************************************************//35. /**********************************/43. .RXD( RXD ), // input - from top44. .H2L_Sig( H2L_Sig ) // output - to U347. /**********************************/54. //BPS_CNT = 85.89934592 * fo for 50Mhz55. // .BPS_CNT(32'd21990233) //256000bps56. // .BPS_CNT(32'd10995116) //128000bps57. // .BPS_CNT(32'd9895605) //115200bps58. .BPS_CNT(32'd824634) //9600bps * 16 82463459. //BPS_CNT = 42.949 67296 * fo for 100Mhz60. // .BPS_CNT(32'd10995116) //256000bps 1099511661. // .BPS_CNT(32'd5497558) //128000bps 549755862. // .BPS_CNT(32'd4947802) //115200bps 494780263. // .BPS_CNT(32'd412317) //9600bps 41231769. .En_Sig( BPS_En_Sig ), // input - from U270. .BPS_CLK( BPS_CLK ), // output - to X71. .BPS_CLKen( BPS_CLKen ) // output - to U274. /**********************************/83. .H2L_Sig( H2L_Sig ), // input - from U184. .RX_En_Sig( RX_En_Sig ), // input - from top85. .RXD( RXD ), // input - from top86. .BPS_CLK( BPS_CLKen ), // input - from U288. .BPS_En_Sig( BPS_En_Sig ), // output - to U289. .RX_Data( RX_Data ), // output - to top90. .RX_Done_Sig( RX_Done_Sig ) // output - to top94. /************************************/
|
上述代码的ModelSim仿真和2.3.3节仿真代码一样,结果如下:为了测试代码的实用性及使用方法,建立测试代码的模型。如下图所示:建立如上图 rx_module_demo.v 组合模块 ,它里边包含了一个控制模块对rx_module.v 的调用。一开始 control_module.v 会拉高 RX_En_Sig 使能 rx_module.v 。当有一阵数据经 RXD 传入,rx_module.v 就会接收,然后过滤后的数据出输出致 RX_Data , 然后再产生一个高脉冲给 RX_Done_Sig。当 control_module.v 接收到RX_Done_Sig 的高脉冲,拉低 RX_En_Sig,并且将 RX_Data 的“前四位”输出致 4 位LED 资源。然后过程再一次重复不断。代码2‑30 control_module.v实例代码
1.//****************************************************************************//3.//# @Date: 2019-05-12 21:11:184.//# @Last Modified by: zlk5.//# @WeChat Official Account: OpenFPGA6.//# @Last Modified time: 2019-05-12 21:26:448.//# @Modification History: 2019-05-12 21:26:449.//# Date By Version Change Description:10.//# ========================================================================= #11.//# 2019-05-12 21:26:4412.//# ========================================================================= #15.//****************************************************************************//30. output [7:0]Number_Data;32. /******************************/37. always @ ( posedge CLOCK or negedge RST_n )40. else if( RX_Done_Sig )41. begin rData <= RX_Data; isEn <= 1'b0; end44. /*********************************/46. assign Number_Data = rData;47. assign RX_En_Sig = isEn;49. /*********************************/
|
在 37~49 行就是这个控制模块的核心功能了。一开始的时候(42 行)就将 isEn 设置为逻辑 1, 这个标志寄存器在 47 行驱动着 RX_En_Sig。换句话说,此时的 rx_module.v 已经进入就绪状态,然后 control_module.v 等待着 RX_Done_Sig 反馈(40 行)。一旦一帧数据接收完毕, RX_Done_Sig 就会产生高脉冲,然后 rData 就会寄存 RX_Data的值。同时间 isEn 被设置为逻辑 0(41行)。在下一个来临,control_module.v 会再一次设置 isEn 为逻辑 1,已做好接收下一组数据的准备。代码2‑31 rx_module_demo.v实例代码
1.//***************************************//3.//# @Date: 2019-04-06 22:44:484.//# @Last Modified by: zlk5.//# @Last Modified time: 2019-05-12 21:36:156.//****************************************//19. /**********************************/29. .RXD( RXD ), // input - from top30. .RX_En_Sig( RX_En_Sig ), // input - from U231. .RX_Done_Sig( RX_Done_Sig ), // output - to U232. .RX_Data( RX_Data ) // output - to U235. /***********************************/38. wire [7:0]Output_Data;44. .RX_Done_Sig( RX_Done_Sig ), // input - from U145. .RX_Data( RX_Data ), // input - from U146. .RX_En_Sig( RX_En_Sig ), // output - to U147. .Number_Data( Output_Data ) // output - to top50. /***********************************/52. assign LED = Output_Data[3:0];
|
连线关系也和“图形”一样。在 52 行,对LED的输出驱动,取致 Output_Data 前四位。这个演示主要是在“串口调试助手”以 “十六进制”发送数据。如果发送 0F ,那么四位LED 就全部都会亮起来;如果输出 0A,那么只有第四位和第二位 LED 亮了起来。为了测试上面设计的TX模块和RX模块功能,这部分主要完成和PC端串口通讯的设计。
PC端串口和芯片(FPGA、DSP、ARM等)通讯的几种方式:串口通讯主要考虑的是传输两端电平匹配, RS232电平(高电平:+12V 低电平:-12V),TTL电平(高电平:芯片端I/O引脚高电平 低电平:0 V)。
|
首先PC以一定的波特率发送数据,在FPGA接收完UART数据后,通过RX_Data与RX_En_Sig信号激励rx_module。接着由FPGA发送接收到的串转并数据,再发送UART数据给PC,最后通过PC接收验证。整个设计的结构如下:中间控制模块作用如同中介,将数据从一方移去另一方,完全没有介入串口接收|发送接口的操作之中。从另一个角度去看,在 rx_tx_interface_demo.v 中的接口(串口接收接口和串口发送接口)和中间模块控制模块,完全是独立的。代码2‑32 inter_control_module.v实例代码
1.//***************************************//3.//# @Date: 2019-05-12 22:32:324.//# @Last Modified by: zlk5.//# @Last Modified time: 2019-05-12 23:25:556.//****************************************//7.module inter_control_module22. /********************************/30. always @ ( posedge CLOCK or negedge RST_n )41. if( RX_Done_Sig ) begin isRX_En_Sig <= 1'b0;i <= i + 1'b1; end42. else begin isRX_En_Sig <= 1'b1;isRX_Data <= RX_Data; end45. if( TX_Done_Sig ) begin isTX_En_Sig <= 1'b0; i <= i + 1'b1; end46. else begin isTX_En_Sig <= 1'b1; isTX_Data<=isRX_Data; end49. begin isRX_En_Sig <= 1'b0; i <= 3'd0; end54. /********************************/56. assign RX_En_Sig = isRX_En_Sig;57. assign TX_En_Sig= isTX_En_Sig;58. assign TX_Data = isTX_Data;60. /********************************/
|
在 29~50 行是中间控制模块的主要功能。在步骤 0,先判断 rx_interface.v 的 FIFO 是否为空,如果不为空 if 条件就成立, i 就递增以示下一个步骤(32 行)。在 34~38 行,表示从 rx_interface.v 读取一个深度的数据。当完成从 rx_interface.v 的 FIFO 读取一个深度的数据以后,数据在 FIFO_Read_Data 信号上已经就绪。在步骤 3,先判断 tx_inerface.v 的 FIFO 是否为满状态?如果tx_interface.v 的 FIFO 不为满状态, i 会递增以示下一个步骤。在这里请注意, FIFO_Write_Data 是直接由 FIFO_Read_Data 驱动(56 行)。在 43~47行,主要是将数据写入 tx_interface.v 的 FIFO 内。