手搓UART

关于UART协议的碎碎念

起因

大三了。从原本的ISRL实验室里逃了出来,属实是喘不过气。没有人带+教授不好约+问题无法解决,在这样的实验室呆着怎么能做好项目呢(恼)

一次偶然的机会,发现了NBS里面隐藏的CICS实验室。环境非常好,并且supervisor非常和蔼可亲。总算能静下心来做点有意义的东西了。

进实验室做的第一个项目是用现有的FPGA板子,实现UART协议。每周去一次,大概三周把这玩意搞下来了,非常有趣。

本文部分参考于https://www.cnblogs.com/liujinggang/p/9535366.html

什么是UART

UART,即 universal asynchronous receiver-transmitter,是最简单也最常见的串口通信协议。从这个名字可以看出,这个协议是非同步的,并且支持全双工通信。

虽然说是非同步的,但是这不代表两个通信设备不需要时钟连接。时钟线,发送线,接收线,就构成了最基本的UART通讯线,只需要三根。因为是串行通讯设备,所以一般来说UART的速度很慢。但是对于简单的物联网IoT设备来说,一秒钟10kB/s级别的传输已经非常足够了。

对于UART协议而言,一个byte的数据会串行化之后,分成八份发送。通常,数据线是处于高电平状态的,在开始发送的时候会将电平拉低。之后会依次发送八个对应的bit,并在发送完成之后将电平拉高。这就是最基础的UART传输方式了。

而传输速度上,一个重要的参数是波特率(Baud Rate)。不同的波特率决定了不同的传输速率。标准的波特率包括了:9600,19200,38400,57600,115200. 波特率的单位是bps,即bit per second。如果说一秒钟能够发送115200bits的信息,那么信息的传输速率就是115200/1000/8=14.4kB/s。这一般也是UART协议能够达到的最高传输速率。然而,真实情况并不能达到这么高的速率。实际上,假如说波特率为115200bits,这就说明了一个bit的持续时间为1/115200=8.7us,那么发送一个byte所需的时间就是其的八倍,69us。但是,别忘了刚才提到的开始位和停止位;为了达成可靠的传输,需要加上两个bit位数的包头和包尾;这样发送一个byte就需要8.7us的十倍即87us的时间。因此,在一秒钟的时间内,这样的循环可以执行1s/87us=11,494.25次,也就是说最大传输速率降低到了11.49kB/s.实际情况下,传输速率可能会比这更低。

verilog实现

接下来讨论如何使用verilog和Nexyz DDR4 FPGA来实现一个标准的UART协议。

时钟发生器

对于一块比较标准的FPGA板子来说,我们假设FPGA的基准频率是50MHz,即每秒钟完成五千万次时钟周期,每个周期20ns。那么要在一秒钟发送115200bits,就相当于说每个bit占用了8.7us.相除得到每个bit需要占用8.7us/20ns=435个周期。因此,如果要完成一次成功的发送,使能信号应该每隔435个周期置1.

我们的目标是实现这样的一个模块,他接收标准的时钟,重置信号,以及发送以及接收器的使能信号,产生发送器和接收器的发送与读取时钟信号。

 1module baudrate_gen
 2(
 3    input   I_clk                  , // 系统50MHz时钟
 4    input   I_rst_n                , // 系统全局复位
 5    input   I_bps_tx_clk_en        , // 串口发送模块波特率时钟使能信号
 6    input   I_bps_rx_clk_en        , // 串口接收模块波特率时钟使能信号
 7    output  O_bps_tx_clk           , // 发送模块波特率产生时钟
 8    output  O_bps_rx_clk             // 接收模块波特率产生时钟
 9);
10
11parameter       C_BPS9600         = 5207         ,    //波特率为9600bps
12                C_BPS19200        = 2603         ,    //波特率为19200bps
13                C_BPS38400        = 1301         ,    //波特率为38400bps
14                C_BPS57600        = 867          ,    //波特率为57600bps
15                C_BPS115200       = 433          ;    //波特率为115200bps
16                
17parameter       C_BPS_SELECT      = C_BPS115200  ;    //波特率选择
18                
19//计数器
20reg [12:0]  R_bps_tx_cnt       ;
21reg [12:0]  R_bps_rx_cnt       ;
22
23//触发block,用于发送器计数器的启动
24always @(posedge I_clk or negedge I_rst_n)
25begin
26    if(!I_rst_n)
27        R_bps_tx_cnt <= 13'd0 ;
28    //如果计数器的值为1,那么就重置计数器
29    else if(I_bps_tx_clk_en == 1'b1)
30        begin
31            if(R_bps_tx_cnt == C_BPS_SELECT)
32                R_bps_tx_cnt <= 13'd0 ;
33            else
34                R_bps_tx_cnt <= R_bps_tx_cnt + 1'b1 ;                 
35        end    
36    else
37        R_bps_tx_cnt <= 13'd0 ;        
38end
39
40//如果计数器为1,那么输出一次发送信号的脉冲
41assign O_bps_tx_clk = (R_bps_tx_cnt == 13'd1) ? 1'b1 : 1'b0 ;
42//接收器也是同样的逻辑
43always @(posedge I_clk or negedge I_rst_n)
44begin
45    if(!I_rst_n)
46        R_bps_rx_cnt <= 13'd0 ;
47    else if(I_bps_rx_clk_en == 1'b1)
48        begin
49            if(R_bps_rx_cnt == C_BPS_SELECT)
50                R_bps_rx_cnt <= 13'd0 ;
51            else
52                R_bps_rx_cnt <= R_bps_rx_cnt + 1'b1 ;                 
53        end    
54    else
55        R_bps_rx_cnt <= 13'd0 ;        
56end
57
58assign O_bps_rx_clk = (R_bps_rx_cnt == C_BPS_SELECT >> 1'b1) ? 1'b1 : 1'b0 ;
59
60endmodule

这样就完成了接收和发送时钟的基本逻辑。

发送器

对于发送器而言,他要做的事情就是根据时钟发生器发来的信号来发送自己的bit。此外,还需要额外的使能信号来启动。

发送器有如下的输入和输出:

输入INPUT输出OUTPUT
时钟信号
全局复位
发送使能信号
发送时钟
并行数据流
串行数据流
发送使能信号
发送结束

其中,输出部分的发送使能信号与时钟发生器的I_bps_tx_clk_en相连,即如果这个值不是高电平则时钟发生器无需为发送器产生发送时钟。这个逻辑也适用于接收器。

verilog代码如下:

  1module uart_txd
  2(
  3    input          I_clk           , // 系统50MHz时钟
  4    input          I_rst_n         , // 系统全局复位
  5    input          I_tx_start      , // 发送使能信号
  6    input          I_bps_tx_clk    , // 发送波特率时钟
  7    input   [7:0]  I_para_data     , // 要发送的并行数据
  8    output  reg    O_rs232_txd     , // 发送的串行数据,在硬件上与串口相连
  9    output  reg    O_bps_tx_clk_en , // 波特率时钟使能信号
 10    output  reg    O_tx_done         // 发送完成的标志
 11);
 12
 13reg  [3:0]  R_state ;
 14
 15reg R_transmiting ; // 数据正在发送标志
 16
 17/////////////////////////////////////////////////////////////////////////////
 18// 产生发送 R_transmiting 标志位
 19/////////////////////////////////////////////////////////////////////////////
 20always @(posedge I_clk or negedge I_rst_n)
 21begin
 22    if(!I_rst_n)
 23        R_transmiting <= 1'b0 ;
 24    else if(O_tx_done)
 25        R_transmiting <= 1'b0 ;
 26    else if(I_tx_start)
 27        R_transmiting <= 1'b1 ;          
 28end
 29
 30/////////////////////////////////////////////////////////////////////////////
 31// 发送数据状态机
 32/////////////////////////////////////////////////////////////////////////////
 33always @(posedge I_clk or negedge I_rst_n)
 34begin
 35    if(!I_rst_n)
 36        begin
 37            R_state      <= 4'd0 ;
 38            O_rs232_txd  <= 1'b1 ; 
 39            O_tx_done    <= 1'b0 ;
 40            O_bps_tx_clk_en <= 1'b0 ;  // 关掉波特率时钟使能信号
 41        end 
 42    else if(R_transmiting) // 检测发送标志被拉高,准备发送数据
 43        begin
 44            O_bps_tx_clk_en <= 1'b1 ;  // 发送数据前的第一件事就是打开波特率时钟使能信号
 45            if(I_bps_tx_clk) // 在波特率时钟的控制下把数据通过一个状态机发送出去,并产生发送完成信号
 46                begin
 47                    case(R_state)
 48                        4'd0  : // 发送起始位
 49                            begin
 50                                O_rs232_txd  <= 1'b0            ;
 51                                O_tx_done    <= 1'b0            ; 
 52                                R_state      <= R_state + 1'b1  ;
 53                            end
 54                        4'd1  : // 发送 I_para_data[0]
 55                            begin
 56                                O_rs232_txd  <= I_para_data[0]  ;
 57                                O_tx_done    <= 1'b0            ; 
 58                                R_state      <= R_state + 1'b1  ;
 59                            end 
 60                        4'd2  : // 发送 I_para_data[1]
 61                            begin
 62                                O_rs232_txd  <= I_para_data[1]   ;
 63                                O_tx_done    <= 1'b0             ; 
 64                                R_state      <= R_state + 1'b1   ;
 65                            end
 66                        4'd3  : // 发送 I_para_data[2]
 67                            begin
 68                                O_rs232_txd  <= I_para_data[2]   ;
 69                                O_tx_done    <= 1'b0             ; 
 70                                R_state      <= R_state + 1'b1   ;
 71                            end
 72                        4'd4  : // 发送 I_para_data[3]
 73                            begin
 74                                O_rs232_txd  <= I_para_data[3]   ;
 75                                O_tx_done    <= 1'b0             ; 
 76                                R_state      <= R_state + 1'b1   ;
 77                            end 
 78                        4'd5  : // 发送 I_para_data[4]
 79                            begin
 80                                O_rs232_txd  <= I_para_data[4]   ;
 81                                O_tx_done    <= 1'b0             ; 
 82                                R_state      <= R_state + 1'b1   ;
 83                            end
 84                        4'd6  : // 发送 I_para_data[5]
 85                            begin
 86                                O_rs232_txd  <= I_para_data[5]   ;
 87                                O_tx_done    <= 1'b0             ; 
 88                                R_state      <= R_state + 1'b1   ;
 89                            end
 90                        4'd7  : // 发送 I_para_data[6]
 91                            begin
 92                                O_rs232_txd  <= I_para_data[6]   ;
 93                                O_tx_done    <= 1'b0             ; 
 94                                R_state      <= R_state + 1'b1   ;
 95                            end
 96                        4'd8  : // 发送 I_para_data[7]
 97                            begin
 98                                O_rs232_txd  <= I_para_data[7]   ;
 99                                O_tx_done    <= 1'b0             ; 
100                                R_state      <= R_state + 1'b1   ;
101                            end 
102                        4'd9  : // 发送 停止位
103                            begin
104                                O_rs232_txd  <= 1'b1             ;
105                                O_tx_done    <= 1'b1             ; 
106                                R_state      <= 4'd0             ;
107                            end
108                        default :R_state      <= 4'd0            ;
109                    endcase
110                end
111        end
112    else
113        begin 
114            O_bps_tx_clk_en  <= 1'b0  ; // 一帧数据发送完毕以后就关掉波特率时钟使能信号 
115            R_state          <= 4'd0  ; 
116            O_tx_done        <= 1'b0  ; 
117            O_rs232_txd      <= 1'b1  ;  
118        end 
119end 
120
121endmodule

接收器

对于接收器而言,它需要这些IO:

输入INPUT输出OUTPUT
时钟信号
全局复位
接收使能信号
接收时钟
串行数据流
并行数据流
接收使能信号
发送结束

可以看到,发送器和接收器是对称的,在串并行的程度上,以及在信号处理的方面。

然而,对于接收器而言,还有一段逻辑需要处理。接收使能信号并不像发送使能信号那样,想发就置1.还记得关于UART设定的讨论吗?在没有发送信息的时候,接收线是高电平。在开始接收到信息时,电平置0.所以对于接收器来说,需要一个逻辑模块来判断接收使能信号是否为1.

 1////////////////////////////////////////////////////////////////////////////////
 2// 功能:把 I_rs232_rxd 打的前两拍,是为了消除亚稳态
 3//      把 I_rs232_rxd 打的后两拍,是为了产生下降沿标志位
 4////////////////////////////////////////////////////////////////////////////////
 5always @(posedge I_clk or negedge I_rst_n)
 6begin
 7    if(!I_rst_n)
 8        begin
 9            R_rs232_rx_reg0 <= 1'b0 ;
10            R_rs232_rx_reg1 <= 1'b0 ;
11            R_rs232_rx_reg2 <= 1'b0 ;
12            R_rs232_rx_reg3 <= 1'b0 ;
13        end 
14    else
15        begin  
16            R_rs232_rx_reg0 <= I_rs232_rxd      ;
17            R_rs232_rx_reg1 <= R_rs232_rx_reg0  ; 
18            R_rs232_rx_reg2 <= R_rs232_rx_reg1  ; 
19            R_rs232_rx_reg3 <= R_rs232_rx_reg2  ; 
20        end   
21end
22// 产生I_rs232_rxd信号的下降沿标志位
23assign W_rs232_rxd_neg    =    (~R_rs232_rx_reg2) & R_rs232_rx_reg3 ;

如果接收使能信号一直是高电平,那么要开启接收器的方法就是观察这里的W_rs232_rxd_neg是否被置1.由于一个bit占用的周期数相比四个周期大得多,所以中间产生的时钟偏移可以忽略不计。

完整的接收器代码如下。

  1module uart_rxd
  2(
  3    input                       I_clk               , // 系统50MHz时钟
  4    input                       I_rst_n             , // 系统全局复位
  5    input                       I_rx_start          , // 接收使能信号
  6    input                       I_bps_rx_clk        , // 接收波特率时钟
  7    input                       I_rs232_rxd         , // 接收的串行数据,在硬件上与串口相连  
  8    output    reg               O_bps_rx_clk_en     , // 波特率时钟使能信号
  9    output    reg               O_rx_done           , // 接收完成标志
 10    output    reg   [7:0]       O_para_data           // 接收到的8-bit并行数据
 11);
 12
 13reg         R_rs232_rx_reg0 ;
 14reg         R_rs232_rx_reg1 ;
 15reg         R_rs232_rx_reg2 ;
 16reg         R_rs232_rx_reg3 ;
 17
 18reg         R_receiving     ;
 19
 20reg [3:0]   R_state         ;
 21reg [7:0]   R_para_data_reg ;
 22
 23wire        W_rs232_rxd_neg ;
 24
 25////////////////////////////////////////////////////////////////////////////////
 26// 功能:把 I_rs232_rxd 打的前两拍,是为了消除亚稳态
 27//          把 I_rs232_rxd 打的后两拍,是为了产生下降沿标志位
 28////////////////////////////////////////////////////////////////////////////////
 29always @(posedge I_clk or negedge I_rst_n)
 30begin
 31    if(!I_rst_n)
 32        begin
 33            R_rs232_rx_reg0 <= 1'b0 ;
 34            R_rs232_rx_reg1 <= 1'b0 ;
 35            R_rs232_rx_reg2 <= 1'b0 ;
 36            R_rs232_rx_reg3 <= 1'b0 ;
 37        end 
 38    else
 39        begin  
 40            R_rs232_rx_reg0 <= I_rs232_rxd      ;
 41            R_rs232_rx_reg1 <= R_rs232_rx_reg0  ; 
 42            R_rs232_rx_reg2 <= R_rs232_rx_reg1  ; 
 43            R_rs232_rx_reg3 <= R_rs232_rx_reg2  ; 
 44        end   
 45end
 46// 产生I_rs232_rxd信号的下降沿标志位
 47assign W_rs232_rxd_neg    =    (~R_rs232_rx_reg2) & R_rs232_rx_reg3 ;
 48
 49////////////////////////////////////////////////////////////////////////////////
 50// 功能:产生发送信号R_receiving
 51////////////////////////////////////////////////////////////////////////////////
 52always @(posedge I_clk or negedge I_rst_n)
 53begin
 54    if(!I_rst_n)
 55        R_receiving <= 1'b0 ;
 56    else if(O_rx_done)
 57        R_receiving <= 1'b0 ;
 58    else if(I_rx_start && W_rs232_rxd_neg)
 59        R_receiving <= 1'b1 ;          
 60end
 61
 62////////////////////////////////////////////////////////////////////////////////
 63// 功能:用状态机把串行的输入数据接收,并转化为并行数据输出
 64////////////////////////////////////////////////////////////////////////////////
 65always @(posedge I_clk or negedge I_rst_n)
 66begin
 67    if(!I_rst_n)
 68        begin
 69            O_rx_done       <= 1'b0 ; 
 70            R_state         <= 4'd0 ;
 71            R_para_data_reg <= 8'd0 ;
 72            O_bps_rx_clk_en <= 1'b0 ;
 73        end 
 74    else if(R_receiving)
 75        begin
 76            O_bps_rx_clk_en <= 1'b1 ; // 打开波特率时钟使能信号
 77            if(I_bps_rx_clk)
 78                begin
 79                    case(R_state)
 80                        4'd0  : // 接收起始位,但不保存
 81                            begin
 82                                R_para_data_reg     <= 8'd0             ;
 83                                O_rx_done           <= 1'b0             ; 
 84                                R_state             <= R_state + 1'b1   ;
 85                            end
 86                        4'd1  : // 接收第0位,保存到R_para_data_reg[0]
 87                            begin
 88                                R_para_data_reg[0]  <= I_rs232_rxd      ;
 89                                O_rx_done           <= 1'b0             ; 
 90                                R_state             <= R_state + 1'b1   ;
 91                            end
 92                        4'd2  : // 接收第1位,保存到R_para_data_reg[1]
 93                            begin
 94                                R_para_data_reg[1]  <= I_rs232_rxd      ;
 95                                O_rx_done           <= 1'b0             ; 
 96                                R_state             <= R_state + 1'b1   ;
 97                            end
 98                        4'd3  : // 接收第2位,保存到R_para_data_reg[2]
 99                            begin
100                                R_para_data_reg[2]  <= I_rs232_rxd      ;
101                                O_rx_done           <= 1'b0             ; 
102                                R_state             <= R_state + 1'b1   ;
103                            end 
104                        4'd4  : // 接收第3位,保存到R_para_data_reg[3]
105                            begin
106                                R_para_data_reg[3]  <= I_rs232_rxd      ;
107                                O_rx_done           <= 1'b0             ; 
108                                R_state             <= R_state + 1'b1   ;
109                            end 
110                        4'd5  : // 接收第4位,保存到R_para_data_reg[4]
111                            begin
112                                R_para_data_reg[4]  <= I_rs232_rxd      ;
113                                O_rx_done           <= 1'b0             ; 
114                                R_state             <= R_state + 1'b1   ;
115                            end
116                        4'd6  : // 接收第5位,保存到R_para_data_reg[5]
117                            begin
118                                R_para_data_reg[5]  <= I_rs232_rxd      ;
119                                O_rx_done           <= 1'b0             ; 
120                                R_state             <= R_state + 1'b1   ;
121                            end
122                        4'd7  :// 接收第6位,保存到R_para_data_reg[6]
123                            begin
124                                R_para_data_reg[6]  <= I_rs232_rxd      ;
125                                O_rx_done           <= 1'b0             ; 
126                                R_state             <= R_state + 1'b1   ;
127                            end
128                        4'd8  : // 接收第7位,保存到R_para_data_reg[7]
129                            begin
130                                R_para_data_reg[7]  <= I_rs232_rxd      ;
131                                O_rx_done           <= 1'b0             ; 
132                                R_state             <= R_state + 1'b1   ;
133                            end 
134                        4'd9  : // 接收停止位,但不保存,并把R_para_data_reg给输出
135                            begin
136                                O_para_data         <= R_para_data_reg  ;
137                                O_rx_done           <= 1'b1             ; 
138                                R_state             <= 4'd0             ;
139                            end 
140                            
141                        default:R_state <= 4'd0                         ;                                                      
142                    endcase 
143                end
144        end
145    else
146        begin
147            O_rx_done           <= 1'b0 ;
148            R_state             <= 4'd0 ;
149            R_para_data_reg     <= 8'd0 ;
150            O_bps_rx_clk_en     <= 1'b0 ; // 接收完毕以后关闭波特率时钟使能信号
151        end          
152end
153
154endmodule

对于接收到的并行信号,可以对其做写入or打印的处理。

what after?

verilog的基本实现已经完成了。我们可以例化一个顶层模块,把这三个东西都放进去,然后连接输入输出。现实中,我们将用FPGA开发板实现这一点。但是在完成这一步之前,我们可以使用vivado的仿真工具先验证一下我们的猜想和代码是否正确。这一块我会在下一个博客中写出。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy