【接口时序】3、UART串口收发的原理与Verilog实现

时间:2023-03-08 19:32:25

一、软件平台与硬件平台

  软件平台:

    1、操作系统:Windows-8.1

    2、开发套件:ISE14.7

    3、仿真工具:ModelSim-10.4-SE

  硬件平台:

    1、FPGA型号:XC6SLX45-2CSG324

    2、USB转UART芯片:Silicon Labs CP2102GM

二、原理介绍

  串口是串行接口(serial port)的简称,也称为串行通信接口或COM接口。串口通信是指采用串行通信协议(serial communication)在一条信号线上将数据一个比特一个比特地逐位进行传输的通信模式。 串口按电气标准及协议来划分,包括RS-232、RS-422、RS485等。其中最常用的就是RS-232接口。

  RS-232接口有以下三个特性:

    1、用了一个9针的连接器"DB-9"(早期的电脑有用25针的连接器"DB-25")

    2、允许全双工通信(即通过串口发送数据和接收数据可以同时进行)

    3、通信的最大速率大约在10KBytes/s左右

  DB-9接口的实物图如下图所示(早期电脑主机后面可以看到这个接口,现在一般都用USB转串口线进行串口通信):

【接口时序】3、UART串口收发的原理与Verilog实现【接口时序】3、UART串口收发的原理与Verilog实现

虽然DB-9接头一共有9根线,但是实现串口通信只需要其中的3根线就可以了,分别是:

    1、pin-2:RXD(receive data),接收串行数据

    2、pin-3:TXD(transmit data),发送串行数据

    3、pin-5:GND(ground),地线

  在串口通信中,数据在1位宽的单条线路上进行传输,一个字节的数据要分为8次,由低位到高位按顺序一位一位的进行传送,这个过程称为数据的"串行化(serialized)"过程。由于串口通信是一种异步通信协议,并没有时钟信号随着数据一起传输,而且空闲状态(没有数据传输的状态)的时候,串行传输线为高电平1,所以发送方发送一个字节数据之前会先发送一个低电平0,接收方收到这个低电平0以后就知道有数据要来了,准备开始接收数据从而实现一次通信。串口通信的时序如下图所示:

【接口时序】3、UART串口收发的原理与Verilog实现

  串口通信的规范如下:

    1、空闲状态(没有数据传输的状态)下,串行传输线上为高电平1

    2、发送方发送低电平0表示数据传输开始,这个低电平表示传输的起始位

    3、8-bit的数据位(1 Byte)是从最低位开始发送,最高位最后发送

    4、数据位的最高位发送完毕以后的下一位是奇偶校验位,这一位可以省略不要,同时,当不发送奇偶校验位的时候接收方也相应的不接收校验位

    5、最后一位是停止位,用高电平1表示停止位

  下面以发送字节0x55为例来说明整个的发送过程:

    先把0x55转化成二进制为:01010101。显然0x55的最低位bit 0是1,次低位bit 1是0,……..,最高位bit 7是0,由于串口是从最低位开始发送一个字节,所以0x55各个位的发送顺序是1-0-1-0-1-0-1-0,波形如下图所示

【接口时序】3、UART串口收发的原理与Verilog实现

下面在给出一个波形,根据上面的规则也可以很容易判断这是发送字节0x13的波形

【接口时序】3、UART串口收发的原理与Verilog实现

接下来的最后一个问题是:串口传输的速度是多少?

  实际上,串口传输的速度用波特率(baudrate)来指定。波特率表示的是每秒发送的比特数,单位是bps(bits-per-seconds),例如,1000 bauds表示1秒钟发送了1000个比特,或者说每个比特持续的时间是1ms。关于串口发送的波特率是有一组标准的规定的,并不是随便一个数字。常用的波特率标准有:

    1、1200 bps

    2、9600 bps (常用)

    3、38400 bps

    4、115200 bps (常用,而且通常情况下是我们能用的最快的波特率)

  波特率为115200 bps时,每个比特持续的时间为(1/115200)=8.7us,所以发送8个bit(1 Byte)需要的时间是8*8.7us=69us。在不考虑奇偶校验位的情况下,发送一个字节还需要发送额外的1个起始位和1个停止位,所以发送1个字节实际所需要的最少时间是10*8.7us=87us,这意味着1s(1000000us)中能发送的字节数为(1000000/87) = 11494,所以在波特率为115200bps的情况下,串口传输数据的速率约为11.5KB/s。而有些电脑的串口有时候需要一个更长的停止位,比如1.5位或2位的停止位,那么发送一个字节所需要的时间比只有一个比特停止位的情况所耗费的时间更长,在这种情况下,串口的传输速率会低于10.5KB/s。

  通过上面一系列的总结以后,可以得出FPGA与PC之间的串口通信主要包括三个模块:波特率产生模块、发射模块和接收模块。

三、目标功能

  1、编写发送模块的verilog代码,并往PC上连续不断发送0x00~0xff这些数据,PC上用串口调试助手进行接收并以16进制显示出来

  2、在第一个功能的基础上编写接收模块的verilog代码,接收模块接收到第一个功能中发送模块发送的数据以后,用接收到的并行数据的低四位驱动板上的四个LED灯

  3、编写一个顶层模块把发送模块和接收模块均例化进去,然后从PC的串口调试助手上发送数据到FPGA,FPGA接收到数据以后把接收的数据返回给串口调试助手显示

四、设计思路与Verilog代码编写

4.1、发送模块波特率时钟的设计与实现

  本节以波特率为115200bps为例来说明波特率模块设计方法,其余波特率可以以此类推。由于我的开发板上的时钟为50MHz,周期T=20ns,而波特率为115200bps,所以1个bit持续的时间是8.7us,那么每个bit占用的周期数N=(8.7us / 20ns) = 434,所以可以定义一个计数器,每当计数器从0计数到433的时候就把计数器清零,然后在计数值为1(这个计数值最好比433的一半要小,这篇博客的最后一部分分析了原因)的情况下产生一个高脉冲。发射模块只要检测到这个高脉冲的到来就发送一个bit,这样就实现了波特率为115200bps的串口数据发送。

  而接收模块的波特率时钟产生逻辑与发送的波特率时钟相比稍有不同。不同之处在于当接收模块检测到I_rs232_rxd的下降沿以后,表示有数据过来,准备开始接收数据了,由于一个bit持续的时间为434个时钟周期,所以为了保证接收模块接收数据的准确性,我们需要在434/2=217个周期,也就是数据的正中间位置的时候把输入的数据接收并存起来。也就是说接收模块的波特率时钟要比发射模块的波特率时钟滞后数个周期

  波特率产生模块的框图如下图所示

【接口时序】3、UART串口收发的原理与Verilog实现

  其中:

  I_clk是系统时钟;

  I_rst_n是系统复位;

  I_tx_bps_en是发射模块波特率使能信号,当I_tx_bps_en为1时O_bps_tx_clk才有时钟信号输出;

  I_rx_bps_en是接收模块波特率使能信号,当I_rx_bps_en为1时O_bps_rx_clk才有时钟信号输出。

  波特率模块的完整代码如下:

module baudrate_gen
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统全局复位
input I_bps_tx_clk_en , // 串口发送模块波特率时钟使能信号
input I_bps_rx_clk_en , // 串口接收模块波特率时钟使能信号
output O_bps_tx_clk , // 发送模块波特率产生时钟
output O_bps_rx_clk // 接收模块波特率产生时钟
); parameter C_BPS9600 = , //波特率为9600bps
C_BPS19200 = , //波特率为19200bps
C_BPS38400 = , //波特率为38400bps
C_BPS57600 = , //波特率为57600bps
C_BPS115200 = ; //波特率为115200bps parameter C_BPS_SELECT = C_BPS115200 ; //波特率选择 reg [:] R_bps_tx_cnt ;
reg [:] R_bps_rx_cnt ; ///////////////////////////////////////////////////////////
// 功能:串口发送模块的波特率时钟产生逻辑
///////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_bps_tx_cnt <= 'd0 ;
else if(I_bps_tx_clk_en == 'b1)
begin
if(R_bps_tx_cnt == C_BPS_SELECT)
R_bps_tx_cnt <= 'd0 ;
else
R_bps_tx_cnt <= R_bps_tx_cnt + 'b1 ;
end
else
R_bps_tx_cnt <= 'd0 ;
end assign O_bps_tx_clk = (R_bps_tx_cnt == 'd1) ? 1'b1 : 'b0 ; ///////////////////////////////////////////////////////////
// 功能:串口接收模块的波特率时钟产生逻辑
///////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_bps_rx_cnt <= 'd0 ;
else if(I_bps_rx_clk_en == 'b1)
begin
if(R_bps_rx_cnt == C_BPS_SELECT)
R_bps_rx_cnt <= 'd0 ;
else
R_bps_rx_cnt <= R_bps_rx_cnt + 'b1 ;
end
else
R_bps_rx_cnt <= 'd0 ;
end assign O_bps_rx_clk = (R_bps_rx_cnt == C_BPS_SELECT >> 'b1) ? 1'b1 : 'b0 ; endmodule

  波特率模块的ModelSim仿真图为

【接口时序】3、UART串口收发的原理与Verilog实现

4.2、发送模块的设计与实现

  有了波特率时钟以后,就可以开始编写发送模块的内部逻辑了。发送模块的结构框图如下图所示

【接口时序】3、UART串口收发的原理与Verilog实现

其中:

  I_clk是系统时钟;

  I_rst_n是系统复位;

  I_tx_start是开始发送信号,当检测到I_tx_start为高电平时,立马把输入I_para_data[7:0]的数据串行化成单bit的发出去;

  I_bps_tx_clk是发送模块波特率时钟信号,当检测到I_bps_tx_clk为高的时候就发送1个bit;

  I_para_data[7:0]是并行的8-bit数据;

  O_rs232_txd是串行的bit数据流;

  O_bps_clk_en是发射波特率时钟启动信号,当它为1是波特率产生模块才能产生发射模块的波特率时钟;

  O_tx_done是发送1字节数据完成的标志位,当一个字节发送完毕以后,O_tx_done产生一个高脉冲。

  以发送字节0x55为例,发送模块几个关键信号的时序图如下图所示

【接口时序】3、UART串口收发的原理与Verilog实现

  发送模块的代码如下:

module uart_txd
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统全局复位
input I_tx_start , // 发送使能信号
input I_bps_tx_clk , // 发送波特率时钟
input [:] I_para_data , // 要发送的并行数据
output reg O_rs232_txd , // 发送的串行数据,在硬件上与串口相连
output reg O_bps_tx_clk_en , // 波特率时钟使能信号
output reg O_tx_done // 发送完成的标志
); reg [:] R_state ; reg R_transmiting ; // 数据正在发送标志 /////////////////////////////////////////////////////////////////////////////
// 产生发送 R_transmiting 标志位
/////////////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_transmiting <= 'b0 ;
else if(O_tx_done)
R_transmiting <= 'b0 ;
else if(I_tx_start)
R_transmiting <= 'b1 ;
end /////////////////////////////////////////////////////////////////////////////
// 发送数据状态机
/////////////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_state <= 'd0 ;
O_rs232_txd <= 'b1 ;
O_tx_done <= 'b0 ;
O_bps_tx_clk_en <= 'b0 ; // 关掉波特率时钟使能信号
end
else if(R_transmiting) // 检测发送标志被拉高,准备发送数据
begin
O_bps_tx_clk_en <= 'b1 ; // 发送数据前的第一件事就是打开波特率时钟使能信号
if(I_bps_tx_clk) // 在波特率时钟的控制下把数据通过一个状态机发送出去,并产生发送完成信号
begin
case(R_state)
'd0 : // 发送起始位
begin
O_rs232_txd <= 'b0 ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd1 : // 发送 I_para_data[0]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd2 : // 发送 I_para_data[1]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd3 : // 发送 I_para_data[2]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd4 : // 发送 I_para_data[3]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd5 : // 发送 I_para_data[4]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd6 : // 发送 I_para_data[5]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd7 : // 发送 I_para_data[6]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd8 : // 发送 I_para_data[7]
begin
O_rs232_txd <= I_para_data[] ;
O_tx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd9 : // 发送 停止位
begin
O_rs232_txd <= 'b1 ;
O_tx_done <= 'b1 ;
R_state <= 4'd0      ;
end
default :R_state <= 4'd0 ;
          endcase 
        end
      end
    else
      begin
        O_bps_tx_clk_en    <= 'b0 ; // 一帧数据发送完毕以后就关掉波特率时钟使能信号
        R_state        <= 'd0 ;
        O_tx_done      <= 'b0 ;
        O_rs232_txd      <= 'b1 ;
      end
end endmodule

  其中当检测到输入信号I_tx_start为高电平以后,发送模块立即把R_transmiting信号拉高,表示开始要发送数据了,在R_transmiting为高电平的期间,打开波特率时钟使能信号并且在波特率时钟的控制下通过一个状态机把并行数据发送出去,并产生发送完成信号O_tx_done,等O_tx_done为高以后再把R_transmiting拉低表示一次发送结束。

  为了实现功能1的效果还需要编写一个顶层模块把波特率模块和发送模块例化进去并产生发送的信号,顶层模块的代码如下:

module uart_tx_top
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统全局复位
output O_rs232_txd // 发送的串行数据,在硬件上与串口相连
); wire W_bps_tx_clk ;
wire W_bps_tx_clk_en ;
wire W_tx_start ;
wire W_tx_done ;
wire [:] W_para_data ; reg [:] R_data_reg ;
reg [:] R_cnt_1s ;
reg R_tx_start_reg ; assign W_tx_start = R_tx_start_reg ;
assign W_para_data = R_data_reg ; /////////////////////////////////////////////////////////////////////
// 产生要发送的数据
/////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_cnt_1s <= 'd0 ;
R_data_reg <= 'd0 ;
R_tx_start_reg  <= 'b0 ;
end
else if(R_cnt_1s == 'd24_999_999)
begin
R_cnt_1s <= 'd0 ;
R_data_reg <= R_data_reg + 'b1 ;
R_tx_start_reg <= 'b1 ;
end
else
begin
R_cnt_1s <= R_cnt_1s + 'b1 ;
R_tx_start_reg <= 'b0 ;
end
end uart_txd U_uart_txd
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_tx_start (W_tx_start ), // 发送使能信号
.I_bps_tx_clk (W_bps_tx_clk ), // 波特率时钟
.I_para_data (W_para_data ), // 要发送的并行数据
.O_rs232_txd (O_rs232_txd ), // 发送的串行数据,在硬件上与串口相连
.O_bps_tx_clk_en (W_bps_tx_clk_en ), // 波特率时钟使能信号
.O_tx_done (W_tx_done ) // 发送完成的标志
); baudrate_gen U_baudrate_gen
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_bps_tx_clk_en (W_bps_tx_clk_en ), // 串口发送模块波特率时钟使能信号
.I_bps_rx_clk_en ( ), // 串口接收模块波特率时钟使能信号
.O_bps_tx_clk (W_bps_tx_clk ), // 发送模块波特率产生时钟
.O_bps_rx_clk ( ) // 接收模块波特率产生时钟
); endmodule

  下载到板之前先用Modelsim仿一下看逻辑是否正确,仿之前把R_cnt_1s这个参数的上限值设置小一点,比如5000,可以加快仿真速度,下图是仿真时序图,显然完全满足设计要求。

【接口时序】3、UART串口收发的原理与Verilog实现

仿真结束以后就绑定管脚然后下载到FPGA中,接着打开电脑的串口调试助手,下图是我的电脑上的显示效果:

【接口时序】3、UART串口收发的原理与Verilog实现

4.3、接收模块的设计与实现

  波特率模块和发送模块都没问题以后,就可以开始编写接收模块的代码了。接收模块的结构框图如下图所示

【接口时序】3、UART串口收发的原理与Verilog实现

  其中:

  I_clk是系统时钟;

  I_rst_n是系统复位;

  I_rx_start是开始发送信号,当I_rx_start一直为高电平时,接收模块检测到有数据就会接收;

  I_bps_rx_clk是接收模块波特率时钟信号,当检测到I_bps_rx_clk为高的时候就接收1个bit;

  I_rs232_rx是串行的bit数据流;

  O_para_data[7:0]是并行的8-bit数据;

  O_bps_rx_clk_en是发射波特率时钟启动信号,当它为1是波特率产生模块才能产生接收模块的波特率时钟;

  O_rx_done是接收1字节数据完成的标志位,当一个字节接收完毕以后,O_rx_done产生一个高脉冲。

  接收模块与发射模块的逻辑结构非常类似,但是由于接收模块需要判断串行数据流的起始位,所以还要加一段检测串行数据流下降沿的逻辑,检测串行数据流下降沿的代码如下:

////////////////////////////////////////////////////////////////////////////////
// 功能:把 I_rs232_rxd 打的前两拍,是为了消除亚稳态
// 把 I_rs232_rxd 打的后两拍,是为了产生下降沿标志位
////////////////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_rs232_rx_reg0 <= 'b0 ;
R_rs232_rx_reg1 <= 'b0 ;
R_rs232_rx_reg2 <= 'b0 ;
R_rs232_rx_reg3 <= 'b0 ;
end
else
begin
R_rs232_rx_reg0 <= I_rs232_rxd ;
R_rs232_rx_reg1 <= R_rs232_rx_reg0 ;
R_rs232_rx_reg2 <= R_rs232_rx_reg1 ;
R_rs232_rx_reg3 <= R_rs232_rx_reg2 ;
end
end
// 产生I_rs232_rxd信号的下降沿标志位
assign W_rs232_rxd_neg = (~R_rs232_rx_reg2) & R_rs232_rx_reg3 ;

  这段逻辑一共把I_rs232_rxd信号打了四拍,其中前两排是为了消除I_rs232_rxd的亚稳态(后面有时间专门讨论亚稳态问题),后两排用来产生I_rs232_rxd信号下降沿的标志位。

  这里以接收0x55这个字节为例来演示接收模块的几个重要信号的时序图如下图所示:

【接口时序】3、UART串口收发的原理与Verilog实现

  接收模块的完整代码如下:

module uart_rxd
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统全局复位
input I_rx_start , // 接收使能信号
input I_bps_rx_clk , // 接收波特率时钟
input I_rs232_rxd , // 接收的串行数据,在硬件上与串口相连
output reg O_bps_rx_clk_en , // 波特率时钟使能信号
output reg O_rx_done , // 接收完成标志
output reg [:] O_para_data // 接收到的8-bit并行数据
); reg R_rs232_rx_reg0 ;
reg R_rs232_rx_reg1 ;
reg R_rs232_rx_reg2 ;
reg R_rs232_rx_reg3 ; reg R_receiving ; reg [:] R_state ;
reg [:] R_para_data_reg ; wire W_rs232_rxd_neg ; ////////////////////////////////////////////////////////////////////////////////
// 功能:把 I_rs232_rxd 打的前两拍,是为了消除亚稳态
// 把 I_rs232_rxd 打的后两拍,是为了产生下降沿标志位
////////////////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_rs232_rx_reg0 <= 'b0 ;
R_rs232_rx_reg1 <= 'b0 ;
R_rs232_rx_reg2 <= 'b0 ;
R_rs232_rx_reg3 <= 'b0 ;
end
else
begin
R_rs232_rx_reg0 <= I_rs232_rxd ;
R_rs232_rx_reg1 <= R_rs232_rx_reg0 ;
R_rs232_rx_reg2 <= R_rs232_rx_reg1 ;
R_rs232_rx_reg3 <= R_rs232_rx_reg2 ;
end
end
// 产生I_rs232_rxd信号的下降沿标志位
assign W_rs232_rxd_neg = (~R_rs232_rx_reg2) & R_rs232_rx_reg3 ; ////////////////////////////////////////////////////////////////////////////////
// 功能:产生发送信号R_receiving
////////////////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_receiving <= 'b0 ;
else if(O_rx_done)
R_receiving <= 'b0 ;
else if(I_rx_start && W_rs232_rxd_neg)
R_receiving <= 'b1 ;
end ////////////////////////////////////////////////////////////////////////////////
// 功能:用状态机把串行的输入数据接收,并转化为并行数据输出
////////////////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
O_rx_done <= 'b0 ;
R_state <= 'd0 ;
R_para_data_reg <= 'd0 ;
O_bps_rx_clk_en <= 'b0 ;
end
else if(R_receiving)
begin
O_bps_rx_clk_en <= 'b1 ; // 打开波特率时钟使能信号
if(I_bps_rx_clk)
begin
case(R_state)
'd0 : // 接收起始位,但不保存
begin
R_para_data_reg <= 'd0 ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd1 : // 接收第0位,保存到R_para_data_reg[0]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd2 : // 接收第1位,保存到R_para_data_reg[1]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd3 : // 接收第2位,保存到R_para_data_reg[2]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd4 : // 接收第3位,保存到R_para_data_reg[3]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd5 : // 接收第4位,保存到R_para_data_reg[4]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd6 : // 接收第5位,保存到R_para_data_reg[5]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd7 :// 接收第6位,保存到R_para_data_reg[6]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd8 : // 接收第7位,保存到R_para_data_reg[7]
begin
R_para_data_reg[] <= I_rs232_rxd ;
O_rx_done <= 'b0 ;
R_state <= R_state + 'b1 ;
end
'd9 : // 接收停止位,但不保存,并把R_para_data_reg给输出
begin
O_para_data <= R_para_data_reg ;
O_rx_done <= 'b1 ;
R_state <= 'd0 ;
end default:R_state <= 'd0 ;
endcase
end
end
else
begin
O_rx_done <= 'b0 ;
R_state <= 'd0 ;
R_para_data_reg <= 'd0 ;
O_bps_rx_clk_en <= 'b0 ; // 接收完毕以后关闭波特率时钟使能信号
end
end endmodule

  在下载到开发板测试之前,可以先用ModelSim软件对模块进行一个功能仿真,方法是直接把接收模块例化到上一小节测试发送模块的例子中,例化的顶层代码如下:

module uart_top
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统全局复位
output [:] O_led_out ,
output O_rs232_txd // 发送的串行数据,在硬件上与串口相连
); wire W_bps_tx_clk ;
wire W_bps_tx_clk_en ;
wire W_bps_rx_clk ;
wire W_bps_rx_clk_en ;
wire W_tx_start ;
wire W_tx_done ;
wire W_rx_done ;
wire [:] W_para_data ;
wire [:] W_rx_para_data ; reg [:] R_data_reg ;
reg [:] R_cnt_1s ;
reg R_tx_start_reg ; assign W_tx_start = R_tx_start_reg ;
assign W_para_data = R_data_reg ;
assign O_led_out  = W_rx_para_data[:] ; /////////////////////////////////////////////////////////////////////
// 产生要发送的数据
/////////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_cnt_1s <= 'd0 ;
R_data_reg <= 'd0 ;
R_tx_start_reg <= 'b0 ;
end
else if(R_cnt_1s == 'd5000)
begin
R_cnt_1s <= 'd0 ;
R_data_reg <= R_data_reg + 'b1 ;
R_tx_start_reg <= 'b1 ;
end
else
begin
R_cnt_1s <= R_cnt_1s + 'b1 ;
R_tx_start_reg <= 'b0 ;
end
end uart_txd U_uart_txd
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_tx_start (W_tx_start ), // 发送使能信号
.I_bps_tx_clk (W_bps_tx_clk ), // 波特率时钟
.I_para_data (W_para_data ), // 要发送的并行数据
.O_rs232_txd (O_rs232_txd ), // 发送的串行数据,在硬件上与串口相连
.O_bps_tx_clk_en (W_bps_tx_clk_en ), // 波特率时钟使能信号
.O_tx_done (W_tx_done ) // 发送完成的标志
); baudrate_gen U_baudrate_gen
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_bps_tx_clk_en (W_bps_tx_clk_en ), // 串口发送模块波特率时钟使能信号
.I_bps_rx_clk_en (W_bps_rx_clk_en ), // 串口接收模块波特率时钟使能信号
.O_bps_tx_clk (W_bps_tx_clk ), // 发送模块波特率产生时钟
.O_bps_rx_clk (W_bps_rx_clk ) // 接收模块波特率产生时钟
); uart_rxd U_uart_rxd
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_rx_start ('b1 ), // 接收使能信号
.I_bps_rx_clk (W_bps_rx_clk ), // 接收波特率时钟
.I_rs232_rxd (O_rs232_txd ), // 接收的串行数据,在硬件上与串口相连
.O_bps_rx_clk_en (W_bps_rx_clk_en ), // 波特率时钟使能信号
.O_rx_done (W_rx_done ), // 接收完成标志
.O_para_data (W_rx_para_data ) // 接收到的8-bit并行数据
); endmodule

  仿真图如下图所示

【接口时序】3、UART串口收发的原理与Verilog实现

  由图可以看到接收数据与发送的数据完全一致,说明逻辑没有问题,接下来就绑定管脚然后把代码下载到FPGA看看效果,正常的效果是,PC的串口调试助手一直按顺序显示00~FF这些数据,板上的LED灯的状态与数据的低四位状态相同。至此,功能二也全部实现完毕。

4.4、串口回显功能的设计与实现

  有了发射模块和接收模块以后,功能三的要求就很简单了,直接写一个顶层模块,把串口的发送模块与接收模块例化进去就可以了,唯一要做的就是把接收模块的接收完成标志位O_rx_done连接到发送模块的I_tx_start上,把接收模块的8-bit并行输出总线O_para_data连接到发送模块的8-bit并行输入总线I_para_data上,下面直接给出顶层的代码:

module uart_top
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统全局复位
input I_rs232_rxd , // 接收的串行数据,在硬件上与串口相连
output O_rs232_txd , // 发送的串行数据,在硬件上与串口相连
output [:] O_led_out
); wire W_bps_tx_clk ;
wire W_bps_tx_clk_en ;
wire W_bps_rx_clk ;
wire W_bps_rx_clk_en ;
wire W_rx_done ;
wire W_tx_done ;
wire [:] W_para_data ; assign O_led_out = W_para_data[:] ; baudrate_gen U_baudrate_gen
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_bps_tx_clk_en (W_bps_tx_clk_en ), // 串口发送模块波特率时钟使能信号
.I_bps_rx_clk_en (W_bps_rx_clk_en ), // 串口接收模块波特率时钟使能信号
.O_bps_tx_clk (W_bps_tx_clk ), // 发送模块波特率产生时钟
.O_bps_rx_clk (W_bps_rx_clk ) // 接收模块波特率产生时钟
); uart_txd U_uart_txd
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_tx_start (W_rx_done ), // 发送使能信号
.I_bps_tx_clk (W_bps_tx_clk ), // 波特率时钟
.I_para_data (W_para_data ), // 要发送的并行数据
.O_rs232_txd (O_rs232_txd ), // 发送的串行数据,在硬件上与串口相连
.O_bps_tx_clk_en (W_bps_tx_clk_en ), // 波特率时钟使能信号
.O_tx_done (W_tx_done ) // 发送完成的标志
); uart_rxd U_uart_rxd
(
.I_clk (I_clk ), // 系统50MHz时钟
.I_rst_n (I_rst_n ), // 系统全局复位
.I_rx_start ('b1 ), // 接收使能信号
.I_bps_rx_clk (W_bps_rx_clk ), // 接收波特率时钟
.I_rs232_rxd (I_rs232_rxd ), // 接收的串行数据,在硬件上与串口相连
.O_bps_rx_clk_en (W_bps_rx_clk_en ), // 波特率时钟使能信号
.O_rx_done (W_rx_done ), // 接收完成标志
.O_para_data (W_para_data ) // 接收到的8-bit并行数据
); endmodule

  建立工程并绑定管脚以后下载到开发板中,利用串口调试助手的自动发送(自动发送的周期最好在200ms以上)功能我对波特率为9600bps和115200bps分别进行了测试,在9600bps的情况下我一共发送了1002512个字节,全部接受正确,115200bps波特率情况下一共发送了512325字节,也全部接受正确,逻辑基本稳定,欢迎大家继续测。我第一次写串口代码的时候出现过在115200bps的情况下发送字节达到10万以上的时候出现误码的情况,原因下一小节再说,上面的代码已经把这个问题修复了,原因就出在波特率模块上。至此,功能三已全部完成。

【接口时序】3、UART串口收发的原理与Verilog实现【接口时序】3、UART串口收发的原理与Verilog实现

五、进一步思考

5.1、波特率模块产生的O_bps_tx_clk滞后O_bps_rx_clk可能出现的问题

  我最开始写的波特率模块如下:

module baudrate_gen
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统全局复位
input I_bps_tx_clk_en , // 串口发送模块波特率时钟使能信号
input I_bps_rx_clk_en , // 串口接收模块波特率时钟使能信号
output O_bps_tx_clk , // 发送模块波特率产生时钟
output O_bps_rx_clk // 接收模块波特率产生时钟
); parameter C_BPS9600 = , //波特率为9600bps
C_BPS19200 = , //波特率为19200bps
C_BPS38400 = , //波特率为38400bps
C_BPS57600 = , //波特率为57600bps
C_BPS115200 = ; //波特率为115200bps parameter C_BPS_SELECT = C_BPS115200 ; //波特率选择 reg [:] R_bps_tx_cnt ;
reg R_bps_tx_clk_reg ; reg [:] R_bps_rx_cnt ; ///////////////////////////////////////////////////////////
// 功能:串口发送模块的波特率时钟产生逻辑
///////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_bps_tx_cnt <= 'd0 ;
R_bps_tx_clk_reg <= 'b0 ;
end
else if(I_bps_tx_clk_en == 'b1)
begin
if(R_bps_tx_cnt == C_BPS_SELECT)
begin
R_bps_tx_cnt <= 'd0 ;
R_bps_tx_clk_reg <= 'b1 ;
end
else
begin
R_bps_tx_cnt <= R_bps_tx_cnt + 'b1 ;
R_bps_tx_clk_reg <= 'b0 ;
end
end
else
begin
R_bps_tx_cnt <= 'd0 ;
R_bps_tx_clk_reg <= 'b0 ;
end
end assign O_bps_tx_clk = R_bps_tx_clk_reg ; ///////////////////////////////////////////////////////////
// 功能:串口接收模块的波特率时钟产生逻辑
///////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_bps_rx_cnt <= 'd0 ;
else if(I_bps_rx_clk_en == 'b1)
begin
if(R_bps_rx_cnt == C_BPS_SELECT)
R_bps_rx_cnt <= 'd0 ;
else
R_bps_rx_cnt <= R_bps_rx_cnt + 'b1 ;
end
else
R_bps_rx_cnt <= 'd0 ;
end assign O_bps_rx_clk = (R_bps_rx_cnt == C_BPS_SELECT >> 'b1) ? 1'b1 : 'b0 ; endmodule

  其仿真如下所示:

【接口时序】3、UART串口收发的原理与Verilog实现

  当发送波特率时钟使能信号打开以后,计数值计满C_BPS_SELECT后才产生一个发送时钟脉冲,而接收波特率时钟只需要计满C_BPS_SELECT的一半就产生了一个时钟脉冲,这就导致在回显实验中,O_bps_tx_clk滞后于O_bps_rx_clk,而回显实验中我们直接把接收完成的标志直接接在了发送开始标志上,所以这就有可能导致,上一次的数据还没发送完的时候这一次的数据已经来了,经过我的测试,使用上面的波特率逻辑,如果不做回显实验,一般没问题,如果做回显实验,在波特率较高,比如115200bps和57600bps的情况下,数据量少的时候不会出错,数据量大的时候一般都会有数据丢失,而在波特率较低的情况下,比如9600bps和2400bps,数据直接是接收一帧漏一帧,比如发送字符串abcdef,接收回来的是ace。我用ChipScope才抓出了这个原因。今后使用的时候要注意这个问题。

5.2、发送数据的状态机和接收数据的状态机可以用移位的方式来做

  事实上那个状态机的发送8-bit数据和接收8-bit数据的部分可以用移位的方法来做,这样写的代码会更短更精炼。今后有空的时候自己在重新写一次。

欢迎关注我的公众号:FPGA之禅

【接口时序】3、UART串口收发的原理与Verilog实现