前置学习:
基础设计二——FPGA学习笔记<3>
基础设计一——FPGA学习笔记<2>
verilog语法——FPGA学习笔记<1>
参考书目:《野火FPGA Verilog 开发实战指南》
目录
一.串口 RS232
<1>简介
<2>物理模型
<3>RS232通信协议
<4>设计实践
1.硬件资源
2.程序设计
(1)uart_rx设计
①波形图分析
②代码设计
③仿真设计
(2)uart_tx设计
①波形图分析
②代码设计
(3)顶层设计
①代码设计
②仿真设计
3.总结
一.串口 RS232
<1>简介
通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作 UART。UART 是一种通用的数据通信协议,也是异步串行通信口(串口)的总称,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。它包括了 RS232、RS499、RS423、RS422 和 RS485 等接口标准规范和总线标准规范。三大低速总线(UART、SPI、IIC)
UART 和 SPI、IIC 不同的是,它是异步通信接口,异步通信中的接收方并不知道数据什么时候会到达,所以双方收发端都要有各自的时钟,在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受方是在数据的起始位和停止位的帮助下实现信息同步的。而 SPI、IIC 是同步通信接口(后面的章节会做详细介绍),同步通信中双方使用频率一致的时钟,在数据传输过程中时钟伴随着数据一起传输,发送方和接收方使用的时钟都是由主机提供的。
UART 通信只有两根信号线,一根是发送数据端口线叫 tx(Transmitter),一根是接收数据端口线叫 rx(Receiver),如图 26-1 所示,对于 PC 来说它的 tx 要和对于 FPGA 来 说的 rx 连接,同样 PC 的 rx 要和 FPGA 的 tx 连接,如果是两个 tx 或者两个 rx 连接那数据 就不能正常被发送出去和接收到,所以不要弄混,记住 rx 和 tx 都是相对自身主体来讲的。 UART 可以实现全双工,即可以同时进行发送数据和接收数据。
设计 FPGA 部分接收串口数据和发送串口数据的模块,最后把两个模块拼接起来,最后通过 loopback 测试(回环测试)来验证设计模块的正确性。所谓 loopback 测试就是发送端发送什么数据,接收端就接收什么数据。
串口 RS232 缺点:距离不远,传输速率相对较慢
串口 RS232优点:
1、很多传感器芯片或 CPU 都带有串口功能,目的是在使用一些传感器或 CPU 时可以通过串口进行调试,十分方便;
2、在较为复杂的高速数据接口和数据链路集合的系统中往往联合调试比较困难,可以先使用串口将数据链路部分验证后,再把串口换成高速数据接口。如在做以太网相关的项目时,可以在调试时先使用串口把整个数据链路调通,然后再把串口换成以太网的接口;
3、串口的数据线一共就两根,也没有时钟线,节省了大量的管脚资源。
<2>物理模型
设备被分为数据终端设备 DTE(计算机、路由)和 数据通讯设备 DCE(调制调解器)。我们以这种通讯模型讲解它们的信号线连接方式及各个 信号线的作用。
旧式台式计算机 RS-232 标准 COM 口(也称 DB9 接口):
串口线中的 RTS、CTS、DSR、DTR 及 DCD 信号,使用逻辑 1 表示信号有效,逻辑 0 表示信号无效。例如,当计算机端控制 DTR 信号线表示为逻辑 1 时,它是为了告知远端的调制调解器,本机已准备好接收数据, 0 则表示还没准备就绪。
<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 继续进入空闲状态,然后等待下一次的数据传输。如图为一个最基本的 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 的数据后,再接收一个波特时间的停止位。
<4>设计实践
1.硬件资源
Artix-7开发板上使用CH340芯片将Rx、Tx信号线转成USB,硬件电路图见参考图书。
在使用时需将 J9 口的 1、2 脚以及 3、4 脚用跳帽连接起来才能正常使 用。
2.程序设计
整体框图:
(1)uart_rx设计
uart_rx按照规定波特率将接受到的1bit串行数据转成8bit并行数据po_data,并有效并行数据有效的标志信号 po_data_flag
①波形图分析
波形设计思路详细解析:
第一部分:
首先画出三个输入信号,时钟和复位,另一个是串行输入数据 rx, rx 串行数据一开始经过了 两级寄存器。
理论上我们应该按照串口接收数据的时序要求找到 rx 的下降沿,然后开始接收起始位的数据,但为什么先将数据打了两拍呢?那就要先从跨时钟域会导致“亚稳态” 的问题上说起。
把一个矩形脉冲的上升沿或下降沿放大后会发现其上升沿和下降沿并不是瞬间被拉高或拉低的,而是有一个倾斜变化的过程,这在 运放中被称为“压摆率”。
如果 FPGA 的系统时钟刚好采集到 rx 信号上升沿或下降沿的中间位置附近(按照概率来讲,如果数据传输量足够大或传输速度足够快时一定会产生这种情况),即 FPGA 在接收 rx 数据时不满足内部寄存器的建立时间 Tsu(指触发器的时钟信号上升沿到来以前,数据稳定不变的最小时间)和保持时间 Th(指触发器的时钟信号上升沿到来以后,数据稳定不变的最小时间),此时 FPGA 的第一级寄存器的输出端在时钟沿到来之后比较长的一段时间内都处于不确定的状态,在 0 和 1 之间处于振荡状态,而不是 等于串口输入的确定的 rx 值。
如图为产生亚稳态的波形示意图,rx 信号经过 FPGA 中的第一级寄存器后输出的 rx_reg1 信号在时钟上升沿 Tco 时间后会有 Tmet(决断时间)的振荡时段,当第一 个寄存器发生亚稳态后,经过 Tmet 的振荡稳定后,第二级寄存器就能采集到一个相对稳定的值。但由于振荡时间 Tmet 是受到很多因素影响的,所以 Tmet 时间有长有短。如图所示,当 Tmet1 时间长到大于一个采样周期后,那第二级寄存器就会采集到亚稳态,但是从第二级寄存器输出的信号就是相对稳定的了。当然会人会问到第二级寄存器的 Tmet2 的持续时间会不会继续延长到大于一个采样周期?这种情况虽然会存在,但是其概率是极小的,寄存器本身就有减小 Tmet 时间让数据快速稳定的作用。 由于在 PC 机中波特率和 rx 信号是同步的,而 rx 信号和 FPGA 的系统时钟 sys_clk 是异步的关系,我们此时要做的是将慢速时钟域(PC 机中的波特率)系统中的 rx 信号同步到快速时钟域(FPGA 中的 sys_clk)系统中,所使用的方法叫电平同步,俗称“打两拍法”。所以 rx 信号进入 FPGA 后会首先经过一级寄存器,出现如图 26-13 所示的亚稳态现 象,导致 rx_reg1 信号的状态不确定是 0 还是 1,就会受其影响使其他相关信号做出不同的判断,有的判断到“0”有的判断到“1”,有的也进入了亚稳态并产生连锁反应,导致后 级相关逻辑电路混乱。为了避免这种情况,rx 信号进来后首先进行打一拍的处理,打一拍后产生 rx_reg1 信号。但 rx_reg1 可能还存在低概率的亚稳态现象,为了进一步降低出现亚稳态的概率,我们将从 rx_reg1 信号再打一拍后产生 rx_reg2 信号,使之能够较大概率保证 rx_reg2 信号是 0 或者 1 中的一种确定情况,这样 rx_reg2 所影响的后级电路就都是相对稳定的了。但一定要注意:打两拍后虽然能让信号稳定到 0 或者 1 中确定的值,但究竟是 0 还是 1 却是随机的,与打拍之前输入信号的值没有必然的关系。
注:单比特信号从慢速时钟域同步到快速时钟域需要使用打两拍的方式消除亚稳态。 第一级寄存器产生亚稳态并经过自身后可以稳定输出的概率为 70%~80%左右,第二级寄存器可以稳定输出的概率为 99%左右,后面再多加寄存器的级数改善效果就不明显了,所以 数据进来后一般选择打两拍即可。 另外单比特信号从快速时钟域同步到慢速时钟域还仅仅使用打两拍的方式会漏采数据,所以往往使用脉冲同步法或的握手信号法;而多比特信号跨时钟域需要进行格雷码编码(多比特顺序数才可以)后才能进行打两拍的处理,或者通过使用 FIFO、RAM 来处理数据与时钟同步的问题。 亚稳态振荡时间 Tmet 关系到后级寄存器的采集稳定问题,Tmet 影响因素包括:器件的生产工艺、温度、环境以及寄存器采集到亚稳态里稳定态的时刻等。甚至某些特定条件,如干扰、辐射等都会造成 Tmet 增长。
第二部分:
打两拍后的 rx_reg2 信号就是我们可以在后级逻辑电路中使用的相对稳定的信号,只 比 rx 信号延后两。下一步我们就可以根据串口接收数据的时序要求找到串口帧起始开始的标志——下降沿,然后按顺序接收数据。由第一部分的分析得 rx_reg1 信号可能是不稳定的, 而 rx_reg2 信号是相对稳定的,所以不能直接用 rx_reg1 信号和 rx_reg2 信号来产生下降沿标志信号,因为 rx_reg1 信号的不稳定性可能会导致由它产生的下降沿标志信号也不稳定。所以如图所示,我们将 rx_reg2 信号再打一拍,得到 rx_reg3 信号,用 rx_reg2 信 号和 rx_reg3 信号产生 staet_nedge 作为下降沿标志信号。
第三部分:
我们检测到了第一个下降沿,后面的信号将以下降沿标志信号 start_nedge 为条件开始接收一帧 10bit 的数据。但新的问题又出现了,我们的 rx 信号本身就是 1bit 的,如 果在判断第一个下降沿后,后面帧中的数据还可能会有下降沿出现,那我们会又产生一个start_nedge 标志信号。我们知道在 Verilog 代码中标志信号(flag)和使能信号(en)都是非常有用的,标志信号只有一拍,非常适合我们产生像下降沿标志这种信号,而使能信号就特别适合在此处使用,即对一段时间区域进行控制锁定。如图所示,当下降沿标志信号 start_nedge 为高电平时拉高工作使能信号 work_en(什么时候拉低在后面讲解),在 work_en 信号为高的时间区域内虽然也会有下降沿 start_nedge 标志信号产生,但是我们可 以根据 work_en 信号就可以判断出此时出现的 start_nedge 标志信号并不是我们想要的串口帧起始下降沿,从而将其过滤除掉。
开始接收一帧数据。我们使用的是 9600bps 的波特率 和 PC 机进行串口通信,PC 机的串口调试助手要将发送数据波特率调整为 9600bps。而 FPGA 内部使用的系统时钟是 50MHz,前面也进行过计算,得出 1bit 需要的时间约为 5208 个(因为一帧只有 10bit,细微的近似计数差别不会产生数据错误,但是如果计数值差的过大,则会产生接收数据的错误)系统时钟周期,那么我们就需要产生一个能计 5208 个数的计数器来依次接收 10 个比特的数据,计数器每计 5208 个数就接收一个新比特的数据。如 图 26-17 所示,计数器名为 baud_cnt,当 work_en 信号为高电平的时候就让计数器计数, 当计数器计 5208 个数(从 0 到 5207)或 work_en 信号为低电平时计数器清零。
第四部分:
现在我们可以根据波特率计数器一个一个接收数据了,我们发现 baud_cnt 计数 器在计数值为 0 到 5207 期间都是数据有效的时刻,那我们该什么时候取数据呢?理论上讲,在数据变化的地方取数是不稳定的,所以我们选择当 baud_cnt 计数器计数到 2603,即中间位置时取数最稳定(其实只要 baud_cnt 计数器在计数值不是在 0 和 5207 这两个最不稳定的时刻取数都可以,更为准确的是多次取值取概率最大的情况)。所以如图所示,在 baud_cnt 计数器计数到中点时产生一个时钟周期的 bit_flag 的取数标志信号,用于 指示该时刻的数据可以被取走。
也就是说我们需要准确的知道此时此刻接收的是第几比特,当接收够 10bit 数据后,我们就停止继续接收数据,等 rx 信号被拉高待恢复到空闲状态后再等待接收下一帧的数据。所以我们还需要 产生一个用于计数该时刻接收的数据是第几个比特的 bit_cnt 计数器。如图所示,刚好可以利用我们已经产生的 bit_flag 取数标志信号,对该信号进行计数既可以知道此时我们接收的数据是第几个比特了。这里我们只让 bit_cnt 计数器的计数值为 8 时再清零,虽然 bit_cnt 计数器的计数值从 0 计数到 8 只有 9 个 bit,但这 9 个 bit 中已经包含的我们所需要 的 8bit 有用的数据,最后的 1bit 停止位没有用,可以不用再进行计数了,但如果非要将 bit_cnt 计数器的计数值计数到 9 后再清零也是可以的。
第五部分:
我们接收到的 rx 信号是串行的,后面的系统要使用的是完整的 8bit 并行数据。 也就是说我们还需要将 1bit 串行数据转换为 8bit 并行数据的串并转换的工作,这也是我们在接口设计中常遇到的一种操作。串并转换就需要做移位,我们要考虑清楚什么时候开始移位,不能提前也不能推后,否则会将无用的数据也移位进来,所以我们需要卡准时间。 如图所示 PC 机的串口调试助手发送的数据是先发送的低位后发送的高位,所以我们接收的 rx 信号也是先接收的低位后接收的高位,我们采用边接收边移位的操作。移位操作的方法我们已经在前面的流水灯章节中讲过,这里不再重复。接下来我们需要确定移位开始和结束的时间。如图所示,当 bit_cnt 计数器的计数值为 1 时说明第一个有用数据已经接收到了,刚好剔除了起始位,就可以进行移位了。注意移位的条件,要在 bit_cnt 计 数器的计数值为 1 到 8 区间内且 bit_flag 取数标志信号同时为高时才能移位,也就是移动 7 次即可,接收最后 1bit 有用数据时就不需要再进行移位了。当移位 7 次后 1bit 的串行数据 已经变为 8bit 的并行数据了,此时产生一个移位完成标志信号 rx_flag。
第六部分:
最后一 点,rx_data 信号是参与移位的数据,在移位的过程中数据是变动的,不可以被后级模块所使用,而可以肯定的是在移位完成标志信号 rx_flag 为高时,rx_data 信号一定是移位完成的 稳定的 8bit 有用数据。如图所示,此时我们当移位完成标志信号 rx_flag 为高时让 rx_data 信号赋值给专门用于输出稳定 8bit 有用数据的 po_data 信号就可以了,但 rx_flag 信号又不能作为 po_data 信号有效的标志信号,所以需要将 rx_flag 信号再打一拍。最后输出的有用 8bit 数据为 po_data 信号和伴随 po_data 信号有效的标志信号 po_flag 信号。
到此为止我们 uart_rx 模块的波形就全部设计好了,此时再看时序图就能理解各个设计。为了获得数据到来标志start_nedge设置了三级寄存器;work_en确定了接收状态,对start_nedge进行管控,不再变化,并开启计数器baud_cnt ;在计数中央采集数据并记录数据个数bit_cnt ;结束置标志位rx_flag,但为稳定,滞后一拍得最终结果和标志位
②代码设计
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 //串转并后的数据有效标志信号
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//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 ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//插入两级寄存器进行数据同步,用来消除亚稳态
//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计数到BAUD_CNT_MAX - 1
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
可以看到,在2-5行声明参数方便修改;7-13行声明输入输出接口;20行定义局部变量;23-32行声明内部寄存器;40-58行rx数据经过三级寄存器赋值;start_nedge是判断(~rx_reg2) && (rx_reg3)进行赋值,对应波形图上的关系;在112行进行了移位赋值,rx_data <= {rx_reg3, rx_data[7:1]},使得低位在右;后面要注意start_nedge和work_en的关系,后面一大部分实际上是以baud_cnt为基石,所以在baud_cnt的赋值中引入work_en的限制即可(另一种不同思路是start_nedge的赋值引入work_en作为判断【&&~work_en】,然后baud_cnt引入start_nedge)。
③仿真设计
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
8-9行因要对输入信号赋值使用reg变量;21-27初始化系统时钟、全局复位和输入信号;29-40模拟拟发送 8 次数据;43行对时钟进行规定;77-86行进行实例化,实例名为代码设计中的模块名,实例化名可以是实例名加_inst;最关键的是48-70行对发送任务的定义,类似c语言中的函数,单独分析:
//定义一个名为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结束
注意这是第一次for循环的使用,for 括号中最后执行的内容只能写 i=i+1;任务以 task 开头,后面紧跟着的是任务名,调用时使用,以 endtask 结束;任务名紧接着是传入参数的定义,i是内部参数定义在括号外(类比c语言函数定义)
第一、第二、第三部分仿真波形如图所示,我们可以清晰的看到将 rx 信号打三拍的操作,并产生了串口帧起始的下降沿标志信号,以及 work_en 信号在串口帧起始的下降沿标志信号为高时拉高,baud_cnt 计数器在 work_en 信号为高时开始计数。
......
(2)uart_tx设计
①波形图分析
前6个信号通过uart_rx的设计可以知道其用途
下面我们就可以按照 5208 个系统时钟周期的波特率间隔来发送 1bit 数据了。理论上我们在第一个 5208 系统时钟周期内 的任意一个位置发送数据都可以,这和接收数据时要在中间位置不同,所以我们直接让当 baud_cnt 计数器的计数值为 1(选择其他的值也可以,但是尽量不要选择 baud_cnt 计数器的计数值为 0 或 5207 这种端点,因为容易出问题)的时候作为发送数据的点,产生 bit_flag 信号,并使 bit_cnt 计数值加一,而下一个 baud_cnt 计数器的计数值为 1 的时候和上一个正好相差 5208 个系统时钟周期,是完全可以满足要求的。发送完一帧数据后要将 work_en 信号拉低。
bit_cnt清零和work_en拉低的条件:让 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_cnt 计数器计数了,就可以把 work_en 信号拉低了,但同时还要将 baud_cnt 计数器清零,等待下一次发送数据时再从 0 开始计数。
②代码设计
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计数到BAUD_CNT_MAX - 1
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
③仿真设计
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
仿真这里测试了发送数据0~7
第三部分仿真波形如图所示,我们可以清晰地看到最后一个 bit_flag 信号为高的时刻,且 bit_cnt 计数器也计数到 9,将停止位发送出去,同时 work_en 信号拉低, baud_cnt 计数器检测到 work_en 信号为低电平后立刻清零并停止计数,等待下一次发送数据时再工作。
(3)顶层设计
①代码设计
`timescale 1ns/1ns
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 = 20'd9600 , //比特率
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
可以看到,顶层模块先定义好顶层输入输出线in/output wire,需要用到的参数parameter和内部模块的连线wire型变量;然后实例化设计好的模块,按照模块代码实例化参数,格式与模块定义时一致,·+参数名引出模块内部参数,括号内是顶层模块的变量,起连接作用。
②仿真设计
module tb_rs232();
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//wire define
wire tx ;
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg rx ;
//********************************************************************//
//***************************** 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
这里的仿真使用了task的嵌套,再实例化了顶层设计模块;从此对模块设计 .V 文件中的参数及实例化理解加深,类似于顶层模块的综合,仿真模块开始时定义的变量时为了后面的实例化所服务的,要么起连线作用(wire),要么起赋值仿真作用(reg),在实例化的括号里连接。
3.总结
“在本章的 Testbench 的设计中我们第一次使用到了 task 任务以及 for 循环语句,这两个语法都在仿真中使用的较多,虽然都是可以综合的但还是推荐初学者尽量不要在 RTL 代码中使用,尤其是对它们理解不深刻的情况下。而我们在 Testbench 中使用就不用担心这么多,且可以大大简化我们的代码,提高效率,是十分好用的,也推荐大家以后再 Testbench 中多尝试使用。”
以及更深入了解Verilog HDL代码的编写,深刻理解了参数、变量、实例化。
知识点总结:
1. 理解亚稳态产生的原理,掌握单比特数据从慢速时钟域到快速时钟域处理亚稳态的方法。
2. 学会使用边沿检测,并记住代码的格式,理解原理。(第三级寄存器和第二级寄存器共同构成下降沿检测(~rx_reg2) && (rx_reg3))
3. 串并转换是接口中很常用的一种方法,用到了移位,要熟练掌握。
4. 掌握 loopback 测试的方法,以后用于我们模块中代码的调试。