通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称作UART。UART 是一种通用的数据通信协议,也是异步串行通信口(串口)的总称,它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。它包括了 RS232、RS499、RS423、RS422 和 RS485 等接口标准规范和总线标准规范。
1. 理论知识
1.1 串口简介
串口作为常用的三大低速总线(UART、SPI、IIC)之一,在设计众多通信接口和调试时占有重要地位。但 UART 和 SPI、IIC 不同的是,它是异步通信接口,异步通信中的接收方并不知道数据什么时候会到达,所以双方收发端都要有各自的时钟,在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受方是在数据的起始位和停止位的帮助下实现信息同步的。而 SPI、IIC 是同步通信接口,同步通信中双方使用频率一致的时钟,在数据传输过程中时钟伴随着数据一起传输,发送方和接收方使用的时钟都是由主机提供的。
UART 通信只有两根信号线,一根是发送数据端口线叫 tx(Transmitter),一根是接收数据端口线叫 rx(Receiver),如图 1 所示,对于 PC 来说它的 tx 要和对于 FPGA 来 说的 rx 连接,同样 PC 的 rx 要和 FPGA 的 tx 连接,如果是两个 tx 或者两个 rx 连接那数据就不能正常被发送出去和接收到,所以不要弄混,记住 rx 和 tx 都是相对自身主体来讲的。 UART 可以实现全双工,即可以同时进行发送数据和接收数据。
图1 串口通信连接图
我们的任务是设计 FPGA 部分接收串口数据和发送串口数据的模块,最后我们把两个模块拼接起来,其结构如图 2 所示,最后通过 loopback 测试(回环测试)来验证设计模块的正确性。所谓 loopback 测试就是发送端发送什么数据,接收端就接收什么数据,这也是非常常用的一种测试手段,如果 loopback 测试成功,则说明从数据发送端到数据接收端之间的数据链路是正常的,以此来验证数据链路的畅通。
图2 串口回环模块框图
串口 RS232 传输数据的距离虽然不远,传输速率也相对较慢,但是串口依然被广泛的用于电路系统的设计中,串口的好处主要表现在以下几个方面: 1、很多传感器芯片或 CPU 都带有串口功能,目的是在使用一些传感器或 CPU 时可以通过串口进行调试,十分方便; 2、在较为复杂的高速数据接口和数据链路集合的系统中往往联合调试比较困难,可以先使用串口将数据链路部分验证后,再把串口换成高速数据接口。如在做以太网相关的项目时,可以在调试时先使用串口把整个数据链路调通,然后再把串口换成以太网的接口; 3、串口的数据线一共就两根,也没有时钟线,节省了大量的管脚资源。
1.2 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 继续进入空闲状态,然后等待下一次的数据传输。
如图 3 所示一个最基本的 RS232 帧结构。
图3 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 的数据后,再接收一个波特时间的停止位。
2. 实战演练
2.1实验目标
设计并实现基于串口 RS232 的数据收、发模块,使用收、发模块,完成串口数据回环实验。
2.2 程序设计
图4 工程整体框图
2.3 串口数据接收模块
我们先设计串口接收模块,该模块的功能是接收通过 PC 机上的串口调试助手发送的固定波特率的数据,串口接收模块按照串口的协议准确接收串行数据,解析提取有用数据后需将其转化为并行数据,因为并行数据在 FPGA 内部传输的效率更高,转化为并行数据后同时产生一个数据有效信号标志信号伴随着并行的有效数据一同输出。
注:为什么还需要输出一个伴随并行数据有效的标志信号,这是因为后级模块或系统在使用该并行数据的时候可能无法知道该时刻采样的数据是不是稳定有效的,而数据有效标志信号的到来就说明数据才该时刻是稳定有效的,起到一个指示作用。当数据有效标志信号为高时,该并行数据就可以被后级模块或系统使用了。
2.3.1 模块框图
图5 串口接收模块框图
图6 串口接收模块波形图
2.3.2 代码编写
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,
output reg po_flag
);
parameter BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
reg rx_reg1 ;
reg rx_reg2 ;
reg rx_reg3 ;
reg start_nedge ;
reg work_en ;
reg [15: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)
rx_reg1 <= 1'b1;
else
rx_reg1 <= rx;
//rx_reg2:第二级寄存器,寄存器空闲状态复位为 1
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
rx_reg2 <= 1'b1;
else
rx_reg2 <= rx_reg1;
//rx_reg3:第三级寄存器和第二级寄存器共同构成下降沿检测
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
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)
start_nedge <= 1'b0;
else if ((rx_reg2 == 1'b0) &&(rx_reg3 == 1'b1) &&(work_en == 1'b0))
start_nedge <= 1'b1;
else
start_nedge <= 1'b0;
//work_en:接收数据工作使能信号
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
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)
baud_cnt <= 16'd0;
else if ((baud_cnt == BAUD_CNT_MAX - 1'b1) || (work_en == 1'b0))
baud_cnt <= 16'd0;
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)
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)
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)
rx_data <= 8'd0;
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)
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)
po_data <= 8'd0;
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)
po_flag <= 1'b0;
else
po_flag <= rx_flag;
endmodule
2.3.3 仿真验证
`timescale 1ns / 1ns
module tb_uart_rx( );
reg sys_clk;
reg sys_rst_n;
reg rx;
wire [7:0] po_data;
wire po_flag;
//初始化系统时钟、全局复位和输入信号
initial
begin
sys_clk = 1;
sys_rst_n <= 0;
rx <= 1;
#20
sys_rst_n <= 1;
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 循环产生一帧数据,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
uart_rx
#(
.UART_BPS (9600),
.CLK_FREQ (50_000_000)
)
uart_rx_inst
(
. sys_clk (sys_clk),
. sys_rst_n(sys_rst_n),
. rx (rx),
. po_data (po_data),
. po_flag (po_flag)
);
endmodule
2.3.4 仿真波形
完成目标图形
2.4 串口数据发送模块
接下来我们继续进行串口发送模块的设计,该模块的功能是将 FPGA 中的数据以固定的波特率发送到 PC 机的串口调试助手并打印出来,串口发送模块按照串口的协议组装成帧,然后按照顺序一个比特一个比特将数据发送至 PC 机,而 FPGA 内部的数据往往都是并行的,需将其转化为串行数据发送。
2.4.1 模块框图
图7 串口发送模块框图
图8 串口发送模块波形图
2.4.2 代码编写
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 数据
);
parameter BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
reg work_en;
reg [15:0] baud_cnt;
reg bit_flag;
reg [3:0] bit_cnt;
//work_en:接收数据工作使能信号
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
work_en <= 1'b0;
else if (pi_flag == 1'b1)
work_en <= 1'b1;
else if ((bit_cnt == 4'd9)&&(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)
baud_cnt <= 16'b0;
else if ((baud_cnt == BAUD_CNT_MAX - 1)||(work_en == 1'b0))
baud_cnt <= 16'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)
bit_flag <= 1'b0;
else if (baud_cnt == 16'b1)
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)
bit_cnt <= 4'd0;
else if ((bit_cnt == 4'd9)&&(bit_flag == 1'b1))
bit_cnt <= 4'd0;
else if (bit_flag == 1'b1)
bit_cnt <= bit_cnt + 1;
//tx:输出数据在满足 rs232 协议(起始位为 0,停止位为 1)的情况下一位一位输出
always @ (posedge sys_clk or negedge sys_rst_n)
if (!sys_rst_n)
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
2.4.3 仿真验证
`timescale 1ns / 1ps
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;
//模拟发送 7 次数据,分别为 0~7
initial
begin
pi_data <= 8'd0;
pi_flag <= 1'b0;
#200
//数据0
pi_data <= 8'd0;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
//数据1
pi_data <= 8'd1;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
//数据2
pi_data <= 8'd2;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
//数据3
pi_data <= 8'd3;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
//数据4
pi_data <= 8'd4;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
//数据5
pi_data <= 8'd5;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
//数据6
pi_data <= 8'd6;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
//数据7
pi_data <= 8'd7;
pi_flag <= 1'b1;
#20
pi_flag <= 1'b0;
#(5208*10*20);
end
uart_tx
#(
.UART_BPS (9600), //串口波特率
.CLK_FREQ (50_000_000) //时钟频率
)
uart_tx_inst
(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.pi_data (pi_data), //模块输入的 8bit 数据
.pi_flag (pi_flag), //并行数据有效标志信号
.tx (tx) //串转并后的 1bit 数据
);
endmodule
2.4.4 仿真波形
2.5 顶层模块
2.5.1 模块框图
2.5.2 代码编写
module rs232
(
input wire sys_clk ,
input wire sys_rst_n,
input wire rx ,
output wire tx
);
wire [7:0] po_data ;
wire po_flag ;
uart_rx
#(
.UART_BPS (9600),
.CLK_FREQ (50_000_000)
)
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 (9600), //串口波特率
.CLK_FREQ (50_000_000) //时钟频率
)
uart_tx_inst
(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.pi_data (po_data), //模块输入的 8bit 数据
.pi_flag (po_flag), //并行数据有效标志信号
.tx (tx) //串转并后的 1bit 数据
);
endmodule
2.5.3 仿真验证
`timescale 1ns / 1ps
module tb_rs232();
reg sys_clk ;
reg sys_rst_n ;
reg rx ;
wire tx ;
//初始化系统时钟、全局复位和输入信号
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
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//传递到任务中的参数,调用任务的时候从外部传进来一个 8 位的值
);
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
rs232 rs232_inst
(
. sys_clk (sys_clk),
. sys_rst_n (sys_rst_n),
. rx (rx),
. tx (tx)
);
endmodule
2.5.4 仿真波形
以上就是混了这么多天写出来的东西.......