目录
一:串口通信简介
二:三种常见的数据通信方式—RS232串口通信
2.1 实验任务
2.2 串口接收模块的设计
2.2.1 代码设计
2.3 串口发送模块的设计
2.3.1 代码设计
2.4 顶层模块编写
2.4.1 代码设计
2.4.2 仿真验证代码
2.4.3 仿真结果
2.4.4 板上验证
一:串口通信简介
通信方式一般分为串行通信和并行通信。并行通信是指多比特数据同时通过并行线进行传送。这种传输方式通信线多、成本高,故不宜进行远距离通信,通常传输距离小于 30 米。串行通信是指数据在一条数据线上,一比特接一比特地按顺序传送的方式。这种运输方式通常节省传输线,大大降低使用成本,但数据传送速度慢。综上可知,串行通信主要应用于长距离、低速率的通信场合。本次实验我们主要讲解下串行通信。
串行通信一般有
2
种通信方式:
同步串行通信
和
异步串行通信
。
同步串行通信需要通信双方在同一时钟的控制下同步传输数据;
异步串行通信是指具有不规则数据段传送特性的串行数据传输。在常见的通信总线协议中,I2C,SPI 属于同步通信而 UART 属于异步通信。同步通信的通信双方必须先建立同步,即双方的时钟要调整到同一个频率,收发双方不停地发送和接收连续的同步比特流。异步通信在发送字符时,发送端可以在任意时刻开始发送字符,所以,在 UART 通信中,数据起始位和停止位是必不可少的。
UART
串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收,如下图所示。对于 PC
来说它的
TX
要和对于
FPGA
来说的
RX
连接,同样
PC
的
RX
要和
FPGA
的
TX
连接,如果是两个TX 或者两个
RX
连接那数据就不能正常被发送出去或者接收到。
UART
在发送或接收过程中的一帧数据由
4
部分组成,起始位、数据位、奇偶校验位和停止位,如下图所示。
起始位:当不传输数据时,UART 数据传输线通常保持高电平。若要开始数据传输,发送 UART 会将传输线从高电平拉到低电平并保持 1 个波特率周期。当接收 UART 检测到高到低电压跃迁时,便开始以波特率对应的频率读取数据帧中的位。
数据帧:
数据帧包含所传输的实际数据。如果使用奇偶校验位,数据帧长度可以是
5
位到
8 位。如果不使用奇偶校验位,数据帧长度可以是
9
位。在很多情况下,数据以最低有效位优先发送。
奇偶校验:奇偶性描述数字是偶数还是奇数。通过奇偶校验位,接收 UART 判断传输期间是否有数据发生改变。
停止位
:为了表示数据包结束,发送
UART
将数据传输线从低电压驱动到高电压并保持
1
到
2
位时间。
波特率:即每秒传输的位数
(bit)
。一般选波特率都会有
9600
,
19200
,
115200等选项。其实意思就是每秒传输这么多个比特位数(bit)
。
二:三种常见的数据通信方式—RS232串口通信
rs232通信:
RS-232 是单端输入输出,
RS-232
标准的串口最常见的接口类型为
DB9
,样式如图
27.1.3
所示,工业控制领域中用到的工控机 一般都配备多个串口,很多老式台式机也都配有串口。但是笔记本电脑以及较新一点的台式机都没有串 口,它们一般通过 USB
转串口线(下图所示
)来实现与外部设备的串口通信。
DB9
接口定义以及各引脚功能说明如下图所示,我们一般只用到其中的
2
(
RXD
)、
3
(
TXD
)、
5 (GND)引脚,其他引脚在普通串口模式下一般不使用,如果大家想了解,可以自行百度下。
2.1 实验任务
本节实验任务是上位机通过串口调试助手发送数据给zynq开发板,zynq开发板
PL
端通过 USB
_UART
串口接收数据并将接收到的数据发送给上位机,完成串口数据环回实验。主要模块如下表所示:
2.2 串口接收模块的设计
首先完成串口接收模块的设计,串口接收模块我们的输入信号主要有系统时钟信号、系统复位信号与串口接收端口。当我们将一帧的数据接收完成后,那么要告诉下级模块已经将一帧数据接收完了,所以输出为接收完成标志和串口接收数据信号。
模块接口框图如下所示:
串口接收模块端口与功能描述如下表所示:
绘制波形图:
在绘制波形图之前,我们首先要确定串口通信的数据格式及波特率。在这里我们选择串口比较常用的 一种模式,数据位为 8
位,停止位为
1
位,无校验位,波特率为
115200bps
。则传输一帧数据的时序图如下图所示:
2.2.1 代码设计
//串转并
module zdyz_rs232_rx(
input sys_clk , //系统时钟
input sys_rst_n , //系统复位,低有效
input uart_rxd , //UART 接收端口
output reg uart_rx_done, //UART 接收完成信号
output reg [7:0] uart_rx_data //UART 接收到的数据
);
parameter CLK_FREQ = 5000_0000; //系统时钟频率
parameter UART_BPS = 115200 ; //串口波特率
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS; //为得到指定波特率,对系统时钟计数 BPS_CNT 次
reg uart_rxd_d0;
reg uart_rxd_d1;//定义两个D触发器进行异步打拍处理
reg rx_flag ; //接收过程标志信号
reg [3:0] rx_cnt ; //接收数据位计数器
reg [15:0] baud_cnt ; //波特率计数器(位宽为16,防止溢出)
reg [7:0 ] rx_data_t ; //接收数据寄存器
wire start_flag;//开始接收的标志,下降沿到来。
//打两拍:波特率时钟和系统时钟不同步,为异步信号,所以要进行打拍处理,防止产生亚稳态
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
end
end
assign start_flag = (uart_rxd_d0 == 0)&&(uart_rxd_d1 == 1);//下降沿到来的表示方法
// rx_flag接收信号的拉高与拉低
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rx_flag <= 1'b0;
else if(start_flag) //检测到起始位
rx_flag <= 1'b1; //接收过程中,标志信号 rx_flag 拉高
//在停止位一半的时候,即接收过程结束,标志信号 rx_flag 拉低
else if((rx_cnt == 4'd9) && (baud_cnt == BAUD_CNT_MAX/2 - 1))//rx_flag 要提前拉低,防止其影响下一帧数据的接收
rx_flag <= 1'b0;
else
rx_flag <= rx_flag;
end
//波特率的计数器计数逻辑
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
baud_cnt <= 0;
else if(rx_flag)begin
if(baud_cnt == BAUD_CNT_MAX - 1)
baud_cnt <= 0;
else
baud_cnt <= baud_cnt + 1;
end
else
baud_cnt <= 0;
end
//位计数实现逻辑
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
rx_cnt <= 0;
else if(rx_flag)begin
if(baud_cnt == BAUD_CNT_MAX - 1)
rx_cnt <= rx_cnt + 1;
else
rx_cnt <= rx_cnt;
end
else
rx_cnt <= 0;//其他情况下都为0,所以不用担心计数超过9,且其计数也不会超过9,当rx_flag为0时就不计数了
end
//根据 rx_cnt 来寄存 rxd 端口的数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
rx_data_t <= 0;
else if(rx_flag)begin //系统处于接收过程时
if(baud_cnt == BAUD_CNT_MAX/2 - 1)begin//判断 baud_cnt 是否计数到数据位的中间
case(rx_cnt)
1:rx_data_t[0] <= uart_rxd_d1; //寄存数据的最低位
2:rx_data_t[1] <= uart_rxd_d1;
3:rx_data_t[2] <= uart_rxd_d1;
4:rx_data_t[3] <= uart_rxd_d1;
5:rx_data_t[4] <= uart_rxd_d1;
6:rx_data_t[5] <= uart_rxd_d1;
7:rx_data_t[6] <= uart_rxd_d1;
8:rx_data_t[7] <= uart_rxd_d1;//寄存数据的高低位
default:rx_data_t <= rx_data_t;
endcase
end
else
rx_data_t <= rx_data_t;
end
else
rx_data_t <= 0;
end
//给接收完成信号和接收到的数据赋值
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
uart_rx_done <= 0;
uart_rx_data <= 0;
end
//当接收数据计数器计数到停止位,且 baud_cnt 计数到停止位的中间时
else if((rx_cnt == 4'd9) && (baud_cnt == BAUD_CNT_MAX/2 - 1))begin
uart_rx_done <= 1; //拉高接收完成信号
uart_rx_data <= rx_data_t;//并对 UART 接收到的数据进行赋值
end
else begin
uart_rx_done <= 0;
uart_rx_data <= uart_rx_data;
end
end
endmodule
2.3 串口发送模块的设计
串口发送模块我们的输入信号主要有系统时钟信号、系统复位信号以及发送使能信号和待发送数据, 输出信号主要有发送忙状态标志和串口发送端口。模块接口框图如下所示:
串口发送模块端口与功能描述如下表所示:
2.3.1 代码设计
module zdyz_rs232_tx(
input sys_clk , //系统时钟
input sys_rst_n , //系统复位,低有效
input uart_tx_en , //UART 的发送使能(发送是有标志起始时间的,接收是根据下降沿到来就开始进入起始位)
input [7:0] uart_tx_data, //UART 要发送的数据
output reg uart_txd , //UART 发送端口
output reg uart_tx_busy //发送忙状态信号
);
//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 115200 ; //串口波特率
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS; //为得到指定波特率,对系统时钟计数 BPS_CNT 次
reg [7:0] tx_data_t; //发送数据寄存器
reg [3:0] tx_cnt ; //发送数据位计数器
reg [15:0] baud_cnt ; //波特率计数器
//当 uart_tx_en 为高时,寄存输入的并行数据(防止发生变化影响发送),并拉高 BUSY 信号
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)begin
tx_data_t <= 0;
uart_tx_busy <= 0;
end
else if(uart_tx_en)begin
tx_data_t <= uart_tx_data;
uart_tx_busy <= 1;
end
/*为了确保环回实验的成功,在程序的 36 行我们将 uart_tx_busy 提前 1/16 个停止位拉低。尽管串口发送数据只是接收数据的反过程,
理论上在传输的时间上是一致的,但是考虑到我们模块里计算波特率会有较小的偏差,并且串口对端的通信设备(如电脑等)收发数据的波特率
同样可能会出现较小的偏差,因此为了确保环回实验的成功,这里将发送模块的停止位略微提前结束。需要说明的是,较小偏差的波特率在
串口通信时是允许的,同样可以保证数据可靠稳定的传输*/
else if((tx_cnt == 4'd9) && (baud_cnt == BAUD_CNT_MAX - BAUD_CNT_MAX/16))begin
uart_tx_busy <= 0;
tx_data_t <= 0;
end
else begin
uart_tx_busy <= uart_tx_busy;
tx_data_t <= tx_data_t;
end
end
//波特率的计数器计数逻辑
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
baud_cnt <= 0;
else if(uart_tx_busy)begin //当处于发送过程时,波特率计数器(baud_cnt)进行循环计数
if(baud_cnt == BAUD_CNT_MAX - 1)
baud_cnt <= 0; //计数达到一个波特率周期后清零
else
baud_cnt <= baud_cnt + 1;
end
else
baud_cnt <= 0;//发送过程结束时计数器清零
end
//位计数实现逻辑
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
tx_cnt <= 0;
else if(uart_tx_busy)begin//处于发送过程时 tx_cnt 才进行计数
if(baud_cnt == BAUD_CNT_MAX - 1)//当波特率计数器计数到一个波特率周期时
tx_cnt <= tx_cnt + 1; //发送数据计数器加 1
else
tx_cnt <= tx_cnt;
end
else
tx_cnt <= 0;//其他情况下都为0,所以不用担心计数超过9,且其计数也不会超过9,当rx_flag为0时就不计数了
end
//根据 tx_cnt 来给 uart 发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
uart_txd <= 1 ;//空闲态为1
else if(uart_tx_busy)begin
case(tx_cnt)
4'd0 : uart_txd <= 1'b0 ; //起始位
4'd1 : uart_txd <= tx_data_t[0]; //数据位最低位
4'd2 : uart_txd <= tx_data_t[1];
4'd3 : uart_txd <= tx_data_t[2];
4'd4 : uart_txd <= tx_data_t[3];
4'd5 : uart_txd <= tx_data_t[4];
4'd6 : uart_txd <= tx_data_t[5];
4'd7 : uart_txd <= tx_data_t[6];
4'd8 : uart_txd <= tx_data_t[7]; //数据位最高位
4'd9 : uart_txd <= 1'b1 ; //停止位
default:uart_txd <= 1'b1 ;
endcase
end
else
uart_txd <= 1 ; //空闲时发送端口为高电平
end
endmodule
2.4 顶层模块编写
在顶层模块中完成了对其余各个子模块的例化。本次实验我们需要设置
2
个参数变量,分别是系统时钟频率 CLK_FREQ
与串口波特率
UART_BPS
,大家使用时可以根据不同的系统时钟频率以及所需要的串口波特率设置这两个变量。zynq7020开发板上的系统时钟为 50MHz
,所以这里将
CLK_FREQ
参数设为50000000。在前文我们说过本次实验的波特率设为
115200
,所以这里的参数
UART_BPS
就设为
115200
。我们可以尝试将串口波特率 UART_BPS
设置为其他值(如
9600
),在模块例化时会将这个变量传递到串口接收与发送模块中,从而实现不同速率的串口通信。
2.4.1 代码设计
module top_uart(
input sys_clk , //外部 50MHz 时钟
input sys_rst_n, //系外部复位信号,低有效
//UART 端口
input uart_rxd , //UART 接收端口(串行数据)
output uart_txd //UART 发送端口
);
parameter CLK_FREQ = 50000000; //定义系统时钟频率
parameter UART_BPS = 115200 ; //定义串口波特率
//wire define
wire uart_rx_done; //UART 接收完成信号
wire [7:0] uart_rx_data; //UART 接收数据(并行数据)
wire uart_tx_busy;
//*****************************************************
//** main code
//*****************************************************
//串口接收
zdyz_rs232_rx #(
.CLK_FREQ (CLK_FREQ),
.UART_BPS (UART_BPS)
)zdyz_rs232_rx(
.sys_clk(sys_clk) , //系统时钟
.sys_rst_n(sys_rst_n) , //系统复位,低有效
.uart_rxd(uart_rxd) , //UART 接收端口
.uart_rx_done(uart_rx_done), //UART 接收完成信号
.uart_rx_data(uart_rx_data)//UART 接收到的数据
);
zdyz_rs232_tx #(
.CLK_FREQ (CLK_FREQ),
.UART_BPS (UART_BPS)
)zdyz_rs232_tx(
.sys_clk(sys_clk) , //系统时钟
.sys_rst_n(sys_rst_n) , //系统复位,低有效
.uart_tx_en(uart_rx_done) , //UART 的发送使能
.uart_tx_data(uart_rx_data), //UART 要发送的数据
.uart_txd(uart_txd) , //UART 发送端口
.uart_tx_busy(uart_tx_busy) //发送忙状态信号
);
endmodule
2.4.2 仿真验证代码
`timescale 1ns/1ns //仿真的单位/仿真的精度
module tb_uart_loopback();
//parameter define
parameter CLK_PERIOD = 20;//时钟周期为 20ns
//reg define
reg sys_clk ; //时钟信号
reg sys_rst_n; //复位信号
reg uart_rxd ; //UART 接收端口
//wire define
wire uart_txd ; //UART 发送端口
//*****************************************************
//** main code
//*****************************************************
//发送 8'h55 8'b0101_0101
initial begin
sys_clk <= 1'b0;
sys_rst_n <= 1'b0;
uart_rxd <= 1'b1;
#200
sys_rst_n <= 1'b1;
#1000
uart_rxd <= 1'b0; //起始位
#8680
uart_rxd <= 1'b1; //D0
#8680
uart_rxd <= 1'b0; //D1
#8680
uart_rxd <= 1'b1; //D2
#8680
uart_rxd <= 1'b0; //D3
#8680
uart_rxd <= 1'b1; //D4
#8680
uart_rxd <= 1'b0; //D5
#8680
uart_rxd <= 1'b1; //D6
#8680
uart_rxd <= 1'b0; //D7
#8680
uart_rxd <= 1'b1; //停止位
#8680
uart_rxd <= 1'b1; //空闲状态
end
//50Mhz 的时钟,周期则为 1/50Mhz=20ns,所以每 10ns,电平取反一次
always #(CLK_PERIOD/2) sys_clk = ~sys_clk;
//例化顶层模块
top_uart top_uart(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
.uart_rxd (uart_rxd ),
.uart_txd (uart_txd )
);
endmodule
2.4.3 仿真结果
2.4.4 板上验证
上述程序已经经过板上验证了,代码正确,注意若发送和接受都选择了16进制的话,那么就输入0~F,如果不选择16进制的话,就可以任意输入阿拉伯数字和26个字母。