第30章、串口RS232
UART,通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),是一种通用的数据通信协议,也是 异步串行通信口(串口) 的总称,它在 发送数据时 将并行数据转换成串行数据 来传输,在 接收数据时 将接收到的 串行数据转换成并行数据。它包括了 RS232、RS499、RS423、RS422和 RS485 等接口标准规范和总线标准规范。
【理论】
【串口简介】
【1 串口是异步通信接口】
三大低速总线:UART、SPI、IIC
UART :异步通信接口。异步通信中的 接收方 并不知道数据什么时候会到达,所以 双方收发端都有各自的时钟,在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受方在数据的 起始位 和 停止位 的帮助下实现 信息同步。
SPI、IIC:同步通信接口,同步通信中双方使用频率一致的时钟,在数据传输过程中 时钟伴随着数据一起传输,发送方和接收方使用的时钟都是由主机提供的。
【2 串口通信只有2根线,可实现 全双工】
UART通信只有两根信号线,一根是发送数据端口线叫 tx(Transmitter),一根是接收数据端口线叫 rx(Receiver);
对于PC来说它的 tx 要和对于FPGA来说的 rx 连接,同样PC的 rx 要和FPGA的 tx 连接,如果是两个tx或者两个rx连接那数据就不能正常被发送出去和接收到,所以不要弄混,记住rx和tx都是相对自身主体来讲的。UART可以实现 全双工,即可以 同时进行发送数据和接收数据。
【3 串口优势】
串口RS232传输数据的距离虽然不远,传输速率也相对较慢,但是串口依然被广泛的用于电路系统的设计中,串口的优势主要表现在以下几个方面:
1、很多传感器芯片或CPU都带有串口功能,目的是在使用一些传感器或CPU时可以通过串口进行调试,十分方便;
2、在较为复杂的高速数据接口和数据链路集合的系统中往往联合调试比较困难,可以先使用串口将数据链路部分验证后,再把串口换成高速数据接口。如在做以太网相关的项目时,可以在调试时先使用串口把整个数据链路调通,然后再把串口换成以太网的接口;
3、串口的数据线一共就两根,也没有时钟线,节省了大量的管脚资源。
【RS-232 信号线】
最初,RS-232串口标准常用于计算机、路由与调制调解器(MODEN,俗称“猫”)之间的通讯,在这种通讯系统中,设备被分为 数据终端设备DTE(计算机、路由)和 数据通讯设备DCE(调制调解器)。
旧式的台式计算机中一般会有 RS-232 标准的 COM 口(也称 DB9 接口),其中接线口以 针式 引出信号线的称为 公头,以 孔式 引出信号线的称为 母头。在计算机中一般引出公头接口,而在调制调解器设备中引出的一般为母头。
在目前使用的串口通讯中,一般只使用 RXD、TXD以及GND 三条信号线,直接传输数据信号。
由于两个通讯设备之间的 收发信号(RXD与TXD) 应交叉相连,所以调 制调解器端的DB9母头 的 收发信号接法一般与公头的相反,两个设备之间连接时,只要使用“直通型”的串口线连接起来即可。
串口线中的RTS、CTS、DSR、DTR及DCD信号,使用逻辑 1 表示信号有效,逻辑 0表示信号无效。例如,当计算机端控制DTR信号线表示为逻辑 1 时,它是为了告知远端的调制调解器,本机已准备好接收数据, 0 则表示还没准备就绪。
【RS-232 通信协议】
1、RS232是UART的一种,没有时钟线,只有 1位宽的 rx(接收)和tx(发送) 两根数据线。
2、rx、tx 位宽:
(1)rx位宽为1bit,PC机通过串口调试助手往FPGA发8bit数据时,FPGA通过串口线 rx一位一位地接收,从 最低位到最高位 依次接收,最后在FPGA里面位拼接成8比特数据。
(2)tx位宽为1bit,FPGA通过串口往PC机发8bit数据时,FPGA把8bit数据通过 tx线一位一位的传给PC机,从 最低位到最高位 依次发送,最后 上位机 通过 串口助手 按照RS232协议把这一位一位的数据位拼接成8bit数据。
3、RS-232 帧结构:
(1)串口数据的 发送与接收 基于帧结构(10bit):
(2)每一帧除了中间包含 8bit 有效数据外,还在每一帧的开头都必须有一个 起始位(固定为0);在每一帧的结束时也必须有一个停止位(固定为1),即最基本的帧结构(不包括校验等)有10bit。
(3)在不发送或者不接收数据的情况下,rx和tx处于空闲状态(保持高电平),如果有数据帧传输时,首先会有一个起始位,然后是8bit的数据位,接着有1bit的停止位,然后rx和tx继续进入空闲状态,然后等待下一次的数据传输。
5、波特率:
(1)码元:在信息传输通道中,携带数据信息的信号单元为码元(因为串口是 1bit 进行传输,所以其码元代表 一个二进制数)。
(2)波特率(码元传输速率):每秒钟通过信号传输的码元数即为码元传输速率(波特率)。常用符号“Baud”表示,其单位为“波特每秒(Bps)”。
(3)串口常见的波特率有 4800、9600、115200等,本实验选用9600的波特率。
6、比特率:
(1)比特率(位传输速率):每秒钟 通信信道传输的 信息量 称为 位传输速率(比特率),其单位为“每秒比特数(bps)”。
(2)比特率=波特率 * 单个调制状态对应的二进制位数:比特率可由波特率计算得出,如果使用的是9600的波特率,其串口的比特率为:9600Bps * 1bit= 9600bps。
7、1 Baud 数据 对应的 计数个数:
(1)波特率为9600,则 串口发送或者接收 1bit数据的时间 为 1/9600s,即( (1s * 10^9)/9600bit)ns ;
(2)系统时钟频率为50MHz,1个时钟周期为 20ns ;
(3)cnt = (1s * 10^9)ns / 9600bit)ns / 20ns ≈ 5208个系统时钟周期,即每个bit数据传输要在50MHz的时钟频率下计数5208次。
8、上位机通过串口发8bit数据时,会自动在发8位有效数据前发一个波特时间的起始位,也会自动在发完8位有效数据后发一个停止位。同理,串口助手接收上位机发送的数据前,必须检测到一个波特时间的起始位才能开始接收数据,接收完8bit的数据后,再接收一个波特时间的停止位。
【实战】
本实验任务是设计FPGA部分 接收串口数据 和 发送串口数据 的模块,再把两个模块拼接起来,最后通过 loopback测试(回环测试)来验证设计模块的正确性。
loopback测试:发送端发送什么数据,接收端就接收什么数据。这是常用的一种测试手段,若测试成功,说明从数据发送端到数据接收端之间的 数据链路 是正常的。
【电路说明】
MAX3232为RS232收发器芯片。由于 RS-232 电平标准的信号 不能直接被控制器直接识别,所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL”的电平信号,才能实现通讯。
【1 电平转换:TTL --> RS-232电平】
(1)根据通信使用的电平标准不同,串口通信可分为 TTL 标准及 RS-232 标准:
(2)FPGA串口输入输出引脚为TTL电平, 用3.3V代表逻辑“1”, 0V代表逻辑“0”。
(3)用 MA3232 芯片对 TTL 电平(fpga引脚)及 RS-232电平(RS-232引脚)的信号进行互相转换。
【2 USB转串口:使用芯片CH340,可使用USB线进行串口调试】
在使用时需将J2、J3口的1、2脚用跳帽连接起来才能正常使用,即开发板上的J2、J3中的 TXD与RX短接、RXD与TX短接。
【3 跳帽选择】
同样的,使用 RS232串口也要选择相应的连接。要使用跳帽将 J6口的5脚和7脚,6脚和8脚 连接起来才能正常使用,即开发板上 J6中的TX与T1INT短接、RX与R1OUT短接。
【程序设计】
【串口数据接收模块(uart_rx)】
模块功能:
(1)接收串行数据:接收 PC机上的串口调试助手 发送的固定波特率的 数据,串口接收模块 按照串口的协议 准确接收 串行数据;
(2)转换并行数据:解析提取有用数据后需将其转化为 并行数据,并生成1个数据有效标志信号。(并行数据传输的效率更高);
【注】数据有效标志信号的到来就说明数据在该时刻是稳定有效的,对后级模块或系统起到一个指示作用。
PC机通过串口调试助手发过来的信号 没有时钟,所以FPGA在接收数据的时候要约定好一个固定的波特率,一个比特一个比特地接收数据,本实验选择的波特率为9600bps,也是RS232接口中相对较慢的一种速率。
第一部分:rx信号跨时钟域,消除亚稳态
首先画出三个输入信号,必不可少的两个输入信号是时钟和复位,另一个是 串行输入数据rx,如下图所示,可见rx串行数据一开始直接打了两拍(经过了两级寄存器),理论上应该按照串口接收数据的时序要求找到 rx的下降沿,然后开始接收 起始位的数据,但为什么先将数据打了两拍呢?那就要先从跨时钟域会导致“亚稳态”的问题上说起。
【跨时钟域】
(1)慢速时钟域 同步到 快速时钟域 :
打两拍 的方式 消除亚稳态。第一级寄存器产生亚稳态并经过自身后可以稳定输出的概率为70%~80%左右,第二级寄存器可以稳定输出的概率为99%左右,后面再多加寄存器的级数改善效果就不明显了,所以数据进来后一般选择打两拍即可。
(2)快速时钟域 同步到 慢速时钟域 :
① 单比特信号:仅仅使用打两拍的方式会 漏采数据,要先使用 脉冲同步法 或 握手信号法,再 打两拍;
② 多比特信号 : 先 格雷码编码(多比特顺序数才可以) 后 打两拍,或者通过使用 FIFO、RAM 来处理数据与时钟同步的问题。
亚稳态振荡时间Tmet关系到后级寄存器的采集稳定问题,Tmet影响因素包括:器件的生产工艺、温度、环境以及寄存器采集到亚稳态里稳定态的时刻等。甚至某些特定条件,如干扰、辐射等都会造成Tmet增长。
【亚稳态】
(1)矩形脉冲的 上升沿 或 下降沿 放大后会发现其上升沿和下降沿并 不是瞬间被拉高或拉低 的,而是有一个 倾斜变化 的过程,这在运放中被称为“压摆率”
(2)建立时间 Tsu:指触发器的时钟信号上升沿到来以前,数据稳定不变的最小时间;
保持时间 Th:指触发器的时钟信号上升沿到来以后,数据稳定不变的最小时间;
(3)亚稳态:FPGA的 系统时钟 刚好采集到 rx信号 上升沿或下降沿的 中间位置 附近,导致不满足内部寄存器的 建立时间Tsu 和 保持时间Th,进而导致FPGA的第一级寄存器的输出端在时钟沿到来之后比较长的一段时间内都处于 不确定的状态,在0和1之间处于振荡状态,而不是等于串口输入的 确定的rx值。
如下图1所示为产生亚稳态的波形示意图,rx信号经过FPGA中的第一级寄存器后输出的 rx_reg1信号在时钟上升沿Tco(延迟)时间后会有Tmet(决断时间)的振荡时段,当第一个寄存器发生亚稳态后,经过Tmet的振荡稳定后,第二级寄存器就能采集到一个相对稳定的值。
如下图2所示,当Tmet1时间长到 大于一个采样周期 后,那第二级寄存器就会采集到 亚稳态,但是从第二级寄存器输出的信号就是相对稳定的了。当然会人会问到第二级寄存器的Tmet2的持续时间会不会继续延长到大于一个采样周期?这种情况虽然会存在,但是其概率是极小的,寄存器本身就有减小Tmet时间让数据快速稳定的作用。
注意:打两拍后虽然能让信号稳定到0或者1中确定的值,但究竟是0还是1却是随机的,与打拍之前输入信号的值没有必然的关系。
第二部分:根据消除亚稳态后的 rx信号 找到数据帧起始位的 下降沿
打两拍后的rx_reg2信号是可以在后级逻辑电路中使用的 相对稳定的信号,下一步就可以根据串口接收数据的时序要求找到串口帧起始 开始的标志——下降沿,然后按顺序接收数据。由第一部分的分析得 rx_reg1信号可能是不稳定的,而 rx_reg2信号是相对稳定的,所以不能直接用rx_reg1信号和rx_reg2信号来产生下降沿标志信号,因为rx_reg1信号的不稳定性可能会导致由它产生的下降沿标志信号也不稳定。
如下图所示,我们将rx_reg2信号再打一拍,得到rx_reg3信号,用rx_reg2信号和rx_reg3信号产生staet_nedge作为下降沿标志信号。
第三部分:防止误判,设置使能信号;设置波特率计数器
检测到了第一个下降沿后,将以 下降沿标志信号start_nedge 为条件开始接收 一帧10bit的数据。
但会出现 在判断第一个下降沿后,遇到后面帧中的数据还可能出现的下降沿,进而又产生一个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个系统时钟周期,那么我们就需要产生一个能计5208个数的计数器来依次接收10个比特的数据,计数器每计5208个数就接收一个新比特的数据。
如下图所示,计数器名为baud_cnt,当work_en信号为高电平的时候就让计数器计数,当计数器计5208个数(从0到5207)或work_en信号为低电平时计数器清零。
第四部分:根据波特率计数器接收数据
baud_cnt计数器在计数值为 0到5207期间都是数据有效的时刻,当baud_cnt计数器计数到 2603,即 中间位置 时取数最稳定(其实只要baud_cnt计数器在计数值不是在0和5207这两个最不稳定的时刻取数都可以,更为准确的是多次取值取概率最大的情况)
所以如下图所示,在baud_cnt计数器计数到中点时产生一个时钟周期的bit_flag的取数标志信号,用于指示该时刻的数据可以被取走。
串口的数据是基于帧的,所以每接收完一帧数据 rx信号都要被拉高,即恢复到 空闲状态重新判断串口帧起始下降沿,以等待下一帧数据的接收,且一帧数据中还包括了起始位和停止位这种无用的数据,而有价值的数据只是中间的8bit数据,也就是说需要准确的知道此时此刻接收的是第几比特,当接收够10bit数据后,就停止继续接收数据,等rx信号被拉高待恢复到空闲状态后再等待接收下一帧的数据。所以还需要产生一个用于计数该时刻接收的数据是第几个比特的 bit_cnt计数器。
如下图所示,利用已经产生的 bit_flag取数标志信号,对该信号进行计数,就可以知道此时接收的数据是第几个比特。这里只让 bit_cnt计数器的 计数值为8 时再清零,虽然bit_cnt计数器的计数值从0计数到8只有9个bit,但这9个bit中已经包含的我们所需要的8bit有用的数据,最后的1bit停止位没有用,可以不用再进行计数了,但如果非要将bit_cnt计数器的计数值计数到9后再清零也是可以的。
如下图所示,当bit_cnt计数器计数到8且 ①处的bit_flag取数标志信号同时为高,说明已经接收到了所有的8bit有用数据,这两个条件必须同时满足时才能让work_en信号拉低。如果仅仅把bit_cnt计数器的计数值计数到8作为work_en信号拉低的条件,而掉①处的bit_flag取数标志信号为高这个条件,就会使work_en信号在绿色虚线位置处拉低,导致最后1bit数据丢失,致使后面接收的帧出错甚至接收不到数据。
第五部分:串并转换
接收到的 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信号。
module uart_rx( input sys_clk , input sys_rst_n , input rx ,
output reg [7:0] po_data , output reg po_flag
);
parameter UART_BPS = 'd9600 ,//数据传输波特率 9600
CLK_FREQ = 'd50_000_000 ;//系统时钟频率 50MHZlocalparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS ;
reg rx_reg1 ;
reg rx_reg2 ;
reg rx_reg3 ;
reg [2:0] bit_cnt ;
wire bit_flag ;//将接收的rx信号打两拍,消除亚稳态。再打一拍用消除亚稳态后的rx_reg2、rx_reg3进行下降沿判断
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rx_reg1 <= 1'b1 ;
else
rx_reg1 <= rx ;
end
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rx_reg2 <= 1'b1 ;
else
rx_reg2 <= rx_reg1 ;
end
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rx_reg3 <= 1'b1 ;
else
rx_reg3 <= rx_reg2 ;
end//找到起始位下降沿,数据开始传输
reg nedge_flag ;
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
nedge_flag <= 1'b0 ;
else if((rx_reg31'b1)&&(rx_reg21'b0))
nedge_flag <= 1'b1 ;
else
nedge_flag <= 1'b0 ;
end
//rx_en:接收数据工作使能信号
reg rx_en ;
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
rx_en <= 1'b0 ;
else if(nedge_flag1'b1)
rx_en <= 1'b1 ;
else if((bit_cnt8)&&(bit_flag))
rx_en <= 1'b0 ;
else
rx_en <= rx_en ;
end//波特率 9600,1s(10^9ns)传输数据 (9600*1)bit,时钟周期 20ns,传输1bit数据需要ns:(10^9ns)/9600
// --> 传输1bit数据需要时钟周期个数:((10^9ns)/9600)/20ns = 5208个,计数 0-5297
reg [12:0] baud_cnt ;
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
baud_cnt <= 13'b0 ;
else if((rx_en==1'b1)&& (baud_cnt == (BAUD_CNT_MAX - 1'b1)))
baud_cnt <= 13'b0 ;
else if(rx_en == 1'b1)
baud_cnt <= baud_cnt + 1'b1 ;
endassign bit_flag = (baud_cnt == ((BAUD_CNT_MAX/2)-1)) ;//在数据传输中间点赋值,更加准确
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
bit_cnt <= 3'b0 ;
else if( rx_en && bit_flag)
bit_cnt <= bit_cnt + 1'b1 ;
else
bit_cnt <= 3'b0 ;
end//串行转并行,移位
reg [7:0] rx_data ;
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
rx_data <= 8'b0 ;
else if((bit_cnt > 1'b0) && (bit_flag == 1'b1))
rx_data <= {rx_reg3,rx_data[7:1]};
end
//移位完成标志信号
reg rx_flag ;
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rx_flag <= 1'b0 ;
else if(rx_en)
rx_flag <= 1'b1 ;
end//输出信号,8bit并行数据
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
po_data <= 8'b0 ;
else if(rx_flag)
po_data <= rx_data ;
else
po_data <= 8'b0 ;
end
//输出完成标志信号
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
po_flag <= 1'b0 ;
else
po_flag <= rx_flag ;
end
endmodule
`timescale 1ns/1ns
module tb_uart_rx();
reg sys_clk ;
reg sys_rst_n ;
reg rx ;
wire [7:0] po_data ;
wire po_flag ;
uart_rx uart_rx_inst01(
. sys_clk (sys_clk ) ,
. sys_rst_n (sys_rst_n) ,
. rx (rx ) ,
. po_data (po_data ) ,
. po_flag (po_flag )
);
//初始化时钟、复位、输入信号
initial begin
sys_clk = 1'b1 ;
sys_rst_n = 1'b0 ;
rx <= 1'b1; //对应起始位前面的一段
#200
sys_rst_n = 1'b1 ;
end
always #10 sys_clk = ~sys_clk ;
//模拟发送 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
//定义一个名为 rx_bit 的任务,每次发送的数据有 10 位
//data 的值分别为 0~7 由 j 的值传递进来
//任务以 task 开头,后面紧跟着的是任务名,调用时使用
task rx_bit(
input [7:0] data //传递到任务中的参数,调用任务的时候从外部传进来一个 8 位的值
);
integer i; //定义一个常量
for(i=0; i<10; i=i+1) begin //用 for 循环产生一帧数据,for 括号中最后执行的内容只能写 i=i+1
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 结束
endmodule
【串口数据发送模块(uart_tx)】
功能是将 FPGA 中的数据以固定 的 波特率 发送到 PC 机的串口调试助手并打印出来,串口发送模块按照串口的协议组装成帧,然后按照顺序一个比特一个比特将数据发送至 PC 机,而 FPGA 内部的数据往往都是 并行的,需将其转化为串行数据发送。
FPGA 发送的串行数据同样没有时钟,所以要和 PC 机接约定 好使用相同的波特率,一个一个地发送比特,为了后面做串口的回环测试我们仍选择使用 9600bps 的波特率。
第一部分:
输入信号:时钟、复位、8bit 有用数据和数据有效标志信号。
8bit 有用数据 pi_data 和数据有效标志信号 pi_flag 是上一级系统发送过来的,设计的模块只需要负责接收即可。当数据有效标志信号 pi_flag 为高时表示数据已经是稳定的可以被使用的,这时就可以把 8bit 数据接收过来了,然后再将这个 8bit 数据按照顺序一个 一个串行发送出去。
baud_cnt:已经和 PC 机约定好了使用9600bps的波特率,所以发送 1bit 数据需要的时间也约为 5208 个系统时钟周期,这就需要产生一个和接收数据时一样的波特率计数器,取名为 baud_cnt,该计数器每计 5208 个数就发送一个新比特的数据,一共发送 10 个比特。
work_en:设置一个 work_en 的工作使能信号,当检测到数据有效标志信号 pi_flag 为高电平时拉高工作使能信号 work_en(什么时候拉低在后面讲解),因为 work_en 信号是持续的高电平,所以当 work_en 信号为高电平时波特率计数器 baud_cnt 进行计数。
第二部分:
1、发送数据按照1帧发送,1帧包含10bit:1bit起始位(默认0)、8bit数据位、1bit结束位 (默认1)
2、在baud_cnt计数到一半时将 bit_flag拉高,开始发送数据;
3、bit_cnt:在 bit_flag拉高 时加1,用于计数发送数据的个数。
第三部分:数据按顺序一个一个发出去,因为接收的时候是先接收的低位后接收的高位,发送的时候也是先发送低位,后发送高位(先低后高)。
【注】
①、bit_cnt计数器什么时候清零的问题:假设让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( input sys_clk , //系统时钟50MHz input sys_rst_n , //全局复位 input [7:0] pi_data , //模块输入的8bit数据 input pi_flag , //并行数据有效标志信号
output reg tx //串转并后的1bit数据
);
////
// Parameter and Internal Signal *******************//
////
//localparam define
parameter UART_BPS = 'd9600 ;//串口波特率
parameter CLK_FREQ = 'd50_000_000 ;//时钟频率
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ;//发送1bit数据需要的5208个时钟周期
//reg define
reg [12:0] baud_cnt ;
reg bit_flag ;
reg [3:0] bit_cnt ;
reg tx_en ;
////
// Main Code ****************************//
//***********************************************//
//tx_en:接收数据工作使能信号
always@(posedge sys_clk or negedge sys_rst_n)begin
if(sys_rst_n == 1'b0)
tx_en <= 1'b0;
else if(pi_flag == 1'b1)
tx_en <= 1'b1;
else if((bit_flag == 1'b1) && (bit_cnt == 4'd9))
tx_en <= 1'b0;
end//baud_cnt:波特率计数器计数,从0计数到5207
always@(posedge sys_clk or negedge sys_rst_n)begin
if(sys_rst_n == 1'b0)
baud_cnt <= 13'b0;
else if((baud_cnt == BAUD_CNT_MAX - 1) || (tx_en == 1'b0))
baud_cnt <= 13'b0;
else if(tx_en == 1'b1)
baud_cnt <= baud_cnt + 1'b1;
end//bit_flag:当baud_cnt计数器计数到1时让bit_flag拉高一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)begin
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;
end//bit_cnt:数据位数个数计数,10个有效数据(含起始位和停止位)到来后计数器清零
always@(posedge sys_clk or negedge sys_rst_n)begin
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)
bit_cnt <= bit_cnt + 1'b1;
end//tx:输出数据在满足rs232协议(起始位为0,停止位为1)的情况下一位一位输出
always@(posedge sys_clk or negedge sys_rst_n)begin
if(sys_rst_n == 1'b0)
tx <= 1'b1; //空闲状态时为高电平
else if(bit_flag == 1'b1)
case(bit_cnt)
0 : tx <= 1'b0; //起始位,默认为0
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; //停止位,默认为1
default : tx <= 1'b1;
endcase
end
endmodule
`timescale 1ns/1ps
module tb_uart_tx();
reg sys_clk ;
reg sys_rst_n ;
reg [7:0] pi_data ;
reg pi_flag ;
wire tx ;
uart_tx uart_tx_inst01(
. sys_clk (sys_clk ), //系统时钟50MHz
. sys_rst_n (sys_rst_n ), //全局复位
. pi_data (pi_data ), //模块输入的8bit数据
. pi_flag (pi_flag ), //并行数据有效标志信号
. tx (tx ) //串转并后的1bit数据
);
initial begin
sys_clk = 1'b1 ;
sys_rst_n = 1'b0 ;
#200
sys_rst_n = 1'b1 ;
end
always #10 sys_clk = ~sys_clk ;
initial begin
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;
// #(5208*20*10); //每发送1bit数据需要5208个时钟周期,所以需要数据延时(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
endmodule
【顶层模块】
在本章的最开始讲过串口可以作为很好用的调试工具使用,比如和其他系统一起做回环测试,也称之为loopback的测试。串口发送模块和串口接收模块因为波特率相同,功能又互补,所以他们自身就可以直接连接到一起进行工作来实现最简单的loopback测试。
大致流程为: PC机的串口调试助手发送一串数据,经过FPGA 后再传回到 PC机的串口调试助手中打印显示。
将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_tx模块的 8bit数据输入端 pi_data和数据有效标志信号输入端pi_flag,将接收到的并行数据重新封装成帧后串行发送到tx端,
④、uart_tx模块的tx端再把数据传给rs232的tx端,rs232的tx端再将数据传回到PC机的串口调试助手中打印显示。
实现了发送什么就接收什么,如果发送和接收的数据不一致,那就说明整个链路存在错误。
【代码】
module top_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 ;
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
);
//------------------------uart_tx_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 (po_data ) , //input [7:0] pi_data
.pi_flag (po_flag ) , //input pi_flag
.tx (tx ) //output tx
);
endmodule
`timescale 1ns/1ns
module tb_top_rs232();
reg sys_clk ;
reg sys_rst_n ;
reg rx ;
wire tx ;
top_rs232 top_rs232_inst01(
. sys_clk (sys_clk ) , //系统时钟50MHz
. sys_rst_n (sys_rst_n ) , //全局复位
. rx (rx ) , //串口接收数据
. tx (tx ) //串口发送数据
);
initial begin
sys_clk = 1'b1 ;
sys_rst_n = 1'b0 ;
#200
sys_rst_n = 1'b1 ;
end
always #10 sys_clk = ~sys_clk ;
//调用任务rx_byte()
initial begin
#200
rx_byte();
end
//定义任务
task rx_byte(); //相当于父类任务,调用子类rx_bit,无需输入
integer j ;
for (j=0;j<8;j=j+1)begin
rx_bit(j); //rx_byte 为rx_bit[0]-rx_bit[7],共8个8bit数据,传输8次
end
endtask
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 ; //停止位
default: rx <= rx ;
endcase
#(5208*20);
end
endtask
endmodule
标签:tx,rx,sys,flag,信号,串口,RS232 From: https://www.cnblogs.com/jasonleeeee/p/17660565.html