微波EDA网,见证研发工程师的成长!
首页 > 硬件设计 > FPGA和CPLD > 小梅哥和你一起深入学习FPGA之串口调试(一)(上)

小梅哥和你一起深入学习FPGA之串口调试(一)(上)

时间:01-26 来源:互联网 点击:
大家好,这几天在各个论坛上,经常就有人在向我咨询基于FPGA的串口通信代码,大部分都是在网上下载一个现成的代码,但是在使用中就遇到了各种问题,于是就发到了论坛上来求助。在阅读了他们的代码之后,我发现几乎出自同一个版本(目前确定为特权同学的基于EPM240入门实验的代码)。他们在调试这个代码的时候,经常存在这样几个问题:1、部分人对该串口通讯模块完全不理解,对每句话,甚至每个模块的功能都不理解;2、部分人采用最原始的画波形的方式来对该模块进行仿真,结果无法得到仿真结果;3、部分人不会使用modelsim对该设计进行仿真;4、绝大部分人不会编写testbench;5、下板测试无法进行正确的字符串收发。在公司内部,我将这种现象和几位老师交流之后,夏宇闻老师建议我专门针对该代码写一个由原理到代码,由仿真到板级的调试笔记。争取用最通俗,也是最笨的办法,手把手的教会大家来调试这个代码。

本调试笔记主要由五个部分组成:原始代码分析;原始代码验证;对原始代码进行修改;对修改后的代码进行验证;对修改后的设计进行板级验证。每个部分,小梅哥都会用图文结合的方式,教大家一步一步的来进行。
原始代码分析

该代码来自小梅哥最崇拜的大神,特权同学。当时小梅哥也是看着特权同学的书和视频教程一步一步走过来的。特权同学的代码实现了单字节的收发测试,没有对连续字节的收发进行测试。特权同学当时也说过,这个只是一个简单的实验,离实际工业应用还有一定的距离。考虑到论坛上很多小伙伴都希望能够实现连续字节的收发功能,因此小梅哥就在特权同学的代码上进行了修改。修改后的代码,输入时钟可以在一定范围内选择任意频率,目前已经支持5种波特率选择(9600、19200、38400、57600、115200),实际小梅哥还做过更高波特率的测试,目前实测在115200波特率的速率下可以实现超过9999999次连续无间断的收发。这里,小梅哥首先将特权同学设计架构在这里列出来,以给读者一个直观的印象。
  


由上图可知,特权同学的UART串口设计主要包含了四个模块:串口发送模块(my_uart_tx)、串口接收模块(my_uart_rx)、串口接收波特率发生器(speed_rx)和串口发送波特率发生器(speed_tx),其中,串口发送波特率发生器主要用来产生串口发送模块发送数据时所需的波特率时钟,串口接收波特率发生器主要用来产生串口接收模块接收数据时的波特率时钟,串口发送模块主要负责在指定波特率的速率下将待发送字节发送出去,串口接收模块则主要负责接收来自其他设备发送过来的串口数据。
my_uart_top模块即串口收发顶层模块实现了各个模块间的信号连接功能,通过该顶层模块的连接,实现了将串口接收到的数据再发送出去的功能,即我们测试串口通信最常用的一种方式——回环测试。特权同学该实验的各个端口和内部信号的意义如表1所示:



该实验的内容为,串口接收模块在检测到发送设备发送过来的数据起始位时,接收中断信号置1,该信号则作为启动串口接收波特率发送器的控制信号,然后在每个波特率时钟上升沿到来时读取串口接收端口(rs232_rx)上的数据。一帧(字节)数据接收完成后,接收中断信号拉低,停止波特率发生器工作,接收完成,系统进入等待状态,等待下一次的数据到来。

同时,接收中断信号的下降沿也作为串口发送模块的发送使能信号,因为一旦接收中断信号的下降沿出现,就表明接收模块完成了一次数据的接收,此时,就开始使能串口发送波特率发生器,串口发送模块则在波特率时钟的上升沿到来时依次将接收模块接收到的数据的每一位(发送模块自动添加起始位和停止位)依次发送出去,当数据发送完成之后,停止串口发送波特率发生器的使能,模块进入等待状态,等待下一次接收中断信号下降沿的到来。

这里,我们首先对该设计的波特率发生器模块进行分析。该模块相对简单,代码如下所示:

以下是代码片段:

1 module speed_select (
2 clk, rst_n ,
3 bps_start , clk_bps
4 );
5
6 input clk; // 50MHz
7 input rst_n ; //
8 input bps_start ; //
9 output clk_bps ; // clk_bps
10
11 /*
12 parameter bps9600 = 5207, // 9600bps
13 bps19200 = 2603, // 19200bps
14 bps38400 = 1301, // 38400bps
15 bps57600 = 867, // 57600bps
16 bps115200 = 433; // 115200bps
17
18 parameter bps9600_2 = 2603,
19 bps19200_2 = 1301,
20 bps38400_2 = 650,
21 bps57600_2 = 433,
22 bps115200_2 = 216;
23 */
24
25 //
26 `define BPS_PARA 5207 // 9600
27 `define BPS_PARA_2 2603 // 9600
28
29 reg[ 12 : 0] cnt ; //
30 reg clk_bps_r ; //
31
32 //---------------------------------------------------------
33 reg[ 2 : 0] uart_ctrl ; // uart
34 //---------------------------------------------------------
35
36 always @ ( posedge clk or negedge rst_n )
37 if(! rst_n ) cnt
38 else if(( cnt == `BPS_PARA ) || ! bps_start ) cnt
39 else cnt
40
41 always @ ( posedge clk or negedge rst_n )
42 if(! rst_n ) clk_bps_r
43 else if( cnt == `BPS_PARA_2 ) clk_bps_r
//clk_bps_r ,
44 else clk_bps_r
45
46 assign clk_bps = clk_bps_r ;
47
48 endmodule

该代码的12-22行用注释的方式告诉了我们,获得不同波特率时波特率分频计数器的值应该为多少,以及波特率计数器计数到一半时的值为多少(该值作为对信号的采样点,因为一般情况下,一位数据,在该位数据保持时间的中间时刻是最稳定的)。26行和27行定义的两个参数BPS_PARA和BPS_PARA_2分别就是波特率分频计数器的最大值和中间值。实际使用时,只需要根据你所需要的波特率,更改这两个参数的值即可 。例如,选择波特率为9600bps时,设定BPS_PARA=5207,BPS_PARA_2=2603。关于这个值的计算,这里暂时不提,后文会有交代。

36行至39行为波特率分频计数器的计数进程,即波特率发生模块没有被使能(! bps_start)或者计数器计数到BPS_PARA后则将计数器清零,其它情况下则每来一个时钟上升沿计数器自加1。通过此进程,则可得到相对精准的波特率定时。

41行至44行为数据采样时刻的生成,上面提到过“一般情况下,一位数据,在该位数据保持时间的中间时刻是最稳定的”,因此这里我们在计数器计数到一半时,产生一个时钟周期的高脉冲,此脉冲作为采样数据的使能信号。
               
以上为波特率发生器的代码及分析,波特率发生模块在例化时被分别例化为串口发送波特率发生器和串口接收波特率发生器。接下来我们再来分析串口接收模块的代码。

1 module my_uart_rx (
2 clk, rst_n ,
3 rs232_rx , rx_data , rx_int ,
4 clk_bps , bps_start
5 );
6
7 input clk; // 50MHz
8 input rst_n ; //
9 input rs232_rx ; // RS232
10 input clk_bps ; // clk_bps
11 output bps_start ; //
12 output [ 7: 0] rx_data ; //
13 output rx_int ; // ,
14
15 //---------------------------------------------------------
16 reg rs232_rx0 , rs232_rx1 , rs232_rx2 , rs232_rx3 ; //
17 wire neg_rs232_rx ; //
18
19 always @ ( posedge clk or negedge rst_n ) begin
20 if(! rst_n ) begin
21 rs232_rx0
22 rs232_rx1
23 rs232_rx2
24 rs232_rx3
25 end
26 else begin
27 rs232_rx0
28 rs232_rx1
29 rs232_rx2
30 rs232_rx3
31 end
32 end
33 //
34 //
35 // 40ns
36 assign neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0; // neg_rs232_rx
37
38 //---------------------------------------------------------
39 reg bps_start_r ;
40 reg[ 3: 0] num; //
41 reg rx_int ; // ,
42
43 always @ ( posedge clk or negedge rst_n )
44 if(! rst_n ) begin
45 bps_start_r
46 rx_int
47 end
48 else if( neg_rs232_rx ) begin
// rs232_rx
49 bps_start_r
50 rx_int
51 end
52 else if( num==4'd12 ) begin //
53 bps_start_r
54 rx_int
55 end
56
57 assign bps_start = bps_start_r ;
58
59 //---------------------------------------------------------
60 reg[ 7 : 0] rx_data_r ; //
61 //---------------------------------------------------------
62
63 reg[ 7 : 0] rx_temp_data ; //
64
65 always @ ( posedge clk or negedge rst_n )
66 if(! rst_n ) begin
67 rx_temp_data
68 num
69 rx_data_r
70 end
71 else if( rx_int ) begin //
72 if( clk_bps ) begin
// , 8bit 1 2
73 num
74 case ( num)
75 4'd1:rx_temp_data[0]
76 4'd2:rx_temp_data [1]
77 4'd3:rx_temp_data [2]
78 4'd4:rx_temp_data [3]
79 4'd5:rx_temp_data [4]
80 4'd6:rx_temp_data [5]
81 4'd7:rx_temp_data [6]
82 4'd8:rx_temp_data [7]
83 default : ;
84 endcase
85 end
86 else if( num == 4'd12 ) begin
// 1+8+1(2)=11bit
87 num
88 rx_data_r
89 end
90 end
91
92 assign rx_data = rx_data_r ;
93
94 endmodule

第19行到第36行为起始位检测部分,19到32行,实现了对rs232_rx端口上电平的连续四个时钟周期的寄存,第36行则对这连续4个时钟上升沿时的rs232_rx端口电平进行逻辑操作,得出rs232_rx端口信号下降沿的到来。neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0,即后两次寄存的状态为低电平而前两次寄存的装填为高电平,则表明该端口上的信号发生了1->0的跳变,即有下降沿出现。neg_rs232_rx信号会产生一个周期的高脉冲。

第43行至第55行则根据neg_rs232_rx和num计数值来控制串口接收波特率发生器的工作和接收中断信号。第65行至第90行则采用线性序列机的设计方式,进行一个字节的数据的接收。

以上为对串口接收模块的一个简单分析,接下来,再进行串口发送模块的分析。

1 module my_uart_tx (
2 clk, rst_n ,
3 rx_data , rx_int , rs232_tx ,
4 clk_bps , bps_start
5 );
6
7 input clk; // 50MHz
8 input rst_n ; //
9 input clk_bps ; // clk_bps_r ,
10 input [ 7 : 0] rx_data ; //
11 input rx_int ;
12 output rs232_tx ; // RS232
13 output bps_start ; //
14
15 //---------------------------------------------------------
16 reg rx_int0 , rx_int1 , rx_int2 ; //rx_int
17 wire neg_rx_int ; // rx_int
18
19 always @ ( posedge clk or negedge rst_n ) begin
20 if(! rst_n ) begin
21 rx_int0
22 rx_int1
23 rx_int2
24 end
25 else begin
26 rx_int0
27 rx_int1
28 rx_int2
29 end
30 end
31
32 assign neg_rx_int = ~rx_int1 & rx_int2 ; // neg_rx_int
33
34 //---------------------------------------------------------
35 reg[ 7 : 0] tx_data ; //
36 //---------------------------------------------------------
37 reg bps_start_r ;
38 reg tx_en ; //
39 reg[ 3: 0] num;
40
41 always @ ( posedge clk or negedge rst_n ) begin
42 if(! rst_n ) begin
43 bps_start_r
44 tx_en
45 tx_data
46 end
47 else if( neg_rx_int ) begin //
48 bps_start_r
49 tx_data
50 tx_en
51 end
52 else if( num==4'd11 ) begin //
53 bps_start_r
54 tx_en
55 end
56 end
57
58 assign bps_start = bps_start_r ;
59
60 //---------------------------------------------------------
61 reg rs232_tx_r ;
62
63 always @ ( posedge clk or negedge rst_n ) begin
64 if(! rst_n ) begin
65 num
66 rs232_tx_r
67 end
68 else if( tx_en ) begin
69 if( clk_bps ) begin
70 num
71 case ( num)
72 4'd0 : rs232_tx_r
73 4'd1 : rs232_tx_r
74 4'd2 : rs232_tx_r
75 4'd3 : rs232_tx_r
76 4'd4 : rs232_tx_r
77 4'd5 : rs232_tx_r
78 4'd6 : rs232_tx_r
79 4'd7 : rs232_tx_r
80 4'd8 : rs232_tx_r
81 4'd9 : rs232_tx_r
82 default : rs232_tx_r
83 endcase
84 end
85 else if( num==4'd11 ) num
86 end
87 end
88
89 assign rs232_tx = rs232_tx_r ;
90
91 endmodule

代码19行到30行对串口接收模块的接收中断信号进行了3次寄存,第32行则通过对连续两次寄存结果的判断,来检测接收中断信号rx_int的下降沿。如果有下降沿到来,neg_rx_int信号则会产生一个时钟周期的高脉冲信号,第47行则通过对该信号的状态判断,来确定是否启动发送波特率发生器模块。如果检测到了该高脉冲,则使能串口发送(tx_en FPGA爱好者学习。
               
原始代码验证

前面,通对设计代码的一个简单分析,弄清楚了特权同学设计代码的基本架构和思路。那么看过特权同学教学视频的都知道,该代码能够实现一个字节的数据收发测试。那么这里,小梅哥就先对该设计进行一个仿真,通过仿真来分析此设计的性能。
仿真的思路很简单,就是通过模拟串口发送过程,给该设计模块发送数据,由前面分析可知,该设计模块接收到数据后,会立即将数据发送出去,因此我们还需要对串口发送出来的数据进行分析,这里,熟悉Uart协议的,我们可以直接观察发送波形。当然,为了更加直观,我们也可以设计一个模拟串口接收数据的仿真模型,通过该模块来读取串口发送出来的数据。考虑到看这篇文章的大多是初学者,为了让大家能够更好的查看我们的仿真结果,同时教大家进行仿真模型的设计,小梅哥还是自己编写了一个虚拟的串口仿真模型。验证时,只需要将该仿真模型挂接到串口模块上,则该模型便能够自动的给串口模块发送数据,同时接收串口发送过来的数据。并会实时的将发送的数据和接收的数据打印出来,实际在观察仿真结果时,我们便只需要观看打印的结果就可以了。该串口仿真模型的代码如下所示:

以下是代码片段:

1 `timescale 1ns/1ps
2
3 module Uart_module ( uart_rx , uart_tx , send_state );
4
5 input uart_rx ;
6 output reg uart_tx ;
7 output reg send_state ;
8
9 reg Clk;
10 reg Rst_n ;
11
12 wire Mid_Flag_send ;
13 wire Mid_Flag_Receive ;
14
15 reg Receive_Baud_Start ;
16 reg [ 7 : 0] rx_data ;
17
18 initial Clk = 1;
19 always #10 Clk = ~Clk;
20
21 speed_select speed_select_Send (
22 . clk( Clk),
23 . rst_n ( Rst_n ),
24 . bps_start ( 1'b1 ),
25 . clk_bps ( Mid_Flag_send )
26 );
27
28 speed_select speed_select_receive (
29 . clk( Clk),
30 . rst_n ( Rst_n ),
31 . bps_start ( Receive_Baud_Start ),
32 . clk_bps ( Mid_Flag_Receive )
33 );
34
35 initial begin
36 Rst_n = 0;
37 uart_tx = 1;
38 send_state = 0;
39 #300 Rst_n = 1;
40
41 $display ( "Set Baud As 9600bps" );
42 #200 ; Uart_Send ( 8'hb6 );
43 #20 ; Uart_Send ( 8'he7 );
44 #80 ; Uart_Send ( 8'hf0 );
45 #500 ; Uart_Send ( 8'h02 );
46 #300 ; Uart_Send ( 8'hb4 );
47 #40 ; Uart_Send ( 8'he5 );
48 #90 ; Uart_Send ( 8'hb0 );
49 #1000 ; Uart_Send ( 8'h32 );
50 #2000000 ;
51 $stop ;
52 end
53
54 task Uart_Send ;
55 input [ 7: 0] Data ;
56 begin
57 send_state = 1;
58 @( posedge Mid_Flag_send) #0.1 uart_tx = 0;
59 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [0];
60 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [1];
61 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [2];
62 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [3];
63 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [4];
64 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [5];
65 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [6];
66 @( posedge Mid_Flag_send) #0.1 uart_tx = Data [7];
67 @( posedge Mid_Flag_send) #0.1 uart_tx = 1;
68 $display ( "Uart_Send Data = %0h" , Data );
69 send_state = 0;
70 end
71 endtask
72
73 initial begin
74 forever begin
75 @( negedge uart_rx )
76 begin
77 Receive_Baud_Start = 1;
78 @( posedge Mid_Flag_Receive);
79 @( posedge Mid_Flag_Receive) rx_data [0] = uart_rx ;
80 @( posedge Mid_Flag_Receive) rx_data [1] = uart_rx ;
81 @( posedge Mid_Flag_Receive) rx_data [2] = uart_rx ;
82 @( posedge Mid_Flag_Receive) rx_data [3] = uart_rx ;
83 @( posedge Mid_Flag_Receive) rx_data [4] = uart_rx ;
84 @( posedge Mid_Flag_Receive) rx_data [5] = uart_rx ;
85 @( posedge Mid_Flag_Receive) rx_data [6] = uart_rx ;
86 @( posedge Mid_Flag_Receive) rx_data [7] = uart_rx ;
87 @( posedge Mid_Flag_Receive) Receive_Baud_Start = 0;
88 $display ( "Uart_receive Data = %0h" , rx_data );
89 end
90 end
91 end
92
93 endmodule
94

因为在将代码复制到word的过程中,会有一定的格式兼容问题,所以文中部分格式不是太规范,望各位理解,另外,完整的代码,小梅哥也以pdf的形式提供了,感兴趣的朋友可以下载学习。

本仿真模型的第一句话“`timescale 1ns/1ps”为仿真精度及时间的说明,定义时间精度为1ps,时间单位为1ns,那么我们在代码编写的过程中,如果写成“#200”则表示延时200ns,因为时间精度为1ps,因此我们还可以进一步提高延时精度,如“#200.1”表示延时200.1ns。一般的测试文件(testbench)中,这句话作为第一句话,必写,当然,时间精度和单位我们可以根据自己的需求更改,如写成“`timescale 1us/1ns”或者“`timescale 1ns/1ns”等都是可以的。

该模块作为一个仿真模型,就是虚拟了一个串口收发仪器,既然是一个串口收发仪器,则必然有串口发送端口和串口接收端口,因此在模块名后面定义了三个端口。这与一般的testbench不同,一般的testbench作为仿真时的顶层,不需要端口,因此模块名后就直接以“;”结束。该模块的三个端口“uart_rx , uart_tx , send_state”分别为串口接收端口、串口发送端口、串口发送状态信号。串口收发端口不用说,大家也已经知道了,串口发送状态信号主要作为指示信号,指示当前仿真模型正在进行数据的发送过程。

第9行至第16行为测试文件中信号的定义,以前我们总是理解说这些信号就是待测试模块的端口,需要在测试文件中定义。那么这里小梅哥更喜欢换一种方式来理解:我们自己的设计,本设计中即特权的串口模块,是一个功能未知的黑盒子,这个黑盒子有一些信号线引出,有的信号线是作为输入的,即需要外部输入一定的信号作为激励,而有的信号线是作为输出的,能够输出一些数据,当然还有一些信号线是既能够作为输入,又能够作为输出的,即三态。我们要想知道这个黑盒子的功能,就需要给这个黑盒子的输入信号线接上信号源,通过给这些输入信号线一定的激励,观察其输出端口上的响应,从而获知该黑盒子的功能。那么在这里,对于待测试模块的输入端口,我们就接上信号发生器,对于输出端口,我们就接上示波器或者逻辑分析仪,这样,我们就能够通过信号发生器给输入端口产生一定的激励,然后通过示波器观察输出端口的输出了。即如下图所示:
  


那么,我们的testbench主要实现信号发生器的功能,既然是信号发生器那么就一定有数据信号输出,这个数据信号输出就可以连接到我们的待测模块上。待测试模块的输出端口,连接到我们的示波器或者逻辑分析仪的探头上,这样就实现了一个完整的测试系统,那么我们信号发生器的信号源,可能命名叫做,a,b,c,d,e……. 而我们示波器的探头则命名为探头1,探头2……接下来就好理解了,在testbench,我们将信号发生器的输出信号定义为reg型,而示波器的探头定义为wire型。我们信号发生器的输出信号线和示波器的探头线都可以任意命名,实际使用时一一对应连接到待测试模块的端口上,也可以就直接与待测模块的各个端口名保持一致。本设计中,小梅哥让testbench中的信号与待测模块的端口保持一致。

第18行和19行为产生50MHz时钟的语句。

因为本仿真模型实质上就是一个串口收发模块,因此也需要有收发波特率发生器,这里小梅哥为了省事,直接调用了特权同学的波特率发生器模块,来作为我仿真模型的波特率发生器。因为该波特率发生器本身也属于待测试部分,小梅哥之所以敢放心的调用,是因为事先我已经通过仿真,确定了该波特率发生器功能的正确性。第21行至33行为分别例化得到发送波特率发生器和接收波特率发生器的代码。

第54行至71行为发送一个完整字节的数据(自动添加起始位和停止位)的代码,该部分写成任务的形式,方便调用。当我们需要发送一个字节的数据时,例如,发送8'hb6,只需要写“Uart_Send ( 8'hb6 )”即可,该任务便将自动执行,将数据发送出去。在一个字节的数据发送完成后,同时使用系统任务$display来打印当前发送的数据是多少,以方便我们直观的观察仿真运行过程。至于$display这个系统任务中各个部分的含义,请读者自行阅读verilog的语法书。代码的42至49行便是调用此任务进行了多次数据的发送。

73行至91行为模拟串口接收部分,通过对串口模块发送出来的数据进行接收,并将接收到的数据用$display函数打印出来。我们只需要阅读发送数据和接收数据后打印出来的信息,即可判断通信是否成功,待测模块功能是否正常。

这里需要注意的是,打印出来的接收数据和发送数据是针对仿真模型来说的,send data是仿真模型发送出去的数据,对应待测模块应该接收到的数据。receive data则是仿真模型接收到的数据,对应待测模块发送的数据。

我们所编写的测试文件,一定要是可控的,即在所有事务完成后,将仿真停下来,否则,仿真会一直进行下去,导致出现大量冗余波形,影响我们对仿真结果的分析。因此在第51行,当所有测试已经完成后,使用系统任务$stop将仿真停下来。

以上对小梅哥写的串口仿真模型进行了介绍,在实际使用中,只需要将该模型与待测模块按照如下图所示的方式连接起来即可。
  


这里,小梅哥使用一个testbench文件作为顶层,将这两个部分连接起来,同时产生my_uart_top工作所需的时钟和复位信号。该文件详细代码如下:

1 `timescale 1ns /1ns
2
3 module Uart_tb ;
4
5 reg Clk;
6 reg Rst_n ;
7
8 wire uart_rx ;
9 wire uart_tx ;
10 wire send_state ;
11
12 my_uart_top u1 (
13 . clk( Clk),
14 . rst_n ( Rst_n ),
15 . rs232_rx ( uart_tx ),
16 . rs232_tx ( uart_rx )
17 );
18
19 Uart_module u2 (
20 . uart_rx ( uart_rx ),
21 . uart_tx ( uart_tx ),
22 . send_state ( send_state )
23 );
24
25 initial begin
26 Clk = 1;
27 Rst_n = 0;
28 #200 ;
29 Rst_n = 1;
30 end
31
32 always #10 Clk = ~Clk;
33
34 endmodule
35

该代码实在简单,只是实现了一个启动时的初始化和50MHz时钟的产生,因此小梅哥就不做任何分析了。

Copyright © 2017-2020 微波EDA网 版权所有

网站地图

Top