目录
说明:
本人使用的是野火家Xilinx Spartan6系列开发板及配套教程,仅供参考。
开发软件:ise14.7 仿真:modelsim 10.5
1. 内容导读
通用异步收发传输器( Universal Asynchronous Receiver/Transmitter) ,通常称作UART。 UART 是一种通用的数据通信协议,也是异步串行通信口(串口)的总称,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。 它包括了RS232、 RS499、 RS423、 RS422 和 RS485 等接口标准规范和总线标准规范。
本章进行串口 RS232 相关知识的学习,通过理论与实践,最终设计并实现基于 RS232 的串口收、发功能模块,并完成串口数据回环实验。
2. 理论知识学习
2.1 串口简介
串口作为常用的三大低速总线(UART、 SPI、 IIC)之一,在设计众多通信接口和调试时占有重要地位。但 UART 和 SPI、 IIC 不同的是,它是异步通信接口,异步通信中的接收方并不知道数据什么时候会到达,所以双方收发端都要有各自的时钟,在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受方是在数据的起始位和停止位的帮助下实现信息同步的。而 SPI、 IIC 是同步通信接口,同步通信中双方使用频率一致的时钟,在数据传输过程中时钟伴随着数据一起传输,发送方和接收方使用的时钟都是由主机提供的。
UART 通信只有两根信号线,一根是发送数据端口线叫 tx(Transmitter),一根是接,收数据端口线叫 rx(Receiver),如图所示,对于 PC 来说它的 tx 要和对于 FPGA 来说的 rx 连接,同样 PC 的 rx 要和 FPGA 的 tx 连接,记住 rx 和 tx 都是相对自身主体来讲的。UART 可以实现全双工,即可以同时进行发送数据和接收数据。
串口的优点:
1.很多传感器芯片或 CPU 都带有串口功能,进行串口调试方便。
2.在较为复杂的高速数据接口和数据链路集合的系统中往往联合调试比较困难,可以先使用串口将数据链路部分验证后,再把串口换成高速数据接口。
3.串口数据线两根使用简单。
缺点:短距离,传输速率相对较慢。
2.2 rs232信号线
在最初的应用中, RS-232 串口标准常用于计算机、路由与调制调解器(MODEN,俗称“猫” )之间的通讯,在这种通讯系统中,设备被分为数据终端设备 DTE(计算机、路由)和数据通讯设备 DCE(调制调解器)。
信号线的连接方式:
准备一根串口线连接在旧式的台式计算机中一般会有 RS-232 标准的 COM 口(也称 DB9 接口)如下图以及FPGA的公头接口上。
由上图可知我们有许多信号线,但一般只使用RXD、 TXD 以及 GND 三条信号线,直接传输数据信号。
现在的电脑大多是USB接口,故我们采用USB数据线连接两端。
2.3 rs232通信协议简介
RS232 是 UART 的一种,没有时钟线,只有两根数据线,分别是 rx 和 tx,这两根线都是1bit 位宽的(也就是传输一位二进制数)。但由于FPGA内部接收模块与发送模块是并行传输的,所以要接收模块要分别经行串转并,并转串的处理。
串口数据的发送与接收是基于帧结构的,即一帧一帧的发送与接收数据。每一帧除了中间包含 8bit 有效数据外,还在每一帧的开头都必须有一个起始位,固定为 0;在每一帧的结束时也必须有一个停止位,固定为 1,即最基本的帧结构(不包括校验等)有10bit。在不发送或者不接收数据的情况下, rx 和 tx 处于空闲状态,此时 rx 和 tx 线都保持高电平。下图是一个最基本的 RS232 帧结构。
波特率:在信息传输通道中,携带数据信息的信号单元叫码元(因为串口是 1bit 进行传的,所以其码元就是代表一个二进制数),每秒钟通过信号传输的码元数称为码元的传输速率,简称波特率, 常用符号“Baud”表示,其单位为“波特每秒(Bps)”。串口常见的波特率有 4800、 9600、 115200 等,我们选用 9600 的波特率进行串口章节的讲解。
比特率:每秒钟通信信道传输的信息量称为位传输速率,简称比特率,其单位为“每秒比特数(bps)”。比特率可由波特率计算得出,公式为:比特率=波特率 * 单个调制状态对应的二进制位数。如果使用的是 9600 的波特率,其串口的比特率为: 9600Bps *1bit= 9600bps。
在9600Bps下,计算传输1bit数据需要花多少个时钟周期?每秒传输9600个码元数,即传1位数据需要1/9600秒。在50Mhz的系统时钟下,一个时钟脉冲为20ns,需要花 (1s * 10^9)ns /
9600bit)ns / 20ns ≈ 5208 个系统时钟周期。
3. 实操
3.1 实验目标
PC 机的串口调试助手发送一串数据,经过 FPGA 后再传回到 PC 机的串口调试助手中打印显
示。
3.2 硬件资源
由于RS-232电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个"电平转换芯片" (MA3232 芯片)转换成控制器能识别的"TTL校准"的电平信号,才能实现通讯。
结构示意图:
RS232 收发器电路:
同时为了方便使用,开发板中还搭载了 USB 转串口的芯片 CH340,供大家使用 USB 线进行串口调试。
3.3 整体说明
实验整体框图
该实验主要分为三个模块。
3.4 串口数据接收模块
3.4.1 模块说明
该模块功能是按照串口的协议接收来自PC 机上的串口调试助手发送的固定波特率的串行数据,解析提取有用数据,再转化为并行数据(并行数据在 FPGA 内部传输的效率更高)后同时产生一个数据有效信号标志信号(后级模块或系统在使用该并行数据的时候可能无法知道该时刻采样的数据是否有效的)伴随着并行的有效数据一同输出。模块框图如下图。
3.4.2 波形设计
uart_rx 功能整体的波形图如下图所示。(在写代码前设计好波形图可以增加写代码功能的效率)
rx_reg1和rx_reg2对rx进行了打两拍处理,原因是时钟信号和数据传输rx是异步的关系,输出很容易产生亚稳态。亚稳态是指触发器无法在某个规定的时间段内到达一个可以稳定的状态。产生亚稳态的本质原因是在时钟上升沿的建立时间或保持时间段输入数据不稳定,导致寄存器输出数据在一段时间也不稳定但在亚稳态后下一个时钟沿来临前会稳定下来(数据随机)。所以使用打两拍的方式消除亚稳态。示意图如下。
有人会疑惑Reg1数据随机那就不是正确的传递了输入数据rx,而且还往下传递了这个数据。确实是这样,但是数据的维持时间比较长(例如本实验就有5208个时钟周期),等到下一个时钟上升沿来临那就不会产生亚稳态且传递的数据正确,错误的数据占比很小。
rx_reg3与rx_reg2电平取反再取与,产生staet_nedge信号,作为开始接收数据的标志。如果将rx_reg3换成rx_reg1,那会有一段亚稳态,导致代码综合出错。
3.4.3 RTL代码
`timescale 1ns/1ns
//接受8位串行数据后转为并行数据
module uart_rx
#(
parameter UART_BPS = 'd9600,
parameter CLK_FREQ = 'd50_000_000
)
(
input wire sys_clk ,
input wire sys_rst_n , //全局复位
input wire rx , //串口接收数据
output reg [7:0] po_data , //串转并后的8bit数据
output reg po_flag //串转并后的数据有效标志信号
);
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ; //接受1bit数据所需时间
//增加代码复用性
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:第一级寄存器,
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_reg1 <= 1'b1; //寄存器空闲状态复位为1
else
rx_reg1 <= rx;
//rx_reg2:第二级寄存器,
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)) //rx_reg2 = 0 且 rx_reg_3 = 1
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
3.4.4 仿真部分
模拟PC机的串口调试助手产生一组串行数据。
`timescale 1ns/1ns
module tb_uart_rx();
reg sys_clk;
reg sys_rst_n;
reg rx;
wire [7:0] po_data;
wire po_flag;
always #10 sys_clk = ~sys_clk;
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
task rx_bit(input [7:0] data);
integer i; //定义一个变量
//integer类型也是一种寄存器数据类型,integer类型的变量为有符号数
for(i=0; i<10; i=i+1) begin //不可用i=i++的方式
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结束
uart_rx uart_rx_inst(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.rx (rx ),
.po_data (po_data ),
.po_flag (po_flag )
);
endmodule
3.5 串口数据发送模块
3.5.1 模块说明
该模块的功能是将 FPGA 中的数据以固定的波特率发送到 PC 机的串口调试助手并打印出来。而FPGA 内部的接收的数据往往都是并行的,需将其转化为串行数据发送。
3.5.2 波形设计
uart_tx 功能整体的波形图如下图。
3.5.3 RTL代码
`timescale 1ns/1ns
//并行数据转化为8bit数据发出fpga
module uart_tx
#(
parameter UART_BPS = 'd9600,
parameter CLK_FREQ = 'd50_000_000
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [7:0] pi_data , //模块输入的8bit数据
input wire pi_flag , //并行数据有效标志信号
output reg tx //并转串后的1bit数据
);
//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 ;
//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:发送1bit数据所需时间
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
3.5.4 仿真部分
`timescale 1ns/1ns
module tb_uart_tx();
reg sys_clk;
reg sys_rst_n;
reg [7:0] pi_data;
reg pi_flag;
wire tx;
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20;
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
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
uart_tx uart_tx_inst(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.pi_data (pi_data ),
.pi_flag (pi_flag ),
.tx (tx )
);
endmodule
3.6 顶层模块
3.6.1 模块说明
我们将FPGA内部传输部分看成一个整体,其设计框图如下图。该模块功能是将串口数据接收模块与串口数据发送模块的各信号之间的连接,需注意的是注意位宽的匹配要精准。
3.6.2 RTL代码
`timescale 1ns/1ns
//FPGA中RS232通信模块
module rs232
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire rx , //串口接收数据
output wire tx //串口发送数据
);
parameter UART_BPS = 20'd9600 , //波特率
CLK_FREQ = 26'd50_000_000 ; //时钟频率
wire [7:0] po_data;
wire po_flag;
//串口接受数据模块
uart_rx
#(
.UART_BPS (UART_BPS ), //串口波特率
.CLK_FREQ (CLK_FREQ ) //时钟频率
)
uart_rx_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.rx (rx ),
.po_data (po_data ),
.po_flag (po_flag )
);
//串口发送数据模块
uart_tx
#(
.UART_BPS (UART_BPS ), //串口波特率
.CLK_FREQ (CLK_FREQ ) //时钟频率
)
uart_tx_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.pi_data (po_data ),
.pi_flag (po_flag ),
.tx (tx )
);
endmodule
3.6.3 仿真部分
`timescale 1ns/1ns
module tb_rs232();
wire tx ;
reg sys_clk ;
reg sys_rst_n ;
reg rx ;
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
rx <= 1'b1;
#20;
sys_rst_n <= 1'b1;
end
initial begin
#200
rx_byte(); //调用任务rx_byte
end
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。for 括号中最后执行的内容不可写成i= i++
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; //起始位为0
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; //终止位为1
endcase
#(5208*20); //每发送1位数据延时5208个时钟周期
end
endtask
rs232 rs232_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.rx (rx ),
.tx (tx )
);
endmodule
3.7 上板测试
由于本人电脑usb转串口故障,这里不展示从PC机发数据到接受数据的图片。
4. 实验总结
1.理解亚稳态的概念以及如何避免亚稳态。
2.如何提取数据以及将数据串转并,并转串。
3.使用for函数产生一帧数据。