5. 串口rs232¶
通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。UART是一种通用的数据通信协议,也是异步串行通信口(串口)的总称,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。它包括了RS 232、RS499、RS423、RS422和RS485等接口标准规范和总线标准规范。
本章节我们会带领读者进行串口RS232相关知识的学习,通过理论与实践,最终设计并实现基于RS232的串口收、发功能模块,并完成串口数据回环实验。
5.1. 理论学习¶
5.1.1. 串口简介¶
串口作为常用的三大低速总线(UART、SPI、IIC)之一,在设计众多通信接口和调试时占有重要地位。但UART和SPI、IIC不同的是,它是异步通信接口,异步通信中的接收方并不知道数据什么时候会到达,所以双方收发端都要有各自的时钟,在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受 方是在数据的起始位和停止位的帮助下实现信息同步的。而SPI、IIC是同步通信接口(后面的章节会做详细介绍),同步通信中双方使用频率一致的时钟,在数据传输过程中时钟伴随着数据一起传输,发送方和接收方使用的时钟都是由主机提供的。
UART通信只有两根信号线,一根是发送数据端口线叫tx(Transmitter),一根是接收数据端口线叫rx(Receiver),如图 34‑1所示,对于PC来说它的tx要和对于FPGA来说的rx连接,同样PC的rx要和FPGA的tx连接,如果是两个tx或者两个rx连接那数据就不能正常被发送出去和接收到,所以不要弄混,记住rx和tx都是相对自身主体来讲的。UART可以实现全双工,即可以同时进行发送数据和接收数据。
图 34‑1 串口通信连接图
我们的任务是设计FPGA部分接收串口数据和发送串口数据的模块,最后我们把两个模块拼接起来,其结构如图 34‑2所示,最后通过loopback测试(回环测试)来验证设计模块的正确性。所谓loopback测试就是发送端发送什么数据,接收端就接收什么数据,这也是非常常用的一种测试手段,如果loopback测试成功,则说明从数据发送端到数据接收端之间的数据链路是正常的,以此来验证数据链路的畅通。
图 34‑2 串口回环模块框图
串口RS232传输数据的距离虽然不远,传输速率也相对较慢,但是串口依然被广泛的用于电路系统的设计中,串口的好处主要表现在以下几个方面:
1、很多传感器芯片或CPU都带有串口功能,目的是在使用一些传感器或CPU时可以通过串口进行调试,十分方便;
2、在较为复杂的高速数据接口和数据链路集合的系统中往往联合调试比较困难,可以先使用串口将数据链路部分验证后,再把串口换成高速数据接口。如在做以太网相关的项目时,可以在调试时先使用串口把整个数据链路调通,然后再把串口换成以太网的接口;
3、串口的数据线一共就两根,也没有时钟线,节省了大量的管脚资源。
5.1.2. RS-232信号线¶
在最初的应用中,RS-232串口标准常用于计算机、路由与调制调解器(MODEN,俗称“猫”)之间的通讯,在这种通讯系统中,设备被分为数据终端设备DTE(计算机、路由)和数据通讯设备DCE(调制调解器)。我们以这种通讯模型讲解它们的信号线连接方式及各个信号线的作用。
在旧式的台式计算机中一般会有 RS-232 标准的 COM 口(也称 DB9 接口),见图 34‑3。
图 34‑3 电脑主板上的 COM 口及串口线
其中接线口以针式引出信号线的称为公头,以孔式引出信号线的称为母头。在计算机中一般引出公头接口,而在调制调解器设备中引出的一般为母头,使用上图中的串口线即可把它与计算机连接起来。通讯时,串口线中传输的信号使用RS-232 标准调制。在各种应用场合下, DB9 接口中的公头及母头的各个引脚的标准信号线接法见图 34‑4。
图 34‑4 DB9 标准的公头及母头接法
图 34‑5 DB9 信号线说明
图 34‑5是计算机端的DB9公头标准接法,由于两个通讯设备之间的收发信号(RXD与TXD)应交叉相连,所以调制调解器端的DB9母头的收发信号接法一般与公头的相反,两个设备之间连接时,只要使用“直通型”的串口线连接起来即可,见图 34‑6。
图 34‑6 计算机与调制调解器的信号线连接
串口线中的RTS、CTS、DSR、DTR及DCD信号,使用逻辑 1 表示信号有效,逻辑 0表示信号无效。例如,当计算机端控制DTR信号线表示为逻辑 1 时,它是为了告知远端的调制调解器,本机已准备好接收数据, 0 则表示还没准备就绪。
在目前的其它工业控制使用的串口通讯中,一般只使用RXD、TXD以及GND三条信号线,直接传输数据信号。而RTS、CTS、DSR、DTR及DCD信号都被裁剪掉了,如果您在前面被这些信号弄得晕头转向,那就直接忽略它们吧。
5.1.3. RS232通信协议简介¶
1、RS232是UART的一种,没有时钟线,只有两根数据线,分别是rx和tx,这两根线都是1bit位宽的。其中rx是接收数据的线,tx是发送数据的线。
2、rx位宽为1bit,PC机通过串口调试助手往FPGA发8bit数据时,FPGA通过串口线rx一位一位地接收,从最低位到最高位依次接收,最后在FPGA里面位拼接成8比特数据。
3、tx位宽为1bit,FPGA通过串口往PC机发8bit数据时,FPGA把8bit数据通过tx线一位一位的传给PC机,从最低位到最高位依次发送,最后上位机通过串口助手按照RS232协议把这一位一位的数据位拼接成8bit数据。
4、串口数据的发送与接收是基于帧结构的,即一帧一帧的发送与接收数据。每一帧除了中间包含8bit有效数据外,还在每一帧的开头都必须有一个起始位,且固定为0;在每一帧的结束时也必须有一个停止位,且固定为1,即最基本的帧结构(不包括校验等)有10bit。在不发送或者不接收数据的情况下,rx和tx处于空闲状 态,此时rx和tx线都保持高电平,如果有数据帧传输时,首先会有一个起始位,然后是8bit的数据位,接着有1bit的停止位,然后rx和tx继续进入空闲状态,然后等待下一次的数据传输。如图 34‑7所示为一个最基本的RS232帧结构。
图 34‑7 RS232帧结构
5、波特率:在信息传输通道中,携带数据信息的信号单元叫码元(因为串口是1bit进行传输的,所以其码元就是代表一个二进制数),每秒钟通过信号传输的码元数称为码元的传输速率,简称波特率,常用符号“Baud”表示,其单位为“波特每秒(Bps)”。串口常见的波特率有4800、9600、115200等,我们选 用9600的波特率进行串口章节的讲解。
6、比特率:每秒钟通信信道传输的信息量称为位传输速率,简称比特率,其单位为“每秒比特数(bps)”。比特率可由波特率计算得出,公式为:比特率=波特率 * 单个调制状态对应的二进制位数。如果使用的是9600的波特率,其串口的比特率为:9600Bps * 1bit= 9600bps。
7、由计算得串口发送或者接收1bit数据的时间为一个波特,即1/9600秒,如果用50MHz(周期为20ns)的系统时钟来计数,需要计数的个数为cnt = (1s * 10^9)ns / 9600bit)ns / 20ns ≈ 5208个系统时钟周期,即每个bit数据之间的间隔要在50MHz的时钟频率下计数5208次。
8、上位机通过串口发8bit数据时,会自动在发8位有效数据前发一个波特时间的起始位,也会自动在发完8位有效数据后发一个停止位。同理,串口助手接收上位机发送的数据前,必须检测到一个波特时间的起始位才能开始接收数据,接收完8bit的数据后,再接收一个波特时间的停止位。
5.2. 实战演练¶
5.2.1. 实验目标¶
设计并实现基于串口RS232的数据收、发模块,使用收、发模块,完成串口数据回环实验。
5.2.2. 硬件资源¶
本次实验我们需使用到开发板上的RS232收发器芯片,RS232收发器电路如图 34‑8所示。
图 34‑8 RS3232收发器电路图
如图 34‑8所示,MAX3232为RS232收发器芯片。由于 RS-232 电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL”的电平信号,才能实现通讯。
根据通讯使用的电平标准不同,串口通讯可分为 TTL 标准及 RS-232 标准,见表格 34‑1。
表格 34‑1 TTL电平标准与RS232电平标准
模块名称 功能描述 ======== ================ uart_rx 串口数据接收模块 uart_tx 串口数据发送模块 rs232 顶层模块 ======== ================
下面分模块为大家讲解。
5.2.2.1. 串口数据接收模块¶
我们先设计串口接收模块,该模块的功能是接收通过PC机上的串口调试助手发送的固定波特率的数据,串口接收模块按照串口的协议准确接收串行数据,解析提取有用数据后需将其转化为并行数据,因为并行数据在FPGA内部传输的效率更高,转化为并行数据后同时产生一个数据有效信号标志信号伴随着并行的有效数据一同输出。
注:为什么还需要输出一个伴随并行数据有效的标志信号,这是因为后级模块或系统在使用该并行数据的时候可能无法知道该时刻采样的数据是不是稳定有效的,而数据有效标志信号的到来就说明数据才该时刻是稳定有效的,起到一个指示作用。当数据有效标志信号为高时,该并行数据就可以被后级模块或系统使用了。
模块框图
我们将串口接收模块取名为uart_rx,根据功能简介我们对整个设计要求有了大致的了解,其中设计的关键点是如何将串行数据转化为并行数据,也就是如何正确接收串行数据的问题。PC机通过串口调试助手发过来的信号没有时钟,所以FPGA在接收数据的时候要约定好一个固定的波特率,一个比特一个比特地接收数据,我们选 择的波特率为9600bps,也是RS232接口中相对较慢的一种速率。
整个模块肯定需要用到时序逻辑,所以先设计好时钟sys_clk和复位sys_rst_n两个输入信号,其次是相对于FPGA的rx端接收PC机通过串口调试助手发送过来的1bit输入信号。输出信号一个是FPGA的rx端接收到的数据转换成的8bit并行数据po_data,另一个是8bit并行数据有效的标志信号 po_data_flag。
根据上面的分析设计出的Visio框图如图 34‑12所示。
图 34‑12 串口接收模块框图
端口列表与功能总结如表格 34‑3所示。
表格 34‑3 串口接收模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_clk |
1Bit |
Input |
工作时钟,频率50MHz |
sys_rst_n |
1Bit |
Input |
复位信号,低电平有效 |
rx |
1Bit |
Input |
串口接收信号 |
po_data |
8Bit |
Output |
串口接收后转成的8bit数据 |
po_data_flag |
1Bit |
Output |
串口接收后转成的8bit数据有效标志信号 |
波形设计
如图 34‑13所示,我们先把实现uart_rx功能整体的波形图列出,然后再详细介绍下面的波形是如何一步步设计实现的。
图 34‑13 串口接收模块波形图
波形设计思路详细解析
第一部分:首先画出三个输入信号,必不可少的两个输入信号是时钟和复位,另一个是串行输入数据rx,如图 34‑14所示,我们发现rx串行数据一开始直接打了两拍,就是经过了两级寄存器,理论上我们应该按照串口接收数据的时序要求找到rx的下降沿,然后开始接收起始位的数据,但为什么先将数据打了两拍呢?那就要先从跨时钟域会导致“亚稳态”的问题上说起。
图 34‑14 数据打拍波形图
大家都一定使用过示波器,当你使用示波器把一个矩形脉冲的上升沿或下降沿放大后会发现其上升沿和下降沿并不是瞬间被拉高或拉低的,而是有一个倾斜变化的过程,这在运放中被称为“压摆率”,如果FPGA的系统时钟刚好采集到rx信号上升沿或下降沿的中间位置附近(按照概率来讲,如果数据传输量足够大或传输速度足够快时一 定会产生这种情况),即FPGA在接收rx数据时不满足内部寄存器的建立时间Tsu(指触发器的时钟信号上升沿到来以前,数据稳定不变的最小时间)和保持时间Th(指触发器的时钟信号上升沿到来以后,数据稳定不变的最小时间),此时FPGA的第一级寄存器的输出端在时钟沿到来之后比较长的一段时间内都处于不确定的状态 ,在0和1之间处于振荡状态,而不是等于串口输入的确定的rx值。
如图 34‑15所示为产生亚稳态的波形示意图,rx信号经过FPGA中的第一级寄存器后输出的rx_reg1信号在时钟上升沿Tco时间后会有Tmet(决断时间)的振荡时段,当第一个寄存器发生亚稳态后,经过Tmet的振荡稳定后,第二级寄存器就能采集到一个相对稳定的值。但由于振荡时间Tmet是受到很多因素影 响的,所以Tmet时间有长有短。如图 34‑16所示,当Tmet1时间长到大于一个采样周期后,那第二级寄存器就会采集到亚稳态,但是从第二级寄存器输出的信号就是相对稳定的了。当然会人会问到第二级寄存器的Tmet2的持续时间会不会继续延长到大于一个采样周期?这种情况虽然会存在,但是其概率是极小的,寄存器 本身就有减小Tmet时间让数据快速稳定的作用。
由于在PC机中波特率和rx信号是同步的,而rx信号和FPGA的系统时钟sys_clk是异步的关系,我们此时要做的是将慢速时钟域(PC机中的波特率)系统中的rx信号同步到快速时钟域(FPGA中的sys_clk)系统中,所使用的方法叫电平同步,俗称“打两拍法”。所以rx信号进入FPGA后会首先经过一级寄 存器,出现如图 34‑15所示的亚稳态现象,导致rx_reg1信号的状态不确定是0还是1,就会受其影响使其他相关信号做出不同的判断,有的判断到“0”有的判断到“1”,有的也进入了亚稳态并产生连锁反应,导致后级相关逻辑电路混乱。为了避免这种情况,rx信号进来后首先进行打一拍的处理,打一拍后产生rx_r eg1信号。但rx_reg1可能还存在低概率的亚稳态现象,为了进一步降低出现亚稳态的概率,我们将从rx_reg1信号再打一拍后产生rx_reg2信号,使之能够较大概率保证rx_reg2信号是0或者1中的一种确定情况,这样rx_reg2所影响的后级电路就都是相对稳定的了。但是大家一定要注意:打两拍后虽 然能让信号稳定到0或者1中确定的值,但究竟是0还是1却是随机的,与打拍之前输入信号的值没有必然的关系。
图 34‑15 亚稳态产生波形图(一)
图 34‑16 亚稳态产生波形图(二)
注:单比特信号从慢速时钟域同步到快速时钟域需要使用打两拍的方式消除亚稳态。第一级寄存器产生亚稳态并经过自身后可以稳定输出的概率为70%~80%左右,第二级寄存器可以稳定输出的概率为99%左右,后面再多加寄存器的级数改善效果就不明显了,所以数据进来后一般选择打两拍即可。
另外单比特信号从快速时钟域同步到慢速时钟域还仅仅使用打两拍的方式会漏采数据,所以往往使用脉冲同步法或的握手信号法;而多比特信号跨时钟域需要进行格雷码编码(多比特顺序数才可以)后才能进行打两拍的处理,或者通过使用FIFO、RAM来处理数据与时钟同步的问题。
亚稳态振荡时间Tmet关系到后级寄存器的采集稳定问题,Tmet影响因素包括:器件的生产工艺、温度、环境以及寄存器采集到亚稳态里稳定态的时刻等。甚至某些特定条件,如干扰、辐射等都会造成Tmet增长。
第二部分:由上面的分析,我们知道了为什么rx信号进入到FPGA后需要先打两拍的原因,打两拍后的rx_reg2信号就是我们可以在后级逻辑电路中使用的相对稳定的信号,只比rx信号延后两。下一步我们就可以根据串口接收数据的时序要求找到串口帧起始开始的标志——下降沿,然后按顺序接收数据。在触摸按 键章节我们分析过如何产生上升沿和下降沿标志,这里我们可以直接使用。由第一部分的分析得rx_reg1信号可能是不稳定的,而rx_reg2信号是相对稳定的,所以不能直接用rx_reg1信号和rx_reg2信号来产生下降沿标志信号,因为rx_reg1信号的不稳定性可能会导致由它产生的下降沿标志信号也不稳定 。所以如图 34‑17所示,我们将rx_reg2信号再打一拍,得到rx_reg3信号,用rx_reg2信号和rx_reg3信号产生staet_nedge作为下降沿标志信号。
图 34‑17 下降沿标志信号产生图
第三部分:我们检测到了第一个下降沿,后面的信号将以下降沿标志信号start_nedge为条件开始接收一帧10bit的数据。但新的问题又出现了,我们的rx信号本身就是1bit的,如果在判断第一个下降沿后,后面帧中的数据还可能会有下降沿出现,那我们会又产生一个start_nedge标志信号, 这样就出现了误判断,那我们该如何避免这种情况呢?这是一个值得思考的问题,在不知道答案之前我们可以发挥自己的想象并尝试使用各种方法来解决这个问题。我们知道在Verilog代码中标志信号(flag)和使能信号(en)都是非常有用的,标志信号只有一拍,非常适合我们产生像下降沿标志这种信号,而使能信号就特别 适合在此处使用,即对一段时间区域进行控制锁定。如图 34‑18所示,当下降沿标志信号start_nedge为高电平时拉高工作使能信号work_en(什么时候拉低在后面讲解),在work_en信号为高的时间区域内虽然也会有下降沿start_nedge标志信号产生,但是我们可以根据work_en 信号就可以判断出此时出现的start_nedge标志信号并不是我们想要的串口帧起始下降沿,从而将其过滤除掉。
图 34‑18 拉高工作使能信号波形图
解决了这个问题之后,我们正式开始接收一帧数据。我们使用的是9600bps的波特率和PC机进行串口通信,PC机的串口调试助手要将发送数据波特率调整为9600bps。而FPGA内部使用的系统时钟是50MHz,前面也进行过计算,得出1bit需要的时间约为5208个(因为一帧只有10bit,细微的近似计数差 别不会产生数据错误,但是如果计数值差的过大,则会产生接收数据的错误)系统时钟周期,那么我们就需要产生一个能计5208个数的计数器来依次接收10个比特的数据,计数器每计5208个数就接收一个新比特的数据。如图 34‑19所示,计数器名为baud_cnt,当work_en信号为高电平的时候就让计数器计数,当计数器计5208个数(从0到5207)或work_en信号为低电平时计数器清零。
图 34‑19 baud_cnt计数器产生波形图
第四部分:现在我们可以根据波特率计数器一个一个接收数据了,我们发现baud_cnt计数器在计数值为0到5207期间都是数据有效的时刻,那我们该什么时候取数据呢?理论上讲,在数据变化的地方取数是不稳定的,所以我们选择当baud_cnt计数器计数到2603,即中间位置时取数最稳定(其实只要b aud_cnt计数器在计数值不是在0和5207这两个最不稳定的时刻取数都可以,更为准确的是多次取值取概率最大的情况)。所以如图 34‑20所示,在baud_cnt计数器计数到中点时产生一个时钟周期的bit_flag的取数标志信号,用于指示该时刻的数据可以被取走。
图 34‑20 bit_flag标志信号产生波形图
串口的数据是基于帧的,所以每接收完一帧数据rx信号都要被拉高,即恢复到空闲状态重新判断串口帧起始下降沿,以等待下一帧数据的接收,且一帧数据中还包括了起始位和停止位这种无用的数据,而对我们有价值的数据只是中间的8bit数据,也就是说我们需要准确的知道我们此时此刻接收的是第几比特,当接收够10bit数据 后,我们就停止继续接收数据,等rx信号被拉高待恢复到空闲状态后再等待接收下一帧的数据。所以我们还需要产生一个用于计数该时刻接收的数据是第几个比特的bit_cnt计数器。如图 34‑21所示,刚好可以利用我们已经产生的bit_flag取数标志信号,对该信号进行计数既可以知道此时我们接收的数据是第几个比 特了。这里我们只让bit_cnt计数器的计数值为8时再清零,虽然bit_cnt计数器的计数值从0计数到8只有9个bit,但这9个bit中已经包含的我们所需要的8bit有用的数据,最后的1bit停止位没有用,可以不用再进行计数了,但如果非要将bit_cnt计数器的计数值计数到9后再清零也是可以的。
图 34‑21 bit_cnt计数器产生波形图
讲到这里我们不要忘记第三部分的遗留问题,那就是work_en信号何时拉低。如图 34‑22所示,当bit_cnt计数器计数到8且①处的bit_flag取数标志信号同时为高,说明我们已经接收到了所有的8bit有用数据,这两个条件必须同时满足时才能让work_en信号拉低。如果仅仅把bit_cnt计数器 的计数值计数到8作为work_en信号拉低的条件,而掉①处的bit_flag取数标志信号为高这个条件,就会使work_en信号在绿色虚线位置处拉低,导致最后1bit数据丢失,致使后面接收的帧出错甚至接收不到数据。
图 34‑22 拉低work_en信号波形图
第五部分:我们接收到的rx信号是串行的,后面的系统要使用的是完整的8bit并行数据。也就是说我们还需要将1bit串行数据转换为8bit并行数据的串并转换的工作,这也是我们在接口设计中常遇到的一种操作。串并转换就需要做移位,我们要考虑清楚什么时候开始移位,不能提前也不能推后,否则会将无用的 数据也移位进来,所以我们需要卡准时间。如图 34‑23所示PC机的串口调试助手发送的数据是先发送的低位后发送的高位,所以我们接收的rx信号也是先接收的低位后接收的高位,我们采用边接收边移位的操作。移位操作的方法我们已经在前面的流水灯章节中讲过,这里不再重复。接下来我们需要确定移位开始和结束的时间。如图 34‑24所示,当bit_cnt计数器的 计数值为1时说明第一个有用数据已经接收到了,刚好剔除了起始位,就可以进行移位了。注意移位的条件,要在bit_cnt计数器的计数值为1到8区间内且bit_flag取数标志信号同时为高时才能移位,也就是移动7次即可,接收最后1bit有用数据时就不需要再进行移位了。当移位7次后1bit的串行数据已经变为8 bit的并行数据了,此时产生一个移位完成标志信号rx_flag。
图 34‑23 数据的接收图
图 34‑24 数据移位波形图
第六部分:此时有很多同学以为我们的串口接收模块就全部完成了,其实还差最后一点。rx_data信号是参与移位的数据,在移位的过程中数据是变动的,不可以被后级模块所使用,而可以肯定的是在移位完成标志信号rx_flag为高时,rx_data信号一定是移位完成的稳定的8bit有用数据。如图 34 ‑25所示,此时我们当移位完成标志信号rx_flag为高时让rx_data信号赋值给专门用于输出稳定8bit有用数据的po_data信号就可以了,但rx_flag信号又不能作为po_data信号有效的标志信号,所以需要将rx_flag信号再打一拍。最后输出的有用8bit数据为po_data信号和伴随 po_data信号有效的标志信号po_flag信号。到此为止我们uart_rx模块的波形就全部设计好了。
图 34‑25 输出po_data波形图
代码编写
波形画出来了,再结合详细的波形分析,代码分分钟就可以搞定。写代码时还是和以前一样按照所画波形的顺序依次编写,这样在信号较多的情况下也不容易漏掉。为了增加模块的通用性,我们将波特率的计数值做成参数的形式,如果使用其他波特率进行通信,就可以将算好的计数值直接替换。模块参考代码详见代码清单 34‑1。
代码清单 34‑1 串口接收模块参考代码(uart_rx.v)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | module uart_rx
#(
parameter UART_BPS = 'd9600, //串口波特率
parameter CLK_FREQ = 'd50_000_000 //时钟频率
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
input wire rx , //串口接收数据
output reg [7:0] po_data , //串转并后的8bit数据
output reg po_flag //串转并后的数据有效标志信号
);
//localparam define
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ;
//reg define
reg rx_reg1 ;
reg rx_reg2 ;
reg rx_reg3 ;
reg start_nedge ;
reg work_en ;
reg [12:0] baud_cnt ;
reg bit_flag ;
reg [3:0] bit_cnt ;
reg [7:0] rx_data ;
reg rx_flag ;
//插入两级寄存器进行数据同步,用来消除亚稳态
//rx_reg1:第一级寄存器,寄存器空闲状态复位为1
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg1 <= 1'b1;
else
rx_reg1 <= rx;
//rx_reg2:第二级寄存器,寄存器空闲状态复位为1
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg2 <= 1'b1;
else
rx_reg2 <= rx_reg1;
//rx_reg3:第三级寄存器和第二级寄存器共同构成下降沿检测
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg3 <= 1'b1;
else
rx_reg3 <= rx_reg2;
//start_nedge:检测到下降沿时start_nedge产生一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
start_nedge <= 1'b0;
else if((~rx_reg2) && (rx_reg3))
start_nedge <= 1'b1;
else
start_nedge <= 1'b0;
//work_en:接收数据工作使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
work_en <= 1'b0;
else if(start_nedge == 1'b1)
work_en <= 1'b1;
else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
work_en <= 1'b0;
//baud_cnt:波特率计数器计数,从0计数到5207
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
baud_cnt <= 13'b0;
else if((baud_cnt == BAUD_CNT_MAX - 1) \|\| (work_en == 1'b0))
baud_cnt <= 13'b0;
else if(work_en == 1'b1)
baud_cnt <= baud_cnt + 1'b1;
//bit_flag:当baud_cnt计数器计数到中间数时采样的数据最稳定,
//此时拉高一个标志信号表示数据可以被取走
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_flag <= 1'b0;
else if(baud_cnt == BAUD_CNT_MAX/2 - 1)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
//bit_cnt:有效数据个数计数器,当8个有效数据(不含起始位和停止位)
//都接收完成后计数器清零
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_cnt <= 4'b0;
else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
bit_cnt <= 4'b0;
else if(bit_flag ==1'b1)
bit_cnt <= bit_cnt + 1'b1;
//rx_data:输入数据进行移位
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_data <= 8'b0;
else if((bit_cnt >= 4'd1)&&(bit_cnt <= 4'd8)&&(bit_flag == 1'b1))
rx_data <= {rx_reg3, rx_data[7:1]};
//rx_flag:输入数据移位完成时rx_flag拉高一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_flag <= 1'b0;
else if((bit_cnt == 4'd8) && (bit_flag == 1'b1))
rx_flag <= 1'b1;
else
rx_flag <= 1'b0;
//po_data:输出完整的8位有效数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_data <= 8'b0;
else if(rx_flag == 1'b1)
po_data <= rx_data;
//po_flag:输出数据有效标志(比rx_flag延后一个时钟周期,为了和po_data同步)
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
po_flag <= 1'b0;
else
po_flag <= rx_flag;
endmodule
|
仿真文件编写
在编写仿真代码时,我们要模拟出PC机的串口调试助手发送串行数据帧的过程,我们首次使用task任务来实现数据一个一个发送的过程。模块仿真参考代码详见代码清单 34‑2。
代码清单 34‑2 串口接收模块仿真参考代码(tb_uart_rx.v)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | module tb_uart_rx();
////
//\* Parameter and Internal Signal \//
////
//reg define
reg sys_clk;
reg sys_rst_n;
reg rx;
//wire define
wire [7:0] po_data;
wire po_flag;
////
//\* Main Code \//
////
//初始化系统时钟、全局复位和输入信号
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
rx <= 1'b1;
#20;
sys_rst_n <= 1'b1;
end
//模拟发送8次数据,分别为0~7
initial begin
#200
rx_bit(8'd0); //任务的调用,任务名+括号中要传递进任务的参数
rx_bit(8'd1);
rx_bit(8'd2);
rx_bit(8'd3);
rx_bit(8'd4);
rx_bit(8'd5);
rx_bit(8'd6);
rx_bit(8'd7);
end
//sys_clk:每10ns电平翻转一次,产生一个50MHz的时钟信号
always #10 sys_clk = ~sys_clk;
//定义一个名为rx_bit的任务,每次发送的数据有10位
//data的值分别为0~7由j的值传递进来
//任务以task开头,后面紧跟着的是任务名,调用时使用
task rx_bit(
//传递到任务中的参数,调用任务的时候从外部传进来一个8位的值
input [7:0] data
);
integer i; //定义一个常量
//用for循环产生一帧数据,for括号中最后执行的内容只能写i=i+1
//不可以写成C语言i=i++的形式
for(i=0; i<10; i=i+1) begin
case(i)
0: rx <= 1'b0;
1: rx <= data[0];
2: rx <= data[1];
3: rx <= data[2];
4: rx <= data[3];
5: rx <= data[4];
6: rx <= data[5];
7: rx <= data[6];
8: rx <= data[7];
9: rx <= 1'b1;
endcase
#(5208*20); //每发送1位数据延时5208个时钟周期
end
endtask //任务以endtask结束
////
//\* Instantiation \//
////
//------------------------uart_rx_inst------------------------
uart_rx uart_rx_inst(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.rx (rx ), //input rx
.po_data (po_data ), //output [7:0] po_data
.po_flag (po_flag ) //output po_flag
);
endmodule
|
仿真波形分析
打开ModelSim后先清空波形信号,重新添加要测试的模块,我们让波形跑了10ms即可完全显示所有波形,然后将波形窗口信号的排列顺序和所画的波形图顺序一致,这样可以更快速的发现我们设计中存在的问题。模拟PC机发送8次(数据值从0到7)串行数据的波形如图 34‑26所示。红色圈①处为接收数据“1”的波形,将其放大详细观察。
图 34‑26 接收模块仿真波形图(一)
整体接收模块的波形如图 34‑27所示,我们可以看到数据是先接收的低位后接收的高位,一共是10bit数据。
图 34‑27 接收模块仿真波形图(二)
第一、第二、第三部分仿真波形如图 34‑28所示,我们可以清晰的看到将rx信号打三拍的操作,并产生了串口帧起始的下降沿标志信号,以及work_en信号在串口帧起始的下降沿标志信号为高时拉高,baud_cnt计数器在work_en信号为高时开始计数。
图 34‑28 接收模块仿真波形图(三)
第四部分仿真波形如图 34‑29所示,取数标志信号bit_flag在baud_cnt计数器计数到2603时产生一个时钟周期的脉冲。
图 34‑29 接收模块仿真波形图(四)
第五部分仿真波形如图 34‑30所示,可以看到rx_data信号在bit_cnt计数器的计数值为1到8区间内且bit_flag取数标志信号同时为高时移位的过程。
图 34‑30 接收模块仿真波形图(五)
第六部分仿真波形如图 34‑31所示,我们可以看到work_en信号拉低的时间,以及产生供后级模块使用的8bit有用数据po_data和数据有效标志信号po_flag。
图 34‑31 接收模块仿真波形图(六)
5.2.2.2. 串口数据发送模块¶
接下来我们继续进行串口发送模块的设计,该模块的功能是将FPGA中的数据以固定的波特率发送到PC机的串口调试助手并打印出来,串口发送模块按照串口的协议组装成帧,然后按照顺序一个比特一个比特将数据发送至PC机,而FPGA内部的数据往往都是并行的,需将其转化为串行数据发送。
模块设计
我们将串口接收模块取名为uart_tx,根据功能简介我们对整个设计要求有了大致的了解,其中设计的关键点是如何将串并行数据转化为串行数据并发送出去,也就是按照顺序将并行数据发送至PC机上。FPGA发送的串行数据同样没有时钟,所以要和PC机接约定好使用相同的波特率,一个一个地发送比特,为了后面做串口的回 环测试我们仍选择使用9600bps的波特率。
整个模块也必须用到时序逻辑,所以先设计好时钟sys_clk和复位sys_rst_n两个输入信号,其次是FPGA要发送的8bits有用数据pi_data和伴随数据有效的标志信号pi_flag。其次是相对于FPGA的tx端发送至PC机中的1bit输出信号。
根据上面的分析设计出的Visio框图如图 34‑32所示。
图 34‑32 串口发送模块框图
端口列表与功能总结如表格 34‑4所示。
表格 34‑4 串口发送模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_clk |
1Bit |
Input |
工作时钟,频率50MHz |
pi_data |
8Bit |
Input |
发要送的8bit并行数据 |
pi_data_flag |
1Bit |
Input |
要发送的8bit并行数据有效标志信号 |
tx |
1Bit |
Output |
串口发送信号 |
波形设计
如图 34‑33所示,我们先把实现uart_tx功能整体的波形图列出,然后再详细介绍下面的波形是如何一步步设计实现的。
图 34‑33 串口发送模块波形图
波形设计思路详细解析
第一部分:首先把四个输入信号画出,分别是时钟、复位、8bit有用数据和数据有效标志信号。8bit有用数据pi_data和数据有效标志信号pi_flag是上一级系统发送过来的,我们设计的模块只需要负责接收即可。当数据有效标志信号pi_flag为高时表示数据已经是稳定的可以被使用的,这时就可 以把8bit数据接收过来了,然后再将这个8bit数据按照顺序一个一个串行发送出去。我们已经和PC机约定好了使用9600bps的波特率,所以发送1bit数据需要的时间也约为5208个系统时钟周期,这就需要产生一个和接收数据时一样的波特率计数器,我们取名为baud_cnt,该计数器每计5208个数就发送 一个新比特的数据,一共发送10个比特。但是仔细一想问题就出现了,波特率计数器baud_cnt计数的条件是什么呢?当检测到数据有效标志信号pi_flag为高时就开始计数吗?这是不行的,因为pi_flag信号只维持一个时钟周期的高电平,并不能让pi_flag信号为高作为波特率计数器baud_cnt计数的 条件,所以我们需要一个控制波特率计数器baud_cnt何时计数的使能信号。如图 34‑34所示,我们产生一个名为work_en的工作使能信号,当检测到数据有效标志信号pi_flag为高电平时拉高工作使能信号work_en(什么时候拉低在后面讲解),因为work_en信号是持续的高电平,所以当work_en信号为高电平时波特率计数器baud_cnt进行计数。
图 34‑34 串口发送模块波形图部分(一)
第二部分:下面我们就可以按照5208个系统时钟周期的波特率间隔来发送1bit数据了。那应该在什么位置开发送呢?我们要先在5208个系统时钟周期内确定好一个发送的点,后面再发送的数据间隔都是5208个时钟周期即可。理论上我们在第一个5208系统时钟周期内的任意一个位置发送数据都可以,这和接 收数据时要在中间位置不同,所以我们直接让当baud_cnt计数器的计数值为1(选择其他的值也可以,但是尽量不要选择baud_cnt计数器的计数值为0或5207这种端点,因为容易出问题)的时候作为发送数据的点,而下一个baud_cnt计数器的计数值为1的时候和上一个正好相差5208个系统时钟周期,是完 全可以满足要求的。
那此时就可以发送数据了吗?我们再来思考一下,发送数据时要发送一帧,也就是需要发送固定1bit为0的起始位、8bit的有用数据和固定1bit为1的停止位,每当baud_cnt计数器的计数值为1的时候就发送1bit的数据,发送第一个起始位的时候没有问题,发送8个有用数据位置的时候也没有问题,发送最后一个 停止位的时候仍是正确的,但是我们只需要发送10个bit的数据就结束了,后面就不需要再发送数据了,此时如果work_en信号还持续为高那么baud_cnt计数器也就会一直计数,那么发送完10bit数据后还会继续发送,这是我们不需要的。所以当发送完一帧数据后我们要将work_en信号拉低,从而使baud _cnt计数器停止,才能够不继续发送数据。那什么时候停止呢?一定要在10个比特的数据都发送完才能停止,那么我们就需要有一个用于计数当前发送了多少个数据的计数器,我们取名为bit_cnt。bit_cnt计数器在baud_cnt计数器的计数值每次为1的时候加1即可。如图 34‑35所示,为了更加直观的表达,我们再多加一个信号,每当baud_cnt计数器的计数值为1的时候产生一个时钟周期的名为bit_flag的允许发送数据标志信号,bit_cnt计数器当标志信号为高时加1(计数到多少后面在后面讲解)。
图 34‑35 串口发送模块波形图部分(二)
第三部分:最后就是数据按顺序一个一个发出去,因为接收的时候是先接收的低位后接收的高位,所以如图 34‑36所示,发送的时候也是先发送低位,后发送高位。
图 34‑36 数据的发送图
不要忘记前面还有两个遗留问题没做,一个是work_en信号什么时候拉低,另一个是bit_cnt计数器计数到多少清零,也是最后的收尾工作了。先说bit_cnt计数器什么时候清零的问题,有的同学说要计数到9,有的同学说要计数到10,其实我们不妨都尝试一下。如所图 34‑37示,假如我们让bit_cnt计 数器计数到9,可以发现最后一个停止位没有对应的计数了,这会有问题吗?我们仔细分析就可以知道,停止位和空闲情况下都为高电平,所以最有一个停止位就没有必要再单独计数了,所以bit_cnt计数器计数到9清零是完全可以的,当然让bit_cnt计数器计数到10更是可以的。
最后再来说说work_en信号拉低的条件,work_en存在的原因就是为了方便baud_cnt计数器计数的,当我们不需要baud_cnt计数器计数的时候也就可以让work_en信号拉低了。当bit_cnt计数器计数到9且bit_flag信号有效时停止位就可以被发送出去了,此时就不再需要baud_cn t计数器计数了,就可以把work_en信号拉低了,但同时还要将baud_cnt计数器清零,等待下一次发送数据时再从0开始计数。到此为止我们uart_tx模块的波形也全部设计好了。
图 34‑37 串口发送模块波形图部分(三)
代码编写
波形画出来了,再结合详细的波形分析,就可以进行代码的编写了,写代码时继续按照所画波形的顺序依次编写。为了增加模块的通用性,这里也将波特率的计数值做成参数的形式,如果使用其他速率的波特率进行通信,就可以将算好的计数值直接替换。模块参考代码详见代码清单 34‑3。
代码清单 34‑3 串口发送模块参考代码(uart_tx.v)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | module uart_tx
#(
parameter UART_BPS = 'd9600, //串口波特率
parameter CLK_FREQ = 'd50_000_000 //时钟频率
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
input wire [7:0] pi_data , //模块输入的8bit数据
input wire pi_flag , //并行数据有效标志信号
output reg tx //串转并后的1bit数据
);
////
//\* Parameter and Internal Signal \//
////
//localparam define
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ;
//reg define
reg [12:0] baud_cnt;
reg bit_flag;
reg [3:0] bit_cnt ;
reg work_en ;
////
//\* Main Code \//
////
//work_en:接收数据工作使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
work_en <= 1'b0;
else if(pi_flag == 1'b1)
work_en <= 1'b1;
else if((bit_flag == 1'b1) && (bit_cnt == 4'd9))
work_en <= 1'b0;
//baud_cnt:波特率计数器计数,从0计数到5207
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
baud_cnt <= 13'b0;
else if((baud_cnt == BAUD_CNT_MAX - 1) \|\| (work_en == 1'b0))
baud_cnt <= 13'b0;
else if(work_en == 1'b1)
baud_cnt <= baud_cnt + 1'b1;
//bit_flag:当baud_cnt计数器计数到1时让bit_flag拉高一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_flag <= 1'b0;
else if(baud_cnt == 13'd1)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
//bit_cnt:数据位数个数计数,10个有效数据(含起始位和停止位)到来后计数器清零
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
bit_cnt <= 4'b0;
else if((bit_flag == 1'b1) && (bit_cnt == 4'd9))
bit_cnt <= 4'b0;
else if((bit_flag == 1'b1) && (work_en == 1'b1))
bit_cnt <= bit_cnt + 1'b1;
//tx:输出数据在满足rs232协议(起始位为0,停止位为1)的情况下一位一位输出
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tx <= 1'b1; //空闲状态时为高电平
else if(bit_flag == 1'b1)
case(bit_cnt)
0 : tx <= 1'b0;
1 : tx <= pi_data[0];
2 : tx <= pi_data[1];
3 : tx <= pi_data[2];
4 : tx <= pi_data[3];
5 : tx <= pi_data[4];
6 : tx <= pi_data[5];
7 : tx <= pi_data[6];
8 : tx <= pi_data[7];
9 : tx <= 1'b1;
default : tx <= 1'b1;
endcase
endmodule
|
仿真文件编写
在编写仿真代码时,我们要模拟出PC机的串口调试助手发送串行数据帧的过程,我们和接收时一样,发送8个并行数据从0到7,同时每个数据要有一个伴随数据有效的标志信号。这次我们不使用task,可以看到我们的仿真代码会很长。模块仿真参考代码详见代码清单 34‑4。
代码清单 34‑4 串口发送模块仿真参考代码(tb_uart_tx.v)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | module tb_uart_tx();
////
//\* Parameter and Internal Signal \//
////
//reg define
reg sys_clk;
reg sys_rst_n;
reg [7:0] pi_data;
reg pi_flag;
//wire define
wire tx;
////
//\* Main Code \//
////
//初始化系统时钟、全局复位
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20;
sys_rst_n <= 1'b1;
end
//模拟发送7次数据,分别为0~7
initial begin
pi_data <= 8'b0;
pi_flag <= 1'b0;
#200
//发送数据0
pi_data <= 8'd0;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
//每发送1bit数据需要5208个时钟周期,一帧数据为10bit
//所以需要数据延时(5208*20*10)后再产生下一个数据
#(5208*20*10);
//发送数据1
pi_data <= 8'd1;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*20*10);
//发送数据2
pi_data <= 8'd2;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*20*10);
//发送数据3
pi_data <= 8'd3;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*20*10);
//发送数据4
pi_data <= 8'd4;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*20*10);
//发送数据5
pi_data <= 8'd5;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*20*10);
//发送数据6
pi_data <= 8'd6;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*20*10);
//发送数据7
pi_data <= 8'd7;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
end
//sys_clk:每10ns电平翻转一次,产生一个50MHz的时钟信号
always #10 sys_clk = ~sys_clk;
////
//\* Instantiation \//
////
//------------------------uart_rx_inst------------------------
uart_tx uart_tx_inst(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.pi_data (pi_data ), //output [7:0] pi_data
.pi_flag (pi_flag ), //output pi_flag
.tx (tx ) //input tx
);
endmodule
|
仿真波形分析
打开ModelSim后先清空波形信号,重新添加要测试的模块,我们让波形跑了10ms即可完全显示所有波形,先将波形窗口信号的排列顺序和所画的波形图顺序一致再进行观察。模拟上级系统发送8次(数据值从0到7)数据和数据有效标志信号,其整体波形如图 34‑38所示。红色圈①处为发送数据“1”的波形,将其放大详细观察。
图 34‑38 发送模块仿真波形图(一)
整体发送模块的波形如图 34‑39所示,我们可以看到数据是先发送的低位后发送的高位,一共是10bit数据。
图 34‑39 发送模块仿真波形图(二)
第一、第二部分仿真波形如图 34‑40所示,可以看到要发送的8bit并行数据为1,同时伴随着一个数据有效标志信号,当数据有效标志信号为高时间work_en信号拉高,当work_en信号为高期间baud_cnt计数器进行计数。baud_cnt计数器计数值为1时bit_flag信号为高,当检测到bit_ flag信号为高时tx就发送一个数据,同时bit_cnt计数器加1。
图 34‑40 发送模块仿真波形图(三)
第三部分仿真波形如图 34‑41所示,我们可以清晰地看到最后一个bit_flag信号为高的时刻,且bit_cnt计数器也计数到9,将停止位发送出去,同时work_en信号拉低,baud_cnt计数器检测到work_en信号为低电平后立刻清零并停止计数,等待下一次发送数据时再工作。
图 34‑41 发送模块仿真波形图(四)
5.2.2.3. 顶层模块¶
串口的接收模块uart_rx和发送模块uart_tx我们都设计好了。在本章的最开始我们也讲过串口可以作为很好用的调试工具使用,比如和其他系统一起做回环测试,我们也称之为loopback的测试。串口发送模块和串口接收模块因为波特率相同,功能又互补,所以他们自身就可以直接连接到一起进行工作来实现最简单的 loopback测试。大致流程为PC机的串口调试助手发送一串数据,经过FPGA后再传回到PC机的串口调试助手中打印显示。
模块设计
FPGA对外可以看成一个整体的模块,如图 34‑42所示,输入需要时钟、复位,同时还有PC机发送过来的串行数据rx信号,输出为发送给PC机串行数据tx信号。我们之前学习过层次化的设计,这次也用到了,我们先设计好的uart_rx模块和uart_tx模块,再组装成系统,可以被看做是自底向上(Bottom- Up)的设计,需要将一个顶层模块来实例化uart_rx模块和uart_tx模块,我们将这个顶层模块取名为rs232。
如图图 34‑43所示,我们将uart_rx模块的输出信号po_data和po_flag分别连接到uart_tx模块的输入信号pi_data和pi_flag,中间的连线我们仍按照uart_rx模块的叫法来。loopback的数据传输的详细过程为:PC机的串口调试助手发送一帧串行数据,给rs232模块 的rx端,rs232的rx端接收到数据后传给uart_rx模块的rx端,uart_rx模块负责解析出一帧数据中的有用数据,并将其转化为8bit并行数据po_data和数据有效标志信号po_flag。8bit并行数据po_data和数据有效标志信号po_flag通过FPGA的内部连线直接传输给uart _rx模块的8bit数据输入端pi_data和数据有效标志信号输入端pi_flag,将接收到的并行数据重新封装成帧后串行发送到tx端,uart_rx模块的tx端再把数据传给rs232的tx端,rs232的tx端再将数据传回到PC机的串口调试助手中打印显示。实现了发送什么就接收什么,如果发送和接收的数 据不一致,那就说明整个链路存在错误。
图 34‑42 顶层模块框图
根据上面的分析设计出的Visio框图如图 34‑43所示。
图 34‑43 模块整体框图
端口列表与功能总结如表格 34‑5所示。
表格 34‑5 顶层模块输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
sys_clk |
1Bit |
Input |
工作时钟,频率50MHz |
sys_rst_n |
1Bit |
Input |
复位信号,低电平有效 |
rx |
1Bit |
Input |
串口接收信号 |
tx |
1Bit |
Output |
串口发送信号 |
代码编写
结构清晰了,底层模块也有了,剩下的工作就是在顶层实例化子模块,然后进行连线即可,连线的时候多比特数据需要特别注意位宽匹配要准确。顶层模块参考代码详见代码清单 34‑5。
代码清单 34‑5 顶层模块参考代码(rs232.v)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | module rs232(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
input wire rx , //串口接收数据
output wire tx //串口发送数据
);
////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter UART_BPS = 14'd9600; //比特率
parameter CLK_FREQ = 26'd50_000_000; //时钟频率
//wire define
wire [7:0] po_data;
wire po_flag
////
//\* Instantiation \//
////
//------------------------uart_rx_inst------------------------
uart_rx
#(
.UART_BPS (UART_BPS), //串口波特率
.CLK_FREQ (CLK_FREQ) //时钟频率
)
uart_rx_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.rx (rx ), //input rx
.po_data (po_data ), //output [7:0] po_data
.po_flag (po_flag ) //output po_flag
);
//------------------------uart_tx_inst------------------------
uart_tx
#(
.UART_BPS (UART_BPS), //串口波特率
.CLK_FREQ (CLK_FREQ) //时钟频率
)
uart_tx_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.pi_data (po_data ), //input [7:0] pi_data
.pi_flag (po_flag ), //input pi_flag
.tx (tx ) //output tx
);
endmodule
|
之前因为底层的结构越来越复杂,没有让大家研究底层的RTL视图,但这次我们RTL视图有了新的作用,用来查看我们模块之间的连线是否正确,这对设计电路的初期检查排错很有用。如图 34‑44所示为综合处的RTL视图,我们可以看到模块之间的连线和位宽都没有问题。
图 34‑44 RTL视图
仿真文件编写
在编写仿真代码时,我们仍要模拟出PC机的串口调试助手发送串行数据帧的过程,如果直接使用uart_rx的仿真代码也是可以的,但我们为了让大家更熟练的使用task,我们又增加了一个task,在一个task中调用另一个task,使得仿真代码更加简洁高效。顶层仿真参考代码详见代码清单 34‑6。
代码清单 34‑6 顶层仿真参考代码(tb_rs232.v)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | module tb_rs232();
////
//\* Parameter and Internal Signal \//
////
//reg define
reg sys_clk;
reg sys_rst_n;
reg rx;
//wire define
wire tx;
////
//\* Main Code \//
////
//初始化系统时钟、全局复位和输入信号
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
rx <= 1'b1;
#20;
sys_rst_n <= 1'b1;
end
//调用任务rx_byte
initial begin
#200
rx_byte();
end
//sys_clk:每10ns电平翻转一次,产生一个50MHz的时钟信号
always #10 sys_clk = ~sys_clk;
//创建任务rx_byte,本次任务调用rx_bit任务,发送8次数据,分别为0~7
task rx_byte(); //因为不需要外部传递参数,所以括号中没有输入
integer j;
for(j=0; j<8; j=j+1) //调用8次rx_bit任务,每次发送的值从0变化7
rx_bit(j);
endtask
//创建任务rx_bit,每次发送的数据有10位,data的值分别为0到7由j的值传递进来
task rx_bit(
input [7:0] data
);
integer i;
for(i=0; i<10; i=i+1) begin
case(i)
0: rx <= 1'b0;
1: rx <= data[0];
2: rx <= data[1];
3: rx <= data[2];
4: rx <= data[3];
5: rx <= data[4];
6: rx <= data[5];
7: rx <= data[6];
8: rx <= data[7];
9: rx <= 1'b1;
endcase
#(5208*20); //每发送1位数据延时5208个时钟周期
end
endtask
////
//\* Instantiation \//
////
//------------------------rs232_inst------------------------
rs232 rs232_inst(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.rx (rx ), //input rx
.tx (tx ) //output tx
);
endmodule
|
仿真波形分析
打开ModelSim后先清空波形信号,重新添加要测试的模块,把uart_rx模块和uart_tx模块的波形都添加进来并分组,我们仍让波形跑10ms,先将波形窗口信号的排列顺序和所画的波形图顺序一致再进行观察。模拟PC机发送8次(数据值从0到7)串行数据的波形如图 34‑45所示。我们放大红色圈①处的波形进行详细观察。
图 34‑45 顶层仿真波形图(一)
如图 34‑46所示,我们可以看到发送的串行数据和对应接收的串行数据是一样的,也就说明我们的rx端口和tx端口的数据相同,同时也再一次验证了我们设计的uart_rx模块和uart_tx模块都是正确的。其实当我们验证过了uart_rx模块后完全可以不用单独再设计uart_tx模块的仿真代码,而继续使用 uart_rx模块的仿真代码然后通过loopback测试来验证uart_tx模块设计的是否正确。
图 34‑46 顶层仿真波形图(二)
5.3. 上板验证测试¶
5.3.1. 引脚约束¶
仿真验证通过后,准备上板验证,上板验证时我们可选择用RS232串口线或USB线(征途pro开发上的USB接口为Tape-C型)进行验证,他们唯一的不同就是引脚的配置和跳帽的连接。使用USB线的引脚配置如表格 34‑6所示,使用串口线的引脚配置如表格 34‑7所示。
表格 34‑6 USB转串口引脚分配表
信号名 |
信号类型 |
对应引脚 |
备注 |
---|---|---|---|
sys_clk |
input |
E1 |
时钟 |
sys_rst_n |
input |
M15 |
复位 |
rx |
input |
N6 |
串口接收数据 |
tx |
output |
N5 |
串口发送数据 |
表格 34‑7 RS232串口引脚分配表
信号名 |
信号类型 |
对应引脚 |
备注 |
---|---|---|---|
sys_clk |
input |
E1 |
时钟 |
sys_rst_n |
input |
M15 |
复位 |
rx |
input |
K8 |
串口接收数据 |
tx |
output |
M7 |
串口发送数据 |
下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,USB转串口和RS232串口的管脚分配图分别如图 34‑47、图 34‑48所示。
图 34‑47 USB转串口管脚分配图
图 34‑48 RS232串口管脚分配图
5.3.1.1. 结果验证¶
若要进行USB转串口的验证,按照如图 34‑49所示连接12V电源、下载器、USB数据线以及短路帽;若要进行串口RS232的验证,按照如图 34‑50所示连接12V电源、下载器、RS232数据线以及短路帽。
图 34‑49 程序下载连接线(一)
图 34‑50 程序下载连接线(二)
打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 34‑51所示。
图 34‑51 下载成功界面
下载成功后即可以开始验证了。按照实验目标描述进行操作,若显示结果与实验目标描述相同,则说明验证成功。
若我们使用的是绑定USB转串口的引脚进行编译下载的工程,我们就使用USB连接线(Tape- C型)进行PC机与开发板连接进行lookback的测试;若我们使用的是绑定RS232串口的引脚进行编译下载的工程,我们就使用RS232串口线进行PC即与开发板连接进行lookback的测试,大家可根据自己已有的连接线进行选择。
连接下载完毕之后我们即可开始测试。打开串口助手,从串口中我们可以看到检测到有接口,若没有检测到接口,请先检查接线是否连接正确。同时我们将波特率设置为我们代码中的波特率9600,具体的设置如图 34‑52所示。
图 34‑52 串口回环测试图(一)
设置完之后我们就可以进行发送数据了,我们发送任意字节数据,若接收的数据与发送的数据一致,则说明验证成功,如图 34‑53所示。
图 34‑53 串口回环测试图(二)
5.4. 章末总结¶
在本章的Testbench的设计中我们第一次使用到了task任务以及for循环语句,这两个语法都在仿真中使用的较多,虽然都是可以综合的但还是推荐初学者尽量不要在RTL代码中使用,尤其是对它们理解不深刻的情况下。而我们在Testbench中使用就不用担心这么多,且可以大大简化我们的代码,提高效率,是十 分好用的,也推荐大家以后再Testbench中多尝试使用。
我们还学习到一个很好用的调试方法,就是loopbang测试。本章中我们只是做了最简单的串口回环,以后我们还可以在uart_rx模块和uart_tx模块中间加入我们设计的更加复杂的其他模块来进行验证。
本章可以说是我们学习FPGA以来的最有代表性的一个小项目了,无论是波形设计还是代码编写都比之前要复杂,所以说这个小项目非常有意义,也很重要,希望大家能够再次深刻体会我们系统的设计方法和流程,并能够自己完全实现。对于后面的学习还会有更加复杂、系统的实战项目,需要大家在学习的过程中多思考、多练习、多总结 ,最终做到完全掌握应用自如。
新语法总结
重点掌握
task(可以互相调用)
for(虽不多见,但是在Testbench中很高效)
知识点总结
理解亚稳态产生的原理,掌握单比特数据从慢速时钟域到快速时钟域处理亚稳态的方法。
学会使用边沿检测,并记住代码的格式,理解原理。
串并转换是接口中很常用的一种方法,用到了移位,要熟练掌握。
掌握loopback测试的方法,以后用于我们模块中代码的调试。
5.5. 拓展训练¶
通过对亚稳态现象的分析,我们可以进一步思考,系统复位往往是连接到外部的按键上,那按键对与FPGA系统的复位输入是否也存在“亚稳态”的问题呢?如果存在我们该如何解决?
能否将我们前面的按键消抖也用边沿检测的方法来实现呢?
尝试更改更高频率的波特率进行loopback的测试。